feat: add log display

main
imspace 2023-03-09 05:36:14 +08:00
parent 77c621b499
commit 043bcb3ae4
7 changed files with 104 additions and 26 deletions

View File

@ -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>
}

View File

@ -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>
<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', {
'btn-disabled': !canExport(result) || (!exportBattle && !exportCoop),
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>
}

View File

@ -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;
}

View File

@ -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>
<LogProvider limit={100}>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
</LogProvider>
</React.StrictMode >
);

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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();