From ea6e7a9ed06487b5a372b353a661b6c20eb60ecf Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 7 May 2026 19:14:04 +0800 Subject: [PATCH] tweaks --- .../deployments/list/environment-filter.tsx | 76 +++++++-- web/features/deployments/list/index.tsx | 155 ++++++++---------- .../deployments/list/instance-card.tsx | 39 +++-- .../deployments/list/new-instance-card.tsx | 9 +- web/features/deployments/list/query-state.ts | 4 + web/features/deployments/nav/index.tsx | 64 +++++--- web/features/deployments/types.ts | 10 -- web/features/deployments/utils.ts | 33 ---- 8 files changed, 204 insertions(+), 186 deletions(-) create mode 100644 web/features/deployments/list/query-state.ts diff --git a/web/features/deployments/list/environment-filter.tsx b/web/features/deployments/list/environment-filter.tsx index 2f068e5675..450cc2b821 100644 --- a/web/features/deployments/list/environment-filter.tsx +++ b/web/features/deployments/list/environment-filter.tsx @@ -8,9 +8,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { useQuery } from '@tanstack/react-query' +import { useQueryState } from 'nuqs' import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { consoleQuery } from '@/service/client' +import { envFilterQueryState } from './query-state' -export type EnvironmentFilterOption = { +type EnvironmentFilterOption = { value: string text: string icon: ReactNode @@ -18,13 +23,64 @@ export type EnvironmentFilterOption = { disabledReason?: string } -export function EnvironmentFilter({ value, options, onChange }: { - value: string - options: EnvironmentFilterOption[] - onChange: (value: string) => void -}) { +type FilterEnvironment = { + id: string + name: string + disabled?: boolean + disabledReason?: string +} + +function getEnvironmentId(env: FilterEnvironment) { + return env.id +} + +function getEnvironmentFilterOption(env: FilterEnvironment): EnvironmentFilterOption { + return { + value: env.id, + text: env.name, + icon: , + disabled: env.disabled, + disabledReason: env.disabledReason, + } +} + +export function EnvironmentFilter() { + const { t } = useTranslation('deployments') const [open, setOpen] = useState(false) - const selectedOption = options.find(option => option.value === value) ?? options[0] + const [envFilter, setEnvFilter] = useQueryState('env', envFilterQueryState) + const { data: environmentOptionsReply } = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentEnvironmentOptions.queryOptions()) + const environmentOptions = environmentOptionsReply?.environments ?? [] + + function getFilterEnvironment(env: (typeof environmentOptions)[number]): FilterEnvironment[] { + if (!env.id) + return [] + return [{ + id: env.id, + name: env.name || env.id, + disabled: env.deployable === false, + disabledReason: env.disabledReason, + }] + } + + const environments = environmentOptions.flatMap(getFilterEnvironment) + const envIdSet = new Set(environments.map(getEnvironmentId)) + const activeFilter = envFilter === 'all' || envFilter === 'not-deployed' || envIdSet.has(envFilter) + ? envFilter + : 'all' + const filterOptions: EnvironmentFilterOption[] = [ + { + value: 'all', + text: t('filter.allEnvs'), + icon: , + }, + ...environments.map(getEnvironmentFilterOption), + { + value: 'not-deployed', + text: t('filter.notDeployed'), + icon: , + }, + ] + const selectedOption = filterOptions.find(option => option.value === activeFilter) ?? filterOptions[0] return ( @@ -51,13 +107,13 @@ export function EnvironmentFilter({ value, options, onChange }: { popupClassName="w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]" >
- {options.map(option => ( + {filterOptions.map(option => ( { if (option.disabled) return - onChange(option.value) + void setEnvFilter(option.value) setOpen(false) }} title={option.disabled ? option.disabledReason : undefined} @@ -71,7 +127,7 @@ export function EnvironmentFilter({ value, options, onChange }: { > {option.icon} {option.text} - {option.value === value && ( + {option.value === activeFilter && ( )} diff --git a/web/features/deployments/list/index.tsx b/web/features/deployments/list/index.tsx index 680aa65fe1..cb6722d0f7 100644 --- a/web/features/deployments/list/index.tsx +++ b/web/features/deployments/list/index.tsx @@ -1,8 +1,9 @@ 'use client' +import type { ChangeEvent } from 'react' import { useQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' -import { debounce, parseAsString, useQueryState } from 'nuqs' +import { debounce, useQueryState } from 'nuqs' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import { consoleQuery } from '@/service/client' @@ -10,36 +11,59 @@ import { CreateInstanceModal } from '../components/create-instance-modal' import { DeployDrawer } from '../components/deploy-drawer' import { RollbackModal } from '../components/rollback-modal' import { SOURCE_APPS_PAGE_SIZE } from '../data' -import { useDeploymentsStore } from '../store' -import { - deploymentSummariesFromList, - sourceAppsFromList, -} from '../utils' import { EnvironmentFilter } from './environment-filter' import { InstanceCard } from './instance-card' import { NewInstanceCard } from './new-instance-card' +import { envFilterQueryState, keywordsQueryState } from './query-state' -export function DeploymentsMain() { +function DeploymentsSearchInput() { const { t } = useTranslation('deployments') - const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal) + const [keywords, setKeywords] = useQueryState('keywords', keywordsQueryState) - const [envFilter, setEnvFilter] = useQueryState( - 'env', - parseAsString.withDefault('all').withOptions({ history: 'push' }), - ) - const [keywords, setKeywords] = useQueryState( - 'keywords', - parseAsString.withDefault('').withOptions({ history: 'push' }), - ) - const debouncedKeywords = useDebounce(keywords, { wait: 300 }) - const queryKeywords = keywords.trim() ? debouncedKeywords : keywords - - const handleKeywordsChange = (next: string) => { + function handleKeywordsChange(next: string) { void setKeywords(next.trim() ? next : null, { limitUrlUpdates: next.trim() ? debounce(300) : undefined, }) } + function handleKeywordsInputChange(e: ChangeEvent) { + handleKeywordsChange(e.target.value) + } + + function handleKeywordsClear() { + handleKeywordsChange('') + } + + return ( + + ) +} + +function DeploymentsListControls() { + return ( +
+
+ + +
+
+ ) +} + +function DeploymentsList() { + const [envFilter] = useQueryState('env', envFilterQueryState) + const [keywords] = useQueryState('keywords', keywordsQueryState) + const debouncedKeywords = useDebounce(keywords, { wait: 300 }) + const queryKeywords = keywords.trim() ? debouncedKeywords : keywords + const requestedEnvironmentId = envFilter !== 'all' && envFilter !== 'not-deployed' ? envFilter : undefined @@ -54,79 +78,32 @@ export function DeploymentsMain() { }, }, })) - const { data: environmentOptionsReply } = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentEnvironmentOptions.queryOptions()) - const apps = sourceAppsFromList(listQuery.data) - const summaries = deploymentSummariesFromList(listQuery.data) - const environments = environmentOptionsReply?.environments?.flatMap((env) => { - if (!env.id) - return [] - return [{ - id: env.id, - name: env.name || env.id, - disabled: env.deployable === false, - disabledReason: env.disabledReason, - }] - }) ?? [] - const envIdSet = new Set(environments.map(e => e.id)) - const activeFilter = envFilter === 'all' || envFilter === 'not-deployed' || envIdSet.has(envFilter) - ? envFilter - : 'all' - - const filterOptions = [ - { - value: 'all', - text: t('filter.allEnvs'), - icon: , - }, - ...environments.map(env => ({ - value: env.id, - text: env.name, - icon: , - disabled: env.disabled, - disabledReason: env.disabledReason, - })), - { - value: 'not-deployed', - text: t('filter.notDeployed'), - icon: , - }, - ] + const apps = listQuery.data?.data ?? [] return ( - <> -
-
-
- { void setEnvFilter(next) }} - options={filterOptions} - /> - handleKeywordsChange(e.target.value)} - onClear={() => handleKeywordsChange('')} - /> -
-
-
- - {apps.map(app => ( - - ))} -
- -
+
+ +
+ + {apps.map(app => app.id + ? ( + + ) + : null)}
+
+
+ ) +} + +export function DeploymentsMain() { + return ( + <> + diff --git a/web/features/deployments/list/instance-card.tsx b/web/features/deployments/list/instance-card.tsx index afeb1db4db..b560d7182c 100644 --- a/web/features/deployments/list/instance-card.tsx +++ b/web/features/deployments/list/instance-card.tsx @@ -1,8 +1,7 @@ 'use client' +import type { AppInstanceCard } from '@dify/contracts/enterprise/types.gen' import type { MouseEvent } from 'react' -import type { AppInfo } from '../types' -import type { AppDeploymentSummary } from '@/features/deployments/types' import type { AppModeEnum } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' import { @@ -21,9 +20,8 @@ import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import { useRouter } from '@/next/navigation' import { useDeploymentsStore } from '../store' -export function InstanceCard({ app, summary }: { - app: AppInfo - summary?: AppDeploymentSummary +export function InstanceCard({ app }: { + app: AppInstanceCard }) { const { t } = useTranslation('deployments') const router = useRouter() @@ -31,7 +29,13 @@ export function InstanceCard({ app, summary }: { const [menuOpen, setMenuOpen] = useState(false) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) - const navigateToDetail = () => router.push(`/deployments/${app.id}/overview`) + if (!app.id) + return null + + const appId = app.id + const appName = app.name ?? appId + const appMode = app.mode ?? 'workflow' + const navigateToDetail = () => router.push(`/deployments/${appId}/overview`) const handleMenuAction = (e: MouseEvent, action: () => void) => { e.stopPropagation() @@ -41,14 +45,14 @@ export function InstanceCard({ app, summary }: { } const statusCount = (status: string) => - summary?.statuses?.find(item => item.status === status)?.count ?? 0 + app.statuses?.find(item => item.status === status)?.count ?? 0 const failedCount = statusCount('failed') + statusCount('deploy_failed') const deployingCount = statusCount('deploying') const readyCount = statusCount('ready') const envCount = failedCount + deployingCount + readyCount - const lastDeployedAt = summary?.lastDeployedAt - ? Date.parse(summary.lastDeployedAt) + const lastDeployedAt = app.lastDeployedAt + ? Date.parse(app.lastDeployedAt) : null const primaryStatus: 'none' | 'failed' | 'deploying' | 'ready' = envCount === 0 @@ -83,7 +87,7 @@ export function InstanceCard({ app, summary }: { return status || 'unknown' } - const statusSummaryTooltip = summary?.statuses?.filter(item => item.count && item.status !== 'undeployed') ?? [] + const statusSummaryTooltip = app.statuses?.filter(item => item.count && item.status !== 'undeployed') ?? [] const statusTooltip = primaryStatus === 'none' ? t('card.tooltip.notDeployed') : ( @@ -114,7 +118,7 @@ export function InstanceCard({ app, summary }: { ? 'bg-util-colors-warning-warning-500 animate-pulse' : 'bg-util-colors-green-green-500' - const appModeLabel = t(`appMode.${app.mode}`, { defaultValue: app.mode }) + const appModeLabel = t(`appMode.${appMode}`, { defaultValue: appMode }) return (
-
{app.name}
+
{appName}
{appModeLabel} @@ -174,8 +177,8 @@ export function InstanceCard({ app, summary }: {
- - {t('card.fromApp', { name: app.sourceAppName ?? app.name })} + + {t('card.fromApp', { name: app.sourceAppName ?? appName })}
@@ -214,7 +217,7 @@ export function InstanceCard({ app, summary }: { handleMenuAction(e, () => openDeployDrawer({ appInstanceId: app.id }))} + onClick={e => handleMenuAction(e, () => openDeployDrawer({ appInstanceId: appId }))} > {t('card.menu.deploy')} diff --git a/web/features/deployments/list/new-instance-card.tsx b/web/features/deployments/list/new-instance-card.tsx index 452779671f..262a82487a 100644 --- a/web/features/deployments/list/new-instance-card.tsx +++ b/web/features/deployments/list/new-instance-card.tsx @@ -2,6 +2,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { useTranslation } from 'react-i18next' +import { useDeploymentsStore } from '../store' type NewInstanceActionProps = { icon: string @@ -37,10 +38,10 @@ function NewInstanceAction({ icon, label, disabled, onClick }: NewInstanceAction ) } -export function NewInstanceCard({ onOpen }: { - onOpen: () => void -}) { +export function NewInstanceCard() { const { t } = useTranslation('deployments') + const openCreateInstanceModal = useDeploymentsStore(state => state.openCreateInstanceModal) + return (
@@ -50,7 +51,7 @@ export function NewInstanceCard({ onOpen }: { toAppInfoFromOverview(data.instance), + select: data => data.instance, })) const listQuery = useQuery(consoleQuery.enterprise.appDeploy.listAppInstances.queryOptions({ @@ -40,24 +71,13 @@ export function DeploymentsNav() { }, enabled: isActive, })) - const apps = sourceAppsFromList(listQuery.data) + const appNavItems = listQuery.data?.data?.flatMap(navItemFromListApp) ?? [] + const currentNavItem = navItemFromOverview(currentInstance) - const navApps = currentInstance && !apps.some(app => app.id === currentInstance.id) - ? [...apps, currentInstance] - : apps const navigationItems: NavItem[] = isActive - ? navApps.map((app) => { - return { - id: app.id, - name: app.name, - link: `/deployments/${app.id}/overview`, - icon_type: (app.iconType ?? null) as AppIconType | null, - icon: app.icon ?? '', - icon_background: app.iconBackground ?? null, - icon_url: app.iconUrl ?? null, - mode: app.mode as unknown as AppModeEnum | undefined, - } - }) + ? currentNavItem && !appNavItems.some(item => item.id === currentNavItem.id) + ? [...appNavItems, currentNavItem] + : appNavItems : [] const curNav = instanceId diff --git a/web/features/deployments/types.ts b/web/features/deployments/types.ts index fa8aa63fc6..b8f8b7947b 100644 --- a/web/features/deployments/types.ts +++ b/web/features/deployments/types.ts @@ -44,16 +44,6 @@ type ConsoleUser = EnterpriseContract.ConsoleUser & { displayName?: string } -export type AppDeploymentSummary = EnterpriseContract.AppInstanceCard & { - createdAt?: Timestamp - description?: string - status?: string -} - -export type ListAppDeploymentsReply = Omit & { - data?: AppDeploymentSummary[] -} - export type AppInstanceOverview = EnterpriseContract.AppInstanceBasicInfo export type RuntimeBindingDisplay = EnterpriseContract.ReleaseRuntimeBinding & { diff --git a/web/features/deployments/utils.ts b/web/features/deployments/utils.ts index 4bfd105266..7067f79bdd 100644 --- a/web/features/deployments/utils.ts +++ b/web/features/deployments/utils.ts @@ -1,6 +1,5 @@ import type { AccessPermissionKind, - AppDeploymentSummary, AppInfo, AppInstanceOverview, AppMode, @@ -8,7 +7,6 @@ import type { ConsoleReleaseSummary, EnvironmentDeploymentRow, EnvironmentOption, - ListAppDeploymentsReply, ListDeploymentEnvironmentOptionsReply, RuntimeBindingDisplay, } from './types' @@ -136,23 +134,6 @@ export function deployedRows(rows?: EnvironmentDeploymentRow[]) { }) ?? [] } -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, - iconBackground: summary.iconBackground, - sourceAppName: summary.sourceAppName, - sourceAppAvailable: summary.sourceAppAvailable, - canCreateRelease: summary.canCreateRelease, - } -} - export function toAppInfoFromOverview(instance?: AppInstanceOverview): AppInfo | undefined { if (!instance?.id) return undefined @@ -172,20 +153,6 @@ export function toAppInfoFromOverview(instance?: AppInstanceOverview): AppInfo | } } -export function sourceAppsFromList(response?: ListAppDeploymentsReply) { - return (response?.data ?? []) - .map(toAppInfoFromSummary) - .filter((app): app is AppInfo => Boolean(app)) -} - -export function deploymentSummariesFromList(response?: ListAppDeploymentsReply): Record { - return Object.fromEntries( - (response?.data ?? []) - .filter(summary => summary.id) - .map(summary => [summary.id!, summary]), - ) -} - export function environmentOptionsFromOptionsReply(response?: ListDeploymentEnvironmentOptionsReply): EnvironmentOption[] { return response?.environments ?.filter(environment => environment.id)