提交 33dbf151 编写于 作者: P Peter Pan

plenty of improvment and bug fix

上级 265bc79a
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
English | [简体中文](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/README_cn.md) English | [简体中文](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/README_cn.md)
**🚧UNDER CONSTRUCTION🚧** **🚧UNDER DEVELOPMENT🚧**
**🚧SOME FEATURE MAY NOT WORK PROPERLY🚧** **🚧SOME FEATURE MAY NOT WORK PROPERLY🚧**
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
[English](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/README.md) | 简体中文 [English](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/README.md) | 简体中文
**🚧仍在建设中🚧** **🚧开发中🚧**
**🚧某些功能可能不能正常工作🚧** **🚧某些功能可能不能正常工作🚧**
......
...@@ -9,6 +9,7 @@ module.exports = { ...@@ -9,6 +9,7 @@ module.exports = {
preprocess: false preprocess: false
} }
], ],
...(process.env.NODE_ENV !== 'production' ? ['babel-plugin-typescript-to-proptypes'] : []) ['emotion'],
...(process.env.NODE_ENV !== 'production' ? ['typescript-to-proptypes'] : [])
] ]
}; };
import React, {FunctionComponent, useState} from 'react'; import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {WithStyled, rem} from '~/utils/style'; import {WithStyled, rem, primaryColor} from '~/utils/style';
import BarLoader from 'react-spinners/BarLoader';
import Chart from '~/components/Chart'; import Chart from '~/components/Chart';
import Pagination from '~/components/Pagination'; import Pagination from '~/components/Pagination';
...@@ -18,14 +19,23 @@ const Wrapper = styled.div` ...@@ -18,14 +19,23 @@ const Wrapper = styled.div`
} }
`; `;
const Loading = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: ${rem(200)};
padding: ${rem(40)} 0;
`;
// TODO: add types // TODO: add types
// eslint-disable-next-line // eslint-disable-next-line
type ChartPageProps<T = any> = { type ChartPageProps<T = any> = {
items?: T[]; items?: T[];
loading?: boolean;
withChart?: (item: T) => React.ReactNode; withChart?: (item: T) => React.ReactNode;
}; };
const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, withChart, className}) => { const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, loading, withChart, className}) => {
const pageSize = 12; const pageSize = 12;
const total = Math.ceil((items?.length ?? 0) / pageSize); const total = Math.ceil((items?.length ?? 0) / pageSize);
...@@ -35,11 +45,17 @@ const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, withC ...@@ -35,11 +45,17 @@ const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, withC
return ( return (
<div className={className}> <div className={className}>
{loading ? (
<Loading>
<BarLoader color={primaryColor} width="20%" height="4px" />
</Loading>
) : (
<Wrapper> <Wrapper>
{pageItems.map((item, index) => ( {pageItems.map((item, index) => (
<Chart key={index}>{withChart?.(item)}</Chart> <Chart key={index}>{withChart?.(item)}</Chart>
))} ))}
</Wrapper> </Wrapper>
)}
<Pagination page={page} total={total} onChange={setPage} /> <Pagination page={page} total={total} onChange={setPage} />
</div> </div>
); );
......
...@@ -81,6 +81,7 @@ type CheckboxProps = { ...@@ -81,6 +81,7 @@ type CheckboxProps = {
value?: boolean; value?: boolean;
onChange?: (value: boolean) => unknown; onChange?: (value: boolean) => unknown;
size?: 'small'; size?: 'small';
title?: string;
disabled?: boolean; disabled?: boolean;
}; };
...@@ -90,6 +91,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({ ...@@ -90,6 +91,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
size, size,
disabled, disabled,
className, className,
title,
onChange onChange
}) => { }) => {
const [checked, setChecked] = useState(!!value); const [checked, setChecked] = useState(!!value);
...@@ -103,7 +105,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({ ...@@ -103,7 +105,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
}; };
return ( return (
<Wrapper disabled={disabled} className={className}> <Wrapper disabled={disabled} className={className} title={title}>
<Input onChange={onChangeInput} checked={checked} disabled={disabled} /> <Input onChange={onChangeInput} checked={checked} disabled={disabled} />
<Inner checked={checked} size={size} disabled={disabled} /> <Inner checked={checked} size={size} disabled={disabled} />
<Content disabled={disabled}>{children}</Content> <Content disabled={disabled}>{children}</Content>
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {rem, math, headerHeight, asideWidth, backgroundColor} from '~/utils/style'; import HashLoader from 'react-spinners/HashLoader';
import {rem, math, headerHeight, asideWidth, backgroundColor, primaryColor} from '~/utils/style';
const margin = rem(20); const margin = rem(20);
const padding = rem(20); const padding = rem(20);
...@@ -30,14 +31,36 @@ const Aside = styled.aside` ...@@ -30,14 +31,36 @@ const Aside = styled.aside`
overflow-y: auto; overflow-y: auto;
`; `;
const Loading = styled.div`
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
overscroll-behavior: none;
cursor: progress;
`;
type ContentProps = { type ContentProps = {
aside?: React.ReactNode; aside?: React.ReactNode;
loading?: boolean;
}; };
const Content: FunctionComponent<ContentProps> = ({children, aside}) => ( const Content: FunctionComponent<ContentProps> = ({children, aside, loading}) => (
<Section> <Section>
<Article aside={!!aside}>{children}</Article> <Article aside={!!aside}>{children}</Article>
{aside && <Aside>{aside}</Aside>} {aside && <Aside>{aside}</Aside>}
{loading && (
<Loading>
<HashLoader size="60px" color={primaryColor} />
</Loading>
)}
</Section> </Section>
); );
......
import React, {FunctionComponent, useEffect, useState, useRef} from 'react'; import React, {FunctionComponent, useLayoutEffect, useState} from 'react';
import fetch from 'isomorphic-unfetch'; import useSWR from 'swr';
import {useTranslation} from '~/utils/i18n'; import {primaryColor} from '~/utils/style';
import {blobFetcher} from '~/utils/fetch';
import GridLoader from 'react-spinners/GridLoader';
type ImageProps = { type ImageProps = {
src?: string; src?: string;
}; };
const Image: FunctionComponent<ImageProps> = ({src}) => { const Image: FunctionComponent<ImageProps> = ({src}) => {
const {t} = useTranslation('common');
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const controller = useRef(null as AbortController | null); const {data} = useSWR(src ?? null, blobFetcher);
useEffect(() => { // use useLayoutEffect hook to prevent image render after url revoked
if (process.browser) { useLayoutEffect(() => {
if (process.browser && data) {
let objectUrl: string | null = null; let objectUrl: string | null = null;
(async () => { objectUrl = URL.createObjectURL(data);
setLoading(true);
controller.current?.abort();
controller.current = new AbortController();
try {
const result = await fetch(src ?? '', {signal: controller.current.signal});
const blob = await result.blob();
objectUrl = URL.createObjectURL(blob);
setUrl(objectUrl); setUrl(objectUrl);
} catch {
// ignore abort error
} finally {
setLoading(false);
}
})();
return () => { return () => {
objectUrl && URL.revokeObjectURL(objectUrl); objectUrl && URL.revokeObjectURL(objectUrl);
}; };
} }
}, [src]); }, [data]);
return loading ? <span>{t('loading')}</span> : <img src={url} />; return !data ? <GridLoader color={primaryColor} size="10px" /> : <img src={url} />;
}; };
export default Image; export default Image;
import React, {FunctionComponent, useEffect, useCallback} from 'react'; import React, {FunctionComponent, useEffect, useCallback} from 'react';
import {EChartOption} from 'echarts'; import {EChartOption} from 'echarts';
import {WithStyled} from '~/utils/style'; import {WithStyled} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import useECharts from '~/hooks/useECharts'; import useECharts from '~/hooks/useECharts';
import * as chart from '~/utils/chart'; import * as chart from '~/utils/chart';
import {formatTime} from '~/utils/scalars';
type LineChartProps = { type LineChartProps = {
title?: string; title?: string;
...@@ -29,18 +31,21 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({ ...@@ -29,18 +31,21 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
loading, loading,
className className
}) => { }) => {
const [ref, echart] = useECharts<HTMLDivElement>({ const {i18n} = useTranslation();
loading: !!loading
const {ref, echart} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom: true
}); });
const xAxisFormatter = useCallback( const xAxisFormatter = useCallback(
(value: number) => (type === 'time' ? new Date(value).toLocaleTimeString() : value), (value: number) => (type === 'time' ? formatTime(value, i18n.language, 'LTS') : value),
[type] [type, i18n.language]
); );
useEffect(() => { useEffect(() => {
if (process.browser) { if (process.browser) {
echart.current?.setOption( echart?.current?.setOption(
{ {
color: chart.color, color: chart.color,
title: { title: {
...@@ -84,18 +89,6 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({ ...@@ -84,18 +89,6 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
} }
}, [data, title, legend, xAxis, type, xAxisFormatter, yRange, tooltip, echart]); }, [data, title, legend, xAxis, type, xAxisFormatter, yRange, tooltip, echart]);
useEffect(() => {
if (process.browser) {
setTimeout(() => {
echart.current?.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true
});
}, 0);
}
}, [echart]);
return <div className={className} ref={ref}></div>; return <div className={className} ref={ref}></div>;
}; };
......
import React, {FunctionComponent} from 'react';
import Head from 'next/head';
type PreloaderProps = {
url: string;
};
const Preloader: FunctionComponent<PreloaderProps> = ({url}) => (
<Head>
<link rel="preload" href={process.env.API_URL + url} as="fetch" crossOrigin="anonymous" />
</Head>
);
export default Preloader;
...@@ -2,8 +2,8 @@ import React, {FunctionComponent, useState} from 'react'; ...@@ -2,8 +2,8 @@ import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import useSWR from 'swr'; import useSWR from 'swr';
import queryString from 'query-string'; import queryString from 'query-string';
import {useTranslation} from '~/utils/i18n'; import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style';
import {em, size, ellipsis, textLightColor} from '~/utils/style'; import GridLoader from 'react-spinners/GridLoader';
import StepSlider from '~/components/StepSlider'; import StepSlider from '~/components/StepSlider';
import Image from '~/components/Image'; import Image from '~/components/Image';
...@@ -78,11 +78,9 @@ type SampleChartProps = { ...@@ -78,11 +78,9 @@ type SampleChartProps = {
}; };
const getImageUrl = (index: number, run: string, tag: string, wallTime: number): string => const getImageUrl = (index: number, run: string, tag: string, wallTime: number): string =>
`${process.env.API_URL}/images/image?${queryString.stringify({index, ts: wallTime, run, tag})}`; `/images/image?${queryString.stringify({index, ts: wallTime, run, tag})}`;
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => { const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => {
const {t} = useTranslation('common');
const {data, error} = useSWR<ImageData[]>(`/images/list?${queryString.stringify({run, tag})}`, { const {data, error} = useSWR<ImageData[]>(`/images/list?${queryString.stringify({run, tag})}`, {
refreshInterval: running ? 15 * 1000 : 0 refreshInterval: running ? 15 * 1000 : 0
}); });
...@@ -97,7 +95,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin ...@@ -97,7 +95,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
</Title> </Title>
<StepSlider value={step} steps={data?.map(item => item.step) ?? []} onChange={setStep} /> <StepSlider value={step} steps={data?.map(item => item.step) ?? []} onChange={setStep} />
<Container fit={fit}> <Container fit={fit}>
{!data && !error && <span>{t('loading')}</span>} {!data && !error && <GridLoader color={primaryColor} size="10px" />}
{data && !error && <Image src={getImageUrl(step, run, tag, data[step].wallTime)} />} {data && !error && <Image src={getImageUrl(step, run, tag, data[step].wallTime)} />}
</Container> </Container>
</Wrapper> </Wrapper>
......
...@@ -56,7 +56,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -56,7 +56,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
outlier, outlier,
running running
}) => { }) => {
const {t} = useTranslation('scalars'); const {t, i18n} = useTranslation(['scalars', 'common']);
// TODO: maybe we can create a custom hook here // TODO: maybe we can create a custom hook here
const {data: datasets, error} = useSWR<DataSet[]>( const {data: datasets, error} = useSWR<DataSet[]>(
...@@ -161,9 +161,9 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -161,9 +161,9 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}; };
}) ?? []; }) ?? [];
const sort = sortingMethodMap[sortingMethod]; const sort = sortingMethodMap[sortingMethod];
return tooltip(sort ? sort(points, data) : points); return tooltip(sort ? sort(points, data) : points, i18n);
}, },
[smoothedDatasets, runs, sortingMethod] [smoothedDatasets, runs, sortingMethod, i18n]
); );
return ( return (
......
...@@ -112,9 +112,9 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({ ...@@ -112,9 +112,9 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({
dimension, dimension,
className className
}) => { }) => {
const [ref, echart] = useECharts<HTMLDivElement>({ const {ref, echart} = useECharts<HTMLDivElement>({
loading, loading,
gl: true gl: dimension === '3d'
}); });
const [highlighted, others] = useMemo(() => dividePoints(points, keyword), [points, keyword]); const [highlighted, others] = useMemo(() => dividePoints(points, keyword), [points, keyword]);
const chartOptions = useMemo( const chartOptions = useMemo(
...@@ -130,14 +130,9 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({ ...@@ -130,14 +130,9 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({
); );
useEffect(() => { useEffect(() => {
if (!process.browser) { if (process.browser) {
return; echart?.current?.setOption(chartOptions, {notMerge: true});
} }
echart.current?.setOption(
chartOptions,
true // not merged
);
}, [chartOptions, echart]); }, [chartOptions, echart]);
return <div className={className} ref={ref}></div>; return <div className={className} ref={ref}></div>;
......
...@@ -31,6 +31,7 @@ const Wrapper = styled.div<{opened?: boolean}>` ...@@ -31,6 +31,7 @@ const Wrapper = styled.div<{opened?: boolean}>`
height: ${height}; height: ${height};
line-height: calc(${height} - 2px); line-height: calc(${height} - 2px);
min-width: ${minWidth}; min-width: ${minWidth};
max-width: 100%;
display: inline-block; display: inline-block;
position: relative; position: relative;
border: 1px solid ${borderColor}; border: 1px solid ${borderColor};
...@@ -67,6 +68,8 @@ const TriggerIcon = styled(Icon)<{opened?: boolean}>` ...@@ -67,6 +68,8 @@ const TriggerIcon = styled(Icon)<{opened?: boolean}>`
const Label = styled.span` const Label = styled.span`
flex-grow: 1; flex-grow: 1;
padding-right: ${em(10)};
${ellipsis()}
`; `;
const List = styled.div<{opened?: boolean; empty?: boolean}>` const List = styled.div<{opened?: boolean; empty?: boolean}>`
...@@ -204,6 +207,7 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({ ...@@ -204,6 +207,7 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
<MultipleListItem <MultipleListItem
value={(value as SelectValueType[]).includes(item.value)} value={(value as SelectValueType[]).includes(item.value)}
key={index} key={index}
title={item.label}
size="small" size="small"
onChange={checked => changeValue(item.value, checked)} onChange={checked => changeValue(item.value, checked)}
> >
...@@ -215,6 +219,7 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({ ...@@ -215,6 +219,7 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
<ListItem <ListItem
selected={item.value === value} selected={item.value === value}
key={index} key={index}
title={item.label}
onClick={() => changeValue(item.value)} onClick={() => changeValue(item.value)}
> >
{item.label} {item.label}
......
...@@ -27,7 +27,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps} ...@@ -27,7 +27,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}
return ( return (
<> <>
<Label>{`${t('step')}: ${steps[step]}`}</Label> <Label>{`${t('step')}: ${steps[step] ?? '...'}`}</Label>
<FullWidthRangeSlider <FullWidthRangeSlider
min={0} min={0}
max={steps.length ? steps.length - 1 : 0} max={steps.length ? steps.length - 1 : 0}
......
import {useState, useEffect} from 'react';
const useDebounce = <T>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
import {useRef, useEffect, useCallback, MutableRefObject} from 'react'; import {useRef, useEffect, useCallback, useState, MutableRefObject} from 'react';
import echarts, {ECharts} from 'echarts'; import echarts, {ECharts} from 'echarts';
import {useTranslation} from '~/utils/i18n'; import {primaryColor, textColor, maskColor} from '~/utils/style';
const useECharts = <T extends HTMLElement>(options: { const useECharts = <T extends HTMLElement>(options: {
loading: boolean; loading?: boolean;
gl?: boolean; gl?: boolean;
}): [MutableRefObject<T | null>, MutableRefObject<ECharts | null>] => { zoom?: boolean;
const {t} = useTranslation('common'); }): {
const ref = useRef(null); ref: MutableRefObject<T | null>;
const echart = useRef(null as ECharts | null); echart: MutableRefObject<ECharts | null> | null;
} => {
const ref = useRef(null as T | null);
const echartInstance = useRef(null as ECharts | null);
const [echart, setEchart] = useState(null as typeof echartInstance | null);
const createChart = useCallback(() => { const createChart = useCallback(() => {
const loadExtension = options.gl ? import('echarts-gl') : Promise.resolve(); (async () => {
loadExtension.then(() => { if (options.gl) {
echart.current = echarts.init((ref.current as unknown) as HTMLDivElement); await import('echarts-gl');
}
echartInstance.current = echarts.init((ref.current as unknown) as HTMLDivElement);
if (options.zoom) {
setTimeout(() => {
echartInstance.current?.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true
}); });
}, [options.gl]); }, 0);
}
setEchart(echartInstance);
})();
}, [options.gl, options.zoom]);
const destroyChart = useCallback(() => { const destroyChart = useCallback(() => {
echart.current?.dispose(); echartInstance.current?.dispose();
setEchart(null);
}, []); }, []);
useEffect(() => { useEffect(() => {
...@@ -29,22 +46,22 @@ const useECharts = <T extends HTMLElement>(options: { ...@@ -29,22 +46,22 @@ const useECharts = <T extends HTMLElement>(options: {
}, [createChart, destroyChart]); }, [createChart, destroyChart]);
useEffect(() => { useEffect(() => {
if (process.browser) { if (process.browser && echart) {
if (options.loading) { if (options.loading) {
echart.current?.showLoading('default', { echartInstance.current?.showLoading('default', {
text: t('loading'), text: '',
color: '#c23531', color: primaryColor,
textColor: '#000', textColor,
maskColor: 'rgba(255, 255, 255, 0.8)', maskColor,
zlevel: 0 zlevel: 0
}); });
} else { } else {
echart.current?.hideLoading(); echartInstance.current?.hideLoading();
} }
} }
}, [t, options.loading]); }, [options.loading, echart]);
return [ref, echart]; return {ref, echart};
}; };
export default useECharts; export default useECharts;
import useDebounce from '~/hooks/useDebounce';
const isEmptyValue = (value: unknown): boolean =>
(Array.isArray(value) && !value.length) || ('string' === typeof value && value === '');
const useSearchValue = <T>(value: T, delay = 275): T => {
const debounced = useDebounce(value, delay);
// return empty value immediately
if (isEmptyValue(value)) {
return value;
}
// if debounced value is empty, return non-empty value immediately
if (isEmptyValue(debounced)) {
return value;
}
return debounced;
};
export default useSearchValue;
import {useReducer, useEffect, useCallback, useMemo} from 'react'; import {useReducer, useEffect, useCallback, useMemo} from 'react';
import {useRouter} from 'next/router';
import useSWR from 'swr'; import useSWR from 'swr';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import intersection from 'lodash/intersection'; import intersection from 'lodash/intersection';
import {Tag} from '~/types'; import {Tag} from '~/types';
import {useRouter} from 'next/router';
type Runs = string[]; type Runs = string[];
type Tags = Record<string, string[]>; type Tags = Record<string, string[]>;
...@@ -143,7 +143,9 @@ const useTagFilters = (type: string) => { ...@@ -143,7 +143,9 @@ const useTagFilters = (type: string) => {
selectedRuns: state.runs, selectedRuns: state.runs,
selectedTags: state.filteredTags, selectedTags: state.filteredTags,
onChangeRuns, onChangeRuns,
onFilterTags onFilterTags,
loadingRuns: !runs,
loadingTags: !tags
}; };
}; };
......
import {Request} from 'express'; import {Request} from 'express';
export default (req: Request) => { export default (req: Request) => {
if (req.query.dimension === '3') { const {dimension, run} = req.query;
if (dimension === '3') {
return { return {
embedding: [ embedding: [
[10.0, 8.04, 3], [10.0, 8.04, 3],
...@@ -16,7 +17,7 @@ export default (req: Request) => { ...@@ -16,7 +17,7 @@ export default (req: Request) => {
[7.0, 4.8, 3], [7.0, 4.8, 3],
[5.0, 5.68, 3] [5.0, 5.68, 3]
], ],
labels: ['yellow', 'blue', 'red', 'king', 'queen', 'man', 'women', 'kid', 'adult', 'light', 'dark'] labels: [`${run}-yellow`, 'blue', 'red', 'king', 'queen', 'man', 'women', 'kid', 'adult', 'light', 'dark']
}; };
} }
return { return {
...@@ -33,6 +34,6 @@ export default (req: Request) => { ...@@ -33,6 +34,6 @@ export default (req: Request) => {
[7.0, 4.8], [7.0, 4.8],
[5.0, 5.68] [5.0, 5.68]
], ],
labels: ['yellow', 'blue', 'red', 'king', 'queen', 'man', 'women', 'kid', 'adult', 'light', 'dark'] labels: [`${run}-yellow`, 'blue', 'red', 'king', 'queen', 'man', 'women', 'kid', 'adult', 'light', 'dark']
}; };
}; };
...@@ -27,6 +27,7 @@ module.exports = { ...@@ -27,6 +27,7 @@ module.exports = {
poweredByHeader: false, poweredByHeader: false,
env: { env: {
...APP, ...APP,
BUILD_ID: '',
DEFAULT_LANGUAGE, DEFAULT_LANGUAGE,
LOCALE_PATH, LOCALE_PATH,
LANGUAGES, LANGUAGES,
......
...@@ -61,10 +61,12 @@ ...@@ -61,10 +61,12 @@
"react-i18next": "11.3.3", "react-i18next": "11.3.3",
"react-input-range": "1.3.0", "react-input-range": "1.3.0",
"react-is": "16.13.0", "react-is": "16.13.0",
"react-spinners": "0.8.0",
"save-svg-as-png": "1.4.17", "save-svg-as-png": "1.4.17",
"styled-components": "5.0.1", "styled-components": "5.0.1",
"swr": "0.1.18", "swr": "0.1.18",
"url": "0.11.0" "url": "0.11.0",
"yargs": "15.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.8.7", "@babel/core": "7.8.7",
...@@ -80,11 +82,13 @@ ...@@ -80,11 +82,13 @@
"@types/react-dom": "16.9.5", "@types/react-dom": "16.9.5",
"@types/styled-components": "5.0.1", "@types/styled-components": "5.0.1",
"@types/webpack": "4.41.7", "@types/webpack": "4.41.7",
"@types/yargs": "15.0.4",
"@typescript-eslint/eslint-plugin": "2.22.0", "@typescript-eslint/eslint-plugin": "2.22.0",
"@typescript-eslint/parser": "2.22.0", "@typescript-eslint/parser": "2.22.0",
"babel-plugin-emotion": "10.0.29",
"babel-plugin-styled-components": "1.10.7", "babel-plugin-styled-components": "1.10.7",
"babel-plugin-typescript-to-proptypes": "1.3.0", "babel-plugin-typescript-to-proptypes": "1.3.0",
"cross-env": "7.0.1", "cross-env": "7.0.2",
"eslint": "6.8.0", "eslint": "6.8.0",
"eslint-config-prettier": "6.10.0", "eslint-config-prettier": "6.10.0",
"eslint-plugin-prettier": "3.1.2", "eslint-plugin-prettier": "3.1.2",
......
import React, {useState, useEffect, useMemo} from 'react'; import React, {useState, useEffect, useMemo} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import styled from 'styled-components'; import styled from 'styled-components';
import {saveSvgAsPng} from 'save-svg-as-png';
import {rem} from '~/utils/style';
import RawButton from '~/components/Button'; import RawButton from '~/components/Button';
import RawRangeSlider from '~/components/RangeSlider'; import RawRangeSlider from '~/components/RangeSlider';
import Content from '~/components/Content'; import Content from '~/components/Content';
import Title from '~/components/Title'; import Title from '~/components/Title';
import Field from '~/components/Field'; import Field from '~/components/Field';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import {rem} from '~/utils/style';
import NodeInfo, {NodeInfoProps} from '~/components/GraphPage/NodeInfo'; import NodeInfo, {NodeInfoProps} from '~/components/GraphPage/NodeInfo';
import Preloader from '~/components/Preloader';
import {Graph, collectDagFacts} from '~/resource/graph'; import {Graph, collectDagFacts} from '~/resource/graph';
import {saveSvgAsPng} from 'save-svg-as-png';
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
const dumbFn = () => {}; const dumbFn = () => {};
...@@ -269,9 +270,10 @@ const Graphs: NextI18NextPage = () => { ...@@ -269,9 +270,10 @@ const Graphs: NextI18NextPage = () => {
return ( return (
<> <>
<Preloader url="/graphs/graph" />
<Title>{t('common:graphs')}</Title> <Title>{t('common:graphs')}</Title>
<Content aside={aside}> <Content aside={aside} loading={!graph}>
<GraphSvg> <GraphSvg>
<g></g> <g></g>
</GraphSvg> </GraphSvg>
......
...@@ -3,6 +3,7 @@ import styled from 'styled-components'; ...@@ -3,6 +3,7 @@ import styled from 'styled-components';
import useSWR from 'swr'; import useSWR from 'swr';
import queryString from 'query-string'; import queryString from 'query-string';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
import useSearchValue from '~/hooks/useSearchValue';
import {rem, em} from '~/utils/style'; import {rem, em} from '~/utils/style';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import Title from '~/components/Title'; import Title from '~/components/Title';
...@@ -17,6 +18,7 @@ import RunningToggle from '~/components/RunningToggle'; ...@@ -17,6 +18,7 @@ import RunningToggle from '~/components/RunningToggle';
import ScatterChart from '~/components/ScatterChart'; import ScatterChart from '~/components/ScatterChart';
import Select, {SelectValueType} from '~/components/Select'; import Select, {SelectValueType} from '~/components/Select';
import AsideDivider from '~/components/AsideDivider'; import AsideDivider from '~/components/AsideDivider';
import Preloader from '~/components/Preloader';
import {Dimension} from '~/types'; import {Dimension} from '~/types';
const dimensions = ['2d', '3d']; const dimensions = ['2d', '3d'];
...@@ -55,6 +57,7 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -55,6 +57,7 @@ const HighDimensional: NextI18NextPage = () => {
useEffect(() => setRun(selectedRun), [setRun, selectedRun]); useEffect(() => setRun(selectedRun), [setRun, selectedRun]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const debouncedSearch = useSearchValue(search);
const [dimension, setDimension] = useState(dimensions[0] as Dimension); const [dimension, setDimension] = useState(dimensions[0] as Dimension);
const [reduction, setReduction] = useState(reductions[0]); const [reduction, setReduction] = useState(reductions[0]);
const [running, setRunning] = useState(true); const [running, setRunning] = useState(true);
...@@ -137,9 +140,15 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -137,9 +140,15 @@ const HighDimensional: NextI18NextPage = () => {
return ( return (
<> <>
<Preloader url="/runs" />
<Title>{t('common:high-dimensional')}</Title> <Title>{t('common:high-dimensional')}</Title>
<Content aside={aside}> <Content aside={aside} loading={!runs}>
<StyledScatterChart points={points} dimension={dimension} loading={!data && !error} keyword={search} /> <StyledScatterChart
points={points}
dimension={dimension}
loading={!data && !error}
keyword={debouncedSearch}
/>
</Content> </Content>
</> </>
); );
......
...@@ -14,6 +14,7 @@ import RunningToggle from '~/components/RunningToggle'; ...@@ -14,6 +14,7 @@ import RunningToggle from '~/components/RunningToggle';
import AsideDivider from '~/components/AsideDivider'; import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage'; import ChartPage from '~/components/ChartPage';
import SampleChart from '~/components/SampleChart'; import SampleChart from '~/components/SampleChart';
import Preloader from '~/components/Preloader';
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
font-size: ${rem(16)}; font-size: ${rem(16)};
...@@ -40,7 +41,9 @@ type Item = { ...@@ -40,7 +41,9 @@ type Item = {
const Samples: NextI18NextPage = () => { const Samples: NextI18NextPage = () => {
const {t} = useTranslation(['samples', 'common']); const {t} = useTranslation(['samples', 'common']);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags} = useTagFilter('images'); const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags, loadingRuns, loadingTags} = useTagFilter(
'images'
);
const ungroupedSelectedTags = useMemo( const ungroupedSelectedTags = useMemo(
() => () =>
selectedTags.reduce((prev, {runs, ...item}) => { selectedTags.reduce((prev, {runs, ...item}) => {
...@@ -103,10 +106,12 @@ const Samples: NextI18NextPage = () => { ...@@ -103,10 +106,12 @@ const Samples: NextI18NextPage = () => {
return ( return (
<> <>
<Preloader url="/runs" />
<Preloader url="/images/tags" />
<Title>{t('common:samples')}</Title> <Title>{t('common:samples')}</Title>
<Content aside={aside}> <Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} /> <TagFilter tags={tags} onChange={onFilterTags} />
<ChartPage items={ungroupedSelectedTags} withChart={withChart} /> <ChartPage items={ungroupedSelectedTags} withChart={withChart} loading={loadingRuns || loadingTags} />
</Content> </Content>
</> </>
); );
......
import React, {useState, useCallback} from 'react'; import React, {useState, useCallback} from 'react';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import useTagFilter from '~/hooks/useTagFilter'; import useTagFilter from '~/hooks/useTagFilter';
import useSearchValue from '~/hooks/useSearchValue';
import Title from '~/components/Title'; import Title from '~/components/Title';
import Content from '~/components/Content'; import Content from '~/components/Content';
import RunSelect from '~/components/RunSelect'; import RunSelect from '~/components/RunSelect';
...@@ -13,6 +14,7 @@ import RunningToggle from '~/components/RunningToggle'; ...@@ -13,6 +14,7 @@ import RunningToggle from '~/components/RunningToggle';
import AsideDivider from '~/components/AsideDivider'; import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage'; import ChartPage from '~/components/ChartPage';
import ScalarChart, {xAxisMap, sortingMethodMap} from '~/components/ScalarChart'; import ScalarChart, {xAxisMap, sortingMethodMap} from '~/components/ScalarChart';
import Preloader from '~/components/Preloader';
import {Tag} from '~/types'; import {Tag} from '~/types';
type XAxis = keyof typeof xAxisMap; type XAxis = keyof typeof xAxisMap;
...@@ -23,7 +25,11 @@ const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest']; ...@@ -23,7 +25,11 @@ const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest'];
const Scalars: NextI18NextPage = () => { const Scalars: NextI18NextPage = () => {
const {t} = useTranslation(['scalars', 'common']); const {t} = useTranslation(['scalars', 'common']);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags} = useTagFilter('scalars'); const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags, loadingRuns, loadingTags} = useTagFilter(
'scalars'
);
const debouncedTags = useSearchValue(selectedTags);
const [smoothing, setSmoothing] = useState(0.6); const [smoothing, setSmoothing] = useState(0.6);
...@@ -83,10 +89,12 @@ const Scalars: NextI18NextPage = () => { ...@@ -83,10 +89,12 @@ const Scalars: NextI18NextPage = () => {
return ( return (
<> <>
<Preloader url="/runs" />
<Preloader url="/scalars/tags" />
<Title>{t('common:scalars')}</Title> <Title>{t('common:scalars')}</Title>
<Content aside={aside}> <Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} /> <TagFilter tags={tags} onChange={onFilterTags} />
<ChartPage items={selectedTags} withChart={withChart} /> <ChartPage items={debouncedTags} withChart={withChart} loading={loadingRuns || loadingTags} />
</Content> </Content>
</> </>
); );
......
{
"longDateFormat": {
"LT": "h:mm A",
"LTS": "h:mm:ss A",
"L": "MM/DD/YYYY",
"LL": "MMMM Do YYYY",
"LLL": "MMMM Do YYYY LT",
"LLLL": "dddd, MMMM Do YYYY LT"
}
}
{ {
"smoothing": "Smoothing", "smoothing": "Smoothing",
"value": "Value",
"smoothed": "Smoothed",
"x-axis": "X-Axis", "x-axis": "X-Axis",
"x-axis-value": { "x-axis-value": {
"step": "Step", "step": "Step",
......
{
"longDateFormat": {
"LT": "HH:mm",
"LTS": "HH:mm:ss",
"L": "YYYY-MM-DD",
"LL": "YYYY MM Do",
"LLL": "YYYY MMMM Do LT",
"LLLL": "YYYY MMMM Do dddd LT"
}
}
{ {
"smoothing": "平滑度", "smoothing": "平滑度",
"value": "值",
"smoothed": "平滑值",
"x-axis": "X轴", "x-axis": "X轴",
"x-axis-value": { "x-axis-value": {
"step": "步", "step": "步",
......
...@@ -2,6 +2,7 @@ import path from 'path'; ...@@ -2,6 +2,7 @@ import path from 'path';
import express from 'express'; import express from 'express';
import next from 'next'; import next from 'next';
import {setConfig} from 'next/config'; import {setConfig} from 'next/config';
import {argv} from 'yargs';
import {nextI18NextMiddleware} from '../utils/i18next/middlewares'; import {nextI18NextMiddleware} from '../utils/i18next/middlewares';
import nextI18next from '../utils/i18n'; import nextI18next from '../utils/i18n';
import config from '../next.config'; import config from '../next.config';
...@@ -10,9 +11,11 @@ const isDev = process.env.NODE_ENV !== 'production'; ...@@ -10,9 +11,11 @@ const isDev = process.env.NODE_ENV !== 'production';
setConfig(config); setConfig(config);
const host = process.env.HOST || 'localhost'; const host = (argv.host as string) || process.env.HOST || 'localhost';
const port = process.env.PORT || 8999; const port: string | number = Number.parseInt(argv.port as string, 10) || process.env.PORT || 8999;
const proxy = process.env.PROXY; const proxy = (argv.proxy as string) || process.env.PROXY;
const delay = Number.parseInt(argv.delay as string, 10);
const app = next({dev: isDev, conf: config}); const app = next({dev: isDev, conf: config});
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
...@@ -25,7 +28,10 @@ const handle = app.getRequestHandler(); ...@@ -25,7 +28,10 @@ const handle = app.getRequestHandler();
server.use(config.env.API_URL, createProxyMiddleware({target: proxy, changeOrigin: true})); server.use(config.env.API_URL, createProxyMiddleware({target: proxy, changeOrigin: true}));
} else if (isDev) { } else if (isDev) {
const {default: mock} = await import('../utils/mock'); const {default: mock} = await import('../utils/mock');
server.use(config.env.API_URL, mock({path: path.resolve(__dirname, '../mock')})); server.use(
config.env.API_URL,
mock({path: path.resolve(__dirname, '../mock'), delay: delay ? () => Math.random() * delay : 0})
);
} }
await nextI18next.initPromise; await nextI18next.initPromise;
......
declare module 'path-match';
declare module 'detect-node'; declare module 'detect-node';
declare module 'path-match';
...@@ -3,13 +3,19 @@ ...@@ -3,13 +3,19 @@
import fetch from 'isomorphic-unfetch'; import fetch from 'isomorphic-unfetch';
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export const fetcher = async (url: string, options?: any, baseUrl = ''): Promise<any> => {
const res = await fetch(baseUrl + process.env.API_URL + url, options); export const fetcher = async <T = any>(url: string, options?: any): Promise<T> => {
const res = await fetch(process.env.API_URL + url, options);
const response = await res.json(); const response = await res.json();
return response && 'data' in response ? response.data : response; return response && 'data' in response ? response.data : response;
}; };
export const cycleFetcher = async (urls: string[], options?: any, baseUrl = ''): Promise<any> => { export const blobFetcher = async (url: string, options?: any): Promise<Blob> => {
return await Promise.all(urls.map(url => fetcher(url, options, baseUrl))); const res = await fetch(process.env.API_URL + url, options);
return await res.blob();
};
export const cycleFetcher = async <T = any>(urls: string[], options?: any): Promise<T[]> => {
return await Promise.all(urls.map(url => fetcher<T>(url, options)));
}; };
import {NextComponentType, NextPageContext} from 'next'; import {NextComponentType, NextPageContext} from 'next';
import moment from 'moment';
import NextI18Next from './i18next'; import NextI18Next from './i18next';
import {env} from '../next.config'; import {env} from '../next.config';
...@@ -8,6 +9,10 @@ const otherLanguages = allLanguages.filter(lang => lang !== defaultLanguage); ...@@ -8,6 +9,10 @@ const otherLanguages = allLanguages.filter(lang => lang !== defaultLanguage);
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
allLanguages.forEach(async (lang: string) => {
moment.updateLocale(lang, await import(`../public/locales/${lang}/moment.json`));
});
const nextI18Next = new NextI18Next({ const nextI18Next = new NextI18Next({
localePath: env.LOCALE_PATH, localePath: env.LOCALE_PATH,
browserLanguageDetection: !isDev, browserLanguageDetection: !isDev,
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import NextLink, {LinkProps} from 'next/link'; import NextLink, {LinkProps} from 'next/link';
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import {defaultConfig} from './default-config'; import {defaultConfig} from './default-config';
import {consoleMessage, isServer} from '../utils'; import {consoleMessage, isServer} from '../utils';
import {InitConfig, Config} from '../types'; import {Config} from '../types';
const deepMergeObjects = ['backend', 'detection']; const deepMergeObjects = ['backend', 'detection'];
const dedupe = (names: string[]) => names.filter((v, i) => names.indexOf(v) === i); const dedupe = (names: string[]) => names.filter((v, i) => names.indexOf(v) === i);
...@@ -29,7 +31,7 @@ export const createConfig = (userConfig: Config): Config => { ...@@ -29,7 +31,7 @@ export const createConfig = (userConfig: Config): Config => {
if (isServer()) { if (isServer()) {
const fs = eval("require('fs')"); const fs = eval("require('fs')");
const path = require('path'); const path = require('path'); // eslint-disable-line @typescript-eslint/no-var-requires
let serverLocalePath = localePath; let serverLocalePath = localePath;
/* /*
......
import {withTranslation, useTranslation, Trans} from 'react-i18next'; import {withTranslation, useTranslation, Trans} from 'react-i18next';
import hoistNonReactStatics from 'hoist-non-react-statics'; import hoistNonReactStatics from 'hoist-non-react-statics';
import {createConfig} from './config/create-config'; import {createConfig} from './config/create-config';
import createI18NextClient from './create-i18next-client'; import createI18NextClient from './create-i18next-client';
import {appWithTranslation, withInternals} from './hocs'; import {appWithTranslation, withInternals} from './hocs';
import {consoleMessage} from './utils'; import {consoleMessage} from './utils';
import {Link} from './components'; import {Link} from './components';
import {wrapRouter} from './router'; import {wrapRouter} from './router';
import { import {
AppWithTranslation, AppWithTranslation,
Config, Config,
......
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
Very important: if you import `Router` from NextJs directly, Very important: if you import `Router` from NextJs directly,
and not this file, your lang subpath routing will break. and not this file, your lang subpath routing will break.
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */
import NextRouter, {SingletonRouter} from 'next/router'; import NextRouter, {SingletonRouter} from 'next/router';
import {lngPathCorrector, subpathIsRequired} from '../utils'; import {lngPathCorrector, subpathIsRequired} from '../utils';
......
/* tslint:disable no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'; import * as React from 'react';
import { import {
...@@ -64,6 +64,7 @@ declare class NextI18Next { ...@@ -64,6 +64,7 @@ declare class NextI18Next {
} }
declare global { declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express { namespace Express {
interface Request { interface Request {
lng?: string; lng?: string;
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import {format as formatUrl, parse as parseUrl} from 'url'; import {format as formatUrl, parse as parseUrl} from 'url';
import {Config} from '../types'; import {Config} from '../types';
...@@ -42,7 +44,7 @@ export const lngPathCorrector = (config: Config, currentRoute: any, currentLangu ...@@ -42,7 +44,7 @@ export const lngPathCorrector = (config: Config, currentRoute: any, currentLangu
throw new Error('Invalid configuration: Current language is not included in all languages array'); throw new Error('Invalid configuration: Current language is not included in all languages array');
} }
let href = parseHref(originalHref); const href = parseHref(originalHref);
let as = parseAs(originalAs, href); let as = parseAs(originalAs, href);
/* /*
...@@ -56,8 +58,8 @@ export const lngPathCorrector = (config: Config, currentRoute: any, currentLangu ...@@ -56,8 +58,8 @@ export const lngPathCorrector = (config: Config, currentRoute: any, currentLangu
Strip any/all subpaths from the `as` value Strip any/all subpaths from the `as` value
*/ */
Object.values(localeSubpaths || {}).forEach((subpath: string) => { Object.values(localeSubpaths || {}).forEach((subpath: string) => {
if (subpathIsPresent(as, subpath)) { if (subpathIsPresent(as as string, subpath)) {
as = removeSubpath(as, subpath); as = removeSubpath(as as string, subpath);
} }
}); });
......
import {FallbackLng} from 'i18next'; import {FallbackLng} from 'i18next';
export const lngsToLoad = (initialLng: string | null, fallbackLng: FallbackLng | false, otherLanguages?: string[]) => { export const lngsToLoad = (initialLng: string | null, fallbackLng: FallbackLng | false, otherLanguages?: string[]) => {
const languages = []; const languages = [];
......
...@@ -27,14 +27,16 @@ export default (options: Options) => { ...@@ -27,14 +27,16 @@ export default (options: Options) => {
mock = await mock(req, res); mock = await mock(req, res);
} }
// sleep
let delay = 0; let delay = 0;
if ('function' === typeof options.delay) { if ('function' === typeof options.delay) {
delay = options.delay(method); delay = options.delay(method);
} else if (options.delay) { } else if (options.delay) {
delay = options.delay; delay = options.delay;
} }
if (delay) {
await sleep(delay); await sleep(delay);
}
if (mock instanceof ArrayBuffer) { if (mock instanceof ArrayBuffer) {
res.send(Buffer.from(mock)); res.send(Buffer.from(mock));
......
...@@ -3,6 +3,7 @@ import minBy from 'lodash/minBy'; ...@@ -3,6 +3,7 @@ import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy'; import maxBy from 'lodash/maxBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import moment from 'moment'; import moment from 'moment';
import {I18n} from '~/utils/i18next/types';
// https://en.wikipedia.org/wiki/Moving_average // https://en.wikipedia.org/wiki/Moving_average
export const transform = (seriesData: number[][], smoothingWeight: number) => { export const transform = (seriesData: number[][], smoothingWeight: number) => {
...@@ -79,8 +80,13 @@ export type TooltipData = { ...@@ -79,8 +80,13 @@ export type TooltipData = {
item: number[]; item: number[];
}; };
export const formatTime = (value: number, language: string, formatter = 'L LTS') =>
moment(Math.floor(value), 'x')
.locale(language)
.format(formatter);
// TODO: make it better, don't concat html // TODO: make it better, don't concat html
export const tooltip = (data: TooltipData[]) => { export const tooltip = (data: TooltipData[], i18n: I18n) => {
const indexPropMap = { const indexPropMap = {
Time: 0, Time: 0,
Step: 1, Step: 1,
...@@ -96,6 +102,14 @@ export const tooltip = (data: TooltipData[]) => { ...@@ -96,6 +102,14 @@ export const tooltip = (data: TooltipData[]) => {
Smoothed: 60, Smoothed: 60,
Relative: 60 Relative: 60
}; };
const translatePropMap = {
Run: 'common:runs',
Time: 'scalars:x-axis-value.wall',
Step: 'scalars:x-axis-value.step',
Value: 'scalars:value',
Smoothed: 'scalars:smoothed',
Relative: 'scalars:x-axis-value.relative'
};
const transformedData = data.map(item => { const transformedData = data.map(item => {
const data = item.item; const data = item.item;
return { return {
...@@ -104,7 +118,7 @@ export const tooltip = (data: TooltipData[]) => { ...@@ -104,7 +118,7 @@ export const tooltip = (data: TooltipData[]) => {
Smoothed: data[indexPropMap.Smoothed].toString().slice(0, 6), Smoothed: data[indexPropMap.Smoothed].toString().slice(0, 6),
Value: data[indexPropMap.Value].toString().slice(0, 6), Value: data[indexPropMap.Value].toString().slice(0, 6),
Step: data[indexPropMap.Step], Step: data[indexPropMap.Step],
Time: moment(Math.floor(data[indexPropMap.Time]), 'x').format('YYYY-MM-DD HH:mm:ss'), Time: formatTime(data[indexPropMap.Time], i18n.language),
// Relative display value should take easy-read into consideration. // Relative display value should take easy-read into consideration.
// Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only. // Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only.
Relative: Math.floor(data[indexPropMap.Relative] * 60 * 60) + 's' Relative: Math.floor(data[indexPropMap.Relative] * 60 * 60) + 's'
...@@ -112,11 +126,11 @@ export const tooltip = (data: TooltipData[]) => { ...@@ -112,11 +126,11 @@ export const tooltip = (data: TooltipData[]) => {
}); });
let headerHtml = '<tr style="font-size:14px;">'; let headerHtml = '<tr style="font-size:14px;">';
headerHtml += Object.keys(transformedData[0]) headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[])
.map(key => { .map(key => {
return `<td style="padding: 0 4px; font-weight: bold; width: ${ return `<td style="padding: 0 4px; font-weight: bold; width: ${widthPropMap[key]}px;">${i18n.t(
widthPropMap[key as keyof typeof transformedData[0]] translatePropMap[key]
}px;">${key}</td>`; )}</td>`;
}) })
.join(''); .join('');
headerHtml += '</tr>'; headerHtml += '</tr>';
......
...@@ -35,6 +35,7 @@ export const backgroundFocusedColor = '#F6F6F6'; ...@@ -35,6 +35,7 @@ export const backgroundFocusedColor = '#F6F6F6';
export const borderColor = '#DDD'; export const borderColor = '#DDD';
export const borderFocusedColor = darken(0.15, borderColor); export const borderFocusedColor = darken(0.15, borderColor);
export const progressBarColor = '#FFF'; export const progressBarColor = '#FFF';
export const maskColor = 'rgba(255, 255, 255, 0.8)';
// transitions // transitions
export const duration = '75ms'; export const duration = '75ms';
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册