This commit is contained in:
Stephen Zhou 2026-05-07 21:36:30 +08:00
parent 0a477fc767
commit de7795aa80
No known key found for this signature in database
3 changed files with 157 additions and 128 deletions

View File

@ -1,5 +1,5 @@
'use client'
import type { App, AppModeEnum } from '@/types/app'
import type { App } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
@ -13,6 +13,7 @@ import AppIcon from '@/app/components/base/app-icon'
import Input from '@/app/components/base/input'
import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { AppModeEnum } from '@/types/app'
import { useDeploymentsStore } from '../store'
const MAX_STUDIO_SOURCE_APPS = 100
@ -20,7 +21,7 @@ const MAX_STUDIO_SOURCE_APPS = 100
type StudioSourceApp = {
id: string
name: string
mode: string
mode: AppModeEnum
iconType?: App['icon_type']
icon?: string
iconBackground?: string
@ -32,7 +33,7 @@ function toStudioSourceAppInfo(app: App): StudioSourceApp {
return {
id: app.id,
name: app.name,
mode: app.mode || 'workflow',
mode: app.mode || AppModeEnum.WORKFLOW,
iconType: app.icon_type,
icon: app.icon,
iconBackground: app.icon_background ?? undefined,
@ -118,7 +119,7 @@ function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps) {
imageUrl={selected.iconUrl}
/>
<AppTypeIcon
type={selected.mode as unknown as AppModeEnum}
type={selected.mode}
wrapperClassName="absolute -bottom-0.5 -right-0.5 w-3 h-3 shadow-sm"
className="h-2 w-2"
/>
@ -187,7 +188,7 @@ function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps) {
imageUrl={app.iconUrl}
/>
<AppTypeIcon
type={app.mode as unknown as AppModeEnum}
type={app.mode}
wrapperClassName="absolute -bottom-0.5 -right-0.5 w-3 h-3 shadow-sm"
className="h-2 w-2"
/>

View File

@ -1,6 +1,6 @@
'use client'
import type { KeyboardEvent } from 'react'
import type { EnvironmentOption } from '../types'
import type { EnvironmentDeploymentRow, EnvironmentOption } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
@ -93,6 +93,90 @@ function NewDeploymentMenu({ appInstanceId, availableEnvs }: {
)
}
function DeploymentRowActions({ appInstanceId, envId, row }: {
appInstanceId: string
envId: string
row: EnvironmentDeploymentRow
}) {
const { t } = useTranslation('deployments')
const [menuOpen, setMenuOpen] = useState(false)
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const cancelDeployment = useMutation(consoleQuery.enterprise.appDeploy.cancelRuntimeDeployment.mutationOptions())
const undeployDeployment = useMutation(consoleQuery.enterprise.appDeploy.undeployRuntimeInstance.mutationOptions())
const isUndeployed = isUndeployedDeploymentRow(row)
const status = deploymentStatus(row)
function handleRuntimeAction() {
const runtimeInstanceId = deploymentId(row)
setMenuOpen(false)
if (status === 'deploying') {
cancelDeployment.mutate({
params: {
appInstanceId,
runtimeInstanceId,
},
body: {
appInstanceId,
runtimeInstanceId,
},
})
return
}
undeployDeployment.mutate({
params: {
appInstanceId,
runtimeInstanceId,
},
body: {
appInstanceId,
runtimeInstanceId,
},
})
}
return (
<div
className="flex shrink-0 items-center gap-1"
onClick={e => e.stopPropagation()}
onKeyDown={e => e.stopPropagation()}
>
<Button size="small" variant="secondary" onClick={() => openDeployDrawer({ appInstanceId, environmentId: envId })}>
{isUndeployed
? t('deployDrawer.deploy')
: status === 'ready'
? t('deployTab.deployOtherVersion')
: status === 'deploying'
? t('deployTab.viewProgress')
: t('deployTab.viewError')}
</Button>
{!isUndeployed && (
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger
aria-label={t('deployTab.moreActions')}
className="flex h-7 w-7 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
>
<span className="i-ri-more-line h-4 w-4" />
</DropdownMenuTrigger>
{menuOpen && (
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[200px]">
<DropdownMenuItem
className="gap-2 px-3"
onClick={handleRuntimeAction}
>
<span className="system-sm-regular text-text-destructive">
{status === 'deploying' ? t('deployTab.cancelDeployment') : t('deployTab.undeploy')}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
)}
</DropdownMenu>
)}
</div>
)
}
export function DeployTab({ instanceId: appInstanceId }: {
instanceId: string
}) {
@ -103,9 +187,6 @@ export function DeployTab({ instanceId: appInstanceId }: {
},
}))
const { data: environmentOptionsReply } = useQuery(consoleQuery.enterprise.appDeploy.listDeploymentEnvironmentOptions.queryOptions())
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
const cancelDeployment = useMutation(consoleQuery.enterprise.appDeploy.cancelRuntimeDeployment.mutationOptions())
const undeployDeployment = useMutation(consoleQuery.enterprise.appDeploy.undeployRuntimeInstance.mutationOptions())
const environmentOptions = environmentOptionsFromOptionsReply(environmentOptionsReply)
const rows = environmentDeployments?.data?.filter(row => row.environment?.id) ?? []
const deployedRuntimeRows = deployedRows(environmentDeployments?.data)
@ -163,71 +244,7 @@ export function DeployTab({ instanceId: appInstanceId }: {
const envId = environmentId(row.environment)
const isUndeployed = isUndeployedDeploymentRow(row)
const isExpanded = !isUndeployed && activeExpanded === envId
const status = deploymentStatus(row)
const release = activeRelease(row)
const actions = (
<div
className="flex shrink-0 items-center gap-1"
onClick={e => e.stopPropagation()}
onKeyDown={e => e.stopPropagation()}
>
<Button size="small" variant="secondary" onClick={() => openDeployDrawer({ appInstanceId, environmentId: envId })}>
{isUndeployed
? t('deployDrawer.deploy')
: status === 'ready'
? t('deployTab.deployOtherVersion')
: status === 'deploying'
? t('deployTab.viewProgress')
: t('deployTab.viewError')}
</Button>
{!isUndeployed && (
<DropdownMenu modal={false}>
<DropdownMenuTrigger
aria-label={t('deployTab.moreActions')}
className="flex h-7 w-7 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
>
<span className="i-ri-more-line h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[200px]">
<DropdownMenuItem
className="gap-2 px-3"
onClick={() => {
const runtimeInstanceId = deploymentId(row)
if (status === 'deploying') {
cancelDeployment.mutate({
params: {
appInstanceId,
runtimeInstanceId,
},
body: {
appInstanceId,
runtimeInstanceId,
},
})
return
}
undeployDeployment.mutate({
params: {
appInstanceId,
runtimeInstanceId,
},
body: {
appInstanceId,
runtimeInstanceId,
},
})
}}
>
<span className="system-sm-regular text-text-destructive">
{status === 'deploying' ? t('deployTab.cancelDeployment') : t('deployTab.undeploy')}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
)
const chevron = !isUndeployed && (
<span
className={cn(
@ -273,7 +290,7 @@ export function DeployTab({ instanceId: appInstanceId }: {
</div>
</div>
<div className="flex shrink-0 items-center gap-1 lg:hidden">
{actions}
<DeploymentRowActions appInstanceId={appInstanceId} envId={envId} row={row} />
{chevron}
</div>
</div>
@ -287,7 +304,7 @@ export function DeployTab({ instanceId: appInstanceId }: {
<DeploymentStatusSummary row={row} />
</div>
<div className="hidden min-w-0 items-center justify-end gap-1 lg:flex">
{actions}
<DeploymentRowActions appInstanceId={appInstanceId} envId={envId} row={row} />
{chevron}
</div>
</div>

View File

@ -25,8 +25,6 @@ export function InstanceCard({ app }: {
}) {
const { t } = useTranslation('deployments')
const { formatTimeFromNow } = useFormatTimeFromNow()
const [menuOpen, setMenuOpen] = useState(false)
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
if (!app.id)
return null
@ -185,58 +183,71 @@ export function InstanceCard({ app }: {
</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
className={cn(
'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity',
menuOpen
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100',
)}
>
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger
aria-label={t('card.moreActions')}
className={cn(
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',
)}
>
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
</DropdownMenuTrigger>
{menuOpen && (
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[216px]">
<DropdownMenuItem
className="gap-2 px-3"
onClick={() => {
setMenuOpen(false)
openDeployDrawer({ appInstanceId: appId })
}}
>
<span className="system-sm-regular text-text-secondary">{t('card.menu.deploy')}</span>
</DropdownMenuItem>
<DropdownMenuLinkItem
className="gap-2 px-3"
render={<Link href={detailHref} />}
>
<span className="system-sm-regular text-text-secondary">{t('card.menu.viewDetail')}</span>
</DropdownMenuLinkItem>
<DropdownMenuSeparator />
<DropdownMenuItem
aria-disabled
title={t('card.menu.deleteDisabled')}
className="cursor-not-allowed gap-2 px-3 opacity-50"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
>
<span className="system-sm-regular text-text-destructive">{t('card.menu.delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
<InstanceCardActions appId={appId} detailHref={detailHref} />
</div>
)
}
function InstanceCardActions({ appId, detailHref }: {
appId: string
detailHref: string
}) {
const { t } = useTranslation('deployments')
const [menuOpen, setMenuOpen] = useState(false)
const openDeployDrawer = useDeploymentsStore(state => state.openDeployDrawer)
return (
<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
className={cn(
'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity',
menuOpen
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100',
)}
>
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger
aria-label={t('card.moreActions')}
className={cn(
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',
)}
</DropdownMenu>
</div>
>
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
</DropdownMenuTrigger>
{menuOpen && (
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[216px]">
<DropdownMenuItem
className="gap-2 px-3"
onClick={() => {
setMenuOpen(false)
openDeployDrawer({ appInstanceId: appId })
}}
>
<span className="system-sm-regular text-text-secondary">{t('card.menu.deploy')}</span>
</DropdownMenuItem>
<DropdownMenuLinkItem
className="gap-2 px-3"
render={<Link href={detailHref} />}
>
<span className="system-sm-regular text-text-secondary">{t('card.menu.viewDetail')}</span>
</DropdownMenuLinkItem>
<DropdownMenuSeparator />
<DropdownMenuItem
aria-disabled
title={t('card.menu.deleteDisabled')}
className="cursor-not-allowed gap-2 px-3 opacity-50"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
>
<span className="system-sm-regular text-text-destructive">{t('card.menu.delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
)}
</DropdownMenu>
</div>
</div>
)