diff --git a/Dockerfile.demo b/Dockerfile.demo
index 49a0909afa9b6bbcfdafd74162a44775588f82bb..e1ac818fa935accc29e57fa6ba23486b2d21bcec 100644
--- a/Dockerfile.demo
+++ b/Dockerfile.demo
@@ -11,7 +11,6 @@ RUN ["pip", "install", "--disable-pip-version-check", "--find-links=dist", "visu
WORKDIR /home/visualdl/frontend
ENV SCOPE server
ENV PUBLIC_PATH /paddle/visualdl/demo
-ENV API_URL /paddle/visualdl/demo/api
RUN ["./scripts/install.sh"]
RUN ["./scripts/build.sh"]
@@ -21,7 +20,6 @@ WORKDIR /home/visualdl
COPY --from=builder /home/visualdl/frontend/ ./
ENV NODE_ENV production
ENV PUBLIC_PATH /paddle/visualdl/demo
-ENV API_URL /paddle/visualdl/demo/api
ENV PING_URL /ping
ENV DEMO true
ENV HOST 0.0.0.0
diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json
index a429daac506639450989aea612295cc576d5c6e8..8a9c48a6c605ad15bf2ae0b88dd80f0d5fba72c4 100644
--- a/frontend/packages/core/package.json
+++ b/frontend/packages/core/package.json
@@ -61,7 +61,7 @@
"react-router-dom": "5.2.0",
"react-spinners": "0.9.0",
"react-toastify": "6.0.8",
- "styled-components": "5.1.1",
+ "styled-components": "5.2.0",
"swr": "0.3.0",
"tippy.js": "6.2.6"
},
diff --git a/frontend/packages/core/public/locales/en/errors.json b/frontend/packages/core/public/locales/en/errors.json
index 74d7b9669d0f399957097ad08d2fca20c2679600..9c60fc90d502061a642ec2785a9044035807ac5b 100644
--- a/frontend/packages/core/public/locales/en/errors.json
+++ b/frontend/packages/core/public/locales/en/errors.json
@@ -7,7 +7,24 @@
"description": "Possible reasons are:",
"title": "No visualized data."
},
- "error-with-status": "A {{statusCode}} error occurred on server",
- "error-without-status": "An error occurred on the server",
- "page-not-found": "Page Not Found"
+ "error": "Error occurred",
+ "network-error": "Network Error",
+ "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"
+ }
}
diff --git a/frontend/packages/core/public/locales/zh/errors.json b/frontend/packages/core/public/locales/zh/errors.json
index dc4f4553289c411f38b2b628b0abe1ca87d2d7a0..b7e888236d7bd892a0baced7d11ff22a04b5ea08 100644
--- a/frontend/packages/core/public/locales/zh/errors.json
+++ b/frontend/packages/core/public/locales/zh/errors.json
@@ -7,7 +7,24 @@
"description": "有以下几种可能原因,请您参考相应解决方案:",
"title": "无可视化结果展示"
},
- "error-with-status": "服务器发生了一个 {{statusCode}} 错误",
- "error-without-status": "服务器发生了一个错误",
- "page-not-found": "页面不存在"
+ "error": "发生错误",
+ "network-error": "网络错误",
+ "page-not-found": "页面不存在",
+ "parse-error": "解析失败",
+ "response-error": {
+ "400": "请求错误",
+ "401": "未授权",
+ "403": "没有权限",
+ "404": "接口不存在",
+ "405": "方法不允许",
+ "408": "请求超时",
+ "413": "请求体过大",
+ "414": "请求地址过长",
+ "500": "服务器内部错误",
+ "501": "服务器未实现",
+ "502": "服务器网关错误",
+ "503": "服务器不可用",
+ "504": "服务器网关超时",
+ "unknown": "服务器错误"
+ }
}
diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx
index 8c1dc84702925f3fa8a28719d06c3468171fdf73..5b54584f091a819183ac08ac70992214279f8257 100644
--- a/frontend/packages/core/src/App.tsx
+++ b/frontend/packages/core/src/App.tsx
@@ -3,10 +3,13 @@ import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'rea
import {headerHeight, position, size} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading';
+import ErrorBoundary from '~/components/ErrorBoundary';
+import ErrorPage from '~/pages/error';
import {Helmet} from 'react-helmet';
import NProgress from 'nprogress';
import Navbar from '~/components/Navbar';
import {SWRConfig} from 'swr';
+import {ToastContainer} from 'react-toastify';
import {fetcher} from '~/utils/fetch';
import init from '@visualdl/wasm';
import routes from '~/routes';
@@ -56,7 +59,7 @@ const Telemetry: FunctionComponent = () => {
};
const App: FunctionComponent = () => {
- const {i18n} = useTranslation();
+ const {t, i18n} = useTranslation('errors');
const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]);
@@ -91,17 +94,23 @@ const App: FunctionComponent = () => {
- }>
-
-
- {routers.map(route => (
-
- ))}
-
-
+ }>
+ }>
+
+
+ {routers.map(route => (
+
+ ))}
+
+
+
+
+
+
)}
+
);
diff --git a/frontend/packages/core/src/components/Audio.tsx b/frontend/packages/core/src/components/Audio.tsx
index fc91d084f16f7ff57905c954fbf1ff4816cc6537..d3d4e7946b451193921fd7a9a89ccbb6f5bb9669 100644
--- a/frontend/packages/core/src/components/Audio.tsx
+++ b/frontend/packages/core/src/components/Audio.tsx
@@ -1,4 +1,3 @@
-import {BlobResponse, blobFetcher} from '~/utils/fetch';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {
WithStyled,
@@ -13,12 +12,14 @@ import {
} from '~/utils/style';
import {AudioPlayer} from '~/utils/audio';
+import type {BlobResponse} from '~/utils/fetch';
import Icon from '~/components/Icon';
import PuffLoader from 'react-spinners/PuffLoader';
import RangeSlider from '~/components/RangeSlider';
import Slider from 'react-rangeslider';
import SyncLoader from 'react-spinners/SyncLoader';
import Tippy from '@tippyjs/react';
+import {fetcher} from '~/utils/fetch';
import mime from 'mime-types';
import moment from 'moment';
import {saveAs} from 'file-saver';
@@ -147,7 +148,7 @@ const Audio = React.forwardRef(
({audioContext, src, cache, onLoading, onLoad, className}, ref) => {
const {t} = useTranslation('common');
- const {data, error, loading} = useRequest(src ?? null, blobFetcher, {
+ const {data, error, loading} = useRequest(src ?? null, fetcher, {
dedupingInterval: cache ?? 2000
});
diff --git a/frontend/packages/core/src/components/Error.tsx b/frontend/packages/core/src/components/Error.tsx
index adca7e90cec13397cc03066b43a716a36c989881..c4ee201bce67570bebf80bac97a503cf2fcdf29e 100644
--- a/frontend/packages/core/src/components/Error.tsx
+++ b/frontend/packages/core/src/components/Error.tsx
@@ -46,6 +46,29 @@ const Wrapper = styled.div`
const reload = () => window.location.reload();
+const ReadmeMap: Record = {
+ zh: 'https://github.com/PaddlePaddle/VisualDL/blob/develop/README.md',
+ en: 'https://github.com/PaddlePaddle/VisualDL/blob/develop/README-en.md'
+};
+
+const UserGuideMap: Record = {
+ 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}> = ({map, children}) => {
+ const {i18n} = useTranslation();
+ return (
+
+ {children}
+
+ );
+};
+
const Error: FunctionComponent = ({className, children}) => {
const {t} = useTranslation('errors');
@@ -61,22 +84,14 @@ const Error: FunctionComponent = ({className, children}) => {
Log files are not generated. Please refer to
-
- README
-
+ README
to create log files.
Log files are generated but data is not written yet. Please refer to
-
- VisualDL User Guide
-
+ VisualDL User Guide
to write visualized data.
diff --git a/frontend/packages/core/src/components/ErrorBoundary.tsx b/frontend/packages/core/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1ca900fc3cfbfab21a8b1df6205c7060943965aa
--- /dev/null
+++ b/frontend/packages/core/src/components/ErrorBoundary.tsx
@@ -0,0 +1,24 @@
+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;
diff --git a/frontend/packages/core/src/components/Image.tsx b/frontend/packages/core/src/components/Image.tsx
index d775aa69054c2674be4add060d16d52210f9659c..b78cb35accdefcf2c7ca8c25a32286ad0a506bc8 100644
--- a/frontend/packages/core/src/components/Image.tsx
+++ b/frontend/packages/core/src/components/Image.tsx
@@ -1,8 +1,9 @@
-import {BlobResponse, blobFetcher} from '~/utils/fetch';
import React, {useImperativeHandle, useLayoutEffect, useState} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
+import type {BlobResponse} from '~/utils/fetch';
import GridLoader from 'react-spinners/GridLoader';
+import {fetcher} from '~/utils/fetch';
import mime from 'mime-types';
import {saveAs} from 'file-saver';
import useRequest from '~/hooks/useRequest';
@@ -21,7 +22,7 @@ const Image = React.forwardRef(({src, cache,
const {t} = useTranslation('common');
const [url, setUrl] = useState('');
- const {data, error, loading} = useRequest(src ?? null, blobFetcher, {
+ const {data, error, loading} = useRequest(src ?? null, fetcher, {
dedupingInterval: cache ?? 2000
});
diff --git a/frontend/packages/core/src/components/Navbar.tsx b/frontend/packages/core/src/components/Navbar.tsx
index caaaa9bb33424438d0a18847fde41f72b54fa5f3..27a5af91ceb7373f05276ff5a1972e40a1d15e4e 100644
--- a/frontend/packages/core/src/components/Navbar.tsx
+++ b/frontend/packages/core/src/components/Navbar.tsx
@@ -196,7 +196,7 @@ const Navbar: FunctionComponent = () => {
const currentPath = useMemo(() => pathname.replace(PUBLIC_PATH, ''), [pathname]);
- const navItems = useNavItems();
+ const [navItems] = useNavItems();
const [items, setItems] = useState([]);
useEffect(() => {
setItems(oldItems =>
diff --git a/frontend/packages/core/src/hooks/useLocalStorage.ts b/frontend/packages/core/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..676e810445d660a5002388da104ef819bef84503
--- /dev/null
+++ b/frontend/packages/core/src/hooks/useLocalStorage.ts
@@ -0,0 +1,10 @@
+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;
diff --git a/frontend/packages/core/src/hooks/useNavItems.ts b/frontend/packages/core/src/hooks/useNavItems.ts
index c05ee98fab7be0479034e48081c95978fc6259e2..989c922d76df2b0de4c5f207d26fc5d4a3b00dc9 100644
--- a/frontend/packages/core/src/hooks/useNavItems.ts
+++ b/frontend/packages/core/src/hooks/useNavItems.ts
@@ -18,7 +18,7 @@ export const navMap = {
const useNavItems = () => {
const [components, setComponents] = useState([]);
- 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,
dedupingInterval: 14 * 1000,
errorRetryInterval: 15 * 1000,
@@ -59,9 +59,9 @@ const useNavItems = () => {
useEffect(() => {
setComponents(filterPages(routes));
- }, [data, filterPages]);
+ }, [filterPages]);
- return components;
+ return [components, loading, error] as const;
};
export default useNavItems;
diff --git a/frontend/packages/core/src/hooks/useQuery.ts b/frontend/packages/core/src/hooks/useQuery.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f94c349f82db1aa495f1c2251bd4bd6864ae6d01
--- /dev/null
+++ b/frontend/packages/core/src/hooks/useQuery.ts
@@ -0,0 +1,12 @@
+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;
diff --git a/frontend/packages/core/src/hooks/useRequest.ts b/frontend/packages/core/src/hooks/useRequest.ts
index 96b2f3ac551784f330f785e90df7f069f0f654c5..9922969a6e644f083d07c6bad571e7d3cc3b2bf0 100644
--- a/frontend/packages/core/src/hooks/useRequest.ts
+++ b/frontend/packages/core/src/hooks/useRequest.ts
@@ -1,43 +1,55 @@
+import type {ConfigInterface, keyInterface, responseInterface} from 'swr';
import {useEffect, useMemo} from 'react';
-import useSWR, {ConfigInterface, keyInterface, responseInterface} from 'swr';
import ee from '~/utils/event';
import type {fetcherFn} from 'swr/dist/types';
+import {toast} from 'react-toastify';
+import useSWR from 'swr';
type Response = responseInterface & {
loading: boolean;
};
-function useRequest(key: keyInterface): Response;
-function useRequest(key: keyInterface, fetcher?: fetcherFn): Response;
-function useRequest(
+function useRequest(key: keyInterface): Response;
+function useRequest(key: keyInterface, fetcher?: fetcherFn): Response;
+function useRequest(
key: keyInterface,
fetcher?: fetcherFn,
config?: ConfigInterface>
): Response;
-function useRequest(
+function useRequest(
key: keyInterface,
fetcher?: fetcherFn,
config?: ConfigInterface>
): Response {
const {data, error, ...other} = useSWR(key, fetcher, config);
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};
}
-function useRunningRequest(key: keyInterface, running: boolean): Response;
-function useRunningRequest(
+function useRunningRequest(key: keyInterface, running: boolean): Response;
+function useRunningRequest(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn
): Response;
-function useRunningRequest(
+function useRunningRequest(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn,
config?: Omit>, 'dedupingInterval' | 'errorRetryInterval'>
): Response;
-function useRunningRequest(
+function useRunningRequest(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn,
diff --git a/frontend/packages/core/src/hooks/useTagFilter.ts b/frontend/packages/core/src/hooks/useTagFilter.ts
index 90f4f3f13599e6c47efd2bae30119df7f53138ef..ebdcf859980566eb58aeef73110e6156e3fdd46a 100644
--- a/frontend/packages/core/src/hooks/useTagFilter.ts
+++ b/frontend/packages/core/src/hooks/useTagFilter.ts
@@ -4,10 +4,9 @@ import {useCallback, useEffect, useMemo, useReducer} from 'react';
import groupBy from 'lodash/groupBy';
import intersectionBy from 'lodash/intersectionBy';
-import queryString from 'query-string';
import uniq from 'lodash/uniq';
import useGlobalState from '~/hooks/useGlobalState';
-import {useLocation} from 'react-router-dom';
+import useQuery from '~/hooks/useQuery';
import {useRunningRequest} from '~/hooks/useRequest';
type Tags = Record;
@@ -148,8 +147,7 @@ const reducer = (state: State, action: Action): State => {
// TODO: refactor to improve performance
const useTagFilter = (type: string, running: boolean) => {
- const location = useLocation();
- const query = useMemo(() => queryString.parse(location.search), [location.search]);
+ const query = useQuery();
const {data, loading, error} = useRunningRequest(`/${type}/tags`, running);
diff --git a/frontend/packages/core/src/pages/error.tsx b/frontend/packages/core/src/pages/error.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6658c23a37918a25a2dda72dd6663a66941a9538
--- /dev/null
+++ b/frontend/packages/core/src/pages/error.tsx
@@ -0,0 +1,29 @@
+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 = ({title, desc, children}) => {
+ const {t} = useTranslation('errors');
+
+ return (
+
+
+ {children || (
+ <>
+ {title ?? t('errors:error')}
+ {desc}
+ >
+ )}
+
+
+ );
+};
+
+export default Error;
diff --git a/frontend/packages/core/src/pages/graph.tsx b/frontend/packages/core/src/pages/graph.tsx
index 46da337560b8058970bb1ed5452f8ca4db298234..6982f8b7a7114e9d6b356701534b7a63a1525d30 100644
--- a/frontend/packages/core/src/pages/graph.tsx
+++ b/frontend/packages/core/src/pages/graph.tsx
@@ -1,11 +1,11 @@
import Aside, {AsideSection} from '~/components/Aside';
-import {BlobResponse, blobFetcher} from '~/utils/fetch';
import type {Documentation, OpenedResult, Properties, SearchItem, SearchResult} from '~/resource/graph/types';
import GraphComponent, {GraphRef} from '~/components/GraphPage/Graph';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import Select, {SelectProps} from '~/components/Select';
import {primaryColor, rem, size} from '~/utils/style';
+import type {BlobResponse} from '~/utils/fetch';
import Button from '~/components/Button';
import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content';
@@ -93,7 +93,7 @@ const Graph: FunctionComponent = () => {
[globalDispatch]
);
- const {data, loading} = useRequest(files ? null : '/graph/graph', blobFetcher);
+ const {data, loading} = useRequest(files ? null : '/graph/graph');
useEffect(() => {
if (data?.data.size) {
diff --git a/frontend/packages/core/src/pages/high-dimensional.tsx b/frontend/packages/core/src/pages/high-dimensional.tsx
index a3b225be4d5650fbb309c5f24d3f18621dafa55b..9370e3d9f77c51285a4c3a375da9df7cd090d8bc 100644
--- a/frontend/packages/core/src/pages/high-dimensional.tsx
+++ b/frontend/packages/core/src/pages/high-dimensional.tsx
@@ -16,9 +16,8 @@ import RunningToggle from '~/components/RunningToggle';
import SearchInput from '~/components/SearchInput';
import type {TagsData} from '~/types';
import Title from '~/components/Title';
-import queryString from 'query-string';
import styled from 'styled-components';
-import {useLocation} from 'react-router-dom';
+import useQuery from '~/hooks/useQuery';
import {useRunningRequest} from '~/hooks/useRequest';
import useSearchValue from '~/hooks/useSearchValue';
import {useTranslation} from 'react-i18next';
@@ -66,8 +65,7 @@ const HighDimensional: FunctionComponent = () => {
}, [data]);
const labelList = useMemo(() => list.map(item => item.label), [list]);
- const location = useLocation();
- const query = useMemo(() => queryString.parse(location.search), [location]);
+ const query = useQuery();
const selectedLabel = useMemo(() => {
const run = Array.isArray(query.run) ? query.run[0] : query.run;
return (run && list.find(item => item.run === run)?.label) ?? list[0]?.label;
diff --git a/frontend/packages/core/src/pages/index.tsx b/frontend/packages/core/src/pages/index.tsx
index 7ba1ac41a9e79999758127804b98ea420285aaef..846fc04135beea4a683351f479b2fdbd949d1f57 100644
--- a/frontend/packages/core/src/pages/index.tsx
+++ b/frontend/packages/core/src/pages/index.tsx
@@ -1,45 +1,56 @@
import React, {FunctionComponent, useEffect} from 'react';
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 styled from 'styled-components';
-import {useHistory} from 'react-router-dom';
import useNavItems from '~/hooks/useNavItems';
import {useTranslation} from 'react-i18next';
-const Loading = styled.div`
+const CenterWrapper = styled.div`
${size(`calc(100vh - ${headerHeight})`, '100vw')}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overscroll-behavior: none;
- cursor: progress;
+`;
+
+const Loading = styled.div`
font-size: ${rem(16)};
line-height: ${rem(60)};
`;
const IndexPage: FunctionComponent = () => {
- const navItems = useNavItems();
+ const [navItems, loading] = useNavItems();
const history = useHistory();
const {t} = useTranslation('common');
+ const location = useLocation();
+
useEffect(() => {
if (navItems.length) {
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) {
- history.replace(navItems[0].children[0].path);
+ history.replace(navItems[0].children[0].path + location.search);
}
}
- }, [navItems, history]);
+ }, [navItems, history, location.search]);
return (
-
-
- {t('common:loading')}
-
+
+ {loading || navItems.length ? (
+
+
+ {t('common:loading')}
+
+ ) : (
+
+ )}
+
);
};
diff --git a/frontend/packages/core/src/pages/scalar.tsx b/frontend/packages/core/src/pages/scalar.tsx
index 9e770100e3b1a750f7a32d12b3e1f36afe1447f6..04076ec28b5d117ef400ffc8ab0b46ec5aa5acbf 100644
--- a/frontend/packages/core/src/pages/scalar.tsx
+++ b/frontend/packages/core/src/pages/scalar.tsx
@@ -1,6 +1,6 @@
import ChartPage, {WithChart} from '~/components/ChartPage';
-import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
-import {SortingMethod, XAxis, sortingMethod as toolTipSortingValues} from '~/resource/scalar';
+import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
+import {SortingMethod, XAxis, parseSmoothing, sortingMethod as toolTipSortingValues} from '~/resource/scalar';
import {AsideSection} from '~/components/Aside';
import Checkbox from '~/components/Checkbox';
@@ -16,6 +16,8 @@ import TimeModeSelect from '~/components/TimeModeSelect';
import Title from '~/components/Title';
import {rem} from '~/utils/style';
import styled from 'styled-components';
+import useLocalStorage from '~/hooks/useLocalStorage';
+import useQuery from '~/hooks/useQuery';
import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next';
@@ -33,12 +35,21 @@ const TooltipSortingDiv = styled.div`
const Scalar: FunctionComponent = () => {
const {t} = useTranslation(['scalar', 'common']);
+ const query = useQuery();
const [running, setRunning] = useState(true);
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.Step);
diff --git a/frontend/packages/core/src/resource/scalar/data.ts b/frontend/packages/core/src/resource/scalar/data.ts
index fa7804cf3ac1ac8d05f122e71eaa365cdc25654b..fe47c37673b80f2d12a4a0126883a55f2987cb1b 100644
--- a/frontend/packages/core/src/resource/scalar/data.ts
+++ b/frontend/packages/core/src/resource/scalar/data.ts
@@ -123,3 +123,12 @@ export const nearestPoint = (data: Dataset[], runs: Run[], step: number) =>
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;
+};
diff --git a/frontend/packages/core/src/utils/fetch.ts b/frontend/packages/core/src/utils/fetch.ts
index 6968a67eb20841d5a8a3aecc71e12c3184f9a3ea..244ff0a195a2308d0d5fca7e6f502338a2453f97 100644
--- a/frontend/packages/core/src/utils/fetch.ts
+++ b/frontend/packages/core/src/utils/fetch.ts
@@ -1,3 +1,5 @@
+import type {TFunction} from 'i18next';
+import i18next from 'i18next';
import queryString from 'query-string';
const API_TOKEN_KEY: string = import.meta.env.SNOWPACK_PUBLIC_API_TOKEN_KEY;
@@ -29,12 +31,17 @@ function addApiToken(options?: RequestInit): RequestInit | undefined {
};
}
-export const fetcher = async (url: string, options?: RequestInit): Promise => {
- const res = await fetch(API_URL + url, addApiToken(options));
- const response = await res.json();
+interface SuccessData {
+ status: 0;
+ data: D;
+}
- return response && 'data' in response ? response.data : response;
-};
+interface ErrorData {
+ status: number;
+ msg?: string;
+}
+
+type Data = SuccessData | ErrorData;
export type BlobResponse = {
data: Blob;
@@ -42,30 +49,77 @@ export type BlobResponse = {
filename: string | null;
};
-export const blobFetcher = async (url: string, options?: RequestInit): Promise => {
- const res = await fetch(API_URL + url, addApiToken(options));
- const data = await res.blob();
- const disposition = res.headers.get('Content-Disposition');
- // support safari
- if (!data.arrayBuffer) {
- data.arrayBuffer = async () =>
- new Promise((resolve, reject) => {
- const fileReader = new FileReader();
- fileReader.addEventListener('load', e =>
- e.target ? resolve(e.target.result as ArrayBuffer) : reject()
- );
- fileReader.readAsArrayBuffer(data);
- });
+function getT(): Promise {
+ return new Promise(resolve => {
+ // Bug of i18next
+ i18next.changeLanguage((undefined as unknown) as string).then(t => resolve(t));
+ });
+}
+
+function logErrorAndReturnT(e: unknown) {
+ if (import.meta.env.MODE === 'development') {
+ console.error(e); // eslint-disable-line no-console
+ }
+ return getT();
+}
+
+export function fetcher(url: string, options?: RequestInit): Promise;
+export function fetcher(url: string, options?: RequestInit): Promise;
+export async function fetcher(url: string, options?: RequestInit): Promise {
+ 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) {
- const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
- if (matches != null && matches[1]) {
- filename = matches[1].replace(/['"]/g, '');
+
+ let response: Data | T;
+ try {
+ if (res.headers.get('content-type')?.includes('application/json')) {
+ 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).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((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 (urls: string[], options?: RequestInit): Promise => {
return await Promise.all(urls.map(url => fetcher(url, options)));
diff --git a/frontend/packages/core/src/utils/i18n.ts b/frontend/packages/core/src/utils/i18n.ts
index 9105c0218023f8a6ea2d66c67e6e249aa23c079c..312427569aa321975a71a296a418f4715ed3d7d4 100644
--- a/frontend/packages/core/src/utils/i18n.ts
+++ b/frontend/packages/core/src/utils/i18n.ts
@@ -19,6 +19,9 @@ i18n.use(initReactI18next)
ns: 'common',
defaultNS: 'common',
load: 'currentOnly',
+ react: {
+ useSuspense: false
+ },
interpolation: {
escapeValue: false
},
diff --git a/frontend/packages/demo/builder/io.ts b/frontend/packages/demo/builder/io.ts
index df1801686c700d1c5da4901c07b471c949cf22b1..e3f0d115ee80060581b003ebc2b18876c11ad55e 100644
--- a/frontend/packages/demo/builder/io.ts
+++ b/frontend/packages/demo/builder/io.ts
@@ -4,8 +4,6 @@ import crypto, {BinaryLike} from 'crypto';
import fetch from 'node-fetch';
import {promises as fs} from 'fs';
-import mime from 'mime-types';
-import mkdirp from 'mkdirp';
import path from 'path';
import querystring from 'querystring';
@@ -110,6 +108,7 @@ export default class IO {
contentType: string,
options?: WriteOptions | WriteOptions['type']
) {
+ const {default: mkdirp} = await import('mkdirp');
const type = 'string' === typeof options ? options : options?.type ?? 'json';
const fileDir = path.join(this.dataDir, IO.dataPath, filePath);
@@ -117,6 +116,7 @@ export default class IO {
let fileContent: Buffer;
let extname: string;
if (type === 'buffer') {
+ const {default: mime} = await import('mime-types');
extname = mime.extension(contentType) || '';
if (extname) {
extname = '.' + extname;
diff --git a/frontend/packages/demo/package.json b/frontend/packages/demo/package.json
index dfa14c61d7c8431178a42eb1f4804f642f202bea..04fa3e81804bc411d889f6fcd5e43337174f500e 100644
--- a/frontend/packages/demo/package.json
+++ b/frontend/packages/demo/package.json
@@ -41,7 +41,7 @@
"get-port": "5.1.1",
"mime-types": "2.1.27",
"mkdirp": "1.0.4",
- "node-fetch": "2.6.0",
+ "node-fetch": "2.6.1",
"rimraf": "3.0.2",
"ts-node": "9.0.0",
"typescript": "4.0.2"
diff --git a/frontend/packages/mock/middleware.ts b/frontend/packages/mock/middleware.ts
index 453f48f7228d87c16634538f58f440095f613eb4..f9fd9e34af6656e08209d115e357f360566e2005 100644
--- a/frontend/packages/mock/middleware.ts
+++ b/frontend/packages/mock/middleware.ts
@@ -26,7 +26,10 @@ export default (options?: Options) => {
}
if (!method) {
- res.status(404).send({});
+ res.status(404).json({
+ status: 1,
+ msg: 'Method does not exist'
+ });
return;
}
@@ -61,7 +64,10 @@ export default (options?: Options) => {
}
}
} catch (e) {
- res.status(500).send(e.message);
+ res.status(500).json({
+ status: 1,
+ msg: e.message
+ });
// eslint-disable-next-line no-console
console.error(e);
}
diff --git a/frontend/packages/server/index.ts b/frontend/packages/server/index.ts
index 934d750a7e843f76121f2f33bb94a6cd1431157d..bf38abd0adaeb25ae72ee7346b8f3210a8452443 100644
--- a/frontend/packages/server/index.ts
+++ b/frontend/packages/server/index.ts
@@ -33,8 +33,12 @@ async function start() {
})
);
} else if (isDemo) {
- const {default: demo} = await import('@visualdl/demo');
- app.use(apiUrl, demo);
+ try {
+ const {default: demo} = await import('@visualdl/demo');
+ app.use(apiUrl, demo);
+ } catch {
+ console.warn('Demo is not installed. Please rebuild server.');
+ }
} else if (isDev) {
const {middleware: mock} = await import('@visualdl/mock');
app.use(apiUrl, mock({delay: delay ? () => Math.random() * delay : 0}));
diff --git a/frontend/packages/server/package.json b/frontend/packages/server/package.json
index 9cfd6dd1bc4eff05dd2b053f0ed8c8aa502c5f07..80637a8217a6f58a80d409a6db87504c4ba67796 100644
--- a/frontend/packages/server/package.json
+++ b/frontend/packages/server/package.json
@@ -37,7 +37,6 @@
],
"dependencies": {
"@visualdl/core": "2.0.0",
- "@visualdl/demo": "2.0.0",
"dotenv": "8.2.0",
"enhanced-resolve": "4.3.0",
"express": "4.17.1",
@@ -54,6 +53,9 @@
"ts-node": "9.0.0",
"typescript": "4.0.2"
},
+ "optionalDependencies": {
+ "@visualdl/demo": "2.0.0"
+ },
"engines": {
"node": ">=12",
"npm": ">=6"
diff --git a/frontend/scripts/install.sh b/frontend/scripts/install.sh
index 8312c88ec67909a7d520e7af41001d3babae801c..b70e7506ef68df05e6d9512db2d58fa1a404fe27 100755
--- a/frontend/scripts/install.sh
+++ b/frontend/scripts/install.sh
@@ -28,3 +28,8 @@ export PATH=$PATH
# yarn install
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)
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index b0b3a979ac94f3957a680717d78cea79adea2c1d..f893b1b416fd3842e0749baffcf521557c4a4a26 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -9963,7 +9963,12 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
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"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
@@ -13028,10 +13033,10 @@ strong-log-transformer@^2.0.0:
minimist "^1.2.0"
through "^2.3.4"
-styled-components@5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d"
- integrity sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg==
+styled-components@5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.2.0.tgz#6dcb5aa8a629c84b8d5ab34b7167e3e0c6f7ed74"
+ integrity sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/traverse" "^7.4.5"
diff --git a/visualdl/server/app.py b/visualdl/server/app.py
index dec358e51ad549bd400c67a77b3d9234614e8fe2..29ae6d26e9ffc82984d51074e8dd89145a94ea8b 100644
--- a/visualdl/server/app.py
+++ b/visualdl/server/app.py
@@ -98,7 +98,10 @@ def create_app(args):
@app.route(public_path + '/')
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 + '/')
def serve_static(filename):