ci / api / api (push) Successful in 55s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m6s
ci / db / db (push) Successful in 52s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m38s
release / android (APK в релиз) (push) Successful in 3m50s
ci / ts / ts (packages + web) (push) Successful in 3m20s
release / release (версия и заметки) (push) Successful in 5s
release / desktop (Linux и Windows в релиз) (push) Successful in 44m45s
release / prune (старые файлы и прогоны) (push) Successful in 7s
55 lines
2.6 KiB
TypeScript
55 lines
2.6 KiB
TypeScript
import type { components } from '@pipu/api-client';
|
|
import { useId } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { formatDateTime } from '../../shared/lib/format';
|
|
import { cx } from '../../shared/ui/ui';
|
|
import s from './library.module.css';
|
|
|
|
type ScanProgress = components['schemas']['ScanProgress'];
|
|
export type SourceStatus = components['schemas']['SourceStatus'];
|
|
|
|
export function StatusBadge({ status }: { status: SourceStatus }) {
|
|
const { t } = useTranslation('admin');
|
|
return <span className={cx(s.status, s[`st_${status}`])}>{t(`sources.status_${status}`)}</span>;
|
|
}
|
|
|
|
/** Прогресс идущего скана или итог последнего. */
|
|
export function ScanLine({ scan, lastScanAt }: { scan: ScanProgress | null; lastScanAt: string | null }) {
|
|
const { t, i18n } = useTranslation('admin');
|
|
const id = useId();
|
|
if (scan?.running) {
|
|
const phase = scan.phase ?? (scan.found === 0 ? 'walking' : scan.to_process > 0 ? 'processing' : 'preparing');
|
|
const determinate = phase === 'processing' && scan.to_process > 0;
|
|
const pct = scan.to_process > 0 ? Math.round((scan.processed / scan.to_process) * 100) : 0;
|
|
const state = phase === 'walking'
|
|
? scan.walked_dirs === undefined ? t('sources.scan_walking') : t('sources.scan_walk_progress', { dirs: scan.walked_dirs.toLocaleString(i18n.language), found: scan.found.toLocaleString(i18n.language) })
|
|
: phase === 'preparing' ? t('sources.scan_preparing')
|
|
: phase === 'finalizing' ? t('sources.scan_finalizing')
|
|
: t('sources.scanning', { processed: scan.processed, total: scan.to_process });
|
|
return (
|
|
<div className={s.scan}>
|
|
<div className={s.scanText} id={id}>
|
|
{state}
|
|
</div>
|
|
{phase === 'walking' && scan.current_dir && <div className={s.scanPath} title={scan.current_dir}>{t('sources.scan_current_dir', { path: scan.current_dir })}</div>}
|
|
<div className={cx(s.bar, !determinate && s.indeterminate)} role="progressbar" aria-labelledby={id} aria-valuenow={determinate ? pct : undefined} aria-valuemin={0} aria-valuemax={100}>
|
|
<span style={determinate ? { width: `${pct}%` } : undefined} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div className={s.scan}>
|
|
<div className={s.scanText}>
|
|
{lastScanAt ? t('sources.last_scan', { date: formatDateTime(lastScanAt, i18n.language) }) : t('sources.never_scanned')}
|
|
</div>
|
|
{scan?.error ? (
|
|
<div className={s.scanError}>{t('sources.scan_failed', { error: scan.error })}</div>
|
|
) : (
|
|
scan && <div className={s.scanSummary}>{t('sources.scan_done', { ...scan })}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|