From 6a3ef21cd85d7a32ded1ed6bbd3cd5aa83547435 Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Sat, 5 Sep 2020 18:00:47 +0800 Subject: [PATCH 1/8] build: update github actions --- .github/workflows/build.yml | 4 ++-- README-en.md | 2 +- README.md | 2 +- frontend/README.md | 2 +- frontend/README_cn.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81a8c4358..b1daf3d8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,8 +7,8 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.6', '3.7', '3.8'] - node-version: ['12', '14'] + python-version: ['3.5'] + node-version: ['12'] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/README-en.md b/README-en.md index 614ab798a..5a3339820 100644 --- a/README-en.md +++ b/README-en.md @@ -6,7 +6,7 @@

-Build Status +Build Status PyPI Downloads License diff --git a/README.md b/README.md index d8a4854de..45ba98fbf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

-Build Status +Build Status PyPI Downloads License diff --git a/frontend/README.md b/frontend/README.md index 4e9c54a23..34e115e5c 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,7 +4,7 @@

- Build Status + Build Status GitHub top language code style: prettier lerna diff --git a/frontend/README_cn.md b/frontend/README_cn.md index f2071d915..269b222a1 100644 --- a/frontend/README_cn.md +++ b/frontend/README_cn.md @@ -4,7 +4,7 @@

