From 7f2d094cf3a811b5ae1de5c14e8d3ac37d24aa11 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:44:03 +0800 Subject: [PATCH] tweaks --- .../deployments/components/deploy-drawer.tsx | 38 +- .../components/deploy-drawer/form.tsx | 1 - .../deployments/components/rollback-modal.tsx | 58 ++- web/features/deployments/data.ts | 348 +--------------- .../deployments/detail/access-tab.tsx | 33 +- .../detail/access-tab/permissions.tsx | 2 - .../deployments/detail/deploy-tab.tsx | 37 +- web/features/deployments/detail/index.tsx | 54 ++- .../deployments/detail/overview-tab.tsx | 38 +- .../deployments/detail/settings-tab.tsx | 48 ++- .../deployments/detail/versions-tab.tsx | 29 +- .../versions-tab/deploy-release-menu.tsx | 35 +- .../deployments/hooks/use-deployment-data.ts | 41 -- .../hooks/use-deployment-mutations.ts | 377 ++++++++++-------- .../deployments/hooks/use-source-apps.ts | 81 ---- web/features/deployments/list/index.tsx | 37 +- .../deployments/list/instance-card.tsx | 87 +--- web/features/deployments/nav/index.tsx | 28 +- web/features/deployments/utils.ts | 74 +++- 19 files changed, 630 insertions(+), 816 deletions(-) delete mode 100644 web/features/deployments/hooks/use-deployment-data.ts delete mode 100644 web/features/deployments/hooks/use-source-apps.ts diff --git a/web/features/deployments/components/deploy-drawer.tsx b/web/features/deployments/components/deploy-drawer.tsx index 2ea1ab97c4..0ba3ebfe30 100644 --- a/web/features/deployments/components/deploy-drawer.tsx +++ b/web/features/deployments/components/deploy-drawer.tsx @@ -2,11 +2,17 @@ import type { FC } from 'react' import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog' +import { skipToken, useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useDeploymentAppData } from '../hooks/use-deployment-data' +import { consoleQuery } from '@/service/client' +import { + DEPLOYMENT_PAGE_SIZE, + SOURCE_APPS_PAGE_SIZE, +} from '../data' import { useStartDeployment } from '../hooks/use-deployment-mutations' -import { useSourceApps } from '../hooks/use-source-apps' import { useDeploymentsStore } from '../store' +import { environmentOptionsFromList } from '../utils' import { DeployForm } from './deploy-drawer/form' const DeployDrawer: FC = () => { @@ -16,13 +22,31 @@ const DeployDrawer: FC = () => { const closeDeployDrawer = useDeploymentsStore(state => state.closeDeployDrawer) const startDeploy = useStartDeployment() const open = drawer.open - const { environmentOptions } = useSourceApps({ enabled: open }) - const { data: appData } = useDeploymentAppData(drawerAppId, { + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + enabled: open, + })) + const { data: releaseHistory } = useQuery(consoleQuery.deployments.releaseHistory.queryOptions({ + input: drawerAppId + ? { + params: { appInstanceId: drawerAppId }, + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + } + : skipToken, enabled: open && Boolean(drawerAppId), - }) + })) + const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data]) const environments = environmentOptions - const releases = appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => release.id) ?? [] + const releases = releaseHistory?.data?.map(row => row.release ?? row).filter(release => release.id) ?? [] const defaultReleaseId = releases[0]?.id const formKey = `${drawer.appId ?? 'none'}-${drawer.environmentId ?? 'any'}-${drawer.releaseId ?? 'new'}-${open ? '1' : '0'}` @@ -35,7 +59,7 @@ const DeployDrawer: FC = () => { {!drawerAppId ?
{t('deployDrawer.notFound')}
- : !appData + : !releaseHistory ? (
diff --git a/web/features/deployments/components/deploy-drawer/form.tsx b/web/features/deployments/components/deploy-drawer/form.tsx index 0dcb52afff..2eafcd9787 100644 --- a/web/features/deployments/components/deploy-drawer/form.tsx +++ b/web/features/deployments/components/deploy-drawer/form.tsx @@ -112,7 +112,6 @@ export const DeployForm: FC = ({ }, } : skipToken, - staleTime: 30 * 1000, })) const previewBindings = releasePreview.data?.bindings ?? [] const modelBindings = previewBindings.filter(isRuntimeModelBinding) diff --git a/web/features/deployments/components/rollback-modal.tsx b/web/features/deployments/components/rollback-modal.tsx index b4dd227232..9d30c73097 100644 --- a/web/features/deployments/components/rollback-modal.tsx +++ b/web/features/deployments/components/rollback-modal.tsx @@ -9,19 +9,27 @@ import { AlertDialogDescription, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' +import { skipToken, useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { toAppInfoFromOverview } from '../data' -import { useCachedDeploymentAppData } from '../hooks/use-deployment-data' +import { consoleQuery } from '@/service/client' +import { + DEPLOYMENT_PAGE_SIZE, + SOURCE_APPS_PAGE_SIZE, +} from '../data' import { useStartDeployment } from '../hooks/use-deployment-mutations' -import { useSourceApps } from '../hooks/use-source-apps' import { useDeploymentsStore } from '../store' import { activeRelease, deployedRows, environmentId, environmentName, + environmentOptionsFromList, releaseCommit, releaseLabel, + sourceAppMapFromApps, + sourceAppsFromList, + toAppInfoFromOverview, } from '../utils' const InfoRow: FC<{ label: string, value: string }> = ({ label, value }) => { @@ -36,20 +44,54 @@ const InfoRow: FC<{ label: string, value: string }> = ({ label, value }) => { const RollbackModal: FC = () => { const { t } = useTranslation('deployments') const modal = useDeploymentsStore(state => state.rollbackModal) - const { data: appData } = useCachedDeploymentAppData(modal.appId) const closeRollbackModal = useDeploymentsStore(state => state.closeRollbackModal) const rollbackDeployment = useStartDeployment() - const { appMap, environmentOptions } = useSourceApps() + const appInput = modal.appId + ? { params: { appInstanceId: modal.appId } } + : undefined + const pagedInput = appInput + ? { + ...appInput, + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + } + : undefined + const { data: overview } = useQuery(consoleQuery.deployments.overview.queryOptions({ + input: appInput ?? skipToken, + enabled: modal.open && Boolean(modal.appId), + })) + const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({ + input: pagedInput ?? skipToken, + enabled: modal.open && Boolean(modal.appId), + })) + const { data: releaseHistory } = useQuery(consoleQuery.deployments.releaseHistory.queryOptions({ + input: pagedInput ?? skipToken, + enabled: modal.open && Boolean(modal.appId), + })) + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + enabled: modal.open, + })) + const sourceApps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data]) + const appMap = useMemo(() => sourceAppMapFromApps(sourceApps), [sourceApps]) + const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data]) - const currentRow = deployedRows(appData?.environmentDeployments.data) + const currentRow = deployedRows(environmentDeployments?.data) .find(row => environmentId(row.environment) === modal.environmentId) const targetRelease = [ - ...(appData?.releaseHistory.data?.map(row => row.release ?? row).filter(release => !!release?.id) ?? []), + ...(releaseHistory?.data?.map(row => row.release ?? row).filter(release => !!release?.id) ?? []), ].find(release => release?.id === modal.targetReleaseId) const currentRelease = activeRelease(currentRow) const environment = currentRow?.environment ?? environmentOptions.find(env => env.id === modal.environmentId) - const app = toAppInfoFromOverview(appData?.overview.instance) + const app = toAppInfoFromOverview(overview?.instance) ?? (modal.appId ? appMap.get(modal.appId) : undefined) const confirm = () => { diff --git a/web/features/deployments/data.ts b/web/features/deployments/data.ts index bfa5a0adf6..0ba9317308 100644 --- a/web/features/deployments/data.ts +++ b/web/features/deployments/data.ts @@ -1,346 +1,2 @@ -import type { AppInfo, AppMode } from './types' -import type { - AccessSubject, - AppDeploymentSummary, - AppInstanceOverview, - ConsoleReleaseSummary, - CreateAppInstanceReply, - GetAccessConfigReply, - GetDeploymentOverviewReply, - ListAppDeploymentsReply, - ListEnvironmentDeploymentsReply, - ListReleaseHistoryReply, -} from '@/contract/console/deployments' -import { queryOptions } from '@tanstack/react-query' -import { consoleClient } 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 - overview: GetDeploymentOverviewReply - environmentDeployments: ListEnvironmentDeploymentsReply - releaseHistory: ListReleaseHistoryReply - accessConfig: GetAccessConfigReply -} - -export type CreateDeploymentParams = { - appId: string - environmentId: string - releaseId?: string - releaseNote?: string -} - -export type CreateInstanceParams = { - sourceAppId: string - name: string - description?: string -} - -export type UpdateInstanceParams = { - name: string - 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 [ - overview, - environmentDeployments, - releaseHistory, - accessConfig, - ] = await Promise.all([ - consoleClient.deployments.overview(input), - consoleClient.deployments.environmentDeployments({ - ...input, - query: { - pageNumber: 1, - resultsPerPage: DEPLOYMENT_PAGE_SIZE, - }, - }), - consoleClient.deployments.releaseHistory({ - ...input, - query: { - pageNumber: 1, - resultsPerPage: DEPLOYMENT_PAGE_SIZE, - }, - }), - consoleClient.deployments.accessConfig(input), - ]) - - return { - appId, - overview, - environmentDeployments, - releaseHistory, - accessConfig, - } -} - -export const deploymentAppDataQueryOptions = (appId: string) => - queryOptions({ - queryKey: deploymentAppDataQueryKey(appId), - queryFn: () => fetchDeploymentAppData(appId), - staleTime: DEPLOYMENT_APP_DATA_STALE_TIME, - }) - -const wait = (delay: number) => 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 fetchDeploymentAppData(appId) - } - catch (error) { - lastError = error - } - } - - throw lastError -} - -export const waitForAppInstanceInDeploymentList = async (appInstanceId: string): Promise => { - let lastError: unknown - - for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) { - if (delay > 0) - await wait(delay) - - try { - const response = await listAppDeployments({ - pageNumber: 1, - resultsPerPage: DEPLOYMENT_PAGE_SIZE, - }) - if (response.data?.some(app => app.id === appInstanceId)) - return response - } - catch (error) { - lastError = error - } - } - - if (lastError) - throw lastError - - return undefined -} - -export const createRelease = async (appId: string, releaseNote?: string): Promise => { - const trimmedReleaseNote = releaseNote?.trim() - const response = await consoleClient.deployments.createRelease({ - params: { - appInstanceId: appId, - }, - body: { - name: trimmedReleaseNote || 'Release', - description: trimmedReleaseNote || undefined, - }, - }) - if (!response.release) - throw new Error('Create release did not return a release.') - return response.release -} - -export const createDeployment = async ({ - appId, - environmentId, - releaseId, - releaseNote, -}: CreateDeploymentParams) => { - let targetReleaseId = releaseId - await consoleClient.deployments.previewRelease({ - params: { - appInstanceId: appId, - }, - body: { - releaseId: targetReleaseId, - }, - }) - if (!targetReleaseId) { - const release = await createRelease(appId, releaseNote) - targetReleaseId = release.id - } - if (!targetReleaseId) - throw new Error('Failed to create a deployable release.') - - return consoleClient.deployments.createDeployment({ - params: { - appInstanceId: appId, - }, - body: { - environmentId, - releaseId: targetReleaseId, - }, - }) -} - -export const cancelDeployment = async (appId: string, runtimeInstanceId: string) => { - return consoleClient.deployments.cancelDeployment({ - params: { - appInstanceId: appId, - runtimeInstanceId, - }, - }) -} - -export const undeployEnvironment = async (appId: string, runtimeInstanceId: string) => { - return consoleClient.deployments.undeployEnvironment({ - params: { - appInstanceId: appId, - runtimeInstanceId, - }, - }) -} - -export const createApiKey = async (appId: string, environmentId: string, name: string) => { - return consoleClient.deployments.createEnvironmentAPIToken({ - params: { - appInstanceId: appId, - }, - body: { - environmentId, - name, - }, - }) -} - -export const deleteApiKey = async (appId: string, apiKeyId: string) => { - return consoleClient.deployments.deleteEnvironmentAPIToken({ - params: { - appInstanceId: appId, - apiKeyId, - }, - }) -} - -export const patchAccessChannel = async (appId: string, enabled: boolean) => { - return consoleClient.deployments.patchAccessChannel({ - params: { - appInstanceId: appId, - }, - body: { - enabled, - }, - }) -} - -export const patchDeveloperAPI = async (appId: string, enabled: boolean) => { - return consoleClient.deployments.patchDeveloperAPI({ - params: { - appInstanceId: appId, - }, - body: { - enabled, - }, - }) -} - -export const updateEnvironmentAccessPolicy = async ( - appId: string, - environmentId: string, - accessMode: string, - subjects: AccessSubject[] = [], -) => { - return consoleClient.deployments.updateEnvironmentAccessPolicy({ - params: { - appInstanceId: appId, - environmentId, - }, - body: { - accessMode, - subjects, - }, - }) -} - -export const createAppInstance = async ({ - sourceAppId, - name, - description, -}: CreateInstanceParams): Promise => { - return consoleClient.deployments.createInstance({ - body: { - sourceAppId, - name, - description, - }, - }) -} - -export const updateAppInstance = async ( - appId: string, - { name, description }: UpdateInstanceParams, -) => { - return consoleClient.deployments.updateInstance({ - params: { - appInstanceId: appId, - }, - body: { - name, - description, - }, - }) -} - -export const deleteAppInstance = async (appId: string) => { - return consoleClient.deployments.deleteInstance({ - params: { - appInstanceId: appId, - }, - }) -} +export const DEPLOYMENT_PAGE_SIZE = 100 +export const SOURCE_APPS_PAGE_SIZE = 100 diff --git a/web/features/deployments/detail/access-tab.tsx b/web/features/deployments/detail/access-tab.tsx index b5fbf695f8..f3575ffdea 100644 --- a/web/features/deployments/detail/access-tab.tsx +++ b/web/features/deployments/detail/access-tab.tsx @@ -6,8 +6,10 @@ import type { AccessSubject, ConsoleEnvironmentSummary, } from '@/contract/console/deployments' +import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' -import { useCachedDeploymentAppData } from '../hooks/use-deployment-data' +import { consoleQuery } from '@/service/client' +import { DEPLOYMENT_PAGE_SIZE } from '../data' import { useGenerateDeploymentApiKey, useRevokeDeploymentApiKey, @@ -37,7 +39,19 @@ type AccessTabProps = { } const AccessTab: FC = ({ instanceId: appId }) => { - const { data: appData } = useCachedDeploymentAppData(appId) + const appInput = { params: { appInstanceId: appId } } + const { data: accessConfig } = useQuery(consoleQuery.deployments.accessConfig.queryOptions({ + input: appInput, + })) + const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({ + input: { + ...appInput, + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + }, + })) const [createdApiToken, setCreatedApiToken] = useState<{ appId: string token: string @@ -47,10 +61,9 @@ const AccessTab: FC = ({ instanceId: appId }) => { const toggleAccessChannel = useToggleDeploymentAccessChannel() const setEnvironmentAccessPolicy = useSetEnvironmentAccessPolicy() - const accessConfig = appData?.accessConfig const deploymentRows = useMemo( - () => deployedRows(appData?.environmentDeployments.data), - [appData?.environmentDeployments.data], + () => deployedRows(environmentDeployments?.data), + [environmentDeployments?.data], ) const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS const deployedEnvs = useMemo( @@ -63,9 +76,17 @@ const AccessTab: FC = ({ instanceId: appId }) => { ) const apiEnabled = accessConfig?.developerApi?.enabled ?? false const apiKeys = accessConfig?.developerApi?.apiKeys ?? [] + const createApiKeyLabel = (environmentId: string) => { + const existingCount = apiKeys.filter(key => + (key.environmentId ?? key.environment?.id) === environmentId, + ).length + const name = deployedEnvs.find(env => env.id === environmentId)?.name ?? 'env' + + return `${name}-key-${String(existingCount + 1).padStart(3, '0')}` + } const handleGenerateApiKey = (environmentId: string) => { generateApiKey.mutate( - { appId, environmentId }, + { appId, environmentId, name: createApiKeyLabel(environmentId) }, { onSuccess: (response) => { if (response.apiToken?.token) diff --git a/web/features/deployments/detail/access-tab/permissions.tsx b/web/features/deployments/detail/access-tab/permissions.tsx index 26d6afac2e..1248299520 100644 --- a/web/features/deployments/detail/access-tab/permissions.tsx +++ b/web/features/deployments/detail/access-tab/permissions.tsx @@ -200,7 +200,6 @@ const SubjectPicker: FC = ({ }, } : skipToken, - staleTime: 30 * 1000, })) const subjects = useMemo( () => subjectsQuery.data?.data @@ -326,7 +325,6 @@ export const EnvironmentPermissionRow: FC = ({ }, } : skipToken, - staleTime: 30 * 1000, })) const detailPolicy = policyQuery.data?.policy const policyKind = accessModeToPermissionKey(detailPolicy?.accessMode ?? summaryPolicy?.accessMode) diff --git a/web/features/deployments/detail/deploy-tab.tsx b/web/features/deployments/detail/deploy-tab.tsx index a5858ba091..94c07a0aa5 100644 --- a/web/features/deployments/detail/deploy-tab.tsx +++ b/web/features/deployments/detail/deploy-tab.tsx @@ -8,11 +8,15 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useCachedDeploymentAppData } from '../hooks/use-deployment-data' +import { consoleQuery } from '@/service/client' +import { + DEPLOYMENT_PAGE_SIZE, + SOURCE_APPS_PAGE_SIZE, +} from '../data' import { useUndeployDeployment } from '../hooks/use-deployment-mutations' -import { useSourceApps } from '../hooks/use-source-apps' import { useDeploymentsStore } from '../store' import { activeRelease, @@ -23,6 +27,7 @@ import { environmentId, environmentMode, environmentName, + environmentOptionsFromList, isUndeployedDeploymentRow, releaseCommit, releaseLabel, @@ -38,18 +43,34 @@ type DeployTabProps = { const DeployTab: FC = ({ instanceId: appId }) => { const { t } = useTranslation('deployments') - const { data: appData } = useCachedDeploymentAppData(appId) + const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({ + input: { + params: { appInstanceId: appId }, + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + }, + })) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const undeployDeployment = useUndeployDeployment() - const { environmentOptions } = useSourceApps() + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + })) + const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data]) const rows = useMemo( - () => appData?.environmentDeployments.data?.filter(row => row.environment?.id) ?? [], - [appData?.environmentDeployments.data], + () => environmentDeployments?.data?.filter(row => row.environment?.id) ?? [], + [environmentDeployments?.data], ) const deployedRuntimeRows = useMemo( - () => deployedRows(appData?.environmentDeployments.data), - [appData?.environmentDeployments.data], + () => deployedRows(environmentDeployments?.data), + [environmentDeployments?.data], ) const deployedEnvIds = new Set(deployedRuntimeRows.map(row => environmentId(row.environment))) diff --git a/web/features/deployments/detail/index.tsx b/web/features/deployments/detail/index.tsx index 96bb7a54bd..87d8829895 100644 --- a/web/features/deployments/detail/index.tsx +++ b/web/features/deployments/detail/index.tsx @@ -3,17 +3,23 @@ import type { FC, ReactNode } from 'react' import type { InstanceDetailTabKey } from './tabs' import { Button } from '@langgenius/dify-ui/button' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels' import useDocumentTitle from '@/hooks/use-document-title' import { useRouter, useSelectedLayoutSegment } from '@/next/navigation' +import { consoleQuery } from '@/service/client' import DeployDrawer from '../components/deploy-drawer' import RollbackModal from '../components/rollback-modal' -import { toAppInfoFromOverview } from '../data' -import { useDeploymentAppData } from '../hooks/use-deployment-data' -import { useSourceApps } from '../hooks/use-source-apps' -import { deployedRows, deploymentStatus } from '../utils' +import { + SOURCE_APPS_PAGE_SIZE, +} from '../data' +import { + deploymentSummariesFromList, + sourceAppMapFromApps, + sourceAppsFromList, +} from '../utils' import { DeploymentSidebar } from './deployment-sidebar' import { isInstanceDetailTabKey } from './tabs' @@ -29,25 +35,31 @@ const InstanceDetail: FC = ({ instanceId, children }) => { const selectedSegment = useSelectedLayoutSegment() const selectedTab = selectedSegment ?? undefined const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview' - const detailQuery = useDeploymentAppData(instanceId, { enabled: Boolean(instanceId) }) - const appData = detailQuery.data - const { appMap, isLoading: isLoadingApps } = useSourceApps() + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + })) useDocumentTitle(t('documentTitle.detail')) - const detailApp = useMemo( - () => toAppInfoFromOverview(appData?.overview.instance), - [appData?.overview.instance], - ) + const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data]) + const appMap = useMemo(() => sourceAppMapFromApps(apps), [apps]) + const summaries = useMemo(() => deploymentSummariesFromList(listQuery.data), [listQuery.data]) const app = useMemo( - () => detailApp ?? appMap.get(instanceId), - [detailApp, instanceId, appMap], - ) - const appDeployments = useMemo( - () => deployedRows(appData?.environmentDeployments.data), - [appData?.environmentDeployments.data], + () => appMap.get(instanceId), + [instanceId, appMap], ) + const summary = summaries[instanceId] + const statusCount = (status: string) => + summary?.statuses?.find(item => item.status === status)?.count ?? 0 + const envCount = summary?.statuses + ?.filter(item => item.status !== 'undeployed') + .reduce((total, item) => total + (item.count ?? 0), 0) ?? 0 - if (!app && (isLoadingApps || detailQuery.isLoading || detailQuery.isFetching)) { + if (!app && listQuery.isLoading) { return (
@@ -67,8 +79,8 @@ const InstanceDetail: FC = ({ instanceId, children }) => { ) } - const deployingCount = appDeployments.filter(row => deploymentStatus(row) === 'deploying').length - const failedCount = appDeployments.filter(row => deploymentStatus(row) === 'deploy_failed').length + const deployingCount = statusCount('deploying') + const failedCount = statusCount('failed') + statusCount('deploy_failed') const appModeLabel = app ? getAppModeLabel(app.mode, tCommon) : t('detail.sourceAppDeleted') return ( @@ -92,7 +104,7 @@ const InstanceDetail: FC = ({ instanceId, children }) => {
{t(`tabs.${activeTab}.description`)}
- {t('detail.envCount', { count: appDeployments.length })} + {t('detail.envCount', { count: envCount })} {deployingCount > 0 && ( <> · diff --git a/web/features/deployments/detail/overview-tab.tsx b/web/features/deployments/detail/overview-tab.tsx index 4401d50ea0..cb9695ac04 100644 --- a/web/features/deployments/detail/overview-tab.tsx +++ b/web/features/deployments/detail/overview-tab.tsx @@ -2,16 +2,24 @@ import type { FC, ReactNode } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels' import { useRouter } from '@/next/navigation' +import { consoleQuery } from '@/service/client' import { StatusBadge } from '../components/status-badge' -import { toAppInfoFromOverview } from '../data' -import { useCachedDeploymentAppData } from '../hooks/use-deployment-data' -import { useSourceApps } from '../hooks/use-source-apps' +import { + SOURCE_APPS_PAGE_SIZE, +} from '../data' import { useDeploymentsStore } from '../store' -import { releaseLabel, webappUrl } from '../utils' +import { + releaseLabel, + sourceAppMapFromApps, + sourceAppsFromList, + toAppInfoFromOverview, + webappUrl, +} from '../utils' type OverviewTabProps = { instanceId: string @@ -92,10 +100,24 @@ const OverviewTab: FC = ({ instanceId }) => { const { t } = useTranslation('deployments') const { t: tCommon } = useTranslation() const router = useRouter() - const { data: appData } = useCachedDeploymentAppData(instanceId) + const input = { params: { appInstanceId: instanceId } } + const { data: overview } = useQuery(consoleQuery.deployments.overview.queryOptions({ + input, + })) + const { data: accessConfig } = useQuery(consoleQuery.deployments.accessConfig.queryOptions({ + input, + })) + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + })) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) - const { appMap } = useSourceApps() - const overview = appData?.overview + const sourceApps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data]) + const appMap = useMemo(() => sourceAppMapFromApps(sourceApps), [sourceApps]) const app = toAppInfoFromOverview(overview?.instance) ?? appMap.get(instanceId) const overviewApp = overview?.instance const deployments = useMemo( @@ -113,7 +135,7 @@ const OverviewTab: FC = ({ instanceId }) => { const appModeLabel = getAppModeLabel(overviewApp?.mode ?? app.mode, tCommon) const webappAccessUrl = webappUrl(overview?.access?.webappUrl) const cliUrl = overview?.access?.cliUrl - const apiKeysCount = overview?.access?.apiKeyCount ?? appData?.accessConfig.developerApi?.apiKeys?.length ?? 0 + const apiKeysCount = overview?.access?.apiKeyCount ?? accessConfig?.developerApi?.apiKeys?.length ?? 0 return (
diff --git a/web/features/deployments/detail/settings-tab.tsx b/web/features/deployments/detail/settings-tab.tsx index 8764856220..0d9cc8466d 100644 --- a/web/features/deployments/detail/settings-tab.tsx +++ b/web/features/deployments/detail/settings-tab.tsx @@ -14,18 +14,24 @@ import { import { Button } from '@langgenius/dify-ui/button' import { toast } from '@langgenius/dify-ui/toast' import { useQuery } from '@tanstack/react-query' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/client' -import { toAppInfoFromOverview } from '../data' -import { useCachedDeploymentAppData } from '../hooks/use-deployment-data' +import { + DEPLOYMENT_PAGE_SIZE, + SOURCE_APPS_PAGE_SIZE, +} from '../data' import { useDeleteDeploymentInstance, useUpdateDeploymentInstance, } from '../hooks/use-deployment-mutations' -import { useSourceApps } from '../hooks/use-source-apps' -import { deployedRows } from '../utils' +import { + deployedRows, + sourceAppMapFromApps, + sourceAppsFromList, + toAppInfoFromOverview, +} from '../utils' type SettingsTabProps = { instanceId: string @@ -181,24 +187,40 @@ const SettingsForm: FC = ({ app, settings, hasDeployments, on const SettingsTab: FC = ({ instanceId }) => { const router = useRouter() - const { data: appData } = useCachedDeploymentAppData(instanceId) const updateInstance = useUpdateDeploymentInstance() const deleteInstance = useDeleteDeploymentInstance() - const { appMap } = useSourceApps() - const app = toAppInfoFromOverview(appData?.overview.instance) ?? appMap.get(instanceId) - const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({ + const appInput = { params: { appInstanceId: instanceId } } + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ input: { - params: { - appInstanceId: instanceId, + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, }, }, - staleTime: 30 * 1000, + })) + const { data: overview } = useQuery(consoleQuery.deployments.overview.queryOptions({ + input: appInput, + })) + const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({ + input: { + ...appInput, + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + }, + })) + const sourceApps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data]) + const appMap = useMemo(() => sourceAppMapFromApps(sourceApps), [sourceApps]) + const app = toAppInfoFromOverview(overview?.instance) ?? appMap.get(instanceId) + const settingsQuery = useQuery(consoleQuery.deployments.settings.queryOptions({ + input: appInput, })) if (!app) return null - const hasDeployments = deployedRows(appData?.environmentDeployments.data).length > 0 + const hasDeployments = deployedRows(environmentDeployments?.data).length > 0 const formKey = `${app.id}-${settingsQuery.data?.name ?? app.name}-${settingsQuery.data?.description ?? app.description ?? ''}` return ( diff --git a/web/features/deployments/detail/versions-tab.tsx b/web/features/deployments/detail/versions-tab.tsx index 6b9d832a9b..db9b64e393 100644 --- a/web/features/deployments/detail/versions-tab.tsx +++ b/web/features/deployments/detail/versions-tab.tsx @@ -2,9 +2,11 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useCachedDeploymentAppData } from '../hooks/use-deployment-data' +import { consoleQuery } from '@/service/client' +import { DEPLOYMENT_PAGE_SIZE } from '../data' import { deployedRows, formatDate, @@ -23,14 +25,29 @@ type VersionsTabProps = { const VersionsTab: FC = ({ instanceId: appId }) => { const { t } = useTranslation('deployments') - const { data: appData } = useCachedDeploymentAppData(appId) + const query = { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + } + const { data: releaseHistory } = useQuery(consoleQuery.deployments.releaseHistory.queryOptions({ + input: { + params: { appInstanceId: appId }, + query, + }, + })) + const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({ + input: { + params: { appInstanceId: appId }, + query, + }, + })) const releaseRows = useMemo( - () => appData?.releaseHistory.data?.filter(row => (row.release ?? row).id) ?? [], - [appData?.releaseHistory.data], + () => releaseHistory?.data?.filter(row => (row.release ?? row).id) ?? [], + [releaseHistory?.data], ) const deploymentRows = useMemo( - () => deployedRows(appData?.environmentDeployments.data), - [appData?.environmentDeployments.data], + () => deployedRows(environmentDeployments?.data), + [environmentDeployments?.data], ) return ( 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 2d2a94e469..e06f6ea3db 100644 --- a/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx +++ b/web/features/deployments/detail/versions-tab/deploy-release-menu.tsx @@ -8,10 +8,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useCachedDeploymentAppData } from '../../hooks/use-deployment-data' -import { useSourceApps } from '../../hooks/use-source-apps' +import { consoleQuery } from '@/service/client' +import { + DEPLOYMENT_PAGE_SIZE, + SOURCE_APPS_PAGE_SIZE, +} from '../../data' import { useDeploymentsStore } from '../../store' import { activeRelease, @@ -20,6 +24,7 @@ import { deploymentStatus, environmentId, environmentName, + environmentOptionsFromList, } from '../../utils' type DeployReleaseMenuProps = { @@ -29,14 +34,32 @@ type DeployReleaseMenuProps = { export const DeployReleaseMenu: FC = ({ appId, releaseId }) => { const { t } = useTranslation('deployments') - const { data: appData } = useCachedDeploymentAppData(appId) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const openRollbackModal = useDeploymentsStore(state => state.openRollbackModal) const [open, setOpen] = useState(false) - const { environmentOptions } = useSourceApps({ enabled: open }) + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + enabled: open, + })) + const { data: environmentDeployments } = useQuery(consoleQuery.deployments.environmentDeployments.queryOptions({ + input: { + params: { appInstanceId: appId }, + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + }, + enabled: open, + })) + const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data]) const environments = environmentOptions.filter(env => env.id) - const deploymentRows = deployedRows(appData?.environmentDeployments.data) + const deploymentRows = deployedRows(environmentDeployments?.data) return ( diff --git a/web/features/deployments/hooks/use-deployment-data.ts b/web/features/deployments/hooks/use-deployment-data.ts deleted file mode 100644 index 6edc4ff9d7..0000000000 --- a/web/features/deployments/hooks/use-deployment-data.ts +++ /dev/null @@ -1,41 +0,0 @@ -'use client' - -import { useQuery } from '@tanstack/react-query' -import { useMemo } from 'react' -import { - deploymentAppDataQueryOptions, - toAppInfoFromOverview, -} from '../data' - -type UseDeploymentDataOptions = { - enabled?: boolean -} - -export function useDeploymentAppData(appId?: string, options: UseDeploymentDataOptions = {}) { - const { enabled = true } = options - - return useQuery({ - ...deploymentAppDataQueryOptions(appId ?? ''), - enabled: enabled && Boolean(appId), - }) -} - -export function useCachedDeploymentAppData(appId?: string) { - return useQuery({ - ...deploymentAppDataQueryOptions(appId ?? ''), - enabled: false, - }) -} - -export function useDeploymentAppInfo(appId?: string, options: UseDeploymentDataOptions = {}) { - const query = useDeploymentAppData(appId, options) - const app = useMemo( - () => toAppInfoFromOverview(query.data?.overview.instance), - [query.data?.overview.instance], - ) - - return { - ...query, - data: app, - } -} diff --git a/web/features/deployments/hooks/use-deployment-mutations.ts b/web/features/deployments/hooks/use-deployment-mutations.ts index 90d5625939..3559259cfe 100644 --- a/web/features/deployments/hooks/use-deployment-mutations.ts +++ b/web/features/deployments/hooks/use-deployment-mutations.ts @@ -1,35 +1,34 @@ 'use client' import type { - CreateDeploymentParams, - CreateInstanceParams, - DeploymentAppData, - UpdateInstanceParams, -} from '../data' -import type { AccessSubject, ConsoleReleaseSummary } from '@/contract/console/deployments' + AccessSubject, + ConsoleReleaseSummary, +} from '@/contract/console/deployments' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { consoleQuery } from '@/service/client' -import { - cancelDeployment, - createApiKey, - createAppInstance, - createDeployment, - deleteApiKey, - deleteAppInstance, - deploymentAppDataQueryKey, - patchAccessChannel, - patchDeveloperAPI, - refreshDeploymentAppDataWhenReady, - undeployEnvironment, - updateAppInstance, - updateEnvironmentAccessPolicy, - waitForAppInstanceInDeploymentList, -} from '../data' +import { consoleClient, consoleQuery } from '@/service/client' +import { DEPLOYMENT_PAGE_SIZE } from '../data' export type CreateDeploymentInstanceResult = { appInstanceId: string initialRelease?: ConsoleReleaseSummary - appData?: DeploymentAppData +} + +type CreateDeploymentParams = { + appId: string + environmentId: string + releaseId?: string + releaseNote?: string +} + +type CreateInstanceParams = { + sourceAppId: string + name: string + description?: string +} + +type UpdateInstanceParams = { + name: string + description?: string } type UpdateDeploymentInstanceParams = { @@ -45,9 +44,12 @@ type UndeployDeploymentParams = { type GenerateApiKeyParams = { appId: string environmentId: string + name: string } -type RevokeApiKeyParams = GenerateApiKeyParams & { +type RevokeApiKeyParams = { + appId: string + environmentId: string apiKeyId: string } @@ -64,22 +66,9 @@ type SetEnvironmentAccessPolicyParams = { subjects: AccessSubject[] } -const createApiKeyLabel = ( - appData: DeploymentAppData | undefined, - environmentId: string, -) => { - const existingCount = appData?.accessConfig.developerApi?.apiKeys?.filter(key => - (key.environmentId ?? key.environment?.id) === environmentId, - ).length ?? 0 - const environmentName = appData - ?.environmentDeployments - .data - ?.find(row => row.environment?.id === environmentId) - ?.environment - ?.name ?? 'env' +const DEPLOYMENT_READINESS_RETRY_DELAYS = [0, 300, 700, 1200] - return `${environmentName}-key-${String(existingCount + 1).padStart(3, '0')}` -} +const wait = (delay: number) => new Promise(resolve => setTimeout(resolve, delay)) export const useCreateDeploymentInstance = () => { const queryClient = useQueryClient() @@ -87,37 +76,39 @@ export const useCreateDeploymentInstance = () => { return useMutation({ mutationKey: consoleQuery.deployments.createInstance.mutationKey(), mutationFn: async (params: CreateInstanceParams): Promise => { - const response = await createAppInstance(params) + const response = await consoleClient.deployments.createInstance({ + body: { + sourceAppId: params.sourceAppId, + name: params.name, + description: params.description, + }, + }) if (!response.appInstanceId) throw new Error('Create app instance did not return an appInstanceId.') - const [appData] = await Promise.all([ - refreshDeploymentAppDataWhenReady(response.appInstanceId).catch(() => undefined), - waitForAppInstanceInDeploymentList(response.appInstanceId).catch(() => undefined), - ]) + for (const delay of DEPLOYMENT_READINESS_RETRY_DELAYS) { + if (delay > 0) + await wait(delay) + + const listResponse = await consoleClient.deployments.list({ + query: { + pageNumber: 1, + resultsPerPage: DEPLOYMENT_PAGE_SIZE, + }, + }).catch(() => undefined) + if (listResponse?.data?.some(app => app.id === response.appInstanceId)) + break + } return { appInstanceId: response.appInstanceId, initialRelease: response.initialRelease, - appData, } }, - onSuccess: async (result) => { - if (result.appData) { - queryClient.setQueryData( - deploymentAppDataQueryKey(result.appInstanceId), - result.appData, - ) - } - - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(result.appInstanceId), - }), - ]) + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -128,25 +119,19 @@ export const useUpdateDeploymentInstance = () => { return useMutation({ mutationKey: consoleQuery.deployments.updateInstance.mutationKey(), mutationFn: ({ appId, ...patch }: UpdateDeploymentInstanceParams) => - updateAppInstance(appId, patch), - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.settings.queryKey({ - input: { - params: { - appInstanceId: variables.appId, - }, - }, - }), - }), - ]) + consoleClient.deployments.updateInstance({ + params: { + appInstanceId: appId, + }, + body: { + name: patch.name, + description: patch.description, + }, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -156,22 +141,14 @@ export const useDeleteDeploymentInstance = () => { return useMutation({ mutationKey: consoleQuery.deployments.deleteInstance.mutationKey(), - mutationFn: (appId: string) => deleteAppInstance(appId), - onSuccess: async (_data, appId) => { - queryClient.removeQueries({ - queryKey: deploymentAppDataQueryKey(appId), - }) - queryClient.removeQueries({ - queryKey: consoleQuery.deployments.settings.queryKey({ - input: { - params: { - appInstanceId: appId, - }, - }, - }), - }) + mutationFn: (appId: string) => consoleClient.deployments.deleteInstance({ + params: { + appInstanceId: appId, + }, + }), + onSuccess: async () => { await queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), + queryKey: consoleQuery.deployments.key(), }) }, }) @@ -182,16 +159,55 @@ export const useStartDeployment = () => { return useMutation({ mutationKey: consoleQuery.deployments.createDeployment.mutationKey(), - mutationFn: (params: CreateDeploymentParams) => createDeployment(params), - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - ]) + mutationFn: async ({ + appId, + environmentId, + releaseId, + releaseNote, + }: CreateDeploymentParams) => { + let targetReleaseId = releaseId + await consoleClient.deployments.previewRelease({ + params: { + appInstanceId: appId, + }, + body: { + releaseId: targetReleaseId, + }, + }) + + if (!targetReleaseId) { + const trimmedReleaseNote = releaseNote?.trim() + const response = await consoleClient.deployments.createRelease({ + params: { + appInstanceId: appId, + }, + body: { + name: trimmedReleaseNote || 'Release', + description: trimmedReleaseNote || undefined, + }, + }) + if (!response.release) + throw new Error('Create release did not return a release.') + targetReleaseId = response.release.id + } + + if (!targetReleaseId) + throw new Error('Failed to create a deployable release.') + + return consoleClient.deployments.createDeployment({ + params: { + appInstanceId: appId, + }, + body: { + environmentId, + releaseId: targetReleaseId, + }, + }) + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -204,19 +220,25 @@ export const useUndeployDeployment = () => { mutationFn: ({ appId, runtimeInstanceId, isDeploying }: UndeployDeploymentParams) => { if (!runtimeInstanceId) return Promise.resolve(undefined) - if (isDeploying) - return cancelDeployment(appId, runtimeInstanceId) - return undeployEnvironment(appId, runtimeInstanceId) + if (isDeploying) { + return consoleClient.deployments.cancelDeployment({ + params: { + appInstanceId: appId, + runtimeInstanceId, + }, + }) + } + return consoleClient.deployments.undeployEnvironment({ + params: { + appInstanceId: appId, + runtimeInstanceId, + }, + }) }, - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - ]) + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -226,22 +248,20 @@ export const useGenerateDeploymentApiKey = () => { return useMutation({ mutationKey: consoleQuery.deployments.createEnvironmentAPIToken.mutationKey(), - mutationFn: ({ appId, environmentId }: GenerateApiKeyParams) => { - const appData = queryClient.getQueryData( - deploymentAppDataQueryKey(appId), - ) - - return createApiKey(appId, environmentId, createApiKeyLabel(appData, environmentId)) - }, - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - ]) + mutationFn: ({ appId, environmentId, name }: GenerateApiKeyParams) => + consoleClient.deployments.createEnvironmentAPIToken({ + params: { + appInstanceId: appId, + }, + body: { + environmentId, + name, + }, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -251,16 +271,16 @@ export const useRevokeDeploymentApiKey = () => { return useMutation({ mutationKey: consoleQuery.deployments.deleteEnvironmentAPIToken.mutationKey(), - mutationFn: ({ appId, apiKeyId }: RevokeApiKeyParams) => deleteApiKey(appId, apiKeyId), - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - ]) + mutationFn: ({ appId, apiKeyId }: RevokeApiKeyParams) => consoleClient.deployments.deleteEnvironmentAPIToken({ + params: { + appInstanceId: appId, + apiKeyId, + }, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -271,19 +291,29 @@ export const useToggleDeploymentAccessChannel = () => { return useMutation({ mutationKey: consoleQuery.deployments.patchAccessChannel.mutationKey(), mutationFn: ({ appId, channel, enabled }: ToggleAccessChannelParams) => { - if (channel === 'api') - return patchDeveloperAPI(appId, enabled) - return patchAccessChannel(appId, enabled) + if (channel === 'api') { + return consoleClient.deployments.patchDeveloperAPI({ + params: { + appInstanceId: appId, + }, + body: { + enabled, + }, + }) + } + return consoleClient.deployments.patchAccessChannel({ + params: { + appInstanceId: appId, + }, + body: { + enabled, + }, + }) }, - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - ]) + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } @@ -298,27 +328,20 @@ export const useSetEnvironmentAccessPolicy = () => { environmentId, accessMode, subjects, - }: SetEnvironmentAccessPolicyParams) => - updateEnvironmentAccessPolicy(appId, environmentId, accessMode, subjects), - onSuccess: async (_data, variables) => { - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.list.key(), - }), - queryClient.invalidateQueries({ - queryKey: deploymentAppDataQueryKey(variables.appId), - }), - queryClient.invalidateQueries({ - queryKey: consoleQuery.deployments.environmentAccessPolicy.queryKey({ - input: { - params: { - appInstanceId: variables.appId, - environmentId: variables.environmentId, - }, - }, - }), - }), - ]) + }: SetEnvironmentAccessPolicyParams) => consoleClient.deployments.updateEnvironmentAccessPolicy({ + params: { + appInstanceId: appId, + environmentId, + }, + body: { + accessMode, + subjects, + }, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.deployments.key(), + }) }, }) } diff --git a/web/features/deployments/hooks/use-source-apps.ts b/web/features/deployments/hooks/use-source-apps.ts deleted file mode 100644 index 34e553a633..0000000000 --- a/web/features/deployments/hooks/use-source-apps.ts +++ /dev/null @@ -1,81 +0,0 @@ -'use client' -import type { AppInfo } from '../types' -import type { AppDeploymentSummary, EnvironmentOption } from '@/contract/console/deployments' -import { useQuery } from '@tanstack/react-query' -import { useMemo } from 'react' -import { consoleQuery } from '@/service/client' -import { toAppInfoFromSummary } from '../data' - -const MAX_SOURCE_APPS = 100 - -type UseSourceAppsOptions = { - enabled?: boolean - environmentId?: string - keyword?: string - notDeployed?: boolean -} - -type DeploymentEnvironmentFilter = { - id?: string - name?: string - kind?: string - disabled?: boolean - disabledReason?: string -} - -export function useSourceApps(options: UseSourceAppsOptions = {}) { - const { enabled = true, environmentId, keyword, notDeployed } = options - const query = useMemo(() => ({ - pageNumber: 1, - resultsPerPage: MAX_SOURCE_APPS, - ...(environmentId ? { environmentId } : {}), - ...(notDeployed ? { notDeployed: true } : {}), - ...(keyword?.trim() ? { query: keyword.trim() } : {}), - }), [environmentId, keyword, notDeployed]) - - const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ - input: { query }, - enabled, - staleTime: 30 * 1000, - })) - - const apps = useMemo(() => { - return (listQuery.data?.data ?? []) - .map(toAppInfoFromSummary) - .filter((app): app is AppInfo => Boolean(app)) - }, [listQuery.data?.data]) - - const appMap = useMemo>(() => { - return new Map(apps.map(a => [a.id, a])) - }, [apps]) - - const summaries = useMemo>(() => { - return Object.fromEntries( - (listQuery.data?.data ?? []) - .filter(summary => summary.id) - .map(summary => [summary.id!, summary]), - ) - }, [listQuery.data?.data]) - - const environmentOptions = useMemo(() => { - return ((listQuery.data?.filters ?? []) as DeploymentEnvironmentFilter[]) - .filter(filter => filter.kind === 'environment' && filter.id) - .map(filter => ({ - id: filter.id, - name: filter.name, - disabled: filter.disabled, - disabledReason: filter.disabledReason, - })) - }, [listQuery.data?.filters]) - - return { - apps, - appMap, - summaries, - environmentOptions, - isLoading: listQuery.isLoading, - isFetching: listQuery.isFetching, - isError: listQuery.isError, - isEmpty: !listQuery.isLoading && apps.length === 0, - } -} diff --git a/web/features/deployments/list/index.tsx b/web/features/deployments/list/index.tsx index f5dd6b08c1..a22faf7920 100644 --- a/web/features/deployments/list/index.tsx +++ b/web/features/deployments/list/index.tsx @@ -1,17 +1,27 @@ 'use client' import type { FC } from 'react' +import { useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' import { debounce, parseAsString, useQueryState } from 'nuqs' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' +import { consoleQuery } from '@/service/client' import CreateInstanceModal from '../components/create-instance-modal' import DeployDrawer from '../components/deploy-drawer' import RollbackModal from '../components/rollback-modal' -import { useSourceApps } from '../hooks/use-source-apps' +import { + SOURCE_APPS_PAGE_SIZE, +} from '../data' import { useDeploymentsStore } from '../store' -import { environmentId, environmentName } from '../utils' +import { + deploymentSummariesFromList, + environmentId, + environmentName, + environmentOptionsFromList, + sourceAppsFromList, +} from '../utils' import { EnvironmentFilter } from './environment-filter' import { InstanceCard } from './instance-card' import { NewInstanceCard } from './new-instance-card' @@ -40,15 +50,20 @@ const DeploymentsMain: FC = () => { const requestedEnvironmentId = envFilter !== 'all' && envFilter !== 'not-deployed' ? envFilter : undefined - const { - apps, - summaries, - environmentOptions, - } = useSourceApps({ - environmentId: requestedEnvironmentId, - notDeployed: envFilter === 'not-deployed', - keyword: queryKeywords.trim() || undefined, - }) + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + ...(requestedEnvironmentId ? { environmentId: requestedEnvironmentId } : {}), + ...(envFilter === 'not-deployed' ? { notDeployed: true } : {}), + ...(queryKeywords.trim() ? { query: queryKeywords.trim() } : {}), + }, + }, + })) + const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data]) + const summaries = useMemo(() => deploymentSummariesFromList(listQuery.data), [listQuery.data]) + const environmentOptions = useMemo(() => environmentOptionsFromList(listQuery.data), [listQuery.data]) const environments = useMemo(() => { return environmentOptions diff --git a/web/features/deployments/list/instance-card.tsx b/web/features/deployments/list/instance-card.tsx index 04086df99c..04e05c4b80 100644 --- a/web/features/deployments/list/instance-card.tsx +++ b/web/features/deployments/list/instance-card.tsx @@ -13,15 +13,13 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' 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 { useCachedDeploymentAppData } from '../hooks/use-deployment-data' import { useDeploymentsStore } from '../store' -import { deployedRows, deploymentStatus, environmentId, environmentName, releaseLabel } from '../utils' type InstanceCardProps = { app: AppInfo @@ -33,7 +31,6 @@ export const InstanceCard: FC = ({ app, summary }) => { const router = useRouter() const { formatTimeFromNow } = useFormatTimeFromNow() const [menuOpen, setMenuOpen] = useState(false) - const { data: appData } = useCachedDeploymentAppData(app.id) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const navigateToDetail = () => router.push(`/deployments/${app.id}/overview`) @@ -45,36 +42,16 @@ export const InstanceCard: FC = ({ app, summary }) => { action() } - const deployments = useMemo( - () => deployedRows(appData?.environmentDeployments.data), - [appData?.environmentDeployments.data], - ) const statusCount = (status: string) => summary?.statuses?.find(item => item.status === status)?.count ?? 0 - const hasSummary = Boolean(summary) - const failedCount = hasSummary - ? statusCount('failed') + statusCount('deploy_failed') - : deployments.filter(row => deploymentStatus(row) === 'deploy_failed').length - const deployingCount = hasSummary - ? statusCount('deploying') - : deployments.filter(row => deploymentStatus(row) === 'deploying').length - const readyCount = hasSummary - ? statusCount('ready') - : deployments.filter(row => deploymentStatus(row) === 'ready').length - const envCount = hasSummary - ? failedCount + deployingCount + readyCount - : deployments.length + const failedCount = statusCount('failed') + statusCount('deploy_failed') + const deployingCount = statusCount('deploying') + const readyCount = statusCount('ready') + const envCount = failedCount + deployingCount + readyCount - const lastDeployedAt = useMemo(() => { - if (summary?.lastDeployedAt) - return new Date(summary.lastDeployedAt).getTime() - if (deployments.length === 0) - return null - return deployments.reduce((latest, row) => { - const t = new Date(row.currentRelease?.createdAt || '').getTime() - return t > latest ? t : latest - }, 0) - }, [deployments, summary?.lastDeployedAt]) + const lastDeployedAt = summary?.lastDeployedAt + ? new Date(summary.lastDeployedAt).getTime() + : null const primaryStatus: 'none' | 'failed' | 'deploying' | 'ready' = envCount === 0 ? 'none' @@ -98,11 +75,6 @@ export const InstanceCard: FC = ({ app, summary }) => { if ((primaryStatus === 'failed' || primaryStatus === 'deploying') && readyCount > 0) secondaryParts.push(t('card.ready', { count: readyCount })) - const statusLabel = (status: ReturnType) => { - if (status === 'deploy_failed') - return t('status.deployFailed') - return t(`status.${status}`) - } const statusSummaryLabel = (status?: string) => { if (status === 'failed' || status === 'deploy_failed') return t('status.deployFailed') @@ -116,38 +88,17 @@ export const InstanceCard: FC = ({ app, summary }) => { const statusSummaryTooltip = summary?.statuses?.filter(item => item.count && item.status !== 'undeployed') ?? [] const statusTooltip = primaryStatus === 'none' ? t('card.tooltip.notDeployed') - : deployments.length > 0 - ? ( -
-
{t('overview.deploymentStatus')}
- {deployments.map((deployment) => { - const status = deploymentStatus(deployment) - return ( -
- - {environmentName(deployment.environment)} - - - {statusLabel(status)} - {' · '} - {releaseLabel(deployment.currentRelease)} - -
- ) - })} -
- ) - : ( -
-
{t('overview.deploymentStatus')}
- {statusSummaryTooltip.map(item => ( -
- {statusSummaryLabel(item.status)} - {item.count} -
- ))} -
- ) + : ( +
+
{t('overview.deploymentStatus')}
+ {statusSummaryTooltip.map(item => ( +
+ {statusSummaryLabel(item.status)} + {item.count} +
+ ))} +
+ ) const healthPillClass = primaryStatus === 'none' ? 'text-text-tertiary bg-background-section-burn' diff --git a/web/features/deployments/nav/index.tsx b/web/features/deployments/nav/index.tsx index 9b8c5d862e..26e641d58e 100644 --- a/web/features/deployments/nav/index.tsx +++ b/web/features/deployments/nav/index.tsx @@ -2,13 +2,18 @@ import type { NavItem } from '@/app/components/header/nav/nav-selector' import type { AppIconType, AppModeEnum } from '@/types/app' +import { skipToken, useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import Nav from '@/app/components/header/nav' import { useParams, useRouter, useSelectedLayoutSegment } from '@/next/navigation' -import { useDeploymentAppInfo } from '../hooks/use-deployment-data' -import { useSourceApps } from '../hooks/use-source-apps' +import { consoleQuery } from '@/service/client' +import { SOURCE_APPS_PAGE_SIZE } from '../data' import { useDeploymentsStore } from '../store' +import { + sourceAppsFromList, + toAppInfoFromOverview, +} from '../utils' const DeploymentsNav = () => { const { t } = useTranslation() @@ -19,11 +24,24 @@ const DeploymentsNav = () => { const instanceId = params?.instanceId const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal) - const { data: currentInstance } = useDeploymentAppInfo(instanceId, { + const { data: currentInstance } = useQuery(consoleQuery.deployments.overview.queryOptions({ + input: instanceId + ? { params: { appInstanceId: instanceId } } + : skipToken, enabled: isActive && Boolean(instanceId), - }) + select: data => toAppInfoFromOverview(data.instance), + })) - const { apps } = useSourceApps({ enabled: isActive }) + const listQuery = useQuery(consoleQuery.deployments.list.queryOptions({ + input: { + query: { + pageNumber: 1, + resultsPerPage: SOURCE_APPS_PAGE_SIZE, + }, + }, + enabled: isActive, + })) + const apps = useMemo(() => sourceAppsFromList(listQuery.data), [listQuery.data]) const navigationItems = useMemo(() => { if (!isActive) diff --git a/web/features/deployments/utils.ts b/web/features/deployments/utils.ts index 9ae3f65347..edad6ff629 100644 --- a/web/features/deployments/utils.ts +++ b/web/features/deployments/utils.ts @@ -1,9 +1,12 @@ -import type { AccessPermissionKind } from './types' +import type { AccessPermissionKind, AppInfo, AppMode } from './types' import type { + AppDeploymentSummary, + AppInstanceOverview, ConsoleEnvironmentSummary, ConsoleReleaseSummary, EnvironmentDeploymentRow, EnvironmentOption, + ListAppDeploymentsReply, RuntimeBindingDisplay, } from '@/contract/console/deployments' import { PUBLIC_API_PREFIX } from '@/config' @@ -104,6 +107,75 @@ export const deployedRows = (rows?: EnvironmentDeploymentRow[]) => && (row.id || runtimeStatus || row.currentRelease || row.detail) }) ?? [] +type DeploymentEnvironmentFilter = { + id?: string + name?: string + kind?: string + disabled?: boolean + disabledReason?: string +} + +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 sourceAppsFromList = (response?: ListAppDeploymentsReply) => { + return (response?.data ?? []) + .map(toAppInfoFromSummary) + .filter((app): app is AppInfo => Boolean(app)) +} + +export const sourceAppMapFromApps = (apps: AppInfo[]) => { + return new Map(apps.map(app => [app.id, app])) +} + +export const deploymentSummariesFromList = (response?: ListAppDeploymentsReply): Record => { + return Object.fromEntries( + (response?.data ?? []) + .filter(summary => summary.id) + .map(summary => [summary.id!, summary]), + ) +} + +export const environmentOptionsFromList = (response?: ListAppDeploymentsReply): EnvironmentOption[] => { + return ((response?.filters ?? []) as DeploymentEnvironmentFilter[]) + .filter(filter => filter.kind === 'environment' && filter.id) + .map(filter => ({ + id: filter.id, + name: filter.name, + disabled: filter.disabled, + disabledReason: filter.disabledReason, + })) +} + export const accessModeToPermissionKey = (mode?: string): AccessPermissionKind => { const normalized = mode?.toLowerCase() ?? '' if (normalized === 'private')