提交 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}>
<Wrapper> {loading ? (
{pageItems.map((item, index) => ( <Loading>
<Chart key={index}>{withChart?.(item)}</Chart> <BarLoader color={primaryColor} width="20%" height="4px" />
))} </Loading>
</Wrapper> ) : (
<Wrapper>
{pageItems.map((item, index) => (
<Chart key={index}>{withChart?.(item)}</Chart>
))}
</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); setUrl(objectUrl);
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);
} 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');
}); }
}, [options.gl]); echartInstance.current = echarts.init((ref.current as unknown) as HTMLDivElement);
if (options.zoom) {
setTimeout(() => {
echartInstance.current?.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true
});
}, 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);
...@@ -12,16 +14,16 @@ export const createConfig = (userConfig: Config): Config => { ...@@ -12,16 +14,16 @@ export const createConfig = (userConfig: Config): Config => {
} }
/* /*
Initial merge of default and user-provided config Initial merge of default and user-provided config
*/ */
const combinedConfig = { const combinedConfig = {
...defaultConfig, ...defaultConfig,
...userConfig ...userConfig
}; };
/* /*
Sensible defaults to prevent user duplication Sensible defaults to prevent user duplication
*/ */
combinedConfig.allLanguages = dedupe(combinedConfig.otherLanguages.concat([combinedConfig.defaultLanguage])); combinedConfig.allLanguages = dedupe(combinedConfig.otherLanguages.concat([combinedConfig.defaultLanguage]));
combinedConfig.whitelist = combinedConfig.allLanguages; combinedConfig.whitelist = combinedConfig.allLanguages;
...@@ -29,22 +31,22 @@ export const createConfig = (userConfig: Config): Config => { ...@@ -29,22 +31,22 @@ 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;
/* /*
Validate defaultNS Validate defaultNS
https://github.com/isaachinman/next-i18next/issues/358 https://github.com/isaachinman/next-i18next/issues/358
*/ */
if (typeof combinedConfig.defaultNS === 'string') { if (typeof combinedConfig.defaultNS === 'string') {
const defaultFile = `/${defaultLanguage}/${combinedConfig.defaultNS}.${localeExtension}`; const defaultFile = `/${defaultLanguage}/${combinedConfig.defaultNS}.${localeExtension}`;
const defaultNSPath = path.join(process.cwd(), localePath, defaultFile); const defaultNSPath = path.join(process.cwd(), localePath, defaultFile);
const defaultNSExists = fs.existsSync(defaultNSPath); const defaultNSExists = fs.existsSync(defaultNSPath);
if (!defaultNSExists) { if (!defaultNSExists) {
/* /*
If defaultNS doesn't exist, try to fall back to the deprecated static folder If defaultNS doesn't exist, try to fall back to the deprecated static folder
https://github.com/isaachinman/next-i18next/issues/523 https://github.com/isaachinman/next-i18next/issues/523
*/ */
const staticDirPath = path.join(process.cwd(), STATIC_LOCALE_PATH, defaultFile); const staticDirPath = path.join(process.cwd(), STATIC_LOCALE_PATH, defaultFile);
const staticDirExists = fs.existsSync(staticDirPath); const staticDirExists = fs.existsSync(staticDirPath);
...@@ -62,16 +64,16 @@ export const createConfig = (userConfig: Config): Config => { ...@@ -62,16 +64,16 @@ export const createConfig = (userConfig: Config): Config => {
} }
/* /*
Set server side backend Set server side backend
*/ */
combinedConfig.backend = { combinedConfig.backend = {
loadPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.${localeExtension}`), loadPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.${localeExtension}`),
addPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.missing.${localeExtension}`) addPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.missing.${localeExtension}`)
}; };
/* /*
Set server side preload (languages and namespaces) Set server side preload (languages and namespaces)
*/ */
combinedConfig.preload = allLanguages; combinedConfig.preload = allLanguages;
if (!combinedConfig.ns) { if (!combinedConfig.ns) {
const getAllNamespaces = (p: string) => const getAllNamespaces = (p: string) =>
...@@ -82,15 +84,15 @@ export const createConfig = (userConfig: Config): Config => { ...@@ -82,15 +84,15 @@ export const createConfig = (userConfig: Config): Config => {
let clientLocalePath = localePath; let clientLocalePath = localePath;
/* /*
Remove public prefix from client site config Remove public prefix from client site config
*/ */
if (localePath.startsWith('public/')) { if (localePath.startsWith('public/')) {
clientLocalePath = localePath.replace(/^public\//, ''); clientLocalePath = localePath.replace(/^public\//, '');
} }
/* /*
Set client side backend Set client side backend
*/ */
combinedConfig.backend = { combinedConfig.backend = {
loadPath: `${process.env.PUBLIC_PATH}/${clientLocalePath}/${localeStructure}.${localeExtension}`, loadPath: `${process.env.PUBLIC_PATH}/${clientLocalePath}/${localeStructure}.${localeExtension}`,
addPath: `${process.env.PUBLIC_PATH}/${clientLocalePath}/${localeStructure}.missing.${localeExtension}` addPath: `${process.env.PUBLIC_PATH}/${clientLocalePath}/${localeStructure}.missing.${localeExtension}`
...@@ -100,16 +102,16 @@ export const createConfig = (userConfig: Config): Config => { ...@@ -100,16 +102,16 @@ export const createConfig = (userConfig: Config): Config => {
} }
/* /*
Set fallback language to defaultLanguage in production Set fallback language to defaultLanguage in production
*/ */
if (!userConfig.fallbackLng) { if (!userConfig.fallbackLng) {
(combinedConfig as any).fallbackLng = (combinedConfig as any).fallbackLng =
process.env.NODE_ENV === 'production' ? combinedConfig.defaultLanguage : false; process.env.NODE_ENV === 'production' ? combinedConfig.defaultLanguage : false;
} }
/* /*
Deep merge with overwrite - goes last Deep merge with overwrite - goes last
*/ */
deepMergeObjects.forEach(obj => { deepMergeObjects.forEach(obj => {
if ((userConfig as any)[obj]) { if ((userConfig as any)[obj]) {
(combinedConfig as any)[obj] = { (combinedConfig as any)[obj] = {
......
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,
......
...@@ -25,10 +25,10 @@ export default function(nexti18next: NextI18Next) { ...@@ -25,10 +25,10 @@ export default function(nexti18next: NextI18Next) {
const middleware = []; const middleware = [];
/* /*
If not using server side language detection, If not using server side language detection,
we need to manually set the language for we need to manually set the language for
each request each request
*/ */
if (!config.serverLanguageDetection) { if (!config.serverLanguageDetection) {
middleware.push((req: Request, _res: Response, next: NextFunction) => { middleware.push((req: Request, _res: Response, next: NextFunction) => {
if (isI18nRoute(req)) { if (isI18nRoute(req)) {
...@@ -39,13 +39,13 @@ export default function(nexti18next: NextI18Next) { ...@@ -39,13 +39,13 @@ export default function(nexti18next: NextI18Next) {
} }
/* /*
This does the bulk of the i18next work This does the bulk of the i18next work
*/ */
middleware.push(i18nextMiddleware.handle(i18n)); middleware.push(i18nextMiddleware.handle(i18n));
/* /*
This does the locale subpath work This does the locale subpath work
*/ */
middleware.push((req: Request, res: Response, next: NextFunction) => { middleware.push((req: Request, res: Response, next: NextFunction) => {
if (isI18nRoute(req) && req.i18n) { if (isI18nRoute(req) && req.i18n) {
let currentLng = lngFromReq(req); let currentLng = lngFromReq(req);
...@@ -59,25 +59,25 @@ export default function(nexti18next: NextI18Next) { ...@@ -59,25 +59,25 @@ export default function(nexti18next: NextI18Next) {
if (lngFromCurrentSubpath !== undefined && lngFromCurrentSubpath !== currentLng) { if (lngFromCurrentSubpath !== undefined && lngFromCurrentSubpath !== currentLng) {
/* /*
If a user has hit a subpath which does not If a user has hit a subpath which does not
match their language, give preference to match their language, give preference to
the path, and change user language. the path, and change user language.
*/ */
req.i18n.changeLanguage(lngFromCurrentSubpath); req.i18n.changeLanguage(lngFromCurrentSubpath);
currentLng = lngFromCurrentSubpath; currentLng = lngFromCurrentSubpath;
} else if (currentLngRequiresSubpath && !currentLngSubpathIsPresent) { } else if (currentLngRequiresSubpath && !currentLngSubpathIsPresent) {
/* /*
If a language subpath is required and If a language subpath is required and
not present, prepend correct subpath not present, prepend correct subpath
*/ */
return redirectWithoutCache(res, addSubpath(req.url, currentLngSubpath)); return redirectWithoutCache(res, addSubpath(req.url, currentLngSubpath));
} }
/* /*
If a locale subpath is present in the URL, If a locale subpath is present in the URL,
modify req.url in place so that NextJs will modify req.url in place so that NextJs will
render the correct route render the correct route
*/ */
if (typeof lngFromCurrentSubpath === 'string') { if (typeof lngFromCurrentSubpath === 'string') {
const params = localeSubpathRoute(req.url); const params = localeSubpathRoute(req.url);
if (params !== false) { if (params !== false) {
......
...@@ -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;
} }
await sleep(delay);
if (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.
先完成此消息的编辑!
想要评论请 注册