summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrookisbusy <[email protected]>2023-04-07 20:43:19 +0800
committerrookisbusy <[email protected]>2023-04-07 20:43:19 +0800
commit2a3579e447459aaeb7998cf968f73c6ef7ff7a0b (patch)
tree5422a8cc8732066ae3d6ef2563b939d03087c20c
parente6852509670fa91fda6a2ef2375608a7b9a4b175 (diff)
feat: add memory chat
-rw-r--r--src/api/memory.ts119
-rw-r--r--src/components/Home.tsx2
-rw-r--r--src/components/MemoryChart.tsx61
-rw-r--r--src/hooks/useLineChart.ts20
-rw-r--r--src/i18n/zh.ts1
-rw-r--r--src/misc/chart-memory.ts64
-rw-r--r--src/misc/chart.ts1
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 ';
},