aboutsummaryrefslogblamecommitdiffstats
path: root/packages/instant/src/components/simulated_progress_bar.tsx
blob: 1fd76ead7e9b67abb9826ddfdcf8ff831225a500 (plain) (tree)
























                                                                                       


                            


























                                                                                                              
                                       











                                                                                                              
                                                               






                                                                     











                                                                                                                    



                                         
                               


















                                                                                                   









                                                                                                




                                                                           

                                                                                                                 
                                                                          





























                                                                                                 
import * as _ from 'lodash';
import * as React from 'react';
import { Dispatch } from 'redux';

import { PROGRESS_STALL_AT_PERCENTAGE, PROGRESS_TICK_INTERVAL_MS } from '../constants';
import { Action, actions } from '../redux/actions';

import { ColorOption } from '../style/theme';

import { Container } from './ui/container';
import { Flex } from './ui/flex';
import { Text } from './ui/text';

const TICKS_PER_SECOND = 1000 / PROGRESS_TICK_INTERVAL_MS;

const curTimeUnix = () => {
    return new Date().getTime();
};

export interface SimulatedProgressBarProps {
    startTimeUnix: number;
    expectedEndTimeUnix: number;
    ended: boolean;
}
enum TickingRunState {
    None = 'None',
    Running = 'Running',
    Finishing = 'Finishing',
}
interface TickingNoneStatus {
    runState: TickingRunState.None;
}
interface TickingRunningStatus {
    runState: TickingRunState.Running;
}
interface TickingFinishingStatus {
    runState: TickingRunState.Finishing;
    increasePercentageEveryTick: number;
}
type TickingStatus = TickingNoneStatus | TickingRunningStatus | TickingFinishingStatus;

export interface SimulatedProgressState {
    percentageDone: number;
    intervalId?: number;
    tickingStatus: TickingStatus;
}
export class SimulatedProgressBar extends React.Component<SimulatedProgressBarProps, SimulatedProgressState> {
    public constructor(props: SimulatedProgressBarProps) {
        super(props);

        // TODO: look into using assert library here?
        if (props.expectedEndTimeUnix <= props.startTimeUnix) {
            throw new Error('End time before start time');
        }

        // TODO: use getFreshState here
        const intervalId = window.setInterval(this._tick.bind(this), PROGRESS_TICK_INTERVAL_MS);
        this.state = {
            percentageDone: 0,
            intervalId,
            tickingStatus: { runState: TickingRunState.Running },
        };
    }

    public componentDidUpdate(prevProps: SimulatedProgressBarProps, prevState: SimulatedProgressState): void {
        const percentLeft = 100 - this.state.percentageDone;
        const increasePercentageEveryTick = percentLeft / TICKS_PER_SECOND;

        // if we just switched to ending, having animate to end
        if (prevProps.ended === false && this.props.ended === true) {
            this.setState({
                tickingStatus: {
                    runState: TickingRunState.Finishing,
                    increasePercentageEveryTick,
                },
            });
            return;
        }

        // later TODO: the new state could be for the wrong order, attach to order state or add concurrency checking

        // if anything else changes, reset internal state
        if (
            prevProps.startTimeUnix !== this.props.startTimeUnix ||
            prevProps.expectedEndTimeUnix !== this.props.expectedEndTimeUnix ||
            prevProps.ended !== this.props.ended
        ) {
            this.setState(this._getFreshState());
        }
    }

    public componentWillUnmount(): void {
        console.log('unmount');
        this._clearTimer();
    }

    public render(): React.ReactNode {
        // TODO: Consider moving to seperate component
        return (
            <Container padding="20px 20px 0px 20px" width="100%">
                <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px">
                    <Container
                        width={`${this.state.percentageDone}%`}
                        backgroundColor={ColorOption.primaryColor}
                        borderRadius="6px"
                        height="6px"
                    />
                </Container>
            </Container>
        );
    }

    private _getFreshState(): SimulatedProgressState {
        this._clearTimer();
        const intervalId = window.setInterval(this._tick.bind(this), PROGRESS_TICK_INTERVAL_MS);
        return {
            percentageDone: 0,
            intervalId,
            tickingStatus: { runState: TickingRunState.Running },
        };
    }

    private _tick(): void {
        const rawPercentageDone =
            this.state.tickingStatus.runState === TickingRunState.Finishing
                ? this._getNewPercentageFinishing(this.state.tickingStatus)
                : this._getNewPercentageNormal();
        const maxPercentage =
            this.state.tickingStatus.runState === TickingRunState.Finishing ? 100 : PROGRESS_STALL_AT_PERCENTAGE;
        const percentageDone = Math.min(rawPercentageDone, maxPercentage);

        this.setState({
            percentageDone,
        });

        if (percentageDone >= 100) {
            this._clearTimer();
        }
    }

    private _clearTimer(): void {
        if (this.state.intervalId) {
            window.clearTimeout(this.state.intervalId);
        }
    }

    // TODO: consider not taking in a parameter here, might be confusing
    private _getNewPercentageFinishing(tickingStatus: TickingFinishingStatus): number {
        return this.state.percentageDone + tickingStatus.increasePercentageEveryTick;
    }

    private _getNewPercentageNormal(): number {
        const elapsedTimeMs = curTimeUnix() - this.props.startTimeUnix;
        const safeElapsedTimeMs = Math.max(elapsedTimeMs, 1);

        const expectedAmountOfTimeMs = this.props.expectedEndTimeUnix - this.props.startTimeUnix;
        const percentageDone = safeElapsedTimeMs / expectedAmountOfTimeMs * 100;
        return percentageDone;
    }
}