Add float action button to pause/start log streaming

This commit is contained in:
Haishan 2021-11-12 20:39:01 +08:00
parent b1ea08a4ee
commit ce3ed3d99f
6 changed files with 50 additions and 50 deletions

View file

@ -2,6 +2,10 @@
"name": "yacd", "name": "yacd",
"version": "0.3.3", "version": "0.3.3",
"description": "Yet another Clash dashboard", "description": "Yet another Clash dashboard",
"prettier": {
"printWidth": 100,
"singleQuote": true
},
"scripts": { "scripts": {
"lint": "eslint --fix --cache src", "lint": "eslint --fix --cache src",
"dev": "vite", "dev": "vite",

View file

@ -1,3 +0,0 @@
module.exports = {
singleQuote: true
};

View file

@ -5,6 +5,12 @@ import { LogsAPIConfig } from 'src/types';
import { buildLogsWebSocketURL, getURLAndInit } from '../misc/request-helper'; import { buildLogsWebSocketURL, getURLAndInit } from '../misc/request-helper';
type AppendLogFn = (x: Log) => void; type AppendLogFn = (x: Log) => void;
enum WebSocketReadyState {
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
}
const endpoint = '/logs'; const endpoint = '/logs';
const textDecoder = new TextDecoder('utf-8'); const textDecoder = new TextDecoder('utf-8');
@ -86,20 +92,13 @@ function makeConnStr(c: LogsAPIConfig) {
let prevConnStr: string; let prevConnStr: string;
let controller: AbortController; let controller: AbortController;
// 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;
export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) { export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) {
if (apiConfig.logLevel === 'uninit') return; if (apiConfig.logLevel === 'uninit') return;
if (fetched || wsState === 1) return; if (fetched || (ws && ws.readyState === WebSocketReadyState.Open)) return;
prevAppendLogFn = appendLog; prevAppendLogFn = appendLog;
wsState = 1;
const url = buildLogsWebSocketURL(apiConfig, endpoint); const url = buildLogsWebSocketURL(apiConfig, endpoint);
ws = new WebSocket(url); ws = new WebSocket(url);
ws.addEventListener('error', () => { ws.addEventListener('error', () => {
wsState = 3;
fetchLogsWithFetch(apiConfig, appendLog); fetchLogsWithFetch(apiConfig, appendLog);
}); });
ws.addEventListener('message', function (event) { ws.addEventListener('message', function (event) {
@ -107,10 +106,14 @@ export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) {
}); });
} }
export function stop() {
ws.close();
if (controller) controller.abort();
}
export function reconnect(apiConfig: LogsAPIConfig) { export function reconnect(apiConfig: LogsAPIConfig) {
if (!prevAppendLogFn || !ws) return; if (!prevAppendLogFn || !ws) return;
ws.close(); ws.close();
wsState = 3;
fetched = false; fetched = false;
fetchLogs(apiConfig, prevAppendLogFn); fetchLogs(apiConfig, prevAppendLogFn);
} }

View file

@ -235,11 +235,7 @@ function Conn({ apiConfig }) {
isRefreshPaused ? <Play size={16} /> : <Pause size={16} /> isRefreshPaused ? <Play size={16} /> : <Pause size={16} />
} }
mainButtonStyles={ mainButtonStyles={
isRefreshPaused isRefreshPaused ? { background: '#e74c3c' } : {}
? {
background: '#e74c3c',
}
: {}
} }
style={fabPosition} style={fabPosition}
text={isRefreshPaused ? 'Resume Refresh' : 'Pause Refresh'} text={isRefreshPaused ? 'Resume Refresh' : 'Pause Refresh'}

View file

@ -1,12 +1,8 @@
import cx from 'clsx'; import cx from 'clsx';
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { areEqual, FixedSizeList as List, ListChildComponentProps } from 'react-window';
areEqual, import { fetchLogs, stop as stopLogs, reconnect as reconnectLogs } from 'src/api/logs';
FixedSizeList as List,
ListChildComponentProps,
} from 'react-window';
import { fetchLogs } from 'src/api/logs';
import ContentHeader from 'src/components/ContentHeader'; import ContentHeader from 'src/components/ContentHeader';
import LogSearch from 'src/components/LogSearch'; import LogSearch from 'src/components/LogSearch';
import { connect } from 'src/components/StateProvider'; import { connect } from 'src/components/StateProvider';
@ -16,10 +12,12 @@ import { getClashAPIConfig } from 'src/store/app';
import { getLogLevel } from 'src/store/configs'; import { getLogLevel } from 'src/store/configs';
import { appendLog, getLogsForDisplay } from 'src/store/logs'; import { appendLog, getLogsForDisplay } from 'src/store/logs';
import { Log, State } from 'src/store/types'; import { Log, State } from 'src/store/types';
import { Pause, Play } from 'react-feather';
import { Fab, position as fabPosition } from './shared/Fab';
import s from './Logs.module.scss'; import s from './Logs.module.scss';
const { useCallback, memo, useEffect } = React; const { useState, useCallback, memo, useEffect } = React;
const paddingBottom = 30; const paddingBottom = 30;
const colors = { const colors = {
@ -51,23 +49,22 @@ function itemKey(index: number, data: LogLineProps[]) {
return item.id; return item.id;
} }
const Row = memo( const Row = memo(({ index, style, data }: ListChildComponentProps<LogLineProps>) => {
({ index, style, data }: ListChildComponentProps<LogLineProps>) => {
const r = data[index]; const r = data[index];
return ( return (
<div style={style}> <div style={style}>
<LogLine {...r} /> <LogLine {...r} />
</div> </div>
); );
}, }, areEqual);
areEqual
);
function Logs({ dispatch, logLevel, apiConfig, logs }) { function Logs({ dispatch, logLevel, apiConfig, logs }) {
const appendLogInternal = useCallback( const [isRefreshPaused, setIsRefreshPaused] = useState(false);
(log) => dispatch(appendLog(log)), const toggleIsRefreshPaused = useCallback(() => {
[dispatch] isRefreshPaused ? reconnectLogs({ ...apiConfig, logLevel }) : stopLogs();
); setIsRefreshPaused((x) => !x);
}, [isRefreshPaused, apiConfig, logLevel]);
const appendLogInternal = useCallback((log) => dispatch(appendLog(log)), [dispatch]);
useEffect(() => { useEffect(() => {
fetchLogs({ ...apiConfig, logLevel }, appendLogInternal); fetchLogs({ ...apiConfig, logLevel }, appendLogInternal);
}, [apiConfig, logLevel, appendLogInternal]); }, [apiConfig, logLevel, appendLogInternal]);
@ -80,10 +77,7 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
<LogSearch /> <LogSearch />
<div ref={refLogsContainer} style={{ paddingBottom }}> <div ref={refLogsContainer} style={{ paddingBottom }}>
{logs.length === 0 ? ( {logs.length === 0 ? (
<div <div className={s.logPlaceholder} style={{ height: containerHeight - paddingBottom }}>
className={s.logPlaceholder}
style={{ height: containerHeight - paddingBottom }}
>
<div className={s.logPlaceholderIcon}> <div className={s.logPlaceholderIcon}>
<SvgYacd width={200} height={200} /> <SvgYacd width={200} height={200} />
</div> </div>
@ -101,6 +95,14 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
> >
{Row} {Row}
</List> </List>
<Fab
icon={isRefreshPaused ? <Play size={16} /> : <Pause size={16} />}
mainButtonStyles={isRefreshPaused ? { background: '#e74c3c' } : {}}
style={fabPosition}
text={isRefreshPaused ? 'Resume Refresh' : 'Pause Refresh'}
onClick={toggleIsRefreshPaused}
></Fab>
</div> </div>
)} )}
</div> </div>

View file

@ -53,18 +53,14 @@
:root { :root {
--font-mono: 'Roboto Mono', Menlo, monospace; --font-mono: 'Roboto Mono', Menlo, monospace;
--font-normal: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, // prettier-ignore
Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, --font-normal: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
--color-focus-blue: #1a73e8; --color-focus-blue: #1a73e8;
--btn-bg: #387cec; --btn-bg: #387cec;
} }
body { body {
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, font-family: var(--font-normal);
Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol, 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial,
sans-serif;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -135,10 +131,12 @@ body {
:root[data-theme='dark'] { :root[data-theme='dark'] {
@include dark; @include dark;
color-scheme: dark;
} }
:root[data-theme='light'] { :root[data-theme='light'] {
@include light; @include light;
color-scheme: light;
} }
.flexCenter { .flexCenter {