From f7014fd1569efc55a355501b12f715e54c2a02a8 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:08:52 +0800 Subject: [PATCH] tweaks --- .../deployments/components/deploy-drawer.tsx | 11 +-- .../deployments/components/rollback-modal.tsx | 7 +- web/features/deployments/data.ts | 69 ++++++++++++--- .../deployments/detail/access-tab.tsx | 4 +- .../deployments/detail/deploy-tab.tsx | 4 +- web/features/deployments/detail/index.tsx | 38 ++------- .../deployments/detail/overview-tab.tsx | 7 +- .../deployments/detail/settings-tab.tsx | 8 +- .../deployments/detail/versions-tab.tsx | 4 +- .../versions-tab/deploy-release-menu.tsx | 4 +- .../deployments/hooks/use-deployment-data.ts | 14 +--- .../deployments/hooks/use-source-apps.ts | 41 +++------ web/features/deployments/list/index.tsx | 2 - .../deployments/list/instance-card.tsx | 7 +- web/features/deployments/nav/index.tsx | 17 ++-- web/features/deployments/store.ts | 84 ++++++++++++++++--- 16 files changed, 186 insertions(+), 135 deletions(-) diff --git a/web/features/deployments/components/deploy-drawer.tsx b/web/features/deployments/components/deploy-drawer.tsx index cb961a6e58..856c09b1e4 100644 --- a/web/features/deployments/components/deploy-drawer.tsx +++ b/web/features/deployments/components/deploy-drawer.tsx @@ -3,19 +3,17 @@ import type { FC } from 'react' import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog' import { useQuery } from '@tanstack/react-query' -import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { deploymentAppDataQueryOptions } from '../data' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentsStore } from '../store' import { DeployForm } from './deploy-drawer/form' const DeployDrawer: FC = () => { const { t } = useTranslation('deployments') const drawer = useDeploymentsStore(state => state.deployDrawer) const drawerAppId = drawer.appId - const storedAppData = useDeploymentsStore(state => drawerAppId ? state.appData[drawerAppId] : undefined) - const applyAppData = useDeploymentsStore(state => state.applyAppData) + const storedAppData = useDeploymentAppData(drawerAppId) const closeDeployDrawer = useDeploymentsStore(state => state.closeDeployDrawer) const startDeploy = useDeploymentsStore(state => state.startDeploy) const open = drawer.open @@ -23,12 +21,9 @@ const DeployDrawer: FC = () => { const appDataQuery = useQuery({ ...deploymentAppDataQueryOptions(drawerAppId ?? ''), + queryFn: () => useDeploymentsStore.getState().fetchAppData(drawerAppId!), enabled: open && Boolean(drawerAppId) && !storedAppData, }) - useEffect(() => { - if (appDataQuery.data) - applyAppData(appDataQuery.data) - }, [appDataQuery.data, applyAppData]) const appData = storedAppData ?? (appDataQuery.data?.appId === drawerAppId ? appDataQuery.data : undefined) const environments = environmentOptions diff --git a/web/features/deployments/components/rollback-modal.tsx b/web/features/deployments/components/rollback-modal.tsx index 77d7269f50..840922896c 100644 --- a/web/features/deployments/components/rollback-modal.tsx +++ b/web/features/deployments/components/rollback-modal.tsx @@ -12,7 +12,7 @@ import { import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store' import { activeRelease, deployedRows, @@ -34,7 +34,8 @@ const InfoRow: FC<{ label: string, value: string }> = ({ label, value }) => { const RollbackModal: FC = () => { const { t } = useTranslation('deployments') const modal = useDeploymentsStore(state => state.rollbackModal) - const appData = useDeploymentsStore(state => modal.appId ? state.appData[modal.appId] : undefined) + const appData = useDeploymentAppData(modal.appId) + const storedApp = useDeploymentInstance(modal.appId) const closeRollbackModal = useDeploymentsStore(state => state.closeRollbackModal) const rollbackDeployment = useDeploymentsStore(state => state.rollbackDeployment) const { appMap, environmentOptions } = useSourceApps() @@ -47,7 +48,7 @@ const RollbackModal: FC = () => { const currentRelease = activeRelease(currentRow) const environment = currentRow?.environment ?? environmentOptions.find(env => env.id === modal.environmentId) - const app = modal.appId ? appMap.get(modal.appId) : undefined + const app = storedApp ?? (modal.appId ? appMap.get(modal.appId) : undefined) const confirm = () => { if (!modal.appId || !modal.environmentId || !modal.targetReleaseId) diff --git a/web/features/deployments/data.ts b/web/features/deployments/data.ts index ab52f4b79b..377e5f3f0f 100644 --- a/web/features/deployments/data.ts +++ b/web/features/deployments/data.ts @@ -1,9 +1,13 @@ +import type { AppInfo, AppMode } from './types' import type { AccessSubject, + AppDeploymentSummary, + AppInstanceOverview, ConsoleReleaseSummary, CreateAppInstanceReply, GetAccessConfigReply, GetDeploymentOverviewReply, + ListAppDeploymentsReply, ListEnvironmentDeploymentsReply, ListReleaseHistoryReply, } from '@/contract/console/deployments' @@ -41,8 +45,54 @@ export type UpdateInstanceParams = { description?: string } +export type ListAppDeploymentsQuery = { + environmentId?: string + notDeployed?: boolean + query?: string + pageNumber?: number + resultsPerPage?: number +} + +export function toAppInfoFromSummary(summary: AppDeploymentSummary): AppInfo | undefined { + if (!summary.id || !summary.name) + return undefined + + return { + id: summary.id, + name: summary.name, + mode: (summary.mode || 'workflow') as AppMode, + iconType: 'emoji', + icon: summary.icon, + description: summary.description ?? undefined, + sourceAppId: summary.sourceAppId, + sourceAppName: summary.sourceAppName, + } +} + +export 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, + } +} + export const deploymentAppDataQueryKey = (appId: string) => ['console', 'deployments', 'app-data', appId] as const +export const listAppDeployments = async (query: ListAppDeploymentsQuery): Promise => { + return consoleClient.deployments.list({ + query, + }) +} + export const fetchDeploymentAppData = async (appId: string): Promise => { const input = { params: { appInstanceId: appId } } const [ @@ -118,7 +168,7 @@ export const refreshDeploymentLists = async () => { }) } -export const waitForAppInstanceInDeploymentList = async (appInstanceId: string) => { +export const waitForAppInstanceInDeploymentList = async (appInstanceId: string): Promise => { let lastError: unknown for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) { @@ -126,19 +176,12 @@ export const waitForAppInstanceInDeploymentList = async (appInstanceId: string) await wait(delay) try { - const response = await getQueryClient().fetchQuery({ - ...consoleQuery.deployments.list.queryOptions({ - input: { - query: { - pageNumber: 1, - resultsPerPage: DEPLOYMENT_PAGE_SIZE, - }, - }, - }), - staleTime: 0, + const response = await listAppDeployments({ + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, }) if (response.data?.some(app => app.id === appInstanceId)) - return + return response } catch (error) { lastError = error @@ -149,6 +192,8 @@ export const waitForAppInstanceInDeploymentList = async (appInstanceId: string) if (lastError) throw lastError + + return undefined } export const createRelease = async (appId: string, releaseNote?: string): Promise => { diff --git a/web/features/deployments/detail/access-tab.tsx b/web/features/deployments/detail/access-tab.tsx index da55a5d0e5..a30a5da614 100644 --- a/web/features/deployments/detail/access-tab.tsx +++ b/web/features/deployments/detail/access-tab.tsx @@ -5,7 +5,7 @@ import type { ConsoleEnvironmentSummary, } from '@/contract/console/deployments' import { useMemo } from 'react' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentsStore } from '../store' import { deployedRows, } from '../utils' @@ -27,7 +27,7 @@ type AccessTabProps = { } const AccessTab: FC = ({ instanceId: appId }) => { - const appData = useDeploymentsStore(state => state.appData[appId]) + const appData = useDeploymentAppData(appId) const createdApiToken = useDeploymentsStore(state => state.createdApiToken) const clearCreatedApiToken = useDeploymentsStore(state => state.clearCreatedApiToken) const generateApiKey = useDeploymentsStore(state => state.generateApiKey) diff --git a/web/features/deployments/detail/deploy-tab.tsx b/web/features/deployments/detail/deploy-tab.tsx index b0176bf558..f93817363a 100644 --- a/web/features/deployments/detail/deploy-tab.tsx +++ b/web/features/deployments/detail/deploy-tab.tsx @@ -11,7 +11,7 @@ import { import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentsStore } from '../store' import { activeRelease, deployedRows, @@ -36,7 +36,7 @@ type DeployTabProps = { const DeployTab: FC = ({ instanceId: appId }) => { const { t } = useTranslation('deployments') - const appData = useDeploymentsStore(state => state.appData[appId]) + const appData = useDeploymentAppData(appId) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const undeployDeployment = useDeploymentsStore(state => state.undeployDeployment) const { environmentOptions } = useSourceApps() diff --git a/web/features/deployments/detail/index.tsx b/web/features/deployments/detail/index.tsx index 98a37abeb2..57d65cf08f 100644 --- a/web/features/deployments/detail/index.tsx +++ b/web/features/deployments/detail/index.tsx @@ -1,9 +1,8 @@ 'use client' import type { FC, ReactNode } from 'react' -import type { AppInfo, AppMode } from '../types' +import type { AppInfo } 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' @@ -14,27 +13,11 @@ import DeployDrawer from '../components/deploy-drawer' import RollbackModal from '../components/rollback-modal' import { useDeploymentData } from '../hooks/use-deployment-data' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentInstance } from '../store' 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 @@ -47,19 +30,14 @@ const InstanceDetail: FC = ({ instanceId, children }) => { const selectedSegment = useSelectedLayoutSegment() const selectedTab = selectedSegment ?? undefined const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview' - const sourceApps = useDeploymentsStore(state => state.sourceApps) - const appData = useDeploymentsStore(state => state.appData) + const storedApp = useDeploymentInstance(instanceId) + const appData = useDeploymentAppData(instanceId) const { appMap, isLoading: isLoadingApps } = useSourceApps() useDocumentTitle(t('documentTitle.detail')) - const appDataForInstance = appData[instanceId] - const appFromData = useMemo( - () => toAppInfoFromOverview(appDataForInstance?.overview.instance), - [appDataForInstance?.overview.instance], - ) const app = useMemo( - () => sourceApps.find(item => item.id === instanceId) ?? appMap.get(instanceId) ?? appFromData, - [sourceApps, instanceId, appMap, appFromData], + () => storedApp ?? appMap.get(instanceId), + [storedApp, instanceId, appMap], ) const detailApps = useMemo(() => [ app ?? { @@ -70,8 +48,8 @@ const InstanceDetail: FC = ({ instanceId, children }) => { ], [app, instanceId]) const detailQuery = useDeploymentData(detailApps, { enabled: Boolean(instanceId) }) const appDeployments = useMemo( - () => deployedRows(appData[instanceId]?.environmentDeployments.data), - [appData, instanceId], + () => deployedRows(appData?.environmentDeployments.data), + [appData?.environmentDeployments.data], ) if (!app && (isLoadingApps || detailQuery.isLoading || detailQuery.isFetching)) { diff --git a/web/features/deployments/detail/overview-tab.tsx b/web/features/deployments/detail/overview-tab.tsx index 33ffbead43..931945a303 100644 --- a/web/features/deployments/detail/overview-tab.tsx +++ b/web/features/deployments/detail/overview-tab.tsx @@ -9,7 +9,7 @@ import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode- import { useRouter } from '@/next/navigation' import { StatusBadge } from '../components/status-badge' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store' import { releaseLabel, webappUrl } from '../utils' type OverviewTabProps = { @@ -91,10 +91,11 @@ const OverviewTab: FC = ({ instanceId }) => { const { t } = useTranslation('deployments') const { t: tCommon } = useTranslation() const router = useRouter() - const appData = useDeploymentsStore(state => state.appData[instanceId]) + const appData = useDeploymentAppData(instanceId) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) + const storedApp = useDeploymentInstance(instanceId) const { appMap } = useSourceApps() - const app = appMap.get(instanceId) + const app = storedApp ?? appMap.get(instanceId) const overview = appData?.overview const overviewApp = overview?.instance const deployments = useMemo( diff --git a/web/features/deployments/detail/settings-tab.tsx b/web/features/deployments/detail/settings-tab.tsx index a6336f1aeb..efff213987 100644 --- a/web/features/deployments/detail/settings-tab.tsx +++ b/web/features/deployments/detail/settings-tab.tsx @@ -20,7 +20,7 @@ import { useTranslation } from 'react-i18next' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/client' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentInstance, useDeploymentsStore } from '../store' import { deployedRows } from '../utils' type SettingsTabProps = { @@ -177,12 +177,12 @@ const SettingsForm: FC = ({ app, settings, hasDeployments, on const SettingsTab: FC = ({ instanceId }) => { const router = useRouter() - const sourceApps = useDeploymentsStore(state => state.sourceApps) - const appData = useDeploymentsStore(state => state.appData[instanceId]) + const storedApp = useDeploymentInstance(instanceId) + const appData = useDeploymentAppData(instanceId) const updateInstance = useDeploymentsStore(state => state.updateInstance) const deleteInstance = useDeploymentsStore(state => state.deleteInstance) const { appMap } = useSourceApps() - const app = sourceApps.find(item => item.id === instanceId) ?? appMap.get(instanceId) + const app = storedApp ?? appMap.get(instanceId) const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({ input: { params: { diff --git a/web/features/deployments/detail/versions-tab.tsx b/web/features/deployments/detail/versions-tab.tsx index 37925d3ea4..242a16d4dc 100644 --- a/web/features/deployments/detail/versions-tab.tsx +++ b/web/features/deployments/detail/versions-tab.tsx @@ -4,7 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData } from '../store' import { deployedRows, formatDate, @@ -23,7 +23,7 @@ type VersionsTabProps = { const VersionsTab: FC = ({ instanceId: appId }) => { const { t } = useTranslation('deployments') - const appData = useDeploymentsStore(state => state.appData[appId]) + const appData = useDeploymentAppData(appId) const releaseRows = useMemo( () => appData?.releaseHistory.data?.filter(row => (row.release ?? row).id) ?? [], [appData?.releaseHistory.data], diff --git a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx index 1f5a56578a..849598b228 100644 --- a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx +++ b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx @@ -11,7 +11,7 @@ import { import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSourceApps } from '../../hooks/use-source-apps' -import { useDeploymentsStore } from '../../store' +import { useDeploymentAppData, useDeploymentsStore } from '../../store' import { activeRelease, deployedRows, @@ -28,7 +28,7 @@ type DeployReleaseMenuProps = { export const DeployReleaseMenu: FC = ({ appId, releaseId }) => { const { t } = useTranslation('deployments') - const appData = useDeploymentsStore(state => state.appData[appId]) + const appData = useDeploymentAppData(appId) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const openRollbackModal = useDeploymentsStore(state => state.openRollbackModal) const [open, setOpen] = useState(false) diff --git a/web/features/deployments/hooks/use-deployment-data.ts b/web/features/deployments/hooks/use-deployment-data.ts index ec0bf4487b..f0edfa7e54 100644 --- a/web/features/deployments/hooks/use-deployment-data.ts +++ b/web/features/deployments/hooks/use-deployment-data.ts @@ -2,7 +2,6 @@ import type { AppInfo } from '../types' import { useQueries } from '@tanstack/react-query' -import { useEffect, useRef } from 'react' import { deploymentAppDataQueryOptions } from '../data' import { useDeploymentsStore } from '../store' @@ -12,26 +11,15 @@ type UseDeploymentDataOptions = { export function useDeploymentData(apps: AppInfo[], options: UseDeploymentDataOptions = {}) { const { enabled = true } = options - const applyAppData = useDeploymentsStore(state => state.applyAppData) const queries = useQueries({ queries: apps.map(app => ({ ...deploymentAppDataQueryOptions(app.id), + queryFn: () => useDeploymentsStore.getState().fetchAppData(app.id), enabled: enabled && Boolean(app.id), })), }) - const queriesRef = useRef(queries) - queriesRef.current = queries - const dataUpdatedAt = queries.map(query => query.dataUpdatedAt).join('|') - - useEffect(() => { - queriesRef.current.forEach((query) => { - if (query.data) - applyAppData(query.data) - }) - }, [applyAppData, dataUpdatedAt]) - return { isLoading: queries.some(query => query.isLoading), isFetching: queries.some(query => query.isFetching), diff --git a/web/features/deployments/hooks/use-source-apps.ts b/web/features/deployments/hooks/use-source-apps.ts index 4956e37f95..91f94ec296 100644 --- a/web/features/deployments/hooks/use-source-apps.ts +++ b/web/features/deployments/hooks/use-source-apps.ts @@ -1,29 +1,13 @@ 'use client' -import type { AppInfo, AppMode } from '../types' +import type { AppInfo } from '../types' import type { AppDeploymentSummary, EnvironmentOption } from '@/contract/console/deployments' import { useQuery } from '@tanstack/react-query' -import { useEffect, useMemo } from 'react' +import { useMemo } from 'react' import { consoleQuery } from '@/service/client' import { useDeploymentsStore } from '../store' const MAX_SOURCE_APPS = 100 -function toAppInfo(summary: AppDeploymentSummary): AppInfo | undefined { - if (!summary.id || !summary.name) - return undefined - - return { - id: summary.id, - name: summary.name, - mode: (summary.mode || 'workflow') as AppMode, - iconType: 'emoji', - icon: summary.icon, - description: summary.description ?? undefined, - sourceAppId: summary.sourceAppId, - sourceAppName: summary.sourceAppName, - } -} - type UseSourceAppsOptions = { enabled?: boolean environmentId?: string @@ -33,7 +17,7 @@ type UseSourceAppsOptions = { export function useSourceApps(options: UseSourceAppsOptions = {}) { const { enabled = true, environmentId, keyword, notDeployed } = options - const seedInstancesFromApps = useDeploymentsStore(state => state.seedInstancesFromApps) + const instancesById = useDeploymentsStore(state => state.instancesById) const query = useMemo(() => ({ pageNumber: 1, @@ -45,16 +29,23 @@ export function useSourceApps(options: UseSourceAppsOptions = {}) { const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ input: { query }, + queryFn: () => useDeploymentsStore.getState().fetchSourceApps(query), enabled, staleTime: 30 * 1000, })) - const apps = useMemo(() => { + const appIds = useMemo(() => { return (listQuery.data?.data ?? []) - .map(toAppInfo) - .filter((app): app is AppInfo => Boolean(app)) + .map(summary => summary.id) + .filter((id): id is string => Boolean(id)) }, [listQuery.data?.data]) + const apps = useMemo(() => { + return appIds + .map(id => instancesById[id]) + .filter((app): app is AppInfo => Boolean(app)) + }, [appIds, instancesById]) + const appMap = useMemo>(() => { return new Map(apps.map(a => [a.id, a])) }, [apps]) @@ -76,12 +67,6 @@ export function useSourceApps(options: UseSourceAppsOptions = {}) { })) ?? [] }, [listQuery.data?.filters]) - useEffect(() => { - if (!enabled || listQuery.isLoading) - return - seedInstancesFromApps(apps) - }, [apps, enabled, listQuery.isLoading, seedInstancesFromApps]) - return { apps, appMap, diff --git a/web/features/deployments/list/index.tsx b/web/features/deployments/list/index.tsx index 559171be69..65c076d26b 100644 --- a/web/features/deployments/list/index.tsx +++ b/web/features/deployments/list/index.tsx @@ -18,7 +18,6 @@ import { NewInstanceCard } from './new-instance-card' const DeploymentsMain: FC = () => { const { t } = useTranslation('deployments') - const appData = useDeploymentsStore(state => state.appData) const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal) const [envFilter, setEnvFilter] = useQueryState( @@ -122,7 +121,6 @@ const DeploymentsMain: FC = () => { ))} diff --git a/web/features/deployments/list/instance-card.tsx b/web/features/deployments/list/instance-card.tsx index 90563db658..0c445eabdc 100644 --- a/web/features/deployments/list/instance-card.tsx +++ b/web/features/deployments/list/instance-card.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC, MouseEvent } from 'react' -import type { DeploymentAppData } from '../data' import type { AppInfo } from '../types' import type { AppDeploymentSummary } from '@/contract/console/deployments' import type { AppModeEnum } from '@/types/app' @@ -20,20 +19,20 @@ import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import { useRouter } from '@/next/navigation' -import { useDeploymentsStore } from '../store' +import { useDeploymentAppData, useDeploymentsStore } from '../store' import { deployedRows, deploymentStatus, environmentId, environmentName, releaseLabel } from '../utils' type InstanceCardProps = { app: AppInfo - appData?: DeploymentAppData summary?: AppDeploymentSummary } -export const InstanceCard: FC = ({ app, appData, summary }) => { +export const InstanceCard: FC = ({ app, summary }) => { const { t } = useTranslation('deployments') const router = useRouter() const { formatTimeFromNow } = useFormatTimeFromNow() const [menuOpen, setMenuOpen] = useState(false) + const appData = useDeploymentAppData(app.id) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const navigateToDetail = () => router.push(`/deployments/${app.id}/overview`) diff --git a/web/features/deployments/nav/index.tsx b/web/features/deployments/nav/index.tsx index 658d041b2b..3b65af7b04 100644 --- a/web/features/deployments/nav/index.tsx +++ b/web/features/deployments/nav/index.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import Nav from '@/app/components/header/nav' import { useParams, useRouter, useSelectedLayoutSegment } from '@/next/navigation' import { useSourceApps } from '../hooks/use-source-apps' -import { useDeploymentsStore } from '../store' +import { useDeploymentInstance, useDeploymentsStore } from '../store' const DeploymentsNav = () => { const { t } = useTranslation() @@ -17,19 +17,18 @@ const DeploymentsNav = () => { const params = useParams<{ instanceId?: string }>() const instanceId = params?.instanceId - const sourceApps = useDeploymentsStore(state => state.sourceApps) const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal) + const currentInstance = useDeploymentInstance(instanceId) - const { appMap } = useSourceApps({ enabled: isActive }) - const apps = useMemo( - () => sourceApps.length > 0 ? sourceApps : [...appMap.values()], - [appMap, sourceApps], - ) + const { apps } = useSourceApps({ enabled: isActive }) const navigationItems = useMemo(() => { if (!isActive) return [] - return apps.map((app) => { + const navApps = currentInstance && !apps.some(app => app.id === currentInstance.id) + ? [...apps, currentInstance] + : apps + return navApps.map((app) => { return { id: app.id, name: app.name, @@ -41,7 +40,7 @@ const DeploymentsNav = () => { mode: app.mode as unknown as AppModeEnum | undefined, } }) - }, [apps, isActive]) + }, [apps, currentInstance, isActive]) const curNav = useMemo(() => { if (!instanceId) diff --git a/web/features/deployments/store.ts b/web/features/deployments/store.ts index 605a69d6cf..897b3c71ee 100644 --- a/web/features/deployments/store.ts +++ b/web/features/deployments/store.ts @@ -1,6 +1,6 @@ -import type { DeploymentAppData } from './data' +import type { DeploymentAppData, ListAppDeploymentsQuery } from './data' import type { AppInfo } from './types' -import type { AccessSubject, APIToken, ConsoleReleaseSummary } from '@/contract/console/deployments' +import type { AccessSubject, APIToken, ConsoleReleaseSummary, ListAppDeploymentsReply } from '@/contract/console/deployments' import { create } from 'zustand' import { cancelDeployment, @@ -9,11 +9,15 @@ import { createDeployment, deleteApiKey, deleteAppInstance, + fetchDeploymentAppData, + listAppDeployments, patchAccessChannel, patchDeveloperAPI, refreshDeploymentAppData, refreshDeploymentAppDataWhenReady, refreshDeploymentLists, + toAppInfoFromOverview, + toAppInfoFromSummary, undeployEnvironment, updateAppInstance, updateEnvironmentAccessPolicy, @@ -57,7 +61,7 @@ export type CreateInstanceResult = { } type DeploymentsState = { - sourceApps: AppInfo[] + instancesById: Record appData: Record createdApiToken?: CreatedApiToken @@ -85,8 +89,10 @@ type DeploymentsState = { openCreateInstanceModal: () => void closeCreateInstanceModal: () => void - seedInstancesFromApps: (apps: AppInfo[]) => void + upsertInstances: (apps: AppInfo[]) => void applyAppData: (data: DeploymentAppData) => void + fetchSourceApps: (query: ListAppDeploymentsQuery) => Promise + fetchAppData: (appId: string) => Promise refreshAppData: (appId: string) => Promise createInstance: (params: CreateInstanceParams) => Promise @@ -115,7 +121,7 @@ type DeploymentsState = { } export const useDeploymentsStore = create((set, get) => ({ - sourceApps: [], + instancesById: {}, appData: {}, createdApiToken: undefined, @@ -141,8 +147,14 @@ export const useDeploymentsStore = create((set, get) => ({ openCreateInstanceModal: () => set({ createInstanceModal: { open: true } }), closeCreateInstanceModal: () => set({ createInstanceModal: { open: false } }), - seedInstancesFromApps: apps => set(() => ({ - sourceApps: apps, + upsertInstances: apps => set(state => ({ + instancesById: apps.reduce((next, app) => { + next[app.id] = { + ...next[app.id], + ...app, + } + return next + }, { ...state.instancesById }), })), applyAppData: data => set(state => ({ @@ -152,9 +164,30 @@ export const useDeploymentsStore = create((set, get) => ({ }, })), + fetchSourceApps: async (query) => { + const response = await listAppDeployments(query) + const apps = response.data + ?.map(toAppInfoFromSummary) + .filter((app): app is AppInfo => Boolean(app)) ?? [] + get().upsertInstances(apps) + return response + }, + + fetchAppData: async (appId) => { + const data = await fetchDeploymentAppData(appId) + get().applyAppData(data) + const app = toAppInfoFromOverview(data.overview.instance) + if (app) + get().upsertInstances([app]) + return data + }, + refreshAppData: async (appId) => { const data = await refreshDeploymentAppData(appId) get().applyAppData(data) + const app = toAppInfoFromOverview(data.overview.instance) + if (app) + get().upsertInstances([app]) }, createInstance: async ({ sourceAppId, name, description }) => { @@ -164,9 +197,20 @@ export const useDeploymentsStore = create((set, get) => ({ set({ createInstanceModal: { open: false } }) await Promise.allSettled([ refreshDeploymentAppDataWhenReady(response.appInstanceId) - .then(data => get().applyAppData(data)), - waitForAppInstanceInDeploymentList(response.appInstanceId), + .then((data) => { + get().applyAppData(data) + const app = toAppInfoFromOverview(data.overview.instance) + if (app) + get().upsertInstances([app]) + }), + waitForAppInstanceInDeploymentList(response.appInstanceId).then((list) => { + const apps = list?.data + ?.map(toAppInfoFromSummary) + .filter((app): app is AppInfo => Boolean(app)) ?? [] + get().upsertInstances(apps) + }), ]) + await refreshDeploymentLists() return { appInstanceId: response.appInstanceId, initialRelease: response.initialRelease, @@ -181,7 +225,16 @@ export const useDeploymentsStore = create((set, get) => ({ await get().refreshAppData(appId) await refreshDeploymentLists() set(state => ({ - sourceApps: state.sourceApps.map(app => app.id === appId ? { ...app, ...patch } : app), + instancesById: { + ...state.instancesById, + [appId]: { + ...state.instancesById[appId], + id: appId, + name: patch.name, + mode: state.instancesById[appId]?.mode ?? 'workflow', + description: patch.description, + }, + }, })) }, @@ -191,8 +244,9 @@ export const useDeploymentsStore = create((set, get) => ({ await deleteAppInstance(appId) set((state) => { const { [appId]: _removed, ...appData } = state.appData + const { [appId]: _removedInstance, ...instancesById } = state.instancesById return { - sourceApps: state.sourceApps.filter(app => app.id !== appId), + instancesById, appData, } }) @@ -278,3 +332,11 @@ export const useDeploymentsStore = create((set, get) => ({ await get().refreshAppData(appId) }, })) + +export const useDeploymentInstance = (appId?: string) => { + return useDeploymentsStore(state => appId ? state.instancesById[appId] : undefined) +} + +export const useDeploymentAppData = (appId?: string) => { + return useDeploymentsStore(state => appId ? state.appData[appId] : undefined) +}