56 lines
3.4 KiB
TypeScript
56 lines
3.4 KiB
TypeScript
import { unwrap } from '@pipu/core';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useSession } from '../../app/session';
|
|
import { useErrorText } from '../../shared/lib/errors';
|
|
import { Button, FormError } from '../../shared/ui/ui';
|
|
import s from '../page.module.css';
|
|
import g from '../genius.module.css';
|
|
|
|
export function GeniusPanel() {
|
|
const { t, i18n } = useTranslation();
|
|
const session = useSession();
|
|
const queries = useQueryClient();
|
|
const errorText = useErrorText();
|
|
const stats = useQuery({
|
|
queryKey: ['genius-stats'],
|
|
queryFn: async () => unwrap(await session.client.GET('/api/v1/admin/genius')),
|
|
refetchInterval: 5000,
|
|
});
|
|
const control = useMutation({
|
|
mutationFn: async (action: 'start' | 'pause' | 'retry') =>
|
|
unwrap(await session.client.POST('/api/v1/admin/genius', { body: { action } })),
|
|
onSuccess: (data) => { queries.setQueryData(['genius-stats'], data); },
|
|
});
|
|
const data = stats.data;
|
|
const cooling = data && Date.parse(data.cooldown_until) > Date.now();
|
|
const state = !data ? t('genius.loading') : !data.configured ? t('genius.disabled') : data.paused ? t('genius.paused') : cooling ? t('genius.cooldown') : data.running ? t('genius.running') : data.pending > 0 ? t('genius.queued') : t('genius.waiting');
|
|
const processed = data ? Math.max(0, data.total - data.pending) : 0;
|
|
const formatNumber = (value: number) => value.toLocaleString(i18n.language);
|
|
return <section className={s.section} aria-label="Genius">
|
|
<div className={s.head}><h2>Genius</h2><span role="status">{state}</span></div>
|
|
<p className={s.subtitle}>{t('genius.description')}</p>
|
|
<FormError>{stats.error ? errorText(stats.error) : control.error ? errorText(control.error) : null}</FormError>
|
|
{data && <>
|
|
{!data.configured && <p>{t('genius.configure')}</p>}
|
|
{data.total > 0 && <div className={g.progress}>
|
|
<span>{t('genius.progress', { processed: formatNumber(processed), total: formatNumber(data.total) })}</span>
|
|
<progress aria-label={t('genius.progress_label')} value={processed} max={data.total} />
|
|
<p className={s.subtitle}>{t('genius.progress_hint')}</p>
|
|
</div>}
|
|
<dl className={g.stats}>
|
|
{(['total', 'pending', 'matched', 'lyrics', 'annotations', 'images', 'unmatched', 'errors'] as const).map((key) =>
|
|
<div key={key}><dt>{t(`genius.${key}`)}</dt><dd>{formatNumber(data[key])}</dd></div>)}
|
|
</dl>
|
|
{data.last_error && <p role="alert">{t('genius.last_error')}: {data.last_error === 'http_401' ? t('genius.token_error') : data.last_error === 'http_403' ? t('genius.access_error') : data.last_error === 'http_429' ? t('genius.cooldown') : data.last_error === 'lyrics_unavailable' ? t('genius.no_lyrics') : t('genius.request_error')}</p>}
|
|
{cooling && <p>{t('genius.resume_at')}: {new Date(data.cooldown_until).toLocaleString()}</p>}
|
|
<div className={g.actions}>
|
|
<Button disabled={!data.configured || control.isPending} onClick={() => control.mutate(data.paused ? 'start' : 'pause')}>{t(data.paused ? 'genius.start' : 'genius.pause')}</Button>
|
|
<Button disabled={!data.configured || control.isPending || data.errors + data.unmatched === 0} onClick={() => control.mutate('retry')}>{t('genius.retry')}</Button>
|
|
</div>
|
|
<p className={s.subtitle}>{t('genius.controls_hint')}</p>
|
|
<p className={s.subtitle}>{t('genius.retry_hint')}</p>
|
|
</>}
|
|
</section>;
|
|
}
|