import type { components } from '@pipu/api-client'; import { unwrap } from '@pipu/core'; import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession } from '../../app/session'; import { useErrorText } from '../../shared/lib/errors'; import { Button, Dialog, Field, FormError, PageSkeleton, Segmented } from '../../shared/ui/ui'; import { JobPanel } from './JobPanel'; import s from './management.module.css'; type Source = components['schemas']['ServerSource']; type Channel = components['schemas']['ServerUpdateRequest']['channel']; export function UpdatesPage() { const { t } = useTranslation('admin'); const session = useSession(); const qc = useQueryClient(); const errorText = useErrorText(); const [sourceId, setSourceId] = useState(''); const [channel, setChannel] = useState('latest'); const [version, setVersion] = useState(''); const [confirm, setConfirm] = useState(false); const [editing, setEditing] = useState(null); const fresh = useRef(false); const queryKey = ['server-updates', sourceId, channel, version]; const query = useQuery({ queryKey, queryFn: async () => { const force = fresh.current; fresh.current = false; return unwrap(await session.client.GET('/api/v1/admin/server/updates', { params: { query: { source_id: sourceId || undefined, channel, version: channel === 'version' ? version : undefined, fresh: force } }, })); }, placeholderData: keepPreviousData, refetchInterval: (q) => q.state.data?.job?.state === 'running' ? 3000 : 60000, retry: false, }); const source = query.data?.sources.find((v) => v.id === sourceId) ?? query.data?.sources[0]; const running = query.data?.job?.state === 'running'; const checking = query.isFetching; const availability = query.isError ? 'unavailable' : query.data?.update_state || 'unknown'; const canInstall = availability === 'available' && !checking && !query.isPlaceholderData && !query.isError && !running; const refresh = () => qc.invalidateQueries({ queryKey: ['server-updates'] }); const save = useMutation({ mutationFn: async (sources: Source[]) => unwrap(await session.client.PUT('/api/v1/admin/server/update-sources', { body: { sources } })), onSuccess: () => { setEditing(null); setSourceId(''); void refresh(); }, }); const start = useMutation({ mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/server/updates', { body: { source_id: source!.id, channel, version: channel === 'version' ? version : undefined, expected_commit: query.data!.target_commit }, })), onSuccess: (job) => { setConfirm(false); qc.setQueryData(queryKey, (old) => old ? { ...old, job } : old); void refresh(); }, onError: () => { void refresh(); }, }); if (query.isPending) return ; return
{t('management.server')}

{t('management.updates')}

{t('management.updates_intro')}

{query.error && {errorText(query.error)}} {query.data && <>
{t('management.current')}{query.data.current_version || '—'}

{t('management.manual')}

{t('management.update_target')}

{source?.url}

({ value, label: t(`management.channel_${value}`) }))} onChange={(v) => { setChannel(v); start.reset(); }} />

{t(`management.hint_${channel}`)}

{channel === 'version' && } {query.data.source_error && {t('management.source_unavailable')}} {start.error && {errorText(start.error)}}
{t(`management.availability.${checking ? 'checking' : running ? 'running' : availability}`)} {query.data.target_version &&

{query.data.target_version} · {query.data.target_commit.slice(0, 12)}

}
{canInstall && }
} {confirm && !start.isPending && setConfirm(false)}>

{t('management.confirm_update_text', { source: source?.name, target: query.data?.target_version || version })}

{t('management.update_warning')}

{start.error && {errorText(start.error)}}
} {editing && !save.isPending && setEditing(null)}>

{t('management.sources_hint')}

{editing.map((item, index) =>
setEditing(editing.map((v, i) => i === index ? { ...v, name } : v))} /> setEditing(editing.map((v, i) => i === index ? { ...v, url } : v))} />
)}
{save.error && {errorText(save.error)}}
}
; }