summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHaishan <[email protected]>2019-04-21 00:05:44 +0800
committerHaishan <[email protected]>2019-04-21 21:58:33 +0800
commit882b168082ddbcbe7991a71a09944f1a60084fc3 (patch)
treed12345be635943537042a929aed8376ae4480324 /src
parenteda2501b1d68c6f82a4a824ffe12caf5be7b33f2 (diff)
squash: feat(config): add options to select traffic chart style
Diffstat (limited to 'src')
-rw-r--r--src/components/Config.js42
-rw-r--r--src/components/Selection.js61
-rw-r--r--src/components/Selection.module.css17
-rw-r--r--src/components/TrafficChart.js194
-rw-r--r--src/components/TrafficChartSample.js54
-rw-r--r--src/ducks/app.js22
-rw-r--r--src/hooks/useLineChart.js28
-rw-r--r--src/misc/chart.js115
8 files changed, 357 insertions, 176 deletions
diff --git a/src/components/Config.js b/src/components/Config.js
index 01bc4bd..f937692 100644
--- a/src/components/Config.js
+++ b/src/components/Config.js
@@ -3,15 +3,24 @@ import PropTypes from 'prop-types';
import { useStoreState, useActions } from 'm/store';
import { getConfigs, fetchConfigs, updateConfigs } from 'd/configs';
-import { clearStorage } from 'd/app';
+import {
+ clearStorage,
+ selectChartStyleIndex,
+ getSelectedChartStyleIndex
+} from 'd/app';
import ContentHeader from 'c/ContentHeader';
import Switch from 'c/Switch';
import ToggleSwitch from 'c/ToggleSwitch';
import Input from 'c/Input';
import Button from 'c/Button';
+import Selection from 'c/Selection';
+import TrafficChartSample from 'c/TrafficChartSample';
+
import s0 from 'c/Config.module.css';
+const propsList = [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }];
+
const optionsRule = [
{
label: 'Global',
@@ -51,11 +60,14 @@ const optionsLogLevel = [
];
const actions = {
+ selectChartStyleIndex,
fetchConfigs,
updateConfigs
};
-const mapStateToProps = s => ({ configs: getConfigs(s) });
+const mapStateToProps = s => ({
+ configs: getConfigs(s)
+});
export default function ConfigContainer() {
const { fetchConfigs } = useActions(actions);
@@ -66,8 +78,13 @@ export default function ConfigContainer() {
return <Config configs={configs} />;
}
+const mapStateToProps2 = s => ({
+ selectedChartStyleIndex: getSelectedChartStyleIndex(s)
+});
+
function Config({ configs }) {
- const { updateConfigs } = useActions(actions);
+ const { updateConfigs, selectChartStyleIndex } = useActions(actions);
+ const { selectedChartStyleIndex } = useStoreState(mapStateToProps2);
// configState to track component internal state
// prevConfigs to track external props.configs
const [configState, _setConfigState] = useState(configs);
@@ -129,6 +146,10 @@ function Config({ configs }) {
}
}
+ function handleChartStyleIndexOnChange(idx) {
+ selectChartStyleIndex(idx);
+ }
+
return (
<div>
<ContentHeader title="Config" />
@@ -198,8 +219,19 @@ function Config({ configs }) {
</div>
<div className={s0.section}>
- <div className={s0.label}>Actions</div>
- <Button label="Log out" onClick={clearStorage} />
+ <div>
+ <div className={s0.label}>Chart Style</div>
+ <Selection
+ OptionComponent={TrafficChartSample}
+ optionPropsList={propsList}
+ selectedIndex={selectedChartStyleIndex}
+ onChange={handleChartStyleIndexOnChange}
+ />
+ </div>
+ <div>
+ <div className={s0.label}>Action</div>
+ <Button label="Log out" onClick={clearStorage} />
+ </div>
</div>
</div>
);
diff --git a/src/components/Selection.js b/src/components/Selection.js
new file mode 100644
index 0000000..12816f9
--- /dev/null
+++ b/src/components/Selection.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { func, array, number } from 'prop-types';
+import cx from 'classnames';
+
+import s from './Selection.module.css';
+
+export default function Selection({
+ OptionComponent,
+ optionPropsList,
+ selectedIndex,
+ onChange
+}) {
+ return (
+ // TODO a11y
+ // tabIndex="0"
+ <div className={s.root}>
+ {optionPropsList.map((props, idx) => {
+ const className = cx(s.item, { [s.itemActive]: idx === selectedIndex });
+ return (
+ <div
+ key={idx}
+ className={className}
+ onClick={ev => {
+ ev.preventDefault();
+ if (idx !== selectedIndex) {
+ onChange(idx);
+ }
+ }}
+ >
+ <OptionComponent {...props} />
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+
+Selection.propTypes = {
+ OptionComponent: func,
+ optionPropsList: array,
+ selectedIndex: number,
+ onChange: func
+};
+
+// for test
+export function Option({ title }) {
+ // eslint-disable-next-line no-undef
+ if (__DEV__) {
+ return (
+ <div
+ style={{
+ width: 100,
+ height: 60,
+ backgroundColor: '#eee'
+ }}
+ >
+ {title}
+ </div>
+ );
+ }
+}
diff --git a/src/components/Selection.module.css b/src/components/Selection.module.css
new file mode 100644
index 0000000..a562058
--- /dev/null
+++ b/src/components/Selection.module.css
@@ -0,0 +1,17 @@
+.root {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.item {
+ flex-grow: 0;
+ flex-wrap: 0;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ cursor: pointer;
+ border: 2px solid transparent;
+}
+
+.itemActive {
+ border-color: #387cec;
+}
diff --git a/src/components/TrafficChart.js b/src/components/TrafficChart.js
index 84773c8..cd9e853 100644
--- a/src/components/TrafficChart.js
+++ b/src/components/TrafficChart.js
@@ -1,193 +1,47 @@
-import React, { useEffect } from 'react';
-import prettyBytes from 'm/pretty-bytes';
+import React, { useMemo } from 'react';
import { fetchData } from '../api/traffic';
-import { unstable_createResource as createResource } from '@hsjs/react-cache';
+import useLineChart from '../hooks/useLineChart';
import { useStoreState } from 'm/store';
-import { getClashAPIConfig, getTheme } from 'd/app';
-
-// const delay = ms => new Promise(r => setTimeout(r, ms));
-const chartJSResource = createResource(() => {
- return import(/* webpackChunkName: "chartjs" */
- /* webpackPrefetch: true */
- /* webpackPreload: true */
- 'chart.js/dist/Chart.min.js').then(c => c.default);
-});
-
-const colorCombo = [
- {
- 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)'
- }
- },
- {
- up: {
- backgroundColor: 'rgb(98, 190, 100)',
- borderColor: 'rgb(78,146,79)'
- },
- down: {
- backgroundColor: 'rgb(160, 230, 66)',
- borderColor: 'rgb(110, 156, 44)'
- }
- },
- {
- up: {
- backgroundColor: 'rgba(94, 175, 223, 0.3)',
- borderColor: 'rgb(94, 175, 223)'
- },
- down: {
- backgroundColor: 'rgba(139, 227, 195, 0.3)',
- borderColor: 'rgb(139, 227, 195)'
- }
- },
- {
- 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 commonDataSetProps = {
- borderWidth: 1,
- lineTension: 0,
- pointRadius: 0
-};
-
-function getColorComboIndexByTheme(theme) {
- return theme === 'dark' ? 0 : 2;
-}
-
-function getUploadProps(theme = 'dark') {
- const i = getColorComboIndexByTheme(theme);
- return {
- ...commonDataSetProps,
- ...colorCombo[i].up,
- label: 'Up'
- };
-}
-
-function getDownloadProps(theme = 'dark') {
- const i = getColorComboIndexByTheme(theme);
- return {
- ...commonDataSetProps,
- ...colorCombo[i].down,
- label: 'Down'
- };
-}
-
-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 ';
- }
- }
- }
- ]
- }
-};
+import { getClashAPIConfig, getSelectedChartStyleIndex } from 'd/app';
+import { chartJSResource, commonDataSetProps, chartStyles } from 'm/chart';
const chartWrapperStyle = {
// make chartjs chart responsive
position: 'relative',
- width: '90%'
+ maxWidth: 1000
};
+const mapStateToProps = s => ({
+ selectedChartStyleIndex: getSelectedChartStyleIndex(s)
+});
+
export default function TrafficChart() {
const Chart = chartJSResource.read();
const { hostname, port, secret } = useStoreState(getClashAPIConfig);
- const theme = useStoreState(getTheme);
-
- useEffect(() => {
- const ctx = document.getElementById('trafficChart').getContext('2d');
- const traffic = fetchData({ hostname, port, secret });
- const upProps = getUploadProps(theme);
- const downProps = getDownloadProps(theme);
- const data = {
+ const { selectedChartStyleIndex } = useStoreState(mapStateToProps);
+ const traffic = fetchData({ hostname, port, secret });
+ const data = useMemo(
+ () => ({
labels: traffic.labels,
datasets: [
{
- ...upProps,
+ ...commonDataSetProps,
+ ...chartStyles[selectedChartStyleIndex].up,
+ label: 'Up',
data: traffic.up
},
{
- ...downProps,
+ ...commonDataSetProps,
+ ...chartStyles[selectedChartStyleIndex].down,
+ label: 'Down',
data: traffic.down
}
]
- };
- const c = new Chart(ctx, {
- type: 'line',
- data,
- options
- });
- const unsubscribe = traffic.subscribe(() => c.update());
- return () => {
- unsubscribe();
- c.destroy();
- };
- }, [hostname, port, secret, theme]);
+ }),
+ [traffic, selectedChartStyleIndex]
+ );
+
+ useLineChart(Chart, 'trafficChart', data, traffic);
return (
<div style={chartWrapperStyle}>
diff --git a/src/components/TrafficChartSample.js b/src/components/TrafficChartSample.js
new file mode 100644
index 0000000..772f6d2
--- /dev/null
+++ b/src/components/TrafficChartSample.js
@@ -0,0 +1,54 @@
+import React, { useMemo } from 'react';
+import useLineChart from '../hooks/useLineChart';
+import { chartJSResource, commonDataSetProps, chartStyles } from 'm/chart';
+
+const extraChartOptions = {
+ legend: {
+ display: false
+ },
+ scales: {
+ xAxes: [{ display: false }],
+ yAxes: [{ display: false }]
+ }
+};
+
+const data1 = [23e3, 35e3, 46e3, 33e3, 90e3, 68e3, 23e3, 45e3];
+const data2 = [184e3, 183e3, 196e3, 182e3, 190e3, 186e3, 182e3, 189e3];
+const labels = data1;
+
+export default function TrafficChart({ id }) {
+ const Chart = chartJSResource.read();
+
+ const data = useMemo(
+ () => ({
+ labels,
+ datasets: [
+ {
+ ...commonDataSetProps,
+ ...chartStyles[id].up,
+ data: data1
+ },
+ {
+ ...commonDataSetProps,
+ ...chartStyles[id].down,
+ data: data2
+ }
+ ]
+ }),
+ []
+ );
+
+ const eleId = 'chart-' + id;
+ useLineChart(Chart, eleId, data, null, extraChartOptions);
+
+ return (
+ <div
+ style={{
+ width: 150,
+ padding: 5
+ }}
+ >
+ <canvas id={eleId} />
+ </div>
+ );
+}
diff --git a/src/ducks/app.js b/src/ducks/app.js
index 399a66e..3f77970 100644
--- a/src/ducks/app.js
+++ b/src/ducks/app.js
@@ -4,9 +4,11 @@ import { closeModal } from 'd/modals';
const UpdateClashAPIConfig = 'app/UpdateClashAPIConfig';
const SwitchTheme = 'app/SwitchTheme';
+const SELECT_CHART_STYLE_INDEX = 'app/SELECT_CHART_STYLE_INDEX';
export const getClashAPIConfig = s => s.app.clashAPIConfig;
export const getTheme = s => s.app.theme;
+export const getSelectedChartStyleIndex = s => s.app.selectedChartStyleIndex;
export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
return async (dispatch, getState) => {
@@ -56,6 +58,19 @@ export function clearStorage() {
}
}
+export function selectChartStyleIndex(selectedChartStyleIndex) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: SELECT_CHART_STYLE_INDEX,
+ payload: {
+ selectedChartStyleIndex
+ }
+ });
+ // side effect
+ saveState(getState().app);
+ };
+}
+
// type Theme = 'light' | 'dark';
const defaultState = {
clashAPIConfig: {
@@ -63,6 +78,7 @@ const defaultState = {
port: '7892',
secret: ''
},
+ selectedChartStyleIndex: 0,
theme: 'dark'
};
@@ -80,7 +96,7 @@ function parseConfigQueryString() {
function getInitialState() {
let s = loadState();
- if (!s) s = defaultState;
+ s = { ...defaultState, ...s };
// TODO flat clashAPIConfig?
const configQuery = parseConfigQueryString();
@@ -105,6 +121,10 @@ export default function reducer(state = getInitialState(), { type, payload }) {
return { ...state, ...payload };
}
+ case SELECT_CHART_STYLE_INDEX: {
+ return { ...state, ...payload };
+ }
+
default:
return state;
}
diff --git a/src/hooks/useLineChart.js b/src/hooks/useLineChart.js
new file mode 100644
index 0000000..2ab23a0
--- /dev/null
+++ b/src/hooks/useLineChart.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { commonChartOptions } from 'm/chart';
+
+const { useEffect } = React;
+const options = commonChartOptions;
+
+export default function useLineChart(
+ Chart,
+ elementId,
+ data,
+ subscription,
+ extraChartOptions = {}
+) {
+ useEffect(() => {
+ const ctx = document.getElementById(elementId).getContext('2d');
+ const c = new Chart(ctx, {
+ type: 'line',
+ data,
+ options: { ...options, ...extraChartOptions }
+ });
+ const unsubscribe =
+ subscription && subscription.subscribe(() => c.update());
+ return () => {
+ unsubscribe && unsubscribe();
+ c.destroy();
+ };
+ }, [Chart, elementId, data, subscription]);
+}
diff --git a/src/misc/chart.js b/src/misc/chart.js
new file mode 100644
index 0000000..0838744
--- /dev/null
+++ b/src/misc/chart.js
@@ -0,0 +1,115 @@
+import { unstable_createResource as createResource } from '@hsjs/react-cache';
+import prettyBytes from 'm/pretty-bytes';
+
+export const chartJSResource = createResource(() => {
+ return import(
+ /* webpackChunkName: "chartjs" */
+ /* webpackPrefetch: true */
+ /* webpackPreload: true */
+ 'chart.js/dist/Chart.min.js'
+ ).then(c => c.default);
+});
+
+export const commonDataSetProps = {
+ borderWidth: 1,
+ lineTension: 0,
+ pointRadius: 0
+};
+
+export const commonChartOptions = {
+ responsive: true,
+ maintainAspectRatio: true,
+ title: {
+ display: false
+ },
+ legend: {
+ display: true,
+ position: 'top',
+ labels: {
+ fontColor: '#ccc',
+ boxWidth: 20
+ }
+ },
+ tooltips: {
+ enabled: false,
+ mode: 'index',
+ intersect: false,
+ animationDuration: 100
+ },
+ 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 ';
+ }
+ }
+ }
+ ]
+ }
+};
+
+export const chartStyles = [
+ {
+ 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)'
+ }
+ },
+ {
+ up: {
+ backgroundColor: 'rgb(98, 190, 100)',
+ borderColor: 'rgb(78,146,79)'
+ },
+ down: {
+ backgroundColor: 'rgb(160, 230, 66)',
+ borderColor: 'rgb(110, 156, 44)'
+ }
+ },
+ {
+ up: {
+ backgroundColor: 'rgba(94, 175, 223, 0.3)',
+ borderColor: 'rgb(94, 175, 223)'
+ },
+ down: {
+ backgroundColor: 'rgba(139, 227, 195, 0.3)',
+ borderColor: 'rgb(139, 227, 195)'
+ }
+ },
+ {
+ 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)'
+ }
+ }
+];
+
+// TODO to remove
+export const colorCombo = chartStyles;