'use client' import type { AccessChannels, AppInstance, EnvironmentDeployment, Release, } from '@dify/contracts/enterprise/types.gen' import type { ReactElement } from 'react' import type { InstanceDetailTabKey } from '../detail/tabs' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useQuery } from '@tanstack/react-query' import { useSetAtom } from 'jotai' import { useTranslation } from 'react-i18next' import { SkeletonRectangle } from '@/app/components/base/skeleton' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import Link from '@/next/link' import { consoleQuery } from '@/service/client' import { EnvironmentDeploymentBadge } from '../deployment-ui' import { deploymentStatusLabelKey } from '../deployment-ui-utils' import { CreateReleaseControl } from '../detail/versions-tab/create-release-control' import { environmentName } from '../environment' import { formatDate, releaseLabel } from '../release' import { deploymentStatus, deploymentStatusPollingInterval, isUndeployedDeploymentRow, } from '../runtime-status' import { openDeployDrawerAtom } from '../store' const VISIBLE_ENVIRONMENT_COUNT = 3 const CARD_RELEASE_QUERY_PAGE_SIZE = 1 function getInstanceTabHref(appInstanceId: string, tabKey: InstanceDetailTabKey) { return `/deployments/${appInstanceId}/${tabKey}` } function hasEnvironment(row: EnvironmentDeployment) { return Boolean(row.environment?.id) } function isActiveDeployment(row: EnvironmentDeployment) { return hasEnvironment(row) && !isUndeployedDeploymentRow(row) } function pickLatestRelease(rows: Release[]): Release | undefined { return [...rows].sort((a, b) => { const aTime = a.createdAt ? Date.parse(a.createdAt) : 0 const bTime = b.createdAt ? Date.parse(b.createdAt) : 0 return bTime - aTime })[0] } function isReleaseDeployed(release: Release | undefined, rows: EnvironmentDeployment[]) { if (!release?.id) return false return rows.some(row => row.currentRelease?.id === release.id) } function releaseSourceLabel(release: Release | undefined, t: ReturnType>['t']) { if (release?.source === 'RELEASE_SOURCE_SOURCE_APP' || release?.sourceAppId) return t('versions.sourceAppOption') if (release?.source === 'RELEASE_SOURCE_UPLOAD') return t('versions.manualDslOption') return '—' } function ReleaseMetaTooltip({ release, deployed, children }: { release?: Release deployed: boolean children: ReactElement }) { const { t } = useTranslation('deployments') if (!release?.id) return children const rows = [ { label: t('card.tooltip.releaseName'), value: releaseLabel(release) }, { label: t('card.tooltip.deploymentStatus'), value: deployed ? t('card.tooltip.deployed') : t('card.tooltip.notDeployedShort') }, { label: t('card.tooltip.source'), value: releaseSourceLabel(release, t) }, { label: t('card.tooltip.createdAt'), value: formatDate(release.createdAt) }, ] return (
{rows.map(row => (
{row.label} {row.value}
))}
) } function EnvironmentChip({ row }: { row: EnvironmentDeployment }) { const { t } = useTranslation('deployments') const name = environmentName(row.environment) const status = deploymentStatus(row) return ( )} />
{name} {t(deploymentStatusLabelKey(status))}
{row.currentRelease?.id && (
{t('card.tooltip.release')} {releaseLabel(row.currentRelease)}
)}
) } function EnvironmentOverflow({ rows }: { rows: EnvironmentDeployment[] }) { const { t } = useTranslation('deployments') return ( {t('card.envOverflow', { count: rows.length })} )} />
{rows.map(row => (
{environmentName(row.environment)} {t(deploymentStatusLabelKey(deploymentStatus(row)))}
))}
) } function DeploymentStatusContent({ rows, isLoading, hasError, emptyAction, }: { rows: EnvironmentDeployment[] isLoading: boolean hasError: boolean emptyAction?: ReactElement }) { const { t } = useTranslation('deployments') const visibleRows = rows.slice(0, VISIBLE_ENVIRONMENT_COUNT) const overflowRows = rows.slice(VISIBLE_ENVIRONMENT_COUNT) if (isLoading) { return (
) } if (hasError) { return ( {t('common.loadFailed')} ) } if (rows.length > 0) { return (
{visibleRows.map(row => ( ))} {overflowRows.length > 0 && }
) } if (emptyAction) return
{emptyAction}
return null } function DeploymentAccessLinks({ appInstanceId, access, isLoading }: { appInstanceId: string access?: AccessChannels isLoading?: boolean }) { const { t } = useTranslation('deployments') if (isLoading) { return (
) } const links = [ access?.webAppEnabled ? { key: 'webapp', href: getInstanceTabHref(appInstanceId, 'access'), label: t('card.access.webApp'), icon: 'i-ri-global-line', } : undefined, access?.webAppEnabled ? { key: 'cli', href: getInstanceTabHref(appInstanceId, 'access'), label: t('card.access.cli'), icon: 'i-ri-terminal-box-line', } : undefined, access?.developerApiEnabled ? { key: 'api-tokens', href: getInstanceTabHref(appInstanceId, 'api-tokens'), label: t('card.access.api'), icon: 'i-ri-code-s-slash-line', } : undefined, ].filter((link): link is { key: string, href: string, label: string, icon: string } => Boolean(link)) if (links.length === 0) return
return (
{links.map(link => ( )} /> {link.label} ))}
) } export function InstanceCard({ app }: { app: AppInstance }) { const { t } = useTranslation('deployments') const { formatTimeFromNow } = useFormatTimeFromNow() const openDeployDrawer = useSetAtom(openDeployDrawerAtom) const appInstanceId = app.id ?? '' const appName = app.name ?? appInstanceId const detailHref = getInstanceTabHref(appInstanceId, 'overview') const input = { params: { appInstanceId } } const instanceQuery = useQuery(consoleQuery.enterprise.appInstanceService.getAppInstance.queryOptions({ input, enabled: Boolean(appInstanceId), })) const accessChannelsQuery = useQuery(consoleQuery.enterprise.accessService.getAccessChannels.queryOptions({ input, enabled: Boolean(appInstanceId), })) const releaseHistoryQuery = useQuery(consoleQuery.enterprise.releaseService.listReleases.queryOptions({ input: { ...input, query: { pageNumber: 1, resultsPerPage: CARD_RELEASE_QUERY_PAGE_SIZE, }, }, enabled: Boolean(appInstanceId), })) const environmentDeploymentsQuery = useQuery(consoleQuery.enterprise.deploymentService.listEnvironmentDeployments.queryOptions({ input, enabled: Boolean(appInstanceId), refetchInterval: query => deploymentStatusPollingInterval(query.state.data), })) if (!app.id) return null const description = (instanceQuery.data?.appInstance?.description ?? app.description)?.trim() const access = accessChannelsQuery.data?.accessChannels const releaseRows = releaseHistoryQuery.data?.data?.filter((release): release is Release & { id: string } => Boolean(release.id)) ?? [] const hasRelease = releaseRows.length > 0 const activeDeploymentRows = environmentDeploymentsQuery.data?.data?.filter(isActiveDeployment) ?? [] const latestRelease = pickLatestRelease(releaseRows) const latestReleaseTime = latestRelease?.createdAt const latestReleaseTimeMs = latestReleaseTime ? Date.parse(latestReleaseTime) : Number.NaN const latestReleaseDeployed = isReleaseDeployed(latestRelease, activeDeploymentRows) const releaseMeta = latestRelease ? [ releaseLabel(latestRelease), Number.isNaN(latestReleaseTimeMs) ? undefined : formatTimeFromNow(latestReleaseTimeMs), ].filter(Boolean).join(' · ') : t('card.notDeployed') const releaseHistoryIsLoading = releaseHistoryQuery.isLoading const statusIsLoading = environmentDeploymentsQuery.isLoading || (!activeDeploymentRows.length && releaseHistoryQuery.isLoading) const statusHasError = environmentDeploymentsQuery.isError || releaseHistoryQuery.isError const showDeployAction = !statusIsLoading && !statusHasError && hasRelease && activeDeploymentRows.length === 0 const showFooterCreateReleaseAction = !releaseHistoryIsLoading && !statusIsLoading && !statusHasError && !hasRelease return (

{appName}

{instanceQuery.isLoading ? (
) : ( description ? (

{description}

) :
)}
openDeployDrawer({ appInstanceId })} > {t('card.menu.deploy')} ) : undefined} />
{showFooterCreateReleaseAction ? (
) : } {releaseMeta}
) }