diff --git a/web/app/components/deployments/index.tsx b/web/app/components/deployments/index.tsx index d78c46f316..cb6ddef11b 100644 --- a/web/app/components/deployments/index.tsx +++ b/web/app/components/deployments/index.tsx @@ -1,6 +1,7 @@ 'use client' import type { FC } from 'react' import type { AppInfo } from './types' +import type { AppDeploymentSummary } from '@/contract/console/deployments' import type { DeploymentAppData } from '@/service/deployments' import type { AppModeEnum } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' @@ -98,15 +99,15 @@ const NewInstanceCard: FC = ({ onOpen }) => { type InstanceCardProps = { app: AppInfo appData?: DeploymentAppData + summary?: AppDeploymentSummary } -const InstanceCard: FC = ({ app, appData }) => { +const InstanceCard: FC = ({ app, appData, summary }) => { const { t } = useTranslation('deployments') const router = useRouter() const { formatTimeFromNow } = useFormatTimeFromNow() const [menuOpen, setMenuOpen] = useState(false) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) - const deleteInstance = useDeploymentsStore(state => state.deleteInstance) const navigateToDetail = () => router.push(`/deployments/${app.id}/overview`) @@ -121,19 +122,32 @@ const InstanceCard: FC = ({ app, appData }) => { () => deployedRows(appData?.environmentDeployments.environmentDeployments), [appData?.environmentDeployments.environmentDeployments], ) - const envCount = deployments.length - const failedCount = deployments.filter(row => deploymentStatus(row) === 'deploy_failed').length - const deployingCount = deployments.filter(row => deploymentStatus(row) === 'deploying').length - const readyCount = deployments.filter(row => deploymentStatus(row) === 'ready').length + const statusCount = (status: string) => + summary?.statusCounts?.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 + ? (summary?.deployed ? failedCount + deployingCount + readyCount : 0) + : deployments.length 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.instance?.lastDeployedAt || row.instance?.lastReadyAt || '').getTime() return t > latest ? t : latest }, 0) - }, [deployments]) + }, [deployments, summary?.lastDeployedAt]) const primaryStatus: 'none' | 'failed' | 'deploying' | 'ready' = envCount === 0 ? 'none' @@ -162,29 +176,51 @@ const InstanceCard: FC = ({ app, appData }) => { return t('status.deployFailed') return t(`status.${status}`) } + const statusSummaryLabel = (status?: string) => { + if (status === 'failed' || status === 'deploy_failed') + return t('status.deployFailed') + if (status === 'deploying') + return t('status.deploying') + if (status === 'ready') + return t('status.ready') + return status || 'unknown' + } + const statusSummaryTooltip = summary?.statusCounts?.filter(item => item.count && item.status !== 'undeployed') ?? [] const statusTooltip = primaryStatus === 'none' ? t('card.tooltip.notDeployed') - : ( -
-
{t('overview.deploymentStatus')}
- {deployments.map((deployment) => { - const status = deploymentStatus(deployment) - return ( -
- - {environmentName(deployment.environment)} - - - {statusLabel(status)} - {' · '} - {releaseLabel(deployment.observedRuntime?.release || deployment.pendingDeployment?.release)} - + : deployments.length > 0 + ? ( +
+
{t('overview.deploymentStatus')}
+ {deployments.map((deployment) => { + const status = deploymentStatus(deployment) + return ( +
+ + {environmentName(deployment.environment)} + + + {statusLabel(status)} + {' · '} + {releaseLabel(deployment.observedRuntime?.release || deployment.pendingDeployment?.release)} + +
+ ) + })} +
+ ) + : ( +
+
{t('overview.deploymentStatus')}
+ {statusSummaryTooltip.map(item => ( +
+ {statusSummaryLabel(item.status)} + {item.count}
- ) - })} -
- ) + ))} +
+ ) const healthPillClass = primaryStatus === 'none' ? 'text-text-tertiary bg-background-section-burn' @@ -314,8 +350,13 @@ const InstanceCard: FC = ({ app, appData }) => { handleMenuAction(e, () => deleteInstance(app.id))} + aria-disabled + title={t('card.menu.deleteDisabled')} + className="cursor-not-allowed gap-2 px-3 opacity-50" + onClick={(e) => { + e.stopPropagation() + e.preventDefault() + }} > {t('card.menu.delete')} @@ -332,6 +373,8 @@ type EnvironmentFilterOption = { value: string text: string icon: React.ReactNode + disabled?: boolean + disabledReason?: string } type EnvironmentFilterProps = { @@ -373,10 +416,19 @@ const EnvironmentFilter: FC = ({ value, options, onChang { + if (option.disabled) + return onChange(option.value) setOpen(false) }} - className="flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pr-2 pl-3 select-none hover:bg-state-base-hover" + title={option.disabled ? option.disabledReason : undefined} + aria-disabled={option.disabled} + className={cn( + 'flex items-center gap-2 rounded-lg py-[6px] pr-2 pl-3 select-none', + option.disabled + ? 'cursor-not-allowed opacity-50' + : 'cursor-pointer hover:bg-state-base-hover', + )} > {option.icon} {option.text} @@ -394,7 +446,6 @@ const EnvironmentFilter: FC = ({ value, options, onChang const DeploymentsMain: FC = () => { const { t } = useTranslation('deployments') - const sourceApps = useDeploymentsStore(state => state.sourceApps) const appData = useDeploymentsStore(state => state.appData) const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal) @@ -417,29 +468,28 @@ const DeploymentsMain: FC = () => { commitKeywords(next) } - const { appMap } = useSourceApps() - const apps = useMemo( - () => sourceApps.length > 0 ? sourceApps : [...appMap.values()], - [appMap, sourceApps], - ) - const appDataList = useMemo(() => Object.values(appData), [appData]) + const requestedEnvironmentId = envFilter !== 'all' && envFilter !== 'not-deployed' + ? envFilter + : undefined + const { + apps, + summaries, + environmentOptions, + } = useSourceApps({ + environmentId: requestedEnvironmentId, + keyword: keywords.trim() || undefined, + }) const environments = useMemo(() => { - const map = new Map() - appDataList.forEach((data) => { - data.candidates.environmentOptions?.forEach((env) => { - const id = environmentId(env) - if (id) - map.set(id, environmentName(env)) - }) - data.environmentDeployments.environmentDeployments?.forEach((row) => { - const id = environmentId(row.environment) - if (id) - map.set(id, environmentName(row.environment)) - }) - }) - return [...map.entries()].map(([id, name]) => ({ id, name })) - }, [appDataList]) + return environmentOptions + .filter(env => environmentId(env)) + .map(env => ({ + id: environmentId(env), + name: environmentName(env), + disabled: env.disabled, + disabledReason: env.disabledReason, + })) + }, [environmentOptions]) const envIdSet = useMemo(() => new Set(environments.map(e => e.id)), [environments]) const activeFilter = envFilter === 'all' || envFilter === 'not-deployed' || envIdSet.has(envFilter) @@ -457,6 +507,8 @@ const DeploymentsMain: FC = () => { value: env.id, text: env.name, icon: , + disabled: env.disabled, + disabledReason: env.disabledReason, })), { value: 'not-deployed', @@ -467,22 +519,10 @@ const DeploymentsMain: FC = () => { }, [environments, t]) const visibleInstances = useMemo(() => { - const byEnv = activeFilter === 'all' - ? apps - : activeFilter === 'not-deployed' - ? apps.filter(app => deployedRows(appData[app.id]?.environmentDeployments.environmentDeployments).length === 0) - : apps.filter(app => deployedRows(appData[app.id]?.environmentDeployments.environmentDeployments).some(row => environmentId(row.environment) === activeFilter)) - - const q = keywords.trim().toLowerCase() - if (!q) - return byEnv - return byEnv.filter((app) => { - return ( - app.name.toLowerCase().includes(q) - || (app.description ?? '').toLowerCase().includes(q) - ) - }) - }, [apps, activeFilter, keywords, appData]) + return activeFilter === 'not-deployed' + ? apps.filter(app => summaries[app.id]?.deployed === false) + : apps + }, [apps, activeFilter, summaries]) return ( <> @@ -513,6 +553,7 @@ const DeploymentsMain: FC = () => { key={app.id} app={app} appData={appData[app.id]} + summary={summaries[app.id]} /> ) })} diff --git a/web/app/components/deployments/instance-detail/settings-tab.tsx b/web/app/components/deployments/instance-detail/settings-tab.tsx index b2c43e321d..30f20c5b02 100644 --- a/web/app/components/deployments/instance-detail/settings-tab.tsx +++ b/web/app/components/deployments/instance-detail/settings-tab.tsx @@ -2,11 +2,8 @@ import type { FC } from 'react' import type { AppInfo } from '../types' import { Button } from '@langgenius/dify-ui/button' -import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' -import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useRouter } from '@/next/navigation' import { deployedRows } from '../api-utils' import { useDeploymentsStore } from '../store' import { useSourceApps } from '../use-source-apps' @@ -22,43 +19,12 @@ type SettingsFormProps = { const SettingsForm: FC = ({ app, hasDeployments }) => { const { t } = useTranslation('deployments') - const router = useRouter() - const updateInstance = useDeploymentsStore(state => state.updateInstance) - const deleteInstance = useDeploymentsStore(state => state.deleteInstance) - - const [name, setName] = useState(app.name) - const [description, setDescription] = useState(app.description ?? '') - - const dirty = name !== app.name || description !== (app.description ?? '') - - const handleSave = () => { - if (!name.trim()) - return - updateInstance(app.id, { - name: name.trim(), - description: description.trim() || undefined, - }) - toast.success(t('settings.updated')) - } - - const handleReset = () => { - setName(app.name) - setDescription(app.description ?? '') - } - - const handleDelete = () => { - if (hasDeployments) { - toast.error(t('settings.undeployFirst')) - return - } - deleteInstance(app.id) - router.push('/deployments') - } return (
{t('settings.general')}
+
{t('settings.readOnly')}
@@ -77,16 +44,17 @@ const SettingsForm: FC = ({ app, hasDeployments }) => {