- Build Status + Build Status GitHub top language code style: prettier lerna From 8cad5ad5de82d77fbdd0b460322e76328d35872b Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Sun, 6 Sep 2020 13:50:44 +0800 Subject: [PATCH 2/8] chore: update dependencies --- frontend/packages/core/package.json | 2 +- frontend/packages/demo/package.json | 2 +- frontend/yarn.lock | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json index a429daac5..8a9c48a6c 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/demo/package.json b/frontend/packages/demo/package.json index dfa14c61d..04fa3e818 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/yarn.lock b/frontend/yarn.lock index b0b3a979a..f893b1b41 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" From a26d928823e3ab4ae96724d6681cf270db7d048f Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Sun, 6 Sep 2020 14:17:25 +0800 Subject: [PATCH 3/8] feat: set smoothing from query string --- frontend/packages/core/src/hooks/useQuery.ts | 12 ++++++++++++ frontend/packages/core/src/hooks/useTagFilter.ts | 6 ++---- .../packages/core/src/pages/high-dimensional.tsx | 6 ++---- frontend/packages/core/src/pages/scalar.tsx | 14 +++++++++++++- 4 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 frontend/packages/core/src/hooks/useQuery.ts diff --git a/frontend/packages/core/src/hooks/useQuery.ts b/frontend/packages/core/src/hooks/useQuery.ts new file mode 100644 index 000000000..f94c349f8 --- /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/useTagFilter.ts b/frontend/packages/core/src/hooks/useTagFilter.ts index 90f4f3f13..ebdcf8599 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/high-dimensional.tsx b/frontend/packages/core/src/pages/high-dimensional.tsx index a3b225be4..9370e3d9f 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/scalar.tsx b/frontend/packages/core/src/pages/scalar.tsx index 9e770100e..f3f5fc0da 100644 --- a/frontend/packages/core/src/pages/scalar.tsx +++ b/frontend/packages/core/src/pages/scalar.tsx @@ -16,9 +16,12 @@ import TimeModeSelect from '~/components/TimeModeSelect'; import Title from '~/components/Title'; import {rem} from '~/utils/style'; import styled from 'styled-components'; +import useQuery from '~/hooks/useQuery'; import useTagFilter from '~/hooks/useTagFilter'; import {useTranslation} from 'react-i18next'; +const DEFAULT_SMOOTHING = 0.6; + const TooltipSortingDiv = styled.div` margin-top: ${rem(20)}; display: flex; @@ -33,12 +36,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 smoothingFromQuery = useMemo(() => { + const parsedSmoothing = Number.parseFloat(String(query.smoothing)); + let smoothing = DEFAULT_SMOOTHING; + if (Number.isFinite(parsedSmoothing) && parsedSmoothing < 1 && parsedSmoothing >= 0) { + smoothing = Math.round(parsedSmoothing * 100) / 100; + } + return smoothing; + }, [query.smoothing]); + const [smoothing, setSmoothing] = useState(smoothingFromQuery); const [xAxis, setXAxis] = useState(XAxis.Step); From ae1ed3706c6963df4747f17a81a00b2464d340ea Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Sun, 6 Sep 2020 14:45:21 +0800 Subject: [PATCH 4/8] feat: remember smoothing in scalar page --- .../core/src/hooks/useLocalStorage.ts | 10 +++++++++ frontend/packages/core/src/pages/scalar.tsx | 21 ++++++++++--------- .../packages/core/src/resource/scalar/data.ts | 9 ++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 frontend/packages/core/src/hooks/useLocalStorage.ts diff --git a/frontend/packages/core/src/hooks/useLocalStorage.ts b/frontend/packages/core/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..676e81044 --- /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/pages/scalar.tsx b/frontend/packages/core/src/pages/scalar.tsx index f3f5fc0da..e7d0debbe 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,7 @@ 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'; @@ -42,15 +43,15 @@ const Scalar: FunctionComponent = () => { const {runs, tags, selectedRuns, onChangeRuns, loading} = useTagFilter('scalar', running); - const smoothingFromQuery = useMemo(() => { - const parsedSmoothing = Number.parseFloat(String(query.smoothing)); - let smoothing = DEFAULT_SMOOTHING; - if (Number.isFinite(parsedSmoothing) && parsedSmoothing < 1 && parsedSmoothing >= 0) { - smoothing = Math.round(parsedSmoothing * 100) / 100; + const [smoothingFromLocalStorage, setSmoothingFromLocalStorage] = useLocalStorage('scalar_smoothing'); + const parsedSmoothing = useMemo(() => { + if (query.smoothing != null) { + return parseSmoothing(query.smoothing); } - return smoothing; - }, [query.smoothing]); - const [smoothing, setSmoothing] = useState(smoothingFromQuery); + 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 fa7804cf3..fe47c3767 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; +}; From 490523bd5408ebe4c59dc3e085bfe58693365a53 Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Sun, 6 Sep 2020 20:27:49 +0800 Subject: [PATCH 5/8] chore: better error handling when fetching data --- .../core/public/locales/en/errors.json | 23 +++- .../core/public/locales/zh/errors.json | 23 +++- frontend/packages/core/src/App.tsx | 2 + .../packages/core/src/components/Audio.tsx | 5 +- .../packages/core/src/components/Error.tsx | 35 ++++-- .../packages/core/src/components/Image.tsx | 5 +- .../packages/core/src/components/Navbar.tsx | 2 +- .../packages/core/src/hooks/useNavItems.ts | 6 +- .../packages/core/src/hooks/useRequest.ts | 30 +++-- frontend/packages/core/src/pages/graph.tsx | 4 +- frontend/packages/core/src/pages/index.tsx | 33 ++++-- frontend/packages/core/src/utils/fetch.ts | 106 +++++++++++++----- frontend/packages/core/src/utils/i18n.ts | 3 + frontend/packages/mock/middleware.ts | 10 +- frontend/packages/server/index.ts | 8 +- frontend/packages/server/package.json | 4 +- visualdl/server/app.py | 5 +- 17 files changed, 226 insertions(+), 78 deletions(-) diff --git a/frontend/packages/core/public/locales/en/errors.json b/frontend/packages/core/public/locales/en/errors.json index 74d7b9669..9c60fc90d 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 dc4f45532..b7e888236 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 8c1dc8470..cac38867b 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -7,6 +7,7 @@ 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'; @@ -102,6 +103,7 @@ const App: FunctionComponent = () => { )} + ); diff --git a/frontend/packages/core/src/components/Audio.tsx b/frontend/packages/core/src/components/Audio.tsx index fc91d084f..d3d4e7946 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 adca7e90c..c4ee201bc 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/Image.tsx b/frontend/packages/core/src/components/Image.tsx index d775aa690..b78cb35ac 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 caaaa9bb3..27a5af91c 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/useNavItems.ts b/frontend/packages/core/src/hooks/useNavItems.ts index c05ee98fa..989c922d7 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/useRequest.ts b/frontend/packages/core/src/hooks/useRequest.ts index 96b2f3ac5..9922969a6 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/pages/graph.tsx b/frontend/packages/core/src/pages/graph.tsx index 46da33756..6982f8b7a 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/index.tsx b/frontend/packages/core/src/pages/index.tsx index 7ba1ac41a..846fc0413 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/utils/fetch.ts b/frontend/packages/core/src/utils/fetch.ts index 6968a67eb..244ff0a19 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 9105c0218..312427569 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/mock/middleware.ts b/frontend/packages/mock/middleware.ts index 453f48f72..f9fd9e34a 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 934d750a7..bf38abd0a 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 9cfd6dd1b..80637a821 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/visualdl/server/app.py b/visualdl/server/app.py index dec358e51..29ae6d26e 100644 --- a/visualdl/server/app.py +++ b/visualdl/server/app.py @@ -98,7 +98,10 @@ def favicon(): @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): From a0b1c94f06c0ee2aa3e3e30a3c1c62e9d51835c2 Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Sun, 6 Sep 2020 21:47:21 +0800 Subject: [PATCH 6/8] feat: add error page --- frontend/packages/core/src/App.tsx | 25 ++++++++++------ .../core/src/components/ErrorBoundary.tsx | 24 +++++++++++++++ frontend/packages/core/src/pages/error.tsx | 29 +++++++++++++++++++ frontend/packages/core/src/pages/scalar.tsx | 2 -- 4 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 frontend/packages/core/src/components/ErrorBoundary.tsx create mode 100644 frontend/packages/core/src/pages/error.tsx diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx index cac38867b..5b54584f0 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -3,6 +3,8 @@ 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'; @@ -57,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]); @@ -92,14 +94,19 @@ const App: FunctionComponent = () => {
    - }> - - - {routers.map(route => ( - - ))} - - + }> + }> + + + {routers.map(route => ( + + ))} + + + + + + )} diff --git a/frontend/packages/core/src/components/ErrorBoundary.tsx b/frontend/packages/core/src/components/ErrorBoundary.tsx new file mode 100644 index 000000000..1ca900fc3 --- /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/pages/error.tsx b/frontend/packages/core/src/pages/error.tsx new file mode 100644 index 000000000..6658c23a3 --- /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/scalar.tsx b/frontend/packages/core/src/pages/scalar.tsx index e7d0debbe..04076ec28 100644 --- a/frontend/packages/core/src/pages/scalar.tsx +++ b/frontend/packages/core/src/pages/scalar.tsx @@ -21,8 +21,6 @@ import useQuery from '~/hooks/useQuery'; import useTagFilter from '~/hooks/useTagFilter'; import {useTranslation} from 'react-i18next'; -const DEFAULT_SMOOTHING = 0.6; - const TooltipSortingDiv = styled.div` margin-top: ${rem(20)}; display: flex; From b376d52c08acbbda298ec4565d1ed4b803c48137 Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Mon, 7 Sep 2020 13:01:04 +0800 Subject: [PATCH 7/8] fix: build error in docker --- Dockerfile.demo | 5 +++-- frontend/scripts/install.sh | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile.demo b/Dockerfile.demo index 49a0909af..9cc4d357c 100644 --- a/Dockerfile.demo +++ b/Dockerfile.demo @@ -11,8 +11,10 @@ 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"] +# re-install esbuild +# I don't know why... +RUN cd node_modules/esbuild && rm -f stamp.txt && node install.js RUN ["./scripts/build.sh"] @@ -21,7 +23,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/scripts/install.sh b/frontend/scripts/install.sh index 8312c88ec..b70e7506e 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) From ad3f0627a98f851ff552fcd8faf92b785061acd4 Mon Sep 17 00:00:00 2001 From: Peter Pan Date: Mon, 7 Sep 2020 14:27:27 +0800 Subject: [PATCH 8/8] fix: build error in docker --- Dockerfile.demo | 3 --- frontend/packages/demo/builder/io.ts | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Dockerfile.demo b/Dockerfile.demo index 9cc4d357c..e1ac818fa 100644 --- a/Dockerfile.demo +++ b/Dockerfile.demo @@ -12,9 +12,6 @@ WORKDIR /home/visualdl/frontend ENV SCOPE server ENV PUBLIC_PATH /paddle/visualdl/demo RUN ["./scripts/install.sh"] -# re-install esbuild -# I don't know why... -RUN cd node_modules/esbuild && rm -f stamp.txt && node install.js RUN ["./scripts/build.sh"] diff --git a/frontend/packages/demo/builder/io.ts b/frontend/packages/demo/builder/io.ts index df1801686..e3f0d115e 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;