SampleChart.tsx 7.2 KB
Newer Older
1
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
P
Peter Pan 已提交
2
import {ellipsis, em, primaryColor, rem, size, textLightColor, textLighterColor} from '~/utils/style';
3

4
import ChartToolbox from '~/components/ChartToolbox';
P
Peter Pan 已提交
5
import GridLoader from 'react-spinners/GridLoader';
P
Peter Pan 已提交
6 7
import {Run} from '~/types';
import StepSlider from '~/components/SamplePage/StepSlider';
8
import {formatTime} from '~/utils';
9 10 11 12 13
import isEmpty from 'lodash/isEmpty';
import queryString from 'query-string';
import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
14 15

const Wrapper = styled.div`
16
    height: 100%;
17
    padding: ${em(20)};
18
    padding-bottom: 0;
19 20 21 22 23 24 25 26 27 28 29
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: stretch;

    > * {
        flex-grow: 0;
        flex-shrink: 0;
    }
`;

P
Peter Pan 已提交
30
const Title = styled.div<{color: string}>`
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: ${em(20)};

    > h4 {
        font-size: ${em(16)};
        font-weight: 700;
        flex-shrink: 1;
        flex-grow: 1;
        padding: 0;
        margin: 0;
        ${ellipsis()}
    }

    > span {
        font-size: ${em(14)};
        flex-shrink: 0;
        flex-grow: 0;
        color: ${textLightColor};
51 52
        ${ellipsis()}
        max-width: 50%;
P
Peter Pan 已提交
53 54 55 56 57 58 59 60 61 62

        &::before {
            content: '';
            display: inline-block;
            ${size(rem(5), rem(17))}
            margin-right: ${rem(8)};
            border-radius: ${rem(2.5)};
            vertical-align: middle;
            background-color: ${props => props.color};
        }
63 64 65
    }
`;

66
const Container = styled.div<{brightness?: number; contrast?: number; fit?: boolean}>`
67 68
    flex-grow: 1;
    flex-shrink: 1;
69
    margin: ${em(20)} 0;
70 71 72 73 74 75
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
`;

P
Peter Pan 已提交
76
const Footer = styled.div`
P
Peter Pan 已提交
77
    margin-bottom: ${rem(18)};
P
Peter Pan 已提交
78 79 80
    display: flex;
    align-items: center;
    justify-content: space-between;
P
Peter Pan 已提交
81 82
`;

P
Peter Pan 已提交
83 84 85 86 87 88 89 90 91 92
const FooterInfo = styled.div`
    color: ${textLighterColor};
    font-size: ${rem(12)};

    > * {
        display: inline-block;
        margin-left: ${rem(10)};
    }
`;

P
Peter Pan 已提交
93
type SampleData = {
94 95 96 97
    step: number;
    wallTime: number;
};

P
Peter Pan 已提交
98 99
export type SampleChartBaseProps = {
    run: Run;
100 101 102 103
    tag: string;
    running?: boolean;
};

P
Peter Pan 已提交
104 105 106 107 108 109 110 111 112 113
type SampleChartRef = {
    save: (filename: string) => void;
};

type SampleChartProps = {
    type: 'image' | 'audio';
    cache: number;
    footer?: JSX.Element;
    content: (ref: React.RefObject<SampleChartRef>, src: string) => JSX.Element;
} & SampleChartBaseProps;
114

P
Peter Pan 已提交
115 116
const getUrl = (type: string, index: number, run: string, tag: string, wallTime: number): string =>
    `/${type}/${type}?${queryString.stringify({index, ts: wallTime, run, tag})}`;
117

