Minor log style tweak

This commit is contained in:
Haishan 2021-05-31 21:50:46 +08:00
parent d4015f6423
commit fcab7cad4f
10 changed files with 101 additions and 68 deletions

View file

@ -1,14 +1,9 @@
import { LogsAPIConfig } from 'src/types'; import { pad0 } from 'src/misc/utils';
import { Log, LogsAPIConfig } from 'src/types';
import { buildLogsWebSocketURL, getURLAndInit } from '../misc/request-helper'; import { buildLogsWebSocketURL, getURLAndInit } from '../misc/request-helper';
type LogEntry = { type AppendLogFn = (x: Log) => void;
time?: string;
id?: string;
even?: boolean;
// and some other props
};
type AppendLogFn = (x: LogEntry) => void;
const endpoint = '/logs'; const endpoint = '/logs';
const textDecoder = new TextDecoder('utf-8'); const textDecoder = new TextDecoder('utf-8');
@ -22,7 +17,7 @@ let fetched = false;
let decoded = ''; let decoded = '';
function appendData(s: string, callback: AppendLogFn) { function appendData(s: string, callback: AppendLogFn) {
let o: LogEntry; let o: Partial<Log>;
try { try {
o = JSON.parse(s); o = JSON.parse(s);
} catch (err) { } catch (err) {
@ -31,7 +26,7 @@ function appendData(s: string, callback: AppendLogFn) {
} }
const now = new Date(); const now = new Date();
const time = now.toLocaleString('zh-Hans'); const time = formatDate(now);
// mutate input param in place intentionally // mutate input param in place intentionally
o.time = time; o.time = time;
o.id = +now - 0 + getRandomStr(); o.id = +now - 0 + getRandomStr();
@ -39,6 +34,17 @@ function appendData(s: string, callback: AppendLogFn) {
callback(o); callback(o);
} }
function formatDate(d: Date) {
// 19-03-09 12:45
const YY = d.getFullYear() % 100;
const MM = pad0(d.getMonth() + 1, 2);
const dd = pad0(d.getDate(), 2);
const HH = pad0(d.getHours(), 2);
const mm = pad0(d.getMinutes(), 2);
const ss = pad0(d.getSeconds(), 2);
return `${YY}-${MM}-${dd} ${HH}:${mm}:${ss}`;
}
function pump(reader: ReadableStreamDefaultReader, appendLog: AppendLogFn) { function pump(reader: ReadableStreamDefaultReader, appendLog: AppendLogFn) {
return reader.read().then(({ done, value }) => { return reader.read().then(({ done, value }) => {
const str = textDecoder.decode(value, { stream: !done }); const str = textDecoder.decode(value, { stream: !done });

View file

@ -3,7 +3,6 @@
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 0.9em; font-size: 0.9em;
padding: 10px;
} }
.logType { .logType {
@ -11,8 +10,7 @@
flex-shrink: 0; flex-shrink: 0;
text-align: center; text-align: center;
width: 66px; width: 66px;
background: green; border-radius: 100px;
border-radius: 5px;
padding: 3px 5px; padding: 3px 5px;
margin: 0 8px; margin: 0 8px;
} }

View file

@ -1,18 +1,23 @@
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 { areEqual, FixedSizeList as List } from 'react-window'; import {
areEqual,
FixedSizeList as List,
ListChildComponentProps,
} from 'react-window';
import { fetchLogs } from 'src/api/logs';
import ContentHeader from 'src/components/ContentHeader';
import LogSearch from 'src/components/LogSearch';
import { connect } from 'src/components/StateProvider';
import SvgYacd from 'src/components/SvgYacd';
import useRemainingViewPortHeight from 'src/hooks/useRemainingViewPortHeight';
import { getClashAPIConfig } from 'src/store/app';
import { getLogLevel } from 'src/store/configs';
import { appendLog, getLogsForDisplay } from 'src/store/logs';
import { Log,State } from 'src/store/types';
import { fetchLogs } from '../api/logs'; import s from './Logs.module.css';
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
import { getClashAPIConfig } from '../store/app';
import { getLogLevel } from '../store/configs';
import { appendLog, getLogsForDisplay } from '../store/logs';
import ContentHeader from './ContentHeader';
import s0 from './Logs.module.css';
import LogSearch from './LogSearch';
import { connect } from './StateProvider';
import SvgYacd from './SvgYacd';
const { useCallback, memo, useEffect } = React; const { useCallback, memo, useEffect } = React;
@ -20,48 +25,44 @@ const paddingBottom = 30;
const colors = { const colors = {
debug: 'none', debug: 'none',
// debug: '#8a8a8a', // debug: '#8a8a8a',
info: '#454545', info: 'var(--bg-log-info-tag)',
// info: '#147d14',
warning: '#b99105', warning: '#b99105',
error: '#c11c1c', error: '#c11c1c',
}; };
type LogLineProps = { type LogLineProps = Partial<Log>;
time?: string;
even?: boolean;
payload?: string;
type?: string;
};
function LogLine({ time, even, payload, type }: LogLineProps) { function LogLine({ time, even, payload, type }: LogLineProps) {
const className = cx({ even }, s0.log); const className = cx({ even }, s.log);
return ( return (
<div className={className}> <div className={className}>
<div className={s0.logMeta}> <div className={s.logMeta}>
<div className={s0.logTime}>{time}</div> <div className={s.logTime}>{time}</div>
<div className={s0.logType} style={{ backgroundColor: colors[type] }}> <div className={s.logType} style={{ backgroundColor: colors[type] }}>
{type} {type}
</div> </div>
<div className={s0.logText}>{payload}</div> <div className={s.logText}>{payload}</div>
</div> </div>
</div> </div>
); );
} }
function itemKey(index, data) { function itemKey(index: number, data: LogLineProps[]) {
const item = data[index]; const item = data[index];
return item.id; return item.id;
} }
// @ts-expect-error ts-migrate(2339) FIXME: Property 'index' does not exist on type '{ childre... Remove this comment to see the full error message const Row = memo(
const Row = memo(({ index, style, data }) => { ({ 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 appendLogInternal = useCallback(
@ -80,23 +81,20 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
<div> <div>
<ContentHeader title={t('Logs')} /> <ContentHeader title={t('Logs')} />
<LogSearch /> <LogSearch />
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message */}
<div ref={refLogsContainer} style={{ paddingBottom }}> <div ref={refLogsContainer} style={{ paddingBottom }}>
{logs.length === 0 ? ( {logs.length === 0 ? (
<div <div
className={s0.logPlaceholder} className={s.logPlaceholder}
// @ts-expect-error ts-migrate(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
style={{ height: containerHeight - paddingBottom }} style={{ height: containerHeight - paddingBottom }}
> >
<div className={s0.logPlaceholderIcon}> <div className={s.logPlaceholderIcon}>
<SvgYacd width={200} height={200} /> <SvgYacd width={200} height={200} />
</div> </div>
<div>{t('no_logs')}</div> <div>{t('no_logs')}</div>
</div> </div>
) : ( ) : (
<div className={s0.logsWrapper}> <div className={s.logsWrapper}>
<List <List
// @ts-expect-error ts-migrate(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
height={containerHeight - paddingBottom} height={containerHeight - paddingBottom}
width="100%" width="100%"
itemCount={logs.length} itemCount={logs.length}
@ -113,7 +111,7 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
); );
} }
const mapState = (s) => ({ const mapState = (s: State) => ({
logs: getLogsForDisplay(s), logs: getLogsForDisplay(s),
logLevel: getLogLevel(s), logLevel: getLogLevel(s),
apiConfig: getClashAPIConfig(s), apiConfig: getClashAPIConfig(s),

View file

@ -124,6 +124,7 @@ body.dark {
--color-btn-fg: #bebebe; --color-btn-fg: #bebebe;
--color-bg-proxy: #303030; --color-bg-proxy: #303030;
--color-row-odd: #282828; --color-row-odd: #282828;
--bg-log-info-tag: #454545;
--bg-modal: #1f1f20; --bg-modal: #1f1f20;
--bg-near-transparent: rgba(255, 255, 255, 0.1); --bg-near-transparent: rgba(255, 255, 255, 0.1);
--bg-tooltip: #111; --bg-tooltip: #111;
@ -151,6 +152,7 @@ body.light {
--color-btn-fg: #101010; --color-btn-fg: #101010;
--color-bg-proxy: #fafafa; --color-bg-proxy: #fafafa;
--color-row-odd: #f5f5f5; --color-row-odd: #f5f5f5;
--bg-log-info-tag: #888;
--bg-modal: #fbfbfb; --bg-modal: #fbfbfb;
--bg-near-transparent: rgba(0, 0, 0, 0.1); --bg-near-transparent: rgba(0, 0, 0, 0.1);
--bg-tooltip: #f0f0f0; --bg-tooltip: #f0f0f0;

View file

@ -1,5 +1,6 @@
.ToggleSwitch { .ToggleSwitch {
user-select: none; user-select: none;
border-radius: 4px;
border: 1px solid #525252; border: 1px solid #525252;
color: var(--color-text); color: var(--color-text);
background: var(--color-toggle-bg); background: var(--color-toggle-bg);

View file

@ -1,4 +1,4 @@
import React from 'react'; import * as React from 'react';
const { useState, useRef, useCallback, useLayoutEffect } = React; const { useState, useRef, useCallback, useLayoutEffect } = React;
@ -9,8 +9,10 @@ const { useState, useRef, useCallback, useLayoutEffect } = React;
* to the bottom of the view port * to the bottom of the view port
* *
*/ */
export default function useRemainingViewPortHeight() { export default function useRemainingViewPortHeight<
const ref = useRef(null); ElementType extends HTMLDivElement
>(): [React.MutableRefObject<ElementType>, number] {
const ref = useRef<ElementType>(null);
const [containerHeight, setContainerHeight] = useState(200); const [containerHeight, setContainerHeight] = useState(200);
const updateContainerHeight = useCallback(() => { const updateContainerHeight = useCallback(() => {
const { top } = ref.current.getBoundingClientRect(); const { top } = ref.current.getBoundingClientRect();

View file

@ -12,7 +12,7 @@ function genCommonHeaders({ secret }: { secret?: string }) {
return h; return h;
} }
function buildWebSocketURLBase(baseURL: string, params: URLSearchParams, endpoint: string) { function buildWebSocketURLBase(baseURL: string, params: URLSearchParams, endpoint: string) {
let qs = '?' + params.toString() const qs = '?' + params.toString()
const url = new URL(baseURL); const url = new URL(baseURL);
url.protocol === 'https:' ? (url.protocol = 'wss:') : (url.protocol = 'ws:'); url.protocol === 'https:' ? (url.protocol = 'wss:') : (url.protocol = 'ws:');
return `${trimTrailingSlash(url.href)}${endpoint}${qs}`; return `${trimTrailingSlash(url.href)}${endpoint}${qs}`;

View file

@ -31,3 +31,11 @@ export function debounce<T extends any[]>(
export function trimTrailingSlash(s: string) { export function trimTrailingSlash(s: string) {
return s.replace(/\/$/, ''); return s.replace(/\/$/, '');
} }
export function pad0(number: number | string, len: number): string {
let output = String(number);
while (output.length < len) {
output = '0' + output;
}
return output;
}

View file

@ -1,10 +1,12 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { DispatchFn, GetStateFn, Log, State } from './types';
const LogSize = 300; const LogSize = 300;
const getLogs = (s) => s.logs.logs; const getLogs = (s: State) => s.logs.logs;
const getTail = (s) => s.logs.tail; const getTail = (s: State) => s.logs.tail;
export const getSearchText = (s) => s.logs.searchText; export const getSearchText = (s: State) => s.logs.searchText;
export const getLogsForDisplay = createSelector( export const getLogsForDisplay = createSelector(
getLogs, getLogs,
getTail, getTail,
@ -25,16 +27,16 @@ export const getLogsForDisplay = createSelector(
} }
); );
export function updateSearchText(text) { export function updateSearchText(text: string) {
return (dispatch) => { return (dispatch: DispatchFn) => {
dispatch('logsUpdateSearchText', (s) => { dispatch('logsUpdateSearchText', (s) => {
s.logs.searchText = text.toLowerCase(); s.logs.searchText = text.toLowerCase();
}); });
}; };
} }
export function appendLog(log) { export function appendLog(log: Log) {
return (dispatch, getState) => { return (dispatch: DispatchFn, getState: GetStateFn) => {
const s = getState(); const s = getState();
const logs = getLogs(s); const logs = getLogs(s);
const tailCurr = getTail(s); const tailCurr = getTail(s);
@ -42,9 +44,7 @@ export function appendLog(log) {
// mutate intentionally for performance // mutate intentionally for performance
logs[tail] = log; logs[tail] = log;
dispatch('logsAppendLog', (s) => { dispatch('logsAppendLog', (s: State) => (s.logs.tail = tail));
s.logs.tail = tail;
});
}; };
} }

View file

@ -65,6 +65,22 @@ export type StateProxies = {
switchProxyCtx?: SwitchProxyCtx; switchProxyCtx?: SwitchProxyCtx;
}; };
///// store.logs
export type Log = {
time: string;
even: boolean;
payload: string;
type: string;
id: string;
};
export type StateLogs = {
searchText: string;
logs: Log[];
tail: number;
};
///// store.configs ///// store.configs
export type StateConfigs = { export type StateConfigs = {
@ -78,7 +94,9 @@ export type State = {
app: StateApp; app: StateApp;
configs: StateConfigs; configs: StateConfigs;
proxies: StateProxies; proxies: StateProxies;
logs: StateLogs;
}; };
export type GetStateFn = () => State; export type GetStateFn = () => State;
export interface DispatchFn { export interface DispatchFn {
(msg: string, change: (s: State) => void): void; (msg: string, change: (s: State) => void): void;