Add float action button to pause/start log streaming
This commit is contained in:
parent
b1ea08a4ee
commit
ce3ed3d99f
6 changed files with 50 additions and 50 deletions
|
@ -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",
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
singleQuote: true
|
|
||||||
};
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue