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

refactor: get rid of webpack & nextjs while introducing es module (#786)

* fix: incorrect locale redirect

* chore: use millisecond

* refactor: get rid of nextjs

* feat: add telemetry

* feat: add disable telemetry option

* fix #739

* fix: compatibility of nodejs 12

* mute webpack

* feat: show min & max value in scalar page

* fix: tooltip sorting chinese translation update

* style: fix lint
上级 e529448f
......@@ -8,7 +8,7 @@ RUN ["pip", "install", "--disable-pip-version-check", "-r", "requirements.txt"]
RUN ["python", "setup.py", "bdist_wheel"]
RUN ["pip", "install", "--disable-pip-version-check", "--find-links=dist", "visualdl"]
WORKDIR frontend
WORKDIR /home/visualdl/frontend
ENV SCOPE server
ENV PUBLIC_PATH /paddle/visualdl/demo
ENV API_URL /paddle/visualdl/demo/api
......
......@@ -9,20 +9,14 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module'
},
ignorePatterns: ['node_modules/', 'dist/', 'output/', '_next'],
ignorePatterns: ['node_modules/', 'dist/', 'output/'],
rules: {
'no-console': 'warn',
'sort-imports': 'error'
},
overrides: [
{
files: [
'packages/cli/**/*',
'packages/mock/**/*',
'packages/demo/**/*',
'packages/server/**/*',
'packages/serverless/**/*'
],
files: ['packages/cli/**/*', 'packages/mock/**/*', 'packages/demo/**/*', 'packages/server/**/*'],
extends: [
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
......@@ -36,7 +30,7 @@ module.exports = {
}
},
{
files: ['packages/core/**/*', 'packages/i18n/**/*'],
files: ['packages/core/**/*'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
......
{
"hooks": {
"pre-commit": "lint-staged"
}
}
......@@ -59,9 +59,8 @@ yarn
[core](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/core/README.md)
[server](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/server/README.md)
[serverless](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/serverless/README.md)
[netron](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/netron/README.md)
[cli](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/cli/README.md)
[i18n](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/i18n/README.md)
[wasm](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/wasm/README.md)
[mock](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/mock/README.md)
......
......@@ -59,9 +59,8 @@ yarn
[core](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/core/README.md)
[server](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/server/README.md)
[serverless](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/serverless/README.md)
[netron](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/netron/README.md)
[cli](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/cli/README.md)
[i18n](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/i18n/README.md)
[wasm](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/wasm/README.md)
[mock](https://github.com/PaddlePaddle/VisualDL/blob/develop/frontend/packages/mock/README.md)
......
const path = require('path');
const fs = require('fs');
const getPackages = filenames => [
...new Set(filenames.map(filename => path.relative(path.join(__dirname, 'packages'), filename).split(path.sep)[0]))
];
module.exports = {
// lint all files when global package.json or eslint config changes.
'./(package.json|.eslintrc.js)': () =>
......@@ -8,11 +12,7 @@ module.exports = {
// check types when ts file or package.json changes.
'./(packages/*/package.json|packages/*/**/*.ts?(x))': filenames =>
[
...new Set(
filenames.map(filename => path.relative(path.join(__dirname, 'packages'), filename).split(path.sep)[0])
)
]
getPackages(filenames)
.map(p => path.join(__dirname, 'packages', p, 'tsconfig.json'))
.filter(p => {
try {
......@@ -26,6 +26,15 @@ module.exports = {
// lint changed files
'**/*.(j|t)s?(x)': filenames => [
`eslint ${filenames.join(' ')}`,
`yarn test --silent --bail --findRelatedTests ${filenames.join(' ')}`
...getPackages(filenames).map(p => {
const filename = path.join(__dirname, 'packages', p, 'package.json');
const packageFile = JSON.parse(fs.readFileSync(filename, 'utf-8'));
if (packageFile.scripts.test === 'jest') {
return `yarn workspace @visualdl/${p} run test --silent --bail --findRelatedTests ${filenames.join(
' '
)}`;
}
return `yarn workspace @visualdl/${p} run test`;
})
]
};
......@@ -29,29 +29,29 @@
"scripts": {
"bootstrap": "lerna bootstrap",
"build": "./scripts/build.sh",
"clean": "rimraf output *.log packages/*/dist packages/core/out packages/wasm/target packages/*/*.log",
"clean": "rimraf output packages/*/dist packages/wasm/target",
"lint": "eslint --ext .tsx,.jsx.ts,.js --ignore-path .gitignore .",
"format": "prettier --write \"**/*.ts\" \"**/*.tsx\" \"**/*.js\"",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"",
"test": "yarn workspaces run test",
"prepublishOnly": "yarn lint && yarn test && yarn build",
"preversion": "yarn lint",
"version": "yarn format && git add -A"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "3.8.0",
"@typescript-eslint/parser": "3.8.0",
"eslint": "7.6.0",
"@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1",
"eslint": "7.8.1",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-react": "7.20.5",
"eslint-plugin-react-hooks": "4.0.8",
"eslint-plugin-react": "7.20.6",
"eslint-plugin-react-hooks": "4.1.0",
"husky": "4.2.5",
"lerna": "3.22.1",
"lint-staged": "10.2.11",
"prettier": "2.0.5",
"lint-staged": "10.2.13",
"prettier": "2.1.1",
"rimraf": "3.0.2",
"typescript": "3.9.7",
"yarn": "1.22.4"
"typescript": "4.0.2",
"yarn": "1.22.5"
},
"engines": {
"node": ">=10",
......
......@@ -23,10 +23,10 @@
"directory": "frontend/packages/cli"
},
"scripts": {
"dev": "cross-env NODE_ENV=development ts-node index.ts",
"build": "tsc",
"dev": "cross-env NODE_ENV=development ts-node index.ts",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 0; #"
"test": "echo \"Error: no test specified\" && exit 0"
},
"bin": "dist/index.js",
"types": "dist/index.d.ts",
......@@ -35,28 +35,23 @@
],
"dependencies": {
"@visualdl/server": "2.0.0",
"open": "7.1.0",
"ora": "4.0.5",
"pm2": "4.4.0",
"open": "7.2.1",
"ora": "5.0.0",
"pm2": "4.4.1",
"yargs": "15.4.1"
},
"devDependencies": {
"@types/node": "14.0.27",
"@types/node": "14.6.3",
"@types/yargs": "15.0.5",
"cross-env": "7.0.2",
"ts-node": "8.10.2",
"typescript": "3.9.7"
"ts-node": "9.0.0",
"typescript": "4.0.2"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
# VisualDL FrontEnd Core
This is core contents of VisualDL pages.
DO NOT use it directly.
## Development
......@@ -12,9 +11,3 @@ yarn dev
```
Now open [http://localhost:3000](http://localhost:3000) with your browser.
You can change the port with `PORT` environment variable:
```bash
PORT=8999 yarn dev
```
import Page404 from '../pages/404';
import React from 'react';
import {shallow} from 'enzyme';
describe('Page 404', () => {
test('page with content `404 - errors:page-not-found`', () => {
const page = shallow(<Page404 namespacesRequired={['errors']} />);
expect(page.text()).toEqual('404 - errors:page-not-found');
});
});
import Icon from '../components/Icon';
import React from 'react';
import {shallow} from 'enzyme';
describe('Icon component', () => {
const click = jest.fn();
const icon = shallow(<Icon type="close" onClick={click} />);
test('icon has one empty `i` element', () => {
expect(icon.is('i')).toBe(true);
expect(icon.children().length).toBe(0);
});
test('icon has class name `vdl-icon`', () => {
expect(icon.hasClass('vdl-icon')).toBe(true);
});
test('icon with type has class name `icon-${type}`', () => {
expect(icon.hasClass('icon-close')).toBe(true);
});
test('icon click', () => {
icon.simulate('click');
expect(click.mock.calls.length).toBe(1);
});
});
module.exports = {
presets: ['next/babel'],
plugins: [
[
'styled-components',
{
ssr: true,
displayName: true,
preprocess: false
}
],
['emotion'],
...(process.env.NODE_ENV !== 'production' ? ['typescript-to-proptypes'] : [])
]
extends: '@snowpack/app-scripts-react/babel.config.json',
plugins: ['styled-components']
};
/* eslint-disable no-console */
import ora from 'ora';
import path from 'path';
import {spawn} from 'child_process';
import {writeFileSync} from 'fs';
const next = require.resolve('next/dist/bin/next');
export const projectRoot = path.dirname(require.resolve('@visualdl/core'));
export default function (action: 'build' | 'export', ...args: string[]): Promise<number> {
return new Promise((resolve, reject) => {
const capitalizedAction = action.replace(/^./, w => w.toUpperCase());
const spinner = ora(`${capitalizedAction} in process...`).start();
const log = path.join(process.cwd(), `${action}.log`);
writeFileSync(log, '', {flag: 'w'});
const p = spawn(next, [action, ...args], {
cwd: projectRoot,
env: {
...process.env,
NODE_ENV: 'production'
}
});
p.stdout.on('data', data => writeFileSync(log, data, {flag: 'a'}));
p.stderr.on('data', data => writeFileSync(log, data, {flag: 'a'}));
p.on('close', code => {
if (code) {
spinner.fail(`${capitalizedAction} failed!`);
console.error(`Please refer to ${log} for more detail.`);
reject(code);
} else {
spinner.succeed(`${capitalizedAction} complete!`);
resolve(code);
}
});
});
}
/* eslint-disable @typescript-eslint/no-var-requires */
const express = require('express');
const mock = require('./mock');
const icons = require('./icons');
const netron = require('./netron');
const wasm = require('./wasm');
const argv = require('yargs').nargs('port', 1).number('port').nargs('host', 1).argv;
const app = express();
app.use(mock.pathname, mock.middleware());
app.use(icons.pathname, icons.middleware());
app.use(netron.pathname, express.static(netron.root, {index: false}));
app.get(`${wasm.pathname}/${wasm.out}`, (_req, res) => {
res.type('application/wasm');
res.sendFile(wasm.source);
});
app.listen(argv.port, argv.host);
process.env.SNOWPACK_PUBLIC_PATH =
process.env.PUBLIC_PATH === '/' || !process.env.PUBLIC_PATH ? '' : process.env.PUBLIC_PATH;
process.env.SNOWPACK_PUBLIC_API_URL = process.env.API_URL || `${process.env.SNOWPACK_PUBLIC_PATH}/api`;
process.env.SNOWPACK_PUBLIC_TELEMETRY_ID = process.env.TELEMETRY_ID || '';
process.env.SNOWPACK_PUBLIC_API_TOKEN_KEY = process.env.API_TOKEN_KEY || '';
process.env.SNOWPACK_PUBLIC_LANGUAGES = process.env.LANGUAGES || 'en,zh';
process.env.SNOWPACK_PUBLIC_DEFAULT_LANGUAGE = process.env.DEFAULT_LANGUAGE || 'en';
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const {promises: fs} = require('fs');
const {default: svgr} = require('@svgr/core');
const babel = require('@babel/core');
const {camelCase} = require('lodash');
const {ensureDir} = require('fs-extra');
const root = path.resolve(__dirname, '../public/icons');
const pathname = '/icons';
const dist = path.resolve(__dirname, '../dist');
const dest = path.join(dist, pathname);
async function transform(file, minified) {
const basename = path.basename(file, '.svg');
let jsx = await svgr(
await fs.readFile(file, 'utf-8'),
{
icon: true,
svgProps: {
fill: 'currentColor',
className: 'vdl-icon'
}
},
{componentName: camelCase(basename).replace(/./, w => w.toUpperCase())}
);
jsx = jsx.replace('import * as React from "react";', 'import React from "../web_modules/react.js";');
const result = await babel.transformAsync(jsx, {
filename: basename + '.jsx',
presets: ['@babel/preset-react'],
minified
});
return result.code;
}
async function build() {
await ensureDir(dest);
const files = await fs.readdir(root);
for (const file of files) {
if (path.extname(file) === '.svg') {
const js = await transform(path.join(root, file), false);
await fs.writeFile(path.join(dest, path.basename(file, '.svg') + '.js'), js, 'utf-8');
}
}
console.log('Icons copied!');
}
if (require.main === module) {
build();
}
const middleware = () => {
return async (req, res) => {
const file = path.join(root, req.path.replace(/\.js$/, '.svg'));
if ((await fs.stat(file)).isFile()) {
res.type('js');
res.send(await transform(file, false));
}
};
};
module.exports = {
middleware,
root,
pathname
};
/* eslint-disable @typescript-eslint/no-var-requires */
const {middleware} = require('@visualdl/mock');
module.exports = {
middleware,
pathname: '/api'
};
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const fs = require('fs-extra');
const root = path.dirname(require('enhanced-resolve').sync(__dirname, '@visualdl/netron'));
const pathname = '/netron';
const dist = path.resolve(__dirname, '../dist');
const dest = path.join(dist, pathname);
async function build() {
await fs.ensureDir(dest);
await fs.copy(root, dest, {preserveTimestamps: true});
console.log('Netron copied!');
}
if (require.main === module) {
build();
}
module.exports = {
root,
dest,
pathname
};
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const fs = require('fs-extra');
const root = path.dirname(require('enhanced-resolve').sync(__dirname, '@visualdl/wasm'));
const source = path.join(root, 'index_bg.wasm');
const dist = path.resolve(__dirname, '../dist');
const pathname = '/wasm';
const out = 'visualdl.wasm';
const dest = path.join(dist, pathname, out);
async function build() {
await fs.ensureDir(path.join(dist, pathname));
await fs.copy(source, dest, {preserveTimestamps: true});
console.log('WebAssembly copied!');
}
if (require.main === module) {
build();
}
module.exports = {
dest,
source,
pathname,
out
};
import React, {FunctionComponent} from 'react';
import {WithStyled} from '~/utils/style';
type IconProps = {
type: string;
onClick?: () => unknown;
};
const Icon: FunctionComponent<IconProps & WithStyled> = ({type, onClick, className}) => {
return <i className={`vdl-icon icon-${type} ${className ?? ''}`} onClick={() => onClick?.()}></i>;
};
export default Icon;
import React, {FunctionComponent} from 'react';
import {headerHeight, position, size} from '~/utils/style';
import Navbar from '~/components/Navbar';
import styled from 'styled-components';
const Main = styled.main`
padding-top: ${headerHeight};
`;
const Header = styled.header`
z-index: 10000;
${size(headerHeight, '100%')}
${position('fixed', 0, 0, null, 0)}
`;
const Layout: FunctionComponent = ({children}) => (
<Main>
<Header>
<Navbar />
</Header>
{children}
</Main>
);
export default Layout;
import React, {FunctionComponent} from 'react';
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, 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;
import React, {FunctionComponent} from 'react';
import Head from 'next/head';
const Title: FunctionComponent = ({children: title}) => (
<Head>
<title>
{'string' === typeof title ? `${title} - ` : ''}
{process.env.title}
</title>
</Head>
);
export default Title;
module.exports = require('next');
/* eslint-disable @typescript-eslint/no-var-requires */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest'
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
coveragePathIgnorePatterns: ['<rootDir>/next.config.js', '<rootDir>/node_modules/'],
testPathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/out/', '<rootDir>/node_modules/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
'~/(.*)': '<rootDir>/$1'
},
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.test.json'
}
}
...require('@snowpack/app-scripts-react/jest.config.js')()
};
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import ReactSixteenAdapter from 'enzyme-adapter-react-16';
import {configure} from 'enzyme';
configure({adapter: new ReactSixteenAdapter()});
/// <reference types="next" />
/// <reference types="next/types/global" />
declare type NextEnv = {
PUBLIC_PATH: string;
API_URL: string;
DEFAULT_LANGUAGE: string;
LANGUAGES: string[];
LOCALE_PATH: string;
};
declare interface NextConfig {
env: NextEnv;
}
const nextConfig: NextConfig;
export default nextConfig;
export const env: NextEnv;
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const pkg = require('./package.json');
const publicPath = process.env.PUBLIC_PATH || '';
const apiUrl = process.env.API_URL || '/api';
const distDir = 'dist';
const APP = {
name: pkg.name,
version: pkg.version,
title: pkg.title,
description: pkg.description,
author: pkg.author,
keywords: pkg.keywords.join(',')
};
const DEFAULT_LANGUAGE = 'en';
const LOCALE_PATH = 'public/locales';
const LANGUAGES = ['en', 'zh'];
const otherLanguages = LANGUAGES.filter(lang => lang !== DEFAULT_LANGUAGE);
module.exports = {
assetPrefix: publicPath,
distDir,
poweredByHeader: false,
env: {
...APP,
DEFAULT_LANGUAGE,
LOCALE_PATH,
LANGUAGES,
API_URL: apiUrl,
PUBLIC_PATH: publicPath,
API_TOKEN_KEY: process.env.API_TOKEN_KEY || ''
},
exportPathMap: defaultPathMap => ({
...defaultPathMap,
...Object.entries(defaultPathMap).reduce((prev, [path, router]) => {
otherLanguages.forEach(lang => (prev[`/${lang}${path}`] = router));
return prev;
}, {})
}),
experimental: {
basePath: publicPath
},
webpack: config => {
const WorkerPlugin = require('worker-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
config.resolve = config.resolve || {};
config.resolve.alias = config.resolve.alias || {};
config.resolve.alias['~'] = path.resolve(__dirname);
config.node = Object.assign({}, config.node, {
// eslint-disable-next-line @typescript-eslint/naming-convention
child_process: 'empty',
fs: 'empty'
});
config.plugins = [
...(config.plugins || []),
new WorkerPlugin({
globalObject: 'self'
}),
new CopyWebpackPlugin({
patterns: [
{
context: path.dirname(require('enhanced-resolve').sync(__dirname, '@visualdl/netron')),
from: '**/*',
to: 'static/netron',
toType: 'dir'
}
]
})
];
return config;
}
};
{
"name": "@visualdl/core",
"version": "2.0.0",
"title": "VisualDL",
"description": "A platform to visualize the deep learning process and result.",
"keywords": [
"visualdl",
......@@ -24,90 +23,87 @@
"directory": "frontend/packages/core"
},
"scripts": {
"dev": "next",
"build": "echo 'This package does not need to build. Please use @visualdl/server or @visualdl/serverless to build from source.'; echo 'If you want to test build function, please run `build:next` instead.'",
"build:next": "next build",
"export": "next export",
"start": "next start",
"test": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest",
"test:coverage": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage"
"dev": "snowpack dev",
"build": "snowpack build",
"start": "yarn dev --reload",
"test": "jest"
},
"main": "dist/index.html",
"files": [
"dist"
],
"dependencies": {
"@tippyjs/react": "4.1.0",
"@visualdl/i18n": "2.0.0",
"@visualdl/netron": "2.0.0",
"@visualdl/wasm": "2.0.0",
"bignumber.js": "9.0.0",
"d3-format": "1.4.4",
"echarts": "4.8.0",
"d3-format": "2.0.0",
"echarts": "4.9.0",
"echarts-gl": "1.1.1",
"eventemitter3": "4.0.4",
"eventemitter3": "4.0.7",
"file-saver": "2.0.2",
"isomorphic-unfetch": "3.0.0",
"lodash": "4.17.19",
"i18next": "19.7.0",
"i18next-browser-languagedetector": "6.0.1",
"i18next-fetch-backend": "3.0.0",
"lodash": "4.17.20",
"mime-types": "2.1.27",
"moment": "2.27.0",
"next": "9.4.4",
"nprogress": "0.2.0",
"polished": "3.6.5",
"prop-types": "15.7.2",
"polished": "3.6.6",
"query-string": "6.13.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-helmet": "6.1.0",
"react-i18next": "11.7.2",
"react-input-range": "1.3.0",
"react-is": "16.13.1",
"react-rangeslider": "2.2.0",
"react-router-dom": "5.2.0",
"react-spinners": "0.9.0",
"react-toastify": "6.0.8",
"save-svg-as-png": "1.4.17",
"styled-components": "5.1.1",
"swr": "0.3.0",
"tippy.js": "6.2.6"
},
"devDependencies": {
"@babel/core": "7.11.1",
"@babel/core": "7.11.5",
"@babel/preset-react": "7.10.4",
"@snowpack/app-scripts-react": "1.10.0",
"@snowpack/plugin-dotenv": "2.0.1",
"@snowpack/plugin-run-script": "2.1.1",
"@svgr/core": "5.4.0",
"@testing-library/jest-dom": "5.11.4",
"@testing-library/react": "10.4.9",
"@types/d3-format": "1.3.1",
"@types/echarts": "4.6.4",
"@types/enzyme": "3.10.5",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/echarts": "4.6.5",
"@types/file-saver": "2.0.1",
"@types/jest": "26.0.8",
"@types/lodash": "4.14.158",
"@types/jest": "26.0.13",
"@types/loadable__component": "5.13.0",
"@types/lodash": "4.14.161",
"@types/mime-types": "2.1.0",
"@types/node": "14.0.27",
"@types/nprogress": "0.2.0",
"@types/react": "16.9.44",
"@types/react": "16.9.49",
"@types/react-dom": "16.9.8",
"@types/react-helmet": "6.1.0",
"@types/react-rangeslider": "2.2.3",
"@types/styled-components": "5.1.2",
"@types/react-router-dom": "5.1.5",
"@types/snowpack-env": "2.3.0",
"@types/styled-components": "5.1.3",
"@visualdl/mock": "2.0.0",
"babel-plugin-emotion": "10.0.33",
"babel-plugin-styled-components": "1.11.1",
"babel-plugin-typescript-to-proptypes": "1.4.0",
"copy-webpack-plugin": "6.0.3",
"core-js": "3.6.5",
"cross-env": "7.0.2",
"css-loader": "4.2.0",
"enhanced-resolve": "4.3.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"enzyme-to-json": "3.5.0",
"jest": "26.2.2",
"ora": "4.0.5",
"ts-jest": "26.1.4",
"typescript": "3.9.7",
"worker-plugin": "4.0.3"
"express": "4.17.1",
"fs-extra": "9.0.1",
"jest": "26.4.2",
"snowpack": "2.10.1",
"typescript": "4.0.2",
"yargs": "15.4.1"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import {headerHeight, rem} from '~/utils/style';
import React from 'react';
import styled from 'styled-components';
const Error = styled.div`
height: calc(100vh - ${headerHeight});
display: flex;
justify-content: center;
align-items: center;
font-size: ${rem(20)};
`;
const Error404: NextI18NextPage = () => {
const {t} = useTranslation('errors');
return <Error>404 - {t('errors:page-not-found')}</Error>;
};
// 404 page cannot have getInitialProps or getServerSideProps
// we need next-i18next support getStaticProps
// https://github.com/zeit/next.js/blob/master/errors/404-get-initial-props.md
// https://github.com/isaachinman/next-i18next/issues/652
// Error404.getInitialProps = () => {
// return {
// namespacesRequired: ['errors']
// };
// };
export default Error404;
import {Router, appWithTranslation} from '~/utils/i18n';
import {fetcher, getApiToken} 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 React from 'react';
import {SWRConfig} from 'swr';
import {ToastContainer} from 'react-toastify';
import queryString from 'query-string';
import {withRouter} from 'next/router';
const API_TOKEN_KEY = process.env.API_TOKEN_KEY;
const PUBLIC_PATH = process.env.PUBLIC_PATH;
class VDLApp extends App {
componentDidMount() {
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', (url: string) => {
NProgress.done();
if (API_TOKEN_KEY) {
const id = getApiToken();
const parsed = queryString.parseUrl(url);
if (id && !parsed.query[API_TOKEN_KEY]) {
this.props.router.replace(
queryString.stringifyUrl({
url: parsed.url,
query: {
...parsed.query,
[API_TOKEN_KEY]: id
}
}),
undefined,
{shallow: true}
);
}
}
});
Router.events.on('routeChangeError', () => NProgress.done());
}
render() {
const {Component, pageProps} = this.props;
return (
<>
<Head>
<title>{process.env.title}</title>
<link rel="shortcut icon" href={`${PUBLIC_PATH}/favicon.ico`} />
<meta
name="viewport"
content="width=device-width,minimum-scale=1,maximum-scale=1,initial-scale=1,user-scalable=no,shrink-to-fit=no"
/>
<meta name="description" content={process.env.description} />
<meta name="keywords" content={process.env.keywords} />
<meta name="author" content={process.env.author} />
</Head>
<GlobalStyle />
<SWRConfig
value={{
fetcher,
revalidateOnFocus: false,
revalidateOnReconnect: false
}}
>
<Layout>
<Component {...pageProps} />
</Layout>
<ToastContainer position="top-center" hideProgressBar closeOnClick={false} />
</SWRConfig>
</>
);
}
}
export default appWithTranslation(withRouter(VDLApp));
import Document, {
DocumentContext,
DocumentInitialProps,
DocumentProps,
Head,
Html,
Main,
NextScript
} from 'next/document';
import {ServerStyleSheet} from '~/utils/style';
interface VDLDocumentProps extends DocumentProps {
language: string;
languageDir: string;
}
export default class VDLDocument extends Document<VDLDocumentProps> {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
// https://github.com/zeit/next.js/blob/canary/examples/with-typescript-styled-components/pages/_document.tsx
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
});
const initialProps = await Document.getInitialProps(ctx);
// steal from https://github.com/isaachinman/next-i18next/issues/20#issuecomment-558799264
// FIXME: https://github.com/i18next/i18next-express-middleware/blob/master/src/index.js#L23-L26
const additionalProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...((ctx.res as any)?.locals || {})
};
return {
...initialProps,
...additionalProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
};
} finally {
sheet.seal();
}
}
render(): JSX.Element {
const {language, languageDir} = this.props;
return (
<Html lang={language} dir={languageDir}>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import {headerHeight, rem} from '~/utils/style';
import styled from 'styled-components';
const ErrorDiv = styled.div`
height: calc(100vh - ${headerHeight});
display: flex;
justify-content: center;
align-items: center;
font-size: ${rem(20)};
`;
interface ErrorProps {
statusCode?: number | null;
}
const Error: NextI18NextPage<ErrorProps> = ({statusCode}) => {
const {t} = useTranslation('errors');
return (
<ErrorDiv>
{statusCode ? t('errors:error-with-status', {statusCode}) : t('errors:error-without-status')}
</ErrorDiv>
);
};
Error.getInitialProps = ({res, err}) => {
let statusCode = null;
if (res) {
({statusCode} = res);
} else if (err) {
({statusCode} = err);
}
return {
namespacesRequired: ['errors'],
statusCode
};
};
export default Error;
import mock from '@visualdl/mock';
const delay = Number.parseInt(process.env.DELAY || '', 10);
export default mock({delay: delay ? () => Math.random() * delay : 0});
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>audio</title>
<path d="M8.628 10.97c-0.333 0-0.653 0.14-0.889 0.391s-0.368 0.589-0.368 0.943v10.541c0 0.737 0.563 1.334 1.258 1.334s1.258-0.597 1.258-1.334v-10.539c0.001-0.354-0.132-0.694-0.368-0.945s-0.557-0.391-0.89-0.391zM16 0c-0.333 0-0.653 0.141-0.889 0.391s-0.368 0.589-0.368 0.943v29.333c0 0.736 0.563 1.333 1.257 1.333s1.257-0.597 1.257-1.333v-29.333c0-0.354-0.132-0.693-0.368-0.943s-0.555-0.391-0.889-0.391zM30.743 13.781c-0.334-0.001-0.654 0.14-0.89 0.391s-0.369 0.591-0.368 0.945v5.773c0 0.737 0.563 1.334 1.258 1.334s1.258-0.597 1.258-1.334v-5.775c0.001-0.354-0.132-0.694-0.367-0.944s-0.556-0.391-0.889-0.391v0.002zM1.257 13.781c-0.334 0-0.654 0.141-0.889 0.391s-0.368 0.59-0.367 0.944v5.773c0 0.737 0.563 1.334 1.258 1.334s1.258-0.597 1.258-1.334v-5.775c0.001-0.354-0.132-0.694-0.368-0.945s-0.557-0.391-0.89-0.391v0.002zM23.372 5.307c-0.334-0.001-0.654 0.14-0.89 0.391s-0.369 0.591-0.368 0.945v20.113c0 0.738 0.563 1.335 1.259 1.335s1.259-0.598 1.259-1.335v-20.113c0.001-0.354-0.132-0.694-0.368-0.945s-0.557-0.391-0.89-0.391z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="44" height="32" viewBox="0 0 44 32">
<title>check-mark</title>
<path d="M17.849 30.793l-14.849-14.849 3.712-3.712 11.136 11.136 22.275-22.272 3.712 3.712z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>chevron-down</title>
<path d="M16 20.8l-13.714-14.4-2.286 2.4 16 16.8 16-16.8-2.286-2.4z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>close</title>
<path d="M32 0.99l-1-0.99-15 15.010-15.010-15.010-0.99 0.99 15.010 15.010-15.010 15 0.99 1 15.010-15 15 15 1-1-15-15 15-15.010z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>dimension</title>
<path d="M1.067 0v0c0.64 0 1.067 0.427 1.067 1.067v29.867c0 0.64-0.427 1.067-1.067 1.067v0c-0.64 0-1.067-0.427-1.067-1.067v-29.867c0-0.64 0.427-1.067 1.067-1.067z"></path>
<path d="M32 30.933v0c0 0.64-0.427 1.067-1.067 1.067h-29.867c-0.64 0-1.067-0.427-1.067-1.067v0c0-0.64 0.427-1.067 1.067-1.067h29.867c0.64 0 1.067 0.427 1.067 1.067z"></path>
<path d="M6.4 20.267c-0.213 0-0.427 0-0.64-0.213-0.427-0.427-0.64-1.067-0.213-1.493l7.040-8.533c0.213-0.213 0.64-0.427 0.853-0.427s0.64 0.213 0.853 0.427l6.4 7.467 6.187-7.467c0.427-0.427 1.067-0.427 1.493-0.213 0.427 0.427 0.427 1.067 0.213 1.493l-7.040 8.533c-0.213 0.213-0.427 0.427-0.853 0.427-0.213 0-0.64-0.213-0.853-0.427l-6.4-7.467-6.187 7.467c-0.213 0.213-0.64 0.427-0.853 0.427z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>download</title>
<path d="M30.844 29.689h-29.69c-0.637 0-1.154 0.517-1.154 1.156 0 0.638 0.517 1.155 1.154 1.155h29.692c0.637 0 1.154-0.517 1.154-1.155 0-0.639-0.516-1.156-1.154-1.156h-0.002zM15.976 27.572c0.329 0 0.626-0.135 0.839-0.354l12.054-12.070c0.216-0.214 0.351-0.511 0.352-0.839 0-0.654-0.533-1.184-1.183-1.184-0.329-0.001-0.624 0.134-0.837 0.348l-10.043 10.056v-22.343c0-0.656-0.529-1.187-1.181-1.187-0.654 0-1.182 0.531-1.182 1.185l0.001 22.344-10.042-10.056c-0.213-0.214-0.511-0.348-0.837-0.347-0.653-0.001-1.182 0.53-1.184 1.181 0 0.329 0.134 0.628 0.35 0.84l12.052 12.072c0.215 0.217 0.512 0.353 0.841 0.353z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>histogram</title>
<path d="M0.8 28.2h30.2c0.4 0 0.8 0.4 0.8 1s-0.4 1-0.8 1h-30.2c-0.4 0-0.8-0.4-0.8-1s0.4-1 0.8-1z"></path>
<path d="M5 23.8c-1.6 0-3-1.4-3-3v-6.6c0-1.6 1.4-3 3-3s3 1.4 3 3v6.6c0 1.8-1.4 3-3 3zM5 13.2c-0.6 0-1 0.6-1 1v6.6c0 0.6 0.4 1 1 1s1-0.4 1-1v-6.6c0-0.4-0.4-1-1-1z"></path>
<path d="M27 23.8c-1.6 0-3-1.4-3-3v-12.8c0-1.6 1.4-3 3-3s3 1.4 3 3v12.8c0 1.8-1.4 3-3 3zM27 7c-0.6 0-1 0.4-1 1v12.8c0 0.6 0.4 1 1 1s1-0.4 1-1v-12.8c0-0.6-0.4-1-1-1z"></path>
<path d="M16 23.8c-1.6 0-3-1.4-3-3v-17c0-1.6 1.4-3 3-3s3 1.4 3 3v17c0 1.8-1.4 3-3 3zM16 2.8c-0.6 0-1 0.4-1 1v17c0 0.6 0.4 1 1 1s1-0.4 1-1v-17c0-0.6-0.4-1-1-1z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="34" height="32" viewBox="0 0 34 32">
<title>image</title>
<path d="M30.933 32h-27.733c-1.707 0-3.2-1.493-3.2-3.2v-25.6c0-1.707 1.493-3.2 3.2-3.2h27.733c1.707 0 3.2 1.493 3.2 3.2v25.6c0 1.707-1.493 3.2-3.2 3.2zM3.2 2.133c-0.64 0-1.067 0.427-1.067 1.067v25.6c0 0.64 0.427 1.067 1.067 1.067h27.733c0.64 0 1.067-0.427 1.067-1.067v-25.6c0-0.64-0.427-1.067-1.067-1.067h-27.733z"></path>
<path d="M24.533 13.867c-2.347 0-4.267-1.92-4.267-4.267s1.92-4.267 4.267-4.267 4.267 1.92 4.267 4.267-1.92 4.267-4.267 4.267zM24.533 7.467c-1.28 0-2.133 0.853-2.133 2.133s0.853 2.133 2.133 2.133 2.133-0.853 2.133-2.133-0.853-2.133-2.133-2.133z"></path>
<path d="M31.36 31.36l-8.533-10.667-5.973 6.187-6.4-9.813-7.467 13.653-1.92-1.067 9.173-16.64 7.040 10.453 5.547-6.187 10.24 12.8z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="34" height="32" viewBox="0 0 34 32">
<title>log-axis</title>
<path d="M32.36 4.435h-22.058c-0.316-2.522-2.448-4.454-5.030-4.454-2.799 0-5.068 2.269-5.068 5.068s2.269 5.068 5.068 5.068c2.11 0 3.919-1.29 4.682-3.124l0.012-0.034h22.394c0.697 0 1.262-0.565 1.262-1.262s-0.565-1.262-1.262-1.262v0zM5.193 7.589c-1.366-0.035-2.46-1.151-2.46-2.522 0-1.394 1.13-2.523 2.523-2.523s2.523 1.129 2.523 2.522v0c-0.024 1.399-1.163 2.523-2.565 2.523-0.008 0-0.015-0-0.022-0h0.001z"></path>
<path d="M7.506 21.047c-0.697 0-1.262 0.565-1.262 1.262s0.565 1.262 1.262 1.262h18.63c0.697 0 1.262-0.565 1.262-1.262s-0.565-1.262-1.262-1.262v0z"></path>
<path d="M5.424 12.636c-0.697 0-1.262 0.565-1.262 1.262s0.565 1.262 1.262 1.262h22.815c0.697 0 1.262-0.565 1.262-1.262s-0.565-1.262-1.262-1.262v0z"></path>
<path d="M11.669 29.457c-0.697 0-1.262 0.565-1.262 1.262s0.565 1.262 1.262 1.262h10.324c0.697 0 1.262-0.565 1.262-1.262s-0.565-1.262-1.262-1.262v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>maximize</title>
<path d="M19.174 31.97c-0.662 0-1.198-0.536-1.198-1.198s0.536-1.198 1.198-1.198h10.241l0.2-10.38c0.039-0.638 0.542-1.148 1.173-1.197l0.004-0c0.636 0.050 1.139 0.559 1.178 1.194l0 0.004v11.139c0 0.012 0 0.026 0 0.040 0 0.86-0.697 1.557-1.557 1.557-0.007 0-0.014-0-0.022-0h0.001zM0.030 12.806v-11.179c-0-0.006-0-0.013-0-0.020 0-0.871 0.706-1.577 1.577-1.577 0.007 0 0.014 0 0.021 0h11.178c0.662 0 1.198 0.536 1.198 1.198s-0.536 1.198-1.198 1.198h-10.38v10.38c0 0.662-0.536 1.198-1.198 1.198s-1.198-0.536-1.198-1.198v0zM1.627 31.97c-0.006 0-0.013 0-0.020 0-0.871 0-1.577-0.706-1.577-1.577 0-0.007 0-0.014 0-0.021v0.001-9.183c0-0.662 0.536-1.198 1.198-1.198s1.198 0.536 1.198 1.198v6.787l8.185-8.185c0.205-0.177 0.474-0.285 0.769-0.285s0.564 0.108 0.77 0.287l-0.001-0.001c0.174 0.206 0.279 0.475 0.279 0.769s-0.105 0.562-0.281 0.77l0.002-0.002-8.185 8.185h6.787c0.662 0 1.198 0.536 1.198 1.198s-0.536 1.198-1.198 1.198v0zM29.574 10.81v-6.787l-8.185 8.185c-0.189 0.111-0.416 0.176-0.658 0.176-0.338 0-0.647-0.127-0.88-0.337l0.001 0.001c-0.174-0.188-0.282-0.441-0.282-0.719s0.107-0.531 0.282-0.719l-0.001 0.001 8.185-8.185h-6.787c-0.662 0-1.198-0.536-1.198-1.198s0.536-1.198 1.198-1.198h9.183c0.006-0 0.013-0 0.020-0 0.871 0 1.577 0.706 1.577 1.577 0 0.007-0 0.014-0 0.021v-0.001 9.183c0 0.662-0.536 1.198-1.198 1.198s-1.198-0.536-1.198-1.198v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>minimize</title>
<path d="M19.176 31.979c-0.662 0-1.198-0.537-1.198-1.198s0.537-1.198 1.198-1.198h10.246l0.2-10.386c0.039-0.639 0.542-1.148 1.174-1.198l0.004-0c0.636 0.050 1.139 0.56 1.178 1.195l0 0.004v11.185c0 0.012 0.001 0.026 0.001 0.040 0 0.86-0.698 1.558-1.558 1.558-0.007 0-0.014-0-0.022-0h0.001z"></path>
<path d="M0.021 12.804v-11.185c-0-0.006-0-0.013-0-0.020 0-0.871 0.706-1.578 1.578-1.578 0.007 0 0.014 0 0.021 0h11.184c0.641 0.049 1.149 0.557 1.198 1.194l0 0.004c-0.049 0.641-0.557 1.149-1.194 1.198l-0.004 0h-10.386v10.386c-0.049 0.641-0.557 1.149-1.194 1.198l-0.004 0c-0.641-0.049-1.149-0.557-1.198-1.194l-0-0.004z"></path>
<path d="M10.867 19.535c0.006-0 0.013-0 0.020-0 0.871 0 1.578 0.706 1.578 1.578 0 0.007-0 0.014-0 0.021v-0.001 9.188c0 0.662-0.537 1.198-1.198 1.198s-1.198-0.537-1.198-1.198v0-6.791l-8.189 8.189c-0.208 0.179-0.481 0.288-0.779 0.288s-0.571-0.109-0.781-0.289l0.002 0.001c-0.17-0.208-0.273-0.476-0.273-0.769s0.103-0.561 0.275-0.771l-0.002 0.002 8.189-8.189h-6.831c-0.662 0-1.198-0.537-1.198-1.198s0.537-1.198 1.198-1.198v0z"></path>
<path d="M21.992 1.599v6.791l8.189-8.189c0.192-0.115 0.423-0.183 0.671-0.183 0.342 0 0.654 0.13 0.888 0.344l-0.001-0.001c0.175 0.188 0.282 0.441 0.282 0.719s-0.107 0.531-0.282 0.72l0.001-0.001-8.209 8.209h6.791c0.662 0 1.198 0.537 1.198 1.198s-0.537 1.198-1.198 1.198h-9.188c-0.006 0-0.013 0-0.020 0-0.871 0-1.578-0.706-1.578-1.578 0-0.007 0-0.014 0-0.021v0.001-9.208c0-0.662 0.537-1.198 1.198-1.198s1.198 0.537 1.198 1.198v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>minus</title>
<path d="M1.45 14.55h29.1c0.801 0 1.45 0.649 1.45 1.45s-0.649 1.45-1.45 1.45h-29.1c-0.801 0-1.45-0.649-1.45-1.45s0.649-1.45 1.45-1.45v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>mute</title>
<path d="M7.009 10.023l7.296-5.856c0.343-0.275 0.813-0.329 1.21-0.139s0.649 0.591 0.649 1.031v21.801c-0 0.439-0.252 0.839-0.648 1.029s-0.865 0.137-1.208-0.137l-7.497-5.984h-5.667c-0.631 0-1.143-0.512-1.143-1.143v-9.459c0-0.631 0.512-1.143 1.143-1.143h5.866z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>pause</title>
<path d="M7 0c2.209 0 4 1.791 4 4v24c0 2.209-1.791 4-4 4s-4-1.791-4-4v-24c0-2.209 1.791-4 4-4z"></path>
<path d="M25 0c2.209 0 4 1.791 4 4v24c0 2.209-1.791 4-4 4s-4-1.791-4-4v-24c0-2.209 1.791-4 4-4z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>play</title>
<path d="M27.184 19.287l-17.96 12.042c-1.816 1.217-4.275 0.732-5.492-1.083-0.437-0.652-0.671-1.419-0.671-2.204v-24.083c0-2.186 1.772-3.958 3.958-3.958 0.785 0 1.552 0.233 2.204 0.671l17.96 12.042c1.816 1.217 2.301 3.676 1.084 5.492-0.287 0.428-0.655 0.796-1.083 1.083z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>plus</title>
<path d="M16 0c0.801 0 1.45 0.649 1.45 1.45v13.1h13.1c0.801 0 1.45 0.649 1.45 1.45s-0.649 1.45-1.45 1.45h-13.1v13.1c0 0.801-0.649 1.45-1.45 1.45s-1.45-0.649-1.45-1.45v0-13.1h-13.1c-0.801 0-1.45-0.649-1.45-1.45s0.649-1.45 1.45-1.45h13.1v-13.1c0-0.801 0.649-1.45 1.45-1.45v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>question-circle</title>
<path d="M14.45 24v0c-0.001 0.015-0.001 0.032-0.001 0.050 0 0.746 0.605 1.351 1.351 1.351 0.729 0 1.322-0.577 1.35-1.298l0-0.003v-0.1c0-0.746-0.604-1.35-1.35-1.35s-1.35 0.604-1.35 1.35v0zM15.84 20.71h-0.11c-0.568-0.060-1.006-0.536-1.006-1.114 0-0.041 0.002-0.081 0.006-0.121l-0 0.005c0.15-1.56 1.29-2.82 2.55-4.080 2-2 2.050-2.65 2.090-3.33 0.002-0.035 0.002-0.077 0.002-0.118 0-0.795-0.318-1.516-0.833-2.042l0 0.001c-0.639-0.648-1.527-1.050-2.508-1.050-0.011 0-0.022 0-0.034 0h0.002c-1.853 0.006-3.354 1.507-3.36 3.359v0.001c-0.056 0.577-0.538 1.024-1.125 1.024s-1.069-0.447-1.125-1.019l-0-0.005c-0-0.021-0-0.045-0-0.069 0-1.537 0.627-2.928 1.64-3.93l0-0c1.008-1.019 2.407-1.65 3.953-1.65 0.017 0 0.033 0 0.049 0l-0.003-0c0.002 0 0.005 0 0.008 0 1.639 0 3.119 0.682 4.17 1.778l0.002 0.002c0.895 0.917 1.447 2.171 1.447 3.555 0 0.097-0.003 0.193-0.008 0.288l0.001-0.013c-0.070 1.44-0.52 2.59-2.74 4.8-1.17 1.17-1.91 2.020-1.91 2.75-0.072 0.563-0.545 0.994-1.119 1h-0.001zM25.72 25.71v0c-2.476 2.472-5.895 4-9.671 4-0.017 0-0.035-0-0.052-0h0.003c-0.017 0-0.037 0-0.058 0-3.773 0-7.189-1.529-9.662-4l0 0c-2.472-2.482-4-5.906-4-9.687 0-0.015 0-0.030 0-0.045v0.002c0.031-7.57 6.175-13.695 13.75-13.695 3.781 0 7.205 1.526 9.691 3.996l-0.001-0.001c2.472 2.476 4 5.895 4 9.671 0 0.017-0 0.035-0 0.052v-0.003c0 0.021 0 0.045 0 0.070 0 3.769-1.529 7.181-4 9.65l-0 0zM16 0v0c-8.837 0-16 7.163-16 16s7.163 16 16 16c8.837 0 16-7.163 16-16v0c0-8.837-7.163-16-16-16v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>reduction</title>
<path d="M1.067 0v0c0.64 0 1.067 0.427 1.067 1.067v29.867c0 0.64-0.427 1.067-1.067 1.067v0c-0.64 0-1.067-0.427-1.067-1.067v-29.867c0-0.64 0.427-1.067 1.067-1.067z"></path>
<path d="M32 30.933v0c0 0.64-0.427 1.067-1.067 1.067h-29.867c-0.64 0-1.067-0.427-1.067-1.067v0c0-0.64 0.427-1.067 1.067-1.067h29.867c0.64 0 1.067 0.427 1.067 1.067z"></path>
<path d="M26.24 23.68c-0.427 0-0.64-0.213-0.853-0.427l-8.747-14.933h-8.533c-0.64 0-1.067-0.427-1.067-1.067s0.427-1.067 1.067-1.067h8.96c0.427 0 0.64 0.213 0.853 0.427l9.173 15.573c0.213 0.427 0.213 1.067-0.427 1.493 0 0-0.213 0-0.427 0z"></path>
<path d="M27.733 17.28v0c0.64 0.213 0.853 0.64 0.853 1.28l-1.067 4.053c-0.213 0.64-0.64 0.853-1.28 0.853v0c-0.64-0.213-0.853-0.64-0.853-1.28l1.067-4.053c0-0.64 0.64-1.067 1.28-0.853z"></path>
<path d="M20.693 21.547v0c0 0.64 0.213 1.067 0.853 1.28l4.267 0.64c0.64 0 1.067-0.213 1.28-0.853v0c0-0.64-0.213-1.067-0.853-1.28l-4.267-0.64c-0.64 0-1.067 0.213-1.28 0.853z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>refresh</title>
<path d="M32 12.571v-8l-2.606 2.606c-2.907-4.348-7.796-7.172-13.344-7.172-8.837 0-16 7.163-16 16s7.163 16 16 16c6.672 0 12.39-4.084 14.791-9.888l0.039-0.106c0.063-0.152 0.099-0.328 0.099-0.513 0-0.571-0.349-1.060-0.845-1.267l-0.009-0.003c-0.152-0.063-0.328-0.099-0.513-0.099-0.571 0-1.060 0.349-1.267 0.845l-0.003 0.009c-2.030 4.895-6.771 8.275-12.301 8.275-7.334 0-13.28-5.946-13.28-13.28s5.946-13.28 13.28-13.28c4.81 0 9.023 2.557 11.353 6.387l0.033 0.059-3.429 3.406z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>restore-size</title>
<path d="M29.8 0h-27.6c-1.215 0-2.2 0.985-2.2 2.2v0 27.6c0 1.215 0.985 2.2 2.2 2.2h27.6c1.215 0 2.2-0.985 2.2-2.2v0-27.6c0-1.215-0.985-2.2-2.2-2.2v0zM2.2 29.8v-27.6h27.6v27.6z"></path>
<path d="M10.78 4c-0.687 0.011-1.24 0.571-1.24 1.26 0 0 0 0 0 0v0 4.2h-4.32c-0.679 0.022-1.22 0.578-1.22 1.259 0 0.007 0 0.014 0 0.022v-0.001c-0 0.006-0 0.013-0 0.020 0 0.678 0.544 1.229 1.219 1.24h5.561c0.676-0.011 1.22-0.562 1.22-1.24 0-0.007-0-0.014-0-0.021v0.001-5.48c0-0.006 0-0.013 0-0.020 0-0.678-0.544-1.229-1.219-1.24h-0.001zM26.78 9.46h-4.32v-4.2c0-0 0-0 0-0 0-0.689-0.553-1.249-1.239-1.26h-0.001c-0.676 0.011-1.22 0.562-1.22 1.24 0 0.007 0 0.014 0 0.021v-0.001 5.48c-0 0.006-0 0.013-0 0.020 0 0.678 0.544 1.229 1.219 1.24h5.561c0.676-0.011 1.22-0.562 1.22-1.24 0-0.007-0-0.014-0-0.021v0.001c0-0.006 0-0.013 0-0.021 0-0.682-0.542-1.237-1.218-1.259l-0.002-0zM10.78 20h-5.56c-0.676 0.011-1.22 0.562-1.22 1.24 0 0.007 0 0.014 0 0.021v-0.001c-0 0.006-0 0.013-0 0.021 0 0.682 0.542 1.237 1.218 1.259l0.002 0h4.32v4.2c0 0 0 0 0 0 0 0.689 0.553 1.249 1.239 1.26h0.001c0.676-0.011 1.22-0.562 1.22-1.24 0-0.007-0-0.014-0-0.021v0.001-5.48c0-0.006 0-0.013 0-0.020 0-0.678-0.544-1.229-1.219-1.24h-0.001zM26.78 20h-5.56c-0.676 0.011-1.22 0.562-1.22 1.24 0 0.007 0 0.014 0 0.021v-0.001 5.48c-0 0.006-0 0.013-0 0.020 0 0.678 0.544 1.229 1.219 1.24h0.001c0.687-0.011 1.24-0.571 1.24-1.26 0-0 0-0 0-0v0-4.2h4.32c0.679-0.022 1.22-0.578 1.22-1.259 0-0.007-0-0.015-0-0.022v0.001c0-0.006 0-0.013 0-0.020 0-0.678-0.544-1.229-1.219-1.24h-0.001z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 28 32">
<title>revert</title>
<path d="M26.846 14.886c-0.373-0.141-0.791-0.071-1.099 0.184s-0.457 0.656-0.392 1.053c0.099 0.595 0.149 1.244 0.149 1.884 0.009 3.117-1.218 6.108-3.407 8.307-2.178 2.21-5.142 3.448-8.231 3.437-3.088 0.009-6.052-1.23-8.231-3.44-2.189-2.199-3.416-5.191-3.407-8.309 0-3.137 1.21-6.091 3.407-8.309 2.179-2.21 5.142-3.448 8.231-3.439 0.129 0 0.26 0.003 0.39 0.007l-2.532 2.553c-0.209 0.211-0.327 0.497-0.327 0.795s0.118 0.585 0.327 0.795c0.436 0.44 1.142 0.44 1.576 0l4.204-4.244c0.209-0.211 0.327-0.497 0.327-0.795s-0.118-0.585-0.327-0.795l-4.205-4.239c-0.209-0.211-0.492-0.33-0.788-0.33s-0.579 0.119-0.788 0.33c-0.209 0.211-0.327 0.497-0.327 0.795s0.118 0.585 0.327 0.795l2.064 2.079c-3.65 0.018-7.144 1.492-9.722 4.1-2.607 2.623-4.070 6.186-4.066 9.901-0.005 3.714 1.457 7.276 4.061 9.899 2.598 2.629 6.127 4.105 9.806 4.1 1.873 0 3.686-0.369 5.398-1.1 1.648-0.7 3.145-1.718 4.407-2.996 1.266-1.275 2.275-2.785 2.972-4.447 0.725-1.725 1.090-3.56 1.090-5.451 0-0.771-0.062-1.531-0.18-2.253-0.065-0.395-0.335-0.726-0.707-0.867z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>scalar</title>
<path d="M29 32h-26c-1.6 0-3-1.4-3-3v-26c0-1.6 1.4-3 3-3h26c1.6 0 3 1.4 3 3v26c0 1.6-1.4 3-3 3zM3 2c-0.6 0-1 0.4-1 1v26c0 0.6 0.4 1 1 1h26c0.6 0 1-0.4 1-1v-26c0-0.6-0.4-1-1-1h-26z"></path>
<path d="M6 21c-0.2 0-0.4 0-0.6-0.2-0.4-0.4-0.4-1-0.2-1.4l6.6-8c0.2-0.2 0.6-0.4 0.8-0.4s0.6 0.2 0.8 0.4l6 7 5.8-7c0.4-0.4 1-0.4 1.4-0.2 0.4 0.4 0.4 1 0.2 1.4l-6.6 8c-0.2 0.2-0.4 0.4-0.8 0.4-0.2 0-0.6-0.2-0.8-0.4l-6-7-5.8 7c-0.2 0.2-0.6 0.4-0.8 0.4z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>search</title>
<path d="M30.293 31.787l-5.547-5.547c-2.773 2.347-6.187 3.627-9.813 3.627-8.32 0-14.933-6.613-14.933-14.933s6.613-14.933 14.933-14.933c8.32 0 14.933 6.613 14.933 14.933 0 3.627-1.28 7.040-3.627 9.813l5.547 5.547-1.493 1.493zM14.933 2.133c-7.040 0-12.8 5.76-12.8 12.8s5.76 12.8 12.8 12.8c2.987 0 5.973-1.067 8.32-3.2l1.28-1.28c2.133-2.347 3.2-5.333 3.2-8.32 0-7.040-5.76-12.8-12.8-12.8z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>text</title>
<path d="M10 8h12v2h-12v-2z"></path>
<path d="M8 24h16v2h-16v-2z"></path>
<path d="M8 6h2v6h-2v-6z"></path>
<path d="M22 6h2v6h-2v-6z"></path>
<path d="M15 9h2v12h-2v-12z"></path>
<path d="M30 32h-28c-1.2 0-2-0.8-2-2v-28c0-1.2 0.8-2 2-2h28c1.2 0 2 0.8 2 2v28c0 1.2-0.8 2-2 2zM2 2v28h28v-28h-28zM2 1v1c0 0 0 0 0 0v-1z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="39" height="32" viewBox="0 0 39 32">
<title>upload</title>
<path d="M31.702 10.073c-1.054-5.763-6.037-10.073-12.026-10.073s-10.972 4.31-12.015 9.998l-0.011 0.075c-4.341 0.982-7.534 4.806-7.534 9.376 0 0.372 0.021 0.739 0.062 1.1l-0.004-0.044c0.628 4.846 4.729 8.551 9.696 8.551 0.027 0 0.054-0 0.081-0l-0.004 0h2.993c0.675 0 1.222-0.547 1.222-1.222s-0.547-1.222-1.222-1.222v0 0h-3.054c-3.977 0-7.201-3.224-7.201-7.201s3.224-7.201 7.201-7.201v0c0-5.397 4.375-9.772 9.772-9.772s9.772 4.375 9.772 9.772v0c0.088-0.004 0.192-0.006 0.296-0.006 3.974 0 7.195 3.221 7.195 7.195s-3.221 7.195-7.195 7.195c-0.104 0-0.208-0.002-0.311-0.007l0.015 0h-3.665c-0.675 0-1.222 0.547-1.222 1.222s0.547 1.222 1.222 1.222v0h3.665c0.024 0 0.052 0 0.080 0 4.958 0 9.054-3.692 9.687-8.477l0.005-0.050c0.036-0.312 0.057-0.674 0.057-1.041 0-4.581-3.204-8.413-7.493-9.379l-0.064-0.012z"></path>
<path d="M20.208 14.972c-0.219-0.209-0.516-0.337-0.843-0.337s-0.624 0.129-0.843 0.338l0-0-3.396 3.445c-0.218 0.22-0.353 0.521-0.354 0.855v0c0.005 0.329 0.139 0.626 0.354 0.843l-0-0c0.219 0.209 0.516 0.337 0.843 0.337s0.624-0.129 0.843-0.338l-0 0 1.356-1.368v12.032c0 0.675 0.547 1.222 1.222 1.222s1.222-0.547 1.222-1.222v0-12.020l1.344 1.368c0.209 0.218 0.504 0.354 0.83 0.354 0.005 0 0.009-0 0.014-0h-0.001c0.331-0.008 0.628-0.147 0.843-0.366l0-0c0.212-0.22 0.343-0.519 0.343-0.849s-0.131-0.63-0.344-0.849l0 0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>volumn-low</title>
<path d="M7.009 10.023l7.296-5.856c0.343-0.275 0.813-0.329 1.21-0.139s0.649 0.591 0.649 1.031v21.801c-0 0.439-0.252 0.839-0.648 1.029s-0.865 0.137-1.208-0.137l-7.497-5.984h-5.667c-0.631 0-1.143-0.512-1.143-1.143v-9.459c0-0.631 0.512-1.143 1.143-1.143h5.866z"></path>
<path d="M21.671 23.007c-0.452 0.431-1.166 0.419-1.604-0.027s-0.436-1.16 0.003-1.604c1.472-1.441 2.3-3.415 2.297-5.475 0.003-1.908-0.707-3.748-1.992-5.159-0.28-0.301-0.377-0.73-0.253-1.123s0.449-0.688 0.852-0.774c0.402-0.086 0.82 0.051 1.093 0.359 1.667 1.831 2.59 4.22 2.586 6.696 0.003 2.674-1.072 5.235-2.982 7.106v0z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>volumn</title>
<path d="M7.009 10.023l7.296-5.856c0.343-0.275 0.813-0.329 1.21-0.139s0.649 0.591 0.649 1.031v21.801c-0 0.439-0.252 0.839-0.648 1.029s-0.865 0.137-1.208-0.137l-7.497-5.984h-5.667c-0.631 0-1.143-0.512-1.143-1.143v-9.459c0-0.631 0.512-1.143 1.143-1.143h5.866z"></path>
<path d="M21.671 23.007c-0.452 0.431-1.166 0.419-1.604-0.027s-0.436-1.16 0.003-1.604c1.472-1.441 2.3-3.415 2.297-5.475 0.003-1.908-0.707-3.748-1.992-5.159-0.28-0.301-0.377-0.73-0.253-1.123s0.449-0.688 0.852-0.774c0.402-0.086 0.82 0.051 1.093 0.359 1.667 1.831 2.59 4.22 2.586 6.696 0.003 2.674-1.072 5.235-2.982 7.106v0z"></path>
<path d="M27.104 27.962c-0.284 0.298-0.705 0.42-1.104 0.321s-0.714-0.405-0.824-0.801c-0.111-0.396-0-0.821 0.289-1.113 2.73-2.8 4.255-6.558 4.249-10.469 0-3.882-1.478-7.53-4.087-10.299-0.425-0.461-0.4-1.177 0.057-1.607s1.173-0.412 1.607 0.040c3.006 3.189 4.709 7.394 4.709 11.866 0 4.566-1.777 8.854-4.896 12.062z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>zoom-in</title>
<path d="M22.062 14.97h-5.073v-5.073c0-0.554-0.449-1.003-1.003-1.003s-1.003 0.449-1.003 1.003v0 5.073h-5.073c-0.554 0-1.003 0.449-1.003 1.003s0.449 1.003 1.003 1.003v0h5.073v5.013c0 0.554 0.449 1.003 1.003 1.003s1.003-0.449 1.003-1.003v0-5.013h5.013c0.554 0 1.003-0.449 1.003-1.003s-0.449-1.003-1.003-1.003v0z"></path>
<path d="M26.574 25.999c2.824-2.755 4.576-6.597 4.576-10.849 0-8.367-6.783-15.15-15.15-15.15s-15.15 6.783-15.15 15.15c0 8.367 6.783 15.15 15.15 15.15 3.258 0 6.276-1.028 8.746-2.778l-0.047 0.032 3.369 4.011c0.224 0.268 0.559 0.437 0.932 0.437 0.67 0 1.214-0.543 1.214-1.214 0-0.296-0.106-0.568-0.283-0.779l0.002 0.002zM24.268 25.126l-0.842 0.602c-2.102 1.497-4.723 2.393-7.553 2.393-7.243 0-13.114-5.872-13.114-13.114s5.871-13.114 13.114-13.114c7.243 0 13.114 5.872 13.114 13.114 0 3.689-1.523 7.022-3.975 9.405l-0.003 0.003z"></path>
</svg>
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="30" height="32" viewBox="0 0 30 32">
<title>zoom-out</title>
<path d="M21.218 14.97h-12.152c-0.554 0-1.003 0.449-1.003 1.003s0.449 1.003 1.003 1.003v0h12.152c0.554 0 1.003-0.449 1.003-1.003s-0.449-1.003-1.003-1.003v0z"></path>
<path d="M25.73 25.999c2.824-2.755 4.576-6.597 4.576-10.849 0-8.367-6.783-15.15-15.15-15.15s-15.15 6.783-15.15 15.15c0 8.367 6.783 15.15 15.15 15.15 3.258 0 6.276-1.028 8.746-2.778l-0.047 0.032 3.369 4.011c0.224 0.268 0.559 0.437 0.932 0.437 0.67 0 1.214-0.543 1.214-1.214 0-0.296-0.106-0.568-0.283-0.779l0.002 0.002zM23.424 25.126l-0.842 0.602c-2.102 1.497-4.723 2.393-7.553 2.393-7.243 0-13.114-5.872-13.114-13.114s5.872-13.114 13.114-13.114c7.243 0 13.114 5.872 13.114 13.114 0 3.689-1.523 7.022-3.975 9.405l-0.003 0.003z"></path>
</svg>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VisualDL</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="%PUBLIC_URL%/_dist_/index.js"></script>
</body>
</html>
{
"download-image": "Download image",
"ignore-outliers": "Ignore outliers in chart scaling",
"max": "Max",
"maximize": "Maximize",
"min": "Min",
"minimize": "Minimize",
"restore": "Selection restore",
"smoothed": "Smoothed",
......
{
"download-image": "下载图片",
"ignore-outliers": "图表缩放时忽略极端值",
"max": "最大值",
"maximize": "最大化",
"min": "最小值",
"minimize": "最小化",
"restore": "还原图表框选",
"smoothed": "Smoothed",
"smoothing": "平滑度",
"toggle-log-axis": "切换对数坐标轴",
"tooltip-sorting": "标签排序方法",
"tooltip-sorting": "详情数据排序",
"tooltip-sorting-value": {
"ascending": "升序",
"default": "默认",
......
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="vdl-icon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="reduction" d="M34.133 960v0c20.48 0 34.133-13.653 34.133-34.133v-955.733c0-20.48-13.653-34.133-34.133-34.133v0c-20.48 0-34.133 13.653-34.133 34.133v955.733c0 20.48 13.653 34.133 34.133 34.133zM1024-29.867v0c0-20.48-13.653-34.133-34.133-34.133h-955.733c-20.48 0-34.133 13.653-34.133 34.133v0c0 20.48 13.653 34.133 34.133 34.133h955.733c20.48 0 34.133-13.653 34.133-34.133zM839.68 202.24c-13.653 0-20.48 6.827-27.307 13.653l-279.893 477.867h-273.067c-20.48 0-34.133 13.653-34.133 34.133s13.653 34.133 34.133 34.133h286.72c13.653 0 20.48-6.827 27.307-13.653l293.547-498.347c6.827-13.653 6.827-34.133-13.653-47.787 0 0-6.827 0-13.653 0zM887.467 407.040v0c20.48-6.827 27.307-20.48 27.307-40.96l-34.133-129.707c-6.827-20.48-20.48-27.307-40.96-27.307v0c-20.48 6.827-27.307 20.48-27.307 40.96l34.133 129.707c0 20.48 20.48 34.133 40.96 27.307zM662.187 270.507v0c0-20.48 6.827-34.133 27.307-40.96l136.533-20.48c20.48 0 34.133 6.827 40.96 27.307v0c0 20.48-6.827 34.133-27.307 40.96l-136.533 20.48c-20.48 0-34.133-6.827-40.96-27.307z" />
<glyph unicode="&#xe901;" glyph-name="dimension" d="M34.133 960v0c20.48 0 34.133-13.653 34.133-34.133v-955.733c0-20.48-13.653-34.133-34.133-34.133v0c-20.48 0-34.133 13.653-34.133 34.133v955.733c0 20.48 13.653 34.133 34.133 34.133zM1024-29.867v0c0-20.48-13.653-34.133-34.133-34.133h-955.733c-20.48 0-34.133 13.653-34.133 34.133v0c0 20.48 13.653 34.133 34.133 34.133h955.733c20.48 0 34.133-13.653 34.133-34.133zM204.8 311.467c-6.827 0-13.653 0-20.48 6.827-13.653 13.653-20.48 34.133-6.827 47.787l225.28 273.067c6.827 6.827 20.48 13.653 27.307 13.653s20.48-6.827 27.307-13.653l204.8-238.933 197.973 238.933c13.653 13.653 34.133 13.653 47.787 6.827 13.653-13.653 13.653-34.133 6.827-47.787l-225.28-273.067c-6.827-6.827-13.653-13.653-27.307-13.653-6.827 0-20.48 6.827-27.307 13.653l-204.8 238.933-197.973-238.933c-6.827-6.827-20.48-13.653-27.307-13.653z" />
<glyph unicode="&#xe902;" glyph-name="revert" horiz-adv-x="887" d="M859.087 483.656c-11.921 4.52-25.325 2.275-35.162-5.888s-14.614-21.005-12.53-33.687c3.17-19.030 4.776-39.808 4.776-60.289 0.28-99.752-38.983-195.47-109.037-265.816-69.71-70.718-164.555-110.329-263.379-109.996-98.83-0.282-193.666 39.356-263.379 110.081-70.054 70.38-109.316 166.125-109.037 265.902 0 100.396 38.712 194.903 109.037 265.902 69.719 70.71 164.555 110.332 263.379 110.039 4.142 0 8.326-0.085 12.467-0.213l-81.017-81.708c-6.702-6.741-10.47-15.899-10.47-25.451s3.767-18.71 10.47-25.451c13.947-14.080 36.557-14.080 50.419 0l134.521 135.809c6.702 6.741 10.47 15.899 10.47 25.451s-3.767 18.71-10.47 25.451l-134.563 135.639c-6.677 6.767-15.749 10.57-25.209 10.57s-18.532-3.804-25.209-10.57c-6.702-6.741-10.47-15.899-10.47-25.451s3.767-18.71 10.47-25.451l66.056-66.518c-116.785-0.569-228.611-47.731-311.093-131.201-83.413-83.924-130.245-197.956-130.126-316.846-0.16-118.839 46.611-232.841 129.957-316.761 83.145-84.139 196.075-131.356 313.798-131.202 59.928 0 117.954 11.819 172.726 35.201 52.73 22.409 100.632 54.973 141.029 95.873 40.497 40.79 72.788 89.111 95.090 142.295 23.202 55.211 34.866 113.921 34.866 174.423 0 24.662-1.986 48.982-5.747 72.107-2.084 12.64-10.711 23.237-22.632 27.756z" />
<glyph unicode="&#xe903;" glyph-name="download" d="M987.004 9.948h-950.089c-20.388 0-36.915-16.54-36.915-36.992 0-20.419 16.558-36.956 36.915-36.956h950.16c20.399 0 36.924 16.537 36.924 36.956 0 20.452-16.525 36.992-36.924 36.992h-0.072zM511.231 77.702c10.54 0 20.026 4.306 26.85 11.32l385.738 386.248c6.898 6.864 11.244 16.364 11.268 26.841 0 20.927-17.071 37.877-37.856 37.877-10.513 0.030-19.961-4.276-26.784-11.143l-321.387-321.794v714.965c0 21.005-16.931 37.984-37.787 37.984-20.913 0-37.838-16.977-37.838-37.91l0.036-715.001-321.351 321.794c-6.827 6.864-16.343 11.131-26.781 11.102-20.904 0.030-37.829-16.947-37.903-37.802 0-10.519 4.301-20.090 11.2-26.883l385.672-386.314c6.892-6.942 16.379-11.284 26.925-11.284z" />
<glyph unicode="&#xe904;" glyph-name="text" d="M320 704h384v-64h-384v64zM256 192h512v-64h-512v64zM256 768h64v-192h-64v192zM704 768h64v-192h-64v192zM480 672h64v-384h-64v384zM960-64h-896c-38.4 0-64 25.6-64 64v896c0 38.4 25.6 64 64 64h896c38.4 0 64-25.6 64-64v-896c0-38.4-25.6-64-64-64zM64 896v-896h896v896h-896zM64 928v-32c0 0 0 0 0 0v32z" />
<glyph unicode="&#xe905;" glyph-name="audio" d="M276.105 608.971c-10.665 0-20.892-4.496-28.433-12.498s-11.777-18.855-11.777-30.172v-337.314c0-23.584 18.017-42.703 40.242-42.703s40.242 19.119 40.242 42.703v337.247c0.017 11.34-4.221 22.22-11.777 30.238s-17.81 12.515-28.496 12.498zM512 960c-10.665 0-20.892-4.496-28.433-12.498s-11.777-18.855-11.777-30.172v-938.661c0-23.566 18.003-42.669 40.211-42.669s40.211 19.104 40.211 42.669v938.661c0 11.317-4.236 22.17-11.777 30.172s-17.769 12.498-28.433 12.498zM983.789 518.994c-10.686 0.018-20.939-4.479-28.496-12.498s-11.794-18.899-11.777-30.238v-184.724c0-23.584 18.017-42.703 40.242-42.703s40.242 19.119 40.242 42.703v184.79c0.017 11.328-4.213 22.198-11.755 30.215s-17.78 12.521-28.455 12.521v-0.066zM40.211 518.994c-10.675 0-20.913-4.505-28.455-12.521s-11.772-18.887-11.755-30.215v-184.724c0-23.584 18.017-42.703 40.242-42.703s40.242 19.119 40.242 42.703v184.79c0.017 11.34-4.221 22.22-11.777 30.238s-17.81 12.515-28.496 12.498v-0.066zM747.895 790.184c-10.686 0.018-20.939-4.479-28.496-12.498s-11.794-18.899-11.777-30.238v-643.619c0-23.602 18.031-42.736 40.273-42.736s40.273 19.133 40.273 42.736v643.619c0.017 11.34-4.221 22.22-11.777 30.238s-17.81 12.515-28.496 12.498z" />
<glyph unicode="&#xe906;" glyph-name="scalar" d="M928-64h-832c-51.2 0-96 44.8-96 96v832c0 51.2 44.8 96 96 96h832c51.2 0 96-44.8 96-96v-832c0-51.2-44.8-96-96-96zM96 896c-19.2 0-32-12.8-32-32v-832c0-19.2 12.8-32 32-32h832c19.2 0 32 12.8 32 32v832c0 19.2-12.8 32-32 32h-832zM192 288c-6.4 0-12.8 0-19.2 6.4-12.8 12.8-12.8 32-6.4 44.8l211.2 256c6.4 6.4 19.2 12.8 25.6 12.8s19.2-6.4 25.6-12.8l192-224 185.6 224c12.8 12.8 32 12.8 44.8 6.4 12.8-12.8 12.8-32 6.4-44.8l-211.2-256c-6.4-6.4-12.8-12.8-25.6-12.8-6.4 0-19.2 6.4-25.6 12.8l-192 224-185.6-224c-6.4-6.4-19.2-12.8-25.6-12.8z" />
<glyph unicode="&#xe907;" glyph-name="histogram" d="M25.6 57.6h966.4c12.8 0 25.6-12.8 25.6-32s-12.8-32-25.6-32h-966.4c-12.8 0-25.6 12.8-25.6 32s12.8 32 25.6 32zM160 198.4c-51.2 0-96 44.8-96 96v211.2c0 51.2 44.8 96 96 96s96-44.8 96-96v-211.2c0-57.6-44.8-96-96-96zM160 537.6c-19.2 0-32-19.2-32-32v-211.2c0-19.2 12.8-32 32-32s32 12.8 32 32v211.2c0 12.8-12.8 32-32 32zM864 198.4c-51.2 0-96 44.8-96 96v409.6c0 51.2 44.8 96 96 96s96-44.8 96-96v-409.6c0-57.6-44.8-96-96-96zM864 736c-19.2 0-32-12.8-32-32v-409.6c0-19.2 12.8-32 32-32s32 12.8 32 32v409.6c0 19.2-12.8 32-32 32zM512 198.4c-51.2 0-96 44.8-96 96v544c0 51.2 44.8 96 96 96s96-44.8 96-96v-544c0-57.6-44.8-96-96-96zM512 870.4c-19.2 0-32-12.8-32-32v-544c0-19.2 12.8-32 32-32s32 12.8 32 32v544c0 19.2-12.8 32-32 32z" />
<glyph unicode="&#xe908;" glyph-name="search" d="M969.387-57.173l-177.493 177.493c-88.747-75.093-197.973-116.053-314.027-116.053-266.24 0-477.867 211.627-477.867 477.867s211.627 477.867 477.867 477.867c266.24 0 477.867-211.627 477.867-477.867 0-116.053-40.96-225.28-116.053-314.027l177.493-177.493-47.787-47.787zM477.867 891.733c-225.28 0-409.6-184.32-409.6-409.6s184.32-409.6 409.6-409.6c95.573 0 191.147 34.133 266.24 102.4l40.96 40.96c68.267 75.093 102.4 170.667 102.4 266.24 0 225.28-184.32 409.6-409.6 409.6z" />
<glyph unicode="&#xe909;" glyph-name="image" horiz-adv-x="1092" d="M989.867-64h-887.467c-54.613 0-102.4 47.787-102.4 102.4v819.2c0 54.613 47.787 102.4 102.4 102.4h887.467c54.613 0 102.4-47.787 102.4-102.4v-819.2c0-54.613-47.787-102.4-102.4-102.4zM102.4 891.733c-20.48 0-34.133-13.653-34.133-34.133v-819.2c0-20.48 13.653-34.133 34.133-34.133h887.467c20.48 0 34.133 13.653 34.133 34.133v819.2c0 20.48-13.653 34.133-34.133 34.133h-887.467zM785.067 516.267c-75.093 0-136.533 61.44-136.533 136.533s61.44 136.533 136.533 136.533 136.533-61.44 136.533-136.533-61.44-136.533-136.533-136.533zM785.067 721.067c-40.96 0-68.267-27.307-68.267-68.267s27.307-68.267 68.267-68.267 68.267 27.307 68.267 68.267-27.307 68.267-68.267 68.267zM1003.52-43.52l-273.067 341.333-191.147-197.973-204.8 314.027-238.933-436.907-61.44 34.133 293.547 532.48 225.28-334.507 177.493 197.973 327.68-409.6z" />
<glyph unicode="&#xe90a;" glyph-name="chevron-down" d="M512.001 294.4l-438.858 460.8-73.143-76.8 512-537.6 512 537.6-73.143 76.8z" />
<glyph unicode="&#xe90b;" glyph-name="check-mark" horiz-adv-x="1408" d="M571.176-25.367l-475.176 475.176 118.794 118.794 356.337-356.337 712.809 712.718 118.794-118.794z" />
<glyph unicode="&#xe90c;" glyph-name="refresh" d="M1024 557.714v256l-83.383-83.383c-93.009 139.122-249.456 229.504-427.013 229.504-282.77 0-512-229.23-512-512s229.23-512 512-512c213.496 0 396.471 130.672 473.31 316.401l1.246 3.399c2.014 4.857 3.184 10.498 3.184 16.412 0 18.265-11.159 33.925-27.031 40.532l-0.29 0.107c-4.857 2.014-10.498 3.184-16.412 3.184-18.265 0-33.925-11.159-40.532-27.031l-0.107-0.29c-64.968-156.628-216.662-264.786-393.618-264.786-234.699 0-424.96 190.261-424.96 424.96s190.261 424.96 424.96 424.96c153.926 0 288.737-81.837 363.298-204.375l1.062-1.88-109.714-108.983z" />
<glyph unicode="&#xe90d;" glyph-name="restore-size" d="M953.6 960h-883.2c-38.881 0-70.4-31.519-70.4-70.4v0-883.2c0-38.881 31.519-70.4 70.4-70.4h883.2c38.881 0 70.4 31.519 70.4 70.4v0 883.2c0 38.881-31.519 70.4-70.4 70.4v0zM70.4 6.4v883.2h883.2v-883.2zM344.96 832c-21.991-0.36-39.68-18.272-39.68-40.315 0-0.002 0-0.004 0-0.005v0-134.4h-138.24c-21.713-0.708-39.045-18.48-39.045-40.3 0-0.232 0.002-0.464 0.006-0.695v0.035c-0.003-0.192-0.005-0.418-0.005-0.645 0-21.691 17.405-39.318 39.012-39.674h177.954c21.64 0.357 39.045 17.983 39.045 39.675 0 0.227-0.002 0.453-0.006 0.679v-0.034 175.36c0.003 0.192 0.005 0.418 0.005 0.645 0 21.691-17.405 39.318-39.012 39.674h-0.034zM856.96 657.28h-138.24v134.4c0 0.002 0 0.003 0 0.005 0 22.043-17.689 39.955-39.646 40.314h-0.034c-21.64-0.357-39.045-17.983-39.045-39.675 0-0.227 0.002-0.453 0.006-0.679v0.034-175.36c-0.003-0.192-0.005-0.418-0.005-0.645 0-21.691 17.405-39.318 39.012-39.674h177.954c21.64 0.357 39.045 17.983 39.045 39.675 0 0.227-0.002 0.453-0.006 0.679v-0.034c0.003 0.196 0.005 0.428 0.005 0.66 0 21.82-17.333 39.592-38.981 40.298l-0.065 0.002zM344.96 320h-177.92c-21.64-0.357-39.045-17.983-39.045-39.675 0-0.227 0.002-0.453 0.006-0.679v0.034c-0.003-0.196-0.005-0.428-0.005-0.66 0-21.82 17.333-39.592 38.981-40.298l0.065-0.002h138.24v-134.4c0-0.002 0-0.003 0-0.005 0-22.043 17.689-39.955 39.646-40.314h0.034c21.64 0.357 39.045 17.983 39.045 39.675 0 0.227-0.002 0.453-0.006 0.679v-0.034 175.36c0.003 0.192 0.005 0.418 0.005 0.645 0 21.691-17.405 39.318-39.012 39.674h-0.034zM856.96 320h-177.92c-21.64-0.357-39.045-17.983-39.045-39.675 0-0.227 0.002-0.453 0.006-0.679v0.034-175.36c-0.003-0.192-0.005-0.418-0.005-0.645 0-21.691 17.405-39.318 39.012-39.674h0.034c21.991 0.36 39.68 18.272 39.68 40.315 0 0.002 0 0.004 0 0.005v0 134.4h138.24c21.713 0.708 39.045 18.48 39.045 40.3 0 0.232-0.002 0.464-0.006 0.695v-0.035c0.003 0.192 0.005 0.418 0.005 0.645 0 21.691-17.405 39.318-39.012 39.674h-0.034z" />
<glyph unicode="&#xe90e;" glyph-name="minimize" d="M613.626-63.326c-21.18 0-38.349 17.169-38.349 38.349s17.169 38.349 38.349 38.349h327.888l6.392 332.362c1.257 20.436 17.343 36.743 37.567 38.34l0.143 0.009c20.367-1.607 36.453-17.913 37.704-38.234l0.006-0.116v-357.928c0.011-0.382 0.017-0.832 0.017-1.282 0-27.534-22.321-49.854-49.854-49.854-0.231 0-0.46 0.002-0.69 0.005h0.035zM0.674 550.265v357.928c-0.003 0.192-0.004 0.417-0.004 0.643 0 27.887 22.606 50.493 50.493 50.493 0.226 0 0.452-0.001 0.677-0.004h357.894c20.517-1.572 36.778-17.832 38.34-38.209l0.009-0.141c-1.572-20.517-17.832-36.778-38.209-38.34l-0.141-0.009h-332.362v-332.362c-1.572-20.517-17.832-36.778-38.209-38.34l-0.141-0.009c-20.517 1.572-36.778 17.832-38.34 38.209l-0.009 0.141zM347.737 334.869c0.192 0.003 0.417 0.004 0.643 0.004 27.887 0 50.493-22.606 50.493-50.493 0-0.226-0.001-0.452-0.004-0.677v0.034-294.012c0-21.18-17.169-38.349-38.349-38.349s-38.349 17.169-38.349 38.349v0 217.314l-262.055-262.055c-6.659-5.722-15.387-9.207-24.927-9.207s-18.268 3.484-24.978 9.249l0.051-0.043c-5.446 6.652-8.746 15.245-8.746 24.608s3.301 17.955 8.801 24.676l-0.055-0.069 262.055 262.055h-218.592c-21.18 0-38.349 17.169-38.349 38.349s17.169 38.349 38.349 38.349v0zM703.747 908.832v-217.314l262.055 262.055c6.139 3.688 13.547 5.869 21.465 5.869 10.95 0 20.924-4.172 28.423-11.012l-0.033 0.030c5.587-6.022 9.014-14.115 9.014-23.010s-3.427-16.988-9.034-23.032l0.020 0.022-262.694-262.694h217.314c21.18 0 38.349-17.169 38.349-38.349s-17.169-38.349-38.349-38.349h-294.012c-0.192-0.003-0.417-0.004-0.643-0.004-27.887 0-50.493 22.606-50.493 50.493 0 0.226 0.001 0.452 0.004 0.677v-0.034 294.652c0 21.18 17.169 38.349 38.349 38.349s38.349-17.169 38.349-38.349v0z" />
<glyph unicode="&#xe90f;" glyph-name="log-axis" horiz-adv-x="1077" d="M1035.529 818.081h-705.846c-10.125 80.716-78.329 142.535-160.97 142.535-89.56 0-162.163-72.603-162.163-162.163s72.603-162.163 162.163-162.163c67.527 0 125.414 41.274 149.809 99.972l0.396 1.074h716.612c22.297 0 40.372 18.075 40.372 40.372s-18.075 40.372-40.372 40.372v0zM166.173 717.151c-43.717 1.128-78.726 36.836-78.726 80.72 0 44.594 36.15 80.745 80.745 80.745 44.585 0 80.731-36.136 80.745-80.718v-0.002c-0.755-44.756-37.214-80.748-82.079-80.748-0.241 0-0.48 0.001-0.72 0.003h0.037zM240.19 286.51c-22.297 0-40.372-18.075-40.372-40.372s18.075-40.372 40.372-40.372h596.167c22.297 0 40.372 18.075 40.372 40.372s-18.075 40.372-40.372 40.372v0zM173.575 555.659c-22.297 0-40.372-18.075-40.372-40.372s18.075-40.372 40.372-40.372h730.069c22.297 0 40.372 18.075 40.372 40.372s-18.075 40.372-40.372 40.372v0zM373.42 17.36c-22.297 0-40.372-18.075-40.372-40.372s18.075-40.372 40.372-40.372h330.382c22.297 0 40.372 18.075 40.372 40.372s-18.075 40.372-40.372 40.372v0z" />
<glyph unicode="&#xe910;" glyph-name="maximize" d="M613.569-63.037c-21.168 0-38.328 17.16-38.328 38.328s17.16 38.328 38.328 38.328h327.703l6.387 332.174c1.257 20.425 17.334 36.722 37.546 38.318l0.143 0.010c20.355-1.606 36.432-17.903 37.683-38.211l0.006-0.115v-356.448c0.010-0.381 0.016-0.83 0.016-1.281 0-27.518-22.308-49.826-49.826-49.826-0.23 0-0.461 0.002-0.691 0.005h0.034zM0.963 550.207v357.726c-0.003 0.191-0.005 0.417-0.005 0.642 0 27.871 22.594 50.465 50.465 50.465 0.225 0 0.453-0.002 0.676-0.005h357.692c21.168 0 38.328-17.16 38.328-38.328s-17.16-38.328-38.328-38.328h-332.174v-332.174c0-21.168-17.16-38.328-38.328-38.328s-38.328 17.16-38.328 38.328v0zM52.067-63.037c-0.191-0.003-0.417-0.005-0.642-0.005-27.871 0-50.465 22.594-50.465 50.465 0 0.225 0.002 0.453 0.005 0.676v-0.034 293.846c0 21.168 17.16 38.328 38.328 38.328s38.328-17.16 38.328-38.328v-217.191l261.907 261.907c6.559 5.674 15.173 9.13 24.594 9.13s18.034-3.456 24.642-9.17l-0.047 0.041c5.555-6.606 8.93-15.206 8.93-24.594s-3.375-17.987-8.979-24.652l0.049 0.058-261.907-261.907h217.191c21.168 0 38.328-17.16 38.328-38.328s-17.16-38.328-38.328-38.328v0zM946.381 614.087v217.191l-261.907-261.907c-6.043-3.546-13.31-5.64-21.064-5.64-10.825 0-20.696 4.079-28.162 10.784l0.039-0.034c-5.583 6.019-9.008 14.108-9.008 22.996s3.426 16.977 9.029 23.019l-0.019-0.021 261.907 261.907h-217.191c-21.168 0-38.328 17.16-38.328 38.328s17.16 38.328 38.328 38.328h293.846c0.191 0.003 0.417 0.005 0.642 0.005 27.871 0 50.465-22.594 50.465-50.465 0-0.225-0.002-0.453-0.005-0.676v0.034-293.846c0-21.168-17.16-38.328-38.328-38.328s-38.328 17.16-38.328 38.328v0z" />
<glyph unicode="&#xe911;" glyph-name="zoom-out" horiz-adv-x="970" d="M678.992 480.965h-388.858c-17.719 0-32.084-14.365-32.084-32.084s14.365-32.084 32.084-32.084v0h388.858c17.719 0 32.084 14.365 32.084 32.084s-14.365 32.084-32.084 32.084v0zM823.37 128.042c90.37 88.144 146.419 211.111 146.419 347.17 0 267.742-217.047 484.789-484.789 484.789s-484.789-217.047-484.789-484.789c0-267.742 217.047-484.789 484.789-484.789 104.255 0 200.823 32.909 279.884 88.905l-1.511-1.016 107.802-128.336c7.175-8.566 17.875-13.976 29.838-13.976 21.451 0 38.841 17.39 38.841 38.841 0 9.488-3.402 18.181-9.052 24.926l0.049-0.061zM749.576 155.955l-26.951-19.25c-67.276-47.899-151.13-76.579-241.682-76.579-231.771 0-419.658 187.888-419.658 419.658s187.888 419.658 419.658 419.658c231.771 0 419.658-187.888 419.658-419.658 0-118.042-48.737-224.701-127.185-300.954l-0.099-0.096z" />
<glyph unicode="&#xe912;" glyph-name="zoom-in" d="M705.992 480.965h-162.345v162.345c0 17.719-14.365 32.084-32.084 32.084s-32.084-14.365-32.084-32.084v0-162.345h-162.345c-17.719 0-32.084-14.365-32.084-32.084s14.365-32.084 32.084-32.084v0h162.345v-160.42c0-17.719 14.365-32.084 32.084-32.084s32.084 14.365 32.084 32.084v0 160.42h160.42c17.719 0 32.084 14.365 32.084 32.084s-14.365 32.084-32.084 32.084v0zM850.369 128.042c90.37 88.144 146.419 211.111 146.419 347.169 0 267.741-217.047 484.788-484.788 484.788s-484.788-217.047-484.788-484.788c0-267.741 217.047-484.788 484.788-484.788 104.255 0 200.823 32.909 279.882 88.905l-1.511-1.016 107.802-128.336c7.175-8.566 17.875-13.976 29.838-13.976 21.451 0 38.841 17.39 38.841 38.841 0 9.488-3.402 18.181-9.052 24.926l0.049-0.061zM776.576 155.955l-26.951-19.25c-67.276-47.899-151.13-76.579-241.682-76.579-231.771 0-419.657 187.888-419.657 419.657s187.888 419.657 419.657 419.657c231.771 0 419.657-187.888 419.657-419.657 0-118.042-48.737-224.701-127.185-300.953l-0.099-0.096z" />
<glyph unicode="&#xe913;" glyph-name="question-circle" d="M462.4 192v0c-0.019-0.477-0.030-1.037-0.030-1.6 0-23.875 19.355-43.23 43.23-43.23 23.312 0 42.315 18.453 43.198 41.549l0.002 0.080v3.2c0 23.859-19.341 43.2-43.2 43.2s-43.2-19.341-43.2-43.2v0zM506.88 297.28h-3.52c-18.161 1.913-32.192 17.145-32.192 35.654 0 1.305 0.070 2.594 0.206 3.864l-0.014-0.158c4.8 49.92 41.28 90.24 81.6 130.56 64 64 65.6 84.8 66.88 106.56 0.049 1.13 0.077 2.455 0.077 3.787 0 25.437-10.164 48.5-26.653 65.349l0.016-0.017c-20.443 20.75-48.849 33.605-80.257 33.605-0.36 0-0.719-0.002-1.078-0.005h0.055c-59.309-0.181-107.339-48.212-107.52-107.503v-0.017c-1.793-18.454-17.226-32.762-36-32.762s-34.207 14.308-35.988 32.614l-0.012 0.148c-0.009 0.657-0.014 1.433-0.014 2.21 0 49.195 20.075 93.702 52.479 125.775l0.015 0.015c32.265 32.612 77.024 52.806 126.499 52.806 0.528 0 1.055-0.002 1.581-0.007l-0.081 0.001c0.073 0 0.159 0 0.245 0 52.446 0 99.795-21.829 133.454-56.897l0.060-0.063c28.645-29.329 46.317-69.484 46.317-113.768 0-3.093-0.086-6.166-0.256-9.216l0.019 0.424c-2.24-46.080-16.64-82.88-87.68-153.6-37.44-37.44-61.12-64.64-61.12-88-2.291-18.004-17.425-31.81-35.821-32h-0.019zM823.040 137.28v0c-79.246-79.093-188.642-128.003-309.461-128.003-0.555 0-1.11 0.001-1.665 0.003h0.086c-0.547-0.002-1.195-0.004-1.843-0.004-120.736 0-230.048 48.914-309.2 128.007l0.003-0.003c-79.101 79.44-128.002 189.004-128.002 309.99 0 0.482 0.001 0.963 0.002 1.445v-0.074c1.004 242.244 197.612 438.233 439.996 438.233 120.985 0 230.565-48.83 310.107-127.857l-0.024 0.023c79.093-79.246 128.003-188.642 128.003-309.461 0-0.555-0.001-1.11-0.003-1.665v0.086c0.004-0.665 0.006-1.451 0.006-2.238 0-120.612-48.921-229.798-128.002-308.798l-0.004-0.004zM512 960v0c-282.77 0-512-229.23-512-512s229.23-512 512-512c282.77 0 512 229.23 512 512v0c0 282.77-229.23 512-512 512v0z" />
<glyph unicode="&#xe914;" glyph-name="minus" d="M46.4 494.4h931.2c25.626 0 46.4-20.774 46.4-46.4s-20.774-46.4-46.4-46.4h-931.2c-25.626 0-46.4 20.774-46.4 46.4s20.774 46.4 46.4 46.4v0z" />
<glyph unicode="&#xe915;" glyph-name="plus" d="M512 960c25.626 0 46.4-20.774 46.4-46.4v-419.2h419.2c25.626 0 46.4-20.774 46.4-46.4s-20.774-46.4-46.4-46.4h-419.2v-419.2c0-25.626-20.774-46.4-46.4-46.4s-46.4 20.774-46.4 46.4v0 419.2h-419.2c-25.626 0-46.4 20.774-46.4 46.4s20.774 46.4 46.4 46.4h419.2v419.2c0 25.626 20.774 46.4 46.4 46.4v0z" />
<glyph unicode="&#xe916;" glyph-name="close" d="M1024 928.32l-32 31.68-480-480.32-480.32 480.32-31.68-31.68 480.32-480.32-480.32-480 31.68-32 480.32 480 480-480 32 32-480 480 480 480.32z" />
<glyph unicode="&#xe917;" glyph-name="upload" horiz-adv-x="1260" d="M1014.475 637.659c-33.74 184.409-193.181 322.34-384.838 322.34s-351.098-137.93-384.473-319.941l-0.365-2.398c-138.917-31.415-241.092-153.795-241.092-300.039 0-11.903 0.677-23.649 1.994-35.199l-0.131 1.412c20.092-155.074 151.333-273.637 310.266-273.637 0.862 0 1.723 0.004 2.584 0.011l-0.132-0.001h95.769c21.589 0 39.090 17.501 39.090 39.090s-17.501 39.090-39.090 39.090v0 0h-97.725c-127.265 0-230.433 103.169-230.433 230.433s103.169 230.433 230.433 230.433v0c0 172.709 140.009 312.718 312.718 312.718s312.718-140.009 312.718-312.718v0c2.83 0.124 6.149 0.196 9.484 0.196 127.157 0 230.238-103.081 230.238-230.238s-103.081-230.238-230.238-230.238c-3.336 0-6.655 0.070-9.956 0.211l0.471-0.016h-117.269c-21.589 0-39.090-17.501-39.090-39.090s17.501-39.090 39.090-39.090v0h117.269c0.758-0.007 1.654-0.010 2.551-0.010 158.656 0 289.716 118.151 309.994 271.265l0.173 1.592c1.152 9.995 1.809 21.577 1.809 33.313 0 146.582-102.518 269.219-239.764 300.124l-2.055 0.389zM646.641 480.91c-7.001 6.684-16.506 10.797-26.972 10.797s-19.97-4.113-26.987-10.81l0.015 0.015-108.669-110.233c-6.968-7.024-11.29-16.684-11.336-27.354v-0.009c0.156-10.537 4.455-20.039 11.338-26.974l-0.002 0.002c7.001-6.684 16.506-10.797 26.972-10.797s19.97 4.113 26.987 10.81l-0.015-0.015 43.39 43.781v-385.033c0-21.589 17.501-39.090 39.090-39.090s39.090 17.501 39.090 39.090v0 384.643l42.999-43.781c6.7-6.992 16.115-11.338 26.546-11.338 0.15 0 0.3 0.001 0.449 0.003h-0.023c10.587 0.267 20.088 4.703 26.966 11.721l0.006 0.007c6.796 7.024 10.984 16.607 10.984 27.167s-4.188 20.144-10.994 27.178l0.010-0.011z" />
<glyph unicode="&#xe918;" glyph-name="mute" d="M224.293 639.273l233.472 187.392c10.973 8.808 26.025 10.54 38.711 4.454s20.753-18.91 20.753-32.98v-697.637c-0.006-14.050-8.060-26.854-20.721-32.943s-27.692-4.387-38.671 4.38l-239.909 191.488h-181.358c-20.198 0-36.572 16.374-36.572 36.572v302.702c0 20.198 16.374 36.572 36.572 36.572h187.721z" />
<glyph unicode="&#xe919;" glyph-name="pause" d="M224 960c70.692 0 128-57.308 128-128v-768c0-70.692-57.308-128-128-128s-128 57.308-128 128v768c0 70.692 57.308 128 128 128zM800 960c70.692 0 128-57.308 128-128v-768c0-70.692-57.308-128-128-128s-128 57.308-128 128v768c0 70.692 57.308 128 128 128z" />
<glyph unicode="&#xe91a;" glyph-name="play" d="M869.901 342.803l-574.736-385.335c-58.106-38.948-136.794-23.433-175.742 34.663-13.99 20.865-21.458 45.418-21.458 70.54v770.67c0 69.949 56.713 126.661 126.661 126.661 25.121 0 49.675-7.469 70.54-21.458l574.725-385.335c58.106-38.948 73.632-117.636 34.673-175.742-9.187-13.701-20.962-25.476-34.663-34.663z" />
<glyph unicode="&#xe91b;" glyph-name="volumn-low" d="M224.293 639.273l233.472 187.392c10.973 8.808 26.025 10.54 38.711 4.454s20.753-18.91 20.753-32.98v-697.637c-0.006-14.050-8.060-26.854-20.721-32.943s-27.692-4.387-38.671 4.38l-239.909 191.488h-181.358c-20.198 0-36.572 16.374-36.572 36.572v302.702c0 20.198 16.374 36.572 36.572 36.572h187.721zM693.467 223.785c-14.468-13.791-37.328-13.412-51.33 0.851s-13.961 37.126 0.093 51.337c47.107 46.117 73.611 109.291 73.509 175.214 0.112 61.057-22.627 119.946-63.744 165.084-8.974 9.64-12.071 23.36-8.11 35.921s14.371 22.020 27.252 24.766c12.88 2.746 26.243-1.645 34.984-11.498 53.352-58.599 82.871-135.024 82.762-214.272 0.1-85.553-34.299-167.534-95.415-227.401v0z" />
<glyph unicode="&#xe91c;" glyph-name="volumn" d="M224.293 639.273l233.472 187.392c10.973 8.808 26.025 10.54 38.711 4.454s20.753-18.91 20.753-32.98v-697.637c-0.006-14.050-8.060-26.854-20.721-32.943s-27.692-4.387-38.671 4.38l-239.909 191.488h-181.358c-20.198 0-36.572 16.374-36.572 36.572v302.702c0 20.198 16.374 36.572 36.572 36.572h187.721zM693.467 223.785c-14.468-13.791-37.328-13.412-51.33 0.851s-13.961 37.126 0.093 51.337c47.107 46.117 73.611 109.291 73.509 175.214 0.112 61.057-22.627 119.946-63.744 165.084-8.974 9.64-12.071 23.36-8.11 35.921s14.371 22.020 27.252 24.766c12.88 2.746 26.243-1.645 34.984-11.498 53.352-58.599 82.871-135.024 82.762-214.272 0.1-85.553-34.299-167.534-95.415-227.401v0zM867.327 65.212c-9.073-9.524-22.561-13.441-35.325-10.26s-22.835 12.972-26.376 25.64c-3.541 12.668-0.006 26.262 9.257 35.601 87.355 89.608 136.162 209.853 135.973 334.994 0 124.233-47.287 240.969-130.779 329.582-13.595 14.739-12.787 37.675 1.811 51.421s37.542 13.174 51.437-1.282c96.183-102.035 150.675-236.617 150.675-379.722 0-146.103-56.869-283.319-156.672-385.975z" />
</font></defs></svg>
\ No newline at end of file
.vdl-icon {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'vdl-icon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-mute:before {
content: '\e918';
}
.icon-volumn-low:before {
content: '\e91b';
}
.icon-volumn:before {
content: '\e91c';
}
.icon-pause:before {
content: '\e919';
}
.icon-play:before {
content: '\e91a';
}
.icon-question-circle:before {
content: '\e913';
}
.icon-minus:before {
content: '\e914';
}
.icon-plus:before {
content: '\e915';
}
.icon-close:before {
content: '\e916';
}
.icon-upload:before {
content: '\e917';
}
.icon-zoom-out:before {
content: '\e911';
}
.icon-zoom-in:before {
content: '\e912';
}
.icon-refresh:before {
content: '\e90c';
}
.icon-restore-size:before {
content: '\e90d';
}
.icon-minimize:before {
content: '\e90e';
}
.icon-log-axis:before {
content: '\e90f';
}
.icon-maximize:before {
content: '\e910';
}
.icon-chevron-down:before {
content: '\e90a';
}
.icon-reduction:before {
content: '\e900';
}
.icon-dimension:before {
content: '\e901';
}
.icon-revert:before {
content: '\e902';
}
.icon-download:before {
content: '\e903';
}
.icon-text:before {
content: '\e904';
}
.icon-audio:before {
content: '\e905';
}
.icon-scalar:before {
content: '\e906';
}
.icon-histogram:before {
content: '\e907';
}
.icon-search:before {
content: '\e908';
}
.icon-image:before {
content: '\e909';
}
.icon-check-mark:before {
content: '\e90b';
}
/* eslint-disable @typescript-eslint/no-var-requires */
require('./builder/environment');
const mock = require('./builder/mock');
const icons = require('./builder/icons');
const netron = require('./builder/netron');
const wasm = require('./builder/wasm');
const port = Number.parseInt(process.env.PORT || 3000, 10);
const devServer = {
port: port + 1,
host: '127.0.0.1'
};
module.exports = {
extends: '@snowpack/app-scripts-react',
plugins: [
'@snowpack/plugin-dotenv',
[
'@snowpack/plugin-run-script',
{
cmd: 'node builder/icons.js && node builder/netron.js && node builder/wasm.js',
watch: `node builder/dev-server.js --port ${devServer.port} --host ${devServer.host}`,
output: 'dashboard'
}
]
],
install: ['@visualdl/wasm'],
alias: {
'~': './src'
},
proxy: {
...[mock.pathname, icons.pathname, netron.pathname, wasm.pathname].reduce((m, pathname) => {
m[(process.env.PUBLIC_PATH || '') + pathname] = `http://${devServer.host}:${devServer.port}${pathname}`;
return m;
}, {})
},
devOptions: {
out: 'dist',
hostname: process.env.HOST || 'localhost',
port
},
buildOptions: {
baseUrl: process.env.PUBLIC_PATH || '/',
clean: true
},
installOptions: {
polyfillNode: true,
namedExports: ['file-saver']
}
};
import React, {FunctionComponent, Suspense, useEffect, useMemo, useState} from 'react';
import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'react-router-dom';
import {headerHeight, position, size} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading';
import {Helmet} from 'react-helmet';
import NProgress from 'nprogress';
import Navbar from '~/components/Navbar';
import {SWRConfig} from 'swr';
import {fetcher} from '~/utils/fetch';
import init from '@visualdl/wasm';
import routes from '~/routes';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const Main = styled.main`
padding-top: ${headerHeight};
`;
const Header = styled.header`
z-index: 10000;
${size(headerHeight, '100%')}
${position('fixed', 0, 0, null, 0)}
`;
const defaultRoute = routes.find(route => route.default);
const routers = routes.reduce<Omit<typeof routes[number], 'children'>[]>((m, route) => {
if (route.children) {
m.push(...route.children);
} else {
m.push(route);
}
return m;
}, []);
const Progress: FunctionComponent = () => {
useEffect(() => {
NProgress.start();
return () => {
NProgress.done();
};
}, []);
return null;
};
const Telemetry: FunctionComponent = () => {
const location = useLocation();
useEffect(() => {
globalThis._hmt.push(['_trackPageview', PUBLIC_PATH + location.pathname]);
}, [location.pathname]);
return null;
};
const App: FunctionComponent = () => {
const {i18n} = useTranslation();
const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]);
const [inited, setInited] = useState(false);
useEffect(() => {
(async () => {
if (!inited) {
await init(`${PUBLIC_PATH}/wasm/visualdl.wasm`);
setInited(true);
}
})();
}, [inited]);
return (
<div className="app">
<Helmet defaultTitle="VisualDL" titleTemplate="%s - VisualDL">
<html lang={i18n.language} dir={dir} />
</Helmet>
<SWRConfig
value={{
fetcher,
revalidateOnFocus: false,
revalidateOnReconnect: false
}}
>
{!inited ? (
<BodyLoading />
) : (
<Main>
<Router basename={PUBLIC_PATH || '/'}>
<Telemetry />
<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>
</Router>
</Main>
)}
</SWRConfig>
</div>
);
};
export default App;
......@@ -24,7 +24,7 @@ import moment from 'moment';
import {saveAs} from 'file-saver';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Container = styled.div`
background-color: ${primaryBackgroundColor};
......@@ -230,42 +230,40 @@ const Audio = React.forwardRef<AudioRef, AudioProps & WithStyled>(
}, [volumn]);
useEffect(() => {
if (process.browser) {
let p: AudioPlayer | null = null;
if (data) {
(async () => {
setDecoding(true);
onLoading?.();
setOffset(0);
setSliderValue(0);
setDuration('00:00');
p = new AudioPlayer({
context: audioContext,
onplay: () => {
setPlaying(true);
startTimer();
},
onstop: () => {
setPlaying(false);
stopTimer();
}
});
const buffer = await data.data.arrayBuffer();
await p.load(buffer, data.type != null ? mime.extension(data.type) || undefined : undefined);
setDecoding(false);
setDuration(formatDuration(p.duration));
onLoad?.({sampleRate: p.sampleRate, duration: p.duration});
player.current = p;
})();
}
return () => {
if (p) {
setPlaying(false);
p.dispose();
player.current = null;
}
};
let p: AudioPlayer | null = null;
if (data) {
(async () => {
setDecoding(true);
onLoading?.();
setOffset(0);
setSliderValue(0);
setDuration('00:00');
p = new AudioPlayer({
context: audioContext,
onplay: () => {
setPlaying(true);
startTimer();
},
onstop: () => {
setPlaying(false);
stopTimer();
}
});
const buffer = await data.data.arrayBuffer();
await p.load(buffer, data.type != null ? mime.extension(data.type) || undefined : undefined);
setDecoding(false);
setDuration(formatDuration(p.duration));
onLoad?.({sampleRate: p.sampleRate, duration: p.duration});
player.current = p;
})();
}
return () => {
if (p) {
setPlaying(false);
p.dispose();
player.current = null;
}
};
}, [data, startTimer, stopTimer, onLoading, onLoad, audioContext]);
const volumnIcon = useMemo(() => {
......
import React, {FunctionComponent} from 'react';
import {position, primaryColor, size} from '~/utils/style';
import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components';
const Wrapper = styled.div`
${size('100vh', '100vw')}
${position('fixed', 0, 0, 0, 0)}
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
overscroll-behavior: none;
cursor: progress;
`;
const BodyLoading: FunctionComponent = () => {
return (
<Wrapper>
<HashLoader size="60px" color={primaryColor} />
</Wrapper>
);
};
export default BodyLoading;
......@@ -22,6 +22,7 @@ import {
transitionProps
} from '~/utils/style';
import type {Icons} from '~/components/Icon';
import RawIcon from '~/components/Icon';
import styled from 'styled-components';
......@@ -85,7 +86,7 @@ const Icon = styled(RawIcon)`
type ButtonProps = {
rounded?: boolean;
icon?: string;
icon?: Icons;
type?: colorTypes;
disabled?: boolean;
onClick?: () => unknown;
......
......@@ -15,11 +15,11 @@ import {
import ee from '~/utils/event';
import styled from 'styled-components';
const Div = styled.div<{maximized?: boolean; width?: string; height?: string}>`
const Div = styled.div<{maximized?: boolean; divWidth?: string; divHeight?: string}>`
${props =>
size(
props.maximized ? `calc(100vh - ${headerHeight} - ${rem(40)})` : props.height || 'auto',
props.maximized ? '100%' : props.width || '100%'
props.maximized ? `calc(100vh - ${headerHeight} - ${rem(40)})` : props.divHeight || 'auto',
props.maximized ? '100%' : props.divWidth || '100%'
)}
background-color: ${backgroundColor};
${sameBorder({radius: math(`${borderRadius} * 2`)})}
......@@ -58,8 +58,8 @@ const Chart: FunctionComponent<ChartProps & WithStyled> = ({cid, width, height,
return (
<Div
maximized={maximized}
width={width}
height={height}
divWidth={width}
divHeight={height}
className={`${maximized ? 'maximized' : ''} ${className ?? ''}`}
>
{children}
......
import React, {FunctionComponent, PropsWithChildren, useCallback, useEffect, useMemo, useState} from 'react';
import {Trans, useTranslation} from '~/utils/i18n';
import {Trans, useTranslation} from 'react-i18next';
import {WithStyled, backgroundColor, headerHeight, link, primaryColor, rem, textLighterColor} from '~/utils/style';
import BarLoader from 'react-spinners/BarLoader';
......@@ -11,7 +11,7 @@ import groupBy from 'lodash/groupBy';
import styled from 'styled-components';
import useSearchValue from '~/hooks/useSearchValue';
const PUBLIC_PATH = process.env.PUBLIC_PATH;
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const StyledPagination = styled(Pagination)`
margin-top: ${rem(20)};
......@@ -131,7 +131,7 @@ const ChartPage = <T extends Item>({
}
return 0;
}),
[items]
[items] // eslint-disable-line react-hooks/exhaustive-deps
);
const total = useMemo(() => Math.ceil(matchedTags.length / pageSize), [matchedTags]);
......@@ -173,7 +173,7 @@ const ChartPage = <T extends Item>({
)}
</Wrapper>
),
[withChart, loading, chartSize, t]
[withChart, loading, chartSize, t] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
......
......@@ -13,12 +13,14 @@ import {
} from '~/utils/style';
import Icon from '~/components/Icon';
import type {Icons} from '~/components/Icon';
import Tippy from '@tippyjs/react';
import styled from 'styled-components';
const Toolbox = styled.div<{reversed?: boolean}>`
font-size: ${em(16)};
line-height: 1;
height: 1em;
display: flex;
flex-direction: ${props => (props.reversed ? 'row-reverse' : 'row')};
align-items: center;
......@@ -43,7 +45,7 @@ const ToolboxItem = styled.a<{active?: boolean; reversed?: boolean}>`
`;
type BaseChartToolboxItem = {
icon: string;
icon: Icons;
tooltip?: string;
};
......@@ -54,7 +56,7 @@ type NormalChartToolboxItem = {
type ToggleChartToolboxItem = {
toggle: true;
activeIcon?: string;
activeIcon?: Icons;
activeTooltip?: string;
onClick?: (value: boolean) => unknown;
} & BaseChartToolboxItem;
......
import React, {FunctionComponent} from 'react';
import {backgroundColor, contentHeight, contentMargin, headerHeight, position, primaryColor, size} from '~/utils/style';
import {backgroundColor, contentHeight, contentMargin, headerHeight, position} from '~/utils/style';
import HashLoader from 'react-spinners/HashLoader';
import BodyLoading from '~/components/BodyLoading';
import styled from 'styled-components';
const Section = styled.section`
......@@ -23,17 +23,6 @@ const Aside = styled.aside`
overflow-y: auto;
`;
const Loading = styled.div`
${size('100vh', '100vw')}
${position('fixed', 0, 0, 0, 0)}
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
overscroll-behavior: none;
cursor: progress;
`;
type ContentProps = {
aside?: React.ReactNode;
loading?: boolean;
......@@ -43,11 +32,7 @@ const Content: FunctionComponent<ContentProps> = ({children, aside, loading}) =>
<Section>
<Article>{children}</Article>
{aside && <Aside>{aside}</Aside>}
{loading && (
<Loading>
<HashLoader size="60px" color={primaryColor} />
</Loading>
)}
{loading && <BodyLoading />}
</Section>
);
......
import React, {FunctionComponent} from 'react';
import {Trans, useTranslation} from '~/utils/i18n';
import {Trans, useTranslation} from 'react-i18next';
import {WithStyled, backgroundColor, em, link, rem, size, textColor, textLightColor} from '~/utils/style';
import styled from 'styled-components';
const PUBLIC_PATH = process.env.PUBLIC_PATH;
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const Wrapper = styled.div`
display: flex;
......
import {Argument as ArgumentType, Property as PropertyType} from '~/resource/graph/types';
import type {Argument as ArgumentType, Property as PropertyType} from '~/resource/graph/types';
import React, {FunctionComponent, useMemo, useState} from 'react';
import {borderColor, em, sameBorder, textLightColor, textLighterColor} from '~/utils/style';
......
import {Documentation, Properties, SearchItem, SearchResult} from '~/resource/graph/types';
import type {Documentation, Properties, SearchItem, SearchResult} from '~/resource/graph/types';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {backgroundColor, borderColor, contentHeight, position, primaryColor, rem, size} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import HashLoader from 'react-spinners/HashLoader';
import logo from '~/assets/images/netron.png';
import styled from 'styled-components';
import {toast} from 'react-toastify';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const PUBLIC_PATH = process.env.PUBLIC_PATH;
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const toolboxHeight = rem(40);
......@@ -158,24 +159,20 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
[onRendered, onSearch, onShowModelProperties, onShowNodeProperties, onShowNodeDocumentation]
);
const dispatch = useCallback((type: string, data?: unknown) => {
if (process.browser) {
iframe.current?.contentWindow?.postMessage(
{
type,
data
},
`${window.location.protocol}//${window.location.host}`
);
}
iframe.current?.contentWindow?.postMessage(
{
type,
data
},
`${window.location.protocol}//${window.location.host}`
);
}, []);
useEffect(() => {
if (process.browser) {
window.addEventListener('message', handler);
dispatch('ready');
return () => {
window.removeEventListener('message', handler);
};
}
window.addEventListener('message', handler);
dispatch('ready');
return () => {
window.removeEventListener('message', handler);
};
}, [handler, dispatch]);
useEffect(() => (ready && dispatch('change-files', files)) || undefined, [dispatch, files, ready]);
......@@ -256,7 +253,7 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
<Content>
<iframe
ref={iframe}
src={`${PUBLIC_PATH ?? ''}/_next/static/netron/index.html`}
src={`${PUBLIC_PATH}/netron/index.html`}
frameBorder={0}
scrolling="no"
marginWidth={0}
......@@ -268,7 +265,7 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
target="_blank"
rel="noreferrer"
>
Powered by <img src={`${PUBLIC_PATH ?? ''}/images/netron.png`} alt="netron" />
Powered by <img src={PUBLIC_PATH + logo} alt="netron" />
</a>
</Content>
</RenderContent>
......
......@@ -2,7 +2,7 @@ import React, {FunctionComponent} from 'react';
import {backgroundColor, borderColor, rem, textLightColor} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Sidebar = styled.div`
height: 100%;
......
......@@ -3,9 +3,9 @@ import {backgroundColor, em, size} from '~/utils/style';
import Icon from '~/components/Icon';
import Properties from '~/components/GraphPage/Properties';
import {Properties as PropertiesType} from '~/resource/graph/types';
import type {Properties as PropertiesType} from '~/resource/graph/types';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Dialog = styled.div`
position: fixed;
......
import React, {FunctionComponent, useCallback} from 'react';
import {Trans, useTranslation} from '~/utils/i18n';
import {Trans, useTranslation} from 'react-i18next';
import {borderRadius, em, textLightColor} from '~/utils/style';
import {Documentation as DocumentationType} from '~/resource/graph/types';
import type {Documentation as DocumentationType} from '~/resource/graph/types';
import GraphSidebar from '~/components/GraphPage/GraphSidebar';
import styled from 'styled-components';
......
......@@ -2,8 +2,8 @@ import React, {FunctionComponent} from 'react';
import GraphSidebar from '~/components/GraphPage/GraphSidebar';
import Properties from '~/components/GraphPage/Properties';
import {Properties as PropertiesType} from '~/resource/graph/types';
import {useTranslation} from '~/utils/i18n';
import type {Properties as PropertiesType} from '~/resource/graph/types';
import {useTranslation} from 'react-i18next';
type NodePropertiesSidebarProps = {
data?: PropertiesType | null;
......
import React, {FunctionComponent} from 'react';
import {Properties as PropertiesType} from '~/resource/graph/types';
import type {Properties as PropertiesType} from '~/resource/graph/types';
import Property from '~/components/GraphPage/Property';
import {em} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Header = styled.div`
font-size: ${em(16)};
......
import {Argument as ArgumentType, NameValues, Property as PropertyType} from '~/resource/graph/types';
import type {Argument as ArgumentType, NameValues, Property as PropertyType} from '~/resource/graph/types';
import React, {FunctionComponent} from 'react';
import {ellipsis, em, sameBorder} from '~/utils/style';
......
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {SearchItem, SearchResult} from '~/resource/graph/types';
import type {SearchItem, SearchResult} from '~/resource/graph/types';
import {
backgroundColor,
backgroundFocusedColor,
......@@ -19,7 +19,7 @@ import Field from '~/components/Field';
import SearchInput from '~/components/SearchInput';
import styled from 'styled-components';
import useSearchValue from '~/hooks/useSearchValue';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const SearchField = styled(Field)`
margin-bottom: ${rem(20)};
......
......@@ -4,7 +4,7 @@ import {em, primaryColor, sameBorder, size, textLightColor} from '~/utils/style'
import Button from '~/components/Button';
import Icon from '~/components/Icon';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const DropZone = styled.div<{actived: boolean}>`
${props =>
......
import {Dimension, Reduction, divide} from '~/resource/high-dimensional';
import type {Dimension, Reduction} from '~/resource/high-dimensional';
import React, {FunctionComponent, useMemo} from 'react';
import {contentHeight, primaryColor, rem} from '~/utils/style';
import ScatterChart from '~/components/ScatterChart';
import {divide} from '~/resource/high-dimensional';
import type {high_dimensional_divide} from '@visualdl/wasm'; // eslint-disable-line @typescript-eslint/no-unused-vars
import queryString from 'query-string';
import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
import wasm from '~/utils/wasm';
const divideWasm = () =>
import('@visualdl/wasm').then(({high_dimensional_divide}): typeof divide => params =>
wasm<typeof high_dimensional_divide>(
'high_dimensional_divide'
).then((high_dimensional_divide): typeof divide => params =>
high_dimensional_divide(params.points, params.labels, !!params.visibility, params.keyword ?? '')
);
const divideWorker = () => new Worker('~/worker/high-dimensional/divide.worker.ts', {type: 'module'});
// const divideWorker = () => new Worker('~/worker/high-dimensional/divide.worker.ts', {type: 'module'});
const StyledScatterChart = styled(ScatterChart)`
height: ${contentHeight};
......@@ -80,7 +85,7 @@ const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({
}),
[data, labelVisibility, keyword]
);
const points = useHeavyWork(divideWasm, divideWorker, divide, divideParams);
const points = useHeavyWork(divideWasm, null, divide, divideParams);
const chartData = useMemo(() => {
return [
......
import {EChartOption, ECharts, EChartsConvertFinder} from 'echarts';
import {
HistogramData,
Modes,
OffsetData,
OverlayData,
OverlayDataItem,
options as chartOptions,
transform
} from '~/resource/histogram';
import type {EChartOption, ECharts, EChartsConvertFinder} from 'echarts';
import type {HistogramData, OffsetData, OverlayData, OverlayDataItem} from '~/resource/histogram';
import LineChart, {LineChartRef} from '~/components/LineChart';
import {Modes, options as chartOptions, transform} from '~/resource/histogram';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import StackChart, {StackChartProps, StackChartRef} from '~/components/StackChart';
import {rem, size} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import {Run} from '~/types';
import type {Run} from '~/types';
import {distance} from '~/utils';
import ee from '~/utils/event';
import {fetcher} from '~/utils/fetch';
import {format} from 'd3-format';
import type {histogram_transform} from '@visualdl/wasm'; // eslint-disable-line @typescript-eslint/no-unused-vars
import minBy from 'lodash/minBy';
import queryString from 'query-string';
import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest';
import useThrottleFn from '~/hooks/useThrottleFn';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
import wasm from '~/utils/wasm';
const formatTooltipXValue = format('.4f');
const formatTooltipYValue = format('.4');
const transformWasm = () =>
import('@visualdl/wasm').then(({histogram_transform}): typeof transform => params =>
wasm<typeof histogram_transform>('histogram_transform').then((histogram_transform): typeof transform => params =>
histogram_transform(params.data, params.mode)
);
const transformWorker = () => new Worker('~/worker/histogram/transform.worker.ts', {type: 'module'});
// const transformWorker = () => new Worker('~/worker/histogram/transform.worker.ts', {type: 'module'});
const Wrapper = styled.div`
${size('100%', '100%')}
......@@ -102,7 +97,7 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
}),
[dataset, mode]
);
const data = useHeavyWork(transformWasm, transformWorker, transform, params);
const data = useHeavyWork(transformWasm, null, transform, params);
const [highlight, setHighlight] = useState<number | null>(null);
useEffect(() => setHighlight(null), [mode]);
......@@ -138,9 +133,9 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
return null as never;
}, [data, mode, run, highlight, t]);
const formatter = {
[Modes.Overlay]: useCallback(
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
const formatter = useMemo(
() => ({
[Modes.Overlay]: (params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
if (!data || highlight == null) {
return '';
}
......@@ -149,31 +144,34 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
);
return series?.seriesName ?? '';
},
[highlight, data]
),
[Modes.Offset]: useCallback((dot: [number, number, number]) => dot[2], [])
} as const;
[Modes.Offset]: (dot: [number, number, number]) => dot[2]
}),
[highlight, data]
);
const pointerFormatter = {
[Modes.Overlay]: useCallback((params: {value: number; axisDimension: 'x' | 'y'}) => {
if (params.axisDimension === 'x') {
return formatTooltipXValue(params.value);
}
if (params.axisDimension === 'y') {
return formatTooltipYValue(params.value);
}
return '' as never;
}, []),
[Modes.Offset]: useCallback((params: {axisDimension: 'x' | 'y'}, dot: [number, number, number]) => {
if (params.axisDimension === 'x') {
return formatTooltipXValue(dot?.[0]);
}
if (params.axisDimension === 'y') {
return formatTooltipYValue(dot?.[1]);
const pointerFormatter = useMemo(
() => ({
[Modes.Overlay]: (params: {value: number; axisDimension: 'x' | 'y'}) => {
if (params.axisDimension === 'x') {
return formatTooltipXValue(params.value);
}
if (params.axisDimension === 'y') {
return formatTooltipYValue(params.value);
}
return '' as never;
},
[Modes.Offset]: (params: {axisDimension: 'x' | 'y'}, dot: [number, number, number]) => {
if (params.axisDimension === 'x') {
return formatTooltipXValue(dot?.[0]);
}
if (params.axisDimension === 'y') {
return formatTooltipYValue(dot?.[1]);
}
return '' as never;
}
return '' as never;
}, [])
};
}),
[]
);
const options = useMemo(
() => ({
......
import React, {FunctionComponent, Suspense, useMemo} from 'react';
import type {WithStyled} from '~/utils/style';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
export type Icons = string;
type IconProps = {
type: Icons;
onClick?: () => unknown;
};
const Icon: FunctionComponent<IconProps & WithStyled> = ({type, onClick, className}) => {
const Svg = useMemo(() => React.lazy(() => import(`${PUBLIC_PATH}/icons/${type}.js`)), [type]);
return (
<i className={`vdl-icon icon-${type} ${className ?? ''}`} onClick={() => onClick?.()}>
<Suspense fallback="">
<Svg />
</Suspense>
</i>
);
};
export default Icon;
......@@ -6,7 +6,7 @@ import GridLoader from 'react-spinners/GridLoader';
import mime from 'mime-types';
import {saveAs} from 'file-saver';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
export type ImageRef = {
save(filename: string): void;
......@@ -21,7 +21,6 @@ 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, {
dedupingInterval: cache ?? 2000
});
......@@ -37,7 +36,7 @@ const Image = React.forwardRef<ImageRef, ImageProps & WithStyled>(({src, cache,
// use useLayoutEffect hook to prevent image render after url revoked
useLayoutEffect(() => {
if (process.browser && data) {
if (data) {
let objectUrl: string | null = null;
objectUrl = URL.createObjectURL(data.data);
setUrl(objectUrl);
......
......@@ -2,7 +2,7 @@ import React, {FunctionComponent} from 'react';
import {rem, size} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const LANGUAGE_FLAGS = [
['zh', ''],
......
......@@ -4,11 +4,11 @@ import React, {useEffect, useImperativeHandle} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts, {Options, Wrapper} from '~/hooks/useECharts';
import {EChartOption} from 'echarts';
import type {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader';
import defaultsDeep from 'lodash/defaultsDeep';
import {formatTime} from '~/utils';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
type LineChartProps = {
options?: EChartOption;
......@@ -58,56 +58,54 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
}));
useEffect(() => {
if (process.browser) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, series, ...defaults} = chart;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, series, ...defaults} = chart;
let chartOptions: EChartOption = defaultsDeep(
{
title: {
text: title ?? ''
},
series: data?.map(item =>
defaultsDeep(
{
// show symbol if there is only one point
showSymbol: (item?.data?.length ?? 0) <= 1,
type: 'line'
},
item,
series
)
let chartOptions: EChartOption = defaultsDeep(
{
title: {
text: title ?? ''
},
series: data?.map(item =>
defaultsDeep(
{
// show symbol if there is only one point
showSymbol: (item?.data?.length ?? 0) <= 1,
type: 'line'
},
item,
series
)
)
},
options,
defaults
);
if ((chartOptions?.xAxis as EChartOption.XAxis).type === 'time') {
chartOptions = defaultsDeep(
{
xAxis: {
axisLabel: {
formatter: (value: number) => formatTime(value, i18n.language, 'LTS')
}
}
},
options,
defaults
chartOptions
);
if ((chartOptions?.xAxis as EChartOption.XAxis).type === 'time') {
chartOptions = defaultsDeep(
{
xAxis: {
axisLabel: {
formatter: (value: number) => formatTime(value, i18n.language, 'LTS')
}
}
},
chartOptions
);
}
if ((chartOptions?.yAxis as EChartOption.YAxis).type === 'time') {
chartOptions = defaultsDeep(
{
yAxis: {
axisLabel: {
formatter: (value: number) => formatTime(value, i18n.language, 'LTS')
}
}
if ((chartOptions?.yAxis as EChartOption.YAxis).type === 'time') {
chartOptions = defaultsDeep(
{
yAxis: {
axisLabel: {
formatter: (value: number) => formatTime(value, i18n.language, 'LTS')
}
},
chartOptions
);
}
echart?.setOption(chartOptions, {notMerge: true});
}
},
chartOptions
);
}
echart?.setOption(chartOptions, {notMerge: true});
}, [options, data, title, i18n.language, echart]);
return (
......
import {Link, config, i18n, useTranslation} from '~/utils/i18n';
import React, {FunctionComponent, useEffect, useMemo, useState} from 'react';
import {Link, LinkProps, useLocation} from 'react-router-dom';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import {
backgroundFocusedColor,
border,
......@@ -14,20 +14,41 @@ import {
textInvertColor,
transitionProps
} from '~/utils/style';
import useNavItems, {NavItem as BaseNavItem} from '~/hooks/useNavItems';
import Icon from '~/components/Icon';
import {InitConfig} from '@visualdl/i18n';
import Language from '~/components/Language';
import type {Route} from '~/routes';
import Tippy from '@tippyjs/react';
import ee from '~/utils/event';
import {getApiToken} from '~/utils/fetch';
import logo from '~/assets/images/logo.svg';
import queryString from 'query-string';
import styled from 'styled-components';
import {useRouter} from 'next/router';
import useNavItems from '~/hooks/useNavItems';
import {useTranslation} from 'react-i18next';
const API_TOKEN_KEY = process.env.API_TOKEN_KEY;
const PUBLIC_PATH = process.env.PUBLIC_PATH;
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const API_TOKEN_KEY: string = import.meta.env.SNOWPACK_PUBLIC_API_TOKEN_KEY;
interface NavbarItemProps extends Route {
cid?: string;
active: boolean;
children?: ({active: boolean} & NonNullable<Route['children']>[number])[];
}
function appendApiToken(url: string) {
if (!API_TOKEN_KEY) {
return url;
}
const parsed = queryString.parseUrl(url);
return queryString.stringifyUrl({
...parsed,
query: {
...parsed.query,
[API_TOKEN_KEY]: getApiToken()
}
});
}
const Nav = styled.nav`
background-color: ${navbarBackgroundColor};
......@@ -70,8 +91,7 @@ const Logo = styled.a`
}
`;
const NavItem = styled.a<{active?: boolean}>`
padding: 0 ${rem(20)};
const NavItem = styled.div<{active?: boolean}>`
height: 100%;
display: inline-flex;
justify-content: center;
......@@ -84,7 +104,21 @@ const NavItem = styled.a<{active?: boolean}>`
background-color: ${navbarHoverBackgroundColor};
}
> .nav-text {
&.nav-item {
padding: 0 ${rem(20)};
}
.nav-link {
display: inline-block;
width: 100%;
height: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
}
.nav-text {
margin: ${rem(20)};
padding: ${rem(10)} 0 ${rem(7)};
${props => border('bottom', rem(3), 'solid', props.active ? navbarHighlightColor : 'transparent')}
${transitionProps('border-bottom')}
......@@ -97,9 +131,8 @@ const SubNav = styled.div`
border-radius: ${borderRadius};
`;
const NavItemChild = styled.a<{active?: boolean}>`
const NavItemChild = styled.div<{active?: boolean}>`
display: block;
padding: 0 ${rem(20)};
line-height: 3em;
&,
......@@ -110,37 +143,36 @@ const NavItemChild = styled.a<{active?: boolean}>`
&:hover {
background-color: ${backgroundFocusedColor};
}
`;
interface NavItem extends BaseNavItem {
cid?: string;
active: boolean;
children?: ({active: boolean} & NonNullable<BaseNavItem['children']>[number])[];
}
> a {
display: block;
padding: 0 ${rem(20)};
}
`;
const changeLanguage = () => {
const {language} = i18n;
const {allLanguages} = config;
const index = allLanguages.indexOf(language);
const nextLanguage = index < 0 || index >= allLanguages.length - 1 ? allLanguages[0] : allLanguages[index + 1];
i18n.changeLanguage(nextLanguage);
const NavbarLink: FunctionComponent<{to?: string} & Omit<LinkProps, 'to'>> = ({to, children, ...props}) => {
return (
<Link to={to ? appendApiToken(to) : ''} {...props}>
{children}
</Link>
);
};
const NavbarItem = React.forwardRef<HTMLAnchorElement, NavItem>(({path, id, cid, active}, ref) => {
const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps>(({id, cid, path, active}, ref) => {
const {t} = useTranslation('common');
const name = useMemo(() => (cid ? `${t(id)} - ${t(cid)}` : t(id)), [t, id, cid]);
if (path) {
// https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag
return (
<Link href={path} passHref>
<NavItem active={active} ref={ref}>
<NavItem active={active} ref={ref}>
<NavbarLink to={path} className="nav-link">
<span className="nav-text">{name}</span>
</NavItem>
</Link>
</NavbarLink>
</NavItem>
);
}
return (
<NavItem active={active} ref={ref}>
<span className="nav-text">{name}</span>
......@@ -152,12 +184,20 @@ NavbarItem.displayName = 'NavbarItem';
const Navbar: FunctionComponent = () => {
const {t, i18n} = useTranslation('common');
const {pathname, basePath} = useRouter();
const {pathname} = useLocation();
const changeLanguage = useCallback(() => {
const language = i18n.language;
const allLanguages = (i18n.options.supportedLngs || []).filter(lng => lng !== 'cimode');
const index = allLanguages.indexOf(language);
const nextLanguage = index < 0 || index >= allLanguages.length - 1 ? allLanguages[0] : allLanguages[index + 1];
i18n.changeLanguage(nextLanguage);
}, [i18n]);
const currentPath = useMemo(() => pathname.replace(basePath, ''), [pathname, basePath]);
const currentPath = useMemo(() => pathname.replace(PUBLIC_PATH, ''), [pathname]);
const navItems = useNavItems();
const [items, setItems] = useState<NavItem[]>([]);
const [items, setItems] = useState<NavbarItemProps[]>([]);
useEffect(() => {
setItems(oldItems =>
navItems.map(item => {
......@@ -196,30 +236,11 @@ const Navbar: FunctionComponent = () => {
);
}, [navItems, currentPath]);
const indexUrl = useMemo(() => {
// TODO: fix type
const subpath = (i18n.options as InitConfig).localeSubpaths?.[i18n.language];
let path = PUBLIC_PATH ?? '';
if (subpath) {
path += `/${subpath}`;
}
path += '/index';
if (API_TOKEN_KEY) {
const id = getApiToken();
if (id) {
path += `?${queryString.stringify({
[API_TOKEN_KEY]: id
})}`;
}
}
return path;
}, [i18n.options, i18n.language]);
return (
<Nav>
<div className="left">
<Logo href={indexUrl}>
<img alt="PaddlePaddle" src={`${PUBLIC_PATH}/images/logo.svg`} />
<Logo href={appendApiToken(PUBLIC_PATH + '/index')}>
<img alt="PaddlePaddle" src={PUBLIC_PATH + logo} />
<span>VisualDL</span>
</Logo>
{items.map(item => {
......@@ -236,11 +257,11 @@ const Navbar: FunctionComponent = () => {
content={
<SubNav>
{item.children.map(child => (
<Link href={child.path} key={child.id} passHref>
<NavItemChild active={child.active}>
<NavItemChild active={child.active} key={child.id}>
<NavbarLink to={child.path}>
{t(item.id)} - {t(child.id)}
</NavItemChild>
</Link>
</NavbarLink>
</NavItemChild>
))}
</SubNav>
}
......@@ -254,10 +275,10 @@ const Navbar: FunctionComponent = () => {
})}
</div>
<div className="right">
<NavItem onClick={changeLanguage}>
<NavItem className="nav-item" onClick={changeLanguage}>
<Language />
</NavItem>
<NavItem onClick={() => ee.emit('refresh')}>
<NavItem className="nav-item" onClick={() => ee.emit('refresh')}>
<Icon type="refresh" />
</NavItem>
</div>
......
import LineChart, {LineChartRef} from '~/components/LineChart';
import {PRCurveData, Run, options as chartOptions, nearestPoint} from '~/resource/pr-curve';
import type {PRCurveData, Run} from '~/resource/pr-curve';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import {options as chartOptions, nearestPoint} from '~/resource/pr-curve';
import {rem, size} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import {EChartOption} from 'echarts';
import type {EChartOption} from 'echarts';
import TooltipTable from '~/components/TooltipTable';
import {cycleFetcher} from '~/utils/fetch';
import ee from '~/utils/event';
......@@ -13,7 +14,7 @@ import queryString from 'query-string';
import {renderToStaticMarkup} from 'react-dom/server';
import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
import zip from 'lodash/zip';
const axisFormatter = format('.4f');
......
import React, {FunctionComponent, useEffect, useState} from 'react';
import {Run, TimeType} from '~/resource/pr-curve';
import {ellipsis, size, textLighterColor} from '~/utils/style';
import Field from '~/components/Field';
import RangeSlider from '~/components/RangeSlider';
import type {Run} from '~/resource/pr-curve';
import {TimeType} from '~/resource/pr-curve';
import {format} from 'd3-format';
import {formatTime} from '~/utils';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const relativeFormatter = format('.2f');
......
......@@ -6,7 +6,7 @@ import {WithStyled, em} from '~/utils/style';
import Button from '~/components/Button';
import Input from '~/components/Input';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Wrapper = styled.nav`
display: flex;
......
......@@ -36,7 +36,8 @@ const Button = styled.a<{selected?: boolean}>`
${transitionProps(['color', 'border-color', 'background-color'])}
/* bring selected one to top in order to cover the sibling's border */
${props => (props.selected ? 'position: relative;' : '')}
${props =>
props.selected ? 'position: relative;' : ''}
&:hover {
border-color: ${props => (props.selected ? primaryColor : borderFocusedColor)};
......
import React, {FunctionComponent, PropsWithChildren, createContext, useCallback, useState} from 'react';
import {WithStyled} from '~/utils/style';
import type {WithStyled} from '~/utils/style';
import styled from 'styled-components';
const Wrapper = styled.div`
......@@ -33,7 +33,7 @@ const RadioGroup = <T extends unknown>({
setSelected(value);
onChange?.(value);
},
[onChange]
[onChange] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
......
import {Route as DomRouter, RouteProps} from 'react-router-dom';
import React, {FunctionComponent, useEffect} from 'react';
import NProgress from 'nprogress';
const Route: FunctionComponent<RouteProps> = props => {
useEffect(() => {
NProgress.start();
return () => {
NProgress.done();
};
}, []);
return <DomRouter {...props} />;
};
export default Route;
......@@ -4,12 +4,12 @@ import {ellipsis, em, rem, size} from '~/utils/style';
import Checkbox from '~/components/Checkbox';
import Field from '~/components/Field';
import {Run} from '~/types';
import type {Run} from '~/types';
import RunningToggle from '~/components/RunningToggle';
import SearchInput from '~/components/SearchInput';
import styled from 'styled-components';
import uniqBy from 'lodash/uniqBy';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const StyledAside = styled(Aside)`
${AsideSection}.run-section {
......
......@@ -4,7 +4,7 @@ import {WithStyled, rem} from '~/utils/style';
import Button from '~/components/Button';
import Tippy from '@tippyjs/react';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Wrapper = styled.div`
display: flex;
......
......@@ -4,7 +4,7 @@ import SampleChart, {SampleChartBaseProps} from '~/components/SamplePage/SampleC
import {format} from 'd3-format';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const formatter = format('.5~s');
......
......@@ -3,14 +3,14 @@ import {ellipsis, em, primaryColor, rem, size, textLightColor, textLighterColor}
import ChartToolbox from '~/components/ChartToolbox';
import GridLoader from 'react-spinners/GridLoader';
import {Run} from '~/types';
import type {Run} from '~/types';
import StepSlider from '~/components/SamplePage/StepSlider';
import {formatTime} from '~/utils';
import isEmpty from 'lodash/isEmpty';
import queryString from 'query-string';
import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Wrapper = styled.div`
height: 100%;
......@@ -130,8 +130,8 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, running, ty
const [step, setStep] = useState(0);
const [src, setSrc] = useState<string>();
const cached = useRef<Record<number, {src: string; timer: NodeJS.Timeout}>>({});
const timer = useRef<NodeJS.Timeout | null>(null);
const cached = useRef<Record<number, {src: string; timer: number}>>({});
const timer = useRef<number | null>(null);
// clear cache if tag or run changed
useEffect(() => {
......@@ -153,6 +153,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, running, ty
}, cache)
};
setSrc(url);
timer.current = null;
}, [type, step, run.label, tag, wallTime, data, cache]);
const download = useCallback(() => {
......@@ -169,7 +170,10 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, running, ty
} else {
timer.current = setTimeout(cacheSrc, 500);
return () => {
timer.current && clearTimeout(timer.current);
if (timer.current != null) {
clearTimeout(timer.current);
timer.current = null;
}
};
}
}, [step, cacheSrc]);
......@@ -178,19 +182,17 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, running, ty
const container = useRef<HTMLDivElement>(null);
const observer = useRef(
process.browser
? new IntersectionObserver(entries => {
if (entries[0].intersectionRatio > 0) {
setViewed(true);
observer.current?.disconnect();
}
})
: null
new IntersectionObserver(entries => {
if (entries[0].intersectionRatio > 0) {
setViewed(true);
observer.current?.disconnect();
}
})
);
useEffect(() => {
const o = observer.current;
if (process.browser && container.current && o) {
if (container.current && o) {
o.observe(container.current);
return () => o.disconnect();
}
......
......@@ -3,7 +3,7 @@ import {em, textLightColor} from '~/utils/style';
import RangeSlider from '~/components/RangeSlider';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const Label = styled.div`
display: flex;
......
import type {Dataset, Range, ScalarDataset} from '~/resource/scalar';
import LineChart, {LineChartRef, XAxisType, YAxisType} from '~/components/LineChart';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import {
Dataset,
Range,
ScalarDataset,
SortingMethod,
XAxis,
axisRange,
chartData,
options as chartOptions,
nearestPoint,
......@@ -14,13 +15,12 @@ import {
transform,
xAxisMap
} from '~/resource/scalar';
import LineChart, {LineChartRef, XAxisType, YAxisType} from '~/components/LineChart';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import {rem, size} from '~/utils/style';
import type {scalar_axis_range, scalar_range, scalar_transform} from '@visualdl/wasm'; // eslint-disable-line @typescript-eslint/no-unused-vars
import ChartToolbox from '~/components/ChartToolbox';
import {EChartOption} from 'echarts';
import {Run} from '~/types';
import type {EChartOption} from 'echarts';
import type {Run} from '~/types';
import TooltipTable from '~/components/TooltipTable';
import {cycleFetcher} from '~/utils/fetch';
import ee from '~/utils/event';
......@@ -30,21 +30,28 @@ import {renderToStaticMarkup} from 'react-dom/server';
import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
import wasm from '~/utils/wasm';
const labelFormatter = format('.8');
const smoothWasm = () =>
import('@visualdl/wasm').then(({scalar_transform}): typeof transform => params =>
wasm<typeof scalar_transform>('scalar_transform').then((scalar_transform): typeof transform => params =>
scalar_transform(params.datasets, params.smoothing)
);
const axisRangeWasm = () =>
wasm<typeof scalar_axis_range>('scalar_axis_range').then((scalar_axis_range): typeof axisRange => params =>
scalar_axis_range(params.datasets, params.outlier)
);
const rangeWasm = () =>
import('@visualdl/wasm').then(({scalar_range}): typeof range => params =>
scalar_range(params.datasets, params.outlier)
wasm<typeof scalar_range>('scalar_range').then((scalar_range): typeof range => params =>
scalar_range(params.datasets)
);
const smoothWorker = () => new Worker('~/worker/scalar/smooth.worker.ts', {type: 'module'});
const rangeWorker = () => new Worker('~/worker/scalar/range.worker.ts', {type: 'module'});
// const smoothWorker = () => new Worker('~/worker/scalar/smooth.worker.ts', {type: 'module'});
// const rangeWorker = () => new Worker('~/worker/scalar/range.worker.ts', {type: 'module'});
const Wrapper = styled.div`
${size('100%', '100%')}
......@@ -122,16 +129,20 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}),
[datasets, smoothing]
);
const smoothedDatasets = useHeavyWork(smoothWasm, smoothWorker, transform, transformParams) ?? [];
const smoothedDatasetsOrUndefined = useHeavyWork(smoothWasm, null, transform, transformParams);
const smoothedDatasets = useMemo(() => smoothedDatasetsOrUndefined ?? [], [smoothedDatasetsOrUndefined]);
const rangeParams = useMemo(
const axisRangeParams = useMemo(
() => ({
datasets: smoothedDatasets,
outlier: !!outlier
}),
[smoothedDatasets, outlier]
);
const yRange = useHeavyWork(rangeWasm, rangeWorker, range, rangeParams);
const yRange = useHeavyWork(axisRangeWasm, null, axisRange, axisRangeParams);
const datasetRangesParams = useMemo(() => ({datasets: smoothedDatasets}), [smoothedDatasets]);
const datasetRanges = useHeavyWork(rangeWasm, null, range, datasetRangesParams);
const ranges: Record<'x' | 'y', Range | undefined> = useMemo(() => {
let x: Range | undefined = undefined;
......@@ -165,19 +176,18 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
const formatter = useCallback(
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
const series: Dataset[number] = Array.isArray(params) ? params[0].data : params.data;
const points = nearestPoint(smoothedDatasets ?? [], runs, series[1]);
const points = nearestPoint(smoothedDatasets ?? [], runs, series[1]).map((point, index) => ({
...point,
...datasetRanges?.[index]
}));
const sort = sortingMethodMap[sortingMethod];
const sorted = sort(points, series);
const {columns, data} = tooltip(
sorted.map(i => i.item),
maxStepLength,
i18n
);
const {columns, data} = tooltip(sorted, maxStepLength, i18n);
return renderToStaticMarkup(
<TooltipTable run={t('common:runs')} runs={sorted.map(i => i.run)} columns={columns} data={data} />
);
},
[smoothedDatasets, runs, sortingMethod, maxStepLength, t, i18n]
[smoothedDatasets, datasetRanges, runs, sortingMethod, maxStepLength, t, i18n]
);
const options = useMemo(
......
......@@ -84,9 +84,7 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({data,
);
useEffect(() => {
if (process.browser) {
echart?.setOption(chartOptions);
}
echart?.setOption(chartOptions);
}, [chartOptions, echart]);
return (
......
......@@ -22,7 +22,7 @@ import Checkbox from '~/components/Checkbox';
import Icon from '~/components/Icon';
import styled from 'styled-components';
import useClickOutside from '~/hooks/useClickOutside';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
import without from 'lodash/without';
export const padding = em(10);
......@@ -37,7 +37,9 @@ const Wrapper = styled.div<{opened?: boolean}>`
background-color: ${backgroundColor};
${sameBorder({radius: true})}
${props => (props.opened ? borderRadiusShortHand('bottom', '0') : '')}
${transitionProps('border-color')}
${transitionProps(
'border-color'
)}
&:hover {
border-color: ${borderFocusedColor};
......@@ -165,6 +167,7 @@ const Select = <T extends unknown>({
setValue
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const isSelected = useMemo(() => !!(multiple ? (value as T[]) && (value as T[]).length !== 0 : (value as T)), [
multiple,
value
......@@ -175,7 +178,7 @@ const Select = <T extends unknown>({
(onChange as OnSingleChange<T>)?.(mutateValue);
setIsOpenedFalse();
},
[setIsOpenedFalse, onChange]
[setIsOpenedFalse, onChange] // eslint-disable-line react-hooks/exhaustive-deps
);
const changeMultipleValue = useCallback(
(mutateValue: T, checked: boolean) => {
......@@ -192,7 +195,7 @@ const Select = <T extends unknown>({
setValue(newValue);
(onChange as OnMultipleChange<T>)?.(newValue);
},
[value, onChange]
[value, onChange] // eslint-disable-line react-hooks/exhaustive-deps
);
const ref = useClickOutside<HTMLDivElement>(setIsOpenedFalse);
......@@ -204,10 +207,11 @@ const Select = <T extends unknown>({
? {value: item as T, label: item + ''}
: (item as SelectListItem<T>)
) ?? [],
[propList]
[propList] // eslint-disable-line react-hooks/exhaustive-deps
);
const isListEmpty = useMemo(() => list.length === 0, [list]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const findLabelByValue = useCallback((v: T) => list.find(item => item.value === v)?.label ?? '', [list]);
const label = useMemo(
() =>
......@@ -216,7 +220,7 @@ const Select = <T extends unknown>({
? (value as T[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as T)
: placeholder || t('common:select'),
[multiple, value, findLabelByValue, isSelected, placeholder, t]
[multiple, value, findLabelByValue, isSelected, placeholder, t] // eslint-disable-line react-hooks/exhaustive-deps
);
return (
......
import * as chart from '~/utils/chart';
import {EChartOption, ECharts, EChartsConvertFinder} from 'echarts';
import type {EChartOption, ECharts, EChartsConvertFinder} from 'echarts';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts, {Options, Wrapper} from '~/hooks/useECharts';
......@@ -62,7 +62,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
maxX: 0,
data: null
};
const rawData = seriesData.data ?? [];
const rawData = useMemo(() => seriesData.data ?? [], [seriesData.data]);
const negativeY = useMemo(() => minY - (maxY - minY) * 0.4, [minY, maxY]);
......@@ -340,9 +340,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
}));
useEffect(() => {
if (process.browser) {
echart?.setOption(chartOptions, {notMerge: true});
}
echart?.setOption(chartOptions, {notMerge: true});
}, [echart, chartOptions]);
useEffect(() => {
......
......@@ -3,7 +3,7 @@ import React, {FunctionComponent, useCallback, useEffect, useState} from 'react'
import RadioButton from '~/components/RadioButton';
import RadioGroup from '~/components/RadioGroup';
import {TimeMode} from '~/types';
import {useTranslation} from '~/utils/i18n';
import {useTranslation} from 'react-i18next';
const timeModes = [TimeMode.Step, TimeMode.Relative, TimeMode.WallTime] as const;
......
import React, {FunctionComponent} from 'react';
import {Helmet} from 'react-helmet';
const Title: FunctionComponent = ({children}) => (
<Helmet>
<title>{children}</title>
</Helmet>
);
export default Title;
import React, {FunctionComponent} from 'react';
import {rem, size} from '~/utils/style';
import {Run} from '~/types';
import type {Run} from '~/types';
import styled from 'styled-components';
const Wrapper = styled.div`
......
......@@ -22,7 +22,7 @@ const useClickOutside = <T extends HTMLElement>(callback: () => void) => {
);
useEffect(() => {
if (process.browser) {
if (globalThis.document) {
document.addEventListener('mousedown', clickListener);
document.addEventListener('touchstart', clickListener);
document.addEventListener('keyup', escapeListener);
......
import {MutableRefObject, useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
import {maskColor, position, primaryColor, size, textColor} from '~/utils/style';
import {ECharts} from 'echarts';
import type {ECharts} from 'echarts';
import {dataURL2Blob} from '~/utils/image';
import {saveAs} from 'file-saver';
import styled from 'styled-components';
......@@ -32,7 +32,7 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
const createChart = useCallback(() => {
(async () => {
const echarts = await import('echarts');
const {default: echarts} = await import('echarts');
if (options.gl) {
await import('echarts-gl');
}
......@@ -68,31 +68,27 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
}, []);
useEffect(() => {
if (process.browser) {
createChart();
return destroyChart;
}
createChart();
return destroyChart;
}, [createChart, destroyChart]);
useEffect(() => {
if (process.browser) {
if (options.loading) {
echartInstance.current?.showLoading('default', {
text: '',
color: primaryColor,
textColor,
maskColor,
zlevel: 0
});
} else {
echartInstance.current?.hideLoading();
}
if (options.loading) {
echartInstance.current?.showLoading('default', {
text: '',
color: primaryColor,
textColor,
maskColor,
zlevel: 0
});
} else {
echartInstance.current?.hideLoading();
}
}, [options.loading]);
const wrapper = useRef<W | null>(null);
useLayoutEffect(() => {
if (options.autoFit && process.browser) {
if (options.autoFit) {
const w = wrapper.current;
if (w) {
const observer = new ResizeObserver(() => {
......
......@@ -14,36 +14,32 @@ const useHeavyWork = <T = unknown, P = unknown>(
const runFallback = useCallback((p: P) => fallback && setResult(fallback(p)), [fallback]);
useEffect(() => {
if (process.browser) {
try {
if (createWasm && typeof WebAssembly !== 'undefined') {
if (!wasm.current) {
wasm.current = createWasm();
}
wasm.current
?.then((work: (arg: P) => T) => setResult(work(params)))
.catch(() => runFallback(params));
return;
try {
if (createWasm && typeof WebAssembly !== 'undefined') {
if (!wasm.current) {
wasm.current = createWasm();
}
wasm.current?.then((work: (arg: P) => T) => setResult(work(params))).catch(() => runFallback(params));
return;
}
if (createWorker && typeof Worker !== 'undefined') {
if (!worker.current) {
worker.current = createWorker();
}
worker.current?.postMessage(params);
worker.current?.addEventListener('message', ({data}: MessageEvent & {data: T}) => setResult(data));
worker.current?.addEventListener('error', () => runFallback(params));
return;
}
} catch (e) {
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.error('Error during heavy work, trying to use fallback');
// eslint-disable-next-line no-console
console.error(e);
if (createWorker && typeof Worker !== 'undefined') {
if (!worker.current) {
worker.current = createWorker();
}
runFallback(params);
worker.current?.postMessage(params);
worker.current?.addEventListener('message', ({data}: MessageEvent & {data: T}) => setResult(data));
worker.current?.addEventListener('error', () => runFallback(params));
return;
}
} catch (e) {
if (import.meta.env.MODE === 'development') {
// eslint-disable-next-line no-console
console.error('Error during heavy work, trying to use fallback');
// eslint-disable-next-line no-console
console.error(e);
}
runFallback(params);
}
runFallback(params);
......
import routes, {Pages, Route} from '~/routes';
import {useCallback, useEffect, useState} from 'react';
import ee from '~/utils/event';
import {fetcher} from '~/utils/fetch';
import useRequest from '~/hooks/useRequest';
enum Pages {
Scalar = 'scalar',
Histogram = 'histogram',
Image = 'image',
Audio = 'audio',
Graph = 'graph',
HighDimensional = 'high-dimensional',
PRCurve = 'pr-curve'
}
export interface NavItem {
id: Pages | string;
visible?: boolean;
path?: string;
children?: {
id: NavItem['id'];
path: string;
}[];
}
const pages: NavItem[] = [
{
id: Pages.Scalar,
path: `/${Pages.Scalar}`
},
{
id: Pages.Histogram,
path: `/${Pages.Histogram}`
},
{
id: 'sample',
visible: true,
children: [
{
id: Pages.Image,
path: `/sample/${Pages.Image}`
},
{
id: Pages.Audio,
path: `/sample/${Pages.Audio}`
}
]
},
{
id: Pages.Graph,
path: `/${Pages.Graph}`
},
{
id: Pages.HighDimensional,
path: `/${Pages.HighDimensional}`
},
{
id: Pages.PRCurve,
path: `/${Pages.PRCurve}`
}
];
export const navMap = {
scalar: Pages.Scalar,
histogram: Pages.Histogram,
......@@ -72,7 +16,7 @@ export const navMap = {
} as const;
const useNavItems = () => {
const [components, setComponents] = useState<NavItem[]>([]);
const [components, setComponents] = useState<Route[]>([]);
const {data, mutate} = useRequest<(keyof typeof navMap)[]>('/components', fetcher, {
refreshInterval: components.length ? 61 * 1000 : 15 * 1000,
......@@ -93,21 +37,18 @@ const useNavItems = () => {
}, [mutate]);
const filterPages = useCallback(
(pages: NavItem[]) => {
(pages: Route[]) => {
const items: string[] = data?.map(item => navMap[item]) ?? [];
return pages.reduce<NavItem[]>((m, page) => {
if (!page.visible && !items.includes(page.id)) {
return m;
}
return pages.reduce<Route[]>((m, page) => {
if (page.children) {
const children = filterPages(page.children);
if (children.length) {
m.push({
...page,
children: children as NavItem['children']
children: children as Route['children']
});
}
} else {
} else if (page.visible !== false && items.includes(page.id)) {
m.push(page);
}
return m;
......@@ -117,7 +58,7 @@ const useNavItems = () => {
);
useEffect(() => {
setComponents(filterPages(pages));
setComponents(filterPages(routes));
}, [data, filterPages]);
return components;
......
......@@ -2,7 +2,7 @@ import {useEffect, useMemo} from 'react';
import useSWR, {ConfigInterface, keyInterface, responseInterface} from 'swr';
import ee from '~/utils/event';
import {fetcherFn} from 'swr/dist/types';
import type {fetcherFn} from 'swr/dist/types';
type Response<D, E> = responseInterface<D, E> & {
loading: boolean;
......
import {Run, Tag, TagWithSingleRun, TagsData} from '~/types';
import type {Run, Tag, TagWithSingleRun, TagsData} from '~/types';
import {color, colorAlt} from '~/utils/chart';
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 {useRouter} from 'next/router';
import {useLocation} from 'react-router-dom';
import {useRunningRequest} from '~/hooks/useRequest';
type Tags = Record<string, string[]>;
......@@ -140,7 +141,8 @@ const reducer = (state: State, action: Action): State => {
// TODO: refactor to improve performance
const useTagFilter = (type: string, running: boolean) => {
const router = useRouter();
const location = useLocation();
const query = useMemo(() => queryString.parse(location.search), [location.search]);
const {data, loading, error} = useRunningRequest<TagsData>(`/${type}/tags`, running);
......@@ -170,11 +172,8 @@ const useTagFilter = (type: string, running: boolean) => {
});
const queryRuns = useMemo(
() =>
router.query.runs
? uniq(Array.isArray(router.query.runs) ? router.query.runs : router.query.runs.split(','))
: [],
[router]
() => (query.runs ? uniq(Array.isArray(query.runs) ? query.runs : query.runs.split(',')) : []),
[query]
);
const runsFromQuery = useMemo(
......
import '~/utils/i18n';
import App from './App';
import BodyLoading from '~/components/BodyLoading';
import {GlobalStyle} from '~/utils/style';
import React from 'react';
import ReactDOM from 'react-dom';
const TELEMETRY_ID: string = import.meta.env.SNOWPACK_PUBLIC_TELEMETRY_ID;
globalThis._hmt = globalThis._hmt || [];
if (import.meta.env.MODE === 'production' && TELEMETRY_ID) {
(function () {
const hm = document.createElement('script');
hm.src = `https://hm.baidu.com/hm.js?${TELEMETRY_ID}`;
const s = document.getElementsByTagName('script')[0];
s.parentNode?.insertBefore(hm, s);
})();
}
ReactDOM.render(
<React.StrictMode>
<GlobalStyle />
<React.Suspense fallback={<BodyLoading />}>
<App />
</React.Suspense>
</React.StrictMode>,
document.getElementById('root')
);
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://www.snowpack.dev/#hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
}
import Aside, {AsideSection} from '~/components/Aside';
import {BlobResponse, blobFetcher} from '~/utils/fetch';
import {Documentation, Properties, SearchItem, SearchResult} from '~/resource/graph/types';
import type {Documentation, Properties, SearchItem, SearchResult} from '~/resource/graph/types';
import GraphComponent, {GraphRef} from '~/components/GraphPage/Graph';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {primaryColor, rem, size} from '~/utils/style';
import Button from '~/components/Button';
......@@ -21,6 +20,7 @@ import Title from '~/components/Title';
import Uploader from '~/components/GraphPage/Uploader';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
const FullWidthButton = styled(Button)`
width: 100%;
......@@ -62,7 +62,7 @@ const Loading = styled.div`
line-height: ${rem(60)};
`;
const Graph: NextI18NextPage = () => {
const Graph: FunctionComponent = () => {
const {t} = useTranslation(['graph', 'common']);
const {data, loading} = useRequest<BlobResponse>('/graph/graph', blobFetcher);
......@@ -272,8 +272,4 @@ const Graph: NextI18NextPage = () => {
);
};
Graph.getInitialProps = () => ({
namespacesRequired: ['graph', 'common']
});
export default Graph;
import Aside, {AsideSection} from '~/components/Aside';
import {Dimension, Reduction} from '~/resource/high-dimensional';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useEffect, useMemo, useState} from 'react';
import type {Dimension, Reduction} from '~/resource/high-dimensional';
import React, {FunctionComponent, useEffect, useMemo, useState} from 'react';
import Select, {SelectProps} from '~/components/Select';
import {em, rem} from '~/utils/style';
......@@ -11,17 +10,18 @@ import Error from '~/components/Error';
import Field from '~/components/Field';
import HighDimensionalChart from '~/components/HighDimensionalPage/HighDimensionalChart';
import Icon from '~/components/Icon';
import Preloader from '~/components/Preloader';
import RadioButton from '~/components/RadioButton';
import RadioGroup from '~/components/RadioGroup';
import RunningToggle from '~/components/RunningToggle';
import SearchInput from '~/components/SearchInput';
import {TagsData} from '~/types';
import type {TagsData} from '~/types';
import Title from '~/components/Title';
import queryString from 'query-string';
import styled from 'styled-components';
import {useRouter} from 'next/router';
import {useLocation} from 'react-router-dom';
import {useRunningRequest} from '~/hooks/useRequest';
import useSearchValue from '~/hooks/useSearchValue';
import {useTranslation} from 'react-i18next';
const dimensions = ['2d', '3d'];
const reductions = ['pca', 'tsne'];
......@@ -48,7 +48,7 @@ type Item = {
label: string;
};
const HighDimensional: NextI18NextPage = () => {
const HighDimensional: FunctionComponent = () => {
const {t} = useTranslation(['high-dimensional', 'common']);
const [running, setRunning] = useState(true);
......@@ -66,7 +66,8 @@ const HighDimensional: NextI18NextPage = () => {
}, [data]);
const labelList = useMemo(() => list.map(item => item.label), [list]);
const {query} = useRouter();
const location = useLocation();
const query = useMemo(() => queryString.parse(location.search), [location]);
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;
......@@ -143,7 +144,6 @@ const HighDimensional: NextI18NextPage = () => {
return (
<>
<Preloader url="/embedding/tags" />
<Title>{t('common:high-dimensional')}</Title>
<Content aside={aside} loading={loading}>
{!loading && !list.length ? (
......@@ -166,8 +166,4 @@ const HighDimensional: NextI18NextPage = () => {
);
};
HighDimensional.getInitialProps = () => ({
namespacesRequired: ['high-dimensional', 'common']
});
export default HighDimensional;
import ChartPage, {WithChart} from '~/components/ChartPage';
import {Modes, modes} from '~/resource/histogram';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useMemo, useState} from 'react';
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
import {AsideSection} from '~/components/Aside';
import Content from '~/components/Content';
import Error from '~/components/Error';
import Field from '~/components/Field';
import HistogramChart from '~/components/HistogramPage/HistogramChart';
import Preloader from '~/components/Preloader';
import RadioButton from '~/components/RadioButton';
import RadioGroup from '~/components/RadioGroup';
import RunAside from '~/components/RunAside';
import {TagWithSingleRun} from '~/types';
import type {TagWithSingleRun} from '~/types';
import Title from '~/components/Title';
import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next';
const Histogram: NextI18NextPage = () => {
const Histogram: FunctionComponent = () => {
const {t} = useTranslation(['histogram', 'common']);
const [running, setRunning] = useState(true);
......@@ -58,7 +57,6 @@ const Histogram: NextI18NextPage = () => {
return (
<>
<Preloader url="/histogram/tags" />
<Title>{t('common:histogram')}</Title>
<Content aside={aside} loading={loading}>
{!loading && !runs.length ? (
......@@ -71,8 +69,4 @@ const Histogram: NextI18NextPage = () => {
);
};
Histogram.getInitialProps = () => ({
namespacesRequired: ['histogram', 'common']
});
export default Histogram;
import {NextI18NextPage, Router, useTranslation} from '~/utils/i18n';
import React, {useEffect} from 'react';
import React, {FunctionComponent, useEffect} from 'react';
import {headerHeight, primaryColor, rem, size} from '~/utils/style';
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`
${size(`calc(100vh - ${headerHeight})`, '100vw')}
......@@ -18,20 +19,21 @@ const Loading = styled.div`
line-height: ${rem(60)};
`;
const Index: NextI18NextPage = () => {
const IndexPage: FunctionComponent = () => {
const navItems = useNavItems();
const history = useHistory();
const {t} = useTranslation('common');
useEffect(() => {
if (navItems.length) {
if (navItems[0].path) {
Router.replace(navItems[0].path);
history.replace(navItems[0].path);
} else if (navItems[0].children?.length && navItems[0].children[0].path) {
Router.replace(navItems[0].children[0].path);
history.replace(navItems[0].children[0].path);
}
}
}, [navItems]);
}, [navItems, history]);
return (
<Loading>
......@@ -41,10 +43,4 @@ const Index: NextI18NextPage = () => {
);
};
Index.getInitialProps = () => {
return {
namespacesRequired: ['common']
};
};
export default Index;
export default IndexPage;
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Run, StepInfo, Tag, TimeType} from '~/resource/pr-curve';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import type {Run, StepInfo, Tag} from '~/resource/pr-curve';
import {borderColor, rem} from '~/utils/style';
import {AsideSection} from '~/components/Aside';
......@@ -9,16 +8,17 @@ import Content from '~/components/Content';
import Error from '~/components/Error';
import Field from '~/components/Field';
import PRCurveChart from '~/components/PRCurvePage/PRCurveChart';
import Preloader from '~/components/Preloader';
import RunAside from '~/components/RunAside';
import StepSlider from '~/components/PRCurvePage/StepSlider';
import TimeModeSelect from '~/components/TimeModeSelect';
import {TimeType} from '~/resource/pr-curve';
import Title from '~/components/Title';
import {cycleFetcher} from '~/utils/fetch';
import queryString from 'query-string';
import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest';
import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next';
const StepSliderWrapper = styled.div`
max-height: 30vh;
......@@ -43,7 +43,7 @@ const StepSliderWrapper = styled.div`
}
`;
const PRCurve: NextI18NextPage = () => {
const PRCurve: FunctionComponent = () => {
const {t} = useTranslation(['pr-curve', 'common']);
const [running, setRunning] = useState(true);
......@@ -145,7 +145,6 @@ const PRCurve: NextI18NextPage = () => {
return (
<>
<Preloader url="/pr-curve/tags" />
<Title>{t('common:pr-curve')}</Title>
<Content aside={aside} loading={loading}>
{!loading && !runs.length ? (
......@@ -158,8 +157,4 @@ const PRCurve: NextI18NextPage = () => {
);
};
PRCurve.getInitialProps = () => ({
namespacesRequired: ['pr-curve', 'common']
});
export default PRCurve;
// cSpell:words ungrouped
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import AudioChart from '~/components/SamplePage/AudioChart';
import Content from '~/components/Content';
import Error from '~/components/Error';
import Preloader from '~/components/Preloader';
import RunAside from '~/components/RunAside';
import Title from '~/components/Title';
import {rem} from '~/utils/style';
import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next';
const chartSize = {
height: rem(244)
};
const Audio: NextI18NextPage = () => {
const Audio: FunctionComponent = () => {
const {t} = useTranslation(['sample', 'common']);
const audioContext = useRef<AudioContext>();
useEffect(() => {
if (process.browser) {
// safari only has webkitAudioContext
const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
audioContext.current = new AudioContext();
// safari only has webkitAudioContext
const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
audioContext.current = new AudioContext();
return () => {
audioContext.current?.close();
};
}
return () => {
audioContext.current?.close();
};
}, []);
const [running, setRunning] = useState(true);
......@@ -58,7 +55,6 @@ const Audio: NextI18NextPage = () => {
return (
<>
<Preloader url="/audio/tags" />
<Title>
{t('common:sample')} - {t('common:audio')}
</Title>
......@@ -78,8 +74,4 @@ const Audio: NextI18NextPage = () => {
);
};
Audio.getInitialProps = () => ({
namespacesRequired: ['sample', 'common']
});
export default Audio;
// cSpell:words ungrouped
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useMemo, useState} from 'react';
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
import {AsideSection} from '~/components/Aside';
import Checkbox from '~/components/Checkbox';
......@@ -10,18 +9,18 @@ import Content from '~/components/Content';
import Error from '~/components/Error';
import Field from '~/components/Field';
import ImageChart from '~/components/SamplePage/ImageChart';
import Preloader from '~/components/Preloader';
import RunAside from '~/components/RunAside';
import Slider from '~/components/Slider';
import Title from '~/components/Title';
import {rem} from '~/utils/style';
import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next';
const chartSize = {
height: rem(406)
};
const Image: NextI18NextPage = () => {
const Image: FunctionComponent = () => {
const {t} = useTranslation(['sample', 'common']);
const [running, setRunning] = useState(true);
......@@ -78,7 +77,6 @@ const Image: NextI18NextPage = () => {
return (
<>
<Preloader url="/image/tags" />
<Title>
{t('common:sample')} - {t('common:image')}
</Title>
......@@ -98,8 +96,4 @@ const Image: NextI18NextPage = () => {
);
};
Image.getInitialProps = () => ({
namespacesRequired: ['sample', 'common']
});
export default Image;
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useMemo, useState} from 'react';
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
import {SortingMethod, XAxis, sortingMethod as toolTipSortingValues} from '~/resource/scalar';
import {AsideSection} from '~/components/Aside';
......@@ -8,17 +7,17 @@ import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content';
import Error from '~/components/Error';
import Field from '~/components/Field';
import Preloader from '~/components/Preloader';
import RunAside from '~/components/RunAside';
import ScalarChart from '~/components/ScalarPage/ScalarChart';
import Select from '~/components/Select';
import Slider from '~/components/Slider';
import {Tag} from '~/types';
import type {Tag} from '~/types';
import TimeModeSelect from '~/components/TimeModeSelect';
import Title from '~/components/Title';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import useTagFilter from '~/hooks/useTagFilter';
import {useTranslation} from 'react-i18next';
const TooltipSortingDiv = styled.div`
margin-top: ${rem(20)};
......@@ -32,7 +31,7 @@ const TooltipSortingDiv = styled.div`
}
`;
const Scalar: NextI18NextPage = () => {
const Scalar: FunctionComponent = () => {
const {t} = useTranslation(['scalar', 'common']);
const [running, setRunning] = useState(true);
......@@ -106,7 +105,6 @@ const Scalar: NextI18NextPage = () => {
return (
<>
<Preloader url="/scalar/tags" />
<Title>{t('common:scalar')}</Title>
<Content aside={aside} loading={loading}>
{!loading && !runs.length ? (
......@@ -119,8 +117,4 @@ const Scalar: NextI18NextPage = () => {
);
};
Scalar.getInitialProps = () => ({
namespacesRequired: ['scalar', 'common']
});
export default Scalar;
import {Point} from './types';
import type {Point} from './types';
export * from './types';
export type {Dimension, Reduction, Point} from './types';
const dividePoints = (points: Point[], keyword?: string) => {
if (!keyword) {
......
import {EChartOption, VisualMap} from 'echarts';
import type {EChartOption, VisualMap} from 'echarts';
import {Modes} from './types';
import type {Modes} from './types';
const baseOptions: EChartOption = {
legend: {
......
import {HistogramData, Modes, OffsetData, OffsetDataItem, OverlayData, OverlayDataItem} from './types';
import type {HistogramData, OffsetData, OffsetDataItem, OverlayData, OverlayDataItem} from './types';
import {Modes} from './types';
function computeHistogram(
data: {left: number; right: number; count: number}[],
......
......@@ -2,6 +2,7 @@ import {Modes} from './types';
export const modes = [Modes.Offset, Modes.Overlay] as const;
export * from './types';
export type {HistogramDataItem, HistogramData, OverlayDataItem, OverlayData, OffsetDataItem, OffsetData} from './types';
export {Modes} from './types';
export * from './chart';
export * from './data';
export * from './types';
export type {PRCurveData, Run, StepInfo, Tag} from './types';
export {TimeType} from './types';
export * from './chart';
export * from './data';
import {Dataset, XAxis} from './types';
import type {Dataset, TooltipData, XAxis} from './types';
import {I18n} from '@visualdl/i18n';
import {Run} from '~/types';
import type I18n from 'i18next';
import type {Run} from '~/types';
import {format} from 'd3-format';
import {formatTime} from '~/utils';
import {xAxisMap} from './index';
......@@ -64,7 +64,7 @@ export const chartData = ({data, runs, xAxis}: {data: Dataset[]; runs: Run[]; xA
})
.flat();
export const tooltip = (data: Dataset, stepLength: number, i18n: I18n) => {
export const tooltip = (data: TooltipData[], stepLength: number, i18n: typeof I18n) => {
return {
columns: [
{
......@@ -75,6 +75,14 @@ export const tooltip = (data: Dataset, stepLength: number, i18n: I18n) => {
label: i18n.t('scalar:value'),
width: '4.285714286em'
},
{
label: i18n.t('scalar:min'),
width: '4.285714286em'
},
{
label: i18n.t('scalar:max'),
width: '4.285714286em'
},
{
label: i18n.t('common:time-mode.step'),
width: `${Math.max(stepLength * 0.571428571, 2.857142857)}em`
......@@ -88,12 +96,14 @@ export const tooltip = (data: Dataset, stepLength: number, i18n: I18n) => {
width: '4.285714286em'
}
],
data: data.map(([time, step, value, smoothed, relative]) => [
valueFormatter(smoothed ?? Number.NaN),
valueFormatter(value ?? Number.NaN),
step,
formatTime(time, i18n.language),
Math.floor(relative * 60 * 60) + 's'
data: data.map(({min, max, item}) => [
valueFormatter(item[3] ?? Number.NaN),
valueFormatter(item[2] ?? Number.NaN),
valueFormatter(min ?? Number.NaN),
valueFormatter(max ?? Number.NaN),
item[1],
formatTime(item[0], i18n.language),
Math.floor(item[4] * 60 * 60) + 's'
])
};
};
import {Dataset, ScalarDataset} from './types';
import type {Dataset, ScalarDataset} from './types';
import BigNumber from 'bignumber.js';
import {Run} from '~/types';
import type {Run} from '~/types';
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import minBy from 'lodash/minBy';
......@@ -46,10 +46,28 @@ export const singlePointRange = (value: number) => ({
max: value ? Math.max(value * 2, 0) : 0.5
});
export const range = ({datasets, outlier}: {datasets: Dataset[]; outlier: boolean}) => {
export const range = ({datasets}: {datasets: Dataset[]}) => {
return datasets?.map(dataset => {
if (dataset.length == 0) {
return {
min: Number.NaN,
max: Number.NaN
};
}
const values = dataset.map(v => v[2]);
return {
min: Math.min(...values) ?? Number.NaN,
max: Math.max(...values) ?? Number.NaN
};
});
};
export const axisRange = ({datasets, outlier}: {datasets: Dataset[]; outlier: boolean}) => {
const ranges = compact(
datasets?.map(dataset => {
if (dataset.length == 0) return;
if (dataset.length == 0) {
return;
}
const values = dataset.map(v => v[2]);
if (!outlier) {
// Get the orgin data range.
......
import {SortingMethod as SM, TooltipData, XAxis} from './types';
import {SortingMethod as SM, XAxis} from './types';
import type {TooltipData} from './types';
import sortBy from 'lodash/sortBy';
export const xAxisMap = {
......@@ -17,6 +18,7 @@ export const sortingMethodMap: Record<SM, (points: TooltipData[], data: number[]
[SM.Nearest]: (points: TooltipData[], data: number[]) => sortBy(points, point => point.item[3] - data[2])
} as const;
export * from './types';
export type {Dataset, ScalarDataset, Range, TooltipData} from './types';
export {XAxis, SortingMethod} from './types';
export * from './chart';
export * from './data';
......@@ -25,4 +25,6 @@ export type Range = {
export type TooltipData = {
run: Run;
item: Dataset[number];
min?: number;
max?: number;
};
import React, {FunctionComponent, LazyExoticComponent} from 'react';
export enum Pages {
Scalar = 'scalar',
Histogram = 'histogram',
Image = 'image',
Audio = 'audio',
Graph = 'graph',
HighDimensional = 'high-dimensional',
PRCurve = 'pr-curve'
}
export interface Route {
id: Pages | string;
default?: boolean;
visible?: boolean;
path?: string;
component?: LazyExoticComponent<FunctionComponent>;
children?: Pick<Route, 'id' | 'path' | 'component'>[];
}
const routes: Route[] = [
{
id: 'index',
default: true,
visible: false,
path: '/index',
component: React.lazy(() => import('~/pages/index'))
},
{
id: Pages.Scalar,
path: '/scalar',
component: React.lazy(() => import('~/pages/scalar'))
},
{
id: Pages.Histogram,
path: '/histogram',
component: React.lazy(() => import('~/pages/histogram'))
},
{
id: 'sample',
children: [
{
id: Pages.Image,
path: '/sample/image',
component: React.lazy(() => import('~/pages/sample/image'))
},
{
id: Pages.Audio,
path: '/sample/audio',
component: React.lazy(() => import('~/pages/sample/audio'))
}
]
},
{
id: Pages.Graph,
path: '/graph',
component: React.lazy(() => import('~/pages/graph'))
},
{
id: Pages.HighDimensional,
path: '/high-dimensional',
component: React.lazy(() => import('~/pages/high-dimensional'))
},
{
id: Pages.PRCurve,
path: '/pr-curve',
component: React.lazy(() => import('~/pages/pr-curve'))
}
];
export default routes;
// TODO: use this instead
// https://github.com/zeit/swr/blob/master/examples/axios-typescript/libs/useRequest.ts
import fetch from 'isomorphic-unfetch';
import queryString from 'query-string';
const API_TOKEN_KEY = process.env.API_TOKEN_KEY;
const API_TOKEN_KEY: string = import.meta.env.SNOWPACK_PUBLIC_API_TOKEN_KEY;
const API_URL: string = import.meta.env.SNOWPACK_PUBLIC_API_URL;
const API_TOKEN_HEADER = 'X-VisualDL-Instance-ID';
const instanceId = process.browser && API_TOKEN_KEY ? queryString.parse(window.location.search)[API_TOKEN_KEY] : '';
const instanceId = API_TOKEN_KEY ? queryString.parse(window.location.search)[API_TOKEN_KEY] : '';
export function getApiToken(): string | string[] | null {
return instanceId ?? null;
......@@ -33,7 +30,7 @@ function addApiToken(options?: RequestInit): RequestInit | undefined {
}
export const fetcher = async <T = unknown>(url: string, options?: RequestInit): Promise<T> => {
const res = await fetch(process.env.API_URL + url, addApiToken(options));
const res = await fetch(API_URL + url, addApiToken(options));
const response = await res.json();
return response && 'data' in response ? response.data : response;
......@@ -46,7 +43,7 @@ export type BlobResponse = {
};
export const blobFetcher = async (url: string, options?: RequestInit): Promise<BlobResponse> => {
const res = await fetch(process.env.API_URL + url, addApiToken(options));
const res = await fetch(API_URL + url, addApiToken(options));
const data = await res.blob();
const disposition = res.headers.get('Content-Disposition');
// support safari
......
import Fetch from 'i18next-fetch-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import i18n from 'i18next';
import {initReactI18next} from 'react-i18next';
const {SNOWPACK_PUBLIC_DEFAULT_LANGUAGE, SNOWPACK_PUBLIC_LANGUAGES, SNOWPACK_PUBLIC_PATH} = import.meta.env;
const defaultLanguage: string = SNOWPACK_PUBLIC_DEFAULT_LANGUAGE;
const allLanguages: string[] = SNOWPACK_PUBLIC_LANGUAGES.split(',');
const PUBLIC_PATH: string = SNOWPACK_PUBLIC_PATH;
i18n.use(initReactI18next)
.use(Fetch)
.use(LanguageDetector)
.init({
fallbackLng: defaultLanguage,
supportedLngs: allLanguages,
cleanCode: true,
ns: 'common',
defaultNS: 'common',
load: 'currentOnly',
interpolation: {
escapeValue: false
},
backend: {
loadPath: `${PUBLIC_PATH}/locales/{{lng}}/{{ns}}.json`
},
detection: {
order: ['localStorage', 'cookie', 'navigator'],
lookupCookie: 'vdl_lng',
lookupLocalStorage: 'vdlLng',
caches: ['localStorage']
}
});
import 'tippy.js/dist/tippy.css';
import 'tippy.js/animations/shift-away-subtle.css';
import 'react-toastify/dist/ReactToastify.css';
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 tippyAnimation from '!!css-loader!tippy.js/animations/shift-away-subtle.css';
import toast from '!!css-loader!react-toastify/dist/ReactToastify.css';
import vdlIcon from '!!css-loader!~/public/style/vdl-icon.css';
const PUBLIC_PATH = process.env.PUBLIC_PATH;
export {default as styled} from 'styled-components';
export * from 'styled-components';
export * from 'polished';
// rename conflict shorthands
......@@ -22,8 +19,6 @@ export {
const {math, size, lighten, darken, normalize, transitions, border, position} = polished;
export const iconFontPath = `${PUBLIC_PATH}/style/fonts/vdl-icon`;
// sizes
const fontSize = '14px';
export const rem = (pxval: string | number): string => polished.rem(pxval, fontSize);
......@@ -101,36 +96,7 @@ export const transitionProps = (props: string | string[], args?: string | {durat
}
return transitions(props, args);
};
export const fontFace = ({queryString, ...args}: {queryString?: string} & Parameters<typeof polished.fontFace>[0]) => {
const formatMap: Record<string, string> = {
eot: 'embedded-opentype',
woff2: 'woff2',
woff: 'woff',
ttf: 'truetype',
svg: 'svg'
};
const f = polished.fontFace(args);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const src: string = (f['@font-face'] as any).src;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(f['@font-face'] as any).src = src.replace(/url\(['"](.*?)\.(\w*?)['"]\)/g, (_, path: string, format: string) => {
let replace = `url("${path}.${format}`;
if (queryString) {
replace += `?${queryString}`;
}
if (format === 'svg') {
replace += `#${args.fontFamily}`;
}
replace += '")';
if (formatMap[format]) {
replace += ` format("${formatMap[format]}")`;
}
return replace;
});
return f;
};
export const link = css`
a {
color: ${primaryColor};
......@@ -164,21 +130,6 @@ export type WithStyled = {
export const GlobalStyle = createGlobalStyle`
${normalize}
${fontFace({
queryString: 'wxo6ka',
fontFamily: 'vdl-icon',
fontFilePath: iconFontPath,
fileFormats: ['ttf', 'woff', 'svg'],
fontWeight: 'normal',
fontStyle: 'normal',
fontDisplay: 'block'
})}
${vdlIcon.toString()}
${toast.toString()}
${tippy.toString()}
${tippyAnimation.toString()}
html {
font-size: ${fontSize};
font-family: 'Merriweather Sans', Helvetica, Arial, sans-serif;
......
import * as funcs from '@visualdl/wasm';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default async <T extends (...args: any[]) => unknown>(name: Exclude<keyof typeof funcs, 'default'>) => {
return funcs[name] as T;
};
import * as React from 'react';
import App from '../src/App';
import {render} from '@testing-library/react';
test('renders learn react link', () => {
const {getByText} = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
{
"include": [
"src",
"test",
"types"
],
"exclude": [
"node_modules"
],
"extends": "@snowpack/app-scripts-react/tsconfig.base.json",
"compilerOptions": {
"jsx": "preserve",
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"esnext",
"esnext.asynciterable",
"dom",
"webworker"
],
"esModuleInterop": true,
"allowJs": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"baseUrl": ".",
// You can't currently define paths in your 'extends' config,
// so we have to set 'baseUrl' & 'paths' here.
// Don't change these unless you know what you're doing.
// See: https://github.com/microsoft/TypeScript/issues/25430
"baseUrl": "./",
/* more strict checking for errors that per-file transpilers like `esbuild` would crash */
"isolatedModules": true,
"paths": {
"*": [
"web_modules/.types/*"
],
"~/*": [
"./*"
"./src/*"
]
},
"types": [
"@types/node"
],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
},
"exclude": [
"node_modules",
"__tests__",
"__mocks__"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
}
// Feel free to add/edit new config options below:
// ...
}
}
{
"compilerOptions": {
"jsx": "react",
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"esnext",
"esnext.asynciterable",
"dom",
"webworker"
],
"esModuleInterop": true,
"allowJs": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
]
},
"types": [
"@types/node",
"@types/jest"
],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": true
}
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.css' {
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: {readonly [key: string]: string};
export default classes;
}
declare module '*.module.scss' {
const classes: {readonly [key: string]: string};
export default classes;
}
declare module '*.module.sass' {
const classes: {readonly [key: string]: string};
export default classes;
}
declare global {
interface Window {
__visualdl_instance_id__?: string | string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_hmt: any[];
}
namespace globalThis {
/* eslint-disable no-var */
var __visualdl_instance_id__: string | string[] | undefined;
var webkitAudioContext: AudioContext | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var _hmt: any[];
/* eslint-enable no-var */
}
}
declare namespace NodeJS {
interface Global {
__visualdl_instance_id__?: string | string[];
}
}
export {};
declare module 'save-svg-as-png' {
export function saveSvgAsPng(node: Node, filename: string): Promise<void>;
}
/* Use this file to declare any custom file extensions for importing */
/* Use this folder to also add/extend a package d.ts file, if needed. */
declare module '*.css';
declare module '*.svg' {
const ref: string;
export default ref;
}
declare module '*.bmp' {
const ref: string;
export default ref;
}
declare module '*.gif' {
const ref: string;
export default ref;
}
declare module '*.jpg' {
const ref: string;
export default ref;
}
declare module '*.jpeg' {
const ref: string;
export default ref;
}
declare module '*.png' {
const ref: string;
export default ref;
}
declare module '*.webp' {
const ref: string;
export default ref;
}
declare module '*.worker.ts' {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}
import {NextComponentType, NextPageContext} from 'next';
import NextI18Next from '@visualdl/i18n';
import {env} from '../next.config';
import moment from 'moment';
const defaultLanguage = env.DEFAULT_LANGUAGE;
const allLanguages = env.LANGUAGES;
const otherLanguages = allLanguages.filter(lang => lang !== defaultLanguage);
const isDev = process.env.NODE_ENV === 'development';
allLanguages.forEach(async (lang: string) => {
moment.updateLocale(lang, await import(`../public/locales/${lang}/moment.json`));
});
const nextI18Next = new NextI18Next({
publicPath: env.PUBLIC_PATH,
localePath: env.LOCALE_PATH,
browserLanguageDetection: !isDev,
serverLanguageDetection: !isDev,
cleanCode: true,
defaultLanguage,
otherLanguages,
localeSubpaths: otherLanguages.reduce((prev, curr) => {
prev[curr] = curr;
return prev;
}, {} as Record<string, string>)
});
export default nextI18Next;
export const {i18n, config, appWithTranslation, withTranslation, useTranslation, Router, Link, Trans} = nextI18Next;
// 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[]},
P & {namespacesRequired: string[]}
>;
export default <P = unknown, R = unknown>(handler: (data: P) => R): void =>
self.addEventListener('message', ({data}: MessageEvent & {data: P}) => {
self.postMessage(handler(data));
});
import {divide} from '~/resource/high-dimensional';
import worker from '~/utils/worker';
worker(divide);
import {transform} from '~/resource/histogram';
import worker from '~/utils/worker';
worker(transform);
import {range} from '~/resource/scalar';
import worker from '~/utils/worker';
worker(range);
import {transform} from '~/resource/scalar';
import worker from '~/utils/worker';
worker(transform);
......@@ -29,35 +29,31 @@
],
"scripts": {
"build": "rimraf dist && ts-node --script-mode builder/index.ts && tsc && cpy data dist/ --parents",
"test": "echo \"Error: no test specified\" && exit 0; #"
"test": "echo \"Error: no test specified\" && exit 0"
},
"devDependencies": {
"@types/express": "4.17.7",
"@types/express": "4.17.8",
"@types/mkdirp": "1.0.1",
"@types/node": "14.0.27",
"@types/node": "14.6.3",
"@types/node-fetch": "2.5.7",
"@types/rimraf": "3.0.0",
"cpy-cli": "3.1.1",
"get-port": "5.1.1",
"mime-types": "2.1.27",
"mkdirp": "1.0.4",
"node-fetch": "2.6.0",
"rimraf": "3.0.2",
"ts-node": "8.10.2",
"typescript": "3.9.7"
"ts-node": "9.0.0",
"typescript": "4.0.2"
},
"peerDependencies": {
"express": "^4.17.1"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
../../LICENSE
\ No newline at end of file
# VisualDL FrontEnd I18n
This is a fork version of [next-i18next](https://github.com/isaachinman/next-i18next) to support serverless.
Will use [this](https://github.com/isaachinman/next-i18next/issues/652#issuecomment-600525867) to replace it in the feature.
## Build
```bash
yarn build
```
import {Handler} from 'express';
import NextI18Next from './types';
declare function nextI18NextMiddleware(nexti18next: NextI18Next): Handler[];
export default nextI18NextMiddleware;
module.exports = require('./dist/middlewares/next-i18next-middleware');
{
"name": "@visualdl/i18n",
"version": "2.0.0",
"description": "A platform to visualize the deep learning process and result.",
"keywords": [
"visualdl",
"paddlepaddle",
"visualization",
"deep learning"
],
"homepage": "https://github.com/PaddlePaddle/VisualDL",
"bugs": {
"url": "https://github.com/PaddlePaddle/VisualDL/issues"
},
"license": "Apache-2.0",
"author": "PeterPanZH <littlepanzh@gmail.com> (https://github.com/PeterPanZH)",
"contributors": [
"Niandalu <littlepanzh@gmail.com> (https://github.com/Niandalu)"
],
"repository": {
"type": "git",
"url": "https://github.com/PaddlePaddle/VisualDL.git",
"directory": "frontend/packages/i18n"
},
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"files": [
"dist",
"types.d.ts",
"middleware.js",
"middleware.d.ts"
],
"main": "dist/index.js",
"types": "types.d.ts",
"dependencies": {
"detect-node": "2.0.4",
"hoist-non-react-statics": "3.3.2",
"i18next": "19.6.3",
"i18next-browser-languagedetector": "5.0.1",
"i18next-fs-backend": "1.0.7",
"i18next-http-backend": "1.0.17",
"i18next-http-middleware": "3.0.2",
"path-match": "1.2.4",
"prop-types": "15.7.2",
"react-i18next": "11.7.0",
"url": "0.11.0"
},
"devDependencies": {
"@types/express": "4.17.7",
"@types/hoist-non-react-statics": "3.3.1",
"@types/node": "14.0.27",
"@types/react": "16.9.44",
"@types/react-dom": "16.9.8",
"typescript": "3.9.7"
},
"peerDependencies": {
"express": "^4.17.1",
"next": "^9.3.4",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"engines": {
"node": ">=10",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
/*
This `Link` component is a wrap of the standard
NextJs `Link` component, with some simple lang
redirect logic in place.
If you haven't already, read this issue comment:
https://github.com/zeit/next.js/issues/2833#issuecomment-414919347
This component automatically provides this functionality:
<Link href="/product?slug=something" as="/products/something">
Wherein `slug` is actually our i18n lang, and it gets
pulled automatically.
Very important: if you import `Link` from NextJs directly,
and not this file, your lang subpath routing will break.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import {Config, I18n} from '../../types';
import NextLink, {LinkProps} from 'next/link';
import {lngPathCorrector, subpathIsRequired} from '../utils';
import PropTypes from 'prop-types';
import React from 'react';
import {withTranslation} from 'react-i18next';
const removeWithTranslationProps = (props: any) => {
const strippedProps = Object.assign({}, props);
delete strippedProps.defaultNS;
delete strippedProps.i18n;
delete strippedProps.i18nOptions;
delete strippedProps.lng;
delete strippedProps.reportNS;
delete strippedProps.t;
delete strippedProps.tReady;
delete strippedProps.forwardedRef;
return strippedProps;
};
type Props = LinkProps & {
i18n: I18n;
nextI18NextInternals: {
config: Config;
};
};
class Link extends React.Component<Props> {
static propTypes = {
as: PropTypes.string,
children: PropTypes.node.isRequired,
href: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
nextI18NextInternals: PropTypes.shape({
config: PropTypes.shape({
defaultLanguage: PropTypes.string.isRequired,
localeSubpaths: PropTypes.object.isRequired
}).isRequired
}).isRequired
};
static defaultProps = {
as: undefined
};
render() {
const {as, children, href, i18n, nextI18NextInternals, ...props} = this.props;
const {config} = nextI18NextInternals;
const {language} = i18n;
if (subpathIsRequired(config, language)) {
const {as: correctedAs, href: correctedHref} = lngPathCorrector(config, {as, href}, language);
return (
<NextLink href={correctedHref} as={correctedAs} {...removeWithTranslationProps(props)}>
{children}
</NextLink>
);
}
return (
<NextLink href={href} as={as} {...removeWithTranslationProps(props)}>
{children}
</NextLink>
);
}
}
/*
Usage of `withTranslation` here is just to
force `Link` to rerender on language change
*/
export default withTranslation()(Link as any);
/* eslint-disable @typescript-eslint/no-explicit-any */
import PropTypes from 'prop-types';
import React from 'react';
import {withTranslation} from 'react-i18next';
interface Props {
tReady: boolean;
}
class NextStaticProvider extends React.Component<Props> {
static propTypes = {
children: PropTypes.node.isRequired,
tReady: PropTypes.bool
};
static defaultProps = {
tReady: true
};
render() {
const {children, tReady} = this.props;
return tReady ? children : null;
}
}
export default withTranslation()(NextStaticProvider as any);
export {default as Link} from './Link';
export {default as NextStaticProvider} from './NextStaticProvider';
/* eslint-disable @typescript-eslint/no-explicit-any */
import {consoleMessage, isServer} from '../utils';
import {Config} from '../../types';
import {defaultConfig} from './default-config';
const deepMergeObjects = ['backend', 'detection'];
const dedupe = (names: string[]) => names.filter((v, i) => names.indexOf(v) === i);
const STATIC_LOCALE_PATH = 'static/locales';
export const createConfig = (userConfig: Config): Config => {
if (typeof userConfig.localeSubpaths === 'string') {
throw new Error('The localeSubpaths option has been changed to an object. Please refer to documentation.');
}
/*
Initial merge of default and user-provided config
*/
const combinedConfig = {
...defaultConfig,
...userConfig
};
/*
Sensible defaults to prevent user duplication
*/
combinedConfig.allLanguages = dedupe(combinedConfig.otherLanguages.concat([combinedConfig.defaultLanguage]));
combinedConfig.whitelist = combinedConfig.allLanguages;
const {allLanguages, defaultLanguage, localeExtension, localePath, localeStructure} = combinedConfig;
if (isServer()) {
const fs = eval("require('fs')");
const path = require('path'); // eslint-disable-line @typescript-eslint/no-var-requires
const projectRoot = combinedConfig.projectRoot || process.cwd();
let serverLocalePath = localePath;
/*
Validate defaultNS
https://github.com/isaachinman/next-i18next/issues/358
*/
if (typeof combinedConfig.defaultNS === 'string') {
const defaultFile = `/${defaultLanguage}/${combinedConfig.defaultNS}.${localeExtension}`;
const defaultNSPath = path.join(projectRoot, localePath, defaultFile);
const defaultNSExists = fs.existsSync(defaultNSPath);
if (!defaultNSExists) {
/*
If defaultNS doesn't exist, try to fall back to the deprecated static folder
https://github.com/isaachinman/next-i18next/issues/523
*/
const staticDirPath = path.join(projectRoot, STATIC_LOCALE_PATH, defaultFile);
const staticDirExists = fs.existsSync(staticDirPath);
if (staticDirExists) {
consoleMessage(
'warn',
'next-i18next: Falling back to /static folder, deprecated in next@9.1.*',
combinedConfig as Config
);
serverLocalePath = STATIC_LOCALE_PATH;
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(`Default namespace not found at ${defaultNSPath}`);
}
}
}
/*
Set server side backend
*/
combinedConfig.backend = {
loadPath: path.join(projectRoot, `${serverLocalePath}/${localeStructure}.${localeExtension}`),
addPath: path.join(projectRoot, `${serverLocalePath}/${localeStructure}.missing.${localeExtension}`)
};
/*
Set server side preload (languages and namespaces)
*/
combinedConfig.preload = allLanguages;
if (!combinedConfig.ns) {
const getAllNamespaces = (p: string) =>
fs.readdirSync(p).map((file: string) => file.replace(`.${localeExtension}`, ''));
combinedConfig.ns = getAllNamespaces(path.join(projectRoot, `${serverLocalePath}/${defaultLanguage}`));
}
} else {
let clientLocalePath = localePath;
/*
Remove public prefix from client site config
*/
if (localePath.startsWith('public/')) {
clientLocalePath = localePath.replace(/^public\//, '');
}
/*
Set client side backend
*/
const publicPath = combinedConfig.publicPath;
combinedConfig.backend = {
loadPath: `${publicPath}/${clientLocalePath}/${localeStructure}.${localeExtension}`,
addPath: `${publicPath}/${clientLocalePath}/${localeStructure}.missing.${localeExtension}`
};
combinedConfig.ns = [combinedConfig.defaultNS];
}
/*
Set fallback language to defaultLanguage in production
*/
if (!userConfig.fallbackLng) {
(combinedConfig as any).fallbackLng =
process.env.NODE_ENV === 'production' ? combinedConfig.defaultLanguage : false;
}
/*
Deep merge with overwrite - goes last
*/
deepMergeObjects.forEach(obj => {
if ((userConfig as any)[obj]) {
(combinedConfig as any)[obj] = {
...(defaultConfig as any)[obj],
...(userConfig as any)[obj]
};
}
});
return combinedConfig as Config;
};
import {isServer} from '../utils';
const DEFAULT_LANGUAGE = 'en';
const OTHER_LANGUAGES: string[] = [];
const DEFAULT_NAMESPACE = 'common';
const LOCALE_PATH = 'public/static/locales';
const LOCALE_STRUCTURE = '{{lng}}/{{ns}}';
const LOCALE_EXTENSION = 'json';
export const defaultConfig = {
publicPath: '',
defaultLanguage: DEFAULT_LANGUAGE,
otherLanguages: OTHER_LANGUAGES,
load: 'currentOnly',
localePath: LOCALE_PATH,
localeStructure: LOCALE_STRUCTURE,
localeExtension: LOCALE_EXTENSION,
localeSubpaths: {},
use: [],
defaultNS: DEFAULT_NAMESPACE,
interpolation: {
escapeValue: false,
formatSeparator: ',',
format: (value: string, format: string) => (format === 'uppercase' ? value.toUpperCase() : value)
},
browserLanguageDetection: true,
serverLanguageDetection: true,
ignoreRoutes: ['/_next/', '/static/', '/public/', '/api/'],
customDetectors: [],
detection: {
lookupCookie: 'next-i18next',
order: ['cookie', 'header', 'querystring'],
caches: ['cookie']
},
react: {
wait: true,
useSuspense: false
},
strictMode: true,
errorStackTraceLimit: 0,
shallowRender: false,
get initImmediate() {
return !isServer();
}
};
import {Config, InitPromise} from '../types';
import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector';
import i18n from 'i18next';
import i18nextHttpBackend from 'i18next-http-backend';
import isNode from 'detect-node';
export default (config: Config) => {
let initPromise: InitPromise | null = null;
if (!i18n.isInitialized) {
if (isNode) {
const i18nextNodeBackend = eval('require("i18next-fs-backend")');
const i18nextMiddleware = eval('require("i18next-http-middleware")');
i18n.use(i18nextNodeBackend);
if (config.serverLanguageDetection) {
const serverDetectors = new i18nextMiddleware.LanguageDetector();
config.customDetectors?.forEach(detector => serverDetectors.addDetector(detector));
i18n.use(serverDetectors);
}
} else {
i18n.use(i18nextHttpBackend);
if (config.browserLanguageDetection) {
const browserDetectors = new I18nextBrowserLanguageDetector();
config.customDetectors?.forEach(detector => browserDetectors.addDetector(detector));
i18n.use(browserDetectors);
}
}
config.use?.forEach(x => i18n.use(x));
initPromise = i18n.init(config);
}
return {i18n, initPromise};
};
/* eslint-disable @typescript-eslint/no-explicit-any */
import {I18nextProvider, withSSR} from 'react-i18next';
import {isServer, lngFromReq, lngPathCorrector, lngsToLoad} from '../utils';
import {AppContext} from 'next/app';
import {I18n} from '../../types';
import NextI18Next from '../index';
import {NextPageContext} from 'next';
import {NextStaticProvider} from '../components';
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import {withRouter} from 'next/router';
interface Props {
initialLanguage: string;
initialI18nStore: any;
i18nServerInstance: any;
}
interface WrappedComponentProps {
pageProps: {
namespacesRequired?: string[];
};
}
type I18nReq = {
i18n?: I18n;
locale?: string;
lng?: string;
language?: string;
};
type I18nRes = {
locals?: {
language?: string;
languageDir?: string;
};
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const appWithTranslation = function (this: NextI18Next, WrappedComponent: any) {
const WrappedComponentWithSSR = withSSR()(WrappedComponent);
const {config, i18n} = this;
const consoleMessage = this.consoleMessage.bind(this);
const clientLoadNamespaces = (lng: string, namespaces: string[]) =>
Promise.all(namespaces.filter(ns => !i18n.hasResourceBundle(lng, ns)).map(ns => i18n.reloadResources(lng, ns)));
class AppWithTranslation extends React.Component<Props> {
constructor(props: any) {
super(props);
if (!isServer()) {
const changeLanguageCallback = (prevLng: string, newLng: string) => {
const {router} = props;
const {query} = router;
let {pathname, asPath} = router;
if (config.publicPath) {
const publicPath = config.publicPath;
if (pathname.indexOf(publicPath) === 0) {
pathname = pathname.replace(publicPath, '');
}
if (asPath.indexOf(publicPath) === 0) {
asPath = asPath.replace(publicPath, '');
}
}
const routeInfo = {pathname, query};
if ((i18n as any).initializedLanguageOnce && typeof newLng === 'string' && prevLng !== newLng) {
const {as, href} = lngPathCorrector(config, {as: asPath, href: routeInfo}, newLng);
router.replace(href, as, {shallow: config.shallowRender});
}
};
const changeLanguage = i18n.changeLanguage.bind(i18n);
i18n.changeLanguage = async (newLng: string, callback = () => null) => {
const prevLng = i18n.language;
if (typeof newLng === 'string' && (i18n as any).initializedLanguageOnce === true) {
const usedNamespaces = Object.entries((i18n.reportNamespaces as any).usedNamespaces)
.filter(x => x[1] === true)
.map(x => x[0]);
await clientLoadNamespaces(newLng, usedNamespaces);
}
return changeLanguage(newLng, () => {
changeLanguageCallback(prevLng, newLng);
callback(null, i18n.t);
});
};
}
}
static async getInitialProps(ctx: AppContext) {
let wrappedComponentProps: WrappedComponentProps = {pageProps: {}};
if (WrappedComponent.getInitialProps) {
wrappedComponentProps = await WrappedComponent.getInitialProps(ctx);
}
if (typeof wrappedComponentProps.pageProps === 'undefined') {
consoleMessage(
'error',
'If you have a getInitialProps method in your custom _app.js file, you must explicitly return pageProps. For more information, see: https://github.com/zeit/next.js#custom-app'
);
}
/*
Initiate vars to return
*/
const req = ctx.ctx.req as (NextPageContext['req'] & I18nReq) | undefined;
let initialI18nStore: Record<string, any> = {};
let initialLanguage = null;
let i18nServerInstance = null;
if (req && !req.i18n) {
const {router} = ctx;
const result = router.asPath.match(/^\/(.*?)\//);
const lng = result ? result[1] : config.defaultLanguage;
req.i18n = i18n.cloneInstance({initImmediate: false, lng});
const res = ctx.ctx.res as (NextPageContext['res'] & I18nRes) | undefined;
const setContextLocale = (lng?: string) => {
// SEE: i18n-express-middleware
req.language = req.locale = req.lng = lng;
if (res) {
res.locals = res.locals || {};
res.locals.language = lng;
res.locals.languageDir = i18n.dir(lng);
}
};
setContextLocale(lng);
i18n.on('languageChanged', setContextLocale);
}
/*
Step 1: Determine initial language
*/
if (req && req.i18n) {
initialLanguage = lngFromReq(req as any);
/*
Perform a lang change in case we're not on the right lang
*/
await req.i18n.changeLanguage(initialLanguage as string);
} else if (Array.isArray(i18n.languages) && i18n.languages.length > 0) {
initialLanguage = i18n.language;
}
/*
Step 2: Determine namespace dependencies
*/
let namespacesRequired = config.ns;
if (Array.isArray(wrappedComponentProps.pageProps.namespacesRequired)) {
({namespacesRequired} = wrappedComponentProps.pageProps);
} else {
consoleMessage(
'warn',
`You have not declared a namespacesRequired array on your page-level component: ${
ctx.Component.displayName || ctx.Component.name || 'Component'
}. This will cause all namespaces to be sent down to the client, possibly negatively impacting the performance of your app. For more info, see: https://github.com/isaachinman/next-i18next#4-declaring-namespace-dependencies`
);
}
/*
We must always send down the defaultNS, otherwise
the client will trigger a request for it and issue
the "Did not expect server HTML to contain a <h1> in <div>"
error
*/
if (typeof config.defaultNS === 'string' && !(namespacesRequired as string[]).includes(config.defaultNS)) {
(namespacesRequired as string[]).push(config.defaultNS);
}
/*
Step 3: Perform data fetching, depending on environment
*/
if (req && req.i18n) {
/*
Detect the languages to load based upon the fallbackLng configuration
*/
const {fallbackLng} = config;
const languagesToLoad = lngsToLoad(initialLanguage, fallbackLng, config.otherLanguages);
/*
Initialize the store with the languagesToLoad and
necessary namespaces needed to render this specific tree
*/
languagesToLoad.forEach(lng => {
initialI18nStore[lng as string] = {};
(namespacesRequired as string[]).forEach(ns => {
initialI18nStore[lng as string][ns] =
((req.i18n as I18n).services.resourceStore.data[lng as string] || {})[ns] || {};
});
});
} else if (Array.isArray(i18n.languages) && i18n.languages.length > 0) {
/*
Load newly-required translations if changing route client side
*/
await clientLoadNamespaces(i18n.languages[0], namespacesRequired as string[]);
initialI18nStore = (i18n as any).store.data;
}
/*
Step 4: Overwrite i18n.toJSON method to be able to serialize the instance
*/
if (req && req.i18n) {
(req.i18n as any).toJSON = () => null;
i18nServerInstance = req.i18n;
}
/*
`pageProps` will get serialized automatically by NextJs
*/
return {
initialI18nStore,
initialLanguage,
i18nServerInstance,
...wrappedComponentProps
};
}
render() {
const {initialLanguage, initialI18nStore, i18nServerInstance} = this.props;
return (
<I18nextProvider i18n={i18nServerInstance || i18n}>
<NextStaticProvider>
<WrappedComponentWithSSR
initialLanguage={initialLanguage}
initialI18nStore={initialI18nStore}
{...this.props}
/>
</NextStaticProvider>
</I18nextProvider>
);
}
}
return hoistNonReactStatics(withRouter(AppWithTranslation as any), WrappedComponent, {getInitialProps: true});
};
export {appWithTranslation} from './app-with-translation';
export {withInternals} from './with-internals';
/* eslint-disable @typescript-eslint/no-explicit-any */
import {NextI18NextInternals} from '../../types';
import React from 'react';
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const withInternals = (WrappedComponent: any, config: NextI18NextInternals) => {
class WithInternals extends React.Component {
static displayName = `withNextI18NextInternals(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
render() {
return <WrappedComponent {...this.props} nextI18NextInternals={config} />;
}
}
return WithInternals;
};
import {
AppWithTranslation,
Config,
I18n,
InitConfig,
InitPromise,
Link as LinkType,
Router,
Trans as TransType,
UseTranslation,
WithTranslationHocType
} from '../types';
import {Trans, useTranslation, withTranslation} from 'react-i18next';
import {appWithTranslation, withInternals} from './hocs';
import {Link} from './components';
import {consoleMessage} from './utils';
import {createConfig} from './config/create-config';
import createI18NextClient from './create-i18next-client';
import hoistNonReactStatics from 'hoist-non-react-statics';
import {wrapRouter} from './router';
export {withTranslation} from 'react-i18next';
export default class NextI18Next {
readonly Trans: TransType;
readonly Link: LinkType;
readonly Router: Router;
readonly i18n: I18n;
readonly initPromise: InitPromise;
readonly config: Config;
readonly useTranslation: UseTranslation;
readonly withTranslation: WithTranslationHocType;
readonly appWithTranslation: AppWithTranslation;
readonly consoleMessage: typeof consoleMessage;
readonly withNamespaces: () => void;
constructor(userConfig: InitConfig) {
this.config = createConfig(userConfig as Config);
this.consoleMessage = consoleMessage.bind(this);
/* Validation */
if (this.config.otherLanguages.length <= 0) {
throw new Error(
'To properly initialise a next-i18next instance you must provide one or more locale codes in config.otherLanguages.'
);
}
this.withNamespaces = () => {
throw new Error(
'next-i18next has upgraded to react-i18next v10 - please rename withNamespaces to withTranslation.'
);
};
const {i18n, initPromise} = createI18NextClient(this.config);
this.i18n = i18n;
this.initPromise = initPromise || Promise.resolve();
this.appWithTranslation = appWithTranslation.bind(this);
this.withTranslation = (namespace, options) => Component =>
hoistNonReactStatics(withTranslation(namespace, options)(Component), Component);
const nextI18NextInternals = {config: this.config, i18n: this.i18n};
this.Link = withInternals(Link, nextI18NextInternals) as LinkType;
this.Router = wrapRouter(nextI18NextInternals);
/* Directly export `react-i18next` methods */
this.Trans = Trans;
this.useTranslation = useTranslation;
}
}
export {default as nextI18NextMiddleware} from './next-i18next-middleware';
import {NextFunction, Request, Response} from 'express';
import {
addSubpath,
lngFromReq,
redirectWithoutCache,
removeSubpath,
subpathFromLng,
subpathIsPresent,
subpathIsRequired
} from '../utils';
import NextI18Next from '../index';
import i18nextMiddleware from 'i18next-http-middleware';
import pathMatch from 'path-match';
const route = pathMatch();
export default function (nexti18next: NextI18Next) {
const {config, i18n} = nexti18next;
const {allLanguages, ignoreRoutes, localeSubpaths, publicPath} = config;
const isI18nRoute = (req: Request) => ignoreRoutes?.every(x => !req.url.startsWith((publicPath || '') + x));
const localeSubpathRoute = route(`/:subpath(${Object.values(localeSubpaths || {}).join('|')})(.*)`);
const middleware = [];
/*
If not using server side language detection,
we need to manually set the language for
each request
*/
if (!config.serverLanguageDetection) {
middleware.push((req: Request, _res: Response, next: NextFunction) => {
if (isI18nRoute(req)) {
req.lng = config.defaultLanguage;
}
next();
});
}
/*
This does the bulk of the i18next work
*/
middleware.push(i18nextMiddleware.handle(i18n));
/*
This does the locale subpath work
*/
middleware.push((req: Request, res: Response, next: NextFunction) => {
if (isI18nRoute(req) && req.i18n) {
let url = publicPath ? req.url.replace(publicPath, '') : req.url;
if (!url) {
url = '/';
}
let currentLng = lngFromReq(req);
const currentLngSubpath = subpathFromLng(config, currentLng);
const currentLngRequiresSubpath = subpathIsRequired(config, currentLng || '');
const currentLngSubpathIsPresent = subpathIsPresent(url, currentLngSubpath);
const lngFromCurrentSubpath = allLanguages.find((l: string) =>
subpathIsPresent(url, subpathFromLng(config, l))
);
if (lngFromCurrentSubpath !== undefined && lngFromCurrentSubpath !== currentLng) {
/*
If a user has hit a subpath which does not
match their language, give preference to
the path, and change user language.
*/
req.i18n.changeLanguage(lngFromCurrentSubpath);
currentLng = lngFromCurrentSubpath;
} else if (currentLngRequiresSubpath && !currentLngSubpathIsPresent) {
/*
If a language subpath is required and
not present, prepend correct subpath
*/
return redirectWithoutCache(res, (publicPath || '') + addSubpath(url, currentLngSubpath));
}
/*
If a locale subpath is present in the URL,
modify req.url in place so that NextJs will
render the correct route
*/
if (typeof lngFromCurrentSubpath === 'string') {
const params = localeSubpathRoute(url);
if (params !== false) {
const {subpath} = params;
req.query = {...req.query, subpath, lng: currentLng};
req.url = (publicPath || '') + removeSubpath(url, subpath);
}
}
}
next();
});
return middleware;
}
export {wrapRouter} from './wrap-router';
/*
This `Router` is a wrap of the standard
NextJs `Router`, with some simple lang
redirect logic in place.
If you haven't already, read this issue comment:
https://github.com/zeit/next.js/issues/2833#issuecomment-414919347
Very important: if you import `Router` from NextJs directly,
and not this file, your lang subpath routing will break.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import NextRouter, {SingletonRouter} from 'next/router';
import {lngPathCorrector, subpathIsRequired} from '../utils';
const propertyFields = ['pathname', 'route', 'query', 'asPath', 'components', 'events'];
const coreMethods = ['reload', 'back', 'beforePopState', 'ready', 'prefetch'];
const wrappedMethods = ['push', 'replace'];
export const wrapRouter = (nextI18NextInternals: any) => {
const Router = {} as SingletonRouter;
propertyFields.forEach(field => {
Object.defineProperty(Router, field, {
get() {
return (NextRouter as any)[field];
}
});
});
coreMethods.forEach(method => {
(Router as any)[method] = (...args: any[]) => (NextRouter as any)[method](...args);
});
wrappedMethods.forEach(method => {
(Router as any)[method] = (path: string, as: any, options: any) => {
const {config, i18n} = nextI18NextInternals;
if (subpathIsRequired(config, i18n.languages[0])) {
const {as: correctedAs, href: correctedHref} = lngPathCorrector(
config,
{as, href: path},
i18n.languages[0]
);
return (NextRouter as any)[method](correctedHref, correctedAs, options);
}
return (NextRouter as any)[method](path, as, options);
};
});
return Router;
};
export const addSubpath = (url: string, subpath: string | null) =>
url
.replace('/', `/${subpath}/`)
.replace(/(https?:\/\/)|(\/)+/g, '$1$2')
.replace(/\/$/, '');
/* eslint-disable no-console */
import {Config} from '../../types';
import NextI18Next from '../index';
type MessageType = 'error' | 'info' | 'warn';
const messageTypes = {
error: 'error',
info: 'info',
warn: 'warn'
};
Object.freeze(messageTypes);
const logMessage = (messageType: MessageType, message: string) => {
if (Object.values(messageTypes).includes(messageType)) {
console[messageType](message);
} else {
console.info(message);
}
};
export const consoleMessage = function (
this: NextI18Next | void,
messageType: MessageType,
message: string,
config?: Config
) {
const {errorStackTraceLimit, strictMode} = config || (this as NextI18Next).config;
const prevStackLimit = Error.stackTraceLimit;
let util;
if (!strictMode) {
return;
}
if (process.env.NODE_ENV !== 'production') {
util = require('util');
} else {
return;
}
/*
Temporarily set the stacktrace to 0 or errorStackTraceLimit,
in order to only display a message
*/
Error.stackTraceLimit = errorStackTraceLimit || 0;
/*
Make room for new message
*/
console.log();
/*
Make sure the message is a string
*/
if (typeof message !== 'string') {
const metaError = new Error();
metaError.name = 'Meta';
metaError.message = `Param message needs to be of type: string. Instead, '${typeof message}' was provided.\n
------------------------------------------------\n
\u200b
The provided ${typeof message}:\n
\u200b
${util.inspect(message, true, 8, true)}
\u200b
------------------------------------------------\n
`;
console.error(metaError);
return;
}
/*
Log the message to console
*/
logMessage(messageType, message);
/*
Reset stack limit
*/
Error.stackTraceLimit = prevStackLimit;
};
export {addSubpath} from './add-subpath';
export {consoleMessage} from './console-message';
export {isServer} from './is-server';
export {lngFromReq} from './lng-from-req';
export {lngPathCorrector} from './lng-path-corrector';
export {lngsToLoad} from './lngs-to-load';
export {redirectWithoutCache} from './redirect-without-cache';
export {removeSubpath} from './remove-subpath';
export {subpathFromLng} from './subpath-from-lng';
export {subpathIsPresent} from './subpath-is-present';
export {subpathIsRequired} from './subpath-is-required';
import isNode from 'detect-node';
export const isServer = () => isNode && typeof window === 'undefined';
import {Config} from '../../types';
import {Request} from 'express';
export const lngFromReq = (req: Request) => {
if (!req.i18n) {
return null;
}
const {allLanguages, defaultLanguage, fallbackLng} = req.i18n.options as Config;
const fallback = fallbackLng || defaultLanguage;
if (!req.i18n.languages) {
return typeof fallback === 'string' ? fallback : null;
}
const language = req.i18n.languages.find(l => allLanguages.includes(l)) || fallback;
if (typeof language === 'string') {
return language;
}
return null;
};
/* eslint-disable @typescript-eslint/no-explicit-any */
import {format as formatUrl, parse as parseUrl} from 'url';
import {removeSubpath, subpathFromLng, subpathIsPresent, subpathIsRequired} from './index';
import {Config} from '../../types';
const parseAs = (originalAs: string | undefined, href: any) => {
const asType = typeof originalAs;
let as: string;
if (asType === 'undefined') {
as = formatUrl(href, {unicode: true});
} else if (asType === 'string') {
as = originalAs;
} else {
throw new Error(`'as' type must be 'string', but it is ${asType}`);
}
return as;
};
const parseHref = (originalHref: any) => {
const hrefType = typeof originalHref;
let href;
if (hrefType === 'string') {
href = parseUrl(originalHref, true /* parseQueryString */);
} else if (hrefType === 'object') {
href = {...originalHref};
href.query = originalHref.query ? {...originalHref.query} : {};
} else {
throw new Error(`'href' type must be either 'string' or 'object', but it is ${hrefType}`);
}
return href;
};
export const lngPathCorrector = (config: Config, currentRoute: any, currentLanguage: string) => {
const {allLanguages, localeSubpaths, publicPath} = config;
const {as: originalAs, href: originalHref} = currentRoute;
if (!allLanguages.includes(currentLanguage)) {
throw new Error('Invalid configuration: Current language is not included in all languages array');
}
const href = parseHref(originalHref);
let as = parseAs(originalAs, href);
if (publicPath) {
const url = parseUrl(as);
url.pathname = url.pathname.replace(publicPath, '');
as = formatUrl(url);
}
/*
url.format prefers the 'url.search' string over
the 'url.query' object, so remove the search
string to ensure the query object is used.
*/
delete href.search;
/*
Strip any/all subpaths from the `as` value
*/
Object.values(localeSubpaths || {}).forEach((subpath: string) => {
if (subpathIsPresent(as, subpath)) {
as = removeSubpath(as, subpath);
}
});
if (subpathIsRequired(config, currentLanguage)) {
const basePath = `${href.protocol}//${href.host}`;
const currentAs = as?.replace(basePath, '');
const subpath = subpathFromLng(config, currentLanguage);
as = `/${subpath}${currentAs}`.replace(/\/$/, '');
href.query.lng = currentLanguage;
href.query.subpath = subpath;
}
return {as, href};
};
import {FallbackLng} from 'i18next';
export const lngsToLoad = (initialLng: string | null, fallbackLng: FallbackLng | false, otherLanguages?: string[]) => {
const languages = [];
if (initialLng) {
languages.push(initialLng);
}
if (fallbackLng) {
if (typeof fallbackLng === 'string' && fallbackLng !== initialLng) {
languages.push(fallbackLng);
}
if (Array.isArray(fallbackLng)) {
languages.push(...fallbackLng);
} else if (initialLng && typeof fallbackLng !== 'string') {
if (typeof fallbackLng[initialLng] === 'string') {
languages.push(fallbackLng[initialLng]);
} else if (Array.isArray(fallbackLng[initialLng])) {
languages.push(...fallbackLng[initialLng]);
}
}
if (!Array.isArray(fallbackLng) && typeof fallbackLng !== 'string' && fallbackLng.default) {
languages.push(fallbackLng.default);
}
}
if (initialLng && initialLng.includes('-') && Array.isArray(otherLanguages)) {
const [languageFromLocale] = initialLng.split('-');
otherLanguages.forEach(otherLanguage => {
if (otherLanguage === languageFromLocale) {
languages.push(otherLanguage);
}
});
}
return languages;
};
import {Response} from 'express';
export const redirectWithoutCache = (res: Response, redirectLocation: string) => {
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
res.redirect(302, redirectLocation);
};
export const removeSubpath = (url: string | undefined, subpath: string) =>
url?.replace(subpath, '').replace(/(https?:\/\/)|(\/)+/g, '$1$2') || '';
import {Config} from '../../types';
export const subpathFromLng = (config: Config, language: string | null) => {
if (typeof language !== 'string') {
return null;
}
const subpath = config.localeSubpaths?.[language];
if (typeof subpath !== 'string') {
return null;
}
return subpath;
};
import {parse as parseUrl} from 'url';
export const subpathIsPresent = (url: string | undefined, subpath: string | null) => {
if (typeof url !== 'string' || typeof subpath !== 'string') {
return false;
}
const {pathname} = parseUrl(url);
return (
typeof pathname === 'string' &&
((pathname.length === subpath.length + 1 && pathname === `/${subpath}`) || pathname.startsWith(`/${subpath}/`))
);
};
import {Config} from '../../types';
export const subpathIsRequired = (config: Config, language: string) =>
typeof config.localeSubpaths?.[language] === 'string';
{
"compilerOptions": {
"jsx": "react",
"target": "es5",
"lib": [
"esnext",
"esnext.asynciterable",
"dom"
],
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"outDir": "dist"
},
"include": [
"src/**/*"
]
}
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'detect-node';
declare module 'path-match';
declare module 'i18next-http-middleware';
import * as React from 'react';
import {TFunction as I18NextTFunction, InitOptions, i18n} from 'i18next';
import {
WithTranslation as ReactI18nextWithTranslation,
TransProps,
useTranslation,
withTranslation
} from 'react-i18next';
import {LinkProps} from 'next/link';
import {SingletonRouter} from 'next/router';
export type InitConfig = {
publicPath?: string;
projectRoot?: string;
browserLanguageDetection?: boolean;
serverLanguageDetection?: boolean;
strictMode?: boolean;
defaultLanguage: string;
ignoreRoutes?: string[];
localePath?: string;
localeStructure?: string;
otherLanguages: string[];
localeSubpaths?: Record<string, string>;
use?: any[];
customDetectors?: any[];
shallowRender?: boolean;
errorStackTraceLimit?: number;
} & InitOptions;
export type Config = {
fallbackLng: boolean;
allLanguages: string[];
whitelist: string[];
preload: string[];
} & InitConfig;
export interface NextI18NextInternals {
config: Config;
i18n: I18n;
}
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;
export type WithTranslationHocType = typeof withTranslation;
export type WithTranslation = ReactI18nextWithTranslation;
export type InitPromise = Promise<TFunction | void>;
declare class NextI18Next {
constructor(config: InitConfig);
Trans: Trans;
Link: Link;
Router: Router;
i18n: I18n;
initPromise: InitPromise;
config: Config;
useTranslation: UseTranslation;
withTranslation: WithTranslationHocType;
appWithTranslation: AppWithTranslation;
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
i18n: I18n;
lng?: string;
}
}
}
export default NextI18Next;
import {Request, Response} from 'express';
/* eslint-disable no-console */
import faker from 'faker';
import path from 'path';
import express from 'express';
import middleware from './middleware';
const sleep = (time: number) => {
return new Promise(resolve => setTimeout(resolve, time));
};
const host = process.env.HOST || 'localhost';
const port = Number.parseInt(process.env.PORT || '', 10) || 8998;
const apiUrl = process.env.API_URL || '/api';
export type Options = {
path?: string;
delay?: number | ((method: string) => number);
};
export interface Options {
host?: string;
port?: number;
apiUrl?: string;
}
export default (options: Options) => {
return async (req: Request, res: Response) => {
let method = req.path;
if (!method) {
method = Array.isArray(req.query.method)
? req.query.method.join('/')
: 'string' === typeof req.query.method
? req.query.method
: '';
} else {
method = method.replace(/^\//, '');
}
const server = express();
if (!method) {
res.status(404).send({});
return;
}
async function start(options?: Options) {
const config = Object.assign({host, port, apiUrl}, options);
try {
let {default: mock} = await import(path.resolve(options.path || path.join(__dirname, 'data'), method));
server.use(config.apiUrl, middleware());
if ('function' === typeof mock) {
mock = await mock(req, res);
}
let delay = 0;
if ('function' === typeof options.delay) {
delay = options.delay(method);
} else if (options.delay) {
delay = options.delay;
}
const s = server.listen(config.port, config.host, () => {
process.send?.('ready');
console.log(`> Ready on http://${config.host}:${config.port}${config.apiUrl}`);
process.on('SIGINT', () => {
s.close((err: Error | undefined) => {
if (err) {
throw err;
}
process.exit(0);
});
});
});
}
if (delay) {
await sleep(delay);
}
if (require.main === module) {
start();
}
if (Buffer.isBuffer(mock)) {
res.send(mock);
} else if (mock instanceof ArrayBuffer) {
res.send(Buffer.from(mock));
} else {
const result = JSON.parse(faker.fake(JSON.stringify(mock, null, 4)));
if (result && 'status' in result && 'data' in result) {
res.json(result);
} else {
res.json({status: 0, msg: '', data: result});
}
}
} catch (e) {
res.status(500).send(e.message);
// eslint-disable-next-line no-console
console.error(e);
}
};
};
export {start, middleware};
import {Request, Response} from 'express';
import faker from 'faker';
import path from 'path';
const sleep = (time: number) => {
return new Promise(resolve => setTimeout(resolve, time));
};
export type Options = {
path?: string;
delay?: number | ((method: string) => number);
};
export default (options?: Options) => {
return async (req: Request, res: Response) => {
let method = req.path;
if (!method) {
method = Array.isArray(req.query.method)
? req.query.method.join('/')
: 'string' === typeof req.query.method
? req.query.method
: '';
} else {
method = method.replace(/^\//, '');
}
if (!method) {
res.status(404).send({});
return;
}
try {
let {default: mock} = await import(path.resolve(options?.path || path.join(__dirname, 'data'), method));
if ('function' === typeof mock) {
mock = await mock(req, res);
}
let delay = 0;
if ('function' === typeof options?.delay) {
delay = options.delay(method);
} else if (options?.delay) {
delay = options.delay;
}
if (delay) {
await sleep(delay);
}
if (Buffer.isBuffer(mock)) {
res.send(mock);
} else if (mock instanceof ArrayBuffer) {
res.send(Buffer.from(mock));
} else {
const result = JSON.parse(faker.fake(JSON.stringify(mock, null, 4)));
if (result && 'status' in result && 'data' in result) {
res.json(result);
} else {
res.json({status: 0, msg: '', data: result});
}
}
} catch (e) {
res.status(500).send(e.message);
// eslint-disable-next-line no-console
console.error(e);
}
};
};
......@@ -23,40 +23,36 @@
"directory": "frontend/packages/mock"
},
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rimraf dist && tsc && cpy assets dist/ --parents",
"test": "echo \"Error: no test specified\" && exit 0; #"
"dev": "ts-node index.ts",
"test": "echo \"Error: no test specified\" && exit 0"
},
"dependencies": {
"faker": "4.1.0",
"express": "4.17.1",
"faker": "5.1.0",
"isomorphic-unfetch": "3.0.0",
"mime-types": "2.1.27"
},
"devDependencies": {
"@types/express": "4.17.7",
"@types/express": "4.17.8",
"@types/faker": "4.1.12",
"@types/node": "14.0.27",
"@types/node": "14.6.3",
"cpy-cli": "3.1.1",
"rimraf": "3.0.2",
"typescript": "3.9.7"
},
"peerDependencies": {
"express": "^4.17.1"
"ts-node": "9.0.0",
"typescript": "4.0.2"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
{
"compilerOptions": {
"target": "es5",
"target": "es2018",
"module": "commonjs",
"lib": [
"esnext",
"esnext.asynciterable",
......
# VisualDL FrontEnd Server
A fork version of [Netron](https://github.com/lutzroeder/netron).
## Build
```bash
yarn build
```
......@@ -24,7 +24,7 @@
},
"scripts": {
"build": "rimraf dist && webpack",
"test": "echo \"Error: no test specified\" && exit 0; #"
"test": "echo \"Error: no test specified\" && exit 0"
},
"files": [
"dist"
......@@ -41,27 +41,23 @@
},
"devDependencies": {
"autoprefixer": "9.8.6",
"copy-webpack-plugin": "6.0.3",
"css-loader": "4.2.0",
"html-webpack-plugin": "4.3.0",
"mini-css-extract-plugin": "0.9.0",
"copy-webpack-plugin": "6.1.0",
"css-loader": "4.2.2",
"html-webpack-plugin": "4.4.1",
"mini-css-extract-plugin": "0.11.0",
"postcss-loader": "3.0.0",
"rimraf": "3.0.2",
"sass": "1.26.10",
"sass-loader": "9.0.3",
"terser": "5.0.0",
"webpack": "4.43.0"
"sass-loader": "10.0.1",
"terser": "5.2.1",
"webpack": "4.44.1",
"webpack-cli": "3.3.12"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
......@@ -18,6 +18,33 @@ const metadata = src.filter(file => path.extname(file) === '.json');
module.exports = {
mode: 'production',
context: __dirname,
stats: {
assets: false,
builtAt: true,
cached: false,
cachedAssets: false,
children: false,
chunks: false,
chunkGroups: false,
chunkModules: false,
chunkOrigins: false,
colors: true,
entrypoints: false,
errors: true,
errorDetails: true,
hash: true,
modules: false,
moduleTrace: false,
performance: false,
providedExports: false,
publicPath: true,
reasons: false,
source: false,
timings: true,
usedExports: false,
version: true,
warnings: false
},
entry: {
index: './src/index.js',
shim: './src/shim.js',
......
/* eslint-disable no-console */
import builder, {projectRoot} from '@visualdl/core/build';
import {cp, mkdir, rm} from 'shelljs';
import config from './webpack.config';
import path from 'path';
import webpack from 'webpack';
const dist = path.resolve(__dirname, 'dist');
console.log('Building server');
console.log(`Source: ${projectRoot}`);
console.log(`Destination: ${dist}`);
async function start() {
try {
await builder('build');
rm('-rf', dist);
mkdir('-p', dist);
cp(
'-Rf',
['dist', 'pages', 'public', 'next.config.js', 'package.json'].map(file => path.join(projectRoot, file)),
dist
);
} catch (e) {
process.exit(e);
}
console.log('Webpack building...');
const compiler = webpack(config as webpack.Configuration);
compiler.run(err => {
if (err) {
console.error(err);
process.exit(1);
}
});
}
start();
......@@ -5,8 +5,8 @@ module.exports = {
apps: [
{
name: 'visualdl',
script: 'index.js',
cwd: require('path').resolve(__dirname, './dist'),
script: 'dist/index.js',
cwd: __dirname,
args: '',
instances: 'max',
autorestart: true,
......
/* eslint-disable no-console */
import config from '@visualdl/core/next.config';
import {config} from 'dotenv';
import express from 'express';
import next from 'next';
import nextI18NextMiddleware from '@visualdl/i18n/middleware';
import path from 'path';
import {setConfig} from 'next/config';
import resolve from 'enhanced-resolve';
config();
const isDev = process.env.NODE_ENV === 'development';
const isDemo = !!process.env.DEMO;
......@@ -15,33 +15,18 @@ const port = Number.parseInt(process.env.PORT || '', 10) || 8999;
const backend = process.env.BACKEND;
const delay = Number.parseInt(process.env.DELAY || '', 10);
const publicPath = process.env.PUBLIC_PATH || '/';
const apiUrl = process.env.API_URL || `${process.env.PUBLIC_PATH || ''}/api`;
const pingUrl = process.env.PING_URL;
const server = express();
async function start() {
setConfig(config);
const app = next({dev: isDev, conf: config});
const handle = app.getRequestHandler();
await app.prepare();
const root = path.dirname(resolve.sync(__dirname, '@visualdl/core'));
if (isDev) {
const {default: webpack} = await import('webpack');
const {default: webpackDevMiddleware} = await import('webpack-dev-middleware');
const {default: webpackConfig} = await import('./webpack.config');
const compiler = webpack(webpackConfig);
server.use(
webpackDevMiddleware(compiler, {
publicPath
})
);
}
const app = express();
async function start() {
if (backend) {
const {createProxyMiddleware} = await import('http-proxy-middleware');
server.use(
config.env.API_URL,
app.use(
apiUrl,
createProxyMiddleware({
target: backend,
changeOrigin: true
......@@ -49,48 +34,43 @@ async function start() {
);
} else if (isDemo) {
const {default: demo} = await import('@visualdl/demo');
server.use(config.env.API_URL, demo);
app.use(apiUrl, demo);
} else if (isDev) {
const {default: mock} = await import('@visualdl/mock');
server.use(config.env.API_URL, mock({delay: delay ? () => Math.random() * delay : 0}));
const {middleware: mock} = await import('@visualdl/mock');
app.use(apiUrl, mock({delay: delay ? () => Math.random() * delay : 0}));
} else {
console.warn('Server is running in production mode but no backend address specified.');
}
if (publicPath !== '/') {
server.get('/', (_req, res) => {
app.get('/', (_req, res) => {
res.redirect(publicPath);
});
}
if (
process.env.PING_URL &&
process.env.PING_URL !== '/' &&
process.env.PING_URL !== publicPath &&
process.env.PING_URL.startsWith('/')
) {
server.get(process.env.PING_URL, (_req, res) => {
if (pingUrl && pingUrl !== '/' && pingUrl !== publicPath && pingUrl.startsWith('/')) {
app.get(pingUrl, (_req, res) => {
res.type('text/plain');
res.status(200).send('OK!');
});
}
const {default: nextI18Next} = await import('@visualdl/core/utils/i18n');
await nextI18Next.initPromise;
server.use(nextI18NextMiddleware(nextI18Next));
app.use(publicPath, express.static(root, {index: false}));
server.get(/\.wasm/, (_req, res, next) => {
app.get(/\.wasm/, (_req, res, next) => {
res.type('application/wasm');
next();
});
server.get('*', (req, res) => handle(req, res));
app.get('*', (_req, res) => {
res.sendFile('index.html', {root});
});
const s = server.listen(port, host, () => {
const server = app.listen(port, host, () => {
process.send?.('ready');
console.log(`> Ready on http://${host}:${port}`);
process.on('SIGINT', () => {
s.close((err: Error | undefined) => {
server.close(err => {
if (err) {
throw err;
}
......@@ -101,19 +81,9 @@ async function start() {
}
if (require.main === module) {
const core = require.resolve('@visualdl/core');
// after webpack building, we dont need to chdir
if ('string' === typeof core) {
const cwd = process.cwd();
const wd = path.dirname(core);
process.chdir(wd);
process.on('exit', () => process.chdir(cwd));
process.on('uncaughtException', () => process.chdir(cwd));
process.on('unhandledRejection', () => process.chdir(cwd));
}
start();
}
export default start;
export {server};
export {app};
......@@ -23,11 +23,10 @@
"directory": "frontend/packages/server"
},
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon --watch index.ts --watch webpack.config.js --exec \"ts-node index.ts\"",
"build": "ts-node --script-mode build.ts",
"build:webpack": "webpack",
"build": "tsc",
"dev": "cross-env NODE_ENV=development nodemon --watch index.ts --exec \"ts-node index.ts\"",
"start": "pm2-runtime ecosystem.config.js",
"test": "echo \"Error: no test specified\" && exit 0; #"
"test": "echo \"Error: no test specified\" && exit 0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
......@@ -39,39 +38,27 @@
"dependencies": {
"@visualdl/core": "2.0.0",
"@visualdl/demo": "2.0.0",
"@visualdl/i18n": "2.0.0",
"dotenv": "8.2.0",
"enhanced-resolve": "4.3.0",
"express": "4.17.1",
"http-proxy-middleware": "1.0.5",
"next": "9.4.4",
"pm2": "4.4.0"
"pm2": "4.4.1"
},
"devDependencies": {
"@types/express": "4.17.7",
"@types/node": "14.0.27",
"@types/shelljs": "0.8.8",
"@types/webpack": "4.41.21",
"@types/webpack-dev-middleware": "3.7.1",
"@types/enhanced-resolve": "3.0.6",
"@types/express": "4.17.8",
"@types/node": "14.6.3",
"@visualdl/mock": "2.0.0",
"cross-env": "7.0.2",
"nodemon": "2.0.4",
"shelljs": "0.8.4",
"ts-loader": "8.0.2",
"ts-node": "8.10.2",
"typescript": "3.9.7",
"webpack": "4.43.0",
"webpack-cli": "3.3.12",
"webpack-dev-middleware": "3.7.2"
"ts-node": "9.0.0",
"typescript": "4.0.2"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
import webpack from 'webpack';
declare const config: webpack.Configuration;
export default config;
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
module.exports = {
mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
entry: path.resolve(__dirname, './index.ts'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js'
},
target: 'node',
module: {
rules: [
{
test: /\.ts$/,
use: require.resolve('ts-loader')
}
]
},
resolve: {
extensions: ['.wasm', '.ts', '.mjs', '.js', '.json']
},
externals: (context, request, callback) => {
if (request.indexOf(__dirname) === 0) {
return callback();
}
if (/node_modules\/webpack\/buildin/.test(request)) {
return callback();
}
if (/^\./.test(request)) {
return callback();
}
if (/^@visualdl\/core/.test(request)) {
return callback();
}
callback(null, 'commonjs ' + request);
}
};
../../LICENSE
\ No newline at end of file
# VisualDL FrontEnd Serverless
Serverless version of VisualDL pages.
This will generate static html pages that can be served by third party web servers such as Flask.
## Build
```bash
yarn build
```
/* eslint-disable no-console */
import builder, {projectRoot} from '@visualdl/core/build';
import path from 'path';
import rimraf from 'rimraf';
const dist = path.join(__dirname, 'dist');
console.log('Building serverless');
console.log(`Source: ${projectRoot}`);
console.log(`Destination: ${dist}`);
function clean(): Promise<void> {
return new Promise((resolve, reject) => {
rimraf(dist, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function start() {
try {
await clean();
await builder('build');
await builder('export', '-o', dist);
} catch (e) {
if ('number' === typeof e) {
process.exit(e);
}
console.error(e);
process.exit(1);
}
process.exit(0);
}
start();
{
"name": "@visualdl/serverless",
"version": "2.0.0",
"description": "A platform to visualize the deep learning process and result.",
"keywords": [
"visualdl",
"paddlepaddle",
"visualization",
"deep learning"
],
"homepage": "https://github.com/PaddlePaddle/VisualDL",
"bugs": {
"url": "https://github.com/PaddlePaddle/VisualDL/issues"
},
"license": "Apache-2.0",
"author": "PeterPanZH <littlepanzh@gmail.com> (https://github.com/PeterPanZH)",
"contributors": [
"Niandalu <littlepanzh@gmail.com> (https://github.com/Niandalu)"
],
"repository": {
"type": "git",
"url": "https://github.com/PaddlePaddle/VisualDL.git",
"directory": "frontend/packages/serverless"
},
"main": "dist/index.html",
"files": [
"dist"
],
"scripts": {
"build": "cross-env ts-node --script-mode build.ts",
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"devDependencies": {
"@types/node": "14.0.27",
"@types/rimraf": "3.0.0",
"@visualdl/core": "2.0.0",
"cross-env": "7.0.2",
"rimraf": "3.0.2",
"ts-node": "8.10.2",
"typescript": "3.9.7"
},
"engines": {
"node": ">=10",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2018",
"skipLibCheck": true,
"esModuleInterop": true
},
"files": [
"build.ts"
]
}
......@@ -28,25 +28,21 @@
"dist/index.js",
"dist/index.d.ts"
],
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "wasm-pack build --release --out-dir dist --out-name index .",
"test": "echo \"Error: no test specified\" && exit 0; #"
"build": "wasm-pack build --release --out-dir dist --out-name index --target web .",
"test": "echo \"Error: no test specified\" && exit 0"
},
"devDependencies": {
"wasm-pack": "0.9.1"
},
"engines": {
"node": ">=10",
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
......@@ -19,10 +19,18 @@ pub fn scalar_transform(js_datasets: &JsValue, smoothing: f64) -> JsValue {
}
#[wasm_bindgen]
pub fn scalar_range(js_datasets: &JsValue, outlier: bool) -> JsValue {
pub fn scalar_range(js_datasets: &JsValue) -> JsValue {
utils::set_panic_hook();
let datasets: Vec<Vec<scalar::Smoothed>> = js_datasets.into_serde().unwrap();
let result = scalar::range(&datasets, outlier);
let result = scalar::range(&datasets);
JsValue::from_serde(&result).unwrap()
}
#[wasm_bindgen]
pub fn scalar_axis_range(js_datasets: &JsValue, outlier: bool) -> JsValue {
utils::set_panic_hook();
let datasets: Vec<Vec<scalar::Smoothed>> = js_datasets.into_serde().unwrap();
let result = scalar::axis_range(&datasets, outlier);
JsValue::from_serde(&result).unwrap()
}
......
......@@ -72,7 +72,27 @@ pub fn transform(datasets: &Vec<Vec<Dataset>>, smoothing: f64) -> Vec<Vec<Smooth
return result;
}
pub fn range(datasets: &Vec<Vec<Smoothed>>, outlier: bool) -> Range {
pub fn range(datasets: &Vec<Vec<Smoothed>>) -> Vec<Range> {
let mut ranges: Vec<Range> = vec![];
for data in datasets.iter() {
let n: usize = data.len();
if n == 0 {
ranges.push(Range::new(f64::NAN, f64::NAN));
}
let values: Vec<f64> = data.iter().map(|x| x.2).collect();
let mut sorted: Vec<f64> = values.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
ranges.push(Range::new(sorted[0], sorted[n - 1]));
}
return ranges;
}
pub fn axis_range(datasets: &Vec<Vec<Smoothed>>, outlier: bool) -> Range {
let mut ranges: Vec<Range> = vec![];
for data in datasets.iter() {
......
......@@ -5,7 +5,7 @@ set -e
WORKING_PATH=$(pwd)
SERVER_DIR="packages/server"
SERVER_DIR_PATH="$WORKING_PATH/$SERVER_DIR"
SERVERLESS_DIR="packages/serverless/dist"
SERVERLESS_DIR="packages/core/dist"
SERVERLESS_DIR_PATH="$WORKING_PATH/$SERVERLESS_DIR"
OUTPUT="output"
OUTPUT_PATH="$WORKING_PATH/$OUTPUT"
......@@ -19,7 +19,7 @@ yarn clean
# build
if [ "$SCOPE" = "serverless" ]; then
npx lerna run --scope "@visualdl/serverless" --include-dependencies build
npx lerna run --scope "@visualdl/core" --include-dependencies build
elif [ "$SCOPE" = "server" ]; then
npx lerna run --scope "@visualdl/server" --include-dependencies build
elif [ "$SCOPE" = "cli" ]; then
......@@ -34,7 +34,7 @@ mkdir -p "$OUTPUT_PATH"
# package server files
if [ -d "$SERVER_DIR_PATH" ]; then
tar zcf "${OUTPUT_PATH}/server.tar.gz" --exclude="node_modules" --exclude="*.log" --exclude=".gitignore" --exclude=".DS_Store" --dereference -C "$SERVER_DIR_PATH" .
tar zcf "${OUTPUT_PATH}/server.tar.gz" --exclude="node_modules" --exclude=".gitignore" --exclude=".DS_Store" --dereference -C "$SERVER_DIR_PATH" .
fi
# package serverless files
......
因为 它太大了无法显示 source diff 。你可以改为 查看blob
......@@ -6,15 +6,19 @@ FRONTEND_DIR=${TOP_DIR}/frontend
BUILD_DIR=${TOP_DIR}/build
FRONTEND_DIST="$BUILD_DIR/package/dist"
mkdir -p "$BUILD_DIR"
build_frontend() {
rm -rf "$FRONTEND_DIST"
mkdir -p "$FRONTEND_DIST"
cd "$FRONTEND_DIR"
. ./scripts/install.sh
SCOPE="serverless" PUBLIC_PATH="/{{PUBLIC_PATH}}" API_URL="/{{PUBLIC_PATH}}/api" API_TOKEN_KEY="{{API_TOKEN_KEY}}" PATH="$PATH" ./scripts/build.sh
SCOPE="serverless" \
PUBLIC_PATH="/{{PUBLIC_PATH}}" \
API_URL="/{{PUBLIC_PATH}}/api" \
API_TOKEN_KEY="{{API_TOKEN_KEY}}" \
TELEMETRY_ID="{{TELEMETRY_ID}}" \
PATH="$PATH" \
./scripts/build.sh
# extract
tar zxf "$FRONTEND_DIR/output/serverless.tar.gz" -C "$FRONTEND_DIST"
......@@ -22,8 +26,8 @@ build_frontend() {
clean_env() {
rm -rf "$TOP_DIR/visualdl/server/dist"
rm -rf "$BUILD_DIR/bdist*"
rm -rf "$BUILD_DIR/lib*"
rm -rf "$BUILD_DIR"
rm -rf "$TOP_DIR/*.egg-info"
}
package() {
......@@ -32,6 +36,8 @@ package() {
clean_env
mkdir -p "$BUILD_DIR"
if [ -z "$USE_CACHED_FRONTEND" ] || [ ! -d "$FRONTEND_DIST" ]; then
build_frontend
fi
......
record_pb2.py
\ No newline at end of file
......@@ -63,7 +63,8 @@ def create_app(args):
babel = Babel(app)
api_call = create_api_call(args.logdir, args.model, args.cache_timeout)
update_util.PbUpdater(args.product).start()
if args.telemetry:
update_util.PbUpdater(args.product).start()
public_path = args.public_path
api_path = public_path + '/api'
......@@ -80,7 +81,8 @@ def create_app(args):
template = Template(
os.path.join(server_path, template_file_path),
PUBLIC_PATH=public_path.lstrip('/'),
API_TOKEN_KEY=''
API_TOKEN_KEY='',
TELEMETRY_ID='63a600296f8a71f576c4806376a9245b' if args.telemetry else ''
)
@app.route('/')
......@@ -96,15 +98,15 @@ def create_app(args):
@app.route(public_path + '/')
def index():
lang = get_locale()
if lang == default_language:
return redirect(public_path + '/index', code=302)
lang = default_language if lang is None else lang
return redirect(public_path + '/' + lang + '/index', code=302)
return redirect(public_path + '/index', code=302)
@app.route(public_path + '/<path:filename>')
def serve_static(filename):
return template.render(filename if re.search(r'\..+$', filename) else filename + '.html')
is_not_page_request = re.search(r'\..+$', filename)
response = template.render(filename if is_not_page_request else 'index.html')
if not is_not_page_request:
response.set_cookie('vdl_lng', get_locale(), path='/', samesite='Strict', secure=False, httponly=False)
return response
@app.route(api_path + '/<path:method>')
def serve_api(method):
......@@ -118,17 +120,6 @@ def create_app(args):
return app
def _open_browser(app, index_url):
while True:
# noinspection PyBroadException
try:
requests.get(index_url)
break
except Exception:
time.sleep(0.5)
webbrowser.open(index_url)
def wait_until_live(args: ParseArgs):
url = 'http://{host}:{port}'.format(host=args.host, port=args.port)
while True:
......
......@@ -24,6 +24,7 @@ default_host = None
default_port = 8040
default_cache_timeout = 20
default_public_path = '/app'
default_product = 'normal'
class DefaultArgs(object):
......@@ -37,7 +38,8 @@ class DefaultArgs(object):
self.api_only = args.get('api_only', False)
self.open_browser = args.get('open_browser', False)
self.model = args.get('model', '')
self.product = args.get('product', 'normal')
self.product = args.get('product', default_product)
self.telemetry = args.get('telemetry', True)
def get_host(host=default_host, port=default_port):
......@@ -96,6 +98,7 @@ class ParseArgs(object):
self.open_browser = args.open_browser
self.model = args.model
self.product = args.product
self.telemetry = args.telemetry
def parse_args():
......@@ -178,8 +181,15 @@ def parse_args():
"--product",
type=str,
action="store",
default="normal",
default=default_product,
help="specify the product")
parser.add_argument(
"--disable-telemetry",
action="store_false",
dest="telemetry",
default=True,
help="disable telemetry"
)
args = parser.parse_args()
......
......@@ -52,7 +52,7 @@ class PbUpdater(threading.Thread):
with open('/visualdl/proto/record_pb2.py', mode='wb') as fp:
fp.write(pb_bin)
print('Update pb file successfully.')
except Exception as err:
except Exception:
pass
def run(self):
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册