feat(admin): wire instance health panel + settings instance (A9/A10)
AdminInstance shows backing-service health (db/redis/ml), read-only source status (env-configured), and a library reindex with graceful handling when no indexable source exists. InstanceSettings shows the active instance and offers switch/sign-out to /connect. Adds getServices/getSources/reindex endpoints. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,8 +29,11 @@ import {
|
||||
useUpdateUserMutation,
|
||||
useDeleteUserMutation,
|
||||
useResetPasswordMutation,
|
||||
useGetServicesQuery,
|
||||
useGetSourcesQuery,
|
||||
useReindexMutation,
|
||||
} from '../../api/endpoints/admin';
|
||||
import type { User } from '../../api/types';
|
||||
import type { User, ServiceStatus } from '../../api/types';
|
||||
|
||||
function StubPanel({ title }: { title: string }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -475,8 +478,174 @@ export function AdminSources() {
|
||||
return <StubPanel title={t('admin.tabs.sources')} />;
|
||||
}
|
||||
|
||||
/** `/admin/instance` — service health, ML_SERVICE_URL, reindex. Scaffold. */
|
||||
const SERVICE_VARIANT: Record<ServiceStatus, 'lime' | 'ember' | 'neutral'> = {
|
||||
ok: 'lime',
|
||||
down: 'ember',
|
||||
skipped: 'neutral',
|
||||
};
|
||||
|
||||
/** `/admin/instance` — backing-service health, source status, library reindex. */
|
||||
export function AdminInstance() {
|
||||
const { t } = useTranslation();
|
||||
return <StubPanel title={t('admin.tabs.instance')} />;
|
||||
const {
|
||||
data: servicesData,
|
||||
isLoading: servicesLoading,
|
||||
isError: servicesError,
|
||||
refetch: refetchServices,
|
||||
} = useGetServicesQuery();
|
||||
const {
|
||||
data: sourcesData,
|
||||
isLoading: sourcesLoading,
|
||||
isError: sourcesError,
|
||||
refetch: refetchSources,
|
||||
} = useGetSourcesQuery();
|
||||
const [reindex, { isLoading: isReindexing }] = useReindexMutation();
|
||||
const [reindexMsg, setReindexMsg] = useState<
|
||||
{ kind: 'success' | 'danger'; text: string } | null
|
||||
>(null);
|
||||
|
||||
const handleReindex = async () => {
|
||||
setReindexMsg(null);
|
||||
try {
|
||||
const jobs = await reindex().unwrap();
|
||||
setReindexMsg({
|
||||
kind: 'success',
|
||||
text: t('admin.instance.reindex.enqueued', { count: jobs.length }),
|
||||
});
|
||||
} catch {
|
||||
// 400 when no indexable source is configured (see admin.py::trigger_reindex).
|
||||
setReindexMsg({
|
||||
kind: 'danger',
|
||||
text: t('admin.instance.reindex.noSource'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Window title={t('admin.tabs.instance')}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
{/* ── Services ─────────────────────────────────────────────── */}
|
||||
<section>
|
||||
<h3 style={{ margin: '0 0 0.5rem', fontSize: '0.95rem' }}>
|
||||
{t('admin.instance.services.title')}
|
||||
</h3>
|
||||
{servicesLoading && <LoadingSkeleton rows={3} height={40} />}
|
||||
{servicesError && <ErrorState onRetry={refetchServices} />}
|
||||
{servicesData && (
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
|
||||
>
|
||||
{(['database', 'redis', 'ml'] as const).map((key) => {
|
||||
const status = servicesData[key];
|
||||
return (
|
||||
<Card
|
||||
key={key}
|
||||
style={{
|
||||
padding: '0.625rem 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1 }}>
|
||||
{t(`admin.instance.services.${key}`)}
|
||||
</span>
|
||||
<Badge variant={SERVICE_VARIANT[status]} dot>
|
||||
{t(`admin.instance.status.${status}`)}
|
||||
</Badge>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Sources (read-only; configured via env) ──────────────── */}
|
||||
<section>
|
||||
<h3 style={{ margin: '0 0 0.25rem', fontSize: '0.95rem' }}>
|
||||
{t('admin.instance.sources.title')}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 0.5rem',
|
||||
fontSize: '0.8125rem',
|
||||
color: 'var(--color-text-3)',
|
||||
}}
|
||||
>
|
||||
{t('admin.instance.sources.envNote')}
|
||||
</p>
|
||||
{sourcesLoading && <LoadingSkeleton rows={2} height={40} />}
|
||||
{sourcesError && <ErrorState onRetry={refetchSources} />}
|
||||
{sourcesData && sourcesData.length === 0 && (
|
||||
<EmptyState icon="⊘" title={t('admin.instance.sources.empty')} />
|
||||
)}
|
||||
{sourcesData && sourcesData.length > 0 && (
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
|
||||
>
|
||||
{sourcesData.map((s) => (
|
||||
<Card
|
||||
key={s.name}
|
||||
style={{
|
||||
padding: '0.625rem 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1 }}>
|
||||
{s.label}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '0.5rem',
|
||||
fontSize: '0.75rem',
|
||||
color: 'var(--color-text-3)',
|
||||
}}
|
||||
>
|
||||
{s.kind}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant={s.available ? 'lime' : 'ember'} dot>
|
||||
{t(
|
||||
s.available
|
||||
? 'admin.instance.sources.available'
|
||||
: 'admin.instance.sources.unavailable',
|
||||
)}
|
||||
</Badge>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Reindex ──────────────────────────────────────────────── */}
|
||||
<section>
|
||||
<h3 style={{ margin: '0 0 0.25rem', fontSize: '0.95rem' }}>
|
||||
{t('admin.instance.reindex.title')}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 0.75rem',
|
||||
fontSize: '0.8125rem',
|
||||
color: 'var(--color-text-3)',
|
||||
}}
|
||||
>
|
||||
{t('admin.instance.reindex.description')}
|
||||
</p>
|
||||
{reindexMsg && (
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<Callout variant={reindexMsg.kind}>{reindexMsg.text}</Callout>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={isReindexing}
|
||||
onClick={() => void handleReindex()}
|
||||
>
|
||||
{t('admin.instance.reindex.button')}
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user