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

better error handling (#797)

* build: update github actions

* chore: update dependencies

* feat: set smoothing from query string

* feat: remember smoothing in scalar page

* chore: better error handling when fetching data

* feat: add error page

* fix: build error in docker

* fix: build error in docker
上级 eb59da7f
...@@ -11,7 +11,6 @@ RUN ["pip", "install", "--disable-pip-version-check", "--find-links=dist", "visu ...@@ -11,7 +11,6 @@ RUN ["pip", "install", "--disable-pip-version-check", "--find-links=dist", "visu
WORKDIR /home/visualdl/frontend WORKDIR /home/visualdl/frontend
ENV SCOPE server ENV SCOPE server
ENV PUBLIC_PATH /paddle/visualdl/demo ENV PUBLIC_PATH /paddle/visualdl/demo
ENV API_URL /paddle/visualdl/demo/api
RUN ["./scripts/install.sh"] RUN ["./scripts/install.sh"]
RUN ["./scripts/build.sh"] RUN ["./scripts/build.sh"]
...@@ -21,7 +20,6 @@ WORKDIR /home/visualdl ...@@ -21,7 +20,6 @@ WORKDIR /home/visualdl
COPY --from=builder /home/visualdl/frontend/ ./ COPY --from=builder /home/visualdl/frontend/ ./
ENV NODE_ENV production ENV NODE_ENV production
ENV PUBLIC_PATH /paddle/visualdl/demo ENV PUBLIC_PATH /paddle/visualdl/demo
ENV API_URL /paddle/visualdl/demo/api
ENV PING_URL /ping ENV PING_URL /ping
ENV DEMO true ENV DEMO true
ENV HOST 0.0.0.0 ENV HOST 0.0.0.0
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-spinners": "0.9.0", "react-spinners": "0.9.0",
"react-toastify": "6.0.8", "react-toastify": "6.0.8",
"styled-components": "5.1.1", "styled-components": "5.2.0",
"swr": "0.3.0", "swr": "0.3.0",
"tippy.js": "6.2.6" "tippy.js": "6.2.6"
}, },
......
...@@ -7,7 +7,24 @@ ...@@ -7,7 +7,24 @@
"description": "Possible reasons are:", "description": "Possible reasons are:",
"title": "No visualized data." "title": "No visualized data."
}, },
"error-with-status": "A {{statusCode}} error occurred on server", "error": "Error occurred",
"error-without-status": "An error occurred on the server", "network-error": "Network Error",
"page-not-found": "Page Not Found" "page-not-found": "Page Not Found",
"parse-error": "Parse Error",
"response-error": {
"400": "Bad Request",
"401": "Unauthorized",
"403": "Permission Denied",
"404": "Interface Does Not Exist",
"405": "Method Not Allowed",
"408": "Request Timeout",
"413": "Request Entity Too Large",
"414": "Request-URI Too Long",
"500": "Internal Server Error",
"501": "Not Implemented",
"502": "Bad Gateway",
"503": "Service Unavailable",
"504": "Gateway Timeout",
"unknown": "Server Error"
}
} }
...@@ -7,7 +7,24 @@ ...@@ -7,7 +7,24 @@
"description": "有以下几种可能原因,请您参考相应解决方案:", "description": "有以下几种可能原因,请您参考相应解决方案:",
"title": "无可视化结果展示" "title": "无可视化结果展示"
}, },
"error-with-status": "服务器发生了一个 {{statusCode}} 错误", "error": "发生错误",
"error-without-status": "服务器发生了一个错误", "network-error": "网络错误",
"page-not-found": "页面不存在" "page-not-found": "页面不存在",
"parse-error": "解析失败",
"response-error": {
"400": "请求错误",
"401": "未授权",
"403": "没有权限",
"404": "接口不存在",
"405": "方法不允许",
"408": "请求超时",
"413": "请求体过大",
"414": "请求地址过长",
"500": "服务器内部错误",
"501": "服务器未实现",
"502": "服务器网关错误",
"503": "服务器不可用",
"504": "服务器网关超时",
"unknown": "服务器错误"
}
} }
...@@ -3,10 +3,13 @@ import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'rea ...@@ -3,10 +3,13 @@ import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'rea
import {headerHeight, position, size} from '~/utils/style'; import {headerHeight, position, size} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading'; import BodyLoading from '~/components/BodyLoading';
import ErrorBoundary from '~/components/ErrorBoundary';
import ErrorPage from '~/pages/error';
import {Helmet} from 'react-helmet'; import {Helmet} from 'react-helmet';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import Navbar from '~/components/Navbar'; import Navbar from '~/components/Navbar';
import {SWRConfig} from 'swr'; import {SWRConfig} from 'swr';
import {ToastContainer} from 'react-toastify';
import {fetcher} from '~/utils/fetch'; import {fetcher} from '~/utils/fetch';
import init from '@visualdl/wasm'; import init from '@visualdl/wasm';
import routes from '~/routes'; import routes from '~/routes';
...@@ -56,7 +59,7 @@ const Telemetry: FunctionComponent = () => { ...@@ -56,7 +59,7 @@ const Telemetry: FunctionComponent = () => {
}; };
const App: FunctionComponent = () => { const App: FunctionComponent = () => {
const {i18n} = useTranslation(); const {t, i18n} = useTranslation('errors');
const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]); const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]);
...@@ -91,17 +94,23 @@ const App: FunctionComponent = () => { ...@@ -91,17 +94,23 @@ const App: FunctionComponent = () => {
<Header> <Header>
<Navbar /> <Navbar />
</Header> </Header>
<Suspense fallback={<Progress />}> <ErrorBoundary fallback={<ErrorPage />}>
<Switch> <Suspense fallback={<Progress />}>
<Redirect exact from="/" to={defaultRoute?.path ?? '/index'} /> <Switch>
{routers.map(route => ( <Redirect exact from="/" to={defaultRoute?.path ?? '/index'} />
<Route key={route.id} path={route.path} component={route.component} /> {routers.map(route => (
))} <Route key={route.id} path={route.path} component={route.component} />
</Switch> ))}
</Suspense> <Route path="*">
<ErrorPage title={t('errors:page-not-found')} />
</Route>
</Switch>
</Suspense>
</ErrorBoundary>
</Router> </Router>
</Main> </Main>
)} )}
<ToastContainer />
</SWRConfig> </SWRConfig>
</div> </div>
); );
......
import {BlobResponse, blobFetcher} from '~/utils/fetch';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import { import {
WithStyled, WithStyled,
...@@ -13,12 +12,14 @@ import { ...@@ -13,12 +12,14 @@ import {
} from '~/utils/style'; } from '~/utils/style';
import {AudioPlayer} from '~/utils/audio'; import {AudioPlayer} from '~/utils/audio';
import type {BlobResponse} from '~/utils/fetch';
import Icon from '~/components/Icon'; import Icon from '~/components/Icon';
import PuffLoader from 'react-spinners/PuffLoader'; import PuffLoader from 'react-spinners/PuffLoader';
import RangeSlider from '~/components/RangeSlider'; import RangeSlider from '~/components/RangeSlider';
import Slider from 'react-rangeslider'; import Slider from 'react-rangeslider';
import SyncLoader from 'react-spinners/SyncLoader'; import SyncLoader from 'react-spinners/SyncLoader';
import Tippy from '@tippyjs/react'; import Tippy from '@tippyjs/react';
import {fetcher} from '~/utils/fetch';
import mime from 'mime-types'; import mime from 'mime-types';
import moment from 'moment'; import moment from 'moment';
import {saveAs} from 'file-saver'; import {saveAs} from 'file-saver';
...@@ -147,7 +148,7 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>( ...@@ -147,7 +148,7 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(
({audioContext, src, cache, onLoading, onLoad, className}, ref) => { ({audioContext, src, cache, onLoading, onLoad, className}, ref) => {
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const {data, error, loading} = useRequest<BlobResponse>(src ?? null, blobFetcher, { const {data, error, loading} = useRequest<BlobResponse>(src ?? null, fetcher, {
dedupingInterval: cache ?? 2000 dedupingInterval: cache ?? 2000
}); });
......
...@@ -46,6 +46,29 @@ const Wrapper = styled.div` ...@@ -46,6 +46,29 @@ const Wrapper = styled.div`
const reload = () => window.location.reload(); const reload = () => window.location.reload();
const ReadmeMap: Record<string, string> = {
zh: 'https://github.com/PaddlePaddle/VisualDL/blob/develop/README.md',
en: 'https://github.com/PaddlePaddle/VisualDL/blob/develop/README-en.md'
};
const UserGuideMap: Record<string, string> = {
zh: 'https://github.com/PaddlePaddle/VisualDL/blob/develop/docs/components/README.md',
en: 'https://github.com/PaddlePaddle/VisualDL/blob/develop/docs/components/UserGuide-en.md'
};
const I18nLink: FunctionComponent<{map: Record<string, string>}> = ({map, children}) => {
const {i18n} = useTranslation();
return (
<a
href={map[i18n.language] ?? map[String(i18n.options.fallbackLng)] ?? map.en}
target="_blank"
rel="noreferrer"
>
{children}
</a>
);
};
const Error: FunctionComponent<WithStyled> = ({className, children}) => { const Error: FunctionComponent<WithStyled> = ({className, children}) => {
const {t} = useTranslation('errors'); const {t} = useTranslation('errors');
...@@ -61,22 +84,14 @@ const Error: FunctionComponent<WithStyled> = ({className, children}) => { ...@@ -61,22 +84,14 @@ const Error: FunctionComponent<WithStyled> = ({className, children}) => {
<li> <li>
<Trans i18nKey="errors:common.1"> <Trans i18nKey="errors:common.1">
Log files are not generated. Please refer to&nbsp; Log files are not generated. Please refer to&nbsp;
<a href="https://github.com/PaddlePaddle/VisualDL" target="_blank" rel="noreferrer"> <I18nLink map={ReadmeMap}>README</I18nLink>
README
</a>
&nbsp;to create log files. &nbsp;to create log files.
</Trans> </Trans>
</li> </li>
<li> <li>
<Trans i18nKey="errors:common.2"> <Trans i18nKey="errors:common.2">
Log files are generated but data is not written yet. Please refer to&nbsp; Log files are generated but data is not written yet. Please refer to&nbsp;
<a <I18nLink map={UserGuideMap}>VisualDL User Guide</I18nLink>
href="https://github.com/PaddlePaddle/VisualDL/blob/develop/docs/components/README.md"
target="_blank"
rel="noreferrer"
>
VisualDL User Guide
</a>
&nbsp;to write visualized data. &nbsp;to write visualized data.
</Trans> </Trans>
</li> </li>
......
import React from 'react';
class ErrorBoundary extends React.Component<{fallback: React.ReactNode}, {hasError: boolean; error: Error | null}> {
state = {
hasError: false,
error: null
};
static getDerivedStateFromError(error: Error) {
return {
hasError: true,
error
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
import {BlobResponse, blobFetcher} from '~/utils/fetch';
import React, {useImperativeHandle, useLayoutEffect, useState} from 'react'; import React, {useImperativeHandle, useLayoutEffect, useState} from 'react';
import {WithStyled, primaryColor} from '~/utils/style'; import {WithStyled, primaryColor} from '~/utils/style';
import type {BlobResponse} from '~/utils/fetch';
import GridLoader from 'react-spinners/GridLoader'; import GridLoader from 'react-spinners/GridLoader';
import {fetcher} from '~/utils/fetch';
import mime from 'mime-types'; import mime from 'mime-types';
import {saveAs} from 'file-saver'; import {saveAs} from 'file-saver';
import useRequest from '~/hooks/useRequest'; import useRequest from '~/hooks/useRequest';
...@@ -21,7 +22,7 @@ const Image = React.forwardRef<ImageRef, ImageProps & WithStyled>(({src, cache, ...@@ -21,7 +22,7 @@ const Image = React.forwardRef<ImageRef, ImageProps & WithStyled>(({src, cache,
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const {data, error, loading} = useRequest<BlobResponse>(src ?? null, blobFetcher, { const {data, error, loading} = useRequest<BlobResponse>(src ?? null, fetcher, {
dedupingInterval: cache ?? 2000 dedupingInterval: cache ?? 2000
}); });
......
...@@ -196,7 +196,7 @@ const Navbar: FunctionComponent = () => { ...@@ -196,7 +196,7 @@ const Navbar: FunctionComponent = () => {
const currentPath = useMemo(() => pathname.replace(PUBLIC_PATH, ''), [pathname]); const currentPath = useMemo(() => pathname.replace(PUBLIC_PATH, ''), [pathname]);
const navItems = useNavItems(); const [navItems] = useNavItems();
const [items, setItems] = useState<NavbarItemProps[]>([]); const [items, setItems] = useState<NavbarItemProps[]>([]);
useEffect(() => { useEffect(() => {
setItems(oldItems => setItems(oldItems =>
......
import {useCallback, useMemo} from 'react';
const useLocalStorage = (key: string) => {
const value = useMemo(() => window.localStorage.getItem(key), [key]);
const setter = useCallback((value: string) => window.localStorage.setItem(key, value), [key]);
const remover = useCallback(() => window.localStorage.removeItem(key), [key]);
return [value, setter, remover] as const;
};
export default useLocalStorage;
...@@ -18,7 +18,7 @@ export const navMap = { ...@@ -18,7 +18,7 @@ export const navMap = {
const useNavItems = () => { const useNavItems = () => {
const [components, setComponents] = useState<Route[]>([]); const [components, setComponents] = useState<Route[]>([]);
const {data, mutate} = useRequest<(keyof typeof navMap)[]>('/components', fetcher, { const {data, loading, error, mutate} = useRequest<(keyof typeof navMap)[]>('/components', fetcher, {
refreshInterval: components.length ? 61 * 1000 : 15 * 1000, refreshInterval: components.length ? 61 * 1000 : 15 * 1000,
dedupingInterval: 14 * 1000, dedupingInterval: 14 * 1000,
errorRetryInterval: 15 * 1000, errorRetryInterval: 15 * 1000,
...@@ -59,9 +59,9 @@ const useNavItems = () => { ...@@ -59,9 +59,9 @@ const useNavItems = () => {
useEffect(() => { useEffect(() => {
setComponents(filterPages(routes)); setComponents(filterPages(routes));
}, [data, filterPages]); }, [filterPages]);
return components; return [components, loading, error] as const;
}; };
export default useNavItems; export default useNavItems;
import type {ParseOptions} from 'query-string';
import queryString from 'query-string';
import {useLocation} from 'react-router-dom';
import {useMemo} from 'react';
const useQuery = (options?: ParseOptions) => {
const location = useLocation();
const query = useMemo(() => queryString.parse(location.search, options), [location.search, options]);
return query;
};
export default useQuery;
import type {ConfigInterface, keyInterface, responseInterface} from 'swr';
import {useEffect, useMemo} from 'react'; import {useEffect, useMemo} from 'react';
import useSWR, {ConfigInterface, keyInterface, responseInterface} from 'swr';
import ee from '~/utils/event'; import ee from '~/utils/event';
import type {fetcherFn} from 'swr/dist/types'; import type {fetcherFn} from 'swr/dist/types';
import {toast} from 'react-toastify';
import useSWR from 'swr';
type Response<D, E> = responseInterface<D, E> & { type Response<D, E> = responseInterface<D, E> & {
loading: boolean; loading: boolean;
}; };
function useRequest<D = unknown, E = unknown>(key: keyInterface): Response<D, E>; function useRequest<D = unknown, E extends Error = Error>(key: keyInterface): Response<D, E>;
function useRequest<D = unknown, E = unknown>(key: keyInterface, fetcher?: fetcherFn<D>): Response<D, E>; function useRequest<D = unknown, E extends Error = Error>(key: keyInterface, fetcher?: fetcherFn<D>): Response<D, E>;
function useRequest<D = unknown, E = unknown>( function useRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: keyInterface,
fetcher?: fetcherFn<D>, fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E, fetcherFn<D>> config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E>; ): Response<D, E>;
function useRequest<D = unknown, E = unknown>( function useRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: keyInterface,
fetcher?: fetcherFn<D>, fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E, fetcherFn<D>> config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E> { ): Response<D, E> {
const {data, error, ...other} = useSWR<D, E>(key, fetcher, config); const {data, error, ...other} = useSWR<D, E>(key, fetcher, config);
const loading = useMemo(() => !!key && !data && !error, [key, data, error]); const loading = useMemo(() => !!key && !data && !error, [key, data, error]);
useEffect(() => {
if (error) {
toast(error.message, {
position: toast.POSITION.TOP_CENTER,
type: toast.TYPE.ERROR
});
}
}, [error]);
return {data, error, loading, ...other}; return {data, error, loading, ...other};
} }
function useRunningRequest<D = unknown, E = unknown>(key: keyInterface, running: boolean): Response<D, E>; function useRunningRequest<D = unknown, E extends Error = Error>(key: keyInterface, running: boolean): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>( function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: keyInterface,
running: boolean, running: boolean,
fetcher?: fetcherFn<D> fetcher?: fetcherFn<D>
): Response<D, E>; ): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>( function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: keyInterface,
running: boolean, running: boolean,
fetcher?: fetcherFn<D>, fetcher?: fetcherFn<D>,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'> config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'>
): Response<D, E>; ): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>( function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: keyInterface,
running: boolean, running: boolean,
fetcher?: fetcherFn<D>, fetcher?: fetcherFn<D>,
......
...@@ -4,10 +4,9 @@ import {useCallback, useEffect, useMemo, useReducer} from 'react'; ...@@ -4,10 +4,9 @@ import {useCallback, useEffect, useMemo, useReducer} from 'react';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import intersectionBy from 'lodash/intersectionBy'; import intersectionBy from 'lodash/intersectionBy';
import queryString from 'query-string';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import useGlobalState from '~/hooks/useGlobalState'; import useGlobalState from '~/hooks/useGlobalState';
import {useLocation} from 'react-router-dom'; import useQuery from '~/hooks/useQuery';
import {useRunningRequest} from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
type Tags = Record<string, string[]>; type Tags = Record<string, string[]>;
...@@ -148,8 +147,7 @@ const reducer = (state: State, action: Action): State => { ...@@ -148,8 +147,7 @@ const reducer = (state: State, action: Action): State => {
// TODO: refactor to improve performance // TODO: refactor to improve performance
const useTagFilter = (type: string, running: boolean) => { const useTagFilter = (type: string, running: boolean) => {
const location = useLocation(); const query = useQuery();
const query = useMemo(() => queryString.parse(location.search), [location.search]);
const {data, loading, error} = useRunningRequest<TagsData>(`/${type}/tags`, running); const {data, loading, error} = useRunningRequest<TagsData>(`/${type}/tags`, running);
......
import React, {FunctionComponent} from 'react';
import Content from '~/components/Content';
import ErrorComponent from '~/components/Error';
import {useTranslation} from 'react-i18next';
type ErrorProps = {
title?: string;
desc?: string;
};
const Error: FunctionComponent<ErrorProps> = ({title, desc, children}) => {
const {t} = useTranslation('errors');
return (
<Content>
<ErrorComponent>
{children || (
<>
<h4>{title ?? t('errors:error')}</h4>
<p>{desc}</p>
</>
)}
</ErrorComponent>
</Content>
);
};
export default Error;
import Aside, {AsideSection} from '~/components/Aside'; import Aside, {AsideSection} from '~/components/Aside';
import {BlobResponse, blobFetcher} from '~/utils/fetch';
import type {Documentation, OpenedResult, Properties, SearchItem, SearchResult} from '~/resource/graph/types'; import type {Documentation, OpenedResult, Properties, SearchItem, SearchResult} from '~/resource/graph/types';
import GraphComponent, {GraphRef} from '~/components/GraphPage/Graph'; import GraphComponent, {GraphRef} from '~/components/GraphPage/Graph';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import Select, {SelectProps} from '~/components/Select'; import Select, {SelectProps} from '~/components/Select';
import {primaryColor, rem, size} from '~/utils/style'; import {primaryColor, rem, size} from '~/utils/style';
import type {BlobResponse} from '~/utils/fetch';
import Button from '~/components/Button'; import Button from '~/components/Button';
import Checkbox from '~/components/Checkbox'; import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content'; import Content from '~/components/Content';
...@@ -93,7 +93,7 @@ const Graph: FunctionComponent = () => { ...@@ -93,7 +93,7 @@ const Graph: FunctionComponent = () => {
[globalDispatch] [globalDispatch]
); );
const {data, loading} = useRequest<BlobResponse>(files ? null : '/graph/graph', blobFetcher); const {data, loading} = useRequest<BlobResponse>(files ? null : '/graph/graph');
useEffect(() => { useEffect(() => {
if (data?.data.size) { if (data?.data.size) {
......
...@@ -16,9 +16,8 @@ import RunningToggle from '~/components/RunningToggle'; ...@@ -16,9 +16,8 @@ import RunningToggle from '~/components/RunningToggle';
import SearchInput from '~/components/SearchInput'; import SearchInput from '~/components/SearchInput';
import type {TagsData} from '~/types'; import type {TagsData} from '~/types';
import Title from '~/components/Title'; import Title from '~/components/Title';
import queryString from 'query-string';
import styled from 'styled-components'; import styled from 'styled-components';
import {useLocation} from 'react-router-dom'; import useQuery from '~/hooks/useQuery';
import {useRunningRequest} from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
import useSearchValue from '~/hooks/useSearchValue'; import useSearchValue from '~/hooks/useSearchValue';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
...@@ -66,8 +65,7 @@ const HighDimensional: FunctionComponent = () => { ...@@ -66,8 +65,7 @@ const HighDimensional: FunctionComponent = () => {
}, [data]); }, [data]);
const labelList = useMemo(() => list.map(item => item.label), [list]); const labelList = useMemo(() => list.map(item => item.label), [list]);
const location = useLocation(); const query = useQuery();
const query = useMemo(() => queryString.parse(location.search), [location]);
const selectedLabel = useMemo(() => { const selectedLabel = useMemo(() => {
const run = Array.isArray(query.run) ? query.run[0] : query.run; const run = Array.isArray(query.run) ? query.run[0] : query.run;
return (run && list.find(item => item.run === run)?.label) ?? list[0]?.label; return (run && list.find(item => item.run === run)?.label) ?? list[0]?.label;
......
import React, {FunctionComponent, useEffect} from 'react'; import React, {FunctionComponent, useEffect} from 'react';
import {headerHeight, primaryColor, rem, size} from '~/utils/style'; import {headerHeight, primaryColor, rem, size} from '~/utils/style';
import {useHistory, useLocation} from 'react-router-dom';
import Error from '~/components/Error';
import HashLoader from 'react-spinners/HashLoader'; import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components'; import styled from 'styled-components';
import {useHistory} from 'react-router-dom';
import useNavItems from '~/hooks/useNavItems'; import useNavItems from '~/hooks/useNavItems';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
const Loading = styled.div` const CenterWrapper = styled.div`
${size(`calc(100vh - ${headerHeight})`, '100vw')} ${size(`calc(100vh - ${headerHeight})`, '100vw')}
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overscroll-behavior: none; overscroll-behavior: none;
cursor: progress; `;
const Loading = styled.div`
font-size: ${rem(16)}; font-size: ${rem(16)};
line-height: ${rem(60)}; line-height: ${rem(60)};
`; `;
const IndexPage: FunctionComponent = () => { const IndexPage: FunctionComponent = () => {
const navItems = useNavItems(); const [navItems, loading] = useNavItems();
const history = useHistory(); const history = useHistory();
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const location = useLocation();
useEffect(() => { useEffect(() => {
if (navItems.length) { if (navItems.length) {
if (navItems[0].path) { if (navItems[0].path) {
history.replace(navItems[0].path); history.replace(navItems[0].path + location.search);
} else if (navItems[0].children?.length && navItems[0].children[0].path) { } else if (navItems[0].children?.length && navItems[0].children[0].path) {
history.replace(navItems[0].children[0].path); history.replace(navItems[0].children[0].path + location.search);
} }
} }
}, [navItems, history]); }, [navItems, history, location.search]);
return ( return (
<Loading> <CenterWrapper>
<HashLoader size="60px" color={primaryColor} /> {loading || navItems.length ? (
<span>{t('common:loading')}</span> <Loading>
</Loading> <HashLoader size="60px" color={primaryColor} />
<span>{t('common:loading')}</span>
</Loading>
) : (
<Error />
)}
</CenterWrapper>
); );
}; };
......
import ChartPage, {WithChart} from '~/components/ChartPage'; import ChartPage, {WithChart} from '~/components/ChartPage';
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react'; import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import {SortingMethod, XAxis, sortingMethod as toolTipSortingValues} from '~/resource/scalar'; import {SortingMethod, XAxis, parseSmoothing, sortingMethod as toolTipSortingValues} from '~/resource/scalar';
import {AsideSection} from '~/components/Aside'; import {AsideSection} from '~/components/Aside';
import Checkbox from '~/components/Checkbox'; import Checkbox from '~/components/Checkbox';
...@@ -16,6 +16,8 @@ import TimeModeSelect from '~/components/TimeModeSelect'; ...@@ -16,6 +16,8 @@ import TimeModeSelect from '~/components/TimeModeSelect';
import Title from '~/components/Title'; import Title from '~/components/Title';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import useLocalStorage from '~/hooks/useLocalStorage';
import useQuery from '~/hooks/useQuery';
import useTagFilter from '~/hooks/useTagFilter'; import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
...@@ -33,12 +35,21 @@ const TooltipSortingDiv = styled.div` ...@@ -33,12 +35,21 @@ const TooltipSortingDiv = styled.div`
const Scalar: FunctionComponent = () => { const Scalar: FunctionComponent = () => {
const {t} = useTranslation(['scalar', 'common']); const {t} = useTranslation(['scalar', 'common']);
const query = useQuery();
const [running, setRunning] = useState(true); const [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, onChangeRuns, loading} = useTagFilter('scalar', running); const {runs, tags, selectedRuns, onChangeRuns, loading} = useTagFilter('scalar', running);
const [smoothing, setSmoothing] = useState(0.6); const [smoothingFromLocalStorage, setSmoothingFromLocalStorage] = useLocalStorage('scalar_smoothing');
const parsedSmoothing = useMemo(() => {
if (query.smoothing != null) {
return parseSmoothing(query.smoothing);
}
return parseSmoothing(smoothingFromLocalStorage);
}, [query.smoothing, smoothingFromLocalStorage]);
const [smoothing, setSmoothing] = useState(parsedSmoothing);
useEffect(() => setSmoothingFromLocalStorage(String(smoothing)), [smoothing, setSmoothingFromLocalStorage]);
const [xAxis, setXAxis] = useState<XAxis>(XAxis.Step); const [xAxis, setXAxis] = useState<XAxis>(XAxis.Step);
......
...@@ -123,3 +123,12 @@ export const nearestPoint = (data: Dataset[], runs: Run[], step: number) => ...@@ -123,3 +123,12 @@ export const nearestPoint = (data: Dataset[], runs: Run[], step: number) =>
item: nearestItem || [0, 0, 0, 0, 0] item: nearestItem || [0, 0, 0, 0, 0]
}; };
}); });
export const parseSmoothing = (value: unknown) => {
const parsedValue = Number.parseFloat(String(value));
let smoothing = 0.6;
if (Number.isFinite(parsedValue) && parsedValue < 1 && parsedValue >= 0) {
smoothing = Math.round(parsedValue * 100) / 100;
}
return smoothing;
};
import type {TFunction} from 'i18next';
import i18next from 'i18next';
import queryString from 'query-string'; import queryString from 'query-string';
const API_TOKEN_KEY: string = import.meta.env.SNOWPACK_PUBLIC_API_TOKEN_KEY; const API_TOKEN_KEY: string = import.meta.env.SNOWPACK_PUBLIC_API_TOKEN_KEY;
...@@ -29,12 +31,17 @@ function addApiToken(options?: RequestInit): RequestInit | undefined { ...@@ -29,12 +31,17 @@ function addApiToken(options?: RequestInit): RequestInit | undefined {
}; };
} }
export const fetcher = async <T = unknown>(url: string, options?: RequestInit): Promise<T> => { interface SuccessData<D> {
const res = await fetch(API_URL + url, addApiToken(options)); status: 0;
const response = await res.json(); data: D;
}
return response && 'data' in response ? response.data : response; interface ErrorData {
}; status: number;
msg?: string;
}
type Data<D> = SuccessData<D> | ErrorData;
export type BlobResponse = { export type BlobResponse = {
data: Blob; data: Blob;
...@@ -42,30 +49,77 @@ export type BlobResponse = { ...@@ -42,30 +49,77 @@ export type BlobResponse = {
filename: string | null; filename: string | null;
}; };
export const blobFetcher = async (url: string, options?: RequestInit): Promise<BlobResponse> => { function getT(): Promise<TFunction> {
const res = await fetch(API_URL + url, addApiToken(options)); return new Promise(resolve => {
const data = await res.blob(); // Bug of i18next
const disposition = res.headers.get('Content-Disposition'); i18next.changeLanguage((undefined as unknown) as string).then(t => resolve(t));
// support safari });
if (!data.arrayBuffer) { }
data.arrayBuffer = async () =>
new Promise<ArrayBuffer>((resolve, reject) => { function logErrorAndReturnT(e: unknown) {
const fileReader = new FileReader(); if (import.meta.env.MODE === 'development') {
fileReader.addEventListener('load', e => console.error(e); // eslint-disable-line no-console
e.target ? resolve(e.target.result as ArrayBuffer) : reject() }
); return getT();
fileReader.readAsArrayBuffer(data); }
});
export function fetcher(url: string, options?: RequestInit): Promise<BlobResponse>;
export function fetcher<T = unknown>(url: string, options?: RequestInit): Promise<T>;
export async function fetcher<T = unknown>(url: string, options?: RequestInit): Promise<BlobResponse | T> {
let res: Response;
try {
res = await fetch(API_URL + url, addApiToken(options));
} catch (e) {
const t = await logErrorAndReturnT(e);
throw new Error(t('errors:network-error'));
}
if (!res.ok) {
const t = await logErrorAndReturnT(res);
throw new Error(t([`errors:response-error.${res.status}`, 'errors:response-error.unknown']));
} }
let filename: string | null = null;
if (disposition && disposition.indexOf('attachment') !== -1) { let response: Data<T> | T;
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition); try {
if (matches != null && matches[1]) { if (res.headers.get('content-type')?.includes('application/json')) {
filename = matches[1].replace(/['"]/g, ''); response = await res.json();
if (response && 'status' in response) {
if (response.status !== 0) {
const t = await logErrorAndReturnT(response);
throw new Error((response as ErrorData).msg || t('errors:error'));
} else {
return (response as SuccessData<T>).data;
}
}
return response;
} else {
const data = await res.blob();
const disposition = res.headers.get('Content-Disposition');
// support safari
if (!data.arrayBuffer) {
data.arrayBuffer = async () =>
new Promise<ArrayBuffer>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener('load', e =>
e.target ? resolve(e.target.result as ArrayBuffer) : reject()
);
fileReader.readAsArrayBuffer(data);
});
}
let filename: string | null = null;
if (disposition && disposition.indexOf('attachment') !== -1) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
return {data, type: res.headers.get('Content-Type'), filename};
} }
} catch (e) {
const t = await logErrorAndReturnT(e);
throw new Error(t('errors:parse-error'));
} }
return {data, type: res.headers.get('Content-Type'), filename}; }
};
export const cycleFetcher = async <T = unknown>(urls: string[], options?: RequestInit): Promise<T[]> => { export const cycleFetcher = async <T = unknown>(urls: string[], options?: RequestInit): Promise<T[]> => {
return await Promise.all(urls.map(url => fetcher<T>(url, options))); return await Promise.all(urls.map(url => fetcher<T>(url, options)));
......
...@@ -19,6 +19,9 @@ i18n.use(initReactI18next) ...@@ -19,6 +19,9 @@ i18n.use(initReactI18next)
ns: 'common', ns: 'common',
defaultNS: 'common', defaultNS: 'common',
load: 'currentOnly', load: 'currentOnly',
react: {
useSuspense: false
},
interpolation: { interpolation: {
escapeValue: false escapeValue: false
}, },
......
...@@ -4,8 +4,6 @@ import crypto, {BinaryLike} from 'crypto'; ...@@ -4,8 +4,6 @@ import crypto, {BinaryLike} from 'crypto';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import {promises as fs} from 'fs'; import {promises as fs} from 'fs';
import mime from 'mime-types';
import mkdirp from 'mkdirp';
import path from 'path'; import path from 'path';
import querystring from 'querystring'; import querystring from 'querystring';
...@@ -110,6 +108,7 @@ export default class IO { ...@@ -110,6 +108,7 @@ export default class IO {
contentType: string, contentType: string,
options?: WriteOptions | WriteOptions['type'] options?: WriteOptions | WriteOptions['type']
) { ) {
const {default: mkdirp} = await import('mkdirp');
const type = 'string' === typeof options ? options : options?.type ?? 'json'; const type = 'string' === typeof options ? options : options?.type ?? 'json';
const fileDir = path.join(this.dataDir, IO.dataPath, filePath); const fileDir = path.join(this.dataDir, IO.dataPath, filePath);
...@@ -117,6 +116,7 @@ export default class IO { ...@@ -117,6 +116,7 @@ export default class IO {
let fileContent: Buffer; let fileContent: Buffer;
let extname: string; let extname: string;
if (type === 'buffer') { if (type === 'buffer') {
const {default: mime} = await import('mime-types');
extname = mime.extension(contentType) || ''; extname = mime.extension(contentType) || '';
if (extname) { if (extname) {
extname = '.' + extname; extname = '.' + extname;
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
"get-port": "5.1.1", "get-port": "5.1.1",
"mime-types": "2.1.27", "mime-types": "2.1.27",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"node-fetch": "2.6.0", "node-fetch": "2.6.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "9.0.0", "ts-node": "9.0.0",
"typescript": "4.0.2" "typescript": "4.0.2"
......
...@@ -26,7 +26,10 @@ export default (options?: Options) => { ...@@ -26,7 +26,10 @@ export default (options?: Options) => {
} }
if (!method) { if (!method) {
res.status(404).send({}); res.status(404).json({
status: 1,
msg: 'Method does not exist'
});
return; return;
} }
...@@ -61,7 +64,10 @@ export default (options?: Options) => { ...@@ -61,7 +64,10 @@ export default (options?: Options) => {
} }
} }
} catch (e) { } catch (e) {
res.status(500).send(e.message); res.status(500).json({
status: 1,
msg: e.message
});
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(e); console.error(e);
} }
......
...@@ -33,8 +33,12 @@ async function start() { ...@@ -33,8 +33,12 @@ async function start() {
}) })
); );
} else if (isDemo) { } else if (isDemo) {
const {default: demo} = await import('@visualdl/demo'); try {
app.use(apiUrl, demo); const {default: demo} = await import('@visualdl/demo');
app.use(apiUrl, demo);
} catch {
console.warn('Demo is not installed. Please rebuild server.');
}
} else if (isDev) { } else if (isDev) {
const {middleware: mock} = await import('@visualdl/mock'); const {middleware: mock} = await import('@visualdl/mock');
app.use(apiUrl, mock({delay: delay ? () => Math.random() * delay : 0})); app.use(apiUrl, mock({delay: delay ? () => Math.random() * delay : 0}));
......
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
], ],
"dependencies": { "dependencies": {
"@visualdl/core": "2.0.0", "@visualdl/core": "2.0.0",
"@visualdl/demo": "2.0.0",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"enhanced-resolve": "4.3.0", "enhanced-resolve": "4.3.0",
"express": "4.17.1", "express": "4.17.1",
...@@ -54,6 +53,9 @@ ...@@ -54,6 +53,9 @@
"ts-node": "9.0.0", "ts-node": "9.0.0",
"typescript": "4.0.2" "typescript": "4.0.2"
}, },
"optionalDependencies": {
"@visualdl/demo": "2.0.0"
},
"engines": { "engines": {
"node": ">=12", "node": ">=12",
"npm": ">=6" "npm": ">=6"
......
...@@ -28,3 +28,8 @@ export PATH=$PATH ...@@ -28,3 +28,8 @@ export PATH=$PATH
# yarn install # yarn install
yarn install --frozen-lockfile yarn install --frozen-lockfile
# re-install esbuild
# I don't know why...
# but this works in docker...
(cd node_modules/esbuild && rm -f stamp.txt && node install.js)
...@@ -9963,7 +9963,12 @@ node-fetch-npm@^2.0.2: ...@@ -9963,7 +9963,12 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0" json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
node-fetch@2.6.0, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0: node-fetch@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0:
version "2.6.0" version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
...@@ -13028,10 +13033,10 @@ strong-log-transformer@^2.0.0: ...@@ -13028,10 +13033,10 @@ strong-log-transformer@^2.0.0:
minimist "^1.2.0" minimist "^1.2.0"
through "^2.3.4" through "^2.3.4"
styled-components@5.1.1: styled-components@5.2.0:
version "5.1.1" version "5.2.0"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.2.0.tgz#6dcb5aa8a629c84b8d5ab34b7167e3e0c6f7ed74"
integrity sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg== integrity sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.0.0" "@babel/helper-module-imports" "^7.0.0"
"@babel/traverse" "^7.4.5" "@babel/traverse" "^7.4.5"
......
...@@ -98,7 +98,10 @@ def create_app(args): ...@@ -98,7 +98,10 @@ def create_app(args):
@app.route(public_path + '/') @app.route(public_path + '/')
def index(): def index():
return redirect(public_path + '/index', code=302) query_string = ''
if request.query_string:
query_string = '?' + request.query_string.decode()
return redirect(public_path + '/index' + query_string, code=302)
@app.route(public_path + '/<path:filename>') @app.route(public_path + '/<path:filename>')
def serve_static(filename): def serve_static(filename):
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册