未验证 提交 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
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
......
......@@ -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"
},
......
......@@ -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"
}
}
......@@ -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": "服务器错误"
}
}
......@@ -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 = () => {
<Header>
<Navbar />
</Header>
<Suspense fallback={<Progress />}>
<Switch>
<Redirect exact from="/" to={defaultRoute?.path ?? '/index'} />
{routers.map(route => (
<Route key={route.id} path={route.path} component={route.component} />
))}
</Switch>
</Suspense>
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Progress />}>
<Switch>
<Redirect exact from="/" to={defaultRoute?.path ?? '/index'} />
{routers.map(route => (
<Route key={route.id} path={route.path} component={route.component} />
))}
<Route path="*">
<ErrorPage title={t('errors:page-not-found')} />
</Route>
</Switch>
</Suspense>
</ErrorBoundary>
</Router>
</Main>
)}
<ToastContainer />
</SWRConfig>
</div>
);
......
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<AudioRef, AudioProps & WithStyled>(
({audioContext, src, cache, onLoading, onLoad, className}, ref) => {
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
});
......
......@@ -46,6 +46,29 @@ const Wrapper = styled.div`
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 {t} = useTranslation('errors');
......@@ -61,22 +84,14 @@ const Error: FunctionComponent<WithStyled> = ({className, children}) => {
<li>
<Trans i18nKey="errors:common.1">
Log files are not generated. Please refer to&nbsp;
<a href="https://github.com/PaddlePaddle/VisualDL" target="_blank" rel="noreferrer">
README
</a>
<I18nLink map={ReadmeMap}>README</I18nLink>
&nbsp;to create log files.
</Trans>
</li>
<li>
<Trans i18nKey="errors:common.2">
Log files are generated but data is not written yet. Please refer to&nbsp;
<a
href="https://github.com/PaddlePaddle/VisualDL/blob/develop/docs/components/README.md"
target="_blank"
rel="noreferrer"
>
VisualDL User Guide
</a>
<I18nLink map={UserGuideMap}>VisualDL User Guide</I18nLink>
&nbsp;to write visualized data.
</Trans>
</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 {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<ImageRef, ImageProps & WithStyled>(({src, cache,
const {t} = useTranslation('common');
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
});
......
......@@ -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<NavbarItemProps[]>([]);
useEffect(() => {
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 = {
const useNavItems = () => {
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,
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;
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 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<D, E> = responseInterface<D, E> & {
loading: boolean;
};
function useRequest<D = unknown, E = unknown>(key: keyInterface): Response<D, E>;
function useRequest<D = unknown, E = unknown>(key: keyInterface, fetcher?: fetcherFn<D>): Response<D, E>;
function useRequest<D = unknown, E = unknown>(
function useRequest<D = unknown, E extends Error = Error>(key: keyInterface): Response<D, E>;
function useRequest<D = unknown, E extends Error = Error>(key: keyInterface, fetcher?: fetcherFn<D>): Response<D, E>;
function useRequest<D = unknown, E extends Error = Error>(
key: keyInterface,
fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E>;
function useRequest<D = unknown, E = unknown>(
function useRequest<D = unknown, E extends Error = Error>(
key: keyInterface,
fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E> {
const {data, error, ...other} = useSWR<D, E>(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<D = unknown, E = unknown>(key: keyInterface, running: boolean): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
function useRunningRequest<D = unknown, E extends Error = Error>(key: keyInterface, running: boolean): Response<D, E>;
function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>
): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'>
): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>,
......
......@@ -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<string, string[]>;
......@@ -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<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 {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<BlobResponse>(files ? null : '/graph/graph', blobFetcher);
const {data, loading} = useRequest<BlobResponse>(files ? null : '/graph/graph');
useEffect(() => {
if (data?.data.size) {
......
......@@ -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;
......
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 (
<Loading>
<HashLoader size="60px" color={primaryColor} />
<span>{t('common:loading')}</span>
</Loading>
<CenterWrapper>
{loading || navItems.length ? (
<Loading>
<HashLoader size="60px" color={primaryColor} />
<span>{t('common:loading')}</span>
</Loading>
) : (
<Error />
)}
</CenterWrapper>
);
};
......
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>(XAxis.Step);
......
......@@ -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;
};
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 <T = unknown>(url: string, options?: RequestInit): Promise<T> => {
const res = await fetch(API_URL + url, addApiToken(options));
const response = await res.json();
interface SuccessData<D> {
status: 0;
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 = {
data: Blob;
......@@ -42,30 +49,77 @@ export type BlobResponse = {
filename: string | null;
};
export const blobFetcher = async (url: string, options?: RequestInit): Promise<BlobResponse> => {
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<ArrayBuffer>((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<TFunction> {
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<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) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
let response: Data<T> | 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<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[]> => {
return await Promise.all(urls.map(url => fetcher<T>(url, options)));
......
......@@ -19,6 +19,9 @@ i18n.use(initReactI18next)
ns: 'common',
defaultNS: 'common',
load: 'currentOnly',
react: {
useSuspense: false
},
interpolation: {
escapeValue: false
},
......
......@@ -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;
......
......@@ -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"
......
......@@ -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);
}
......
......@@ -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}));
......
......@@ -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"
......
......@@ -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)
......@@ -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"
......
......@@ -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 + '/<path:filename>')
def serve_static(filename):
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册