未验证 提交 b2b2eb41 编写于 作者: P Peter Pan 提交者: GitHub

opt-in for SSG, remove server-side data fetching & server-side i18n (#574)

* refactor: initialize VisualDL 2.0

* refactor: fix dev server problem

* refactor: add i18n

* refactor: fix i18n

* infra i18n支持

* styled-components for debug

* infra. essential hack for styled comopnent

* use yaml for translation

* fix: navbar url problem

* feat: add nuxt module to build locales

* fix: index route redirect error

* feat: add page title

* refactor: move i18n to module

* feat: add html lang attribute

* R.I.P

* Hello React

* refactor: initialize VisualDL 2.0

* fix: layout rerender

* add favicon

* add page title

* add meta tags

* feat: finish tag filter

* refactor: hook tastes good

* add single select

* finish components

* add api server

* scalars segregate metrics

* json-server sucks

* echarts

* add eslint

* bug fix

* finish scalars page

* change layout, fix aside

* add commit hook

* use tag filter hook

* add chart loading

* encapsulate run select

* samples page under construction

* finish images

* feat: graph page, still need some polishment

* finish high-dimensional

* fix mock data problem

* update readme

* fix build

* fix: use Buffer.from instead of constractor

* update Readme

* add simplified chinese readme

* build: travis

* build: travis

* style: fix type errors

* build: travis-ci

* fix: remove unused page

* add docker build

* opt-in for SSG, remove server-side data fetching & server-side i18n
Co-authored-by: NNiandalu <Niandalu@users.noreply.github.com>
上级 73c30cdb
...@@ -18,7 +18,7 @@ module.exports = { ...@@ -18,7 +18,7 @@ module.exports = {
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: 'module' sourceType: 'module'
}, },
plugins: ['react', 'react-hooks', '@typescript-eslint'], plugins: ['react-hooks'],
settings: { settings: {
react: { react: {
version: 'detect' version: 'detect'
...@@ -26,11 +26,11 @@ module.exports = { ...@@ -26,11 +26,11 @@ module.exports = {
}, },
rules: { rules: {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'react/prop-types': 'off', 'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error', 'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn', 'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
'no-console': 'warn' 'no-console': 'warn'
} }
}; };
from node:12
# Create app directory
WORKDIR /usr/src/app
ENV NODE_ENV production
COPY package.json .
COPY yarn.lock .
RUN npm i -g pm2
RUN yarn
COPY dist dist
COPY public public
EXPOSE 8999
CMD ["pm2-runtime", "dist/server/index.js"]
<p align="center"> <p align="center">
<img align="center" style="width:480px" width="480" src="https://raw.githubusercontent.com/PaddlePaddle/VisualDL/develop/frontend/public/images/logo-visualdl.svg?sanitize=true" /> <a href="https://github.com/PaddlePaddle/VisualDL"><img align="center" style="width:480px" width="480" src="https://raw.githubusercontent.com/PaddlePaddle/VisualDL/develop/frontend/public/images/logo-visualdl.svg?sanitize=true" alt="VisualDL" /></a>
</p> </p>
<br /> <br />
......
<p align="center"> <p align="center">
<img align="center" style="width:480px" width="480" src="https://raw.githubusercontent.com/PaddlePaddle/VisualDL/develop/frontend/public/images/logo-visualdl.svg?sanitize=true" /> <a href="https://github.com/PaddlePaddle/VisualDL"><img align="center" style="width:480px" width="480" src="https://raw.githubusercontent.com/PaddlePaddle/VisualDL/develop/frontend/public/images/logo-visualdl.svg?sanitize=true" alt="VisualDL" /></a>
</p> </p>
<br /> <br />
......
import React, {FunctionComponent, useState} from 'react'; import React, {FunctionComponent, useState, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
WithStyled, WithStyled,
...@@ -93,6 +93,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({ ...@@ -93,6 +93,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
onChange onChange
}) => { }) => {
const [checked, setChecked] = useState(!!value); const [checked, setChecked] = useState(!!value);
useEffect(() => setChecked(!!value), [setChecked, value]);
const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => { const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) { if (disabled) {
return; return;
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import {NodeType, TypedNode} from '~/resource/graph'; import {NodeType, TypedNode} from '~/resource/graph';
import styled from 'styled-components'; import styled from 'styled-components';
import {WithStyled} from '~/utils/style'; import {WithStyled} from '~/utils/style';
......
import React, {FunctionComponent, useEffect, useState, useRef} from 'react'; import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import fetch from 'isomorphic-unfetch'; import fetch from 'isomorphic-unfetch';
type ImageProps = { type ImageProps = {
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
import {useTranslation, Link} from '~/utils/i18n'; import Link from 'next/link';
import {useTranslation} from 'react-i18next';
import {rem, headerColor, duration, easing, lighten, transitions} from '~/utils/style'; import {rem, headerColor, duration, easing, lighten, transitions} from '~/utils/style';
const navItems = ['scalars', 'samples', 'graphs', 'high-dimensional']; const navItems = ['scalars', 'samples', 'graphs', 'high-dimensional'];
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent, useCallback} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {WithStyled, em, size, half, math, primaryColor, textLighterColor, backgroundColor} from '~/utils/style'; import {WithStyled, em, size, half, math, primaryColor, textLighterColor, backgroundColor} from '~/utils/style';
import InputRange, {Range} from 'react-input-range'; import InputRange, {Range} from 'react-input-range';
...@@ -75,7 +75,7 @@ const RangeSlider: FunctionComponent<RangeSliderProps & WithStyled> = ({ ...@@ -75,7 +75,7 @@ const RangeSlider: FunctionComponent<RangeSliderProps & WithStyled> = ({
value, value,
disabled disabled
}) => { }) => {
const onChangeRange = (range: number | Range) => onChange?.(range as number); const onChangeRange = useCallback((range: number | Range) => onChange?.(range as number), [onChange]);
return ( return (
<Wrapper className={className} disabled={disabled}> <Wrapper className={className} disabled={disabled}>
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import Select, {SelectValueType} from '~/components/Select'; import Select, {SelectValueType} from '~/components/Select';
const Title = styled.div` const Title = styled.div`
......
import React, {FunctionComponent, useState, useCallback} from 'react'; import React, {FunctionComponent, useState, useCallback} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import Button from '~/components/Button'; import Button from '~/components/Button';
const StyledButton = styled(Button)` const StyledButton = styled(Button)`
......
import React, {FunctionComponent, useState} from 'react'; 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 {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import {em, size, ellipsis, textLightColor} from '~/utils/style'; import {em, size, ellipsis, textLightColor} from '~/utils/style';
import StepSlider from '~/components/StepSlider'; import StepSlider from '~/components/StepSlider';
import Image from '~/components/Image'; import Image from '~/components/Image';
......
...@@ -7,7 +7,7 @@ import maxBy from 'lodash/maxBy'; ...@@ -7,7 +7,7 @@ import maxBy from 'lodash/maxBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import {EChartOption} from 'echarts'; import {EChartOption} from 'echarts';
import {em, size} from '~/utils/style'; import {em, size} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import {cycleFetcher} from '~/utils/fetch'; import {cycleFetcher} from '~/utils/fetch';
import {transform, range, tooltip, TooltipData} from '~/utils/scalars'; import {transform, range, tooltip, TooltipData} from '~/utils/scalars';
import * as chart from '~/utils/chart'; import * as chart from '~/utils/chart';
......
import React, {FunctionComponent, useState, useCallback} from 'react'; import React, {FunctionComponent, useState, useCallback, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import without from 'lodash/without'; import without from 'lodash/without';
import {useTranslation} from 'react-i18next';
import useClickOutside from '~/hooks/useClickOutside'; import useClickOutside from '~/hooks/useClickOutside';
import {useTranslation} from '~/utils/i18n';
import { import {
WithStyled, WithStyled,
em, em,
...@@ -147,6 +147,12 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({ ...@@ -147,6 +147,12 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
const setIsOpenedFalse = useCallback(() => setIsOpened(false), []); const setIsOpenedFalse = useCallback(() => setIsOpened(false), []);
const [value, setValue] = useState(multiple ? (Array.isArray(propValue) ? propValue : []) : propValue); const [value, setValue] = useState(multiple ? (Array.isArray(propValue) ? propValue : []) : propValue);
useEffect(() => setValue(multiple ? (Array.isArray(propValue) ? propValue : []) : propValue), [
multiple,
propValue,
setValue
]);
const isSelected = !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType)); const isSelected = !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType));
const changeValue = (mutateValue: SelectValueType, checked?: boolean) => { const changeValue = (mutateValue: SelectValueType, checked?: boolean) => {
let newValue; let newValue;
......
import React, {FunctionComponent, useState} from 'react'; import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import Field from '~/components/Field'; import Field from '~/components/Field';
import RangeSlider from '~/components/RangeSlider'; import RangeSlider from '~/components/RangeSlider';
......
import React, {FunctionComponent, useState} from 'react'; import React, {FunctionComponent, useState, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {em, textLightColor} from '~/utils/style'; import {em, textLightColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import RangeSlider from '~/components/RangeSlider'; import RangeSlider from '~/components/RangeSlider';
const Label = styled.div` const Label = styled.div`
...@@ -23,6 +23,7 @@ type StepSliderProps = { ...@@ -23,6 +23,7 @@ type StepSliderProps = {
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}) => { const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}) => {
const {t} = useTranslation('samples'); const {t} = useTranslation('samples');
const [step, setStep] = useState(value); const [step, setStep] = useState(value);
useEffect(() => setStep(value), [setStep, value]);
return ( return (
<> <>
......
import React, {FunctionComponent, useState, useCallback} from 'react'; import React, {FunctionComponent, useState, useCallback, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import {rem, math, ellipsis} from '~/utils/style'; import {rem, math, ellipsis} from '~/utils/style';
import SearchInput from '~/components/SearchInput'; import SearchInput from '~/components/SearchInput';
import Tag from '~/components/Tag'; import Tag from '~/components/Tag';
...@@ -52,7 +52,11 @@ const TagFilter: FunctionComponent<TagFilterProps> = ({value, tags: propTags, on ...@@ -52,7 +52,11 @@ const TagFilter: FunctionComponent<TagFilterProps> = ({value, tags: propTags, on
); );
const [matchedCount, setMatchedCount] = useState(propTags?.length ?? 0); const [matchedCount, setMatchedCount] = useState(propTags?.length ?? 0);
useEffect(() => setMatchedCount(propTags?.length ?? 0), [propTags, setMatchedCount]);
const [inputValue, setInputValue] = useState(value || ''); const [inputValue, setInputValue] = useState(value || '');
useEffect(() => setInputValue(value || ''), [value, setInputValue]);
const [selectedValue, setSelectedValue] = useState(''); const [selectedValue, setSelectedValue] = useState('');
const hasSelectedValue = selectedValue !== ''; const hasSelectedValue = selectedValue !== '';
const allText = inputValue || t('all'); const allText = inputValue || t('all');
......
import {useRef, useEffect, useCallback, MutableRefObject} from 'react'; import {useRef, useEffect, useCallback, MutableRefObject} from 'react';
import echarts, {ECharts} from 'echarts'; import echarts, {ECharts} from 'echarts';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
const useECharts = <T extends HTMLElement>( const useECharts = <T extends HTMLElement>(
loading: boolean loading: boolean
......
import {useReducer} from 'react'; import {useReducer, useEffect, useCallback, useMemo} from 'react';
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 {NextPageContext} from 'next';
import {fetcher} from '~/utils/fetch';
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[]>;
const groupTags = (runs: Runs, tags?: Tags): Tag[] =>
Object.entries(
groupBy<{label: Tag['label']; run: Tag['runs'][number]}>(
runs
// get tags of selected runs
.filter(run => runs.includes(run))
// group by runs
.reduce((prev, run) => {
if (tags && tags[run]) {
Array.prototype.push.apply(
prev,
tags[run].map(label => ({label, run}))
);
}
return prev;
}, []),
tag => tag.label
)
).map(([label, tags]) => ({label, runs: tags.map(tag => tag.run)}));
type State = { type State = {
runs: Runs; runs: Runs;
initTags: Tags;
tags: Tag[]; tags: Tag[];
filteredTags: Tag[]; filteredTags: Tag[];
}; };
enum ActionType { enum ActionType {
setRuns, setRuns,
initTags,
setTags, setTags,
setFilteredTags setFilteredTags
} }
...@@ -47,6 +28,11 @@ type ActionSetRuns = { ...@@ -47,6 +28,11 @@ type ActionSetRuns = {
payload: Runs; payload: Runs;
}; };
type ActionInitTags = {
type: ActionType.initTags;
payload: Tags;
};
type ActionSetTags = { type ActionSetTags = {
type: ActionType.setTags; type: ActionType.setTags;
payload: Tag[]; payload: Tag[];
...@@ -57,78 +43,99 @@ type ActionSetFilteredTags = { ...@@ -57,78 +43,99 @@ type ActionSetFilteredTags = {
payload: Tag[]; payload: Tag[];
}; };
type Action = ActionSetRuns | ActionSetTags | ActionSetFilteredTags; type Action = ActionSetRuns | ActionInitTags | ActionSetTags | ActionSetFilteredTags;
type InitData = {
runs: Runs;
tags: Tags;
};
export type Props = { const groupTags = (runs: Runs, tags?: Tags): Tag[] =>
tags: Tags; Object.entries(
runs: Runs; groupBy<{label: Tag['label']; run: Tag['runs'][number]}>(
selectedRuns: Runs; runs
}; // get tags of selected runs
.filter(run => runs.includes(run))
// group by runs
.reduce((prev, run) => {
if (tags && tags[run]) {
Array.prototype.push.apply(
prev,
tags[run].map(label => ({label, run}))
);
}
return prev;
}, []),
tag => tag.label
)
).map(([label, tags]) => ({label, runs: tags.map(tag => tag.run)}));
export const defaultProps = { const reducer = (state: State, action: Action): State => {
tags: {}, switch (action.type) {
runs: [] case ActionType.setRuns:
const runTags = groupTags(action.payload, state.initTags);
return {
...state,
runs: action.payload,
tags: runTags,
filteredTags: runTags
};
case ActionType.initTags:
const newTags = groupTags(state.runs, action.payload);
return {
...state,
initTags: action.payload,
tags: newTags,
filteredTags: newTags
};
case ActionType.setTags:
return {
...state,
tags: action.payload,
filteredTags: action.payload
};
case ActionType.setFilteredTags:
return {
...state,
filteredTags: action.payload
};
default:
throw new Error();
}
}; };
type GetInitialProps = (type: string, context: NextPageContext, f: typeof fetcher) => Promise<Props>; const useTagFilters = (type: string) => {
const router = useRouter();
export const getInitialProps: GetInitialProps = async (type, {query}, fetcher) => { const {data: runs} = useSWR<Runs>('/runs');
const [runs, tags] = await Promise.all([fetcher('/runs').then(uniq), fetcher(`/${type}/tags`)]); const {data: tags} = useSWR<Tags>(`/${type}/tags`);
return {
runs,
selectedRuns: query.runs
? intersection(uniq(Array.isArray(query.runs) ? query.runs : query.runs.split(',')), runs)
: runs,
tags
};
};
const useTagFilters = (type: string, selectedRuns: Runs, initData: InitData) => { const selectedRuns = useMemo(
const {data: runs} = useSWR('/runs', {initialData: initData.runs}); () =>
const {data: tags} = useSWR(`/${type}/tags`, {initialData: initData.tags}); runs
? router.query.runs
const reducer = (state: State, action: Action): State => { ? intersection(
switch (action.type) { uniq(Array.isArray(router.query.runs) ? router.query.runs : router.query.runs.split(',')),
case ActionType.setRuns: runs
const newTags = groupTags(action.payload, tags); )
return { : runs
...state, : [],
runs: action.payload, [router, runs]
tags: newTags, );
filteredTags: newTags
};
case ActionType.setTags:
return {
...state,
tags: action.payload,
filteredTags: action.payload
};
case ActionType.setFilteredTags:
return {
...state,
filteredTags: action.payload
};
default:
throw Error();
}
};
const [state, dispatch] = useReducer( const [state, dispatch] = useReducer(
reducer, reducer,
{ {
runs: selectedRuns, runs: selectedRuns,
initTags: {},
tags: groupTags(selectedRuns, tags) tags: groupTags(selectedRuns, tags)
}, },
initArgs => ({...initArgs, filteredTags: initArgs.tags}) initArgs => ({...initArgs, filteredTags: initArgs.tags})
); );
const onChangeRuns = (runs: Runs) => dispatch({type: ActionType.setRuns, payload: runs}); const onChangeRuns = useCallback((runs: Runs) => dispatch({type: ActionType.setRuns, payload: runs}), [dispatch]);
const onFilterTags = (tags: Tag[]) => dispatch({type: ActionType.setFilteredTags, payload: tags}); const onInitTags = useCallback((tags: Tags) => dispatch({type: ActionType.initTags, payload: tags}), [dispatch]);
const onFilterTags = useCallback((tags: Tag[]) => dispatch({type: ActionType.setFilteredTags, payload: tags}), [
dispatch
]);
useEffect(() => onInitTags(tags || {}), [onInitTags, tags]);
useEffect(() => onChangeRuns(selectedRuns), [onChangeRuns, selectedRuns]);
return { return {
runs, runs,
......
...@@ -27,6 +27,8 @@ ...@@ -27,6 +27,8 @@
"dev": "cross-env NODE_ENV=development nodemon --watch server --ext ts --exec \"ts-node --project=tsconfig.server.json\" server/index.ts", "dev": "cross-env NODE_ENV=development nodemon --watch server --ext ts --exec \"ts-node --project=tsconfig.server.json\" server/index.ts",
"build:next": "next build", "build:next": "next build",
"build:server": "tsc --project tsconfig.server.json", "build:server": "tsc --project tsconfig.server.json",
"build": "./scripts/build.sh",
"export": "yarn build:next && next export",
"start": "NODE_ENV=production node dist/server/index.js", "start": "NODE_ENV=production node dist/server/index.js",
"lint": "tsc --noEmit && eslint --ext .tsx,.jsx.ts,.js --ignore-path .gitignore .", "lint": "tsc --noEmit && eslint --ext .tsx,.jsx.ts,.js --ignore-path .gitignore .",
"format": "prettier --write \"**/*.ts\" \"**/*.tsx\" \"**/*.js\" \"**/*.jsx\"", "format": "prettier --write \"**/*.ts\" \"**/*.tsx\" \"**/*.js\" \"**/*.jsx\"",
...@@ -37,15 +39,20 @@ ...@@ -37,15 +39,20 @@
"echarts": "4.6.0", "echarts": "4.6.0",
"echarts-gl": "1.1.1", "echarts-gl": "1.1.1",
"express": "4.17.1", "express": "4.17.1",
"i18next": "19.3.2",
"i18next-browser-languagedetector": "4.0.2",
"i18next-chained-backend": "2.0.1",
"i18next-localstorage-backend": "3.1.1",
"i18next-xhr-backend": "3.2.2",
"isomorphic-unfetch": "3.0.0", "isomorphic-unfetch": "3.0.0",
"lodash": "4.17.15", "lodash": "4.17.15",
"moment": "2.24.0", "moment": "2.24.0",
"next": "9.2.2", "next": "9.2.2",
"next-i18next": "4.2.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"polished": "3.4.4", "polished": "3.4.4",
"react": "16.13.0", "react": "16.13.0",
"react-dom": "16.13.0", "react-dom": "16.13.0",
"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",
"save-svg-as-png": "1.4.17", "save-svg-as-png": "1.4.17",
......
import '~/public/style/vdl-icon.css'; import '~/public/style/vdl-icon.css';
import '~/utils/i18n';
import React from 'react'; import React from 'react';
import {NextComponentType, NextPageContext} from 'next'; import Router from 'next/router';
import App from 'next/app'; import App from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import {SWRConfig} from 'swr'; import {SWRConfig} from 'swr';
import {fetcher} from '~/utils/fetch'; import {fetcher} from '~/utils/fetch';
import {Router, appWithTranslation} from '~/utils/i18n';
import {GlobalStyle} from '~/utils/style'; import {GlobalStyle} from '~/utils/style';
import Title from '~/components/Title';
import Layout from '~/components/Layout'; import Layout from '~/components/Layout';
Router.events.on('routeChangeStart', () => NProgress.start()); Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done()); Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done()); Router.events.on('routeChangeError', () => NProgress.done());
type AppProps<P = {}> = { class VDLApp extends App {
// eslint-disable-next-line
Component: {title?: string} & NextComponentType<NextPageContext, any, P>;
};
class VDLApp extends App<AppProps> {
render() { render() {
const {Component, pageProps} = this.props; const {Component, pageProps} = this.props;
...@@ -37,7 +31,6 @@ class VDLApp extends App<AppProps> { ...@@ -37,7 +31,6 @@ class VDLApp extends App<AppProps> {
<meta name="keywords" content={process.env.keywords} /> <meta name="keywords" content={process.env.keywords} />
<meta name="author" content={process.env.author} /> <meta name="author" content={process.env.author} />
</Head> </Head>
<Title>{Component.title}</Title>
<GlobalStyle /> <GlobalStyle />
<SWRConfig <SWRConfig
value={{ value={{
...@@ -55,4 +48,4 @@ class VDLApp extends App<AppProps> { ...@@ -55,4 +48,4 @@ class VDLApp extends App<AppProps> {
} }
} }
export default appWithTranslation(VDLApp); export default VDLApp;
import Document, {Head, Main, NextScript, DocumentContext, DocumentProps} from 'next/document'; import Document, {Head, Main, NextScript, DocumentContext} from 'next/document';
import {ServerStyleSheet} from '~/utils/style'; import {ServerStyleSheet} from '~/utils/style';
interface VDLDocumentProps extends DocumentProps { export default class VDLDocument extends Document {
languageDirection: string;
language: string;
}
export default class VDLDocument extends Document<VDLDocumentProps> {
static async getInitialProps(ctx: DocumentContext) { static async getInitialProps(ctx: DocumentContext) {
// https://github.com/zeit/next.js/blob/canary/examples/with-typescript-styled-components/pages/_document.tsx // https://github.com/zeit/next.js/blob/canary/examples/with-typescript-styled-components/pages/_document.tsx
const sheet = new ServerStyleSheet(); const sheet = new ServerStyleSheet();
...@@ -20,17 +15,8 @@ export default class VDLDocument extends Document<VDLDocumentProps> { ...@@ -20,17 +15,8 @@ export default class VDLDocument extends Document<VDLDocumentProps> {
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
// stealed from https://github.com/isaachinman/next-i18next/issues/20#issuecomment-558799264
// FIXME: https://github.com/i18next/i18next-express-middleware/blob/master/src/index.js#L23-L26
// eslint-disable-next-line
const {locals} = ctx.res as any;
const additionalProps = {
languageDirection: locals.languageDirection as string,
language: locals.language as string
};
return { return {
...initialProps, ...initialProps,
...additionalProps,
styles: ( styles: (
<> <>
{initialProps.styles} {initialProps.styles}
...@@ -44,9 +30,8 @@ export default class VDLDocument extends Document<VDLDocumentProps> { ...@@ -44,9 +30,8 @@ export default class VDLDocument extends Document<VDLDocumentProps> {
} }
render() { render() {
const {languageDirection, language} = this.props;
return ( return (
<html lang={language} dir={languageDirection}> <html>
<Head /> <Head />
<body> <body>
<Main /> <Main />
......
import React from 'react'; import React from 'react';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import {NextPage} from 'next';
interface ErrorProps { interface ErrorProps {
statusCode?: number | null; statusCode?: number | null;
} }
const Error: NextI18NextPage<ErrorProps> = ({statusCode}) => { const Error: NextPage<ErrorProps> = ({statusCode}) => {
const {t} = useTranslation('errors'); const {t} = useTranslation('errors');
return <p>{statusCode ? t('error-with-status', {statusCode}) : t('error-without-status')}</p>; return <p>{statusCode ? t('error-with-status', {statusCode}) : t('error-without-status')}</p>;
...@@ -19,7 +20,6 @@ Error.getInitialProps = ({res, err}) => { ...@@ -19,7 +20,6 @@ Error.getInitialProps = ({res, err}) => {
({statusCode} = err); ({statusCode} = err);
} }
return { return {
namespacesRequired: ['errors'],
statusCode statusCode
}; };
}; };
......
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 {NextPage} from 'next';
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} from 'react-i18next';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import {fetcher} from '~/utils/fetch'; import {fetcher} from '~/utils/fetch';
import NodeInfo, {NodeInfoProps} from '~/components/GraphPage/NodeInfo'; import NodeInfo, {NodeInfoProps} from '~/components/GraphPage/NodeInfo';
...@@ -239,9 +240,7 @@ const useDagreD3 = (graph: Graph | undefined) => { ...@@ -239,9 +240,7 @@ const useDagreD3 = (graph: Graph | undefined) => {
return {currentNode, displaySwitch, setDisplaySwitch, downloadImage, fitScreen, scale, setScale}; return {currentNode, displaySwitch, setDisplaySwitch, downloadImage, fitScreen, scale, setScale};
}; };
// eslint-disable-next-line @typescript-eslint/no-empty-interface const Graphs: NextPage = () => {
interface GraphsProps {}
const Graphs: NextI18NextPage<GraphsProps> = () => {
const {t} = useTranslation(['graphs', 'common']); const {t} = useTranslation(['graphs', 'common']);
const {data: graph} = useSWR<{data: Graph}>('/graphs/graph', fetcher); const {data: graph} = useSWR<{data: Graph}>('/graphs/graph', fetcher);
const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph ? graph.data : undefined); const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph ? graph.data : undefined);
...@@ -283,10 +282,4 @@ const Graphs: NextI18NextPage<GraphsProps> = () => { ...@@ -283,10 +282,4 @@ const Graphs: NextI18NextPage<GraphsProps> = () => {
); );
}; };
Graphs.getInitialProps = () => {
return {
namespacesRequired: ['graphs', 'common']
};
};
export default Graphs; export default Graphs;
import React, {useState} from 'react'; import React, {useState, useEffect} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import uniq from 'lodash/uniq';
import useSWR from 'swr'; import useSWR from 'swr';
import {withFetcher} from '~/utils/fetch'; import {NextPage} from 'next';
import {useRouter} from 'next/router';
import {rem, em} from '~/utils/style'; import {rem, em} from '~/utils/style';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import Title from '~/components/Title'; import Title from '~/components/Title';
import Content from '~/components/Content'; import Content from '~/components/Content';
import SearchInput from '~/components/SearchInput'; import SearchInput from '~/components/SearchInput';
...@@ -43,22 +43,24 @@ type Data = { ...@@ -43,22 +43,24 @@ type Data = {
labels: string[]; labels: string[];
}; };
type HighDimensionalProps = { const HighDimensional: NextPage = () => {
runs: string[];
selectedRun: string;
};
const HighDimensional: NextI18NextPage<HighDimensionalProps> = ({runs, selectedRun}) => {
const {t} = useTranslation(['high-dimensional', 'common']); const {t} = useTranslation(['high-dimensional', 'common']);
const {query} = useRouter();
const queryRun = Array.isArray(query.run) ? query.run[0] : query.run;
const {data: runs} = useSWR<string[]>('/runs');
const selectedRun = runs?.includes(queryRun) ? queryRun : runs?.[0];
const [run, setRun] = useState(selectedRun); const [run, setRun] = useState(selectedRun);
useEffect(() => setRun(selectedRun), [setRun, selectedRun]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
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);
const {data, error} = useSWR<Data>( const {data, error} = useSWR<Data>(
`/embeddings/embeddings?run=${encodeURIComponent(run)}&dimension=${Number.parseInt( `/embeddings/embeddings?run=${encodeURIComponent(run ?? '')}&dimension=${Number.parseInt(
dimension dimension
)}&reduction=${reduction}`, )}&reduction=${reduction}`,
{ {
...@@ -128,17 +130,4 @@ const HighDimensional: NextI18NextPage<HighDimensionalProps> = ({runs, selectedR ...@@ -128,17 +130,4 @@ const HighDimensional: NextI18NextPage<HighDimensionalProps> = ({runs, selectedR
); );
}; };
HighDimensional.defaultProps = {
runs: []
};
HighDimensional.getInitialProps = withFetcher(async ({query}, fetcher) => {
const runs = await fetcher('/runs').then(uniq);
return {
runs,
selectedRun: runs.includes(query.run) ? query.run : runs[0],
namespacesRequired: ['high-dimensional', 'common']
};
});
export default HighDimensional; export default HighDimensional;
import {useEffect} from 'react'; import {useEffect} from 'react';
import {NextI18NextPage, Router} from '~/utils/i18n'; import {NextPage} from 'next';
import Router from 'next/router';
const Index: NextI18NextPage = () => { const Index: NextPage = () => {
useEffect(() => { useEffect(() => {
Router.replace('/scalars'); Router.replace('/scalars');
}, []); }, []);
...@@ -9,10 +10,4 @@ const Index: NextI18NextPage = () => { ...@@ -9,10 +10,4 @@ const Index: NextI18NextPage = () => {
return null; return null;
}; };
Index.getInitialProps = () => {
return {
namespacesRequired: []
};
};
export default Index; export default Index;
import React, {useState, useCallback, useMemo} from 'react'; import React, {useState, useCallback, useMemo} from 'react';
import {useTranslation} from 'react-i18next';
import styled from 'styled-components'; import styled from 'styled-components';
import {NextPage} from 'next';
import {rem, em} from '~/utils/style'; import {rem, em} from '~/utils/style';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import useTagFilter from '~/hooks/useTagFilter';
import useTagFilter, {
getInitialProps as getTagFilterInitialProps,
defaultProps as defaultTagFilterProps,
Props as TagFilterProps
} from '~/hooks/useTagFilter';
import {withFetcher} from '~/utils/fetch';
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';
...@@ -42,19 +38,10 @@ type Item = { ...@@ -42,19 +38,10 @@ type Item = {
label: string; label: string;
}; };
type SamplesProps = TagFilterProps & {}; const Samples: NextPage = () => {
const Samples: NextI18NextPage<SamplesProps> = ({tags: propTags, runs: propRuns, selectedRuns: propSelectedRuns}) => {
const {t} = useTranslation(['samples', 'common']); const {t} = useTranslation(['samples', 'common']);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags} = useTagFilter( const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags} = useTagFilter('images');
'images',
propSelectedRuns,
{
runs: propRuns,
tags: propTags
}
);
const ungroupedSelectedTags = useMemo( const ungroupedSelectedTags = useMemo(
() => () =>
selectedTags.reduce((prev, {runs, ...item}) => { selectedTags.reduce((prev, {runs, ...item}) => {
...@@ -126,15 +113,4 @@ const Samples: NextI18NextPage<SamplesProps> = ({tags: propTags, runs: propRuns, ...@@ -126,15 +113,4 @@ const Samples: NextI18NextPage<SamplesProps> = ({tags: propTags, runs: propRuns,
); );
}; };
Samples.defaultProps = {
...defaultTagFilterProps
};
Samples.getInitialProps = withFetcher(async (context, fetcher) => {
return {
...(await getTagFilterInitialProps('images', context, fetcher)),
namespacesRequired: ['samples', 'common']
};
});
export default Samples; export default Samples;
import React, {useState, useCallback} from 'react'; import React, {useState, useCallback} from 'react';
import {useTranslation, NextI18NextPage} from '~/utils/i18n'; import {useTranslation} from 'react-i18next';
import useTagFilter, { import {NextPage} from 'next';
getInitialProps as getTagFilterInitialProps, import useTagFilter from '~/hooks/useTagFilter';
defaultProps as defaultTagFilterProps,
Props as TagFilterProps
} from '~/hooks/useTagFilter';
import {withFetcher} from '~/utils/fetch';
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';
...@@ -25,19 +21,10 @@ const xAxisValues = ['step', 'relative', 'wall']; ...@@ -25,19 +21,10 @@ const xAxisValues = ['step', 'relative', 'wall'];
type TooltiopSorting = keyof typeof sortingMethodMap; type TooltiopSorting = keyof typeof sortingMethodMap;
const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest']; const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest'];
type ScalarsProps = TagFilterProps & {}; const Scalars: NextPage = () => {
const Scalars: NextI18NextPage<ScalarsProps> = ({tags: propTags, runs: propRuns, selectedRuns: propSelectedRuns}) => {
const {t} = useTranslation(['scalars', 'common']); const {t} = useTranslation(['scalars', 'common']);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags} = useTagFilter( const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags} = useTagFilter('scalars');
'scalars',
propSelectedRuns,
{
runs: propRuns,
tags: propTags
}
);
const [smoothing, setSmoothing] = useState(0.6); const [smoothing, setSmoothing] = useState(0.6);
...@@ -106,15 +93,4 @@ const Scalars: NextI18NextPage<ScalarsProps> = ({tags: propTags, runs: propRuns, ...@@ -106,15 +93,4 @@ const Scalars: NextI18NextPage<ScalarsProps> = ({tags: propTags, runs: propRuns,
); );
}; };
Scalars.defaultProps = {
...defaultTagFilterProps
};
Scalars.getInitialProps = withFetcher(async (context, fetcher) => {
return {
...(await getTagFilterInitialProps('scalars', context, fetcher)),
namespacesRequired: ['scalars', 'common']
};
});
export default Scalars; export default Scalars;
#!/bin/bash
set -e
if [ ! -d dist ]; then
echo "Please build first!"
exit 1
fi
docker build -t paddlepaddle/visualdl .
...@@ -2,10 +2,7 @@ import path from 'path'; ...@@ -2,10 +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 nextI18NextMiddleware from 'next-i18next/middleware';
import config from '../next.config'; import config from '../next.config';
import nextI18next from '../utils/i18n';
import mock from '../utils/mock';
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
...@@ -20,12 +17,10 @@ const handle = app.getRequestHandler(); ...@@ -20,12 +17,10 @@ const handle = app.getRequestHandler();
const server = express(); const server = express();
if (isDev) { 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')}));
} }
await nextI18next.initPromise;
server.use(nextI18NextMiddleware(nextI18next));
server.get('*', (req, res) => handle(req, res)); server.get('*', (req, res) => handle(req, res));
server.listen(port); server.listen(port);
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
"extend": "./tsconfig.json", "extend": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "ES2018",
"esModuleInterop": true, "esModuleInterop": true,
"typeRoots": ["types"], "typeRoots": ["types"],
"outDir": "dist" "outDir": "dist"
......
import next, {NextPageContext} from 'next';
import i18NextExpressMiddleware from 'i18next-express-middleware';
declare module 'next' {
// FIXME: ~/node_modules/i18next-express-middleware/index.d.ts
interface NextI18NextPageContext extends NextPageContext {
req?: Express.Request;
res?: Express.Response;
}
}
// TODO: use this instead // TODO: use this instead
// https://github.com/zeit/swr/blob/master/examples/axios-typescript/libs/useRequest.ts // https://github.com/zeit/swr/blob/master/examples/axios-typescript/libs/useRequest.ts
import fetch from 'isomorphic-unfetch'; import fetch from 'isomorphic-unfetch';
import {NextPageContext} from 'next';
import {Request} from 'express';
/* 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> => { export const fetcher = async (url: string, options?: any, baseUrl = ''): Promise<any> => {
...@@ -15,12 +13,3 @@ export const fetcher = async (url: string, options?: any, baseUrl = ''): Promise ...@@ -15,12 +13,3 @@ export const fetcher = async (url: string, options?: any, baseUrl = ''): Promise
export const cycleFetcher = async (urls: string[], options?: any, baseUrl = ''): Promise<any> => { export const cycleFetcher = async (urls: string[], options?: any, baseUrl = ''): Promise<any> => {
return await Promise.all(urls.map(url => fetcher(url, options, baseUrl))); return await Promise.all(urls.map(url => fetcher(url, options, baseUrl)));
}; };
type GetInitialProps<T = any> = (context: NextPageContext, f: typeof fetcher) => T | Promise<T>;
export const withFetcher = (getInitialProps: GetInitialProps) => (context: NextPageContext) => {
const {req} = context;
// FIXME
const baseUrl = req ? `${((req as unknown) as Request).protocol}://${req.headers.host}` : '';
return getInitialProps(context, (url: string, options: unknown) => fetcher(url, options, baseUrl));
};
import {NextComponentType, NextPageContext} from 'next'; import i18n, {InitOptions} from 'i18next';
import NextI18Next from 'next-i18next'; import {initReactI18next} from 'react-i18next';
import Backend from 'i18next-chained-backend';
import LocalStorageBackend from 'i18next-localstorage-backend';
import XHR from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
const isProduction = process.env.NODE_ENV === 'production'; const options: InitOptions = {
react: {
useSuspense: false
},
const nextI18Next = new NextI18Next({ load: 'languageOnly',
browserLanguageDetection: isProduction, fallbackLng: 'en',
serverLanguageDetection: isProduction,
defaultNS: 'common', defaultNS: 'common',
defaultLanguage: 'en', ns: ['common'],
otherLanguages: ['zh'],
localeSubpaths: { interpolation: {
zh: 'zh' escapeValue: false // not needed for react as it escapes by default
} }
}); };
if (process.browser) {
i18n
// load translation using xhr -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
// learn more: https://github.com/i18next/i18next-xhr-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
backend: {
backends: [LocalStorageBackend, XHR],
backendOptions: [
{
defaultVersion: '1' // TODO: use build id
},
{
loadPath: `${process.env.PUBLIC_PATH}/locales/{{lng}}/{{ns}}.json`,
allowMultiLoading: false,
crossDomain: true,
overrideMimeType: true
}
]
},
// from ~/node_modules/next/types/index.d.ts detection: {},
// https://gitlab.com/kachkaev/website-frontend/-/blob/master/src/i18n.ts#L64-68
export type NextI18NextPage<P = {}, IP = P> = NextComponentType<
NextPageContext,
IP & {namespacesRequired: string[]},
P & {namespacesRequired: string[]}
>;
export default nextI18Next; ...options
});
} else {
i18n.use(initReactI18next).init(options);
}
export const {i18n, appWithTranslation, withTranslation, useTranslation, Router, Link, Trans} = nextI18Next; export default i18n;
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册