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

dark mode (#814)

* feat: dark mode support

* chore: use redux to manage global state

* chore: update dependencies

* build: increase yarn network timeout to avoid build error in github action

* feat: dark mode support

* chore: add module preload
上级 7ecc58e7
......@@ -38,17 +38,17 @@
"version": "yarn format && git add -A"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1",
"eslint": "7.8.1",
"@typescript-eslint/eslint-plugin": "4.1.1",
"@typescript-eslint/parser": "4.1.1",
"eslint": "7.9.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-react": "7.20.6",
"eslint-plugin-react-hooks": "4.1.0",
"husky": "4.2.5",
"eslint-plugin-react-hooks": "4.1.2",
"husky": "4.3.0",
"lerna": "3.22.1",
"lint-staged": "10.3.0",
"prettier": "2.1.1",
"lint-staged": "10.4.0",
"prettier": "2.1.2",
"rimraf": "3.0.2",
"typescript": "4.0.2",
"yarn": "1.22.5"
......
......@@ -36,12 +36,12 @@
"dependencies": {
"@visualdl/server": "2.0.9",
"open": "7.2.1",
"ora": "5.0.0",
"ora": "5.1.0",
"pm2": "4.4.1",
"yargs": "15.4.1"
"yargs": "16.0.3"
},
"devDependencies": {
"@types/node": "14.6.4",
"@types/node": "14.10.3",
"@types/yargs": "15.0.5",
"cross-env": "7.0.2",
"ts-node": "9.0.0",
......
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable no-console */
const path = require('path');
const {promises: fs} = require('fs');
const {BosClient} = require('@baiducloud/sdk');
const mime = require('mime-types');
const endpoint = process.env.BOS_ENDPOINT || 'http://bj.bcebos.com';
const ak = process.env.BOS_AK;
const sk = process.env.BOS_SK;
const version = process.env.CDN_VERSION || 'latest';
const config = {
endpoint,
credentials: {
ak,
sk
}
};
const bucket = 'visualdl-static';
const client = new BosClient(config);
async function getFiles(dir) {
const result = [];
try {
const files = await fs.readdir(dir, {withFileTypes: true});
for (const file of files) {
if (file.isFile()) {
const name = path.join(dir, file.name);
result.push({
name,
mime: mime.lookup(name),
size: (await fs.stat(name)).size
});
} else if (file.isDirectory()) {
result.push(...(await getFiles(path.join(dir, file.name))));
}
}
} catch (e) {
console.error(e);
}
return result;
}
async function main(directory) {
if (!ak || !sk) {
console.error('No AK and SK specified!');
process.exit(1);
}
let files = [];
try {
const stats = await fs.stat(directory);
if (stats.isDirectory()) {
files = (await getFiles(directory)).map(file => ({filename: path.relative(directory, file.name), ...file}));
} else if (stats.isFile()) {
files.push({
filename: path.relative(path.basename(directory)),
name: directory,
mime: mime.lookup(directory),
size: stats.size
});
} else {
console.error(`${directory} does not exist!`);
process.exit(1);
}
} catch (e) {
console.error(e);
process.exit(1);
}
for (const file of files) {
(function (f) {
client
.putObjectFromFile(bucket, `assets/${version}/${f.filename}`, f.name, {
'content-length': f.size,
'content-type': `${f.mime}; charset=utf-8`
})
.then(() => console.log([f.name, f.mime, f.size].join(', ')))
.catch(error => console.error(f, error));
})(file);
}
}
module.exports = main;
......@@ -33,3 +33,5 @@ process.env.SNOWPACK_PUBLIC_API_TOKEN_KEY = process.env.API_TOKEN_KEY || '';
process.env.SNOWPACK_PUBLIC_LANGUAGES = process.env.LANGUAGES || 'en,zh';
// default language
process.env.SNOWPACK_PUBLIC_DEFAULT_LANGUAGE = process.env.DEFAULT_LANGUAGE || 'en';
// theme
process.env.SNOWPACK_PUBLIC_THEME = process.env.THEME || '';
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const {promises: fs} = require('fs');
const ENV_INJECT = 'const env = window.__snowpack_env__ || {}; export default env;';
const dest = path.resolve(__dirname, '../dist/__snowpack__');
const envFile = path.join(dest, 'env.js');
module.exports = async () => {
await fs.rename(envFile, path.join(dest, 'env.local.js'));
await fs.writeFile(envFile, ENV_INJECT, 'utf-8');
};
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const {promises: fs} = require('fs');
const {minify} = require('html-minifier');
const dist = path.resolve(__dirname, '../dist');
const input = path.join(dist, 'index.html');
const output = path.join(dist, 'index.tpl.html');
function envProviderTemplate(baseUri) {
return `
<script type="module">
import env from '${baseUri}/__snowpack__/env.local.js'; window.__snowpack_env__ = env;
</script>
`;
}
const ENV_PROVIDER = envProviderTemplate(process.env.SNOWPACK_PUBLIC_BASE_URI);
const ENV_TEMPLATE_PROVIDER = envProviderTemplate('%BASE_URI%');
function injectProvider(content, provider) {
const scriptPos = content.indexOf('<script ');
return content.slice(0, scriptPos) + provider + content.slice(scriptPos);
}
function prependPublicPath(content, publicPath) {
return content.replace(/\b(src|href)=(['"]?)([^'"\s>]*)/gi, (_matched, attr, quote, url) => {
if (/^\/(_dist_|__snowpack__|web_modules|favicon.ico)\b/.test(url)) {
url = publicPath + url;
}
return attr + '=' + quote + url;
});
}
async function writeMinified(file, content) {
await fs.writeFile(
file,
minify(content, {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
sortAttributes: true,
sortClassName: true
}),
'utf-8'
);
}
module.exports = async () => {
const index = await fs.readFile(input, 'utf-8');
const indexWithPublicPath = prependPublicPath(index, process.env.SNOWPACK_PUBLIC_PATH);
const injected = injectProvider(indexWithPublicPath, ENV_PROVIDER);
await writeMinified(input, injected);
const template = prependPublicPath(index, '%PUBLIC_URL%');
const injectedTemplate = injectProvider(template, ENV_TEMPLATE_PROVIDER);
await writeMinified(output, injectedTemplate);
};
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable no-console */
require('dotenv').config();
require('./environment');
const path = require('path');
const {promises: fs} = require('fs');
const {BosClient} = require('@baiducloud/sdk');
const mime = require('mime-types');
const {minify} = require('html-minifier');
const endpoint = process.env.BOS_ENDPOINT || 'http://bj.bcebos.com';
const ak = process.env.BOS_AK;
const sk = process.env.BOS_SK;
const version = process.env.CDN_VERSION || 'latest';
const config = {
endpoint,
credentials: {
ak,
sk
}
};
const bucket = 'visualdl-static';
const client = new BosClient(config);
async function getFiles(dir) {
const result = [];
try {
const files = await fs.readdir(dir, {withFileTypes: true});
for (const file of files) {
if (file.isFile()) {
const name = path.join(dir, file.name);
result.push({
name,
mime: mime.lookup(name),
size: (await fs.stat(name)).size
});
} else if (file.isDirectory()) {
result.push(...(await getFiles(path.join(dir, file.name))));
}
}
} catch (e) {
console.error(e);
}
return result;
}
async function pushCdn(directory) {
if (!ak || !sk) {
console.error('No AK and SK specified!');
process.exit(1);
}
let files = [];
try {
const stats = await fs.stat(directory);
if (stats.isDirectory()) {
files = (await getFiles(directory)).map(file => ({filename: path.relative(directory, file.name), ...file}));
} else if (stats.isFile()) {
files.push({
filename: path.relative(path.basename(directory)),
name: directory,
mime: mime.lookup(directory),
size: stats.size
});
} else {
console.error(`${directory} does not exist!`);
process.exit(1);
}
} catch (e) {
console.error(e);
process.exit(1);
}
for (const file of files) {
(function (f) {
client
.putObjectFromFile(bucket, `assets/${version}/${f.filename}`, f.name, {
'content-length': f.size,
'content-type': `${f.mime}; charset=utf-8`
})
.then(() => console.log([f.name, f.mime, f.size].join(', ')))
.catch(error => console.error(f, error));
})(file);
}
}
const pushCdn = require('./cdn');
const injectTemplate = require('./inject-template');
const injectEnv = require('./inject-env');
const dist = path.resolve(__dirname, '../dist');
const dest = path.join(dist, '__snowpack__');
const publicDir = path.resolve(__dirname, '../public');
function envProviderTemplate(baseUri) {
return `
<script type="module">
import env from '${baseUri}/__snowpack__/env.local.js'; globalThis.env = env;
</script>
`;
}
const ENV_INJECT = 'const env = globalThis.env || {}; export default env;';
const ENV_PROVIDER = envProviderTemplate(process.env.SNOWPACK_PUBLIC_BASE_URI);
const ENV_TEMPLATE_PROVIDER = envProviderTemplate('%BASE_URI%');
async function injectProvider(input, provider, output) {
const file = await fs.readFile(input, 'utf-8');
const scriptPos = file.indexOf('<script ');
const newFile = file.slice(0, scriptPos) + provider + file.slice(scriptPos);
await fs.writeFile(
output || input,
minify(newFile, {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
sortAttributes: true,
sortClassName: true
}),
'utf-8'
);
}
async function main() {
await injectProvider(path.join(dist, 'index.html'), ENV_PROVIDER);
await injectProvider(path.join(publicDir, 'index.html'), ENV_TEMPLATE_PROVIDER, path.join(dist, 'index.tpl.html'));
await injectTemplate();
const envFile = path.join(dest, 'env.js');
await fs.rename(envFile, path.join(dest, 'env.local.js'));
await fs.writeFile(envFile, ENV_INJECT, 'utf-8');
await injectEnv();
if (process.env.CDN_VERSION) {
// TODO: do not upload index.html & index.tpl.html & __snowpack__/env.local.js
......
......@@ -48,10 +48,10 @@
"i18next-fetch-backend": "3.0.0",
"lodash": "4.17.20",
"mime-types": "2.1.27",
"moment": "2.27.0",
"moment": "2.28.0",
"nprogress": "0.2.0",
"polished": "3.6.6",
"query-string": "6.13.1",
"query-string": "6.13.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-helmet": "6.1.0",
......@@ -59,9 +59,11 @@
"react-input-range": "1.3.0",
"react-is": "16.13.1",
"react-rangeslider": "2.2.0",
"react-redux": "7.2.1",
"react-router-dom": "5.2.0",
"react-spinners": "0.9.0",
"react-toastify": "6.0.8",
"redux": "4.0.5",
"styled-components": "5.2.0",
"swr": "0.3.0",
"tippy.js": "6.2.6"
......@@ -74,14 +76,15 @@
"@baiducloud/sdk": "1.0.0-rc.22",
"@snowpack/app-scripts-react": "1.10.0",
"@snowpack/plugin-dotenv": "2.0.1",
"@snowpack/plugin-run-script": "2.1.1",
"@snowpack/plugin-optimize": "0.2.1",
"@snowpack/plugin-run-script": "2.1.2",
"@svgr/core": "5.4.0",
"@testing-library/jest-dom": "5.11.4",
"@testing-library/react": "11.0.2",
"@testing-library/react": "11.0.4",
"@types/d3-format": "1.3.1",
"@types/echarts": "4.6.5",
"@types/file-saver": "2.0.1",
"@types/jest": "26.0.13",
"@types/jest": "26.0.14",
"@types/loadable__component": "5.13.0",
"@types/lodash": "4.14.161",
"@types/mime-types": "2.1.0",
......@@ -90,6 +93,7 @@
"@types/react-dom": "16.9.8",
"@types/react-helmet": "6.1.0",
"@types/react-rangeslider": "2.2.3",
"@types/react-redux": "7.1.9",
"@types/react-router-dom": "5.1.5",
"@types/snowpack-env": "2.3.0",
"@types/styled-components": "5.1.3",
......@@ -102,9 +106,9 @@
"html-minifier": "4.0.0",
"http-proxy-middleware": "1.0.5",
"jest": "26.4.2",
"snowpack": "2.10.1",
"snowpack": "2.11.1",
"typescript": "4.0.2",
"yargs": "15.4.1"
"yargs": "16.0.3"
},
"engines": {
"node": ">=12",
......
......@@ -32,6 +32,7 @@
"imports": "Imports",
"inputs": "Inputs",
"license": "License",
"location": "Location",
"name": "Name",
"outputs": "Outputs",
"producer": "Producer",
......
......@@ -32,6 +32,7 @@
"imports": "导入",
"inputs": "输入",
"license": "许可证",
"location": "位置",
"name": "名称",
"outputs": "输出",
"producer": "框架",
......
......@@ -16,6 +16,14 @@ module.exports = {
extends: '@snowpack/app-scripts-react',
plugins: [
'@snowpack/plugin-dotenv',
[
'@snowpack/plugin-optimize',
{
minifyHTML: false, // we will do it later in post-build
preloadModules: true,
target: ['chrome63', 'firefox67', 'safari11.1', 'edge79'] // browsers support es module
}
],
[
'@snowpack/plugin-run-script',
{
......@@ -43,7 +51,7 @@ module.exports = {
port
},
buildOptions: {
baseUrl: process.env.SNOWPACK_PUBLIC_PATH || '/',
baseUrl: '/', // set it in post-build
clean: true
},
installOptions: {
......
import React, {FunctionComponent, Suspense, useEffect, useMemo, useState} from 'react';
import React, {FunctionComponent, Suspense, useCallback, useEffect, useMemo, useState} from 'react';
import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'react-router-dom';
import {THEME, matchMedia} from '~/utils/theme';
import {headerHeight, position, size} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading';
......@@ -10,10 +11,12 @@ import NProgress from 'nprogress';
import Navbar from '~/components/Navbar';
import {SWRConfig} from 'swr';
import {ToastContainer} from 'react-toastify';
import {actions} from '~/store';
import {fetcher} from '~/utils/fetch';
import init from '@visualdl/wasm';
import routes from '~/routes';
import styled from 'styled-components';
import {useDispatch} from 'react-redux';
import {useTranslation} from 'react-i18next';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
......@@ -74,6 +77,22 @@ const App: FunctionComponent = () => {
})();
}, [inited]);
const dispatch = useDispatch();
const toggleTheme = useCallback(
(e: MediaQueryListEvent) => dispatch(actions.theme.setTheme(e.matches ? 'dark' : 'light')),
[dispatch]
);
useEffect(() => {
if (!THEME) {
matchMedia.addEventListener('change', toggleTheme);
return () => {
matchMedia.removeEventListener('change', toggleTheme);
};
}
}, [toggleTheme]);
return (
<div className="app">
<Helmet defaultTitle="VisualDL" titleTemplate="%s - VisualDL">
......
import React, {FunctionComponent} from 'react';
import {WithStyled, asideWidth, borderColor, rem, size} from '~/utils/style';
import {WithStyled, asideWidth, rem, size, transitionProps} from '~/utils/style';
import styled from 'styled-components';
......@@ -7,9 +7,10 @@ export const AsideSection = styled.section`
margin: ${rem(20)};
&:not(:last-child) {
border-bottom: 1px solid ${borderColor};
border-bottom: 1px solid var(--border-color);
padding-bottom: ${rem(20)};
margin-bottom: 0;
${transitionProps('border-color')}
}
`;
......
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {
WithStyled,
primaryActiveColor,
primaryBackgroundColor,
primaryColor,
primaryFocusedColor,
rem,
size,
textLightColor,
textLighterColor
} from '~/utils/style';
import {WithStyled, primaryColor, rem, size, transitionProps} from '~/utils/style';
import {AudioPlayer} from '~/utils/audio';
import type {BlobResponse} from '~/utils/fetch';
......@@ -28,35 +18,39 @@ import useRequest from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
const Container = styled.div`
background-color: ${primaryBackgroundColor};
background-color: var(--audio-background-color);
border-radius: ${rem(8)};
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 ${rem(20)};
${transitionProps('background-color')}
> .control {
font-size: ${rem(16)};
${size(rem(16), rem(16))}
line-height: 1;
margin: 0 ${rem(10)};
color: ${primaryColor};
color: var(--primary-color);
cursor: pointer;
${transitionProps('color')}
&.volumn {
font-size: ${rem(20)};
${size(rem(20), rem(20))}
}
&.disabled {
color: ${textLightColor};
color: var(--text-light-color);
cursor: not-allowed;
}
&:hover {
color: ${primaryFocusedColor};
color: var(--primary-focused-color);
}
&:active {
color: ${primaryActiveColor};
color: var(--primary-active-color);
}
}
......@@ -66,9 +60,10 @@ const Container = styled.div`
}
> .time {
color: ${textLighterColor};
color: var(--text-lighter-color);
font-size: ${rem(12)};
margin: 0 ${rem(5)};
${transitionProps('color')}
}
`;
......@@ -82,15 +77,16 @@ const VolumnSlider = styled(Slider)`
outline: none;
border-radius: ${rem(2)};
user-select: none;
${transitionProps('color')}
--color: ${primaryColor};
--color: var(--primary-color);
&:hover {
--color: ${primaryFocusedColor};
--color: var(--primary-focused-color);
}
&:active {
--color: ${primaryActiveColor};
--color: var(--primary-active-color);
}
.rangeslider__fill {
......@@ -102,6 +98,7 @@ const VolumnSlider = styled(Slider)`
border-bottom-right-radius: ${rem(2)};
border-top: ${rem(4)} solid var(--color);
box-sizing: content-box;
${transitionProps(['background-color', 'color'])}
}
.rangeslider__handle {
......@@ -111,6 +108,7 @@ const VolumnSlider = styled(Slider)`
left: -${rem(2)};
border-radius: 50%;
outline: none;
${transitionProps('background-color')}
.rangeslider__handle-tooltip,
.rangeslider__handle-label {
......
import React, {FunctionComponent} from 'react';
import {position, primaryColor, size} from '~/utils/style';
import {position, primaryColor, size, transitionProps} from '~/utils/style';
import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components';
......@@ -7,12 +7,13 @@ 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);
background-color: var(--mask-color);
display: flex;
justify-content: center;
align-items: center;
overscroll-behavior: none;
cursor: progress;
${transitionProps('background-color')}
`;
const BodyLoading: FunctionComponent = () => {
......
import React, {FunctionComponent} from 'react';
import {
WithStyled,
borderActiveColor,
borderColor,
borderFocusedColor,
borderRadius,
css,
dangerActiveColor,
dangerColor,
dangerFocusedColor,
ellipsis,
em,
half,
primaryActiveColor,
primaryColor,
primaryFocusedColor,
sameBorder,
textColor,
textInvertColor,
textLighterColor,
transitionProps
} from '~/utils/style';
import {WithStyled, borderRadius, css, ellipsis, em, half, sameBorder, transitionProps} from '~/utils/style';
import type {Icons} from '~/components/Icon';
import RawIcon from '~/components/Icon';
import {colors} from '~/utils/theme';
import styled from 'styled-components';
const height = em(36);
const colors = {
primary: {
default: primaryColor,
active: primaryActiveColor,
focused: primaryFocusedColor
},
danger: {
default: dangerColor,
active: dangerActiveColor,
focused: dangerFocusedColor
}
};
const defaultColor = {
default: borderColor,
active: borderActiveColor,
focused: borderFocusedColor
default: 'var(--border-color)',
focused: 'var(--border-focused-color)',
active: 'var(--border-active-color)'
} as const;
type colorTypes = keyof typeof colors;
......@@ -61,13 +29,14 @@ const Wrapper = styled.a<{type?: colorTypes; rounded?: boolean; disabled?: boole
border-radius: ${props => (props.rounded ? half(height) : borderRadius)};
${props => (props.type ? '' : sameBorder({color: defaultColor.default}))}
background-color: ${props => (props.type ? colors[props.type].default : 'transparent')};
color: ${props => (props.disabled ? textLighterColor : props.type ? textInvertColor : textColor)};
color: ${props =>
props.disabled ? 'var(--text-lighter-color)' : props.type ? colors[props.type].text : 'var(--text-color)'};
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
display: inline-block;
vertical-align: top;
text-align: center;
padding: 0 ${em(20)};
${transitionProps(['background-color', 'border-color'])}
${transitionProps(['background-color', 'border-color', 'color'])}
${ellipsis()}
&:hover,
......
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {
WithStyled,
backgroundColor,
borderRadius,
headerHeight,
math,
primaryColor,
rem,
sameBorder,
size,
transitionProps
} from '~/utils/style';
import {WithStyled, borderRadius, headerHeight, math, rem, sameBorder, size, transitionProps} from '~/utils/style';
import ee from '~/utils/event';
import styled from 'styled-components';
......@@ -21,13 +10,13 @@ const Div = styled.div<{maximized?: boolean; divWidth?: string; divHeight?: stri
props.maximized ? `calc(100vh - ${headerHeight} - ${rem(40)})` : props.divHeight || 'auto',
props.maximized ? '100%' : props.divWidth || '100%'
)}
background-color: ${backgroundColor};
background-color: var(--background-color);
${sameBorder({radius: math(`${borderRadius} * 2`)})}
${transitionProps(['border-color', 'box-shadow'])}
${transitionProps(['border-color', 'box-shadow', 'background-color'])}
position: relative;
&:hover {
border-color: ${primaryColor};
border-color: var(--primary-color);
box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.05);
}
`;
......
import React, {FunctionComponent, useState} from 'react';
import {
backgroundColor,
borderRadius,
em,
rem,
size,
textColor,
textLighterColor,
transitionProps
} from '~/utils/style';
import {borderRadius, em, rem, size, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import styled from 'styled-components';
const Wrapper = styled.div`
background-color: ${backgroundColor};
background-color: var(--background-color);
border-radius: ${borderRadius};
${transitionProps('background-color')}
& + & {
margin-top: ${rem(4)};
......@@ -28,14 +20,16 @@ const Header = styled.div`
justify-content: space-between;
align-items: center;
padding: 0 ${em(20)};
color: ${textLighterColor};
color: var(--text-lighter-color);
cursor: pointer;
${transitionProps('color')}
> h3 {
color: ${textColor};
color: var(--text-color);
flex-grow: 1;
margin: 0;
font-weight: 700;
${transitionProps('color')}
}
> .total {
......@@ -44,8 +38,9 @@ const Header = styled.div`
`;
const Content = styled.div`
border-top: 1px solid #eee;
border-top: 1px solid var(--border-color);
padding: ${rem(20)};
${transitionProps('border-color')}
`;
const CollapseIcon = styled(Icon)<{opened?: boolean}>`
......
import React, {FunctionComponent, PropsWithChildren, useCallback, useEffect, useMemo, useState} from 'react';
import {Trans, useTranslation} from 'react-i18next';
import {WithStyled, backgroundColor, headerHeight, link, primaryColor, rem, textLighterColor} from '~/utils/style';
import {WithStyled, headerHeight, link, primaryColor, rem, transitionProps} from '~/utils/style';
import BarLoader from 'react-spinners/BarLoader';
import Chart from '~/components/Chart';
......@@ -53,15 +53,16 @@ const Empty = styled.div<{height?: string}>`
width: 100%;
text-align: center;
font-size: ${rem(16)};
color: ${textLighterColor};
color: var(--text-lighter-color);
line-height: ${rem(24)};
height: ${props => props.height ?? 'auto'};
padding: ${rem(320)} 0 ${rem(70)};
background-color: ${backgroundColor};
background-color: var(--background-color);
background-image: url(${`${PUBLIC_PATH}/images/empty.svg`});
background-repeat: no-repeat;
background-position: calc(50% + ${rem(25)}) ${rem(70)};
background-size: ${rem(280)} ${rem(244)};
${transitionProps(['color', 'background-color'])}
${link}
`;
......@@ -131,7 +132,7 @@ const ChartPage = <T extends Item>({
}
return 0;
}),
[items] // eslint-disable-line react-hooks/exhaustive-deps
[items]
);
const total = useMemo(() => Math.ceil(matchedTags.length / pageSize), [matchedTags]);
......@@ -173,7 +174,7 @@ const ChartPage = <T extends Item>({
)}
</Wrapper>
),
[withChart, loading, chartSize, t] // eslint-disable-line react-hooks/exhaustive-deps
[withChart, loading, chartSize, t]
);
return (
......
import React, {FunctionComponent, useCallback, useState} from 'react';
import {
WithStyled,
em,
primaryActiveColor,
primaryColor,
primaryFocusedColor,
rem,
textColor,
textLightColor,
textLighterColor,
transitionProps
} from '~/utils/style';
import {WithStyled, em, rem, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import type {Icons} from '~/components/Icon';
......@@ -28,19 +17,19 @@ const Toolbox = styled.div<{reversed?: boolean}>`
const ToolboxItem = styled.a<{active?: boolean; reversed?: boolean}>`
cursor: pointer;
color: ${props => (props.active ? primaryColor : textLighterColor)};
color: ${props => (props.active ? 'var(--primary-color)' : 'var(--text-lighter-color)')};
${transitionProps('color')}
&:hover {
color: ${props => (props.active ? primaryFocusedColor : textLightColor)};
color: ${props => (props.active ? 'var(--primary-focused-color)' : 'var(--text-light-color)')};
}
&:active {
color: ${props => (props.active ? primaryActiveColor : textColor)};
color: ${props => (props.active ? 'var(--primary-active-color)' : 'var(--text-color)')};
}
& + & {
${props => `margin-${props.reversed ? 'right' : 'left'}: ${rem(14)};`}
margin: ${props => (props.reversed ? `0 ${rem(14)} 0 0` : `0 0 0 ${rem(14)}`)};
}
`;
......
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {
WithStyled,
backgroundColor,
darken,
ellipsis,
em,
half,
lighten,
math,
position,
primaryColor,
sameBorder,
size,
textInvertColor,
textLighterColor,
transitionProps
} from '~/utils/style';
import {WithStyled, ellipsis, em, half, math, position, sameBorder, size, transitionProps} from '~/utils/style';
import styled from 'styled-components';
......@@ -43,20 +27,21 @@ const Input = styled.input.attrs<{disabled?: boolean}>(props => ({
`;
const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>`
color: ${props => (props.checked ? textInvertColor : 'transparent')};
color: ${props => (props.checked ? 'var(--text-invert-color)' : 'transparent')};
flex-shrink: 0;
${props => size(math(`${checkSize} * ${props.size === 'small' ? 0.875 : 1}`))}
margin: ${half(`${height} - ${checkSize}`)} 0;
margin-right: ${em(10)};
${props => sameBorder({color: props.disabled || !props.checked ? textLighterColor : primaryColor})};
${props =>
sameBorder({color: props.disabled || !props.checked ? 'var(--text-lighter-color)' : 'var(--primary-color)'})};
background-color: ${props =>
props.disabled
? props.checked
? textLighterColor
: lighten(1 / 3, textLighterColor)
? 'var(--text-lighter-color)'
: 'var(--text-lighter-color)'
: props.checked
? primaryColor
: backgroundColor};
? 'var(--primary-color)'
: 'var(--background-color)'};
background-image: ${props => (props.checked ? `url("${checkMark}")` : 'none')};
background-repeat: no-repeat;
background-position: center center;
......@@ -66,14 +51,19 @@ const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>
${Wrapper}:hover > & {
border-color: ${props =>
props.disabled ? textLighterColor : props.checked ? primaryColor : darken(0.1, textLighterColor)};
props.disabled
? 'var(--text-lighter-color)'
: props.checked
? 'var(--primary-color)'
: 'var(--text-lighter-color)'};
}
`;
const Content = styled.div<{disabled?: boolean}>`
line-height: ${height};
flex-grow: 1;
${props => (props.disabled ? `color: ${textLighterColor};` : '')}
${props => (props.disabled ? 'color: var(--text-lighter-color);' : '')}
${transitionProps('color')}
${ellipsis()}
`;
......
import React, {FunctionComponent} from 'react';
import {backgroundColor, contentHeight, contentMargin, headerHeight, position} from '~/utils/style';
import {contentHeight, contentMargin, headerHeight, position, transitionProps} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading';
import styled from 'styled-components';
......@@ -16,11 +16,12 @@ const Article = styled.article`
const Aside = styled.aside`
flex: none;
background-color: ${backgroundColor};
background-color: var(--background-color);
height: ${`calc(100vh - ${headerHeight})`};
${position('sticky', headerHeight, 0, null, null)}
overflow-x: hidden;
overflow-y: auto;
${transitionProps('background-color')}
`;
type ContentProps = {
......
import React, {FunctionComponent} from 'react';
import {Trans, useTranslation} from 'react-i18next';
import {WithStyled, backgroundColor, em, link, rem, size, textColor, textLightColor} from '~/utils/style';
import {WithStyled, em, link, rem, size, transitionProps} from '~/utils/style';
import styled from 'styled-components';
......@@ -10,9 +10,10 @@ const Wrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
background-color: ${backgroundColor};
background-color: var(--background-color);
height: 100%;
width: 100%;
${transitionProps('background-color')}
> .image {
background-image: url(${`${PUBLIC_PATH}/images/empty.svg`});
......@@ -24,13 +25,15 @@ const Wrapper = styled.div`
> .inner {
width: calc(50% - ${rem(280)});
color: ${textLightColor};
color: var(--text-light-color);
${transitionProps('color')}
${link}
h4 {
color: ${textColor};
color: var(--text-color);
font-size: ${em(18)};
font-weight: 700;
${transitionProps('color')}
}
p {
......
import {GlobalDispatchContext, GlobalStateContext, globalState} from '~/hooks/useGlobalState';
import React, {useReducer} from 'react';
import type {FunctionComponent} from 'react';
import type {GlobalState as GlobalStateType} from '~/hooks/useGlobalState';
interface GlobalDispatch {
(state: GlobalStateType, newState: Partial<GlobalStateType>): GlobalStateType;
}
// TODO: use redux
const GlobalState: FunctionComponent = ({children}) => {
const [state, dispatch] = useReducer<GlobalDispatch>(
(state, newState) =>
Object.entries(newState).reduce(
(m, [key, value]) => {
if (m.hasOwnProperty(key)) {
m[key] = {...m[key], ...value};
} else {
m[key] = value;
}
return m;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...state} as any
),
globalState
);
return (
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>{children}</GlobalDispatchContext.Provider>
</GlobalStateContext.Provider>
);
};
export default GlobalState;
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 {em, sameBorder, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import styled from 'styled-components';
......@@ -41,16 +41,18 @@ const Wrapper = styled.div`
cursor: pointer;
font-size: ${em(14)};
margin-left: ${em(10)};
color: ${textLighterColor};
color: var(--text-lighter-color);
${transitionProps('color')}
&:hover,
&:active {
color: ${textLightColor};
color: var(--text-light-color);
}
}
&:not(:first-child) {
border-top: 1px solid ${borderColor};
border-top: 1px solid var(--border-color);
${transitionProps('border-color')}
}
}
`;
......
import type {Documentation, OpenedResult, 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 {contentHeight, position, primaryColor, rem, size, transitionProps} 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 useTheme from '~/hooks/useTheme';
import {useTranslation} from 'react-i18next';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
......@@ -22,11 +23,12 @@ const toolboxHeight = rem(40);
const Wrapper = styled.div`
position: relative;
height: ${contentHeight};
background-color: ${backgroundColor};
background-color: var(--background-color);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
${transitionProps('background-color')}
`;
const RenderContent = styled.div<{show: boolean}>`
......@@ -41,8 +43,9 @@ const RenderContent = styled.div<{show: boolean}>`
const Toolbox = styled(ChartToolbox)`
height: ${toolboxHeight};
border-bottom: 1px solid ${borderColor};
border-bottom: 1px solid var(--border-color);
padding: 0 ${rem(20)};
${transitionProps('border-color')}
`;
const Content = styled.div`
......@@ -57,13 +60,13 @@ const Content = styled.div`
> .powered-by {
display: block;
${position('absolute', null, null, rem(20), rem(30))}
color: #ddd;
color: var(--graph-copyright-color);
font-size: ${rem(14)};
user-select: none;
img {
height: 1em;
opacity: 0.5;
filter: var(--graph-copyright-logo-filter);
vertical-align: middle;
}
}
......@@ -125,6 +128,8 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
) => {
const {t} = useTranslation('graph');
const theme = useTheme();
const [ready, setReady] = useState(false);
const [loading, setLoading] = useState(false);
const [rendered, setRendered] = useState(false);
......@@ -204,6 +209,8 @@ const Graph = React.forwardRef<GraphRef, GraphProps>(
ready
]);
useEffect(() => (ready && dispatch('toggle-theme', theme)) || undefined, [dispatch, theme, ready]);
useImperativeHandle(ref, () => ({
export(type) {
dispatch('export', type);
......
import React, {FunctionComponent} from 'react';
import {backgroundColor, borderColor, rem, textLightColor} from '~/utils/style';
import {rem, transitionProps} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const Sidebar = styled.div`
height: 100%;
background-color: ${backgroundColor};
background-color: var(--background-color);
`;
const Title = styled.div`
......@@ -15,13 +15,15 @@ const Title = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid ${borderColor};
border-bottom: 1px solid var(--border-color);
margin: 0 ${rem(20)};
${transitionProps('border-color')}
> .close {
flex: none;
color: ${textLightColor};
color: var(--text-light-color);
cursor: pointer;
${transitionProps('color')}
}
`;
......
import React, {FunctionComponent} from 'react';
import {backgroundColor, em, size} from '~/utils/style';
import {em, size, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import Properties from '~/components/GraphPage/Properties';
......@@ -14,8 +14,9 @@ const Dialog = styled.div`
width: 100vw;
height: 100vh;
overscroll-behavior: none;
background-color: rgba(255, 255, 255, 0.8);
background-color: var(--mask-color);
z-index: 999;
${transitionProps('background-color')}
> .modal {
width: ${em(536)};
......@@ -28,11 +29,12 @@ const Dialog = styled.div`
> .modal-header {
padding: 0 ${em(40, 18)};
height: ${em(47, 18)};
background-color: #eee;
background-color: var(--model-header-background-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: ${em(18)};
${transitionProps('background-color')}
> .modal-title {
flex: auto;
......@@ -49,9 +51,10 @@ const Dialog = styled.div`
> .modal-body {
padding: ${em(40)};
background-color: ${backgroundColor};
background-color: var(--background-color);
overflow: auto;
max-height: calc(80vh - ${em(47)});
${transitionProps('background-color')}
}
}
`;
......
import React, {FunctionComponent, useCallback} from 'react';
import {Trans, useTranslation} from 'react-i18next';
import {borderRadius, em, textLightColor} from '~/utils/style';
import {borderRadius, em, transitionProps} from '~/utils/style';
import type {Documentation as DocumentationType} from '~/resource/graph/types';
import GraphSidebar from '~/components/GraphPage/GraphSidebar';
......@@ -46,11 +46,12 @@ const Documentation = styled.div`
pre {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
background-color: rgba(216, 216, 216, 0.5);
color: ${textLightColor};
background-color: var(--code-background-color);
color: var(--code-color);
padding: ${em(10)};
border-radius: ${borderRadius};
overflow: auto;
${transitionProps('color')}
code {
background-color: transparent;
......@@ -61,10 +62,11 @@ const Documentation = styled.div`
code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
background-color: rgba(216, 216, 216, 0.5);
color: ${textLightColor};
background-color: var(--code-background-color);
color: var(--code-color);
padding: ${em(2)} ${em(4)};
border-radius: ${em(2)};
${transitionProps('color')}
}
`;
......
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import type {SearchItem, SearchResult} from '~/resource/graph/types';
import {
backgroundColor,
backgroundFocusedColor,
css,
ellipsis,
em,
primaryColor,
rem,
sameBorder,
size,
textLightColor,
transitionProps,
triangle
} from '~/utils/style';
import {css, ellipsis, em, rem, sameBorder, size, transitionProps, triangle} from '~/utils/style';
import Field from '~/components/Field';
import SearchInput from '~/components/SearchInput';
......@@ -32,17 +19,19 @@ const SearchField = styled(Field)`
}
> a:last-child {
color: ${primaryColor};
color: var(--primary-color);
cursor: pointer;
margin-left: ${rem(10)};
flex: none;
${transitionProps('color')}
}
`;
const Empty = styled.div`
padding: ${rem(100)} 0;
text-align: center;
color: ${textLightColor};
color: var(--text-light-color);
${transitionProps('color')}
`;
const Wrapper = styled.div`
......@@ -59,7 +48,7 @@ const Item = styled.li`
padding: ${em(10)} ${em(12)};
cursor: pointer;
width: 100%;
background-color: ${backgroundColor};
background-color: var(--background-color);
display: flex;
align-items: center;
${transitionProps('background-color')}
......@@ -71,7 +60,7 @@ const Item = styled.li`
}
&:hover {
background-color: ${backgroundFocusedColor};
background-color: var(--background-focused-color);
}
`;
......
import React, {FunctionComponent, useCallback, useState} from 'react';
import {em, primaryColor, sameBorder, size, textLightColor} from '~/utils/style';
import {em, sameBorder, size, transitionProps} from '~/utils/style';
import Button from '~/components/Button';
import Icon from '~/components/Icon';
......@@ -12,18 +12,21 @@ const DropZone = styled.div<{actived: boolean}>`
width: '1px',
type: 'dashed',
radius: em(16),
color: props.actived ? primaryColor : undefined
color: props.actived ? 'var(--primary-color)' : undefined
})}
background-color: ${props => (props.actived ? '#f2f6ff' : '#f9f9f9')};
background-color: ${props =>
props.actived ? 'var(--graph-uploader-active-background-color)' : 'var(--graph-uploader-background-color)'};
${size('43.2%', '68%')}
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
${transitionProps('border-color', 'background-color')}
> .upload-icon {
font-size: ${em(64)};
color: ${primaryColor};
color: var(--primary-color);
${transitionProps('color')}
}
> span {
......@@ -45,11 +48,12 @@ const SupportTable = styled.table`
line-height: 2;
&:first-of-type {
color: ${textLightColor};
color: var(--text-light-color);
text-align: right;
padding-right: ${em(10)};
font-size: ${em(16)};
width: ${em(250)};
${transitionProps('color')}
}
}
`;
......
import {WithStyled, borderFocusedColor, em, half, sameBorder, textLighterColor, transitionProps} from '~/utils/style';
import {WithStyled, em, half, sameBorder, transitionProps} from '~/utils/style';
import React from 'react';
import styled from 'styled-components';
......@@ -13,15 +13,19 @@ const StyledInput = styled.input<{rounded?: boolean}>`
display: inline-block;
outline: none;
${props => sameBorder({radius: !props.rounded || half(height)})};
${transitionProps('border-color')}
background-color: var(--input-background-color);
color: var(--text-color);
caret-color: var(--text-color);
${transitionProps(['border-color', 'background-color', 'caret-color', 'color'])}
&:hover,
&:focus {
border-color: ${borderFocusedColor};
border-color: var(--border-focused-color);
}
&::placeholder {
color: ${textLighterColor};
color: var(--text-lighter-color);
${transitionProps('color')}
}
`;
......
......@@ -2,7 +2,7 @@ import * as chart from '~/utils/chart';
import React, {useEffect, useImperativeHandle} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts, {Options, Wrapper} from '~/hooks/useECharts';
import useECharts, {Options, Wrapper, useChartTheme} from '~/hooks/useECharts';
import type {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader';
......@@ -46,6 +46,8 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
onInit
});
const theme = useChartTheme();
useImperativeHandle(ref, () => ({
restore: () => {
echart?.dispatchAction({
......@@ -79,6 +81,7 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
)
},
options,
theme,
defaults
);
if ((chartOptions?.xAxis as EChartOption.XAxis).type === 'time') {
......@@ -106,7 +109,7 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
);
}
echart?.setOption(chartOptions, {notMerge: true});
}, [options, data, title, i18n.language, echart]);
}, [options, data, title, theme, i18n.language, echart]);
return (
<Wrapper ref={wrapper} className={className}>
......
import {Link, LinkProps, useLocation} from 'react-router-dom';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import {
backgroundFocusedColor,
border,
borderRadius,
navbarBackgroundColor,
navbarHighlightColor,
navbarHoverBackgroundColor,
primaryColor,
rem,
size,
textColor,
textInvertColor,
transitionProps
} from '~/utils/style';
import {border, borderRadius, rem, size, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import Language from '~/components/Language';
......@@ -52,13 +39,14 @@ function appendApiToken(url: string) {
}
const Nav = styled.nav`
background-color: ${navbarBackgroundColor};
color: ${textInvertColor};
background-color: var(--navbar-background-color);
color: var(--navbar-text-color);
${size('100%')}
padding: 0 ${rem(20)};
display: flex;
justify-content: space-between;
align-items: stretch;
${transitionProps(['background-color', 'color'])}
> .left {
display: flex;
......@@ -97,12 +85,12 @@ const NavItem = styled.div<{active?: boolean}>`
display: inline-flex;
justify-content: center;
align-items: center;
background-color: ${navbarBackgroundColor};
background-color: var(--navbar-background-color);
cursor: pointer;
${transitionProps('background-color')}
&:hover {
background-color: ${navbarHoverBackgroundColor};
background-color: var(--navbar-hover-background-color);
}
&.nav-item {
......@@ -121,7 +109,7 @@ const NavItem = styled.div<{active?: boolean}>`
.nav-text {
margin: ${rem(20)};
padding: ${rem(10)} 0 ${rem(7)};
${props => border('bottom', rem(3), 'solid', props.active ? navbarHighlightColor : 'transparent')}
${props => border('bottom', rem(3), 'solid', props.active ? 'var(--navbar-highlight-color)' : 'transparent')}
${transitionProps('border-bottom')}
text-transform: uppercase;
}
......@@ -138,11 +126,11 @@ const NavItemChild = styled.div<{active?: boolean}>`
&,
&:visited {
color: ${props => (props.active ? primaryColor : textColor)};
color: ${props => (props.active ? 'var(--primary-color)' : 'var(--text-color)')};
}
&:hover {
background-color: ${backgroundFocusedColor};
background-color: var(--background-focused-color);
}
> a {
......
import React, {FunctionComponent, useEffect, useState} from 'react';
import {ellipsis, size, textLighterColor} from '~/utils/style';
import {ellipsis, size, transitionProps} from '~/utils/style';
import Field from '~/components/Field';
import RangeSlider from '~/components/RangeSlider';
......@@ -13,10 +13,11 @@ import {useTranslation} from 'react-i18next';
const relativeFormatter = format('.2f');
const TimeDisplay = styled.div`
color: ${textLighterColor};
color: var(--text-lighter-color);
font-size: 0.857142857em;
padding-left: 1.666666667em;
margin-bottom: 0.416666667em;
${transitionProps('color')}
`;
const Label = styled.span<{color: string}>`
......
......@@ -2,17 +2,11 @@ import {EventContext, ValueContext} from '~/components/RadioGroup';
import React, {FunctionComponent, PropsWithChildren, useCallback, useContext} from 'react';
import {
WithStyled,
backgroundColor,
borderColor,
borderFocusedColor,
borderRadius,
borderRadiusShortHand,
ellipsis,
em,
primaryColor,
sameBorder,
textColor,
textInvertColor,
transitionProps
} from '~/utils/style';
......@@ -24,23 +18,22 @@ const maxWidth = em(144);
const Button = styled.a<{selected?: boolean}>`
cursor: pointer;
background-color: ${props => (props.selected ? primaryColor : backgroundColor)};
color: ${props => (props.selected ? textInvertColor : textColor)};
background-color: ${props => (props.selected ? 'var(--primary-color)' : 'var(--background-color)')};
color: ${props => (props.selected ? 'var(--primary-text-color)' : 'var(--text-color)')};
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${minWidth};
padding: 0 ${em(8)};
text-align: center;
${ellipsis(maxWidth)}
${props => sameBorder({color: props.selected ? primaryColor : borderColor})};
${props => sameBorder({color: props.selected ? 'var(--primary-color)' : 'var(--border-color)'})};
${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)};
border-color: ${props => (props.selected ? 'var(--primary-color)' : 'var(--border-focused-color)')};
}
&:first-of-type {
......
......@@ -33,7 +33,7 @@ const RadioGroup = <T extends unknown>({
setSelected(value);
onChange?.(value);
},
[onChange] // eslint-disable-line react-hooks/exhaustive-deps
[onChange]
);
return (
......
import InputRange, {Range} from 'react-input-range';
import React, {FunctionComponent, useCallback} from 'react';
import {
WithStyled,
backgroundColor,
em,
half,
position,
primaryActiveColor,
primaryColor,
primaryFocusedColor,
sameBorder,
size,
textLighterColor
} from '~/utils/style';
import {WithStyled, em, half, position, sameBorder, size, transitionProps} from '~/utils/style';
import styled from 'styled-components';
const height = em(20);
const railHeight = em(4);
const thumbSize = em(12);
const railColor = '#DBDEEB';
const Wrapper = styled.div<{disabled?: boolean}>`
height: ${height};
......@@ -32,14 +19,14 @@ const Wrapper = styled.div<{disabled?: boolean}>`
display: none;
}
--color: ${primaryColor};
--color: var(--primary-color);
&:hover {
--color: ${primaryFocusedColor};
--color: var(--primary-focused-color);
}
&:active {
--color: ${primaryActiveColor};
--color: var(--primary-active-color);
}
&__track {
......@@ -49,16 +36,18 @@ const Wrapper = styled.div<{disabled?: boolean}>`
${size(railHeight, '100%')}
${position('absolute', '50%', null, null, null)}
margin-top: -${half(railHeight)};
background-color: ${railColor};
background-color: var(--slider-rail-color);
border-radius: ${half(railHeight)};
${transitionProps('background-color')}
}
&--active {
height: ${railHeight};
position: absolute;
background-color: ${props => (props.disabled ? textLighterColor : 'var(--color)')};
background-color: ${props => (props.disabled ? 'var(--text-lighter-color)' : 'var(--color)')};
border-radius: ${half(railHeight)};
outline: none;
${transitionProps('background-color')}
}
}
......@@ -72,10 +61,11 @@ const Wrapper = styled.div<{disabled?: boolean}>`
${props =>
sameBorder({
width: em(3),
color: props.disabled ? textLighterColor : 'var(--color)',
color: props.disabled ? 'var(--text-lighter-color)' : 'var(--color)',
radius: half(thumbSize)
})}
background-color: ${backgroundColor};
background-color: var(--slider-gripper-color);
${transitionProps(['border-color', 'background-color'])}
}
}
`;
......
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ellipsis, em, primaryColor, rem, size, textLightColor, textLighterColor} from '~/utils/style';
import {ellipsis, em, primaryColor, rem, size, transitionProps} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import GridLoader from 'react-spinners/GridLoader';
......@@ -47,9 +47,10 @@ const Title = styled.div<{color: string}>`
font-size: ${em(14)};
flex-shrink: 0;
flex-grow: 0;
color: ${textLightColor};
color: var(--text-light-color);
${ellipsis()}
max-width: 50%;
${transitionProps('color')}
&::before {
content: '';
......@@ -81,8 +82,9 @@ const Footer = styled.div`
`;
const FooterInfo = styled.div`
color: ${textLighterColor};
color: var(--text-lighter-color);
font-size: ${rem(12)};
${transitionProps('color')}
> * {
display: inline-block;
......
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {em, textLightColor} from '~/utils/style';
import {em, transitionProps} from '~/utils/style';
import RangeSlider from '~/components/RangeSlider';
import styled from 'styled-components';
......@@ -8,9 +8,10 @@ import {useTranslation} from 'react-i18next';
const Label = styled.div`
display: flex;
justify-content: space-between;
color: ${textLightColor};
color: var(--text-light-color);
font-size: ${em(12)};
margin-bottom: ${em(5)};
${transitionProps('color')}
> :not(:first-child) {
flex-grow: 0;
......
import React, {FunctionComponent, useEffect, useMemo} from 'react';
import {WithStyled, backgroundColor, position, primaryColor, size} from '~/utils/style';
import {WithStyled, position, primaryColor, size, transitionProps} from '~/utils/style';
import useECharts, {useChartTheme} from '~/hooks/useECharts';
import GridLoader from 'react-spinners/GridLoader';
import styled from 'styled-components';
import useECharts from '~/hooks/useECharts';
const Wrapper = styled.div`
position: relative;
background-color: ${backgroundColor};
background-color: var(--background-color);
${transitionProps('background-color')}
> .echarts {
height: 100%;
......@@ -70,17 +71,20 @@ const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({data,
gl,
autoFit: true
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {tooltip, ...theme} = useChartTheme(gl);
const chartOptions = useMemo(
() => ({
...(gl ? options3D : options2D),
...theme,
series:
data?.map(series => ({
...(gl ? series3D : series2D),
...series
})) ?? []
}),
[gl, data]
[gl, data, theme]
);
useEffect(() => {
......
import Input, {InputProps, padding} from '~/components/Input';
import React, {FunctionComponent, useCallback, useRef} from 'react';
import {WithStyled, math, position, textColor, textLightColor, textLighterColor} from '~/utils/style';
import {WithStyled, math, position, transitionProps} from '~/utils/style';
import Icon from '~/components/Icon';
import styled from 'styled-components';
......@@ -24,7 +24,8 @@ const SearchIcon = styled(Icon)`
transform-origin: center;
${position('absolute', '50%', null, null, padding)}
pointer-events: none;
color: ${textLighterColor};
color: var(--text-lighter-color);
${transitionProps('color')}
`;
const CloseIcon = styled(Icon)`
......@@ -33,14 +34,15 @@ const CloseIcon = styled(Icon)`
transform-origin: center;
${position('absolute', '50%', padding, null, null)}
cursor: pointer;
color: ${textLighterColor};
color: var(--text-lighter-color);
${transitionProps('color')}
&:hover {
color: ${textLightColor};
color: var(--text-light-color);
}
&:active {
color: ${textColor};
color: var(--text-color);
}
`;
......
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import {
WithStyled,
backgroundColor,
backgroundFocusedColor,
borderColor,
borderFocusedColor,
borderRadius,
borderRadiusShortHand,
css,
......@@ -12,9 +8,7 @@ import {
em,
math,
sameBorder,
selectedColor,
size,
textLighterColor,
transitionProps
} from '~/utils/style';
......@@ -34,15 +28,13 @@ const Wrapper = styled.div<{opened?: boolean}>`
max-width: 100%;
display: inline-block;
position: relative;
background-color: ${backgroundColor};
background-color: var(--background-color);
${sameBorder({radius: true})}
${props => (props.opened ? borderRadiusShortHand('bottom', '0') : '')}
${transitionProps(
'border-color'
)}
${transitionProps('border-color', 'background-color')}
&:hover {
border-color: ${borderFocusedColor};
border-color: var(--border-focused-color);
}
`;
......@@ -53,7 +45,8 @@ const Trigger = styled.div<{selected?: boolean}>`
justify-content: space-between;
align-items: center;
cursor: pointer;
${props => (props.selected ? '' : `color: ${textLighterColor}`)}
${props => (props.selected ? '' : 'color: var(--text-lighter-color)')}
${transitionProps('color')}
`;
const TriggerIcon = styled(Icon)<{opened?: boolean}>`
......@@ -81,17 +74,18 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>`
left: -1px;
padding: ${padding} 0;
border: inherit;
border-top-color: ${borderColor};
border-top-color: var(--border-color);
${borderRadiusShortHand('bottom', borderRadius)}
display: ${props => (props.opened ? 'block' : 'none')};
z-index: 9999;
line-height: 1;
background-color: inherit;
box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.05);
${transitionProps(['border-color', 'color'])}
${props =>
props.empty
? {
color: textLighterColor,
color: 'var(--text-lighter-color)',
textAlign: 'center'
}
: ''}
......@@ -106,14 +100,14 @@ const listItem = css`
${transitionProps(['color', 'background-color'])}
&:hover {
background-color: ${backgroundFocusedColor};
background-color: var(--background-focused-color);
}
`;
const ListItem = styled.div<{selected?: boolean}>`
${ellipsis()}
${listItem}
${props => (props.selected ? `color: ${selectedColor};` : '')}
${props => (props.selected ? `color: var(--select-selected-text-color);` : '')}
`;
const MultipleListItem = styled(Checkbox)<{selected?: boolean}>`
......@@ -167,7 +161,6 @@ 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
......@@ -178,7 +171,7 @@ const Select = <T extends unknown>({
(onChange as OnSingleChange<T>)?.(mutateValue);
setIsOpenedFalse();
},
[setIsOpenedFalse, onChange] // eslint-disable-line react-hooks/exhaustive-deps
[setIsOpenedFalse, onChange]
);
const changeMultipleValue = useCallback(
(mutateValue: T, checked: boolean) => {
......@@ -195,7 +188,7 @@ const Select = <T extends unknown>({
setValue(newValue);
(onChange as OnMultipleChange<T>)?.(newValue);
},
[value, onChange] // eslint-disable-line react-hooks/exhaustive-deps
[value, onChange]
);
const ref = useClickOutside<HTMLDivElement>(setIsOpenedFalse);
......@@ -207,11 +200,10 @@ const Select = <T extends unknown>({
? {value: item as T, label: item + ''}
: (item as SelectListItem<T>)
) ?? [],
[propList] // eslint-disable-line react-hooks/exhaustive-deps
[propList]
);
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(
() =>
......@@ -220,7 +212,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] // eslint-disable-line react-hooks/exhaustive-deps
[multiple, value, findLabelByValue, isSelected, placeholder, t]
);
return (
......
import React, {FunctionComponent, useCallback, useState} from 'react';
import {borderColor, borderFocusedColor, rem, size, transitionProps} from '~/utils/style';
import {height, padding} from '~/components/Input';
import {rem, size, transitionProps} from '~/utils/style';
import BigNumber from 'bignumber.js';
import RangeSlider from '~/components/RangeSlider';
......@@ -17,14 +17,17 @@ const Input = styled.input`
display: inline-block;
outline: none;
padding: ${padding};
${transitionProps('border-color')}
${transitionProps(['border-color', 'color', 'caret-color'])}
border: none;
border-bottom: 1px solid ${borderColor};
border-bottom: 1px solid var(--border-color);
text-align: center;
background-color: transparent;
color: var(--text-color);
caret-color: var(--text-color);
&:hover,
&:focus {
border-bottom-color: ${borderFocusedColor};
border-bottom-color: var(--border-focused-color);
}
`;
......
......@@ -2,8 +2,8 @@ import * as chart from '~/utils/chart';
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';
import {WithStyled, primaryColor, transitionProps} from '~/utils/style';
import useECharts, {Options, Wrapper, useChartTheme} from '~/hooks/useECharts';
import GridLoader from 'react-spinners/GridLoader';
import defaultsDeep from 'lodash/defaultsDeep';
......@@ -13,11 +13,12 @@ import useThrottleFn from '~/hooks/useThrottleFn';
const Tooltip = styled.div`
position: absolute;
z-index: 1;
background-color: rgba(0, 0, 0, 0.75);
color: #fff;
background-color: var(--tooltip-background-color);
color: var(--tooltip-text-color);
border-radius: 4px;
padding: 5px;
display: none;
${transitionProps(['color', 'background-color'])}
`;
type renderItem = NonNullable<EChartOption.SeriesCustom['renderItem']>;
......@@ -150,6 +151,8 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
[pointerLabelFormatter]
);
const theme = useChartTheme();
const chartOptions = useMemo<EChartOption>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, toolbox, series, ...defaults} = chart;
......@@ -213,9 +216,10 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
]
},
options,
theme,
defaults
);
}, [options, title, rawData, minX, maxX, minY, maxY, negativeY, renderItem, axisPointerLabelFormatter]);
}, [options, title, theme, rawData, minX, maxX, minY, maxY, negativeY, renderItem, axisPointerLabelFormatter]);
const mouseout = useCallback(() => {
setHighlight(null);
......
import React, {FunctionComponent} from 'react';
import {
WithStyled,
backgroundColor,
em,
half,
lightActiveColor,
lightColor,
lightFocusedColor,
primaryColor,
transitionProps
} from '~/utils/style';
import {WithStyled, em, half, transitionProps} from '~/utils/style';
import styled from 'styled-components';
......@@ -21,17 +11,17 @@ const Span = styled.span<{active?: boolean}>`
line-height: ${height};
display: inline-block;
border-radius: ${half(height)};
color: ${prop => (prop.active ? backgroundColor : primaryColor)};
background-color: ${prop => (prop.active ? primaryColor : lightColor)};
color: ${prop => (prop.active ? 'var(--background-color)' : 'var(--primary-color)')};
background-color: ${prop => (prop.active ? 'var(--primary-color)' : 'var(--tag-background-color)')};
cursor: pointer;
${transitionProps(['color', 'background-color'])}
&:hover {
background-color: ${prop => (prop.active ? primaryColor : lightFocusedColor)};
background-color: ${prop => (prop.active ? 'var(--primary-color)' : 'var(--tag-focused-background-color)')};
}
&:active {
background-color: ${prop => (prop.active ? primaryColor : lightActiveColor)};
background-color: ${prop => (prop.active ? 'var(--primary-color)' : 'var(--tag-active-background-color)')};
}
`;
......
import {MutableRefObject, useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
import {maskColor, position, primaryColor, size, textColor} from '~/utils/style';
import {MutableRefObject, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import {position, primaryColor, size} from '~/utils/style';
import type {ECharts} from 'echarts';
import {dataURL2Blob} from '~/utils/image';
import {saveAs} from 'file-saver';
import styled from 'styled-components';
import {themes} from '~/utils/theme';
import useTheme from '~/hooks/useTheme';
export type Options = {
loading?: boolean;
......@@ -26,6 +28,7 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
const ref = useRef<T | null>(null);
const echartInstance = useRef<ECharts | null>(null);
const [echart, setEchart] = useState<ECharts | null>(null);
const theme = useTheme();
const onInit = useRef(options.onInit);
const onDispose = useRef(options.onDispose);
......@@ -82,14 +85,14 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
echartInstance.current?.showLoading('default', {
text: '',
color: primaryColor,
textColor,
maskColor,
textColor: themes[theme].textColor,
maskColor: themes[theme].maskColor,
zlevel: 0
});
} else {
echartInstance.current?.hideLoading();
}
}, [options.loading]);
}, [options.loading, theme]);
const wrapper = useRef<W | null>(null);
useLayoutEffect(() => {
......@@ -138,3 +141,128 @@ export const Wrapper = styled.div`
align-items: center;
}
`;
export const useChartTheme = (gl?: boolean) => {
const theme = useTheme();
const tt = useMemo(() => themes[theme], [theme]);
if (gl) {
return {
title: {
textStyle: {
color: tt.textColor
}
},
tooltip: {
backgroundColor: tt.tooltipBackgroundColor,
borderColor: tt.tooltipBackgroundColor,
textStyle: {
color: tt.tooltipTextColor
}
},
xAxis3D: {
nameTextStyle: {
color: tt.textLighterColor
},
axisLabel: {
color: tt.textLighterColor
},
axisLine: {
lineStyle: {
color: tt.borderColor
}
},
splitLine: {
lineStyle: {
color: tt.borderColor
}
}
},
yAxis3D: {
nameTextStyle: {
color: tt.textLighterColor
},
axisLabel: {
color: tt.textLighterColor
},
axisLine: {
lineStyle: {
color: tt.borderColor
}
},
splitLine: {
lineStyle: {
color: tt.borderColor
}
}
},
zAxis3D: {
nameTextStyle: {
color: tt.textLighterColor
},
axisLabel: {
color: tt.textLighterColor
},
axisLine: {
lineStyle: {
color: tt.borderColor
}
},
splitLine: {
lineStyle: {
color: tt.borderColor
}
}
}
};
}
return {
title: {
textStyle: {
color: tt.textColor
}
},
tooltip: {
backgroundColor: tt.tooltipBackgroundColor,
borderColor: tt.tooltipBackgroundColor,
textStyle: {
color: tt.tooltipTextColor
}
},
xAxis: {
nameTextStyle: {
color: tt.textLighterColor
},
axisLabel: {
color: tt.textLighterColor
},
axisLine: {
lineStyle: {
color: tt.borderColor
}
},
splitLine: {
lineStyle: {
color: tt.borderColor
}
}
},
yAxis: {
nameTextStyle: {
color: tt.textLighterColor
},
axisLabel: {
color: tt.textLighterColor
},
axisLine: {
lineStyle: {
color: tt.borderColor
}
},
splitLine: {
lineStyle: {
color: tt.borderColor
}
}
}
};
};
import {createContext, useContext} from 'react';
import type {Dispatch} from 'react';
export interface GlobalState {
scalar: {
runs: string[];
};
histogram: {
runs: string[];
};
image: {
runs: string[];
};
audio: {
runs: string[];
};
prCurve: {
runs: string[];
};
graph: {
model: FileList | File[] | null;
};
}
export const globalState: GlobalState = {
scalar: {
runs: []
},
histogram: {
runs: []
},
image: {
runs: []
},
audio: {
runs: []
},
prCurve: {
runs: []
},
graph: {
model: null
}
};
export const GlobalStateContext = createContext<GlobalState>(globalState);
export const GlobalDispatchContext = createContext<Dispatch<Partial<GlobalState>>>(() => void 0);
const useGlobalState = () => [useContext(GlobalStateContext), useContext(GlobalDispatchContext)] as const;
export default useGlobalState;
......@@ -23,7 +23,7 @@ function useRequest<D = unknown, E extends Error = Error>(
config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E> {
const {data, error, ...other} = useSWR<D, E>(key, fetcher, config);
const loading = useMemo(() => !!key && !data && !error, [key, data, error]);
const loading = useMemo(() => !!key && data === void 0 && !error, [key, data, error]);
useEffect(() => {
if (error) {
......
import type {Run, Tag, TagWithSingleRun, TagsData} from '~/types';
import {actions, selectors} from '~/store';
import {color, colorAlt} from '~/utils/chart';
import {useCallback, useEffect, useMemo, useReducer} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import type {Page} from '~/store/runs/types';
import {cache} from 'swr';
import camelCase from 'lodash/camelCase';
import groupBy from 'lodash/groupBy';
import intersection from 'lodash/intersection';
import intersectionBy from 'lodash/intersectionBy';
import uniq from 'lodash/uniq';
import useGlobalState from '~/hooks/useGlobalState';
import useQuery from '~/hooks/useQuery';
import {useRunningRequest} from '~/hooks/useRequest';
......@@ -160,7 +161,7 @@ const reducer = (state: State, action: Action): State => {
};
// TODO: refactor to improve performance
const useTagFilter = (type: string, running: boolean) => {
const useTagFilter = (type: Page, running: boolean) => {
const query = useQuery();
const {data, loading, error} = useRunningRequest<TagsData>(`/${type}/tags`, running);
......@@ -168,13 +169,9 @@ const useTagFilter = (type: string, running: boolean) => {
// clear cache in order to fully reload data when switching page
useEffect(() => () => cache.delete(`/${type}/tags`), [type]);
const pageName = useMemo(() => camelCase(type), [type]);
const [globalState, globalDispatch] = useGlobalState();
const storedRuns = useMemo(
() => ((globalState as unknown) as Record<string, {runs: string[]}>)[camelCase(pageName)]?.runs ?? [],
[pageName, globalState]
);
const storeDispatch = useDispatch();
const selector = useMemo(() => selectors.runs.getRunsByPage(type), [type]);
const storedRuns = useSelector(selector);
const runs: string[] = useMemo(() => data?.runs ?? [], [data]);
const tags: Tags = useMemo(
......@@ -222,15 +219,9 @@ const useTagFilter = (type: string, running: boolean) => {
});
}
}, [queryRuns, state.runs]);
useEffect(
() =>
globalDispatch({
[pageName]: {
runs: state.globalRuns
}
}),
[pageName, state.globalRuns, globalDispatch]
);
useEffect(() => {
storeDispatch(actions.runs.setSelectedRuns(type, state.globalRuns));
}, [storeDispatch, state.globalRuns, type]);
const tagsWithSingleRun = useMemo(
() =>
......
import {selectors} from '~/store';
import {useSelector} from 'react-redux';
const useTheme = () => useSelector(selectors.theme.theme);
export default useTheme;
......@@ -2,10 +2,11 @@ import '~/utils/i18n';
import App from './App';
import BodyLoading from '~/components/BodyLoading';
import GlobalState from '~/components/GlobalState';
import {GlobalStyle} from '~/utils/style';
import {Provider} from 'react-redux';
import React from 'react';
import ReactDOM from 'react-dom';
import store from '~/store';
const TELEMETRY_ID: string = import.meta.env.SNOWPACK_PUBLIC_TELEMETRY_ID;
......@@ -23,9 +24,9 @@ ReactDOM.render(
<React.StrictMode>
<GlobalStyle />
<React.Suspense fallback={<BodyLoading />}>
<GlobalState>
<Provider store={store}>
<App />
</GlobalState>
</Provider>
</React.Suspense>
</React.StrictMode>,
document.getElementById('root')
......
......@@ -3,7 +3,9 @@ import type {Documentation, OpenedResult, Properties, SearchItem, SearchResult}
import GraphComponent, {GraphRef} from '~/components/GraphPage/Graph';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import Select, {SelectProps} from '~/components/Select';
import {actions, selectors} from '~/store';
import {primaryColor, rem, size} from '~/utils/style';
import {useDispatch, useSelector} from 'react-redux';
import type {BlobResponse} from '~/utils/fetch';
import Button from '~/components/Button';
......@@ -20,7 +22,6 @@ import Search from '~/components/GraphPage/Search';
import Title from '~/components/Title';
import Uploader from '~/components/GraphPage/Uploader';
import styled from 'styled-components';
import useGlobalState from '~/hooks/useGlobalState';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
......@@ -71,11 +72,12 @@ const Loading = styled.div`
const Graph: FunctionComponent = () => {
const {t} = useTranslation(['graph', 'common']);
const [globalState, globalDispatch] = useGlobalState();
const storeDispatch = useDispatch();
const storeModel = useSelector(selectors.graph.model);
const graph = useRef<GraphRef>(null);
const file = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<FileList | File[] | null>(globalState.graph.model);
const [files, setFiles] = useState<FileList | File[] | null>(storeModel);
const onClickFile = useCallback(() => {
if (file.current) {
file.current.value = '';
......@@ -86,11 +88,11 @@ const Graph: FunctionComponent = () => {
(e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target;
if (target && target.files && target.files.length) {
globalDispatch({graph: {model: target.files}});
storeDispatch(actions.graph.setModel(target.files));
setFiles(target.files);
}
},
[globalDispatch]
[storeDispatch]
);
const {data, loading} = useRequest<BlobResponse>(files ? null : '/graph/graph');
......
import ChartPage, {WithChart} from '~/components/ChartPage';
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 {rem, transitionProps} from '~/utils/style';
import {AsideSection} from '~/components/Aside';
import Content from '~/components/Content';
......@@ -33,9 +33,10 @@ const StepSliderWrapper = styled.div`
}
+ .run-section {
border-top: 1px solid ${borderColor};
border-top: 1px solid var(--border-color);
margin-top: 0;
padding-top: ${rem(20)};
${transitionProps('border-color')}
}
&:empty + .run-section {
......
import {ActionTypes} from './types';
import type {Model} from './types';
export function setModel(model: Model) {
return {
type: ActionTypes.SET_MODEL,
model
};
}
import type {GraphActionTypes, GraphState} from './types';
import {ActionTypes} from './types';
const initState: GraphState = {
model: null
};
function graphReducer(state = initState, action: GraphActionTypes): GraphState {
switch (action.type) {
case ActionTypes.SET_MODEL:
return {
...state,
model: action.model
};
default:
return state;
}
}
export default graphReducer;
import type {RootState} from '../index';
export const model = (state: RootState) => state.graph.model;
export type Model = FileList | File[] | null;
export enum ActionTypes {
SET_MODEL = 'SET_MODEL'
}
export interface GraphState {
model: Model;
}
export type Page = keyof GraphState;
interface SetModelAction {
type: ActionTypes.SET_MODEL;
model: Model;
}
export type GraphActionTypes = SetModelAction;
import * as graphActions from './graph/actions';
import * as graphSelectors from './graph/selectors';
import * as runsActions from './runs/actions';
import * as runsSelectors from './runs/selectors';
import * as themeActions from './theme/actions';
import * as themeSelectors from './theme/selectors';
import {combineReducers, createStore} from 'redux';
import graphReducer from './graph/reducers';
import runsReducer from './runs/reducers';
import themeReducer from './theme/reducers';
const rootReducer = combineReducers({
graph: graphReducer,
theme: themeReducer,
runs: runsReducer
});
export default createStore(rootReducer);
export const selectors = {
graph: graphSelectors,
runs: runsSelectors,
theme: themeSelectors
};
export const actions = {
graph: graphActions,
runs: runsActions,
theme: themeActions
};
export type RootState = ReturnType<typeof rootReducer>;
import type {Page, Runs} from './types';
import {ActionTypes} from './types';
export function setSelectedRuns(page: Page, runs: Runs) {
return {
type: ActionTypes.SET_SELECTED_RUNS,
page,
runs
};
}
import type {RunsActionTypes, RunsState} from './types';
import {ActionTypes} from './types';
const initState: RunsState = {
scalar: [],
histogram: [],
image: [],
audio: [],
'pr-curve': []
};
function runsReducer(state = initState, action: RunsActionTypes): RunsState {
switch (action.type) {
case ActionTypes.SET_SELECTED_RUNS:
return {
...state,
[action.page]: action.runs
};
default:
return state;
}
}
export default runsReducer;
import type {Page} from './types';
import type {RootState} from '../index';
export const getRunsByPage = (page: Page) => (state: RootState) => state.runs[page];
export type Runs = string[];
export enum ActionTypes {
SET_SELECTED_RUNS = 'SET_SELECTED_RUNS'
}
export interface RunsState {
scalar: Runs;
histogram: Runs;
image: Runs;
audio: Runs;
'pr-curve': Runs;
}
export type Page = keyof RunsState;
interface SetSelectedRunsAction {
type: ActionTypes.SET_SELECTED_RUNS;
page: Page;
runs: Runs;
}
export type RunsActionTypes = SetSelectedRunsAction;
import {ActionTypes} from './types';
import type {Theme} from './types';
export function setTheme(theme: Theme) {
return {
type: ActionTypes.SET_THEME,
theme
};
}
import type {ThemeActionTypes, ThemeState} from './types';
import {ActionTypes} from './types';
import {theme} from '~/utils/theme';
const initState: ThemeState = {
theme
};
function themeReducer(state = initState, action: ThemeActionTypes): ThemeState {
switch (action.type) {
case ActionTypes.SET_THEME:
return {
...state,
theme: action.theme
};
default:
return state;
}
}
export default themeReducer;
import type {RootState} from '../index';
export const theme = (state: RootState) => state.theme.theme;
import type {Theme} from '~/utils/theme';
export type {Theme} from '~/utils/theme';
export enum ActionTypes {
SET_THEME = 'SET_THEME'
}
export interface ThemeState {
theme: Theme;
}
interface SetThemeAction {
type: ActionTypes.SET_THEME;
theme: Theme;
}
export type ThemeActionTypes = SetThemeAction;
import {colors} from '~/utils/theme';
import {format} from 'd3-format';
import {primaryColor} from '~/utils/style';
export const color = [
'#2932E1',
......@@ -44,6 +44,7 @@ export const colorAlt = [
export const title = {
textStyle: {
color: '#000',
fontSize: 16,
fontWeight: 'bold'
},
......@@ -54,6 +55,10 @@ export const title = {
export const tooltip = {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderColor: 'rgba(0, 0, 0, 0.75)',
textStyle: {
color: '#fff'
},
hideDelay: 100,
enterable: false,
axisPointer: {
......@@ -62,11 +67,11 @@ export const tooltip = {
show: true
},
lineStyle: {
color: '#2932E1',
color: colors.primary.default,
type: 'dashed'
},
crossStyle: {
color: '#2932E1',
color: colors.primary.default,
type: 'dashed'
}
}
......@@ -139,6 +144,10 @@ export const yAxis = {
type: 'value',
name: '',
splitNumber: 4,
nameTextStyle: {
fontSize: 12,
color: '#666'
},
axisLine: {
lineStyle: {
color: '#CCC'
......@@ -163,7 +172,7 @@ export const series = {
hoverAnimation: false,
animationDuration: 100,
lineStyle: {
color: primaryColor,
color: colors.primary.default,
width: 1.5
}
};
......@@ -4,6 +4,7 @@ import 'react-toastify/dist/ReactToastify.css';
import * as polished from 'polished';
import {colors, variables} from '~/utils/theme';
import {createGlobalStyle, keyframes} from 'styled-components';
import {css} from 'styled-components';
......@@ -17,7 +18,7 @@ export {
fontFace as fontFaceShortHand
} from 'polished';
const {math, size, lighten, darken, normalize, transitions, border, position} = polished;
const {math, size, normalize, transitions, border, position} = polished;
// sizes
const fontSize = '14px';
......@@ -31,35 +32,11 @@ export const asideWidth = rem(260);
export const borderRadius = '4px';
export const progressSpinnerSize = '20px';
// colors
export const primaryColor = '#2932E1';
export const dangerColor = '#FF3912';
export const primaryFocusedColor = lighten(0.08, primaryColor);
export const primaryActiveColor = lighten(0.12, primaryColor);
export const dangerFocusedColor = lighten(0.08, dangerColor);
export const dangerActiveColor = lighten(0.12, dangerColor);
export const selectedColor = '#1A73E8';
export const lightColor = '#F4F5FC';
export const lightFocusedColor = darken(0.03, lightColor);
export const lightActiveColor = darken(0.06, lightColor);
export const textColor = '#333';
export const textLightColor = '#666';
export const textLighterColor = '#999';
export const textInvertColor = '#FFF';
export const bodyBackgroundColor = '#F4F4F4';
export const primaryBackgroundColor = '#F2F6FF';
export const backgroundColor = '#FFF';
export const backgroundFocusedColor = '#F6F6F6';
export const borderColor = '#DDD';
export const borderFocusedColor = darken(0.15, borderColor);
export const borderActiveColor = darken(0.3, borderColor);
export const navbarBackgroundColor = '#1527C2';
export const navbarHoverBackgroundColor = lighten(0.05, navbarBackgroundColor);
export const navbarHighlightColor = '#596cd6';
export const progressBarColor = '#FFF';
export const maskColor = 'rgba(255, 255, 255, 0.8)';
export const tooltipBackgroundColor = 'rgba(0, 0, 0, 0.6)';
export const tooltipTextColor = '#FFF';
// shims
// TODO: remove and use colors in theme instead
export const primaryColor = colors.primary.default;
export const primaryFocusedColor = colors.primary.focused;
export const primaryActiveColor = colors.primary.active;
// transitions
export const duration = '75ms';
......@@ -72,12 +49,12 @@ export const sameBorder = (
| number
| {width?: string | number; type?: string; color?: string; radius?: string | boolean},
type = 'solid',
color = borderColor,
color = 'var(--border-color)',
radius?: string | boolean
) => {
if ('object' === typeof width) {
type = width.type ?? 'solid';
color = width.color ?? borderColor;
color = width.color ?? 'var(--border-color)';
radius = width.radius === true ? borderRadius : width.radius;
width = width.width ?? '1px';
}
......@@ -99,16 +76,16 @@ export const transitionProps = (props: string | string[], args?: string | {durat
export const link = css`
a {
color: ${primaryColor};
color: var(--primary-color);
cursor: pointer;
${transitionProps('color')};
&:hover {
color: ${primaryFocusedColor};
color: var(--primary-focused-color);
}
&:active {
color: ${primaryActiveColor};
color: var(--primary-active-color);
}
}
`;
......@@ -130,6 +107,8 @@ export type WithStyled = {
export const GlobalStyle = createGlobalStyle`
${normalize}
${variables}
html {
font-size: ${fontSize};
font-family: 'Merriweather Sans', Helvetica, Arial, sans-serif;
......@@ -140,8 +119,9 @@ export const GlobalStyle = createGlobalStyle`
html,
body {
height: 100%;
background-color: ${bodyBackgroundColor};
color: ${textColor};
background-color: var(--body-background-color);
color: var(--text-color);
${transitionProps(['background-color', 'color'])}
}
a {
......@@ -162,19 +142,21 @@ export const GlobalStyle = createGlobalStyle`
}
#nprogress .bar {
background: ${progressBarColor};
background-color: var(--progress-bar-color);
z-index: 99999;
${position('fixed', 0, null, null, 0)}
${size('2px', '100%')}
${transitionProps('background-color')}
}
#nprogress .peg {
display: block;
${position('absolute', null, 0, null, null)}
${size('100%', rem(100))}
box-shadow: 0 0 rem(10) ${progressBarColor}, 0 0 ${rem(5)} ${progressBarColor};
box-shadow: 0 0 rem(10) var(--progress-bar-color), 0 0 ${rem(5)} var(--progress-bar-color);
opacity: 1;
transform: rotate(3deg) translate(0px, -${rem(4)});
${transitionProps('box-shadow')}
}
#nprogress .spinner {
......@@ -188,11 +170,13 @@ export const GlobalStyle = createGlobalStyle`
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: ${progressBarColor};
border-left-color: ${progressBarColor};
border-top-color: var(--progress-bar-color);
border-left-color: var(--progress-bar-color);
border-radius: 50%;
animation: ${spinner} 400ms linear infinite;
${transitionProps('border-color')}
}
.nprogress-custom-parent {
......@@ -213,7 +197,8 @@ export const GlobalStyle = createGlobalStyle`
}
.Toastify__toast--default {
color: ${textColor};
color: var(--text-color);
${transitionProps('color')}
}
.Toastify__toast-body {
......@@ -223,10 +208,11 @@ export const GlobalStyle = createGlobalStyle`
[data-tippy-root] .tippy-box {
z-index: 10002;
color: ${textColor};
background-color: ${backgroundColor};
box-shadow: 0 0 10px 0 rgba(0,0,0,0.10);
color: var(--text-color);
background-color: var(--background-color);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
border-radius: ${borderRadius};
${transitionProps(['color', 'background-color'])}
> .tippy-content {
padding: 0;
......@@ -234,22 +220,26 @@ export const GlobalStyle = createGlobalStyle`
display: flow-root;
}
> .tippy-arrow {
${transitionProps('border-color')}
}
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: ${backgroundColor};
border-top-color: var(--background-color);
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: ${backgroundColor};
border-bottom-color: var(--background-color);
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: ${backgroundColor};
border-left-color: var(--background-color);
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: ${backgroundColor};
border-right-color: var(--background-color);
}
&[data-theme~='tooltip'] {
color: ${tooltipTextColor};
background-color: ${tooltipBackgroundColor};
color: var(--tooltip-text-color);
background-color: var(--tooltip-background-color);
box-shadow: none;
> .tippy-content {
......@@ -257,16 +247,16 @@ export const GlobalStyle = createGlobalStyle`
}
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: ${tooltipBackgroundColor};
border-top-color: var(--tooltip-background-color);
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: ${tooltipBackgroundColor};
border-bottom-color: var(--tooltip-background-color);
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: ${tooltipBackgroundColor};
border-left-color: var(--tooltip-background-color);
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: ${tooltipBackgroundColor};
border-right-color: var(--tooltip-background-color);
}
}
}
......
import {darken, lighten} from 'polished';
import {css} from 'styled-components';
import kebabCase from 'lodash/kebabCase';
export type Theme = 'light' | 'dark';
export const THEME: Theme | undefined = import.meta.env.SNOWPACK_PUBLIC_THEME;
export const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
export const theme = THEME || (matchMedia.matches ? 'dark' : 'light');
export const colors = {
primary: {
default: '#2932e1',
focused: lighten(0.08, '#2932e1'),
active: lighten(0.12, '#2932e1'),
text: '#fff'
},
danger: {
default: '#ff3912',
focused: lighten(0.08, '#ff3912'),
active: lighten(0.12, '#ff3912'),
text: '#fff'
}
} as const;
export const themes = {
light: {
textColor: '#333',
textLightColor: '#666',
textLighterColor: '#999',
textInvertColor: '#fff',
backgroundColor: '#fff',
backgroundFocusedColor: '#f6f6f6',
bodyBackgroundColor: '#f4f4f4',
borderColor: '#ddd',
borderFocusedColor: darken(0.15, '#ddd'),
borderActiveColor: darken(0.3, '#ddd'),
navbarTextColor: '#fff',
navbarBackgroundColor: '#1527c2',
navbarHoverBackgroundColor: lighten(0.05, '#1527c2'),
navbarHighlightColor: '#596cd6',
tagBackgroundColor: '#f4f5fc',
tagFocusedBackgroundColor: darken(0.03, '#f4f5fc'),
tagActiveBackgroundColor: darken(0.06, '#f4f5fc'),
inputBackgroundColor: '#fff',
selectSelectedTextColor: '#1a73e8',
sliderRailColor: '#dbdeeb',
sliderGripperColor: '#fff',
modelHeaderBackgroundColor: '#eee',
codeColor: '#666',
codeBackgroundColor: 'rgba(216, 216, 216, 0.5)',
audioBackgroundColor: '#f2f6ff',
tooltipTextColor: '#fff',
tooltipBackgroundColor: 'rgba(0, 0, 0, 0.6)',
progressBarColor: '#fff',
maskColor: 'rgba(255, 255, 255, 0.8)',
graphUploaderBackgroundColor: '#f9f9f9',
graphUploaderActiveBackgroundColor: '#f2f6ff',
graphCopyrightColor: '#ddd',
graphCopyrightLogoFilter: 'opacity(25%)'
},
dark: {
textColor: '#cfcfd1',
textLightColor: '#575757',
textLighterColor: '#757575',
textInvertColor: '#000',
backgroundColor: '#1d1d1f',
backgroundFocusedColor: '#333',
bodyBackgroundColor: '#121214',
borderColor: '#3f3f42',
borderFocusedColor: lighten(0.15, '#3f3f42'),
borderActiveColor: lighten(0.3, '#3f3f42'),
navbarTextColor: '#fff',
navbarBackgroundColor: '#262629',
navbarHoverBackgroundColor: lighten(0.05, '#262629'),
navbarHighlightColor: '#fff',
tagBackgroundColor: '#333',
tagFocusedBackgroundColor: lighten(0.3, '#333'),
tagActiveBackgroundColor: lighten(0.4, '#333'),
inputBackgroundColor: '#262629',
selectSelectedTextColor: '#1a73e8',
sliderRailColor: '#727275',
sliderGripperColor: '#cfcfd1',
modelHeaderBackgroundColor: '#303033',
codeColor: '#cfcfd1',
codeBackgroundColor: '#3f3f42',
audioBackgroundColor: '#303033',
tooltipTextColor: '#d1d1d1',
tooltipBackgroundColor: '#292929',
progressBarColor: '#fff',
maskColor: 'rgba(0, 0, 0, 0.8)',
graphUploaderBackgroundColor: '#262629',
graphUploaderActiveBackgroundColor: '#303033',
graphCopyrightColor: '#565657',
graphCopyrightLogoFilter: 'invert(35%) sepia(5%) saturate(79%) hue-rotate(202deg) brightness(88%) contrast(86%)'
}
} as const;
function generateColorVariables(color: typeof colors) {
return Object.entries(color)
.map(([name, variant]) =>
Object.entries(variant)
.map(([key, value]) => {
if (key === 'default') {
return `--${kebabCase(name)}-color: ${value};`;
}
return `--${kebabCase(name)}-${kebabCase(key)}-color: ${value};`;
})
.join('\n')
)
.join('\n');
}
function generateThemeVariables(theme: Record<string, string>) {
return Object.entries(theme)
.map(([key, value]) => `--${kebabCase(key)}: ${value};`)
.join('\n');
}
const mediaQuery = css`
@media (prefers-color-scheme: dark) {
${generateThemeVariables(themes.dark)}
}
`;
export const variables = css`
:root {
${generateColorVariables(colors)}
${generateThemeVariables(themes[THEME || 'light'])}
${(!THEME && mediaQuery) || ''}
}
`;
......@@ -34,7 +34,7 @@
"devDependencies": {
"@types/express": "4.17.8",
"@types/mkdirp": "1.0.1",
"@types/node": "14.6.4",
"@types/node": "14.10.3",
"@types/node-fetch": "2.5.7",
"@types/rimraf": "3.0.0",
"cpy-cli": "3.1.1",
......
......@@ -41,8 +41,8 @@
},
"devDependencies": {
"@types/express": "4.17.8",
"@types/faker": "4.1.12",
"@types/node": "14.6.4",
"@types/faker": "5.1.0",
"@types/node": "14.10.3",
"cpy-cli": "3.1.1",
"rimraf": "3.0.2",
"ts-node": "9.0.0",
......
......@@ -40,16 +40,17 @@
"pako": "1.0.11"
},
"devDependencies": {
"autoprefixer": "9.8.6",
"autoprefixer": "10.0.0",
"copy-webpack-plugin": "6.1.0",
"css-loader": "4.2.2",
"css-loader": "4.3.0",
"html-webpack-plugin": "4.4.1",
"mini-css-extract-plugin": "0.11.0",
"postcss-loader": "3.0.0",
"mini-css-extract-plugin": "0.11.2",
"postcss": "8.0.5",
"postcss-loader": "4.0.2",
"rimraf": "3.0.2",
"sass": "1.26.10",
"sass-loader": "10.0.2",
"terser": "5.3.0",
"terser": "5.3.1",
"webpack": "4.44.1",
"webpack-cli": "3.3.12"
},
......
......@@ -62,6 +62,8 @@ host.BrowserHost = class {
return this._view.toggleNames(data);
case 'toggle-direction':
return this._view.toggleDirection(data);
case 'toggle-theme':
return this._view.toggleTheme(data);
case 'export':
return this._view.export(`${document.title}.${data}`);
case 'change-graph':
......
......@@ -45,6 +45,10 @@ text {
font-size: 11px;
text-rendering: geometricPrecision;
fill: #000;
.dark & {
fill: #CFCFD1;
}
}
.node-item {
......@@ -184,6 +188,10 @@ text {
.node-attribute path {
fill: #fff;
stroke-width: 0;
.dark & {
fill: #262629;
}
}
.graph-item-input {
......
......@@ -105,6 +105,10 @@ view.View = class {
return this._showHorizontal;
}
toggleTheme(theme) {
this._host.document.body.className = theme;
}
_reload() {
this._host.status('loading');
if (this._model && this._activeGraph) {
......
......@@ -46,7 +46,7 @@
"devDependencies": {
"@types/enhanced-resolve": "3.0.6",
"@types/express": "4.17.8",
"@types/node": "14.6.4",
"@types/node": "14.10.3",
"@visualdl/mock": "2.0.9",
"cross-env": "7.0.2",
"nodemon": "2.0.4",
......
此差异已折叠。
......@@ -18,6 +18,7 @@ build_frontend() {
API_URL="{{API_URL}}" \
API_TOKEN_KEY="{{API_TOKEN_KEY}}" \
TELEMETRY_ID="{{TELEMETRY_ID}}" \
THEME="{{THEME}}" \
PATH="$PATH" \
./scripts/build.sh
......
......@@ -90,8 +90,8 @@ def create_app(args):
PUBLIC_PATH=public_path,
BASE_URI=public_path,
API_URL=api_path,
API_TOKEN_KEY='',
TELEMETRY_ID='63a600296f8a71f576c4806376a9245b' if args.telemetry else ''
TELEMETRY_ID='63a600296f8a71f576c4806376a9245b' if args.telemetry else '',
THEME='' if args.theme is None else args.theme
)
@app.route('/')
......
......@@ -26,6 +26,8 @@ default_cache_timeout = 20
default_public_path = '/app'
default_product = 'normal'
support_themes = ['light', 'dark']
class DefaultArgs(object):
def __init__(self, args):
......@@ -40,6 +42,7 @@ class DefaultArgs(object):
self.model = args.get('model', '')
self.product = args.get('product', default_product)
self.telemetry = args.get('telemetry', True)
self.theme = args.get('theme', None)
self.dest = args.get('dest', '')
self.behavior = args.get('behavior', '')
......@@ -65,6 +68,11 @@ def validate_args(args):
logger.error('Public path should always start with a `/`.')
sys.exit(-1)
# theme not support
if args.theme is not None and args.theme not in support_themes:
logger.error('Theme {} is not support.'.format(args.theme))
sys.exit(-1)
def format_args(args):
# set default public path according to API mode option
......@@ -101,6 +109,7 @@ class ParseArgs(object):
self.model = args.model
self.product = args.product
self.telemetry = args.telemetry
self.theme = args.theme
self.dest = args.dest
self.behavior = args.behavior
......@@ -194,6 +203,14 @@ def parse_args():
default=True,
help="disable telemetry"
)
parser.add_argument(
"--theme",
action="store",
dest="theme",
default=None,
choices=support_themes,
help="set theme"
)
parser.add_argument(
'dest',
nargs='?',
......
......@@ -21,6 +21,13 @@ from flask import (Response, send_from_directory)
class Template(object):
extname = [".html", ".js", ".css"]
defaults = {
'PUBLIC_PATH': '/app',
'API_TOKEN_KEY': '',
'TELEMETRY_ID': '',
'THEME': ''
}
def __init__(self, path, **context):
if not os.path.exists(path):
raise Exception("template file does not exist.")
......@@ -33,7 +40,9 @@ class Template(object):
rel_path = os.path.relpath(file_path, path).replace(os.path.sep, '/')
with open(file_path, "r", encoding="UTF-8") as f:
content = f.read()
for key, value in context.items():
envs = self.defaults.copy()
envs.update(context)
for key, value in envs.items():
content = content.replace("{{" + key + "}}", value)
self.files[rel_path] = content, mimetypes.guess_type(file)[0]
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册