This commit is contained in:
Stephen Zhou 2026-05-07 20:08:54 +08:00
parent 3f36471ec0
commit cfb1e0217f
No known key found for this signature in database
3 changed files with 102 additions and 106 deletions

View File

@ -7,7 +7,8 @@ import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels' import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
import useDocumentTitle from '@/hooks/use-document-title' import useDocumentTitle from '@/hooks/use-document-title'
import { useRouter, useSelectedLayoutSegment } from '@/next/navigation' import Link from '@/next/link'
import { useSelectedLayoutSegment } from '@/next/navigation'
import { consoleQuery } from '@/service/client' import { consoleQuery } from '@/service/client'
import { DeployDrawer } from '../components/deploy-drawer' import { DeployDrawer } from '../components/deploy-drawer'
import { RollbackModal } from '../components/rollback-modal' import { RollbackModal } from '../components/rollback-modal'
@ -20,7 +21,6 @@ export function InstanceDetail({ instanceId, children }: {
}) { }) {
const { t } = useTranslation('deployments') const { t } = useTranslation('deployments')
const { t: tCommon } = useTranslation() const { t: tCommon } = useTranslation()
const router = useRouter()
const selectedSegment = useSelectedLayoutSegment() const selectedSegment = useSelectedLayoutSegment()
const selectedTab = selectedSegment ?? undefined const selectedTab = selectedSegment ?? undefined
const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview' const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview'
@ -47,7 +47,7 @@ export function InstanceDetail({ instanceId, children }: {
return ( return (
<div className="flex h-full flex-col items-center justify-center gap-3 bg-background-body"> <div className="flex h-full flex-col items-center justify-center gap-3 bg-background-body">
<div className="title-xl-semi-bold text-text-primary">{t('detail.notFound')}</div> <div className="title-xl-semi-bold text-text-primary">{t('detail.notFound')}</div>
<Button variant="secondary" onClick={() => router.push('/deployments')}> <Button nativeButton={false} variant="secondary" render={<Link href="/deployments" />}>
<span aria-hidden className="i-ri-arrow-left-line h-4 w-4" /> <span aria-hidden className="i-ri-arrow-left-line h-4 w-4" />
{t('detail.backToInstances')} {t('detail.backToInstances')}
</Button> </Button>

View File

@ -5,7 +5,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels' import { getAppModeLabel } from '@/app/components/app-sidebar/app-info/app-mode-labels'
import { useRouter } from '@/next/navigation' import Link from '@/next/link'
import { consoleQuery } from '@/service/client' import { consoleQuery } from '@/service/client'
import { StatusBadge } from '../components/status-badge' import { StatusBadge } from '../components/status-badge'
import { DEPLOYMENT_PAGE_SIZE } from '../data' import { DEPLOYMENT_PAGE_SIZE } from '../data'
@ -17,6 +17,10 @@ import {
type SwitchableTab = 'deploy' | 'versions' | 'access' | 'settings' type SwitchableTab = 'deploy' | 'versions' | 'access' | 'settings'
function tabHref(appId: string, tab: SwitchableTab): string {
return `/deployments/${appId}/${tab}`
}
function Section({ title, action, children }: { function Section({ title, action, children }: {
title: string title: string
action?: ReactNode action?: ReactNode
@ -79,7 +83,7 @@ function AccessOverviewRow({ label, enabled, hint, meta }: AccessOverviewRowProp
) )
} }
function overviewDeploymentStatus(status?: string) { function overviewDeploymentStatus(status?: string): 'deploying' | 'deploy_failed' | 'ready' {
const normalized = status?.toLowerCase() ?? '' const normalized = status?.toLowerCase() ?? ''
if (normalized.includes('deploying') || normalized.includes('pending')) if (normalized.includes('deploying') || normalized.includes('pending'))
return 'deploying' return 'deploying'
@ -93,7 +97,6 @@ export function OverviewTab({ instanceId }: {
}) { }) {
const { t } = useTranslation('deployments') const { t } = useTranslation('deployments')
const { t: tCommon } = useTranslation() const { t: tCommon } = useTranslation()
const router = useRouter()
const input = { params: { appInstanceId: instanceId } } const input = { params: { appInstanceId: instanceId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({ const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input, input,
@ -121,10 +124,6 @@ export function OverviewTab({ instanceId }: {
const appId = overviewApp.id const appId = overviewApp.id
const appName = overviewApp.name ?? appId const appName = overviewApp.name ?? appId
const switchTab = (tab: SwitchableTab) => {
router.push(`/deployments/${appId}/${tab}`)
}
const appModeLabel = getAppModeLabel(overviewApp.mode ?? 'workflow', tCommon) const appModeLabel = getAppModeLabel(overviewApp.mode ?? 'workflow', tCommon)
const webappAccessUrl = webappUrl(overview?.access?.webappUrl) const webappAccessUrl = webappUrl(overview?.access?.webappUrl)
const cliUrl = overview?.access?.cliUrl const cliUrl = overview?.access?.cliUrl
@ -145,7 +144,7 @@ export function OverviewTab({ instanceId }: {
<Section <Section
title={t('overview.deploymentStatus')} title={t('overview.deploymentStatus')}
action={( action={(
<Button size="small" variant="secondary" onClick={() => switchTab('deploy')}> <Button nativeButton={false} size="small" variant="secondary" render={<Link href={tabHref(appId, 'deploy')} />}>
{t('overview.viewDeployments')} {t('overview.viewDeployments')}
<span className="i-ri-arrow-right-up-line h-3.5 w-3.5" /> <span className="i-ri-arrow-right-up-line h-3.5 w-3.5" />
</Button> </Button>
@ -160,20 +159,23 @@ export function OverviewTab({ instanceId }: {
? t(canCreateRelease ? 'overview.noReleaseYet' : 'overview.noReleaseSourceUnavailable') ? t(canCreateRelease ? 'overview.noReleaseYet' : 'overview.noReleaseSourceUnavailable')
: t('overview.notDeployedYet')} : t('overview.notDeployedYet')}
</div> </div>
<Button {releaseRows.length === 0
size="small" ? canCreateRelease
variant="primary" ? (
disabled={releaseRows.length === 0 && !canCreateRelease} <Button nativeButton={false} size="small" variant="primary" render={<Link href={tabHref(appId, 'versions')} />}>
onClick={() => { {t('overview.createRelease')}
if (releaseRows.length === 0) { </Button>
switchTab('versions') )
return : (
} <Button size="small" variant="primary" disabled>
openDeployDrawer({ appInstanceId: appId }) {t('overview.createRelease')}
}} </Button>
> )
{releaseRows.length === 0 ? t('overview.createRelease') : t('overview.deploy')} : (
</Button> <Button size="small" variant="primary" onClick={() => openDeployDrawer({ appInstanceId: appId })}>
{t('overview.deploy')}
</Button>
)}
</div> </div>
) )
: ( : (
@ -199,7 +201,7 @@ export function OverviewTab({ instanceId }: {
<Section <Section
title={t('overview.accessStatus')} title={t('overview.accessStatus')}
action={( action={(
<Button size="small" variant="secondary" onClick={() => switchTab('access')}> <Button nativeButton={false} size="small" variant="secondary" render={<Link href={tabHref(appId, 'access')} />}>
{t('overview.configureAccess')} {t('overview.configureAccess')}
<span className="i-ri-arrow-right-up-line h-3.5 w-3.5" /> <span className="i-ri-arrow-right-up-line h-3.5 w-3.5" />
</Button> </Button>

View File

@ -1,13 +1,13 @@
'use client' 'use client'
import type { AppInstanceCard } from '@dify/contracts/enterprise/types.gen' import type { AppInstanceCard } from '@dify/contracts/enterprise/types.gen'
import type { MouseEvent } from 'react'
import type { AppModeEnum } from '@/types/app' import type { AppModeEnum } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLinkItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu' } from '@langgenius/dify-ui/dropdown-menu'
@ -17,14 +17,13 @@ import { useTranslation } from 'react-i18next'
import { AppTypeIcon } from '@/app/components/app/type-selector' import { AppTypeIcon } from '@/app/components/app/type-selector'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useRouter } from '@/next/navigation' import Link from '@/next/link'
import { useDeploymentsStore } from '../store' import { useDeploymentsStore } from '../store'
export function InstanceCard({ app }: { export function InstanceCard({ app }: {
app: AppInstanceCard app: AppInstanceCard
}) { }) {
const { t } = useTranslation('deployments') const { t } = useTranslation('deployments')
const router = useRouter()
const { formatTimeFromNow } = useFormatTimeFromNow() const { formatTimeFromNow } = useFormatTimeFromNow()
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer) const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
@ -35,14 +34,7 @@ export function InstanceCard({ app }: {
const appId = app.id const appId = app.id
const appName = app.name ?? appId const appName = app.name ?? appId
const appMode = app.mode ?? 'workflow' const appMode = app.mode ?? 'workflow'
const navigateToDetail = () => router.push(`/deployments/${appId}/overview`) const detailHref = `/deployments/${appId}/overview`
const handleMenuAction = (e: MouseEvent<HTMLElement>, action: () => void) => {
e.stopPropagation()
e.preventDefault()
setMenuOpen(false)
action()
}
const statusCount = (status: string) => const statusCount = (status: string) =>
app.statuses?.find(item => item.status === status)?.count ?? 0 app.statuses?.find(item => item.status === status)?.count ?? 0
@ -122,75 +114,78 @@ export function InstanceCard({ app }: {
return ( return (
<div <div
onClick={(e) => {
e.preventDefault()
navigateToDetail()
}}
className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg" className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
> >
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pt-[14px] pb-3"> <Link
<div className="relative shrink-0"> href={detailHref}
<AppIcon className="flex h-full flex-col rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-state-accent-solid"
size="large" >
iconType="emoji" <div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pt-[14px] pb-3">
icon={app.icon} <div className="relative shrink-0">
background={app.iconBackground} <AppIcon
/> size="large"
<AppTypeIcon iconType="emoji"
type={appMode as AppModeEnum} icon={app.icon}
wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm" background={app.iconBackground}
className="h-3 w-3" />
/> <AppTypeIcon
</div> type={appMode as AppModeEnum}
<div className="w-0 grow py-px"> wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm"
<div className="flex items-center text-sm leading-5 font-semibold text-text-secondary"> className="h-3 w-3"
<div className="truncate" title={appName}>{appName}</div> />
</div> </div>
<div className="truncate text-[10px] leading-[18px] font-medium text-text-tertiary" title={appModeLabel}> <div className="w-0 grow py-px">
{appModeLabel} <div className="flex items-center text-sm leading-5 font-semibold text-text-secondary">
<div className="truncate" title={appName}>{appName}</div>
</div>
<div className="truncate text-[10px] leading-[18px] font-medium text-text-tertiary" title={appModeLabel}>
{appModeLabel}
</div>
</div> </div>
</div> </div>
</div> <div className="flex grow flex-col gap-2 px-[14px]">
<div className="flex grow flex-col gap-2 px-[14px]"> <Tooltip>
<Tooltip> <TooltipTrigger
<TooltipTrigger render={(
render={( <div className="flex min-w-0 items-center gap-1.5">
<div className="flex min-w-0 items-center gap-1.5"> <span
<span className={cn(
className={cn( 'inline-flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 system-xs-medium',
'inline-flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 system-xs-medium', healthPillClass,
healthPillClass, )}
)} >
> <span className={cn('h-1.5 w-1.5 rounded-full', healthDotClass)} />
<span className={cn('h-1.5 w-1.5 rounded-full', healthDotClass)} /> {primaryText}
{primaryText}
</span>
{secondaryParts.length > 0 && (
<span className="truncate system-xs-regular text-text-tertiary">
{secondaryParts.join(' · ')}
</span> </span>
)} {secondaryParts.length > 0 && (
</div> <span className="truncate system-xs-regular text-text-tertiary">
)} {secondaryParts.join(' · ')}
/> </span>
<TooltipContent>{statusTooltip}</TooltipContent> )}
</Tooltip> </div>
<div className="flex min-w-0 items-center gap-1.5 system-xs-regular text-text-tertiary"> )}
<span aria-hidden className="i-ri-apps-2-line h-3.5 w-3.5 shrink-0 text-text-quaternary" /> />
<span className="truncate" title={app.sourceAppName ?? appName}> <TooltipContent>{statusTooltip}</TooltipContent>
{t('card.fromApp', { name: app.sourceAppName ?? appName })} </Tooltip>
</span> <div className="flex min-w-0 items-center gap-1.5 system-xs-regular text-text-tertiary">
<span aria-hidden className="i-ri-apps-2-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
<span className="truncate" title={app.sourceAppName ?? appName}>
{t('card.fromApp', { name: app.sourceAppName ?? appName })}
</span>
</div>
</div> </div>
</div> <div className="absolute right-0 bottom-1 left-0 flex h-[42px] shrink-0 items-center pt-1 pr-[6px] pb-[6px] pl-[14px]">
<div className="absolute right-0 bottom-1 left-0 flex h-[42px] shrink-0 items-center pt-1 pr-[6px] pb-[6px] pl-[14px]"> <div className="mr-[41px] flex min-w-0 grow items-center gap-1.5 system-xs-regular text-text-tertiary">
<div className="mr-[41px] flex min-w-0 grow items-center gap-1.5 system-xs-regular text-text-tertiary"> <span aria-hidden className="i-ri-time-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
<span aria-hidden className="i-ri-time-line h-3.5 w-3.5 shrink-0 text-text-quaternary" /> <span className="truncate">
<span className="truncate"> {lastDeployedAt
{lastDeployedAt ? t('card.lastDeployed', { time: formatTimeFromNow(lastDeployedAt) })
? t('card.lastDeployed', { time: formatTimeFromNow(lastDeployedAt) }) : t('card.neverDeployed')}
: t('card.neverDeployed')} </span>
</span> </div>
</div> </div>
</Link>
<div className="pointer-events-none absolute right-0 bottom-1 left-0 flex h-[42px] shrink-0 items-center pt-1 pr-[6px] pb-[6px] pl-[14px]">
<div <div
className={cn( className={cn(
'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity', 'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity',
@ -206,10 +201,6 @@ export function InstanceCard({ app }: {
menuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent', menuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent',
'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover', 'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover',
)} )}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
> >
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" /> <span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -217,16 +208,19 @@ export function InstanceCard({ app }: {
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[216px]"> <DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[216px]">
<DropdownMenuItem <DropdownMenuItem
className="gap-2 px-3" className="gap-2 px-3"
onClick={e => handleMenuAction(e, () => openDeployDrawer({ appInstanceId: appId }))} onClick={() => {
setMenuOpen(false)
openDeployDrawer({ appInstanceId: appId })
}}
> >
<span className="system-sm-regular text-text-secondary">{t('card.menu.deploy')}</span> <span className="system-sm-regular text-text-secondary">{t('card.menu.deploy')}</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuLinkItem
className="gap-2 px-3" className="gap-2 px-3"
onClick={e => handleMenuAction(e, navigateToDetail)} render={<Link href={detailHref} />}
> >
<span className="system-sm-regular text-text-secondary">{t('card.menu.viewDetail')}</span> <span className="system-sm-regular text-text-secondary">{t('card.menu.viewDetail')}</span>
</DropdownMenuItem> </DropdownMenuLinkItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
aria-disabled aria-disabled