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

frontend code polishment & build progress update (#585)

* v2.0.0-beta.12

* v2.0.0-beta.13

* fix: use new development server

* v2.0.0-beta.14

* build: build frontend from npm tarball

* fix: update python setup

* build: fix build on windows

* add loading & empty style
上级 b3ddbd77
......@@ -24,10 +24,36 @@ English | [简体中文](https://github.com/PaddlePaddle/VisualDL/blob/develop/f
**🚧PULL REQUESTS WELCOMED🚧**
## Development
## Usage
> nodejs ≥ 10 and npm ≥ 6 are required.
```bash
npm install -g visualdl
# or
yarn global add visualdl
```
Then you can start visualdl server by
```bash
visualdl start --backend="http://127.0.0.1:8040"
```
To stop visualdl server, just type
```bash
visualdl stop
```
For more usage infomation, please type
```bash
visualdl -h
```
## Development
First, install all dependencies:
```bash
......
......@@ -6,17 +6,23 @@ const argv = require('yargs')
.usage('Usage: $0 <command> [options]')
.command('start', 'Start VisualDL server')
.command('stop', 'Stop VisualDL server')
.example('$0 start --host 192.168.0.2 --port 3000', 'Start VisualDL server at http://192.168.0.2:3000')
.example(
'$0 start --backend="http://172.17.0.82:8040"',
'Start VisualDL server with backend address http://172.17.0.82:8040'
)
.alias('p', 'port')
.nargs('p', 1)
.nargs('port', 1)
.describe('p', 'Port of server')
.nargs('host', 1)
.describe('host', 'Host of server')
.nargs('proxy', 1)
.describe('proxy', 'Backend proxy address')
.alias('b', 'backend')
.nargs('b', 1)
.nargs('backend', 1)
.describe('b', 'Backend API address')
.boolean('open')
.describe('open', 'Open browser when server is ready')
.demandOption(['b'])
.help('h')
.alias('h', 'help')
.epilog('Visit https://github.com/PaddlePaddle/VisualDL for more infomation.').argv;
......@@ -86,7 +92,7 @@ pm2.connect(err => {
NODE_ENV: 'production',
HOST: host,
PORT: port,
PROXY: argv.proxy
BACKEND: argv.backend
}
},
err => {
......
import React, {FunctionComponent, useState} from 'react';
import React, {FunctionComponent, useState, useMemo} from 'react';
import styled from 'styled-components';
import {WithStyled, rem, primaryColor} from '~/utils/style';
import BarLoader from 'react-spinners/BarLoader';
import {WithStyled, rem, primaryColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import Chart from '~/components/Chart';
import Pagination from '~/components/Pagination';
......@@ -27,6 +28,15 @@ const Loading = styled.div`
padding: ${rem(40)} 0;
`;
const Empty = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-size: ${rem(20)};
height: ${rem(150)};
flex-grow: 1;
`;
// TODO: add types
// eslint-disable-next-line
type ChartPageProps<T = any> = {
......@@ -36,12 +46,14 @@ type ChartPageProps<T = any> = {
};
const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, loading, withChart, className}) => {
const {t} = useTranslation('common');
const pageSize = 12;
const total = Math.ceil((items?.length ?? 0) / pageSize);
const [page, setPage] = useState(1);
const pageItems = items?.slice((page - 1) * pageSize, page * pageSize) ?? [];
const pageItems = useMemo(() => items?.slice((page - 1) * pageSize, page * pageSize) ?? [], [items, page]);
return (
<div className={className}>
......@@ -51,9 +63,11 @@ const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, loadi
</Loading>
) : (
<Wrapper>
{pageItems.map((item, index) => (
<Chart key={index}>{withChart?.(item)}</Chart>
))}
{pageItems.length ? (
pageItems.map((item, index) => <Chart key={index}>{withChart?.(item)}</Chart>)
) : (
<Empty>{t('empty')}</Empty>
)}
</Wrapper>
)}
<Pagination page={page} total={total} onChange={setPage} />
......
import React, {FunctionComponent, useState, useEffect} from 'react';
import React, {FunctionComponent, useState, useEffect, useCallback} from 'react';
import styled from 'styled-components';
import {
WithStyled,
......@@ -96,13 +96,16 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
}) => {
const [checked, setChecked] = useState(!!value);
useEffect(() => setChecked(!!value), [setChecked, value]);
const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
return;
}
setChecked(e.target.checked);
onChange?.(e.target.checked);
};
const onChangeInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
return;
}
setChecked(e.target.checked);
onChange?.(e.target.checked);
},
[disabled, onChange]
);
return (
<Wrapper disabled={disabled} className={className} title={title}>
......
import React, {FunctionComponent, useMemo} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import queryString from 'query-string';
import {rem, primaryColor} from '~/utils/style';
import useRequest from '~/hooks/useRequest';
import useHeavyWork from '~/hooks/useHeavyWork';
import {divide, Dimension, Reduction, DivideParams, Point} from '~/resource/high-dimensional';
import ScatterChart from '~/components/ScatterChart';
......@@ -52,12 +52,13 @@ const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({
reduction,
dimension
}) => {
const {data, error} = useSWR<Data>(
const {data, error} = useRequest<Data>(
`/embeddings/embedding?${queryString.stringify({
run: run ?? '',
dimension: Number.parseInt(dimension),
reduction
})}`,
undefined,
{
refreshInterval: running ? 15 * 1000 : 0
}
......
import React, {FunctionComponent, useLayoutEffect, useState} from 'react';
import useSWR from 'swr';
import useRequest from '~/hooks/useRequest';
import {primaryColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import {blobFetcher} from '~/utils/fetch';
import GridLoader from 'react-spinners/GridLoader';
......@@ -9,9 +10,11 @@ type ImageProps = {
};
const Image: FunctionComponent<ImageProps> = ({src}) => {
const {t} = useTranslation('common');
const [url, setUrl] = useState('');
const {data} = useSWR(src ?? null, blobFetcher);
const {data, error, loading} = useRequest<Blob>(src ?? null, blobFetcher);
// use useLayoutEffect hook to prevent image render after url revoked
useLayoutEffect(() => {
......@@ -25,7 +28,15 @@ const Image: FunctionComponent<ImageProps> = ({src}) => {
}
}, [data]);
return !data ? <GridLoader color={primaryColor} size="10px" /> : <img src={url} />;
if (loading) {
return <GridLoader color={primaryColor} size="10px" />;
}
if (error) {
return <div>{t('error')}</div>;
}
return <img src={url} />;
};
export default Image;
import React, {FunctionComponent, useEffect, useCallback} from 'react';
import styled from 'styled-components';
import {EChartOption} from 'echarts';
import {WithStyled} from '~/utils/style';
import GridLoader from 'react-spinners/GridLoader';
import {WithStyled, primaryColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import useECharts from '~/hooks/useECharts';
import {formatTime} from '~/utils';
import * as chart from '~/utils/chart';
const Wrapper = styled.div`
position: relative;
> .echarts {
height: 100%;
}
> .loading {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
`;
type LineChartProps = {
title?: string;
legend?: string[];
......@@ -89,7 +110,16 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
}
}, [data, title, legend, xAxis, type, xAxisFormatter, yRange, tooltip, echart]);
return <div className={className} ref={ref}></div>;
return (
<Wrapper className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={ref}></div>
</Wrapper>
);
};
export default LineChart;
import React, {FunctionComponent} from 'react';
import React, {FunctionComponent, useMemo, useCallback} from 'react';
import styled from 'styled-components';
import {
WithStyled,
......@@ -75,32 +75,48 @@ const Pagination: FunctionComponent<PaginationProps & WithStyled> = ({page, tota
const padding = 2;
const around = 2;
const startEllipsis = page - padding - around - 1 > 0;
const endEllipsis = page + padding + around < total;
const start =
page - around - 1 <= 0 ? [] : Array.from(new Array(Math.min(padding, page - around - 1)), (_v, i) => i + 1);
const end =
page + around >= total
? []
: Array.from(
new Array(Math.min(padding, total - page - around)),
(_v, i) => total - padding + i + 1 + Math.max(padding - total + page + around, 0)
);
const before =
page - 1 <= 0
? []
: Array.from(
new Array(Math.min(around, page - 1)),
(_v, i) => page - around + i + Math.max(around - page + 1, 0)
);
const after = page >= total ? [] : Array.from(new Array(Math.min(around, total - page)), (_v, i) => page + i + 1);
const startEllipsis = useMemo(() => page - padding - around - 1 > 0, [page]);
const endEllipsis = useMemo(() => page + padding + around < total, [page, total]);
const start = useMemo(
() =>
page - around - 1 <= 0 ? [] : Array.from(new Array(Math.min(padding, page - around - 1)), (_v, i) => i + 1),
[page]
);
const end = useMemo(
() =>
page + around >= total
? []
: Array.from(
new Array(Math.min(padding, total - page - around)),
(_v, i) => total - padding + i + 1 + Math.max(padding - total + page + around, 0)
),
[page, total]
);
const before = useMemo(
() =>
page - 1 <= 0
? []
: Array.from(
new Array(Math.min(around, page - 1)),
(_v, i) => page - around + i + Math.max(around - page + 1, 0)
),
[page]
);
const after = useMemo(
() => (page >= total ? [] : Array.from(new Array(Math.min(around, total - page)), (_v, i) => page + i + 1)),
[page, total]
);
const genLink = useCallback(
(arr: number[]) =>
arr.map(i => (
<Li key={i}>
<A onClick={() => onChange?.(i)}>{i}</A>
</Li>
)),
[onChange]
);
const genLink = (arr: number[]) =>
arr.map(i => (
<Li key={i}>
<A onClick={() => onChange?.(i)}>{i}</A>
</Li>
));
const hellip = (
<Li>
<Span>&hellip;</Span>
......
import React, {FunctionComponent, useContext} from 'react';
import React, {FunctionComponent, useContext, useCallback} from 'react';
import styled from 'styled-components';
import {
WithStyled,
......@@ -70,11 +70,11 @@ const RadioButton: FunctionComponent<RadioButtonProps & WithStyled> = ({
const groupValue = useContext(ValueContext);
const onChange = useContext(EventContext);
const onClick = () => {
const onClick = useCallback(() => {
if (value && onChange && groupValue !== value) {
onChange(value);
}
};
}, [value, onChange, groupValue]);
return (
<Button className={className} title={title} selected={groupValue === value || selected} onClick={onClick}>
......
import React, {FunctionComponent, useState} from 'react';
import React, {FunctionComponent, useState, useMemo} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import queryString from 'query-string';
import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style';
import isEmpty from 'lodash/isEmpty';
import GridLoader from 'react-spinners/GridLoader';
import {em, size, ellipsis, primaryColor, textLightColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import useRequest from '~/hooks/useRequest';
import Image from '~/components/Image';
import StepSlider from '~/components/SamplesPage/StepSlider';
......@@ -81,12 +83,31 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number):
`/images/image?${queryString.stringify({index, ts: wallTime, run, tag})}`;
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => {
const {data, error} = useSWR<ImageData[]>(`/images/list?${queryString.stringify({run, tag})}`, {
refreshInterval: running ? 15 * 1000 : 0
});
const {t} = useTranslation('common');
const {data, error, loading} = useRequest<ImageData[]>(
`/images/list?${queryString.stringify({run, tag})}`,
undefined,
{
refreshInterval: running ? 15 * 1000 : 0
}
);
const [step, setStep] = useState(0);
const Content = useMemo(() => {
if (loading) {
return <GridLoader color={primaryColor} size="10px" />;
}
if (error) {
return <span>{t('error')}</span>;
}
if (isEmpty(data)) {
return <span>{t('empty')}</span>;
}
return <Image src={getImageUrl(step, run, tag, (data as ImageData[])[step].wallTime)} />;
}, [loading, error, data, step, run, tag, t]);
return (
<Wrapper>
<Title>
......@@ -94,10 +115,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
<span>{run}</span>
</Title>
<StepSlider value={step} steps={data?.map(item => item.step) ?? []} onChange={setStep} />
<Container fit={fit}>
{!data && !error && <GridLoader color={primaryColor} size="10px" />}
{data && !error && <Image src={getImageUrl(step, run, tag, data[step].wallTime)} />}
</Container>
<Container fit={fit}>{Content}</Container>
</Wrapper>
);
};
......
import React, {FunctionComponent, useCallback, useMemo} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import queryString from 'query-string';
import {EChartOption} from 'echarts';
import {em, size} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import useRequest from '~/hooks/useRequest';
import useHeavyWork from '~/hooks/useHeavyWork';
import {cycleFetcher} from '~/utils/fetch';
import {
......@@ -66,7 +66,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
const {t, i18n} = useTranslation(['scalars', 'common']);
// TODO: maybe we can create a custom hook here
const {data: datasets, error} = useSWR<Dataset[]>(
const {data: datasets, error, loading} = useRequest<Dataset[]>(
runs.map(run => `/scalars/list?${queryString.stringify({run, tag})}`),
(...urls) => cycleFetcher(urls),
{
......@@ -142,6 +142,10 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
[smoothedDatasets, runs, sortingMethod, i18n]
);
if (error) {
return <span>{t('common:error')}</span>;
}
return (
<StyledLineChart
title={tag}
......@@ -151,7 +155,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
type={type}
tooltip={formatter}
data={data}
loading={!datasets && !error}
loading={loading}
/>
);
};
......
import React, {FunctionComponent, useEffect, useMemo} from 'react';
import {WithStyled} from '~/utils/style';
import styled from 'styled-components';
import GridLoader from 'react-spinners/GridLoader';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts from '~/hooks/useECharts';
const Wrapper = styled.div`
position: relative;
> .echarts {
height: 100%;
}
> .loading {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
`;
const SYMBOL_SIZE = 12;
const options2D = {
......@@ -69,7 +90,16 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({data,
}
}, [chartOptions, echart]);
return <div className={className} ref={ref}></div>;
return (
<Wrapper className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={ref}></div>
</Wrapper>
);
};
export default ScatterChart;
import React, {FunctionComponent, useState, useCallback, useEffect} from 'react';
import React, {FunctionComponent, useState, useCallback, useEffect, useMemo} from 'react';
import styled from 'styled-components';
import without from 'lodash/without';
import {useTranslation} from '~/utils/i18n';
......@@ -156,41 +156,56 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
setValue
]);
const isSelected = !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType));
const changeValue = (mutateValue: SelectValueType, checked?: boolean) => {
let newValue;
if (multiple) {
newValue = value as SelectValueType[];
if (checked) {
if (!newValue.includes(mutateValue)) {
newValue = [...newValue, mutateValue];
const isSelected = useMemo(
() => !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType)),
[multiple, value]
);
const changeValue = useCallback(
(mutateValue: SelectValueType, checked?: boolean) => {
let newValue;
if (multiple) {
newValue = value as SelectValueType[];
if (checked) {
if (!newValue.includes(mutateValue)) {
newValue = [...newValue, mutateValue];
}
} else {
if (newValue.includes(mutateValue)) {
newValue = without(newValue, mutateValue);
}
}
} else {
if (newValue.includes(mutateValue)) {
newValue = without(newValue, mutateValue);
}
newValue = mutateValue;
}
} else {
newValue = mutateValue;
}
setValue(newValue);
onChange?.(newValue);
if (!multiple) {
setIsOpenedFalse();
}
};
setValue(newValue);
onChange?.(newValue);
if (!multiple) {
setIsOpenedFalse();
}
},
[multiple, value, setIsOpenedFalse, onChange]
);
const ref = useClickOutside(setIsOpenedFalse);
const list = propList?.map(item => ('string' === typeof item ? {value: item, label: item} : item)) ?? [];
const isListEmpty = list.length === 0;
const list = useMemo(
() => propList?.map(item => ('string' === typeof item ? {value: item, label: item} : item)) ?? [],
[propList]
);
const isListEmpty = useMemo(() => list.length === 0, [list]);
const findLabelByValue = (v: SelectValueType) => list.find(item => item.value === v)?.label ?? '';
const label = isSelected
? multiple
? (value as SelectValueType[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as SelectValueType)
: placeholder || t('select');
const findLabelByValue = useCallback((v: SelectValueType) => list.find(item => item.value === v)?.label ?? '', [
list
]);
const label = useMemo(
() =>
isSelected
? multiple
? (value as SelectValueType[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as SelectValueType)
: placeholder || t('select'),
[multiple, value, findLabelByValue, isSelected, placeholder, t]
);
return (
<Wrapper ref={ref} opened={isOpened} className={className}>
......
import React, {FunctionComponent, useState, useCallback, useEffect} from 'react';
import React, {FunctionComponent, useState, useCallback, useEffect, useMemo} from 'react';
import styled from 'styled-components';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
......@@ -43,12 +43,18 @@ const TagFilter: FunctionComponent<TagFilterProps> = ({value, tags: propTags, on
const {t} = useTranslation('common');
const tagGroups = sortBy(
Object.entries(groupBy<TagType>(propTags || [], tag => tag.label.split('/')[0])).map(([label, tags]) => ({
label,
tags
})),
tag => tag.label
const tagGroups = useMemo(
() =>
sortBy(
Object.entries(groupBy<TagType>(propTags || [], tag => tag.label.split('/')[0])).map(
([label, tags]) => ({
label,
tags
})
),
tag => tag.label
),
[propTags]
);
const [matchedCount, setMatchedCount] = useState(propTags?.length ?? 0);
......@@ -58,8 +64,8 @@ const TagFilter: FunctionComponent<TagFilterProps> = ({value, tags: propTags, on
useEffect(() => setInputValue(value || ''), [value, setInputValue]);
const [selectedValue, setSelectedValue] = useState('');
const hasSelectedValue = selectedValue !== '';
const allText = inputValue || t('all');
const hasSelectedValue = useMemo(() => selectedValue !== '', [selectedValue]);
const allText = useMemo(() => inputValue || t('all'), [inputValue, t]);
const onInputChange = useCallback(
(value: string) => {
......
import {useRef, useEffect, useCallback, useState, MutableRefObject} from 'react';
import echarts, {ECharts} from 'echarts';
import {ECharts} from 'echarts';
import {primaryColor, textColor, maskColor} from '~/utils/style';
const useECharts = <T extends HTMLElement>(options: {
......@@ -16,6 +16,7 @@ const useECharts = <T extends HTMLElement>(options: {
const createChart = useCallback(() => {
(async () => {
const echarts = await import('echarts');
if (options.gl) {
await import('echarts-gl');
}
......
import {useMemo} from 'react';
import useSWR, {responseInterface, keyInterface, ConfigInterface} from 'swr';
import {fetcherFn} from 'swr/dist/types';
type Response<D, E> = responseInterface<D, E> & {
loading: boolean;
};
function useRequest<D = unknown, E = unknown>(key: keyInterface): Response<D, E>;
function useRequest<D = unknown, E = unknown>(key: keyInterface, fetcher?: fetcherFn<D>): Response<D, E>;
function useRequest<D = unknown, E = unknown>(
key: keyInterface,
fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E>
): Response<D, E>;
function useRequest<D = unknown, E = unknown>(
key: keyInterface,
fetcher?: fetcherFn<D>,
config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E> {
const {data, error, ...other} = useSWR<D, E>(key, fetcher, config);
const loading = useMemo(() => !data && !error, [data, error]);
return {data, error, loading, ...other};
}
export default useRequest;
import {useReducer, useEffect, useCallback, useMemo} from 'react';
import {useRouter} from 'next/router';
import useSWR from 'swr';
import groupBy from 'lodash/groupBy';
import uniq from 'lodash/uniq';
import intersection from 'lodash/intersection';
import useRequest from '~/hooks/useRequest';
import {Tag} from '~/types';
type Runs = string[];
......@@ -102,8 +102,8 @@ const reducer = (state: State, action: Action): State => {
const useTagFilters = (type: string) => {
const router = useRouter();
const {data: runs} = useSWR<Runs>('/runs');
const {data: tags} = useSWR<Tags>(`/${type}/tags`);
const {data: runs, loading: loadingRuns} = useRequest<Runs>('/runs');
const {data: tags, loading: loadingTags} = useRequest<Tags>(`/${type}/tags`);
const selectedRuns = useMemo(
() =>
......@@ -144,8 +144,8 @@ const useTagFilters = (type: string) => {
selectedTags: state.filteredTags,
onChangeRuns,
onFilterTags,
loadingRuns: !runs,
loadingTags: !tags
loadingRuns,
loadingTags
};
};
......
......@@ -2,7 +2,7 @@ import fetch from 'isomorphic-unfetch';
import {Request, Response} from 'express';
const images = [
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582885214651&di=77163884e9a374391b4302d8bdd4fa1d&imgtype=0&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2F50124b1944e8887b95154598f8b5212886ddf03641f19-GbcSBV_fw658',
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1583866603102&di=13e63561829699f6eda8405ba5a84cad&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201704%2F30%2F20170430172141_YSjU4.jpeg',
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582889403799&di=bb4db115c1227e081852bbb95336150b&imgtype=0&src=http%3A%2F%2Fres.hpoi.net.cn%2Fgk%2Fcover%2Fn%2F2015%2F02%2Fff897b88ccd5417f91d4159a8ea343a6.jpg%3Fdate%3D1464606291000',
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582889975692&di=cd91e6c70d07ef496bfcca20597eb5af&imgtype=0&src=http%3A%2F%2Fimg3.duitang.com%2Fuploads%2Fitem%2F201411%2F28%2F20141128211355_HPfYT.thumb.224_0.gif',
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582890006236&di=9f030009422b91e8753f8c476426fc39&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201812%2F22%2F20181222172346_ykcdh.thumb.224_0.gif',
......
{
"name": "visualdl",
"version": "2.0.0-beta.14",
"version": "2.0.0-beta.15",
"title": "VisualDL",
"description": "A platform to visualize the deep learning process and result.",
"keywords": [
......
import React, {useState, useEffect, useMemo} from 'react';
import useSWR from 'swr';
import styled from 'styled-components';
import {saveSvgAsPng} from 'save-svg-as-png';
import isEmpty from 'lodash/isEmpty';
import useRequest from '~/hooks/useRequest';
import {rem} from '~/utils/style';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import RawButton from '~/components/Button';
import RawRangeSlider from '~/components/RangeSlider';
import Content from '~/components/Content';
import Title from '~/components/Title';
import Field from '~/components/Field';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import NodeInfo, {NodeInfoProps} from '~/components/GraphsPage/NodeInfo';
import Preloader from '~/components/Preloader';
import {Graph, collectDagFacts} from '~/resource/graphs';
......@@ -28,6 +29,14 @@ const Button = styled(RawButton)`
}
`;
const Empty = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-size: ${rem(20)};
height: ${rem(150)};
`;
const RangeSlider = styled(RawRangeSlider)`
width: 100%;
`;
......@@ -129,7 +138,7 @@ const useDag = (graph?: Graph) => {
};
};
const useDagreD3 = (graph: Graph | undefined) => {
const useDagreD3 = (graph?: Graph) => {
const [currentNode, setCurrentNode] = useState<NodeInfoProps['node']>(undefined);
const {dagInfo, displaySwitch, setDisplaySwitch} = useDag(graph);
const [downloadImage, setDownloadImageFn] = useState<() => void>(() => dumbFn);
......@@ -241,8 +250,9 @@ const useDagreD3 = (graph: Graph | undefined) => {
const Graphs: NextI18NextPage = () => {
const {t} = useTranslation(['graphs', 'common']);
const {data: graph} = useSWR<{data: Graph}>('/graphs/graph');
const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph ? graph.data : undefined);
const {data, error, loading} = useRequest<{data: Graph}>('/graphs/graph');
const graph = useMemo(() => (loading || isEmpty(data?.data) ? undefined : data?.data), [loading, data]);
const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph);
const aside = (
<section>
......@@ -268,15 +278,30 @@ const Graphs: NextI18NextPage = () => {
</section>
);
const ContentInner = useMemo(() => {
if (loading) {
return null;
}
if (error) {
return <Empty>{t('common:error')}</Empty>;
}
if (!graph) {
return <Empty>{t('common:empty')}</Empty>;
}
return (
<GraphSvg>
<g></g>
</GraphSvg>
);
}, [loading, error, graph, t]);
return (
<>
<Preloader url="/graphs/graph" />
<Title>{t('common:graphs')}</Title>
<Content aside={aside} loading={!graph}>
<GraphSvg>
<g></g>
</GraphSvg>
<Content aside={aside} loading={loading}>
{ContentInner}
</Content>
</>
);
......
import React, {useState, useEffect} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import {useRouter} from 'next/router';
import useRequest from '~/hooks/useRequest';
import useSearchValue from '~/hooks/useSearchValue';
import {rem, em} from '~/utils/style';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
......@@ -40,7 +40,7 @@ const HighDimensional: NextI18NextPage = () => {
const {query} = useRouter();
const queryRun = Array.isArray(query.run) ? query.run[0] : query.run;
const {data: runs} = useSWR<string[]>('/runs');
const {data: runs, error, loading} = useRequest<string[]>('/runs');
const selectedRun = runs?.includes(queryRun) ? queryRun : runs?.[0];
const [run, setRun] = useState(selectedRun);
......@@ -106,15 +106,19 @@ const HighDimensional: NextI18NextPage = () => {
<>
<Preloader url="/runs" />
<Title>{t('common:high-dimensional')}</Title>
<Content aside={aside} loading={!runs}>
<HighDimensionalChart
dimension={dimension}
keyword={debouncedSearch}
run={run ?? ''}
running={running}
labelVisibility={labelVisibility}
reduction={reduction}
/>
<Content aside={aside} loading={loading}>
{error ? (
<div>{t('common:error')}</div>
) : loading ? null : (
<HighDimensionalChart
dimension={dimension}
keyword={debouncedSearch}
run={run ?? ''}
running={running}
labelVisibility={labelVisibility}
reduction={reduction}
/>
)}
</Content>
</>
);
......
......@@ -12,5 +12,6 @@
"select-runs": "Select Runs",
"running": "Running",
"stopped": "Stopped",
"loading": "Loading"
"loading": "Loading",
"error": "Error occurred"
}
......@@ -12,5 +12,6 @@
"select-runs": "选择数据流",
"running": "运行中",
"stopped": "已停止",
"loading": "载入中"
"loading": "载入中",
"error": "发生错误"
}
......@@ -59,13 +59,15 @@ export const chartData = ({data, runs, smooth, xAxis}: ChartDataParams) =>
// [3] smoothed value
// [4] relative
const name = runs[i];
const color = chart.color[i % chart.color.length];
const colorAlt = chart.colorAlt[i % chart.colorAlt.length];
return [
{
name,
z: i,
lineStyle: {
width: chart.series.lineStyle.width,
opacity: 0.5
color: colorAlt,
width: chart.series.lineStyle.width
},
data: dataset,
encode: {
......@@ -77,6 +79,9 @@ export const chartData = ({data, runs, smooth, xAxis}: ChartDataParams) =>
{
name,
z: runs.length + i,
itemStyle: {
color
},
data: dataset,
encode: {
x: [xAxisMap[xAxis]],
......
......@@ -16,7 +16,7 @@ setConfig(config);
const host = process.env.HOST || 'localhost';
const port = Number.parseInt(process.env.PORT, 10) || 8999;
const proxy = process.env.PROXY;
const backend = process.env.BACKEND;
const delay = Number.parseInt(process.env.DELAY, 10);
const server = express();
......@@ -26,9 +26,9 @@ const handle = app.getRequestHandler();
async function start() {
await app.prepare();
if (proxy) {
if (backend) {
const {createProxyMiddleware} = await import('http-proxy-middleware');
server.use(config.env.API_URL, createProxyMiddleware({target: proxy, changeOrigin: true}));
server.use(config.env.API_URL, createProxyMiddleware({target: backend, changeOrigin: true}));
} else if (isDev) {
const {default: mock} = await import('../utils/mock');
server.use(
......
export const color = ['#2932E1', '#25C9FF', '#981EFF', '#D8DAF6', '#E9F9FF', '#F3E8FF'];
export const color = [
'#2932E1',
'#00CC88',
'#981EFF',
'#066BFF',
'#3AEB0D',
'#E71ED5',
'#25C9FF',
'#0DEBB0',
'#FF0287',
'#00E2FF',
'#00FF9D',
'#D50505',
'#FFAA00',
'#A3EB0D',
'#FF0900',
'#FF6600',
'#FFEA00',
'FE4A3B'
];
export const colorAlt = [
'#9498F0',
'#66E0B8',
'#CB8EFF',
'#6AA6FF',
'#89F36E',
'#F178E6',
'#7CDFFF',
'#6EF3D0',
'#FF80C3',
'#7FF0FF',
'#66FFC4',
'#E66969',
'#FFD47F',
'#A3EB0D',
'#E9F9FF',
'#FFB27F',
'#FFF266',
'#FE9289'
];
export const title = {
textStyle: {
......
......@@ -16,19 +16,24 @@ function check_duplicated($filename_format) {
}
}
function build_frontend() {
function build_frontend_from_source() {
cd $FRONTEND_DIR
npm install
npm run build
foreach ($file_name in "manifest.*.js","index.*.js","vendor.*.js") {
echo $file_name
check_duplicated $file_name
}
$env:PUBLIC_PATH="/app"
$env:API_URL="/api"
# TODO:
# ./scripts/build.sh
}
function build_frontend() {
$PACKAGE_NAME="visualdl"
$SRC=npm view $PACKAGE_NAME dist.tarball
Invoke-WebRequest -Uri "$SRC" -OutFile "$BUILD_DIR/$PACKAGE_NAME.tar.gz"
# Need Windows 10 Insider Build 17063 and later
tar -zxf "$BUILD_DIR/$PACKAGE_NAME.tar.gz" -C "$BUILD_DIR"
}
function build_frontend_fake() {
cd $FRONTEND_DIR
mkdir -p dist
mkdir -p "$BUILD_DIR/package/serverless"
}
function build_backend() {
......@@ -56,16 +61,19 @@ function clean_env() {
rm -Recurse -Force -ErrorAction Ignore $BUILD_DIR/lib*
rm -Recurse -Force -ErrorAction Ignore $BUILD_DIR/temp*
rm -Recurse -Force -ErrorAction Ignore $BUILD_DIR/scripts*
rm -Recurse -Force -ErrorAction Ignore $BUILD_DIR/*.tar.gz
rm -Recurse -Force -ErrorAction Ignore $BUILD_DIR/package
}
function package() {
cp -Recurse $FRONTEND_DIR/dist $TOP_DIR/visualdl/server/
mkdir -p $TOP_DIR/visualdl/server/dist
cp -Recurse $BUILD_DIR/package/serverless/* $TOP_DIR/visualdl/server/dist
cp $BUILD_DIR/visualdl/logic/Release/core.pyd $TOP_DIR/visualdl
cp $BUILD_DIR/visualdl/logic/Release/core.pyd $TOP_DIR/visualdl/python/
}
build_frontend
clean_env
build_frontend
build_backend
build_onnx_graph
package
......@@ -15,8 +15,9 @@ build_frontend_from_source() {
build_frontend() {
local PACKAGE_NAME="visualdl"
local SRC=`npm view ${PACKAGE_NAME} dist.tarball`
wget $SRC -O "$BUILD_DIR/$PACKAGE_NAME.tar.gz"
local SRC=`npm view ${PACKAGE_NAME}@latest dist.tarball`
# wget $SRC -O "$BUILD_DIR/$PACKAGE_NAME.tar.gz"
curl -o "$BUILD_DIR/$PACKAGE_NAME.tar.gz" $SRC
tar zxf "$BUILD_DIR/$PACKAGE_NAME.tar.gz" -C "$BUILD_DIR"
}
......@@ -48,6 +49,8 @@ clean_env() {
rm -rf $BUILD_DIR/lib*
rm -rf $BUILD_DIR/temp*
rm -rf $BUILD_DIR/scripts*
rm -rf $BUILD_DIR/*.tar.gz
rm -rf $BUILD_DIR/package
}
package() {
......@@ -60,13 +63,14 @@ package() {
ARG=$1
echo "ARG: " $ARG
clean_env
if [ "$ARG" = "travis-CI" ]; then
build_frontend_fake
else
build_frontend
fi
clean_env
build_backend
build_onnx_graph
package
......@@ -45,7 +45,7 @@ export PYTHONPATH=$PYTHONPATH:"$SCRIPT_DIR/.."
FRONTEND_PORT=8999
VDL_BIN="./build/node_modules/.bin/visualdl"
$VDL_BIN start --port=$FRONTEND_PORT --host=$HOST --proxy="http://$HOST:$PORT"
$VDL_BIN start --port=$FRONTEND_PORT --host=$HOST --backend="http://$HOST:$PORT"
function finish {
$VDL_BIN stop
......
#!/bin/bash
set -ex
mode=$1
readonly TOP_DIR=$(pwd)
readonly core_path=$TOP_DIR/build/visualdl/logic
readonly python_path=$TOP_DIR/visualdl/python
readonly max_file_size=1000000 # 1MB
# version number follow the rule of https://semver.org/
readonly version_number=`cat VERSION_NUMBER | sed 's/\([0-9]*.[0-9]*.[0-9]*\).*/\1/g'`
sudo="sudo"
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then sudo=""; fi
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
curl -O http://python-distribute.org/distribute_setup.py
python distribute_setup.py
curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py
python get-pip.py
fi
$sudo pip install numpy
$sudo pip install Flask
$sudo pip install Pillow
$sudo pip install protobuf
export PYTHONPATH="${core_path}:${python_path}"
# install the visualdl wheel first
package() {
# some bug with frontend build
# a environment variable to skip frontend build
export VS_BUILD_MODE="travis-CI"
cd $TOP_DIR/visualdl/server
# manully install protobuf3
curl -OL https://github.com/google/protobuf/releases/download/v3.5.0/protoc-3.5.0-linux-x86_64.zip
unzip protoc-3.5.0-linux-x86_64.zip -d protoc3
export PATH="$PATH:$(pwd)/protoc3/bin"
chmod +x protoc3/bin/*
cd $TOP_DIR
python setup.py bdist_wheel
$sudo pip install dist/visualdl-${version_number}*.whl
}
backend_test() {
cd $TOP_DIR
mkdir -p build
cd build
cmake ..
make
make test
}
frontend_test() {
cd $TOP_DIR
cd frontend
npm install
npm run build
}
server_test() {
$sudo pip install google
$sudo pip install protobuf==3.5.1
cd $TOP_DIR/visualdl/server
bash graph_test.sh
cd $TOP_DIR/
python -m visualdl.server.lib_test
}
# check the size of files in the repo.
# reject PR that has some big data included.
bigfile_reject() {
cd $TOP_DIR
# it failed to exclude .git, remove it first.
rm -rf .git
local largest_file=$(find . -path .git -prune -o -printf '%s %p\n' | sort -nr | grep -v "CycleGAN"| head -n1)
local size=$(echo "$largest_file" | awk '{print $1}')
if [ "$size" -ge "$max_file_size" ]; then
echo $largest_file
echo "file size exceed $max_file_size"
echo "Should not add large data or binary file."
exit -1
fi
}
echo "mode" $mode
if [ $mode = "backend" ]; then
backend_test
elif [ $mode = "all" ]; then
# bigfile_reject should be tested first, or some files downloaded may fail this test.
bigfile_reject
package
frontend_test
backend_test
server_test
elif [ $mode = "local" ]; then
#frontend_test
backend_test
server_test
else
frontend_test
fi
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册