feat: add log display
parent
77c621b499
commit
043bcb3ae4
|
|
@ -1,16 +1,17 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type CheckboxProps = {
|
type CheckboxProps = {
|
||||||
|
disabled?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
value?: boolean
|
value?: boolean
|
||||||
onChange?: (value: boolean) => void
|
onChange?: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox: React.FC<CheckboxProps> = ({ value, onChange, children }) => {
|
export const Checkbox: React.FC<CheckboxProps> = ({ disabled, value, onChange, children }) => {
|
||||||
return <div className="form-control">
|
return <div className="form-control">
|
||||||
<label className="label cursor-pointer">
|
<label className="label cursor-pointer">
|
||||||
<span className="label-text">{children}</span>
|
<span className="label-text">{children}</span>
|
||||||
<input type="checkbox" checked={value ?? false} onChange={() => onChange?.(!value)} className="checkbox" />
|
<input type="checkbox" checked={value ?? false} disabled={disabled} onChange={() => onChange?.(!value)} className="checkbox" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { usePromise } from 'hooks/usePromise';
|
import { usePromise } from 'hooks/usePromise';
|
||||||
import React, { useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { canExport, getProfile, setProfile } from 'services/config';
|
import { canExport, getProfile, setProfile } from 'services/config';
|
||||||
import { run } from 'services/s3si';
|
import { run, useLog } from 'services/s3si';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
import { Loading } from './Loading';
|
import { Loading } from './Loading';
|
||||||
|
|
||||||
|
|
@ -39,16 +39,40 @@ export const RunPanel: React.FC<RunPanelProps> = () => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const disabled = !canExport(result);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Checkbox value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
<div className="tooltip" data-tip={disabled ? t('请先完成登录和stat.ink的API密钥设置') : undefined}>
|
||||||
<Checkbox value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
<Checkbox disabled={disabled || loading} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
||||||
<button
|
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||||
onClick={onClick}
|
<button
|
||||||
className={classNames('btn', {
|
onClick={onClick}
|
||||||
'btn-disabled': !canExport(result) || (!exportBattle && !exportCoop),
|
className={classNames('btn w-full', {
|
||||||
'loading': loading,
|
'btn-disabled': disabled || (!exportBattle && !exportCoop),
|
||||||
})}
|
'loading': loading,
|
||||||
>{t('导出')}</button>
|
})}
|
||||||
|
>{t('导出')}</button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LogPanelProps = {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogPanel: React.FC<LogPanelProps> = ({ className }) => {
|
||||||
|
const { renderedLogs } = useLog();
|
||||||
|
const div = useRef<HTMLDivElement>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (div.current) {
|
||||||
|
div.current.scrollTop = div.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [renderedLogs])
|
||||||
|
|
||||||
|
return <div ref={div} className={`bg-neutral overflow-auto rounded p-4 ${className}`}>
|
||||||
|
{renderedLogs.length === 0 && <pre><code>{t('欢迎! 请点击"导出"按钮开始使用.')}</code></pre>}
|
||||||
|
{renderedLogs.map((line, i) => <pre key={i}><code>{line}</code></pre>)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ body {
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
overflow: hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
@ -42,5 +43,5 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-card {
|
.full-card {
|
||||||
@apply card m-2 h-full;
|
@apply card p-2 h-full;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { LogProvider } from "services/s3si";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<LogProvider limit={100}>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
</BrowserRouter>
|
<App />
|
||||||
</React.StrictMode>
|
</BrowserRouter>
|
||||||
|
</LogProvider>
|
||||||
|
</React.StrictMode >
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ErrorContent } from 'components/ErrorContent';
|
import { ErrorContent } from 'components/ErrorContent';
|
||||||
import { Loading } from 'components/Loading';
|
import { Loading } from 'components/Loading';
|
||||||
import { RunPanel } from 'components/RunPanel';
|
import { LogPanel, RunPanel } from 'components/RunPanel';
|
||||||
import { STAT_INK } from 'constant';
|
import { STAT_INK } from 'constant';
|
||||||
import { usePromise } from 'hooks/usePromise';
|
import { usePromise } from 'hooks/usePromise';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
@ -28,14 +28,15 @@ export const Home: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <div className='flex p-2 w-full h-full gap-2'>
|
||||||
<div className='full-card'>
|
<div className='max-w-full md:max-w-sm flex-auto'>
|
||||||
<h1 className='mb-4'>{t('欢迎!')}</h1>
|
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
|
<LogPanel className='sm:hidden max-h-[10rem]' />
|
||||||
<RunPanel />
|
<RunPanel />
|
||||||
<Link to='/settings' className='btn'>{t('配置')}</Link>
|
<Link to='/settings' className='btn'>{t('配置')}</Link>
|
||||||
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<LogPanel className='hidden sm:block flex-1' />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,5 +60,5 @@ export async function setProfile(index: number, profile: Profile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canExport(profile: Profile): boolean {
|
export function canExport(profile: Profile): boolean {
|
||||||
return !!profile.state.loginState?.sessionToken
|
return !!(profile.state.loginState?.sessionToken && profile.state.statInkApiKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { invoke } from "@tauri-apps/api";
|
import { invoke } from "@tauri-apps/api";
|
||||||
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
||||||
import { ExportOpts, State } from "jsonrpc/types";
|
import { ExportOpts, Log, State } from "jsonrpc/types";
|
||||||
import { useCallback } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
const client = new JSONRPCClient<S3SIService>({
|
const client = new JSONRPCClient<S3SIService>({
|
||||||
transport: new StdioTransport()
|
transport: new StdioTransport()
|
||||||
}).getProxy();
|
}).getProxy();
|
||||||
|
const LOG_SUB = new Set<(logs: Log[]) => void>();
|
||||||
|
|
||||||
async function getLogs() {
|
async function getLogs() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -31,10 +32,57 @@ async function getLogs() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const cb of LOG_SUB) {
|
||||||
|
cb(r.result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getLogs()
|
getLogs()
|
||||||
|
|
||||||
|
const LOG_CONTEXT = createContext<{
|
||||||
|
logs: Log[],
|
||||||
|
renderedLogs: React.ReactNode[]
|
||||||
|
}>({
|
||||||
|
logs: [],
|
||||||
|
renderedLogs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useLog = () => {
|
||||||
|
return useContext(LOG_CONTEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLevel(log: Log) {
|
||||||
|
return `[${log.level.toUpperCase()}]`.padEnd(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLog(log: Log) {
|
||||||
|
return `${renderLevel(log)} ${log.msg.map(String).join(' ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogProvider: React.FC<{ limit?: number, children?: React.ReactNode }> = ({ children, limit = 10 }) => {
|
||||||
|
const [logs, setLogs] = useState<Log[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cb = (logs: Log[]) => {
|
||||||
|
setLogs(old => [...old, ...logs].slice(-limit));
|
||||||
|
}
|
||||||
|
LOG_SUB.add(cb);
|
||||||
|
return () => {
|
||||||
|
LOG_SUB.delete(cb);
|
||||||
|
}
|
||||||
|
}, [limit])
|
||||||
|
|
||||||
|
|
||||||
|
const renderedLogs = useMemo(() => logs.map(renderLog), [logs])
|
||||||
|
|
||||||
|
return <LOG_CONTEXT.Provider value={{
|
||||||
|
logs,
|
||||||
|
renderedLogs,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</LOG_CONTEXT.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const login = useCallback(async () => {
|
const login = useCallback(async () => {
|
||||||
const result = await client.loginSteps();
|
const result = await client.loginSteps();
|
||||||
Loading…
Reference in New Issue