fix(appstore): резервный REST-опрос прогресса установки (не зависать при потере WS)
All checks were successful
Deploy to Production / Trigger production update (push) Successful in 8s
All checks were successful
Deploy to Production / Trigger production update (push) Successful in 8s
Мастер установки получал прогресс только по WebSocket; при потерянном событии (или долгом docker compose up с тяжёлыми образами) UI замирал на последнем шаге. Добавлен поллинг /operations раз в 3с как фолбэк. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { useServers } from '@/hooks/use-nodes';
|
||||
import { useIPAMNetworks, useIPAMPools } from '@/hooks/use-ipam';
|
||||
import { useInstallApp, useInstalledApp, useOperationProgress } from '@/hooks/use-appstore';
|
||||
import { useInstallApp, useInstalledApp, useOperationProgress, useAppOperations } from '@/hooks/use-appstore';
|
||||
import type { AppOperation, CatalogApp } from '@/types/appstore';
|
||||
|
||||
interface Props {
|
||||
@@ -62,6 +62,18 @@ export default function InstallWizard({ app, open, onOpenChange }: Props) {
|
||||
}, []);
|
||||
useOperationProgress(phase === 'progress' ? installedAppId : null, onOp);
|
||||
|
||||
// Резервный опрос статуса через REST: если WebSocket-событие потеряно
|
||||
// (или установка идёт долго, напр. docker compose тянет тяжёлые образы),
|
||||
// прогресс всё равно обновляется и мастер не «зависает» на старом шаге.
|
||||
const { data: polledOps } = useAppOperations(
|
||||
phase === 'progress' ? (installedAppId ?? 0) : 0,
|
||||
3000,
|
||||
);
|
||||
useEffect(() => {
|
||||
const latest = polledOps?.[0]; // операции отсортированы по времени (свежая первой)
|
||||
if (latest && latest.installed_app_id === installedAppId) onOp(latest);
|
||||
}, [polledOps, installedAppId, onOp]);
|
||||
|
||||
// видимые поля формы (random генерируются на сервере, пользователю не показываем)
|
||||
const visibleFields = useMemo(
|
||||
() => (app.form_fields ?? []).filter((f) => (f.type || '').toLowerCase() !== 'random'),
|
||||
|
||||
@@ -73,11 +73,13 @@ export function useInstalledApp(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useAppOperations(id: number) {
|
||||
export function useAppOperations(id: number, pollMs?: number) {
|
||||
return useQuery({
|
||||
queryKey: appstoreKeys.operations(id),
|
||||
queryFn: () => apiClient.get<{ items: AppOperation[] }>(`/api/appstore/apps/${id}/operations`).then((r) => r.items),
|
||||
enabled: id > 0,
|
||||
// Резервный опрос (когда задан) — прогресс не зависит только от WebSocket.
|
||||
refetchInterval: pollMs && pollMs > 0 ? pollMs : false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"by": "by",
|
||||
"source": "Source",
|
||||
"version": "Version",
|
||||
"port": "Port",
|
||||
"disclaimer": "This compose file runs as-is from a third-party repository. Review what will be installed.",
|
||||
"view_compose": "View source compose",
|
||||
"back": "Back to catalog",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"by": "автор",
|
||||
"source": "Источник",
|
||||
"version": "Версия",
|
||||
"port": "Порт",
|
||||
"disclaimer": "Этот compose-файл исполняется как есть из стороннего репозитория. Проверьте, что будет установлено.",
|
||||
"view_compose": "Показать исходный compose",
|
||||
"back": "К каталогу",
|
||||
|
||||
Reference in New Issue
Block a user