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

plenty of improvment and bug fix

上级 265bc79a
......@@ -18,7 +18,7 @@
English | [简体中文](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/README_cn.md)
**🚧UNDER CONSTRUCTION🚧**
**🚧UNDER DEVELOPMENT🚧**
**🚧SOME FEATURE MAY NOT WORK PROPERLY🚧**
......
......@@ -18,7 +18,7 @@
[English](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/README.md) | 简体中文
**🚧仍在建设中🚧**
**🚧开发中🚧**
**🚧某些功能可能不能正常工作🚧**
......
......@@ -9,6 +9,7 @@ module.exports = {
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 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 Pagination from '~/components/Pagination';
......@@ -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
// eslint-disable-next-line
type ChartPageProps<T = any> = {
items?: T[];
loading?: boolean;
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 total = Math.ceil((items?.length ?? 0) / pageSize);
......@@ -35,11 +45,17 @@ const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, withC
return (
<div className={className}>
<Wrapper>
{pageItems.map((item, index) => (
<Chart key={index}>{withChart?.(item)}</Chart>
))}
</Wrapper>
{loading ? (
<Loading>
<BarLoader color={primaryColor} width="20%" height="4px" />
</Loading>
) : (
<Wrapper>
{pageItems.map((item, index) => (
<Chart key={index}>{withChart?.(item)}</Chart>
))}
</Wrapper>
)}
<Pagination page={page} total={total} onChange={setPage} />
</div>
);
......
......@@ -81,6 +81,7 @@ type CheckboxProps = {
value?: boolean;
onChange?: (value: boolean) => unknown;
size?: 'small';
title?: string;
disabled?: boolean;
};
......@@ -90,6 +91,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
size,
disabled,
className,
title,
onChange
}) => {
const [checked, setChecked] = useState(!!value);
......@@ -103,7 +105,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
};
return (
<Wrapper disabled={disabled} className={className}>
<Wrapper disabled={disabled} className={className} title={title}>
<Input onChange={onChangeInput} checked={checked} disabled={disabled} />
<Inner checked={checked} size={size} disabled={disabled} />
<Content disabled={disabled}>{children}</Content>
......
import React, {FunctionComponent} from 'react';
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 padding = rem(20);
......@@ -30,14 +31,36 @@ const Aside = styled.aside`
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 = {
aside?: React.ReactNode;
loading?: boolean;
};
const Content: FunctionComponent<ContentProps> = ({children, aside}) => (
const Content: FunctionComponent<ContentProps> = ({children, aside, loading}) => (
<Section>
<Article aside={!!aside}>{children}</Article>
{aside && <Aside>{aside}</Aside>}
{loading && (
<Loading>
<HashLoader size="60px" color={primaryColor} />
</Loading>
)}
</Section>
);
......
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import fetch from 'isomorphic-unfetch';
import {useTranslation} from '~/utils/i18n';
import React, {FunctionComponent, useLayoutEffect, useState} from 'react';
import useSWR from 'swr';
import {primaryColor} from '~/utils/style';
import {blobFetcher} from '~/utils/fetch';
import GridLoader from 'react-spinners/GridLoader';
type ImageProps = {
src?: string;
};
const Image: FunctionComponent<ImageProps> = ({src}) => {
const {t} = useTranslation('common');
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const controller = useRef(null as AbortController | null);
const {data} = useSWR(src ?? null, blobFetcher);
useEffect(() => {
if (process.browser) {
// use useLayoutEffect hook to prevent image render after url revoked
useLayoutEffect(() => {
if (process.browser && data) {
let objectUrl: string | null = null;
(async () => {
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);
} catch {
// ignore abort error
} finally {
setLoading(false);
}
})();
objectUrl = URL.createObjectURL(data);
setUrl(objectUrl);
return () => {
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;
import React, {FunctionComponent, useEffect, useCallback} from 'react';
import {EChartOption} from 'echarts';
import {WithStyled} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import useECharts from '~/hooks/useECharts';
import * as chart from '~/utils/chart';
import {formatTime} from '~/utils/scalars';
type LineChartProps = {
title?: string;
......@@ -29,18 +31,21 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
loading,
className
}) => {
const [ref, echart] = useECharts<HTMLDivElement>({
loading: !!loading
const {i18n} = useTranslation();
const {ref, echart} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom: true
});
const xAxisFormatter = useCallback(
(value: number) => (type === 'time' ? new Date(value).toLocaleTimeString() : value),
[type]
(value: number) => (type === 'time' ? formatTime(value, i18n.language, 'LTS') : value),
[type, i18n.language]
);
useEffect(() => {
if (process.browser) {
echart.current?.setOption(
echart?.current?.setOption(
{
color: chart.color,
title: {
......@@ -84,18 +89,6 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
}
}, [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>;
};
......
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';
import styled from 'styled-components';
import useSWR from 'swr';
import queryString from 'query-string';
import {useTranslation} from '~/utils/i18n';
import {em, size, ellipsis, textLightColor} from '~/utils/style';
import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style';
import GridLoader from 'react-spinners/GridLoader';
import StepSlider from '~/components/StepSlider';
import Image from '~/components/Image';
......@@ -78,11 +78,9 @@ type SampleChartProps = {
};
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 {t} = useTranslation('common');
const {data, error} = useSWR<ImageData[]>(`/images/list?${queryString.stringify({run, tag})}`, {
refreshInterval: running ? 15 * 1000 : 0
});
......@@ -97,7 +95,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
</Title>
<StepSlider value={step} steps={data?.map(item => item.step) ?? []} onChange={setStep} />
<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)} />}
</Container>
</Wrapper>
......
......@@ -56,7 +56,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
outlier,
running
}) => {
const {t} = useTranslation('scalars');
const {t, i18n} = useTranslation(['scalars', 'common']);
// TODO: maybe we can create a custom hook here
const {data: datasets, error} = useSWR<DataSet[]>(
......@@ -161,9 +161,9 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
};
}) ?? [];
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 (
......
......@@ -112,9 +112,9 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({
dimension,
className
}) => {
const [ref, echart] = useECharts<HTMLDivElement>({
const {ref, echart} = useECharts<HTMLDivElement>({
loading,
gl: true
gl: dimension === '3d'
});
const [highlighted, others] = useMemo(() => dividePoints(points, keyword), [points, keyword]);
const chartOptions = useMemo(
......@@ -130,14 +130,9 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({
);
useEffect(() => {
if (!process.browser) {
return;
if (process.browser) {
echart?.current?.setOption(chartOptions, {notMerge: true});
}
echart.current?.setOption(
chartOptions,
true // not merged
);
}, [chartOptions, echart]);
return <div className={className} ref={ref}></div>;
......
......@@ -31,6 +31,7 @@ const Wrapper = styled.div<{opened?: boolean}>`
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${minWidth};
max-width: 100%;
display: inline-block;
position: relative;
border: 1px solid ${borderColor};
......@@ -67,6 +68,8 @@ const TriggerIcon = styled(Icon)<{opened?: boolean}>`
const Label = styled.span`
flex-grow: 1;
padding-right: ${em(10)};
${ellipsis()}
`;
const List = styled.div<{opened?: boolean; empty?: boolean}>`
......@@ -204,6 +207,7 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
<MultipleListItem
value={(value as SelectValueType[]).includes(item.value)}
key={index}
title={item.label}
size="small"
onChange={checked => changeValue(item.value, checked)}
>
......@@ -215,6 +219,7 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
<ListItem
selected={item.value === value}
key={index}
title={item.label}
onClick={() => changeValue(item.value)}
>
{item.label}
......
......@@ -27,7 +27,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}
return (
<>
<Label>{`${t('step')}: ${steps[step]}`}</Label>
<Label>{`${t('step')}: ${steps[step] ?? '...'}`}</Label>
<FullWidthRangeSlider
min={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 {useTranslation} from '~/utils/i18n';
import {primaryColor, textColor, maskColor} from '~/utils/style';
const useECharts = <T extends HTMLElement>(options: {
loading: boolean;
loading?: boolean;
gl?: boolean;
}): [MutableRefObject<T | null>, MutableRefObject<ECharts | null>] => {
const {t} = useTranslation('common');
const ref = useRef(null);
const echart = useRef(null as ECharts | null);
zoom?: boolean;
}): {
ref: MutableRefObject<T | 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 loadExtension = options.gl ? import('echarts-gl') : Promise.resolve();
loadExtension.then(() => {
echart.current = echarts.init((ref.current as unknown) as HTMLDivElement);
});
}, [options.gl]);
(async () => {
if (options.gl) {
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
});
}, 0);
}
setEchart(echartInstance);
})();
}, [options.gl, options.zoom]);
const destroyChart = useCallback(() => {
echart.current?.dispose();
echartInstance.current?.dispose();
setEchart(null);
}, []);
useEffect(() => {
......@@ -29,22 +46,22 @@ const useECharts = <T extends HTMLElement>(options: {
}, [createChart, destroyChart]);
useEffect(() => {
if (process.browser) {
if (process.browser && echart) {
if (options.loading) {
echart.current?.showLoading('default', {
text: t('loading'),
color: '#c23531',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, 0.8)',
echartInstance.current?.showLoading('default', {
text: '',
color: primaryColor,
textColor,
maskColor,
zlevel: 0
});
} else {
echart.current?.hideLoading();
echartInstance.current?.hideLoading();
}
}
}, [t, options.loading]);
}, [options.loading, echart]);
return [ref, echart];
return {ref, echart};
};
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 {useRouter} from 'next/router';
import useSWR from 'swr';
import groupBy from 'lodash/groupBy';
import uniq from 'lodash/uniq';
import intersection from 'lodash/intersection';
import {Tag} from '~/types';
import {useRouter} from 'next/router';
type Runs = string[];
type Tags = Record<string, string[]>;
......@@ -143,7 +143,9 @@ const useTagFilters = (type: string) => {
selectedRuns: state.runs,
selectedTags: state.filteredTags,
onChangeRuns,
onFilterTags
onFilterTags,
loadingRuns: !runs,
loadingTags: !tags
};
};
......
import {Request} from 'express';
export default (req: Request) => {
if (req.query.dimension === '3') {
const {dimension, run} = req.query;
if (dimension === '3') {
return {
embedding: [
[10.0, 8.04, 3],
......@@ -16,7 +17,7 @@ export default (req: Request) => {
[7.0, 4.8, 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 {
......@@ -33,6 +34,6 @@ export default (req: Request) => {
[7.0, 4.8],
[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 = {
poweredByHeader: false,
env: {
...APP,
BUILD_ID: '',
DEFAULT_LANGUAGE,
LOCALE_PATH,
LANGUAGES,
......
......@@ -61,10 +61,12 @@
"react-i18next": "11.3.3",
"react-input-range": "1.3.0",
"react-is": "16.13.0",
"react-spinners": "0.8.0",
"save-svg-as-png": "1.4.17",
"styled-components": "5.0.1",
"swr": "0.1.18",
"url": "0.11.0"
"url": "0.11.0",
"yargs": "15.1.0"
},
"devDependencies": {
"@babel/core": "7.8.7",
......@@ -80,11 +82,13 @@
"@types/react-dom": "16.9.5",
"@types/styled-components": "5.0.1",
"@types/webpack": "4.41.7",
"@types/yargs": "15.0.4",
"@typescript-eslint/eslint-plugin": "2.22.0",
"@typescript-eslint/parser": "2.22.0",
"babel-plugin-emotion": "10.0.29",
"babel-plugin-styled-components": "1.10.7",
"babel-plugin-typescript-to-proptypes": "1.3.0",
"cross-env": "7.0.1",
"cross-env": "7.0.2",
"eslint": "6.8.0",
"eslint-config-prettier": "6.10.0",
"eslint-plugin-prettier": "3.1.2",
......
import React, {useState, useEffect, useMemo} from 'react';
import useSWR from 'swr';
import styled from 'styled-components';
import {saveSvgAsPng} from 'save-svg-as-png';
import {rem} from '~/utils/style';
import RawButton from '~/components/Button';
import RawRangeSlider from '~/components/RangeSlider';
import Content from '~/components/Content';
import Title from '~/components/Title';
import Field from '~/components/Field';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import {rem} from '~/utils/style';
import NodeInfo, {NodeInfoProps} from '~/components/GraphPage/NodeInfo';
import Preloader from '~/components/Preloader';
import {Graph, collectDagFacts} from '~/resource/graph';
import {saveSvgAsPng} from 'save-svg-as-png';
// eslint-disable-next-line @typescript-eslint/no-empty-function
const dumbFn = () => {};
......@@ -269,9 +270,10 @@ const Graphs: NextI18NextPage = () => {
return (
<>
<Preloader url="/graphs/graph" />
<Title>{t('common:graphs')}</Title>
<Content aside={aside}>
<Content aside={aside} loading={!graph}>
<GraphSvg>
<g></g>
</GraphSvg>
......
......@@ -3,6 +3,7 @@ import styled from 'styled-components';
import useSWR from 'swr';
import queryString from 'query-string';
import {useRouter} from 'next/router';
import useSearchValue from '~/hooks/useSearchValue';
import {rem, em} from '~/utils/style';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import Title from '~/components/Title';
......@@ -17,6 +18,7 @@ import RunningToggle from '~/components/RunningToggle';
import ScatterChart from '~/components/ScatterChart';
import Select, {SelectValueType} from '~/components/Select';
import AsideDivider from '~/components/AsideDivider';
import Preloader from '~/components/Preloader';
import {Dimension} from '~/types';
const dimensions = ['2d', '3d'];
......@@ -55,6 +57,7 @@ const HighDimensional: NextI18NextPage = () => {
useEffect(() => setRun(selectedRun), [setRun, selectedRun]);
const [search, setSearch] = useState('');
const debouncedSearch = useSearchValue(search);
const [dimension, setDimension] = useState(dimensions[0] as Dimension);
const [reduction, setReduction] = useState(reductions[0]);
const [running, setRunning] = useState(true);
......@@ -137,9 +140,15 @@ const HighDimensional: NextI18NextPage = () => {
return (
<>
<Preloader url="/runs" />
<Title>{t('common:high-dimensional')}</Title>
<Content aside={aside}>
<StyledScatterChart points={points} dimension={dimension} loading={!data && !error} keyword={search} />
<Content aside={aside} loading={!runs}>
<StyledScatterChart
points={points}
dimension={dimension}
loading={!data && !error}
keyword={debouncedSearch}
/>
</Content>
</>
);
......
......@@ -14,6 +14,7 @@ import RunningToggle from '~/components/RunningToggle';
import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage';
import SampleChart from '~/components/SampleChart';
import Preloader from '~/components/Preloader';
const StyledIcon = styled(Icon)`
font-size: ${rem(16)};
......@@ -40,7 +41,9 @@ type Item = {
const Samples: NextI18NextPage = () => {
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(
() =>
selectedTags.reduce((prev, {runs, ...item}) => {
......@@ -103,10 +106,12 @@ const Samples: NextI18NextPage = () => {
return (
<>
<Preloader url="/runs" />
<Preloader url="/images/tags" />
<Title>{t('common:samples')}</Title>
<Content aside={aside}>
<Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} />
<ChartPage items={ungroupedSelectedTags} withChart={withChart} />
<ChartPage items={ungroupedSelectedTags} withChart={withChart} loading={loadingRuns || loadingTags} />
</Content>
</>
);
......
import React, {useState, useCallback} from 'react';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import useTagFilter from '~/hooks/useTagFilter';
import useSearchValue from '~/hooks/useSearchValue';
import Title from '~/components/Title';
import Content from '~/components/Content';
import RunSelect from '~/components/RunSelect';
......@@ -13,6 +14,7 @@ import RunningToggle from '~/components/RunningToggle';
import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage';
import ScalarChart, {xAxisMap, sortingMethodMap} from '~/components/ScalarChart';
import Preloader from '~/components/Preloader';
import {Tag} from '~/types';
type XAxis = keyof typeof xAxisMap;
......@@ -23,7 +25,11 @@ const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest'];
const Scalars: NextI18NextPage = () => {
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);
......@@ -83,10 +89,12 @@ const Scalars: NextI18NextPage = () => {
return (
<>
<Preloader url="/runs" />
<Preloader url="/scalars/tags" />
<Title>{t('common:scalars')}</Title>
<Content aside={aside}>
<Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} />
<ChartPage items={selectedTags} withChart={withChart} />
<ChartPage items={debouncedTags} withChart={withChart} loading={loadingRuns || loadingTags} />
</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",
"value": "Value",
"smoothed": "Smoothed",
"x-axis": "X-Axis",
"x-axis-value": {
"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": "平滑度",
"value": "值",
"smoothed": "平滑值",
"x-axis": "X轴",
"x-axis-value": {
"step": "步",
......
......@@ -2,6 +2,7 @@ import path from 'path';
import express from 'express';
import next from 'next';
import {setConfig} from 'next/config';
import {argv} from 'yargs';
import {nextI18NextMiddleware} from '../utils/i18next/middlewares';
import nextI18next from '../utils/i18n';
import config from '../next.config';
......@@ -10,9 +11,11 @@ const isDev = process.env.NODE_ENV !== 'production';
setConfig(config);
const host = process.env.HOST || 'localhost';
const port = process.env.PORT || 8999;
const proxy = process.env.PROXY;
const host = (argv.host as string) || process.env.HOST || 'localhost';
const port: string | number = Number.parseInt(argv.port as string, 10) || process.env.PORT || 8999;
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 handle = app.getRequestHandler();
......@@ -25,7 +28,10 @@ const handle = app.getRequestHandler();
server.use(config.env.API_URL, createProxyMiddleware({target: proxy, changeOrigin: true}));
} else if (isDev) {
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;
......
declare module 'path-match';
declare module 'detect-node';
declare module 'path-match';
......@@ -3,13 +3,19 @@
import fetch from 'isomorphic-unfetch';
/* 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();
return response && 'data' in response ? response.data : response;
};
export const cycleFetcher = async (urls: string[], options?: any, baseUrl = ''): Promise<any> => {
return await Promise.all(urls.map(url => fetcher(url, options, baseUrl)));
export const blobFetcher = async (url: string, options?: any): Promise<Blob> => {
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 moment from 'moment';
import NextI18Next from './i18next';
import {env} from '../next.config';
......@@ -8,6 +9,10 @@ const otherLanguages = allLanguages.filter(lang => lang !== defaultLanguage);
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({
localePath: env.LOCALE_PATH,
browserLanguageDetection: !isDev,
......
......@@ -17,6 +17,7 @@
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import PropTypes from 'prop-types';
import NextLink, {LinkProps} from 'next/link';
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import {defaultConfig} from './default-config';
import {consoleMessage, isServer} from '../utils';
import {InitConfig, Config} from '../types';
import {Config} from '../types';
const deepMergeObjects = ['backend', 'detection'];
const dedupe = (names: string[]) => names.filter((v, i) => names.indexOf(v) === i);
......@@ -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 = {
...defaultConfig,
...userConfig
};
/*
Sensible defaults to prevent user duplication
*/
Sensible defaults to prevent user duplication
*/
combinedConfig.allLanguages = dedupe(combinedConfig.otherLanguages.concat([combinedConfig.defaultLanguage]));
combinedConfig.whitelist = combinedConfig.allLanguages;
......@@ -29,22 +31,22 @@ export const createConfig = (userConfig: Config): Config => {
if (isServer()) {
const fs = eval("require('fs')");
const path = require('path');
const path = require('path'); // eslint-disable-line @typescript-eslint/no-var-requires
let serverLocalePath = localePath;
/*
Validate defaultNS
https://github.com/isaachinman/next-i18next/issues/358
*/
Validate defaultNS
https://github.com/isaachinman/next-i18next/issues/358
*/
if (typeof combinedConfig.defaultNS === 'string') {
const defaultFile = `/${defaultLanguage}/${combinedConfig.defaultNS}.${localeExtension}`;
const defaultNSPath = path.join(process.cwd(), localePath, defaultFile);
const defaultNSExists = fs.existsSync(defaultNSPath);
if (!defaultNSExists) {
/*
If defaultNS doesn't exist, try to fall back to the deprecated static folder
https://github.com/isaachinman/next-i18next/issues/523
*/
If defaultNS doesn't exist, try to fall back to the deprecated static folder
https://github.com/isaachinman/next-i18next/issues/523
*/
const staticDirPath = path.join(process.cwd(), STATIC_LOCALE_PATH, defaultFile);
const staticDirExists = fs.existsSync(staticDirPath);
......@@ -62,16 +64,16 @@ export const createConfig = (userConfig: Config): Config => {
}
/*
Set server side backend
*/
Set server side backend
*/
combinedConfig.backend = {
loadPath: path.join(process.cwd(), `${serverLocalePath}/${localeStructure}.${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;
if (!combinedConfig.ns) {
const getAllNamespaces = (p: string) =>
......@@ -82,15 +84,15 @@ export const createConfig = (userConfig: Config): Config => {
let clientLocalePath = localePath;
/*
Remove public prefix from client site config
*/
Remove public prefix from client site config
*/
if (localePath.startsWith('public/')) {
clientLocalePath = localePath.replace(/^public\//, '');
}
/*
Set client side backend
*/
Set client side backend
*/
combinedConfig.backend = {
loadPath: `${process.env.PUBLIC_PATH}/${clientLocalePath}/${localeStructure}.${localeExtension}`,
addPath: `${process.env.PUBLIC_PATH}/${clientLocalePath}/${localeStructure}.missing.${localeExtension}`
......@@ -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) {
(combinedConfig as any).fallbackLng =
process.env.NODE_ENV === 'production' ? combinedConfig.defaultLanguage : false;
}
/*
Deep merge with overwrite - goes last
*/
Deep merge with overwrite - goes last
*/
deepMergeObjects.forEach(obj => {
if ((userConfig as any)[obj]) {
(combinedConfig as any)[obj] = {
......
import {withTranslation, useTranslation, Trans} from 'react-i18next';
import hoistNonReactStatics from 'hoist-non-react-statics';
import {createConfig} from './config/create-config';
import createI18NextClient from './create-i18next-client';
import {appWithTranslation, withInternals} from './hocs';
import {consoleMessage} from './utils';
import {Link} from './components';
import {wrapRouter} from './router';
import {
AppWithTranslation,
Config,
......
......@@ -25,10 +25,10 @@ export default function(nexti18next: NextI18Next) {
const middleware = [];
/*
If not using server side language detection,
we need to manually set the language for
each request
*/
If not using server side language detection,
we need to manually set the language for
each request
*/
if (!config.serverLanguageDetection) {
middleware.push((req: Request, _res: Response, next: NextFunction) => {
if (isI18nRoute(req)) {
......@@ -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));
/*
This does the locale subpath work
*/
This does the locale subpath work
*/
middleware.push((req: Request, res: Response, next: NextFunction) => {
if (isI18nRoute(req) && req.i18n) {
let currentLng = lngFromReq(req);
......@@ -59,25 +59,25 @@ export default function(nexti18next: NextI18Next) {
if (lngFromCurrentSubpath !== undefined && lngFromCurrentSubpath !== currentLng) {
/*
If a user has hit a subpath which does not
match their language, give preference to
the path, and change user language.
*/
If a user has hit a subpath which does not
match their language, give preference to
the path, and change user language.
*/
req.i18n.changeLanguage(lngFromCurrentSubpath);
currentLng = lngFromCurrentSubpath;
} else if (currentLngRequiresSubpath && !currentLngSubpathIsPresent) {
/*
If a language subpath is required and
not present, prepend correct subpath
*/
If a language subpath is required and
not present, prepend correct subpath
*/
return redirectWithoutCache(res, addSubpath(req.url, currentLngSubpath));
}
/*
If a locale subpath is present in the URL,
modify req.url in place so that NextJs will
render the correct route
*/
If a locale subpath is present in the URL,
modify req.url in place so that NextJs will
render the correct route
*/
if (typeof lngFromCurrentSubpath === 'string') {
const params = localeSubpathRoute(req.url);
if (params !== false) {
......
......@@ -9,6 +9,9 @@
Very important: if you import `Router` from NextJs directly,
and not this file, your lang subpath routing will break.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import NextRouter, {SingletonRouter} from 'next/router';
import {lngPathCorrector, subpathIsRequired} from '../utils';
......
/* tslint:disable no-explicit-any */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react';
import {
......@@ -64,6 +64,7 @@ declare class NextI18Next {
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
lng?: string;
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import {format as formatUrl, parse as parseUrl} from 'url';
import {Config} from '../types';
......@@ -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');
}
let href = parseHref(originalHref);
const href = parseHref(originalHref);
let as = parseAs(originalAs, href);
/*
......@@ -56,8 +58,8 @@ export const lngPathCorrector = (config: Config, currentRoute: any, currentLangu
Strip any/all subpaths from the `as` value
*/
Object.values(localeSubpaths || {}).forEach((subpath: string) => {
if (subpathIsPresent(as, subpath)) {
as = removeSubpath(as, subpath);
if (subpathIsPresent(as as string, subpath)) {
as = removeSubpath(as as string, subpath);
}
});
......
import {FallbackLng} from 'i18next';
export const lngsToLoad = (initialLng: string | null, fallbackLng: FallbackLng | false, otherLanguages?: string[]) => {
const languages = [];
......
......@@ -27,14 +27,16 @@ export default (options: Options) => {
mock = await mock(req, res);
}
// sleep
let delay = 0;
if ('function' === typeof options.delay) {
delay = options.delay(method);
} else if (options.delay) {
delay = options.delay;
}
await sleep(delay);
if (delay) {
await sleep(delay);
}
if (mock instanceof ArrayBuffer) {
res.send(Buffer.from(mock));
......
......@@ -3,6 +3,7 @@ import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import sortBy from 'lodash/sortBy';
import moment from 'moment';
import {I18n} from '~/utils/i18next/types';
// https://en.wikipedia.org/wiki/Moving_average
export const transform = (seriesData: number[][], smoothingWeight: number) => {
......@@ -79,8 +80,13 @@ export type TooltipData = {
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
export const tooltip = (data: TooltipData[]) => {
export const tooltip = (data: TooltipData[], i18n: I18n) => {
const indexPropMap = {
Time: 0,
Step: 1,
......@@ -96,6 +102,14 @@ export const tooltip = (data: TooltipData[]) => {
Smoothed: 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 data = item.item;
return {
......@@ -104,7 +118,7 @@ export const tooltip = (data: TooltipData[]) => {
Smoothed: data[indexPropMap.Smoothed].toString().slice(0, 6),
Value: data[indexPropMap.Value].toString().slice(0, 6),
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.
// Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only.
Relative: Math.floor(data[indexPropMap.Relative] * 60 * 60) + 's'
......@@ -112,11 +126,11 @@ export const tooltip = (data: TooltipData[]) => {
});
let headerHtml = '<tr style="font-size:14px;">';
headerHtml += Object.keys(transformedData[0])
headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[])
.map(key => {
return `<td style="padding: 0 4px; font-weight: bold; width: ${
widthPropMap[key as keyof typeof transformedData[0]]
}px;">${key}</td>`;
return `<td style="padding: 0 4px; font-weight: bold; width: ${widthPropMap[key]}px;">${i18n.t(
translatePropMap[key]
)}</td>`;
})
.join('');
headerHtml += '</tr>';
......
......@@ -35,6 +35,7 @@ export const backgroundFocusedColor = '#F6F6F6';
export const borderColor = '#DDD';
export const borderFocusedColor = darken(0.15, borderColor);
export const progressBarColor = '#FFF';
export const maskColor = 'rgba(255, 255, 255, 0.8)';
// transitions
export const duration = '75ms';
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册