diff options
| author | rookisbusy <[email protected]> | 2023-04-07 20:43:19 +0800 |
|---|---|---|
| committer | rookisbusy <[email protected]> | 2023-04-07 20:43:19 +0800 |
| commit | 2a3579e447459aaeb7998cf968f73c6ef7ff7a0b (patch) | |
| tree | 5422a8cc8732066ae3d6ef2563b939d03087c20c /src | |
| parent | e6852509670fa91fda6a2ef2375608a7b9a4b175 (diff) | |
feat: add memory chat
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/memory.ts | 119 | ||||
| -rw-r--r-- | src/components/Home.tsx | 2 | ||||
| -rw-r--r-- | src/components/MemoryChart.tsx | 61 | ||||
| -rw-r--r-- | src/hooks/useLineChart.ts | 20 | ||||
| -rw-r--r-- | src/i18n/zh.ts | 1 | ||||
| -rw-r--r-- | src/misc/chart-memory.ts | 64 | ||||
| -rw-r--r-- | src/misc/chart.ts | 1 |
7 files changed, 268 insertions, 0 deletions
diff --git a/src/api/memory.ts b/src/api/memory.ts new file mode 100644 index 0000000..b6614bb --- /dev/null +++ b/src/api/memory.ts @@ -0,0 +1,119 @@ +import { ClashAPIConfig } from '~/types'; + +import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper'; + +const endpoint = '/memory'; +const textDecoder = new TextDecoder('utf-8'); + +const Size = 150; + +const memory = { + labels: Array(Size).fill(0), + inuse: Array(Size), + oslimit: Array(Size), + + size: Size, + subscribers: [], + appendData(o: { inuse: number; oslimit: number }) { + this.inuse.shift(); + this.oslimit.shift(); + this.labels.shift(); + + const l = Date.now(); + this.inuse.push(o.inuse); + this.oslimit.push(o.oslimit); + this.labels.push(l); + + this.subscribers.forEach((f) => f(o)); + }, + + subscribe(listener: (x: any) => void) { + this.subscribers.push(listener); + return () => { + const idx = this.subscribers.indexOf(listener); + this.subscribers.splice(idx, 1); + }; + }, +}; + +let fetched = false; +let decoded = ''; + +function parseAndAppend(x: string) { + memory.appendData(JSON.parse(x)); +} + +function pump(reader: ReadableStreamDefaultReader) { + return reader.read().then(({ done, value }) => { + const str = textDecoder.decode(value, { stream: !done }); + decoded += str; + + const splits = decoded.split('\n'); + + const lastSplit = splits[splits.length - 1]; + + for (let i = 0; i < splits.length - 1; i++) { + parseAndAppend(splits[i]); + } + + if (done) { + parseAndAppend(lastSplit); + decoded = ''; + + // eslint-disable-next-line no-console + console.log('GET /memory streaming done'); + fetched = false; + return; + } else { + decoded = lastSplit; + } + return pump(reader); + }); +} + +// 1 OPEN +// other value CLOSED +// similar to ws readyState but not the same +// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState +let wsState: number; +function fetchData(apiConfig: ClashAPIConfig) { + if (fetched || wsState === 1) return memory; + wsState = 1; + const url = buildWebSocketURL(apiConfig, endpoint); + const ws = new WebSocket(url); + ws.addEventListener('error', function (_ev) { + wsState = 3; + }); + ws.addEventListener('close', function (_ev) { + wsState = 3; + fetchDataWithFetch(apiConfig); + }); + ws.addEventListener('message', function (event) { + parseAndAppend(event.data); + }); + return memory; +} + +function fetchDataWithFetch(apiConfig: ClashAPIConfig) { + if (fetched) return memory; + fetched = true; + const { url, init } = getURLAndInit(apiConfig); + fetch(url + endpoint, init).then( + (response) => { + if (response.ok) { + const reader = response.body.getReader(); + pump(reader); + } else { + fetched = false; + } + }, + (err) => { + // eslint-disable-next-line no-console + console.log('fetch /memory error', err); + fetched = false; + } + ); + return memory; +} + +export { fetchData }; diff --git a/src/components/Home.tsx b/src/components/Home.tsx index d7ddbab..6687fbb 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import ContentHeader from './ContentHeader'; import s0 from './Home.module.scss'; import Loading from './Loading'; +import MemoryChart from './MemoryChart'; import TrafficChart from './TrafficChart'; import TrafficNow from './TrafficNow'; @@ -19,6 +20,7 @@ export default function Home() { <div className={s0.chart}> <Suspense fallback={<Loading height="200px" />}> <TrafficChart /> + <MemoryChart /> </Suspense> </div> </div> diff --git a/src/components/MemoryChart.tsx b/src/components/MemoryChart.tsx new file mode 100644 index 0000000..9f6febb --- /dev/null +++ b/src/components/MemoryChart.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { State } from '~/store/types'; + +import { fetchData } from '../api/memory'; +import { useLineChartMemory } from '../hooks/useLineChart'; +import { + chartJSResource, + chartStyles, + commonDataSetProps, + memoryChartOptions, +} from '../misc/chart-memory'; +import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app'; +import { connect } from './StateProvider'; + +const { useMemo } = React; + +const chartWrapperStyle = { + // make chartjs chart responsive + position: 'relative', + maxWidth: 1000, + marginTop: '1em', +}; + +const mapState = (s: State) => ({ + apiConfig: getClashAPIConfig(s), + selectedChartStyleIndex: getSelectedChartStyleIndex(s), +}); + +export default connect(mapState)(MemoryChart); + +function MemoryChart({ apiConfig, selectedChartStyleIndex }) { + const ChartMod = chartJSResource.read(); + const memory = fetchData(apiConfig); + const { t } = useTranslation(); + const data = useMemo( + () => ({ + labels: memory.labels, + datasets: [ + { + ...commonDataSetProps, + ...memoryChartOptions, + ...chartStyles[selectedChartStyleIndex].inuse, + label: t('Memory'), + data: memory.inuse, + }, + ], + }), + [memory, selectedChartStyleIndex, t] + ); + + useLineChartMemory(ChartMod.Chart, 'MemoryChart', data, memory); + + return ( + // @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message + <div style={chartWrapperStyle}> + <canvas id="MemoryChart" /> + </div> + ); +} diff --git a/src/hooks/useLineChart.ts b/src/hooks/useLineChart.ts index 88ee660..62b306b 100644 --- a/src/hooks/useLineChart.ts +++ b/src/hooks/useLineChart.ts @@ -2,6 +2,7 @@ import type { ChartConfiguration } from 'chart.js'; import React from 'react'; import { commonChartOptions } from '~/misc/chart'; +import { memoryChartOptions } from '~/misc/chart-memory'; const { useEffect } = React; @@ -23,3 +24,22 @@ export default function useLineChart( }; }, [chart, elementId, data, subscription, extraChartOptions]); } + +export function useLineChartMemory( + chart: typeof import('chart.js').Chart, + elementId: string, + data: ChartConfiguration['data'], + subscription: any, + extraChartOptions = {} +) { + useEffect(() => { + const ctx = (document.getElementById(elementId) as HTMLCanvasElement).getContext('2d'); + const options = { ...memoryChartOptions, ...extraChartOptions }; + const c = new chart(ctx, { type: 'line', data, options }); + const unsubscribe = subscription && subscription.subscribe(() => c.update()); + return () => { + unsubscribe && unsubscribe(); + c.destroy(); + }; + }, [chart, elementId, data, subscription, extraChartOptions]); +} diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 1fb9d26..0016159 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -11,6 +11,7 @@ export const data = { 'Download Total': '下载总量', 'Active Connections': '活动连接', 'Memory Total': '内存用量', + Memory: '内存', 'Pause Refresh': '暂停刷新', 'Resume Refresh': '继续刷新', close_all_connections: '关闭所有连接', diff --git a/src/misc/chart-memory.ts b/src/misc/chart-memory.ts new file mode 100644 index 0000000..4887b7f --- /dev/null +++ b/src/misc/chart-memory.ts @@ -0,0 +1,64 @@ +import { createAsset } from 'use-asset'; + +import prettyBytes from './pretty-bytes'; +export const chartJSResource = createAsset(() => { + return import('~/misc/chart-lib'); +}); + +export const commonDataSetProps = { borderWidth: 1, pointRadius: 0, tension: 0.2, fill: true }; + +export const memoryChartOptions: import('chart.js').ChartOptions<'line'> = { + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { labels: { boxWidth: 20 } }, + }, + scales: { + x: { display: false, type: 'category' }, + y: { + type: 'linear', + display: true, + grid: { + display: true, + color: '#555', + drawTicks: false, + }, + border: { + dash: [3, 6], + }, + ticks: { + maxTicksLimit: 3, + callback(value: number) { + return prettyBytes(value); + }, + }, + }, + }, +}; + +export const chartStyles = [ + { + inuse: { + backgroundColor: 'rgba( 116, 162, 249, 0.8)', + borderColor: 'rgb(116, 162, 249)', + }, + }, + { + inuse: { + backgroundColor: 'rgb(98, 190, 100)', + borderColor: 'rgb(78,146,79)', + }, + }, + { + inuse: { + backgroundColor: 'rgba(94, 175, 223, 0.3)', + borderColor: 'rgb(94, 175, 223)', + }, + }, + { + inuse: { + backgroundColor: 'rgba(242, 174, 62, 0.3)', + borderColor: 'rgb(242, 174, 62)', + }, + }, +]; diff --git a/src/misc/chart.ts b/src/misc/chart.ts index 56ffe87..07b1b51 100644 --- a/src/misc/chart.ts +++ b/src/misc/chart.ts @@ -27,6 +27,7 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = { dash: [3, 6], }, ticks: { + maxTicksLimit: 5, callback(value: number) { return prettyBytes(value) + '/s '; }, |
