From 79591ca7bd8303dcf43d5a816b09a6bd73138912 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:50:23 +0800 Subject: [PATCH] fix state --- web/features/deployments/data.ts | 62 ++++++++++++++++++++++- web/features/deployments/detail/index.tsx | 41 ++++++++++++--- web/features/deployments/store.ts | 19 +++++-- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/web/features/deployments/data.ts b/web/features/deployments/data.ts index 61fa47b9be..ab52f4b79b 100644 --- a/web/features/deployments/data.ts +++ b/web/features/deployments/data.ts @@ -9,10 +9,11 @@ import type { } from '@/contract/console/deployments' import { queryOptions } from '@tanstack/react-query' import { getQueryClient } from '@/context/get-query-client' -import { consoleClient } from '@/service/client' +import { consoleClient, consoleQuery } from '@/service/client' const DEPLOYMENT_PAGE_SIZE = 100 const DEPLOYMENT_APP_DATA_STALE_TIME = 30 * 1000 +const DEPLOYMENT_READINESS_RETRY_DELAYS = [0, 300, 700, 1200] export type DeploymentAppData = { appId: string @@ -91,6 +92,65 @@ export const refreshDeploymentAppData = async (appId: string): Promise new Promise(resolve => setTimeout(resolve, delay)) + +export const refreshDeploymentAppDataWhenReady = async (appId: string): Promise => { + let lastError: unknown + + for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) { + if (delay > 0) + await wait(delay) + + try { + return await refreshDeploymentAppData(appId) + } + catch (error) { + lastError = error + } + } + + throw lastError +} + +export const refreshDeploymentLists = async () => { + await getQueryClient().invalidateQueries({ + queryKey: consoleQuery.deployments.list.key(), + }) +} + +export const waitForAppInstanceInDeploymentList = async (appInstanceId: string) => { + let lastError: unknown + + for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) { + if (delay > 0) + await wait(delay) + + try { + const response = await getQueryClient().fetchQuery({ + ...consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + }, + }), + staleTime: 0, + }) + if (response.data?.some(app => app.id === appInstanceId)) + return + } + catch (error) { + lastError = error + } + } + + await refreshDeploymentLists() + + if (lastError) + throw lastError +} + export const createRelease = async (appId: string, releaseNote?: string): Promise => { const trimmedReleaseNote = releaseNote?.trim() const response = await consoleClient.deployments.createRelease({ diff --git a/web/features/deployments/detail/index.tsx b/web/features/deployments/detail/index.tsx index cbb0391038..98a37abeb2 100644 --- a/web/features/deployments/detail/index.tsx +++ b/web/features/deployments/detail/index.tsx @@ -1,7 +1,9 @@ 'use client' import type { FC, ReactNode } from 'react' +import type { AppInfo, AppMode } from '../types' import type { InstanceDetailTabKey } from './tabs' +import type { AppInstanceOverview } from '@/contract/console/deployments' import { Button } from '@langgenius/dify-ui/button' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -17,6 +19,22 @@ import { deployedRows, deploymentStatus } from '../utils' import { DeploymentSidebar } from './deployment-sidebar' import { isInstanceDetailTabKey } from './tabs' +function toAppInfoFromOverview(instance?: AppInstanceOverview): AppInfo | undefined { + if (!instance?.id) + return undefined + + return { + id: instance.id, + name: instance.name ?? instance.id, + mode: (instance.mode || 'workflow') as AppMode, + iconType: 'emoji', + icon: instance.icon, + description: instance.description ?? undefined, + sourceAppId: instance.sourceAppId, + sourceAppName: instance.sourceAppName, + } +} + type InstanceDetailProps = { instanceId: string children: ReactNode @@ -34,18 +52,29 @@ const InstanceDetail: FC = ({ instanceId, children }) => { const { appMap, isLoading: isLoadingApps } = useSourceApps() useDocumentTitle(t('documentTitle.detail')) - const app = useMemo( - () => sourceApps.find(item => item.id === instanceId) ?? appMap.get(instanceId), - [sourceApps, instanceId, appMap], + const appDataForInstance = appData[instanceId] + const appFromData = useMemo( + () => toAppInfoFromOverview(appDataForInstance?.overview.instance), + [appDataForInstance?.overview.instance], ) - const detailApps = useMemo(() => app ? [app] : [], [app]) - useDeploymentData(detailApps, { enabled: detailApps.length > 0 }) + const app = useMemo( + () => sourceApps.find(item => item.id === instanceId) ?? appMap.get(instanceId) ?? appFromData, + [sourceApps, instanceId, appMap, appFromData], + ) + const detailApps = useMemo(() => [ + app ?? { + id: instanceId, + name: instanceId, + mode: 'workflow', + }, + ], [app, instanceId]) + const detailQuery = useDeploymentData(detailApps, { enabled: Boolean(instanceId) }) const appDeployments = useMemo( () => deployedRows(appData[instanceId]?.environmentDeployments.data), [appData, instanceId], ) - if (isLoadingApps && !app) { + if (!app && (isLoadingApps || detailQuery.isLoading || detailQuery.isFetching)) { return (
diff --git a/web/features/deployments/store.ts b/web/features/deployments/store.ts index 767abf3aa7..605a69d6cf 100644 --- a/web/features/deployments/store.ts +++ b/web/features/deployments/store.ts @@ -12,9 +12,12 @@ import { patchAccessChannel, patchDeveloperAPI, refreshDeploymentAppData, + refreshDeploymentAppDataWhenReady, + refreshDeploymentLists, undeployEnvironment, updateAppInstance, updateEnvironmentAccessPolicy, + waitForAppInstanceInDeploymentList, } from './data' export type StartDeployParams = { @@ -138,11 +141,8 @@ export const useDeploymentsStore = create((set, get) => ({ openCreateInstanceModal: () => set({ createInstanceModal: { open: true } }), closeCreateInstanceModal: () => set({ createInstanceModal: { open: false } }), - seedInstancesFromApps: apps => set(state => ({ + seedInstancesFromApps: apps => set(() => ({ sourceApps: apps, - appData: Object.fromEntries( - Object.entries(state.appData).filter(([appId]) => apps.some(app => app.id === appId)), - ), })), applyAppData: data => set(state => ({ @@ -162,6 +162,11 @@ export const useDeploymentsStore = create((set, get) => ({ if (!response.appInstanceId) throw new Error('Create app instance did not return an appInstanceId.') set({ createInstanceModal: { open: false } }) + await Promise.allSettled([ + refreshDeploymentAppDataWhenReady(response.appInstanceId) + .then(data => get().applyAppData(data)), + waitForAppInstanceInDeploymentList(response.appInstanceId), + ]) return { appInstanceId: response.appInstanceId, initialRelease: response.initialRelease, @@ -174,6 +179,7 @@ export const useDeploymentsStore = create((set, get) => ({ description: patch.description, }) await get().refreshAppData(appId) + await refreshDeploymentLists() set(state => ({ sourceApps: state.sourceApps.map(app => app.id === appId ? { ...app, ...patch } : app), })) @@ -190,23 +196,27 @@ export const useDeploymentsStore = create((set, get) => ({ appData, } }) + await refreshDeploymentLists() }, startDeploy: async ({ appId, environmentId, releaseId, releaseNote }) => { set({ deployDrawer: { open: false } }) await createDeployment({ appId, environmentId, releaseId, releaseNote }) await get().refreshAppData(appId) + await refreshDeploymentLists() }, retryDeploy: async (appId, environmentId, targetReleaseId) => { await createDeployment({ appId, environmentId, releaseId: targetReleaseId }) await get().refreshAppData(appId) + await refreshDeploymentLists() }, rollbackDeployment: async (appId, environmentId, targetReleaseId) => { set({ rollbackModal: { open: false } }) await createDeployment({ appId, environmentId, releaseId: targetReleaseId }) await get().refreshAppData(appId) + await refreshDeploymentLists() }, undeployDeployment: async (appId, _environmentId, runtimeInstanceId, isDeploying) => { @@ -217,6 +227,7 @@ export const useDeploymentsStore = create((set, get) => ({ else await undeployEnvironment(appId, runtimeInstanceId) await get().refreshAppData(appId) + await refreshDeploymentLists() }, generateApiKey: async (appId, environmentId) => {