Панель и сеть читают сохранённое наблюдение и прогресс deploy. Качество и свежесть сессии видны на спикерах и пирах, без подмены статуса неопрошенным Established. Co-authored-by: Cursor <cursoragent@cursor.com>
206 lines
7.0 KiB
TypeScript
206 lines
7.0 KiB
TypeScript
import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
|
|
import { apiJSON, apiMutate } from '@/lib/api-client'
|
|
import { overviewKeys } from '@/queries/overview'
|
|
import type { operations } from '@/types/api.gen'
|
|
import type {
|
|
BgpPeerCreate,
|
|
BgpPeerPatch,
|
|
BgpSpeakerCreate,
|
|
BgpSpeakerPatch,
|
|
BirdStatus,
|
|
PeerDiscoveryApprove,
|
|
PeerDiscoveriesResponse,
|
|
PeerDiscoveryRow,
|
|
PeerRow,
|
|
PeersResponse,
|
|
SpeakerRow,
|
|
SpeakersResponse,
|
|
} from '@/types/api'
|
|
|
|
export const NETWORK_AUTO_REFRESH_MS = 30_000
|
|
|
|
export const networkKeys = {
|
|
all: ['network'] as const,
|
|
peers: () => [...networkKeys.all, 'peers'] as const,
|
|
discovered: () => [...networkKeys.all, 'discovered'] as const,
|
|
speakers: () => [...networkKeys.all, 'speakers'] as const,
|
|
bird: () => [...networkKeys.all, 'bird'] as const,
|
|
}
|
|
|
|
type TelemetryRefreshResponse = operations['requestTelemetryRefresh']['responses'][202]['content']['application/json']
|
|
|
|
/** Request a background poll; GET queries continue reading the latest saved snapshots. */
|
|
export function useRequestTelemetryRefreshMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: () =>
|
|
apiMutate<TelemetryRefreshResponse>('/v1/telemetry/refresh', 'POST', undefined, {
|
|
idempotent: false,
|
|
}),
|
|
onSuccess: ({ requested }) => {
|
|
if (requested) {
|
|
toast.success('Запрос на обновление телеметрии принят. Данные появятся после фонового опроса.')
|
|
} else {
|
|
toast.message('Запрос объединён с недавним обновлением или нет доступных целей.')
|
|
}
|
|
void qc.invalidateQueries({ queryKey: networkKeys.peers() })
|
|
void qc.invalidateQueries({ queryKey: networkKeys.discovered() })
|
|
void qc.invalidateQueries({ queryKey: networkKeys.speakers() })
|
|
void qc.invalidateQueries({ queryKey: overviewKeys.peers() })
|
|
void qc.invalidateQueries({ queryKey: overviewKeys.speakers() })
|
|
},
|
|
onError: (e) =>
|
|
toast.error(e instanceof Error ? e.message : 'Не удалось запросить обновление телеметрии'),
|
|
})
|
|
}
|
|
|
|
export function networkPeersQueryOptions() {
|
|
return queryOptions<PeersResponse>({
|
|
queryKey: networkKeys.peers(),
|
|
queryFn: () => apiJSON<PeersResponse>('/v1/peers?limit=200&live=1'),
|
|
staleTime: 15_000,
|
|
})
|
|
}
|
|
|
|
export function networkDiscoveredPeersQueryOptions() {
|
|
return queryOptions<PeerDiscoveriesResponse>({
|
|
queryKey: networkKeys.discovered(),
|
|
queryFn: () => apiJSON<PeerDiscoveriesResponse>('/v1/peers/discovered?status=pending'),
|
|
staleTime: 10_000,
|
|
})
|
|
}
|
|
|
|
export function networkSpeakersQueryOptions() {
|
|
return queryOptions<SpeakersResponse>({
|
|
queryKey: networkKeys.speakers(),
|
|
queryFn: () => apiJSON<SpeakersResponse>('/v1/speakers?limit=200&live=1'),
|
|
staleTime: 15_000,
|
|
})
|
|
}
|
|
|
|
export function networkBirdQueryOptions() {
|
|
return queryOptions<BirdStatus>({
|
|
queryKey: networkKeys.bird(),
|
|
queryFn: () => apiJSON<BirdStatus>('/v1/bird/status'),
|
|
staleTime: 15_000,
|
|
})
|
|
}
|
|
|
|
function invalidateNetwork(qc: ReturnType<typeof useQueryClient>) {
|
|
void qc.invalidateQueries({ queryKey: networkKeys.all })
|
|
void qc.invalidateQueries({ queryKey: ['overview'] })
|
|
}
|
|
|
|
export function useCreatePeerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (body: BgpPeerCreate) =>
|
|
apiMutate<PeerRow>('/v1/peers', 'POST', body, { idempotent: false }),
|
|
onSuccess: () => {
|
|
toast.success('Пир создан')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось создать пира'),
|
|
})
|
|
}
|
|
|
|
export function useUpdatePeerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ id, body }: { id: string; body: BgpPeerPatch }) =>
|
|
apiMutate<PeerRow>(`/v1/peers/${id}`, 'PATCH', body, { idempotent: false }),
|
|
onSuccess: () => {
|
|
toast.success('Пир обновлён')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось обновить пира'),
|
|
})
|
|
}
|
|
|
|
export function useDeletePeerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiMutate(`/v1/peers/${id}`, 'DELETE', undefined, { idempotent: false }),
|
|
onSuccess: () => {
|
|
toast.success('Пир удалён')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось удалить пира'),
|
|
})
|
|
}
|
|
|
|
export function useApproveDiscoveredPeerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ id, body }: { id: string; body?: PeerDiscoveryApprove }) =>
|
|
apiMutate<{ peer: PeerRow; discovery: PeerDiscoveryRow }>(
|
|
`/v1/peers/discovered/${id}/approve`,
|
|
'POST',
|
|
body ?? {},
|
|
{ idempotent: false },
|
|
),
|
|
onSuccess: () => {
|
|
toast.success('Пир одобрен')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось одобрить пира'),
|
|
})
|
|
}
|
|
|
|
export function useRejectDiscoveredPeerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiMutate<PeerDiscoveryRow>(`/v1/peers/discovered/${id}/reject`, 'POST', {}, {
|
|
idempotent: false,
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success('Пир отклонён')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось отклонить пира'),
|
|
})
|
|
}
|
|
|
|
export function useCreateSpeakerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (body: BgpSpeakerCreate) =>
|
|
apiMutate<SpeakerRow>('/v1/speakers', 'POST', body, { idempotent: false }),
|
|
onSuccess: () => {
|
|
toast.success('Спикер создан')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось создать спикера'),
|
|
})
|
|
}
|
|
|
|
export function useUpdateSpeakerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ id, body }: { id: string; body: BgpSpeakerPatch }) =>
|
|
apiMutate<SpeakerRow>(`/v1/speakers/${id}`, 'PATCH', body, { idempotent: false }),
|
|
onSuccess: () => {
|
|
toast.success('Спикер обновлён')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось обновить спикера'),
|
|
})
|
|
}
|
|
|
|
export function useDeleteSpeakerMutation() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiMutate(`/v1/speakers/${id}`, 'DELETE', undefined, { idempotent: false }),
|
|
onSuccess: () => {
|
|
toast.success('Спикер удалён')
|
|
invalidateNetwork(qc)
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось удалить спикера'),
|
|
})
|
|
}
|