feat: add log display
parent
77c621b499
commit
043bcb3ae4
|
|
@ -1,16 +1,17 @@
|
|||
import React from 'react'
|
||||
|
||||
type CheckboxProps = {
|
||||
disabled?: boolean
|
||||
children?: React.ReactNode
|
||||
value?: boolean
|
||||
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">
|
||||
<label className="label cursor-pointer">
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import classNames from 'classnames';
|
||||
import { usePromise } from 'hooks/usePromise';
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { canExport, getProfile, setProfile } from 'services/config';
|
||||
import { run } from 'services/s3si';
|
||||
import { run, useLog } from 'services/s3si';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { Loading } from './Loading';
|
||||
|
||||
|
|
@ -39,16 +39,40 @@ export const RunPanel: React.FC<RunPanelProps> = () => {
|
|||
setLoading(false);
|
||||
}
|
||||
}
|
||||
const disabled = !canExport(result);
|
||||
|
||||
return <>
|
||||
<Checkbox value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
||||
<Checkbox value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={classNames('btn', {
|
||||
'btn-disabled': !canExport(result) || (!exportBattle && !exportCoop),
|
||||
'loading': loading,
|
||||
})}
|
||||
>{t('导出')}</button>
|
||||
<div className="tooltip" data-tip={disabled ? t('请先完成登录和stat.ink的API密钥设置') : undefined}>
|
||||
<Checkbox disabled={disabled || loading} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
||||
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={classNames('btn w-full', {
|
||||
'btn-disabled': disabled || (!exportBattle && !exportCoop),
|
||||
'loading': loading,
|
||||
})}
|
||||
>{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 {
|
||||
height: 100vh;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
@ -42,5 +43,5 @@ body {
|
|||
}
|
||||
|
||||
.full-card {
|
||||
@apply card m-2 h-full;
|
||||
@apply card p-2 h-full;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { LogProvider } from "services/s3si";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
<LogProvider limit={100}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</LogProvider>
|
||||
</React.StrictMode >
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ErrorContent } from 'components/ErrorContent';
|
||||
import { Loading } from 'components/Loading';
|
||||
import { RunPanel } from 'components/RunPanel';
|
||||
import { LogPanel, RunPanel } from 'components/RunPanel';
|
||||
import { STAT_INK } from 'constant';
|
||||
import { usePromise } from 'hooks/usePromise';
|
||||
import React from 'react'
|
||||
|
|
@ -28,14 +28,15 @@ export const Home: React.FC = () => {
|
|||
</>
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className='full-card'>
|
||||
<h1 className='mb-4'>{t('欢迎!')}</h1>
|
||||
return <div className='flex p-2 w-full h-full gap-2'>
|
||||
<div className='max-w-full md:max-w-sm flex-auto'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<LogPanel className='sm:hidden max-h-[10rem]' />
|
||||
<RunPanel />
|
||||
<Link to='/settings' className='btn'>{t('配置')}</Link>
|
||||
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||
</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 {
|
||||
return !!profile.state.loginState?.sessionToken
|
||||
return !!(profile.state.loginState?.sessionToken && profile.state.statInkApiKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { invoke } from "@tauri-apps/api";
|
||||
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
||||
import { ExportOpts, State } from "jsonrpc/types";
|
||||
import { useCallback } from "react";
|
||||
import { ExportOpts, Log, State } from "jsonrpc/types";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
const client = new JSONRPCClient<S3SIService>({
|
||||
transport: new StdioTransport()
|
||||
}).getProxy();
|
||||
const LOG_SUB = new Set<(logs: Log[]) => void>();
|
||||
|
||||
async function getLogs() {
|
||||
while (true) {
|
||||
|
|
@ -31,10 +32,57 @@ async function getLogs() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
for (const cb of LOG_SUB) {
|
||||
cb(r.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = () => {
|
||||
const login = useCallback(async () => {
|
||||
const result = await client.loginSteps();
|
||||
Loading…
Reference in New Issue