first commit
27
.babelrc
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"modules": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
|
"@babel/plugin-syntax-import-meta",
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/plugin-proposal-json-strings",
|
||||||
|
"@babel/plugin-proposal-export-namespace-from",
|
||||||
|
"@babel/plugin-proposal-export-default-from",
|
||||||
|
"@babel/plugin-proposal-do-expressions"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"development": {
|
||||||
|
"plugins": [
|
||||||
|
"react-hot-loader/babel"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
.eslintrc.yml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
node: true
|
||||||
|
jest/globals: true
|
||||||
|
es6: true
|
||||||
|
|
||||||
|
parser: babel-eslint
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- import
|
||||||
|
- react
|
||||||
|
- jest
|
||||||
|
|
||||||
|
|
||||||
|
extends:
|
||||||
|
- eslint:recommended
|
||||||
|
- plugin:import/errors
|
||||||
|
- plugin:react/recommended
|
||||||
|
|
||||||
|
|
||||||
|
# globals:
|
||||||
|
# Promise: true
|
||||||
|
|
||||||
|
rules:
|
||||||
|
quotes: ["error", "single"]
|
||||||
|
strict: ["error", "never"]
|
||||||
|
no-console: "warn"
|
||||||
|
react/jsx-uses-react: "error"
|
||||||
|
react/jsx-uses-vars: "error"
|
||||||
|
react/react-in-jsx-scope: "error"
|
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
*.*~
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
public/
|
||||||
|
deploy_ghpages.sh
|
||||||
|
tags
|
||||||
|
*.log
|
BIN
assets/yacd-128.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/yacd-64.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
93
package.json
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
{
|
||||||
|
"name": "yacd",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Yet another Clash dashboard",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint src",
|
||||||
|
"dll": "webpack --config webpack.dll.config.js",
|
||||||
|
"start": "NODE_ENV=development node server.js",
|
||||||
|
"clean": "rm -rf public && mkdir public",
|
||||||
|
"build:webpack": "NODE_ENV=production webpack --config webpack.config.js --progress --colors",
|
||||||
|
"build:assets": "cp -r assets/* public/",
|
||||||
|
"build": "npm-run-all clean build:webpack build:assets",
|
||||||
|
"pretty": "prettier --single-quote --write 'src/**/*.{js,scss}'"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,scss}": [
|
||||||
|
"prettier --single-quote --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"author": "Han Haishan <haishanhan@gmail.com> (htttp://haishan.me)",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/polyfill": "^7.0.0",
|
||||||
|
"chart.js": "^2.7.3",
|
||||||
|
"chat.js": "^1.0.2",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"history": "^4.7.2",
|
||||||
|
"modern-normalize": "^0.5.0",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^16.5.2",
|
||||||
|
"react-dom": "^16.5.2",
|
||||||
|
"react-loadable": "^5.5.0",
|
||||||
|
"react-modal": "^3.6.1",
|
||||||
|
"react-redux": "^5.0.6",
|
||||||
|
"react-router-dom": "^4.2.2",
|
||||||
|
"react-router-redux": "^5.0.0-alpha.9",
|
||||||
|
"redux": "^4.0.1",
|
||||||
|
"redux-logger": "^3.0.6",
|
||||||
|
"redux-thunk": "^2.2.0",
|
||||||
|
"whatwg-fetch": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.1.2",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||||
|
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||||
|
"@babel/preset-env": "^7.1.0",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"autoprefixer": "^9.2.1",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"babel-loader": "^8.0.4",
|
||||||
|
"css-loader": "^1.0.0",
|
||||||
|
"cssnano": "^4.1.5",
|
||||||
|
"eslint": "^5.7.0",
|
||||||
|
"eslint-plugin-import": "2.14.0",
|
||||||
|
"eslint-plugin-jest": "^21.25.1",
|
||||||
|
"eslint-plugin-react": "7.11.1",
|
||||||
|
"file-loader": "^2.0.0",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"husky": "^1.0.1",
|
||||||
|
"lint-staged": "^7.3.0",
|
||||||
|
"mini-css-extract-plugin": "^0.4.3",
|
||||||
|
"node-sass": "^4.9.4",
|
||||||
|
"npm-run-all": "^4.1.3",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"prettier": "^1.14.3",
|
||||||
|
"react-hot-loader": "^4.2.0",
|
||||||
|
"sass-loader": "^7.0.1",
|
||||||
|
"style-loader": "^0.23.0",
|
||||||
|
"svg-sprite-loader": "^4.1.2",
|
||||||
|
"uglifyjs-webpack-plugin": "^2.0.1",
|
||||||
|
"webpack": "^4.21.0",
|
||||||
|
"webpack-bundle-analyzer": "^3.0.3",
|
||||||
|
"webpack-cli": "^3.1.2",
|
||||||
|
"webpack-dev-middleware": "3.4.0",
|
||||||
|
"webpack-hot-middleware": "^2.22.2"
|
||||||
|
}
|
||||||
|
}
|
78
server.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('./webpack.config');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
// const WebpackDevServer = require('webpack-dev-server');
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const devMiddleware = require('webpack-dev-middleware');
|
||||||
|
const hotMiddleware = require('webpack-hot-middleware');
|
||||||
|
|
||||||
|
const { PORT } = process.env;
|
||||||
|
const port = PORT ? Number(PORT) : 3000;
|
||||||
|
|
||||||
|
config.entry.app.unshift(
|
||||||
|
// activate HMR for React
|
||||||
|
// 'react-hot-loader/patch',
|
||||||
|
|
||||||
|
'webpack-hot-middleware/client'
|
||||||
|
// // bundle the client for webpack-dev-server
|
||||||
|
// // and connect to the provided endpoint
|
||||||
|
// 'webpack-dev-server/client?http://0.0.0.0:' + port,
|
||||||
|
// // bundle the client for hot reloading
|
||||||
|
// // only- means to only hot reload for successful updates
|
||||||
|
// 'webpack/hot/only-dev-server'
|
||||||
|
);
|
||||||
|
config.plugins.push(
|
||||||
|
// enable HMR globally
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
// prints more readable module names in the browser console on HMR updates
|
||||||
|
new webpack.NamedModulesPlugin()
|
||||||
|
);
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
// webpack-dev-server config
|
||||||
|
const publicPath = config.output.publicPath;
|
||||||
|
const stats = {
|
||||||
|
colors: true,
|
||||||
|
cached: false,
|
||||||
|
cachedAssets: false,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// webpack-dev-server options
|
||||||
|
// const options = {
|
||||||
|
// hotOnly: true,
|
||||||
|
// host: '0.0.0.0',
|
||||||
|
// contentBase: path.join(__dirname, 'public'),
|
||||||
|
// publicPath,
|
||||||
|
// stats,
|
||||||
|
// overlay: {
|
||||||
|
// warnings: true,
|
||||||
|
// errors: true
|
||||||
|
// },
|
||||||
|
// historyApiFallback: true
|
||||||
|
// };
|
||||||
|
|
||||||
|
const options = { publicPath, stats };
|
||||||
|
|
||||||
|
app.use(devMiddleware(compiler, options));
|
||||||
|
app.use(hotMiddleware(compiler));
|
||||||
|
|
||||||
|
app.use('*', (req, res, next) => {
|
||||||
|
const filename = path.join(compiler.outputPath, 'index.html');
|
||||||
|
compiler.outputFileSystem.readFile(filename, (err, result) => {
|
||||||
|
if (err) return next(err);
|
||||||
|
|
||||||
|
res.set('content-type', 'text/html');
|
||||||
|
res.send(result);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, '0.0.0.0', () => {
|
||||||
|
console.log('\n>> Listening at http://0.0.0.0:' + port + '\n');
|
||||||
|
});
|
22
src/api/configs.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { getAPIURL } = require('../config');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchConfigs() {
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
return await fetch(apiURL.configs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConfigs(o) {
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
return await fetch(apiURL.configs, {
|
||||||
|
method: 'PUT',
|
||||||
|
// mode: 'cors',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(o)
|
||||||
|
});
|
||||||
|
}
|
64
src/api/logs.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { getAPIURL } = require('../config');
|
||||||
|
const textDecoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
const Size = 300;
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
logs: [],
|
||||||
|
size: Size,
|
||||||
|
updateCallback: null,
|
||||||
|
appendData(o) {
|
||||||
|
const now = new Date();
|
||||||
|
const time = now.toLocaleString('zh-Hans');
|
||||||
|
// mutate input param in place intentionally
|
||||||
|
o.time = time;
|
||||||
|
o.id = now - 0;
|
||||||
|
this.logs.unshift(o);
|
||||||
|
if (this.logs.length > this.size) this.logs.pop();
|
||||||
|
// TODO consider throttle this
|
||||||
|
if (this.updateCallback) this.updateCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function pump(reader) {
|
||||||
|
return reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
console.log('done');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = textDecoder.decode(value);
|
||||||
|
// console.log(t);
|
||||||
|
const l = t[t.length - 1];
|
||||||
|
let o;
|
||||||
|
try {
|
||||||
|
o = JSON.parse(t);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
'lastchar',
|
||||||
|
t.length,
|
||||||
|
' is r',
|
||||||
|
l === '\r',
|
||||||
|
' is n',
|
||||||
|
l === '\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
store.appendData(o);
|
||||||
|
return pump(reader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetched = false;
|
||||||
|
function fetchLogs() {
|
||||||
|
if (fetched) return store;
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
fetch(apiURL.logs).then(response => {
|
||||||
|
fetched = true;
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
pump(reader);
|
||||||
|
});
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchLogs };
|
50
src/api/proxies.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { getAPIURL } = require('../config');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
$ curl "http://127.0.0.1:8080/proxies/Proxy" -XPUT -d '{ "name": "ss3" }' -i
|
||||||
|
HTTP/1.1 400 Bad Request
|
||||||
|
Vary: Origin
|
||||||
|
Date: Tue, 16 Oct 2018 16:38:20 GMT
|
||||||
|
Content-Length: 56
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
{"error":"Selector update error: Proxy does not exist"}
|
||||||
|
|
||||||
|
~
|
||||||
|
$ curl "http://127.0.0.1:8080/proxies/GLOBAL" -XPUT -d '{ "name": "Proxy" }' -i
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Origin
|
||||||
|
Date: Tue, 16 Oct 2018 16:38:33 GMT
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function fetchProxies() {
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
const res = await fetch(apiURL.proxies);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestToSwitchProxy(name1, name2) {
|
||||||
|
const body = { name: name2 };
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
const url = `${apiURL.proxies}/${name1}`;
|
||||||
|
return await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestDelayForProxy(name) {
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
const qs = `timeout=5000&url=http://www.google.com/generate_204`;
|
||||||
|
const url = `${apiURL.proxies}/${name}/delay?${qs}`;
|
||||||
|
return await fetch(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchProxies, requestToSwitchProxy, requestDelayForProxy };
|
67
src/api/traffic.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { getAPIURL } = require('../config');
|
||||||
|
const textDecoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
const Size = 150;
|
||||||
|
|
||||||
|
const traffic = {
|
||||||
|
labels: Array(Size),
|
||||||
|
// labels: [],
|
||||||
|
up: Array(Size),
|
||||||
|
down: Array(Size),
|
||||||
|
|
||||||
|
size: Size,
|
||||||
|
subscribers: [],
|
||||||
|
appendData(o) {
|
||||||
|
this.up.push(o.up);
|
||||||
|
this.down.push(o.down);
|
||||||
|
const t = new Date();
|
||||||
|
const l = '' + t.getMinutes() + t.getSeconds();
|
||||||
|
this.labels.push(l);
|
||||||
|
if (this.up.length > this.size) this.up.shift();
|
||||||
|
if (this.down.length > this.size) this.down.shift();
|
||||||
|
if (this.labels.length > this.size) this.labels.shift();
|
||||||
|
|
||||||
|
this.subscribers.forEach(f => f(o));
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe(listener) {
|
||||||
|
const me = this;
|
||||||
|
this.subscribers.push(listener);
|
||||||
|
return function unsubscribe() {
|
||||||
|
const idx = me.subscribers.indexOf(listener);
|
||||||
|
me.subscribers.splice(idx, 1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function pump(reader) {
|
||||||
|
return reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
console.log('done');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = textDecoder.decode(value);
|
||||||
|
// console.log('response', t);
|
||||||
|
const o = JSON.parse(t);
|
||||||
|
|
||||||
|
traffic.appendData(o);
|
||||||
|
|
||||||
|
return pump(reader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetched = false;
|
||||||
|
function fetchData() {
|
||||||
|
if (fetched) return traffic;
|
||||||
|
const apiURL = getAPIURL();
|
||||||
|
fetch(apiURL.traffic).then(response => {
|
||||||
|
fetched = true;
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
pump(reader);
|
||||||
|
});
|
||||||
|
return traffic;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchData };
|
21
src/app.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import 'modern-normalize/modern-normalize.css';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import createHistory from 'history/createHashHistory';
|
||||||
|
// import createHistory from 'history/createBrowserHistory';
|
||||||
|
import configureStore from './store/configureStore';
|
||||||
|
|
||||||
|
import Root from './components/Root';
|
||||||
|
|
||||||
|
const history = createHistory();
|
||||||
|
const store = configureStore(history);
|
||||||
|
const props = { history, store };
|
||||||
|
|
||||||
|
Modal.setAppElement('#app');
|
||||||
|
const render = (Component, props = {}) => {
|
||||||
|
ReactDOM.render(<Component {...props} />, document.getElementById('app'));
|
||||||
|
};
|
||||||
|
|
||||||
|
render(Root, props);
|
83
src/components/APIConfig.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Input from 'c/Input';
|
||||||
|
import Button from 'c/Button';
|
||||||
|
|
||||||
|
import s0 from './APIConfig.module.scss';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { getClashAPIConfig, updateClashAPIConfig } from 'd/app';
|
||||||
|
|
||||||
|
const mapStateToProps = s => {
|
||||||
|
const apiConfig = getClashAPIConfig(s);
|
||||||
|
return { apiConfig };
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
updateClashAPIConfig: bindActionCreators(updateClashAPIConfig, dispatch)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class APIConfig extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
apiConfig: PropTypes.object.isRequired,
|
||||||
|
updateClashAPIConfig: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hostname: this.props.apiConfig.hostname,
|
||||||
|
port: this.props.apiConfig.port
|
||||||
|
};
|
||||||
|
|
||||||
|
handleInputOnChange = e => {
|
||||||
|
const target = e.target;
|
||||||
|
const { name } = target;
|
||||||
|
|
||||||
|
let value;
|
||||||
|
if (name === 'port') {
|
||||||
|
if (Number(target.value) < 0 || Number(target.value) > 65535) return;
|
||||||
|
}
|
||||||
|
value = target.value.trim();
|
||||||
|
if (value === '') return;
|
||||||
|
this.setState({ [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleConfirmOnClick = e => {
|
||||||
|
const { hostname, port } = this.state;
|
||||||
|
this.props.updateClashAPIConfig(hostname, port);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hostname, port } = this.state;
|
||||||
|
return (
|
||||||
|
<div className={s0.root}>
|
||||||
|
<div className={s0.header}>RESTful API config for Clash</div>
|
||||||
|
<div className={s0.body}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="hostname"
|
||||||
|
value={hostname}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
name="port"
|
||||||
|
value={port}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={s0.footer}>
|
||||||
|
<Button label="Confirm" onClick={this.handleConfirmOnClick} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(APIConfig);
|
25
src/components/APIConfig.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.root {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 10px 0 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
div:nth-child(2) {
|
||||||
|
width: 120px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 20px 0 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
52
src/components/APIDiscovery.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Modal from 'c/Modal';
|
||||||
|
import APIConfig from 'c/APIConfig';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { closeModal } from 'd/modals';
|
||||||
|
import { fetchConfigs } from 'd/configs';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const modals = state.modals;
|
||||||
|
return { modals };
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
closeModal: bindActionCreators(closeModal, dispatch),
|
||||||
|
fetchConfigs: bindActionCreators(fetchConfigs, dispatch)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class APIDiscovery extends Component {
|
||||||
|
// static propTypes = {
|
||||||
|
// isOpen: PropTypes.bool.isRequired,
|
||||||
|
// onRequestClose: PropTypes.func.isRequired
|
||||||
|
// };
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchConfigs();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { modals, closeModal } = this.props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={modals.apiConfig}
|
||||||
|
shouldCloseOnOverlayClick={false}
|
||||||
|
shouldCloseOnEsc={false}
|
||||||
|
onRequestClose={() => closeModal('apiConfig')}
|
||||||
|
>
|
||||||
|
<APIConfig />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(APIDiscovery);
|
25
src/components/Button.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import s0 from 'c/Button.module.scss';
|
||||||
|
|
||||||
|
class Button extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onClick: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<button className={s0.btn} onClick={this.props.onClick}>
|
||||||
|
{this.props.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
12
src/components/Button.module.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.btn {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
outline: none;
|
||||||
|
color: #ddd;
|
||||||
|
background: #606060;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
&:hover {
|
||||||
|
background: darken(#555, 3%);
|
||||||
|
}
|
||||||
|
}
|
165
src/components/Config.js
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { getConfigs, fetchConfigs, updateConfigs } from 'd/configs';
|
||||||
|
|
||||||
|
import ContentHeader from 'c/ContentHeader';
|
||||||
|
|
||||||
|
import Switch from 'c/Switch';
|
||||||
|
import ToggleSwitch from 'c/ToggleSwitch';
|
||||||
|
import Input from 'c/Input';
|
||||||
|
import s0 from 'c/Config.module.scss';
|
||||||
|
|
||||||
|
const optionsRule = [
|
||||||
|
{
|
||||||
|
label: 'Global',
|
||||||
|
value: 'Global'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rule',
|
||||||
|
value: 'Rule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Direct',
|
||||||
|
value: 'Direct'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const optionsLogLevel = [
|
||||||
|
{
|
||||||
|
label: 'info',
|
||||||
|
value: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'warning',
|
||||||
|
value: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'error',
|
||||||
|
value: 'error'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'debug',
|
||||||
|
value: 'debug'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
fetchConfigs,
|
||||||
|
updateConfigs
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = s => {
|
||||||
|
return {
|
||||||
|
configs: getConfigs(s)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return bindActionCreators(actions, dispatch);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Config extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
configs: PropTypes.object,
|
||||||
|
fetchConfigs: PropTypes.func,
|
||||||
|
updateConfigs: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchConfigs();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputOnChange = ev => {
|
||||||
|
const { configs } = this.props;
|
||||||
|
const target = ev.target;
|
||||||
|
const { name } = target;
|
||||||
|
|
||||||
|
let value;
|
||||||
|
switch (target.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
value = target.checked;
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
value = Number(target.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = target.value;
|
||||||
|
}
|
||||||
|
if (configs[name] === value) return;
|
||||||
|
this.props.updateConfigs({ [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { configs } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContentHeader title="Config" />
|
||||||
|
<div className={s0.root}>
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>HTTP Proxy Port</div>
|
||||||
|
<Input
|
||||||
|
name="port"
|
||||||
|
value={configs.port}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>SOCKS5 Proxy Port</div>
|
||||||
|
<Input
|
||||||
|
name="socket-port"
|
||||||
|
value={configs['socket-port']}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>Redir Port</div>
|
||||||
|
<Input
|
||||||
|
name="redir-port"
|
||||||
|
value={configs['redir-port']}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>Allow LAN</div>
|
||||||
|
<Switch
|
||||||
|
name="allow-lan"
|
||||||
|
checked={configs['allow-lan']}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>Mode</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
options={optionsRule}
|
||||||
|
name="mode"
|
||||||
|
value={configs.mode}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>Log Level</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
options={optionsLogLevel}
|
||||||
|
name="log-level"
|
||||||
|
value={configs['log-level']}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Config);
|
16
src/components/Config.module.scss
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.root {
|
||||||
|
padding: 10px 40px;
|
||||||
|
color: #ddd;
|
||||||
|
|
||||||
|
// display: flex;
|
||||||
|
// flex-wrap: wrap;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 340px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
// color: #aaa;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
20
src/components/ContentHeader.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import s0 from './ContentHeader.module.scss';
|
||||||
|
|
||||||
|
class ContentHeader extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
title: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={s0.root}>
|
||||||
|
<h1 className={s0.h1}>{this.props.title}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentHeader;
|
12
src/components/ContentHeader.module.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.root {
|
||||||
|
height: 76px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1 {
|
||||||
|
padding: 0 40px;
|
||||||
|
color: #ddd;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
}
|
32
src/components/Home.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
// import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import ContentHeader from 'c/ContentHeader';
|
||||||
|
import TrafficChart from 'c/TrafficChart';
|
||||||
|
import TrafficNow from 'c/TrafficNow';
|
||||||
|
import s0 from 'c/Home.module.scss';
|
||||||
|
|
||||||
|
class Home extends Component {
|
||||||
|
// static propTypes = {
|
||||||
|
// match: PropTypes.object
|
||||||
|
// };
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// const { match } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContentHeader title="Overview" />
|
||||||
|
<div className={s0.root}>
|
||||||
|
<div>
|
||||||
|
<TrafficNow />
|
||||||
|
</div>
|
||||||
|
<div className={s0.chart}>
|
||||||
|
<TrafficChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
6
src/components/Home.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.root {
|
||||||
|
padding: 10px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
}
|
22
src/components/Icon.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
|
const Icon = ({ id, width = 20, height = 20, className, ...props }) => {
|
||||||
|
const c = cx('icon', id, className);
|
||||||
|
const href = '#' + id;
|
||||||
|
return (
|
||||||
|
<svg className={c} width={width} height={height} {...props}>
|
||||||
|
<use xlinkHref={href} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Icon.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
className: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Icon;
|
61
src/components/Input.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import s0 from './Input.module.scss';
|
||||||
|
|
||||||
|
class Input extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
type: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
name: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
type: 'number',
|
||||||
|
placeholder: 'Please input'
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
value: this.props.value,
|
||||||
|
lastValueFromProps: this.props.value
|
||||||
|
};
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
if (props.value !== state.lastValueFromProps) {
|
||||||
|
return {
|
||||||
|
lastValueFromProps: props.value,
|
||||||
|
value: props.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputOnChange = e => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const int = parseInt(value, 10);
|
||||||
|
if (int < 0 || int > 65535) return;
|
||||||
|
this.setState({ value: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { onChange, name, type, placeholder } = this.props;
|
||||||
|
const { value } = this.state;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className={s0.input}
|
||||||
|
name={name}
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onBlur={onChange}
|
||||||
|
onChange={this.handleInputOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input;
|
23
src/components/Input.module.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.input {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-color: #2d2d30;
|
||||||
|
background-image: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #3f3f3f;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #c1c1c1;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: inherit;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0 15px;
|
||||||
|
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
15
src/components/Loadable.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import L from 'react-loadable';
|
||||||
|
|
||||||
|
import Loading from './Loading';
|
||||||
|
|
||||||
|
// error will passed if the component failed to load
|
||||||
|
// const Loading = ({ error }) => <div>loading</div>;
|
||||||
|
|
||||||
|
const Loadable = opts =>
|
||||||
|
L({
|
||||||
|
loading: Loading,
|
||||||
|
delay: 200,
|
||||||
|
...opts
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Loadable;
|
14
src/components/Loading.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './Loading.module.scss';
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
return (
|
||||||
|
<div className={style.loading}>
|
||||||
|
<div className={style.left + ' ' + style.circle} />
|
||||||
|
<div className={style.right + ' ' + style.circle} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
50
src/components/Loading.module.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
$color1: #2a477a;
|
||||||
|
$color2: #dddddd;
|
||||||
|
|
||||||
|
@keyframes moveRight {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveLeft {
|
||||||
|
0% {
|
||||||
|
transform: translate(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 30vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
background-color: $color1;
|
||||||
|
left: 50%;
|
||||||
|
animation: moveRight 1s ease-in-out 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
background-color: $color2;
|
||||||
|
right: 50%;
|
||||||
|
animation: moveLeft 1s ease-in-out 0s infinite alternate;
|
||||||
|
}
|
89
src/components/Logs.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Icon from 'c/Icon';
|
||||||
|
import ContentHeader from 'c/ContentHeader';
|
||||||
|
import { fetchLogs } from '../api/logs';
|
||||||
|
|
||||||
|
import yacd from 's/yacd.svg';
|
||||||
|
|
||||||
|
import s0 from 'c/Logs.module.scss';
|
||||||
|
const colors = {
|
||||||
|
debug: '#8a8a8a',
|
||||||
|
info: '#147d14',
|
||||||
|
warning: '#b99105',
|
||||||
|
error: '#c11c1c'
|
||||||
|
};
|
||||||
|
class LogLine extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
time: PropTypes.string,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
payload: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { time, type, payload } = this.props;
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<div className={s0.logMeta}>
|
||||||
|
<div className={s0.logTime}>{time}</div>
|
||||||
|
<div className={s0.logType} style={{ backgroundColor: colors[type] }}>
|
||||||
|
{type}
|
||||||
|
</div>
|
||||||
|
<div className={s0.logText}>{payload}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logs extends Component {
|
||||||
|
// static propTypes = {
|
||||||
|
// isOpen: PropTypes.bool.isRequired,
|
||||||
|
// onRequestClose: PropTypes.func.isRequired
|
||||||
|
// };
|
||||||
|
state = {
|
||||||
|
logs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
handle = null;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.handle = fetchLogs();
|
||||||
|
this.setState({ logs: this.handle.logs });
|
||||||
|
this.handle.updateCallback = () => {
|
||||||
|
this.setState({ logs: this.handle.logs });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.handle.updateCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { logs } = this.state;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContentHeader title="Logs" />
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className={s0.logPlaceholder}>
|
||||||
|
<div>
|
||||||
|
<Icon id={yacd.id} width={200} height={200} />
|
||||||
|
</div>
|
||||||
|
<div>No logs yet, hang tight...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={s0.logs}>
|
||||||
|
<ul className={s0.logUl}>
|
||||||
|
{logs.map(l => (
|
||||||
|
<LogLine key={l.id} {...l} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logs;
|
64
src/components/Logs.module.scss
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
$colorf: #eee;
|
||||||
|
$heightHeader: 76px;
|
||||||
|
|
||||||
|
.logMeta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logType {
|
||||||
|
color: $colorf;
|
||||||
|
text-align: center;
|
||||||
|
width: 56px;
|
||||||
|
font-size: 10px;
|
||||||
|
background: green;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logTime {
|
||||||
|
// color: $colorf;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logText {
|
||||||
|
display: flex;
|
||||||
|
font-family: 'Source Code Pro', Menlo, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////
|
||||||
|
|
||||||
|
.logUl {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
color: $colorf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////
|
||||||
|
|
||||||
|
.logs {
|
||||||
|
padding: 10px 40px;
|
||||||
|
height: calc(100vh - #{$heightHeader});
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logPlaceholder {
|
||||||
|
height: calc(100vh - #{$heightHeader});
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #2d2d30;
|
||||||
|
opacity: 0.3;
|
||||||
|
|
||||||
|
div:nth-child(2) {
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
}
|
34
src/components/Modal.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
|
import s0 from './Modal.module.scss';
|
||||||
|
|
||||||
|
class ModalAPIConfig extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onRequestClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isOpen, onRequestClose, children, ...rest } = this.props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
contentLabel="test"
|
||||||
|
className={s0.content}
|
||||||
|
overlayClassName={s0.overlay}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalAPIConfig;
|
22
src/components/Modal.module.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
outline: none;
|
||||||
|
position: absolute;
|
||||||
|
color: #ddd;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: #444;
|
||||||
|
padding: 20px;
|
||||||
|
// border: 1px solid #ccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
103
src/components/Proxies.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import ContentHeader from 'c/ContentHeader';
|
||||||
|
import Proxy from 'c/Proxy';
|
||||||
|
import Button from 'c/Button';
|
||||||
|
|
||||||
|
import cx from 'classnames';
|
||||||
|
import s0 from 'c/Proxies.module.scss';
|
||||||
|
|
||||||
|
const th = cx(s0.row, s0.th, 'border-bottom');
|
||||||
|
const colItem = cx(s0.colItem, 'border-bottom');
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { getProxies, fetchProxies, requestDelayAll } from 'd/proxies';
|
||||||
|
|
||||||
|
function mapStateToProps(s) {
|
||||||
|
return {
|
||||||
|
proxies: getProxies(s)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
fetchProxies: bindActionCreators(fetchProxies, dispatch),
|
||||||
|
requestDelayAll: bindActionCreators(requestDelayAll, dispatch)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Proxies extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
proxies: PropTypes.object.isRequired,
|
||||||
|
fetchProxies: PropTypes.func.isRequired,
|
||||||
|
requestDelayAll: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchProxies();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { proxies, requestDelayAll } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContentHeader title="Proxies" />
|
||||||
|
|
||||||
|
<div className={s0.root}>
|
||||||
|
<div className={s0.btnGroup}>
|
||||||
|
<Button label="Test Latency" onClick={requestDelayAll} />
|
||||||
|
</div>
|
||||||
|
<div className={th}>
|
||||||
|
<div className={s0.col1}>Name</div>
|
||||||
|
<div className={s0.col2}>Type</div>
|
||||||
|
<div className={s0.col3}>All</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{Object.keys(proxies).map(k => {
|
||||||
|
const o = proxies[k];
|
||||||
|
return <ProxyRow name={k} key={k} {...o} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProxyRow extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
all: PropTypes.array,
|
||||||
|
now: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, type, all, now } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={s0.row}>
|
||||||
|
<div className={s0.col1}>{name}</div>
|
||||||
|
<div className={s0.col2}>{type}</div>
|
||||||
|
<div className={s0.col3}>
|
||||||
|
{all &&
|
||||||
|
all.map(p => {
|
||||||
|
return (
|
||||||
|
<div className={colItem} key={p}>
|
||||||
|
<Proxy name={p} parentName={name} checked={p === now} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Proxies);
|
47
src/components/Proxies.module.scss
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
$heightHeader: 76px;
|
||||||
|
|
||||||
|
.root {
|
||||||
|
color: #eee;
|
||||||
|
padding: 10px 40px;
|
||||||
|
height: calc(100vh - #{$heightHeader});
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col1 {
|
||||||
|
width: 150px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col2 {
|
||||||
|
width: 150px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col3 {
|
||||||
|
width: 350px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colItem {
|
||||||
|
padding: 8px 0;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnGroup {
|
||||||
|
color: #eee;
|
||||||
|
// padding: 10px 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
105
src/components/Proxy.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import s0 from 'c/Proxy.module.scss';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { getDelay, switchProxy, requestDelayForProxy } from 'd/proxies';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const delay = getDelay(state);
|
||||||
|
return { delay };
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
switchProxy: bindActionCreators(switchProxy, dispatch),
|
||||||
|
requestDelay: bindActionCreators(requestDelayForProxy, dispatch)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
good: '#67C23A',
|
||||||
|
normal: '#E6A23C',
|
||||||
|
bad: '#F56C6C',
|
||||||
|
na: '#909399'
|
||||||
|
};
|
||||||
|
|
||||||
|
class Proxy extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
parentName: PropTypes.string,
|
||||||
|
checked: PropTypes.bool,
|
||||||
|
switchProxy: PropTypes.func,
|
||||||
|
requestDelay: PropTypes.func,
|
||||||
|
delay: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { name, delay, requestDelay } = this.props;
|
||||||
|
if (delay[name]) return;
|
||||||
|
|
||||||
|
requestDelay(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRadioOnChange = ev => {
|
||||||
|
const { name, parentName, checked, switchProxy } = this.props;
|
||||||
|
if (checked) return;
|
||||||
|
switchProxy(parentName, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, parentName, checked, switchProxy, delay } = this.props;
|
||||||
|
const id = parentName + ':' + name;
|
||||||
|
// XXX default to 0 might not a good idea
|
||||||
|
const latency = delay[name] || 0;
|
||||||
|
return (
|
||||||
|
<label className={s0.Proxy} htmlFor={id}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleRadioOnChange}
|
||||||
|
/>
|
||||||
|
<div className={s0.name}>{name}</div>
|
||||||
|
<LatencyLabel val={latency} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LatencyLabel extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
val: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { val } = this.props;
|
||||||
|
let bg = colorMap.na;
|
||||||
|
if (val < 100) {
|
||||||
|
bg = colorMap.good;
|
||||||
|
} else if (val < 300) {
|
||||||
|
bg = colorMap.normal;
|
||||||
|
} else {
|
||||||
|
bg = colorMap.bad;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={s0.LatencyLabel}
|
||||||
|
style={{
|
||||||
|
background: bg
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{val}</div>
|
||||||
|
<div>ms</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Proxy);
|
22
src/components/Proxy.module.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
.Proxy {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1;
|
||||||
|
// width: 100px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.LatencyLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #e6a23c;
|
||||||
|
|
||||||
|
div:nth-child(2) {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
61
src/components/Root.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { ConnectedRouter } from 'react-router-redux';
|
||||||
|
import { Route, Link } from 'react-router-dom';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
|
||||||
|
import SideBar from 'c/SideBar';
|
||||||
|
import Home from 'c/Home';
|
||||||
|
import Logs from 'c/Logs';
|
||||||
|
import Proxies from 'c/Proxies';
|
||||||
|
import Config from 'c/Config';
|
||||||
|
|
||||||
|
import APIDiscovery from 'c/APIDiscovery';
|
||||||
|
|
||||||
|
// testing...
|
||||||
|
// import StyleGuide from 'c/StyleGuide';
|
||||||
|
// import Loading from 'c/Loading';
|
||||||
|
|
||||||
|
// for loading async chunk...not used yet
|
||||||
|
|
||||||
|
// import Loadable from './Loadable';
|
||||||
|
// const delay = t => new Promise(r => setTimeout(r, t));
|
||||||
|
// const AsyncAbout = Loadable({
|
||||||
|
// loader: () => delay(800).then(() => import('./About'))
|
||||||
|
// });
|
||||||
|
// const AsyncHello = Loadable({
|
||||||
|
// loader: () => import('./Hello')
|
||||||
|
// });
|
||||||
|
|
||||||
|
import './Root.scss';
|
||||||
|
import s0 from './Root.module.scss';
|
||||||
|
|
||||||
|
const Root = ({ store, history }) => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<div className={s0.app}>
|
||||||
|
<APIDiscovery />
|
||||||
|
<Route path="/" component={SideBar} />
|
||||||
|
<div style={{ flexGrow: '1', overflow: 'scroll' }}>
|
||||||
|
<Route exact path="/" component={Home} />
|
||||||
|
<Route exact path="/overview" component={Home} />
|
||||||
|
<Route exact path="/configs" component={Config} />
|
||||||
|
<Route exact path="/logs" component={Logs} />
|
||||||
|
<Route exact path="/proxies" component={Proxies} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
// <Route exact path="/__0" component={StyleGuide} />
|
||||||
|
// <Route exact path="/__1" component={Loading} />
|
||||||
|
|
||||||
|
Root.propTypes = {
|
||||||
|
store: PropTypes.object,
|
||||||
|
history: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
// hot export Root
|
||||||
|
// https://github.com/gaearon/react-hot-loader/tree/v4.0.1#getting-started
|
||||||
|
export default hot(module)(Root);
|
7
src/components/Root.module.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
background: #202020;
|
||||||
|
|
||||||
|
min-height: 640px;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
55
src/components/Root.scss
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
.border-left,
|
||||||
|
.border-top,
|
||||||
|
.border-bottom {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
%border {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
transform: scaleY(0.5) translateZ(0);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
%border1 {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
transform: scaleX(0.5) translateZ(0);
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-top::before {
|
||||||
|
@extend %border;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-bottom::after {
|
||||||
|
@extend %border;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left::before {
|
||||||
|
@extend %border1;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
68
src/components/SideBar.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
// import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
|
import Icon from 'c/Icon';
|
||||||
|
|
||||||
|
import activity from 's/activity.svg';
|
||||||
|
import settings from 's/settings.svg';
|
||||||
|
import globe from 's/globe.svg';
|
||||||
|
import file from 's/file.svg';
|
||||||
|
import yacd from 's/yacd.svg';
|
||||||
|
|
||||||
|
import s from 'c/SideBar.module.scss';
|
||||||
|
|
||||||
|
class SideBarRowDump extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
iconId: PropTypes.string,
|
||||||
|
labelText: PropTypes.string,
|
||||||
|
pathname: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { iconId, labelText, to, pathname } = this.props;
|
||||||
|
const cls = pathname === to ? s.rowActive : s.row;
|
||||||
|
return (
|
||||||
|
<Link to={to} className={cls}>
|
||||||
|
<Icon id={iconId} width={28} height={28} />
|
||||||
|
<div className={s.label}>{labelText}</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const { pathname } = state.router.location;
|
||||||
|
return { pathname };
|
||||||
|
};
|
||||||
|
const mapDispatchToProps = null;
|
||||||
|
|
||||||
|
const SideBarRow = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(SideBarRowDump);
|
||||||
|
|
||||||
|
class SideBar extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={s.root}>
|
||||||
|
<div className={s.logo}>
|
||||||
|
<Icon id={yacd.id} width={80} height={80} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={s.rows}>
|
||||||
|
<SideBarRow to="/" iconId={activity.id} labelText="Overview" />
|
||||||
|
<SideBarRow to="/proxies" iconId={globe.id} labelText="Proxies" />
|
||||||
|
<SideBarRow to="/configs" iconId={settings.id} labelText="Config" />
|
||||||
|
<SideBarRow to="/logs" iconId={file.id} labelText="Logs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SideBar;
|
50
src/components/SideBar.module.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
.root {
|
||||||
|
background: #2d2d30;
|
||||||
|
// width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
color: #2a477a;
|
||||||
|
transition: color 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1f52ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a router linke
|
||||||
|
.rowActive,
|
||||||
|
.row {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 20px;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// color: white;
|
||||||
|
// background: #494b4e;
|
||||||
|
// }
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: #c7c7c7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rowActive {
|
||||||
|
color: white;
|
||||||
|
background: #494b4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
60
src/components/StyleGuide.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import ToggleSwitch from 'c/ToggleSwitch';
|
||||||
|
import Input from 'c/Input';
|
||||||
|
import Switch from 'c/Switch';
|
||||||
|
import Button from 'c/Button';
|
||||||
|
import Modal from 'c/Modal';
|
||||||
|
import APIConfig from 'c/APIConfig';
|
||||||
|
|
||||||
|
const paneStyle = {
|
||||||
|
padding: '20px 0'
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsRule = [
|
||||||
|
{
|
||||||
|
label: 'Global',
|
||||||
|
value: 'Global'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rule',
|
||||||
|
value: 'Rule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Direct',
|
||||||
|
value: 'Direct'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const Pane = ({ children }) => <div style={paneStyle}>{children}</div>;
|
||||||
|
|
||||||
|
class StyleGuide extends PureComponent {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Pane>
|
||||||
|
<Switch />
|
||||||
|
</Pane>
|
||||||
|
<Pane>
|
||||||
|
<Input />
|
||||||
|
</Pane>
|
||||||
|
<Pane>
|
||||||
|
<ToggleSwitch
|
||||||
|
name="test"
|
||||||
|
options={optionsRule}
|
||||||
|
value="Rule"
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
</Pane>
|
||||||
|
<Pane>
|
||||||
|
<Button label="Test Latency" />
|
||||||
|
</Pane>
|
||||||
|
<Modal isOpen={true} onRequestClose={() => {}}>
|
||||||
|
<APIConfig />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StyleGuide;
|
35
src/components/Switch.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import s0 from 'c/Switch.module.scss';
|
||||||
|
|
||||||
|
class Switch extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
checked: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
name: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
checked: false,
|
||||||
|
name: '',
|
||||||
|
onChange: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { checked, onChange, name } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={name}
|
||||||
|
checked={checked}
|
||||||
|
className={s0.switch}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Switch;
|
50
src/components/Switch.module.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// steal from https://codepen.io/joshnh/pen/hjbuH
|
||||||
|
|
||||||
|
$white: #fff;
|
||||||
|
// $green: #53d76a;
|
||||||
|
$grey: #d3d3d3;
|
||||||
|
$color-theme: #047aff;
|
||||||
|
|
||||||
|
input.switch[type='checkbox'] {
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: darken($white, 2%);
|
||||||
|
border: 1px solid $grey;
|
||||||
|
border-radius: 26px;
|
||||||
|
box-shadow: inset 0 0 0 1px $grey;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 28px;
|
||||||
|
position: relative;
|
||||||
|
transition: border 0.25s 0.15s, box-shadow 0.25s 0.3s, padding 0.25s;
|
||||||
|
width: 44px;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
background-color: $white;
|
||||||
|
border: 1px solid $grey;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: inset 0 -3px 3px hsla(0, 0%, 0%, 0.025),
|
||||||
|
0 1px 4px hsla(0, 0%, 0%, 0.15), 0 4px 4px hsla(0, 0%, 0%, 0.1);
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 26px;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 0;
|
||||||
|
transition: border 0.25s 0.15s, left 0.25s 0.1s, right 0.15s 0.175s;
|
||||||
|
}
|
||||||
|
&:checked {
|
||||||
|
border-color: $color-theme;
|
||||||
|
box-shadow: inset 0 0 0 13px $color-theme;
|
||||||
|
padding-left: 18px;
|
||||||
|
transition: border 0.25s, box-shadow 0.25s, padding 0.25s 0.15s;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
border-color: $color-theme;
|
||||||
|
left: 16px;
|
||||||
|
right: 0;
|
||||||
|
transition: border 0.25s, left 0.15s 0.25s, right 0.25s 0.175s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
src/components/ToggleSwitch.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import s0 from 'c/ToggleSwitch.module.scss';
|
||||||
|
|
||||||
|
class ToggleSwitch extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
options: PropTypes.array,
|
||||||
|
value: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
onChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Global',
|
||||||
|
value: 'Global'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rule',
|
||||||
|
value: 'Rule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Direct',
|
||||||
|
value: 'Direct'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
value: 'Rule',
|
||||||
|
name: 'rand0'
|
||||||
|
};
|
||||||
|
|
||||||
|
// handleRadioOnChange = ev => {
|
||||||
|
// ev.preventDefault();
|
||||||
|
// const value = ev.target.value;
|
||||||
|
// if (this.state.value === value) return;
|
||||||
|
// this.setState({ value });
|
||||||
|
// };
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { options, name, value, onChange } = this.props;
|
||||||
|
const w = (100 / options.length).toPrecision(3);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={s0.ToggleSwitch}>
|
||||||
|
{options.map((o, idx) => {
|
||||||
|
if (value === o.value) this.idx = idx;
|
||||||
|
const id = `${name}-${o.label}`;
|
||||||
|
let className = idx === 0 ? '' : 'border-left';
|
||||||
|
return (
|
||||||
|
<label htmlFor={id} key={id} className={className}>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
type="radio"
|
||||||
|
value={o.value}
|
||||||
|
checked={value === o.value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<div>{o.label}</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<a
|
||||||
|
className={s0.slider}
|
||||||
|
style={{
|
||||||
|
width: w + '%',
|
||||||
|
left: this.idx * w + '%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToggleSwitch;
|
34
src/components/ToggleSwitch.module.scss
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
.ToggleSwitch {
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid #525252;
|
||||||
|
color: #eee;
|
||||||
|
background: #353535;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
transition: left 0.2s ease-out;
|
||||||
|
background: #181818;
|
||||||
|
}
|
161
src/components/TrafficChart.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Chart from 'chart.js/dist/Chart.min.js';
|
||||||
|
import prettyBytes from 'm/pretty-bytes';
|
||||||
|
|
||||||
|
import { fetchData } from '../api/traffic';
|
||||||
|
|
||||||
|
const colorCombo = {
|
||||||
|
0: {
|
||||||
|
down: {
|
||||||
|
backgroundColor: 'rgba(176, 209, 132, 0.8)',
|
||||||
|
borderColor: 'rgb(176, 209, 132)'
|
||||||
|
},
|
||||||
|
up: {
|
||||||
|
backgroundColor: 'rgba(181, 220, 231, 0.8)',
|
||||||
|
borderColor: 'rgb(181, 220, 231)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
up: {
|
||||||
|
backgroundColor: 'rgba(242, 174, 62, 0.3)',
|
||||||
|
borderColor: 'rgb(242, 174, 62)'
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
backgroundColor: 'rgba(69, 154, 248, 0.3)',
|
||||||
|
borderColor: 'rgb(69, 154, 248)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upProps = {
|
||||||
|
...colorCombo['0'].up,
|
||||||
|
label: 'Up',
|
||||||
|
borderWidth: 1,
|
||||||
|
lineTension: 0,
|
||||||
|
pointRadius: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const downProps = {
|
||||||
|
...colorCombo['0'].down,
|
||||||
|
label: 'Down',
|
||||||
|
borderWidth: 1,
|
||||||
|
lineTension: 0,
|
||||||
|
pointRadius: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
title: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
fontColor: '#ccc',
|
||||||
|
boxWidth: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
// it's hard to follow the tooltip while the data is streaming
|
||||||
|
// so disable it for now
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
animationDuration: 100
|
||||||
|
// callbacks: {
|
||||||
|
// label(tooltipItem, data) {
|
||||||
|
// console.log(tooltipItem);
|
||||||
|
// const { datasetIndex, yLabel } = tooltipItem;
|
||||||
|
// const l = data.datasets[tooltipItem.datasetIndex].label;
|
||||||
|
// console.log(yLabel);
|
||||||
|
// const b = prettyBytes(parseInt(yLabel, 10));
|
||||||
|
// return l + b;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'nearest',
|
||||||
|
intersect: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
display: false,
|
||||||
|
gridLines: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
display: true,
|
||||||
|
gridLines: {
|
||||||
|
display: true,
|
||||||
|
color: '#555',
|
||||||
|
borderDash: [3, 6],
|
||||||
|
drawBorder: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback(value) {
|
||||||
|
return prettyBytes(value) + '/s ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrafficChart extends Component {
|
||||||
|
traffic = {
|
||||||
|
labels: [],
|
||||||
|
up: [],
|
||||||
|
down: []
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const ctx = document.getElementById('myChart').getContext('2d');
|
||||||
|
this.traffic = fetchData();
|
||||||
|
const data = {
|
||||||
|
labels: this.traffic.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
...upProps,
|
||||||
|
data: this.traffic.up
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...downProps,
|
||||||
|
data: this.traffic.down
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const c = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
this.unsubscribe = this.traffic.subscribe(() => c.update());
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '80%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas id="myChart" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrafficChart;
|
45
src/components/TrafficNow.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import prettyBytes from 'm/pretty-bytes';
|
||||||
|
|
||||||
|
import { fetchData } from '../api/traffic';
|
||||||
|
|
||||||
|
import s0 from 'c/TrafficNow.module.scss';
|
||||||
|
|
||||||
|
class TrafficNow extends Component {
|
||||||
|
state = {
|
||||||
|
upStr: '',
|
||||||
|
downStr: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.traffic = fetchData();
|
||||||
|
this.unsubscribe = this.traffic.subscribe(o => {
|
||||||
|
this.setState({
|
||||||
|
upStr: prettyBytes(o.up) + '/s',
|
||||||
|
downStr: prettyBytes(o.down) + '/s'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { upStr, downStr } = this.state;
|
||||||
|
return (
|
||||||
|
<div className={s0.TrafficNow}>
|
||||||
|
<div className={s0.up}>
|
||||||
|
<div>Upload</div>
|
||||||
|
<div>{upStr}</div>
|
||||||
|
</div>
|
||||||
|
<div className={s0.down}>
|
||||||
|
<div>Download</div>
|
||||||
|
<div>{downStr}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrafficNow;
|
26
src/components/TrafficNow.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.TrafficNow {
|
||||||
|
color: #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up,
|
||||||
|
.down {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
width: 200px;
|
||||||
|
|
||||||
|
div:nth-child(1) {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
div:nth-child(2) {
|
||||||
|
padding: 10px 0 0;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.up {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.down {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
13
src/config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// const apiBaseURL = 'http://127.0.0.1:1234';
|
||||||
|
let apiBaseURL = 'http://127.0.0.1:7899';
|
||||||
|
const updateAPIBaseURL = url => (apiBaseURL = url);
|
||||||
|
const getAPIURL = () => ({
|
||||||
|
proxies: apiBaseURL + '/proxies',
|
||||||
|
logs: apiBaseURL + '/logs',
|
||||||
|
traffic: apiBaseURL + '/traffic',
|
||||||
|
configs: apiBaseURL + '/configs'
|
||||||
|
});
|
||||||
|
|
||||||
|
export { apiBaseURL, getAPIURL, updateAPIBaseURL };
|
64
src/ducks/app.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { loadState, saveState } from 'm/storage';
|
||||||
|
import { fetchConfigs } from 'd/configs';
|
||||||
|
import { closeModal } from 'd/modals';
|
||||||
|
import { apiBaseURL, updateAPIBaseURL } from '../config';
|
||||||
|
|
||||||
|
const UpdateClashAPIConfig = 'app/UpdateClashAPIConfig';
|
||||||
|
|
||||||
|
const StorageKey = 'yacd.haishan.me';
|
||||||
|
|
||||||
|
export const getClashAPIConfig = s => s.app.clashAPIConfig;
|
||||||
|
|
||||||
|
// TODO to support secret
|
||||||
|
export function updateClashAPIConfig(iHostname, iPort) {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const hostname = iHostname.trim().replace(/^http(s)\:\/\//, '');
|
||||||
|
const port = iPort;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UpdateClashAPIConfig,
|
||||||
|
payload: { hostname, port }
|
||||||
|
});
|
||||||
|
|
||||||
|
// side effect
|
||||||
|
updateAPIBaseURL('http://' + hostname + ':' + port);
|
||||||
|
saveState(StorageKey, getState().app);
|
||||||
|
|
||||||
|
dispatch(closeModal('apiConfig'));
|
||||||
|
dispatch(fetchConfigs());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function retrieveAPIHostnameAndPort(apiBaseURL) {
|
||||||
|
const match = /^http:\/\/(\S+?):(\d+)/.exec(apiBaseURL);
|
||||||
|
if (!match) return {};
|
||||||
|
return {
|
||||||
|
hostname: match[1],
|
||||||
|
port: match[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const defaultState = {
|
||||||
|
clashAPIConfig: retrieveAPIHostnameAndPort(apiBaseURL)
|
||||||
|
};
|
||||||
|
function getInitialState() {
|
||||||
|
let s = loadState(StorageKey);
|
||||||
|
if (!s) s = defaultState;
|
||||||
|
|
||||||
|
// FIXME using data from multi source is NOT OK
|
||||||
|
const { hostname, port } = s.clashAPIConfig;
|
||||||
|
updateAPIBaseURL('http://' + hostname + ':' + port);
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
const initialState = getInitialState();
|
||||||
|
|
||||||
|
export default function reducer(state = initialState, { type, payload }) {
|
||||||
|
switch (type) {
|
||||||
|
case UpdateClashAPIConfig: {
|
||||||
|
return { ...state, clashAPIConfig: { ...payload } };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
97
src/ducks/configs.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as configsAPI from 'a/configs';
|
||||||
|
import { openModal } from 'd/modals';
|
||||||
|
|
||||||
|
const CompletedFetchConfigs = 'configs/CompletedFetchConfigs';
|
||||||
|
const OptimisticUpdateConfigs = 'proxies/OptimisticUpdateConfigs';
|
||||||
|
|
||||||
|
// const CompletedRequestDelayForProxy = 'proxies/CompletedRequestDelayForProxy';
|
||||||
|
|
||||||
|
export const getConfigs = s => s.configs;
|
||||||
|
|
||||||
|
export function fetchConfigs() {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await configsAPI.fetchConfigs();
|
||||||
|
} catch (err) {
|
||||||
|
// FIXME
|
||||||
|
console.log('Error fetch configs', err);
|
||||||
|
dispatch(openModal('apiConfig'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 404) {
|
||||||
|
dispatch(openModal('apiConfig'));
|
||||||
|
} else {
|
||||||
|
console.log('Error fetch configs', res.statusText);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await res.json();
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CompletedFetchConfigs,
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateConfigs(partialConfg) {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
configsAPI
|
||||||
|
.updateConfigs(partialConfg)
|
||||||
|
.then(
|
||||||
|
res => {
|
||||||
|
if (res.ok === false) {
|
||||||
|
console.log('Error update configs', res.statusText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
console.log('Error update configs', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// fetch updated configs to refresh to UI
|
||||||
|
dispatch(fetchConfigs());
|
||||||
|
});
|
||||||
|
const configsCurr = getConfigs(getState());
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: OptimisticUpdateConfigs,
|
||||||
|
payload: {
|
||||||
|
...configsCurr,
|
||||||
|
...partialConfg
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
port: 7890,
|
||||||
|
'socket-port': 7891,
|
||||||
|
'redir-port': 0,
|
||||||
|
'allow-lan': false,
|
||||||
|
mode: 'Rule',
|
||||||
|
'log-level': 'info'
|
||||||
|
|
||||||
|
/////
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(state = initialState, { type, payload }) {
|
||||||
|
switch (type) {
|
||||||
|
// case CompletedRequestDelayForProxy:
|
||||||
|
// case OptimisticSwitchProxy:
|
||||||
|
case OptimisticUpdateConfigs:
|
||||||
|
case CompletedFetchConfigs: {
|
||||||
|
return { ...state, ...payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
14
src/ducks/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import { routerReducer as router } from 'react-router-redux';
|
||||||
|
import app from './app';
|
||||||
|
import modals from './modals';
|
||||||
|
import proxies from './proxies';
|
||||||
|
import configs from './configs';
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
router,
|
||||||
|
app,
|
||||||
|
modals,
|
||||||
|
proxies,
|
||||||
|
configs
|
||||||
|
});
|
31
src/ducks/modals.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const OpenModal = 'modals/OpenModal';
|
||||||
|
const CloseModal = 'modals/CloseModal';
|
||||||
|
|
||||||
|
export function openModal(modalName) {
|
||||||
|
return {
|
||||||
|
type: OpenModal,
|
||||||
|
payload: modalName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeModal(modalName) {
|
||||||
|
return {
|
||||||
|
type: CloseModal,
|
||||||
|
payload: modalName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
apiConfig: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(state = initialState, { type, payload }) {
|
||||||
|
switch (type) {
|
||||||
|
case OpenModal:
|
||||||
|
return { ...initialState, [payload]: true };
|
||||||
|
case CloseModal:
|
||||||
|
return { ...initialState, [payload]: false };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
128
src/ducks/proxies.js
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as proxiesAPI from 'a/proxies';
|
||||||
|
|
||||||
|
export const getProxies = s => s.proxies.proxies;
|
||||||
|
export const getDelay = s => s.proxies.delay;
|
||||||
|
|
||||||
|
const CompletedFetchProxies = 'proxies/CompletedFetchProxies';
|
||||||
|
const OptimisticSwitchProxy = 'proxies/OptimisticSwitchProxy';
|
||||||
|
const CompletedRequestDelayForProxy = 'proxies/CompletedRequestDelayForProxy';
|
||||||
|
|
||||||
|
export function fetchProxies() {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
// TODO handle errors
|
||||||
|
|
||||||
|
const proxiesCurr = getProxies(getState());
|
||||||
|
// TODO this is too aggressive...
|
||||||
|
if (Object.keys(proxiesCurr).length > 0) return;
|
||||||
|
|
||||||
|
// TODO show loading animation?
|
||||||
|
const json = await proxiesAPI.fetchProxies();
|
||||||
|
let { proxies = {} } = json;
|
||||||
|
dispatch({
|
||||||
|
type: CompletedFetchProxies,
|
||||||
|
payload: { proxies }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchProxy(name1, name2) {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
// TODO display error message
|
||||||
|
proxiesAPI
|
||||||
|
.requestToSwitchProxy(name1, name2)
|
||||||
|
.then(
|
||||||
|
res => {
|
||||||
|
if (res.ok === false) {
|
||||||
|
console.log('failed to swith proxy', res.statusText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
console.log(err, 'failed to swith proxy');
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// fetchProxies again
|
||||||
|
dispatch(fetchProxies());
|
||||||
|
});
|
||||||
|
// optimistic UI update
|
||||||
|
const proxiesCurr = getProxies(getState());
|
||||||
|
const proxiesNext = { ...proxiesCurr };
|
||||||
|
if (proxiesNext[name1] && proxiesNext[name1].now) {
|
||||||
|
proxiesNext[name1].now = name2;
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: OptimisticSwitchProxy,
|
||||||
|
payload: { proxies: proxiesNext }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDelayForProxyOnce(name) {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const res = await proxiesAPI.requestDelayForProxy(name);
|
||||||
|
if (res.ok === false) {
|
||||||
|
console.log('Error', res.statusText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { delay } = await res.json();
|
||||||
|
|
||||||
|
const delayPrev = getDelay(getState());
|
||||||
|
const delayNext = {
|
||||||
|
...delayPrev,
|
||||||
|
[name]: delay
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: CompletedRequestDelayForProxy,
|
||||||
|
payload: { delay: delayNext }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// const proxyTypeListTo = [
|
||||||
|
// 'Vmess',
|
||||||
|
// 'Shadowsocks'
|
||||||
|
// ];
|
||||||
|
|
||||||
|
export function requestDelayForProxy(name) {
|
||||||
|
return async dispatch => {
|
||||||
|
await dispatch(requestDelayForProxyOnce(name));
|
||||||
|
await dispatch(requestDelayForProxyOnce(name));
|
||||||
|
await dispatch(requestDelayForProxyOnce(name));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestDelayAll() {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const proxies = getProxies(state);
|
||||||
|
const keys = Object.keys(proxies);
|
||||||
|
const proxyNames = [];
|
||||||
|
keys.forEach(k => {
|
||||||
|
if (proxies[k].type === 'Vmess' || proxies[k].type === 'Shadowsocks') {
|
||||||
|
proxyNames.push(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(proxyNames.map(p => dispatch(requestDelayForProxy(p))));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
proxies: {},
|
||||||
|
delay: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(state = initialState, { type, payload }) {
|
||||||
|
switch (type) {
|
||||||
|
case CompletedRequestDelayForProxy:
|
||||||
|
case OptimisticSwitchProxy:
|
||||||
|
case CompletedFetchProxies: {
|
||||||
|
return { ...state, ...payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
26
src/index.template.ejs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="application-name" content="yacd">
|
||||||
|
<meta name="description" content="Yet Another Clash Dashboard">
|
||||||
|
<meta name="theme-color" content="#202020">
|
||||||
|
<link id="favicon" rel="icon" type="image/png" sizes="64x64" href="yacd-64.png">
|
||||||
|
<link id="favicon" rel="icon" type="image/png" sizes="128x128" href="yacd-128.png">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
|
||||||
|
<% for (key in htmlWebpackPlugin.files.css) { %>
|
||||||
|
<link href="<%= htmlWebpackPlugin.files.css[key] %>" rel="stylesheet">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<% for (key in htmlWebpackPlugin.files.chunks) { %>
|
||||||
|
<script src="<%= htmlWebpackPlugin.files.chunks[key].entry %>" type="text/javascript"></script>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
17
src/misc/pretty-bytes.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
'use strict';
|
||||||
|
// steal from https://github.com/sindresorhus/pretty-bytes/blob/master/index.js
|
||||||
|
|
||||||
|
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
export default number => {
|
||||||
|
if (number < 1000) {
|
||||||
|
return number + ' B';
|
||||||
|
}
|
||||||
|
const exponent = Math.min(
|
||||||
|
Math.floor(Math.log10(number) / 3),
|
||||||
|
UNITS.length - 1
|
||||||
|
);
|
||||||
|
number = Number((number / Math.pow(1000, exponent)).toPrecision(3));
|
||||||
|
const unit = UNITS[exponent];
|
||||||
|
return number + ' ' + unit;
|
||||||
|
};
|
30
src/misc/storage.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// manage localStorage
|
||||||
|
|
||||||
|
function loadState(key) {
|
||||||
|
try {
|
||||||
|
const serialized = localStorage.getItem(key);
|
||||||
|
if (!serialized) return undefined;
|
||||||
|
return JSON.parse(serialized);
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState(key, state) {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(state);
|
||||||
|
localStorage.setItem(key, serialized);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearState(key) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { loadState, saveState, clearState };
|
27
src/store/configureStore.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import thunkMiddleware from 'redux-thunk';
|
||||||
|
import rootReducer from '../ducks';
|
||||||
|
// import { loadState } from '../utils';
|
||||||
|
import { routerMiddleware } from 'react-router-redux';
|
||||||
|
|
||||||
|
// const preloadedState = loadState();
|
||||||
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
||||||
|
export default function configureStore(history) {
|
||||||
|
const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
composeEnhancers(
|
||||||
|
applyMiddleware(thunkMiddleware, routerMiddleware(history))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
module.hot.accept('../ducks', () => {
|
||||||
|
const nextRootReducer = require('../ducks').default;
|
||||||
|
store.replaceReducer(nextRootReducer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
1
src/svg/activity.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
|
After Width: | Height: | Size: 250 B |
1
src/svg/cpu.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
After Width: | Height: | Size: 667 B |
1
src/svg/file.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
After Width: | Height: | Size: 440 B |
1
src/svg/github.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
|
After Width: | Height: | Size: 527 B |
1
src/svg/globe.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
After Width: | Height: | Size: 380 B |
1
src/svg/heart.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
After Width: | Height: | Size: 372 B |
1
src/svg/settings.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
After Width: | Height: | Size: 979 B |
1
src/svg/star.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
|
After Width: | Height: | Size: 339 B |
1
src/svg/x.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
After Width: | Height: | Size: 299 B |
8
src/svg/yacd.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="320" height="320" viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M68.142 52.056c9.396-1.515 29.798 27.762 45.809 57.699 18.907-8.164 75.199-7.859 97.664 0 4.799-15.898 28.706-58.455 38.546-57.699 4.065.09 13.145 32.285 23.568 96.695 2.793 17.259 6.883 48.36 12.271 93.305C245.489 260.019 203.995 269 161.518 269c-42.477 0-84.983-8.981-127.518-26.944 16.497-125.657 27.878-188.99 34.142-190z" stroke="#EEE" stroke-width="5" fill="currentColor"/>
|
||||||
|
<circle stroke="#EEE" fill="#EEE" cx="226" cy="184" r="15"/>
|
||||||
|
<circle stroke="#EEE" fill="#EEE" cx="103" cy="184" r="15"/>
|
||||||
|
<path d="M163 214.042c2.067 0 4.637 6.54 10.108 6.54 4.567.16 5.383-1.65 7.074-1.677 1.656 0 1.818 1.174 1.818 1.677.027 2.166-5.347 3.686-8.349 3.418-7.475.101-9.703-5.451-10.651-5.451-.91 0-3.833 5.451-10.194 5.451-7.143-.113-8.799-1.471-8.799-3.099.01-.783.802-1.54 1.685-1.505 1.39-.041 4.576 1.027 7.35 1.038 3.585-.016 8.163-6.392 9.958-6.392z" fill="#FFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1 KiB |
207
webpack.common.js
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
process.env.BABEL_ENV = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
// ----- devtool
|
||||||
|
|
||||||
|
let devtool;
|
||||||
|
if (isDev) {
|
||||||
|
// https://webpack.js.org/configuration/devtool/
|
||||||
|
devtool = 'eval';
|
||||||
|
} else {
|
||||||
|
// https://webpack.js.org/configuration/devtool/
|
||||||
|
devtool = 'source-map';
|
||||||
|
}
|
||||||
|
module.exports.devtool = devtool;
|
||||||
|
|
||||||
|
// ---- entry
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
// vendor: ['babel-polyfill'],
|
||||||
|
app: ['./src/index.js']
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.entry = entry;
|
||||||
|
|
||||||
|
// ---- output
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
path: path.resolve(__dirname, 'public'),
|
||||||
|
filename: '[name].bundle.js',
|
||||||
|
publicPath: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.output = output;
|
||||||
|
|
||||||
|
// ----- rules
|
||||||
|
const jsRule = {
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: ['babel-loader']
|
||||||
|
};
|
||||||
|
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
|
const cssExtractPlugin = new MiniCssExtractPlugin({
|
||||||
|
filename: isDev ? '[name].bundle.css' : '[name].[chunkhash].css'
|
||||||
|
});
|
||||||
|
|
||||||
|
const LOCAL_IDENT_NAME_DEV = '[path]---[name]---[local]---[hash:base64:5]';
|
||||||
|
const LOCAL_IDENT_NAME_PROD = '[hash:base64:10]';
|
||||||
|
const localIdentName = isDev ? LOCAL_IDENT_NAME_DEV : LOCAL_IDENT_NAME_PROD;
|
||||||
|
const getCssLoaderOptions = (opt = {}) => ({
|
||||||
|
minimize: true,
|
||||||
|
localIdentName,
|
||||||
|
...opt
|
||||||
|
});
|
||||||
|
|
||||||
|
const cssnano = require('cssnano');
|
||||||
|
const loaders = {
|
||||||
|
style: { loader: 'style-loader' },
|
||||||
|
css: { loader: 'css-loader', options: getCssLoaderOptions() },
|
||||||
|
cssModule: {
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: getCssLoaderOptions({ modules: true })
|
||||||
|
},
|
||||||
|
postcss: {
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
plugins: () => [require('autoprefixer'), cssnano()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sass: {
|
||||||
|
loader: 'sass-loader'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssDevRule = {
|
||||||
|
test: /\.css$/,
|
||||||
|
exclude: /\.module\.css$/,
|
||||||
|
use: [loaders.style, loaders.css, loaders.postcss]
|
||||||
|
};
|
||||||
|
const cssProdRule = {
|
||||||
|
test: /\.css$/,
|
||||||
|
exclude: /\.module\.css$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, loaders.css, loaders.postcss]
|
||||||
|
};
|
||||||
|
const cssModulesDevRule = {
|
||||||
|
test: /\.module\.css$/,
|
||||||
|
use: [loaders.style, loaders.cssModule, loaders.postcss]
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssModulesProdRule = {
|
||||||
|
test: /\.module\.css$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, loaders.cssModule, loaders.postcss]
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileRule = {
|
||||||
|
test: /\.(ttf|eot|woff|woff2)(\?.+)?$/,
|
||||||
|
use: ['file-loader']
|
||||||
|
};
|
||||||
|
|
||||||
|
const sassDevRule = {
|
||||||
|
test: /\.scss$/,
|
||||||
|
exclude: /\.module\.scss$/,
|
||||||
|
use: [loaders.style, loaders.css, loaders.postcss, loaders.sass]
|
||||||
|
};
|
||||||
|
|
||||||
|
const sassProdRule = {
|
||||||
|
test: /\.scss$/,
|
||||||
|
exclude: /\.module\.scss$/,
|
||||||
|
use: [MiniCssExtractPlugin.loader, loaders.css, loaders.postcss, loaders.sass]
|
||||||
|
};
|
||||||
|
|
||||||
|
const sassCssModuleDevRule = {
|
||||||
|
test: /\.module\.scss$/,
|
||||||
|
use: [loaders.style, loaders.cssModule, loaders.postcss, loaders.sass]
|
||||||
|
};
|
||||||
|
|
||||||
|
const sassCssModuleProdRule = {
|
||||||
|
test: /\.module\.scss$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
loaders.cssModule,
|
||||||
|
loaders.postcss,
|
||||||
|
loaders.sass
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.jsRule = jsRule;
|
||||||
|
module.exports.fileRule = fileRule;
|
||||||
|
module.exports.cssDevRule = cssDevRule;
|
||||||
|
module.exports.cssProdRule = cssProdRule;
|
||||||
|
module.exports.cssModulesDevRule = cssModulesDevRule;
|
||||||
|
module.exports.cssModulesProdRule = cssModulesProdRule;
|
||||||
|
module.exports.sassDevRule = sassDevRule;
|
||||||
|
module.exports.sassProdRule = sassProdRule;
|
||||||
|
module.exports.sassCssModuleDevRule = sassCssModuleDevRule;
|
||||||
|
module.exports.sassCssModuleProdRule = sassCssModuleProdRule;
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
js: jsRule,
|
||||||
|
file: fileRule,
|
||||||
|
css: isDev ? cssDevRule : cssProdRule,
|
||||||
|
cssModules: isDev ? cssModulesDevRule : cssModulesProdRule,
|
||||||
|
sass: isDev ? sassDevRule : sassProdRule,
|
||||||
|
sassCssModules: isDev ? sassCssModuleDevRule : sassCssModuleProdRule
|
||||||
|
};
|
||||||
|
module.exports.rules = rules;
|
||||||
|
|
||||||
|
// ----- plugins
|
||||||
|
|
||||||
|
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
|
||||||
|
const definePlugin = new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: JSON.stringify('production')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// webpack 4 enable optimization concatenateModules by default
|
||||||
|
// https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a
|
||||||
|
// const moduleConcatPlugin = new webpack.optimize.ModuleConcatenationPlugin();
|
||||||
|
|
||||||
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
|
const bundleAnalyzerPlugin = new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: 'static',
|
||||||
|
reportFilename: 'report.html'
|
||||||
|
});
|
||||||
|
|
||||||
|
// prints more readable module names in the browser console on HMR updates
|
||||||
|
|
||||||
|
module.exports.definePlugin = definePlugin;
|
||||||
|
|
||||||
|
let plugins = [];
|
||||||
|
let pluginsCommon = [];
|
||||||
|
if (isDev) {
|
||||||
|
// in webpack 4 / namedModules will be enabled by default
|
||||||
|
plugins = [...pluginsCommon];
|
||||||
|
} else {
|
||||||
|
plugins = [
|
||||||
|
...pluginsCommon,
|
||||||
|
new webpack.HashedModuleIdsPlugin(),
|
||||||
|
definePlugin,
|
||||||
|
// see https://github.com/webpack-contrib/uglifyjs-webpack-plugin
|
||||||
|
new UglifyJSPlugin({
|
||||||
|
// enable parallelization.
|
||||||
|
// default number of concurrent runs: os.cpus().length - 1.
|
||||||
|
parallel: true,
|
||||||
|
// enable file caching.
|
||||||
|
// default path to cache directory:
|
||||||
|
// node_modules/.cache/uglifyjs-webpack-plugin.
|
||||||
|
cache: true
|
||||||
|
// debug
|
||||||
|
// uglifyOptions: {
|
||||||
|
// compress: false,
|
||||||
|
// mangle: false
|
||||||
|
// }
|
||||||
|
}),
|
||||||
|
cssExtractPlugin,
|
||||||
|
bundleAnalyzerPlugin
|
||||||
|
];
|
||||||
|
}
|
||||||
|
module.exports.plugins = plugins;
|
116
webpack.config.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const { rules, plugins } = require('./webpack.common');
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
const resolveDir = dir => path.resolve(__dirname, dir);
|
||||||
|
const HTMLPlugin = require('html-webpack-plugin');
|
||||||
|
const html = new HTMLPlugin({
|
||||||
|
title: 'yacd - Yet Another Clash Dashboard',
|
||||||
|
template: 'src/index.template.ejs',
|
||||||
|
inject: false,
|
||||||
|
filename: 'index.html'
|
||||||
|
});
|
||||||
|
|
||||||
|
const svgSpriteRule = {
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: ['svg-sprite-loader']
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- entry
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
app: ['@babel/polyfill', './src/app.js']
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- output
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
path: path.resolve(__dirname, 'public'),
|
||||||
|
filename: isDev ? '[name].bundle.js' : '[name].[chunkhash].js',
|
||||||
|
publicPath: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// const polyfill = ['whatwg-fetch'];
|
||||||
|
// entry.polyfill = polyfill;
|
||||||
|
|
||||||
|
const vendor = ['redux', 'react', 'react-dom', 'react-router-dom'];
|
||||||
|
|
||||||
|
// if (!isDev) entry.vendor = vendor; // generate common vendor bundle in prod
|
||||||
|
|
||||||
|
// if (isDev) {
|
||||||
|
// const dllRefPlugin = new webpack.DllReferencePlugin({
|
||||||
|
// context: '.',
|
||||||
|
// manifest: require('./public/vendor-manifest.json')
|
||||||
|
// });
|
||||||
|
// plugins.push(dllRefPlugin);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// since we don't use dll plugin for now - we still get vendor's bundled in a separate bundle
|
||||||
|
// entry.vendor = vendor;
|
||||||
|
// entry.react = react;
|
||||||
|
|
||||||
|
const mode = isDev ? 'development' : 'production';
|
||||||
|
|
||||||
|
const definePlugin = new webpack.DefinePlugin({
|
||||||
|
__DEV__: JSON.stringify(isDev),
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plugins.push(html);
|
||||||
|
plugins.push(definePlugin);
|
||||||
|
|
||||||
|
let devtool;
|
||||||
|
if (isDev) {
|
||||||
|
devtool = 'eval-source-map';
|
||||||
|
} else {
|
||||||
|
// devtool = 'source-map';
|
||||||
|
devtool = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool,
|
||||||
|
entry,
|
||||||
|
output,
|
||||||
|
mode,
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
a: resolveDir('src/api'),
|
||||||
|
s: resolveDir('src/svg'),
|
||||||
|
m: resolveDir('src/misc'),
|
||||||
|
d: resolveDir('src/ducks'),
|
||||||
|
c: resolveDir('src/components')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
svgSpriteRule,
|
||||||
|
rules.js,
|
||||||
|
rules.file,
|
||||||
|
rules.css,
|
||||||
|
rules.cssModules,
|
||||||
|
rules.sass,
|
||||||
|
rules.sassCssModules
|
||||||
|
]
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
// see https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693#optimizationruntimechunk
|
||||||
|
cacheGroups: {
|
||||||
|
core: {
|
||||||
|
test: module => {
|
||||||
|
if (/\/node_modules\/core-js\//.test(module.resource)) return true;
|
||||||
|
},
|
||||||
|
chunks: 'all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runtimeChunk: true
|
||||||
|
},
|
||||||
|
plugins
|
||||||
|
};
|
42
webpack.dll.config.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx']
|
||||||
|
},
|
||||||
|
entry: {
|
||||||
|
vendor: [
|
||||||
|
'babel-polyfill',
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'redux',
|
||||||
|
'react-router-dom'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'public'),
|
||||||
|
// this will generate vendor.bundle.js
|
||||||
|
// commons chunk plugin will also vendor.bundle.js
|
||||||
|
// the benefit is we reuse the vendor <script /> tag in our index.html
|
||||||
|
//
|
||||||
|
// since this dll plugin is only used in dev
|
||||||
|
// and commons chunk plugin is only used in prod
|
||||||
|
// there is no conflict
|
||||||
|
filename: '[name].dll.js',
|
||||||
|
library: '[name]_[hash]'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DllPlugin({
|
||||||
|
path: path.join(__dirname, 'public', '[name]-manifest.json'),
|
||||||
|
name: '[name]_[hash]'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// const output = {
|
||||||
|
// filename: '[name].bundle.js',
|
||||||
|
// publicPath: '/assets'
|
||||||
|
// };
|