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

feat: support model graph from backend (#662)

* feat: support model graph from backend

* feat: add filename in graph api

* feat: support api token key in visualdl service
上级 3e8604ba
......@@ -9,14 +9,11 @@ import {
textColor,
textLightColor,
textLighterColor,
tooltipBackgroundColor,
tooltipTextColor,
transitionProps
} from '~/utils/style';
import Icon from '~/components/Icon';
import ReactTooltip from 'react-tooltip';
import {nanoid} from 'nanoid';
import Tippy from '@tippyjs/react';
import styled from 'styled-components';
const Toolbox = styled.div<{reversed?: boolean}>`
......@@ -65,17 +62,15 @@ type ToggleChartToolboxItem = {
export type ChartTooboxItem = NormalChartToolboxItem | ToggleChartToolboxItem;
type ChartToolboxProps = {
cid?: string;
items: ChartTooboxItem[];
reversed?: boolean;
tooltipPlace?: 'top' | 'bottom' | 'left' | 'right';
tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';
};
const ChartToolbox: FunctionComponent<ChartToolboxProps & WithStyled> = ({
cid,
tooltipPlacement,
items,
reversed,
tooltipPlace,
className
}) => {
const [activeStatus, setActiveStatus] = useState<boolean[]>(new Array(items.length).fill(false));
......@@ -96,37 +91,39 @@ const ChartToolbox: FunctionComponent<ChartToolboxProps & WithStyled> = ({
[items, activeStatus]
);
const [id] = useState(`chart-toolbox-tooltip-${cid || nanoid()}`);
const getToolboxItem = useCallback(
(item: ChartTooboxItem, index: number) => (
<ToolboxItem
key={index}
reversed={reversed}
active={item.toggle && !item.activeIcon && activeStatus[index]}
onClick={() => onClick(index)}
>
<Icon type={item.toggle ? (activeStatus[index] && item.activeIcon) || item.icon : item.icon} />
</ToolboxItem>
),
[activeStatus, onClick, reversed]
);
return (
<>
<Toolbox className={className} reversed={reversed}>
{items.map((item, index) => (
<ToolboxItem
key={index}
reversed={reversed}
active={item.toggle && !item.activeIcon && activeStatus[index]}
onClick={() => onClick(index)}
data-for={item.tooltip ? id : null}
data-tip={
item.tooltip
? item.toggle
? (activeStatus[index] && item.activeTooltip) || item.tooltip
: item.tooltip
: null
}
>
<Icon type={item.toggle ? (activeStatus[index] && item.activeIcon) || item.icon : item.icon} />
</ToolboxItem>
))}
{items.map((item, index) =>
item.tooltip ? (
<Tippy
content={
item.toggle ? (activeStatus[index] && item.activeTooltip) || item.tooltip : item.tooltip
}
placement={tooltipPlacement || 'top'}
key={index}
>
{getToolboxItem(item, index)}
</Tippy>
) : (
getToolboxItem(item, index)
)
)}
</Toolbox>
<ReactTooltip
id={id}
place={tooltipPlace ?? 'top'}
textColor={tooltipTextColor}
backgroundColor={tooltipBackgroundColor}
effect="solid"
/>
</>
);
};
......
......@@ -42,6 +42,8 @@ const Wrapper = styled.div`
}
`;
const reload = () => window.location.reload();
const Error: FunctionComponent<WithStyled> = ({className, children}) => {
const {t} = useTranslation('errors');
......@@ -79,7 +81,7 @@ const Error: FunctionComponent<WithStyled> = ({className, children}) => {
<li>
<Trans i18nKey="errors:common.3">
Log files are generated and data is writte. Please try to&nbsp;
<a href="javascript:location.reload()">Refresh</a>.
<a onClick={reload}>Refresh</a>.
</Trans>
</li>
<li>
......
......@@ -5,6 +5,7 @@ import {backgroundColor, borderColor, contentHeight, position, primaryColor, rem
import ChartToolbox from '~/components/ChartToolbox';
import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components';
import {toast} from 'react-toastify';
import {useTranslation} from '~/utils/i18n';
const toolboxHeight = rem(40);
......@@ -80,7 +81,7 @@ export type GraphRef = {
};
type GraphProps = {
files: FileList | null;
files: FileList | File[] | null;
uploader: JSX.Element;
showAttributes: boolean;
showInitializers: boolean;
......@@ -135,6 +136,12 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
return;
case 'search':
return onSearch?.(data);
case 'cancel':
return setLoading(false);
case 'error':
toast(data);
setLoading(false);
return;
case 'show-model-properties':
return onShowModelProperties?.(data);
case 'show-node-properties':
......@@ -146,13 +153,6 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
},
[onRendered, onSearch, onShowModelProperties, onShowNodeProperties, onShowNodeDocumentation]
);
useEffect(() => {
if (process.browser) {
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}
}, [handler]);
const dispatch = useCallback((type: string, data?: unknown) => {
if (process.browser) {
iframe.current?.contentWindow?.postMessage(
......@@ -164,11 +164,28 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
);
}
}, []);
useEffect(() => {
if (process.browser) {
window.addEventListener('message', handler);
dispatch('ready');
return () => {
window.removeEventListener('message', handler);
};
}
}, [handler, dispatch]);
useEffect(() => dispatch('change-files', files), [dispatch, files]);
useEffect(() => dispatch('toggle-attributes', showAttributes), [dispatch, showAttributes]);
useEffect(() => dispatch('toggle-initializers', showInitializers), [dispatch, showInitializers]);
useEffect(() => dispatch('toggle-names', showNames), [dispatch, showNames]);
useEffect(() => (ready && dispatch('change-files', files)) || undefined, [dispatch, files, ready]);
useEffect(() => (ready && dispatch('toggle-attributes', showAttributes)) || undefined, [
dispatch,
showAttributes,
ready
]);
useEffect(() => (ready && dispatch('toggle-initializers', showInitializers)) || undefined, [
dispatch,
showInitializers,
ready
]);
useEffect(() => (ready && dispatch('toggle-names', showNames)) || undefined, [dispatch, showNames, ready]);
useImperativeHandle(ref, () => ({
export(type) {
......@@ -225,7 +242,7 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
}
]}
reversed
tooltipPlace="bottom"
tooltipPlacement="bottom"
/>
<Content>
<iframe
......
......@@ -15,6 +15,7 @@ import Icon from '~/components/Icon';
import {InitConfig} from '@visualdl/i18n';
import Language from '~/components/Language';
import ee from '~/utils/event';
import {getApiToken} from '~/utils/fetch';
import styled from 'styled-components';
import useNavItems from '~/hooks/useNavItems';
import {useRouter} from 'next/router';
......@@ -105,7 +106,14 @@ const Navbar: FunctionComponent = () => {
if (subpath) {
path += `/${subpath}`;
}
return `${path}/index`;
path += '/index';
if (process.env.API_TOKEN_KEY) {
const id = getApiToken();
if (id) {
path += `?${process.env.API_TOKEN_KEY}=${id}`;
}
}
return path;
}, [i18n.options, i18n.language]);
return (
......
......@@ -4,12 +4,26 @@ import Head from 'next/head';
type PreloaderProps = {
url: string;
as?:
| 'audio'
| 'document'
| 'embed'
| 'fetch'
| 'font'
| 'image'
| 'object'
| 'script'
| 'style'
| 'track'
| 'worker'
| 'video';
};
const Preloader: FunctionComponent<PreloaderProps> = ({url}) => (
<Head>
<link rel="preload" href={process.env.API_URL + url} as="fetch" crossOrigin="anonymous" />
</Head>
);
const Preloader: FunctionComponent<PreloaderProps> = ({url, as}) =>
process.env.API_TOKEN_KEY ? null : (
<Head>
<link rel="preload" href={process.env.API_URL + url} crossOrigin="anonymous" as={as || 'fetch'} />
</Head>
);
export default Preloader;
......@@ -2,8 +2,7 @@ import React, {FunctionComponent, useEffect, useState} from 'react';
import {WithStyled, rem} from '~/utils/style';
import Button from '~/components/Button';
import ReactTooltip from 'react-tooltip';
import {nanoid} from 'nanoid';
import Tippy from '@tippyjs/react';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
......@@ -40,23 +39,16 @@ const RunningToggle: FunctionComponent<RunningToggleProps & WithStyled> = ({runn
onToggle?.(state);
}, [onToggle, state]);
const [id] = useState(`running-toggle-tooltip-${nanoid()}`);
return (
<Wrapper className={className}>
<span>{t(state ? 'running' : 'stopped')}</span>
<div data-for={id} data-tip>
<StyledButton onClick={() => setState(s => !s)} type={state ? 'danger' : 'primary'} rounded>
{t(state ? 'stop' : 'run')}
</StyledButton>
</div>
<ReactTooltip
id={id}
place="top"
type="dark"
effect="solid"
getContent={() => t(state ? 'stop-realtime-refresh' : 'start-realtime-refresh')}
/>
<Tippy content={t(state ? 'stop-realtime-refresh' : 'start-realtime-refresh') + ''} hideOnClick={false}>
<div>
<StyledButton onClick={() => setState(s => !s)} type={state ? 'danger' : 'primary'} rounded>
{t(state ? 'stop' : 'run')}
</StyledButton>
</div>
</Tippy>
</Wrapper>
);
};
......
......@@ -31,6 +31,7 @@ module.exports = {
DEFAULT_LANGUAGE,
LOCALE_PATH,
LANGUAGES,
API_TOKEN_KEY: process.env.API_TOKEN_KEY || '',
PUBLIC_PATH: publicPath,
API_URL: apiUrl
},
......
......@@ -32,6 +32,7 @@
"test": "echo \"Error: no test specified\" && exit 0"
},
"dependencies": {
"@tippyjs/react": "4.0.2",
"@visualdl/i18n": "2.0.0-beta.43",
"@visualdl/netron": "2.0.0-beta.43",
"@visualdl/wasm": "2.0.0-beta.43",
......@@ -44,22 +45,22 @@
"lodash": "4.17.15",
"mime-types": "2.1.27",
"moment": "2.26.0",
"nanoid": "3.1.9",
"next": "9.4.4",
"nprogress": "0.2.0",
"polished": "3.6.4",
"prop-types": "15.7.2",
"query-string": "6.13.0",
"query-string": "6.13.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-hooks-worker": "0.9.0",
"react-input-range": "1.3.0",
"react-is": "16.13.1",
"react-spinners": "0.8.3",
"react-tooltip": "4.2.6",
"react-toastify": "6.0.5",
"save-svg-as-png": "1.4.17",
"styled-components": "5.1.1",
"swr": "0.2.2"
"swr": "0.2.2",
"tippy.js": "6.2.3"
},
"devDependencies": {
"@babel/core": "7.10.2",
......@@ -69,7 +70,7 @@
"@types/mime-types": "2.1.0",
"@types/node": "14.0.13",
"@types/nprogress": "0.2.0",
"@types/react": "16.9.35",
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"@types/styled-components": "5.1.0",
"@visualdl/mock": "2.0.0-beta.43",
......
import App, {AppContext, AppProps} from 'next/app';
import {GlobalStyle, iconFontPath} from '~/utils/style';
import {Router, appWithTranslation} from '~/utils/i18n';
import {fetcher, getApiToken, setApiToken} from '~/utils/fetch';
import App from 'next/app';
import {GlobalStyle} from '~/utils/style';
import Head from 'next/head';
import Layout from '~/components/Layout';
import NProgress from 'nprogress';
import Preloader from '~/components/Preloader';
import React from 'react';
import {SWRConfig} from 'swr';
import {fetcher} from '~/utils/fetch';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
import {ToastContainer} from 'react-toastify';
import queryString from 'query-string';
import {withRouter} from 'next/router';
class VDLApp extends App {
constructor(props: AppProps) {
super(props);
if (process.browser && process.env.API_TOKEN_KEY) {
const query = queryString.parse(window.location.search);
setApiToken(query[process.env.API_TOKEN_KEY]);
}
}
componentDidMount() {
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', (url: string) => {
NProgress.done();
if (process.env.API_TOKEN_KEY) {
const id = getApiToken();
const parsed = queryString.parseUrl(url);
if (id && !parsed.query[process.env.API_TOKEN_KEY]) {
this.props.router.replace(
queryString.stringifyUrl({
url: parsed.url,
query: {
...parsed.query,
[process.env.API_TOKEN_KEY]: id
}
}),
undefined,
{shallow: true}
);
}
}
});
Router.events.on('routeChangeError', () => NProgress.done());
}
render() {
const {Component, pageProps} = this.props;
return (
<>
{['ttf', 'woff', 'svg'].map(ext => (
<Preloader url={`${iconFontPath}.${ext}`} as="font" key={ext} />
))}
<Head>
<title>{process.env.title}</title>
<link rel="shortcut icon" href={`${process.env.PUBLIC_PATH}/favicon.ico`} />
......@@ -41,10 +77,20 @@ class VDLApp extends App {
<Layout>
<Component {...pageProps} />
</Layout>
<ToastContainer position="top-center" hideProgressBar closeOnClick={false} />
</SWRConfig>
</>
);
}
static async getInitialProps(appContext: AppContext) {
const appProps = await App.getInitialProps(appContext);
if (process.env.API_TOKEN_KEY) {
setApiToken(appContext.router.query[process.env.API_TOKEN_KEY]);
}
return {...appProps};
}
}
export default appWithTranslation(VDLApp);
export default appWithTranslation(withRouter(VDLApp));
import Aside, {AsideSection} from '~/components/Aside';
import {BlobResponse, blobFetcher} from '~/utils/fetch';
import {Documentation, Properties, SearchItem, SearchResult} from '~/resource/graphs/types';
import Graph, {GraphRef} from '~/components/GraphsPage/Graph';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {primaryColor, rem, size} from '~/utils/style';
import Button from '~/components/Button';
import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content';
import Field from '~/components/Field';
import HashLoader from 'react-spinners/HashLoader';
import ModelPropertiesDialog from '~/components/GraphsPage/ModelPropertiesDialog';
import NodeDocumentationSidebar from '~/components/GraphsPage/NodeDocumentationSidebar';
import NodePropertiesSidebar from '~/components/GraphsPage/NodePropertiesSidebar';
import Search from '~/components/GraphsPage/Search';
import Title from '~/components/Title';
import Uploader from '~/components/GraphsPage/Uploader';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
const FullWidthButton = styled(Button)`
width: 100%;
......@@ -45,12 +48,26 @@ const SearchSection = styled(AsideSection)`
}
`;
const Loading = styled.div`
${size('100%', '100%')}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overscroll-behavior: none;
cursor: progress;
font-size: ${rem(16)};
line-height: ${rem(60)};
`;
const Graphs: NextI18NextPage = () => {
const {t} = useTranslation(['graphs', 'common']);
const {data, loading} = useRequest<BlobResponse>('/graphs/graph', blobFetcher);
const graph = useRef<GraphRef>(null);
const file = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<FileList | null>(null);
const [files, setFiles] = useState<FileList | File[] | null>(null);
const onClickFile = useCallback(() => {
if (file.current) {
file.current.value = '';
......@@ -63,6 +80,11 @@ const Graphs: NextI18NextPage = () => {
setFiles(target.files);
}
}, []);
useEffect(() => {
if (data?.data.size) {
setFiles([new File([data.data], data.filename || 'unknwon_model')]);
}
}, [data]);
const [search, setSearch] = useState('');
const [searching, setSearching] = useState(false);
......@@ -84,7 +106,10 @@ const Graphs: NextI18NextPage = () => {
const [nodeData, setNodeData] = useState<Properties | null>(null);
const [nodeDocumentation, setNodeDocumentation] = useState<Documentation | null>(null);
useEffect(() => setSearch(''), [showAttributes, showInitializers, showNames]);
useEffect(() => {
setSearch('');
setSearchResult({text: '', result: []});
}, [files, showAttributes, showInitializers, showNames]);
const bottom = useMemo(
() =>
......@@ -99,7 +124,7 @@ const Graphs: NextI18NextPage = () => {
const [rendered, setRendered] = useState(false);
const aside = useMemo(() => {
if (!rendered) {
if (!rendered || loading) {
return null;
}
if (nodeDocumentation) {
......@@ -186,6 +211,7 @@ const Graphs: NextI18NextPage = () => {
showInitializers,
showNames,
rendered,
loading,
nodeData,
nodeDocumentation
]);
......@@ -197,22 +223,28 @@ const Graphs: NextI18NextPage = () => {
<Title>{t('common:graphs')}</Title>
<ModelPropertiesDialog data={modelData} onClose={() => setModelData(null)} />
<Content aside={aside}>
<Graph
ref={graph}
files={files}
uploader={uploader}
showAttributes={showAttributes}
showInitializers={showInitializers}
showNames={showNames}
onRendered={() => setRendered(true)}
onSearch={data => setSearchResult(data)}
onShowModelProperties={data => setModelData(data)}
onShowNodeProperties={data => {
setNodeData(data);
setNodeDocumentation(null);
}}
onShowNodeDocumentation={data => setNodeDocumentation(data)}
/>
{loading ? (
<Loading>
<HashLoader size="60px" color={primaryColor} />
</Loading>
) : (
<Graph
ref={graph}
files={files}
uploader={uploader}
showAttributes={showAttributes}
showInitializers={showInitializers}
showNames={showNames}
onRendered={() => setRendered(true)}
onSearch={data => setSearchResult(data)}
onShowModelProperties={data => setModelData(data)}
onShowNodeProperties={data => {
setNodeData(data);
setNodeDocumentation(null);
}}
onShowNodeDocumentation={data => setNodeDocumentation(data)}
/>
)}
<input
ref={file}
type="file"
......
declare global {
interface Window {
__visualdl_instance_id__?: string;
}
namespace globalThis {
// eslint-disable-next-line no-var
var __visualdl_instance_id__: string | undefined;
}
}
declare namespace NodeJS {
interface Global {
__visualdl_instance_id__?: string;
}
}
export {};
// TODO: use this instead
// https://github.com/zeit/swr/blob/master/examples/axios-typescript/libs/useRequest.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import fetch from 'isomorphic-unfetch';
export const fetcher = async <T = any>(url: string, options?: any): Promise<T> => {
const res = await fetch(process.env.API_URL + url, options);
const API_TOKEN_HEADER = 'X-VisualDL-Instance-ID';
function addApiToken(options?: RequestInit): RequestInit | undefined {
if (!process.env.API_TOKEN_KEY || !globalThis.__visualdl_instance_id__) {
return options;
}
const {headers, ...rest} = options || {};
return {
...rest,
headers: {
...(headers || {}),
[API_TOKEN_HEADER]: globalThis.__visualdl_instance_id__
}
};
}
export function setApiToken(id?: string | string[] | null) {
const instanceId = Array.isArray(id) ? id[0] : id;
globalThis.__visualdl_instance_id__ = instanceId || '';
}
export function getApiToken() {
return globalThis.__visualdl_instance_id__ || '';
}
export const fetcher = async <T = unknown>(url: string, options?: RequestInit): Promise<T> => {
const res = await fetch(process.env.API_URL + url, addApiToken(options));
const response = await res.json();
return response && 'data' in response ? response.data : response;
......@@ -15,14 +38,23 @@ export const fetcher = async <T = any>(url: string, options?: any): Promise<T> =
export type BlobResponse = {
data: Blob;
type: string | null;
filename: string | null;
};
export const blobFetcher = async (url: string, options?: any): Promise<BlobResponse> => {
const res = await fetch(process.env.API_URL + url, options);
export const blobFetcher = async (url: string, options?: RequestInit): Promise<BlobResponse> => {
const res = await fetch(process.env.API_URL + url, addApiToken(options));
const data = await res.blob();
return {data, type: res.headers.get('Content-Type')};
const disposition = res.headers.get('Content-Disposition');
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};
};
export const cycleFetcher = async <T = any>(urls: string[], options?: any): Promise<T[]> => {
export const cycleFetcher = async <T = unknown>(urls: string[], options?: RequestInit): Promise<T[]> => {
return await Promise.all(urls.map(url => fetcher<T>(url, options)));
};
......@@ -33,6 +33,7 @@ export const {i18n, config, appWithTranslation, withTranslation, useTranslation,
// from ~/node_modules/next/types/index.d.ts
// https://gitlab.com/kachkaev/website-frontend/-/blob/master/src/i18n.ts#L64-68
// eslint-disable-next-line @typescript-eslint/ban-types
export type NextI18NextPage<P = {}, IP = P> = NextComponentType<
NextPageContext,
IP & {namespacesRequired: string[]},
......
......@@ -3,6 +3,8 @@ import * as polished from 'polished';
import {createGlobalStyle, keyframes} from 'styled-components';
import {css} from 'styled-components';
import tippy from '!!css-loader!tippy.js/dist/tippy.css';
import toast from '!!css-loader!react-toastify/dist/ReactToastify.css';
import vdlIcon from '!!css-loader!~/public/style/vdl-icon.css';
export {default as styled} from 'styled-components';
......@@ -13,6 +15,8 @@ export {borderRadius as borderRadiusShortHand, borderColor as borderColorShortHa
const {math, size, lighten, darken, normalize, fontFace, transitions, border, position} = polished;
export const iconFontPath = `${process.env.PUBLIC_PATH}/style/fonts/vdl-icon`;
// sizes
const fontSize = '14px';
export const rem = (pxval: string | number): string => polished.rem(pxval, fontSize);
......@@ -124,7 +128,7 @@ export const GlobalStyle = createGlobalStyle`
${fontFace({
fontFamily: 'vdl-icon',
fontFilePath: `${process.env.PUBLIC_PATH}/style/fonts/vdl-icon`,
fontFilePath: iconFontPath,
fileFormats: ['ttf', 'woff', 'svg'],
fontWeight: 'normal',
fontStyle: 'normal',
......@@ -132,6 +136,8 @@ export const GlobalStyle = createGlobalStyle`
})}
${vdlIcon.toString()}
${toast.toString()}
${tippy.toString()}
html {
font-size: ${fontSize};
......@@ -207,4 +213,39 @@ export const GlobalStyle = createGlobalStyle`
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
.Toastify__toast-container {
z-index: 10001;
.Toastify__toast {
border-radius: ${borderRadius};
}
.Toastify__toast--default {
color: ${textColor};
}
.Toastify__toast-body {
padding: 0 1.428571429em;
}
}
.tippy-box {
z-index: 10002;
color: ${tooltipTextColor};
background-color: ${tooltipBackgroundColor};
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: ${tooltipBackgroundColor};
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: ${tooltipBackgroundColor};
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: ${tooltipBackgroundColor};
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: ${tooltipBackgroundColor};
}
}
`;
......@@ -41,7 +41,7 @@
"i18next-browser-languagedetector": "4.2.0",
"i18next-fs-backend": "1.0.6",
"i18next-http-backend": "1.0.15",
"i18next-http-middleware": "2.1.0",
"i18next-http-middleware": "2.1.2",
"path-match": "1.2.4",
"prop-types": "15.7.2",
"react-i18next": "11.5.0",
......@@ -51,7 +51,7 @@
"@types/express": "4.17.6",
"@types/hoist-non-react-statics": "3.3.1",
"@types/node": "14.0.13",
"@types/react": "16.9.35",
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"typescript": "3.9.5"
},
......
......@@ -50,6 +50,7 @@ export type Trans = (props: TransProps) => any;
export type Link = React.ComponentClass<LinkProps>;
export type Router = SingletonRouter;
export type UseTranslation = typeof useTranslation;
// eslint-disable-next-line @typescript-eslint/ban-types
export type AppWithTranslation = <P extends object>(Component: React.ComponentType<P> | React.ElementType<P>) => any;
export type TFunction = I18NextTFunction;
export type I18n = i18n;
......
......@@ -17,6 +17,7 @@ host.BrowserHost = class {
}
this._type = this._meta.type ? this._meta.type[0] : 'Browser';
this._version = this._meta.version ? this._meta.version[0] : null;
this._ready = false;
}
get document() {
......@@ -69,12 +70,18 @@ host.BrowserHost = class {
return this._view.showModelProperties();
case 'show-node-documentation':
return this._view.showNodeDocumentation(data);
case 'ready':
if (this._ready) {
return this.status('ready');
}
return;
}
}
},
false
);
this._ready = true;
this.status('ready');
}
......@@ -89,11 +96,15 @@ host.BrowserHost = class {
}
error(message, detail) {
alert((message == 'Error' ? '' : message + ' ') + detail);
this.message('error', (message === 'Error' ? '' : message + ' ') + detail);
}
confirm(message, detail) {
return confirm(message + ' ' + detail);
const result = confirm(message + ' ' + detail);
if (!result) {
this.message('cancel');
}
return result;
}
require(id) {
......@@ -147,6 +158,11 @@ host.BrowserHost = class {
_changeFiles(files) {
if (files && files.length) {
files = Array.from(files);
const file = files.find(file => this._view.accept(file.name));
if (!file) {
this.error('Error opening file.', 'Cannot open file ' + files[0].name);
return;
}
this._open(
files.find(file => this._view.accept(file.name)),
files
......
......@@ -58,6 +58,9 @@ view.View = class {
}
toggleAttributes(toggle) {
if (toggle != null && !(toggle ^ this._showAttributes)) {
return;
}
this._showAttributes = toggle == null ? !this._showAttributes : toggle;
this._reload();
}
......@@ -67,6 +70,9 @@ view.View = class {
}
toggleInitializers(toggle) {
if (toggle != null && !(toggle ^ this._showInitializers)) {
return;
}
this._showInitializers = toggle == null ? !this._showInitializers : toggle;
this._reload();
}
......@@ -76,6 +82,9 @@ view.View = class {
}
toggleNames(toggle) {
if (toggle != null && !(toggle ^ this._showNames)) {
return;
}
this._showNames = toggle == null ? !this._showNames : toggle;
this._reload();
}
......
......@@ -1042,6 +1042,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.8.7":
version "7.10.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
......@@ -2224,6 +2231,11 @@
dependencies:
debug "^4.1.1"
"@popperjs/core@^2.3.2":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.2.tgz#7c6dc4ecef16149fd7a736710baa1b811017fdca"
integrity sha512-JlGTGRYHC2QK+DDbePyXdBdooxFq2+noLfWpRqJtkxcb/oYWzOF0kcbfvvbWrwevCC1l6hLUg1wHYT+ona5BWQ==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
......@@ -2289,6 +2301,14 @@
dependencies:
defer-to-connect "^1.0.1"
"@tippyjs/react@4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.0.2.tgz#fadd14f1e36dd4f63f5f241cc78f3a1bb8394250"
integrity sha512-iAKTjUmrXqTTJ4HZRDgmvVfUiv9pTzJoDjPLDbmvB6vttkuYvZ/o8NhHa72vMFgHpiMFNoYWtB8OCRR6x5Zs8w==
dependencies:
prop-types "^15.6.2"
tippy.js "^6.2.0"
"@types/anymatch@*":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
......@@ -2495,7 +2515,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@16.9.35":
"@types/react@*":
version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
......@@ -2503,6 +2523,14 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/react@16.9.36":
version "16.9.36"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.36.tgz#ade589ff51e2a903e34ee4669e05dbfa0c1ce849"
integrity sha512-mGgUb/Rk/vGx4NCvquRuSH0GHBQKb1OqpGS9cT9lFxlTLHZgkksgI60TuIxubmn7JuCb+sENHhQciqa0npm0AQ==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/rimraf@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.0.tgz#b9d03f090ece263671898d57bb7bb007023ac19f"
......@@ -4032,7 +4060,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@2.2.6:
classnames@2.2.6, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
......@@ -4820,7 +4848,7 @@ csso@^4.0.2:
dependencies:
css-tree "1.0.0-alpha.39"
csstype@^2.2.0, csstype@^2.5.7:
csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7:
version "2.6.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
......@@ -5370,6 +5398,14 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-helpers@^5.0.1:
version "5.1.4"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b"
integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^2.6.7"
dom-serializer@0, dom-serializer@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
......@@ -7175,10 +7211,10 @@ i18next-http-backend@1.0.15:
dependencies:
node-fetch "2.6.0"
i18next-http-middleware@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/i18next-http-middleware/-/i18next-http-middleware-2.1.0.tgz#0396971fe6d9fcf82c109f6ee72268d89776d7d1"
integrity sha512-OgtOFDjsIL9R4eXWrpge9TCMc52L2kL1d/9HsErPA14DyqfHc7NVCampFIuqTfpmc4janwcG66r5r5p9zdrS+Q==
i18next-http-middleware@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/i18next-http-middleware/-/i18next-http-middleware-2.1.2.tgz#ec6a50a64b3c12ef5791acd6c1bb56b4fa3cbf2d"
integrity sha512-pW0gvRn1pqNswnmEocCWBSRAAyvT8/3wvJ5EKrwzqb+ZlJFh7GlYNC+vhYIJuLWKZE1G8bE69/EPSoih0UAM6Q==
i18next@19.4.5:
version "19.4.5"
......@@ -8878,11 +8914,6 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
nanoid@3.1.9:
version "3.1.9"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.9.tgz#1f148669c70bb2072dc5af0666e46edb6cd31fb2"
integrity sha512-fFiXlFo4Wkuei3i6w9SQI6yuzGRTGi8Z2zZKZpUxv/bQlBi4jtbVPBSNFZHQA9PNjofWqtIa8p+pnsc0kgZrhQ==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
......@@ -10666,10 +10697,10 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
query-string@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.0.tgz#8d875f66581c854d7480ac79478abb847de742f6"
integrity sha512-KJe8p8EUcixhPCp4cJoTYVfmgKHjnAB/Pq3fiqlmyNHvpHnOL5U4YE7iI2PYivGHp4HFocWz300906BAQX0H7g==
query-string@6.13.1:
version "6.13.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad"
integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
......@@ -10801,13 +10832,24 @@ react-spinners@0.8.3:
dependencies:
"@emotion/core" "^10.0.15"
react-tooltip@4.2.6:
version "4.2.6"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.6.tgz#a3d5f0d1b0c597c0852ba09c5e2af0019b7cfc70"
integrity sha512-KX/zCsPFCI8RuulzBX86U+Ur7FvgGNRBdb7dUu0ndo8Urinn48nANq9wfq4ABlehweQjPzLl7XdNAtLKza+I3w==
react-toastify@6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-6.0.5.tgz#6435b2bf6a298863bc71342dcc88e8283cdb4630"
integrity sha512-1YXSb6Jr478c1TJEyVpxLHFvtmeXGMvdpZc0fke/7lK+MoLBC+NFgB74bq+C2SZe6LdK+K1voEURJoY88WqWvA==
dependencies:
classnames "^2.2.6"
prop-types "^15.7.2"
uuid "^7.0.3"
react-transition-group "^4.4.1"
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@16.13.1:
version "16.13.1"
......@@ -12464,6 +12506,13 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tippy.js@6.2.3, tippy.js@^6.2.0:
version "6.2.3"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.3.tgz#0a5db67dc6bd9129233b26052b7ae2b2047fd73e"
integrity sha512-MzqHMrr2C0IC8ZUnG5kLQPxonWJ7V+Usqiy2W5b+dCvAfousio0mA85h+Ea5wRq94AQGd8mbFGeciRgkP+F+7w==
dependencies:
"@popperjs/core" "^2.3.2"
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
......@@ -12989,11 +13038,6 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
v8-compile-cache@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
......
......@@ -37,14 +37,18 @@ def gen_result(data=None, status=0, msg=''):
}
def result(mimetype='application/json'):
def result(mimetype='application/json', headers=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
data = func(*args, **kwargs)
def wrapper(self, *args, **kwargs):
data = func(self, *args, **kwargs)
if mimetype == 'application/json':
data = json.dumps(gen_result(data))
return data, mimetype
if callable(headers):
headers_output = headers(self)
else:
headers_output = headers
return data, mimetype, headers_output
return wrapper
return decorator
......@@ -60,6 +64,7 @@ class Api(object):
def __init__(self, logdir, model, cache_timeout):
self._reader = LogReader(logdir)
self._reader.model = model
self.model_name = os.path.basename(model)
# use a memory cache to reduce disk reading frequency.
cache = MemCache(timeout=cache_timeout)
......@@ -145,7 +150,7 @@ class Api(object):
key = os.path.join('data/plugin/embeddings/embeddings', run, tag)
return self._get_with_retry(key, lib.get_embeddings, run, tag)
@result('application/octet-stream')
@result('application/octet-stream', lambda s: {"Content-Disposition": 'attachment; filename="%s"' % s.model_name} if len(s.model_name) else None)
def graphs_graph(self):
key = os.path.join('data/plugin/graphs/graph')
return self._get_with_retry(key, lib.get_graph)
......
......@@ -98,9 +98,8 @@ def create_app(args):
@app.route(api_path + '/<path:method>')
def serve_api(method):
data, mimetype = api_call(method, request.args)
return make_response(Response(data, mimetype=mimetype))
data, mimetype, headers = api_call(method, request.args)
return make_response(Response(data, mimetype=mimetype, headers=headers))
return app
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册