P
Peter Pan 已提交
118 119
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, running, type, cache, footer, content}) => {
    const {t, i18n} = useTranslation(['sample', 'common']);
120

P
Peter Pan 已提交
121
    const sampleRef = useRef<SampleChartRef>(null);
122

P
Peter Pan 已提交
123 124
    const {data, error, loading} = useRunningRequest<SampleData[]>(
        `/${type}/list?${queryString.stringify({run: run.label, tag})}`,
125
        !!running
126
    );
127

128 129
    const steps = useMemo(() => data?.map(item => item.step) ?? [], [data]);

130
    const [step, setStep] = useState(0);
131 132 133 134 135 136 137 138 139 140 141
    const [src, setSrc] = useState<string>();

    const cached = useRef<Record<number, {src: string; timer: NodeJS.Timeout}>>({});
    const timer = useRef<NodeJS.Timeout | null>(null);

    // clear cache if tag or run changed
    useEffect(() => {
        Object.values(cached.current).forEach(({timer}) => clearTimeout(timer));
        cached.current = {};
    }, [tag, run]);

142 143
    const wallTime = useMemo(() => data?.[step].wallTime ?? 0, [data, step]);

P
Peter Pan 已提交
144
    const cacheSrc = useCallback(() => {
145 146 147
        if (!data) {
            return;
        }
P
Peter Pan 已提交
148
        const url = getUrl(type, step, run.label, tag, wallTime);
149
        cached.current[step] = {
P
Peter Pan 已提交
150
            src: url,
151 152
            timer: setTimeout(() => {
                ((s: number) => delete cached.current[s])(step);
P
Peter Pan 已提交
153
            }, cache)
154
        };
P
Peter Pan 已提交
155 156
        setSrc(url);
    }, [type, step, run.label, tag, wallTime, data, cache]);
157

P
Peter Pan 已提交
158 159 160
    const download = useCallback(() => {
        sampleRef.current?.save(`${run.label}-${tag}-${steps[step]}-${wallTime.toString().replace(/\./, '_')}`);
    }, [run.label, tag, steps, step, wallTime]);
161 162 163 164 165 166 167

    useEffect(() => {
        if (cached.current[step]) {
            // cached, return immediately
            setSrc(cached.current[step].src);
        } else if (isEmpty(cached.current)) {
            // first load, return immediately
P
Peter Pan 已提交
168
            cacheSrc();
169
        } else {
P
Peter Pan 已提交
170
            timer.current = setTimeout(cacheSrc, 500);
171 172 173 174
            return () => {
                timer.current && clearTimeout(timer.current);
            };
        }
P
Peter Pan 已提交
175
    }, [step, cacheSrc]);
176

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    const [viewed, setViewed] = useState<boolean>(false);

    const container = useRef<HTMLDivElement>(null);
    const observer = useRef(
        process.browser
            ? new IntersectionObserver(entries => {
                  if (entries[0].intersectionRatio > 0) {
                      setViewed(true);
                      observer.current?.disconnect();
                  }
              })
            : null
    );

    useEffect(() => {
        const o = observer.current;
        if (process.browser && container.current && o) {
            o.observe(container.current);
            return () => o.disconnect();
        }
    }, []);

199
    const Content = useMemo(() => {
200
        // show loading when deferring
201
        if (loading || !cached.current[step] || !viewed) {
202 203
            return <GridLoader color={primaryColor} size="10px" />;
        }
204
        if (!data && error) {
205
            return <span>{t('common:error')}</span>;
206 207
        }
        if (isEmpty(data)) {
208
            return <span>{t('common:empty')}</span>;
209
        }
P
Peter Pan 已提交
210 211 212 213 214
        if (src) {
            return content(sampleRef, src);
        }
        return null;
    }, [viewed, loading, error, data, step, src, t, content]);
215

216 217
    return (
        <Wrapper>
P
Peter Pan 已提交
218
            <Title color={run.colors[0]}>
219
                <h4>{tag}</h4>
P
Peter Pan 已提交
220
                <span>{run.label}</span>
221
            </Title>
P
Peter Pan 已提交
222
            <StepSlider value={step} steps={steps} onChange={setStep} onChangeComplete={cacheSrc}>
P
Peter Pan 已提交
223
                {formatTime(wallTime, i18n.language)}
224
            </StepSlider>
P
Peter Pan 已提交
225 226 227 228 229 230 231 232 233 234 235
            <Container ref={container}>{Content}</Container>
            <Footer>
                <ChartToolbox
                    items={[
                        {
                            icon: 'download',
                            tooltip: t(`sample:download-${type}`),
                            onClick: download
                        }
                    ]}
                />
P
Peter Pan 已提交
236 237 238 239 240 241 242 243
                <FooterInfo>
                    <span>
                        {t('sample:sample')}
                        {t('common:colon')}
                        {data?.length ? `${step + 1}/${data.length}` : '--/--'}
                    </span>
                    {footer}
                </FooterInfo>
P
Peter Pan 已提交
244
            </Footer>
245 246 247 248 249
        </Wrapper>
    );
};

export default SampleChart;