This commit is contained in:
Stephen Zhou 2026-05-08 16:42:18 +08:00
parent e7e6ccd11a
commit a223869a23
No known key found for this signature in database
9 changed files with 382 additions and 317 deletions

View File

@ -1,74 +1,17 @@
'use client'
import type {
ConsoleEnvironment,
EnvironmentAccessRow,
} from '@dify/contracts/enterprise/types.gen'
import { useQuery } from '@tanstack/react-query'
import { consoleQuery } from '@/service/client'
import {
deployedRows,
} from '../utils'
import { AccessChannelsSection } from './access-tab/channels-section'
import { DeveloperApiSection } from './access-tab/developer-api-section'
import { AccessPermissionsSection } from './access-tab/permissions-section'
import { getUrlOrigin } from './access-tab/url'
const EMPTY_ACCESS_PERMISSIONS: EnvironmentAccessRow[] = []
function uniqueEnvironments(environments: (ConsoleEnvironment | undefined)[]) {
return environments.filter((environment, index): environment is ConsoleEnvironment => {
if (!environment?.id)
return false
return environments.findIndex(candidate => candidate?.id === environment.id) === index
})
}
export function AccessTab({ instanceId: appId }: {
instanceId: string
}) {
const appInput = { params: { appInstanceId: appId } }
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input: appInput,
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
const deploymentRows = deployedRows(environmentDeployments?.data)
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
const deployedEnvs = uniqueEnvironments([
...deploymentRows.map(row => row.environment),
...policies.map(policy => policy.environment),
...(accessConfig?.accessChannels?.webappRows?.map(row => row.environment) ?? []),
])
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
const webappRows = accessConfig?.accessChannels?.webappRows?.filter(row => row.url) ?? []
const runEnabled = accessConfig?.accessChannels?.enabled ?? false
const cliDomain = getUrlOrigin(accessConfig?.accessChannels?.cli?.url)
const cliDocsUrl = cliDomain ? `${cliDomain}/cli` : undefined
return (
<div className="flex w-full max-w-[960px] flex-col gap-5 p-6">
<AccessPermissionsSection
appId={appId}
environments={deployedEnvs}
policies={policies}
/>
<AccessChannelsSection
appId={appId}
runEnabled={runEnabled}
webappRows={webappRows}
cliDomain={cliDomain}
cliDocsUrl={cliDocsUrl}
/>
<DeveloperApiSection
appId={appId}
apiEnabled={apiEnabled}
apiUrl={accessConfig?.developerApi?.apiUrl}
environments={deployedEnvs}
apiKeys={apiKeys}
/>
<AccessPermissionsSection appId={appId} />
<AccessChannelsSection appId={appId} />
<DeveloperApiSection appId={appId} />
</div>
)
}

View File

@ -1,19 +1,15 @@
'use client'
import type { WebAppAccessRow } from '@dify/contracts/enterprise/types.gen'
import { Switch } from '@langgenius/dify-ui/switch'
import { useMutation } from '@tanstack/react-query'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { environmentName, webappUrl } from '../../utils'
import { CopyPill, EndpointRow, Section } from './common'
import { getUrlOrigin } from './url'
type AccessChannelsSectionProps = {
appId: string
runEnabled: boolean
webappRows: WebAppAccessRow[]
cliDomain?: string
cliDocsUrl?: string
}
function AccessChannelsSwitch({ appId, checked }: {
@ -37,12 +33,17 @@ function AccessChannelsSwitch({ appId, checked }: {
export function AccessChannelsSection({
appId,
runEnabled,
webappRows,
cliDomain,
cliDocsUrl,
}: AccessChannelsSectionProps) {
const { t } = useTranslation('deployments')
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input: {
params: { appInstanceId: appId },
},
}))
const runEnabled = accessConfig?.accessChannels?.enabled ?? false
const webappRows = accessConfig?.accessChannels?.webappRows?.filter(row => row.url) ?? []
const cliDomain = getUrlOrigin(accessConfig?.accessChannels?.cli?.url)
const cliDocsUrl = cliDomain ? `${cliDomain}/cli` : undefined
return (
<Section

View File

@ -1,6 +1,5 @@
'use client'
import type { ConsoleEnvironment, DeveloperApiKeyRow } from '@dify/contracts/enterprise/types.gen'
import { Switch } from '@langgenius/dify-ui/switch'
import { useMutation } from '@tanstack/react-query'
import { useAtomValue, useSetAtom } from 'jotai'
@ -9,13 +8,10 @@ import { consoleQuery } from '@/service/client'
import { createdDeveloperApiTokenAtom } from '../../store'
import { ApiKeyGenerateMenu, ApiKeyList } from './api-keys'
import { CopyPill, Section } from './common'
import { useAccessEnvironmentScope } from './use-access-environment-scope'
type DeveloperApiSectionProps = {
appId: string
apiEnabled: boolean
apiUrl?: string
environments: ConsoleEnvironment[]
apiKeys: DeveloperApiKeyRow[]
}
function DeveloperApiSwitch({ appId, checked }: {
@ -80,12 +76,12 @@ function CreatedApiTokenCard({ appId }: {
export function DeveloperApiSection({
appId,
apiEnabled,
apiUrl,
environments,
apiKeys,
}: DeveloperApiSectionProps) {
const { t } = useTranslation('deployments')
const { accessConfig, environments } = useAccessEnvironmentScope(appId)
const apiEnabled = accessConfig?.developerApi?.enabled ?? false
const apiUrl = accessConfig?.developerApi?.apiUrl
const apiKeys = accessConfig?.developerApi?.apiKeys ?? []
return (
<Section

View File

@ -1,22 +1,19 @@
'use client'
import type { ConsoleEnvironment, EnvironmentAccessRow } from '@dify/contracts/enterprise/types.gen'
import { useTranslation } from 'react-i18next'
import { Section } from './common'
import { EnvironmentPermissionRow } from './permissions'
import { useAccessEnvironmentScope } from './use-access-environment-scope'
type AccessPermissionsSectionProps = {
appId: string
environments: ConsoleEnvironment[]
policies: EnvironmentAccessRow[]
}
export function AccessPermissionsSection({
appId,
environments,
policies,
}: AccessPermissionsSectionProps) {
const { t } = useTranslation('deployments')
const { environments, policies } = useAccessEnvironmentScope(appId)
return (
<Section

View File

@ -0,0 +1,40 @@
import type {
ConsoleEnvironment,
EnvironmentAccessRow,
} from '@dify/contracts/enterprise/types.gen'
import { useQuery } from '@tanstack/react-query'
import { consoleQuery } from '@/service/client'
import { deployedRows } from '../../utils'
const EMPTY_ACCESS_PERMISSIONS: EnvironmentAccessRow[] = []
function uniqueEnvironments(environments: (ConsoleEnvironment | undefined)[]) {
return environments.filter((environment, index): environment is ConsoleEnvironment => {
if (!environment?.id)
return false
return environments.findIndex(candidate => candidate?.id === environment.id) === index
})
}
export function useAccessEnvironmentScope(appId: string) {
const appInput = { params: { appInstanceId: appId } }
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input: appInput,
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
const deploymentRows = deployedRows(environmentDeployments?.data)
const policies = accessConfig?.permissions ?? EMPTY_ACCESS_PERMISSIONS
const environments = uniqueEnvironments([
...deploymentRows.map(row => row.environment),
...policies.map(policy => policy.environment),
...(accessConfig?.accessChannels?.webappRows?.map(row => row.environment) ?? []),
])
return {
accessConfig,
environments,
policies,
}
}

View File

@ -101,12 +101,41 @@ function DeployFromOverviewButton({ appId }: {
)
}
export function OverviewTab({ instanceId }: {
instanceId: string
function BasicInfoSection({ appId }: {
appId: string
}) {
const { t } = useTranslation('deployments')
const { t: tCommon } = useTranslation()
const input = { params: { appInstanceId: instanceId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input: {
params: { appInstanceId: appId },
},
}))
const overviewApp = overview?.instance
if (!overviewApp?.id)
return null
const appName = overviewApp.name ?? overviewApp.id
const appModeLabel = getAppModeLabel(toAppMode(overviewApp.mode), tCommon)
return (
<Section title={t('overview.basicInfo')}>
<div className="flex flex-col divide-y divide-divider-subtle">
<InfoRow label={t('overview.name')} value={appName} />
<InfoRow label={t('overview.description')} value={overviewApp.description ?? t('overview.emptyValue')} />
<InfoRow label={t('overview.sourceApp')} value={overviewApp.sourceAppName ?? appName} />
<InfoRow label={t('overview.appMode')} value={appModeLabel} />
</div>
</Section>
)
}
function DeploymentStatusSection({ appId }: {
appId: string
}) {
const { t } = useTranslation('deployments')
const input = { params: { appInstanceId: appId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input,
}))
@ -119,9 +148,6 @@ export function OverviewTab({ instanceId }: {
},
},
}))
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input,
}))
const overviewApp = overview?.instance
const deployments = overview?.deployments?.filter(row => row.environment?.id && row.status?.toLowerCase() !== 'undeployed') ?? []
const releaseRows = releaseHistory?.data?.filter(row => row.id) ?? []
@ -130,112 +156,124 @@ export function OverviewTab({ instanceId }: {
if (!overviewApp?.id)
return null
const appId = overviewApp.id
const appName = overviewApp.name ?? appId
const appModeLabel = getAppModeLabel(toAppMode(overviewApp.mode), tCommon)
return (
<Section
title={t('overview.deploymentStatus')}
action={(
<Button nativeButton={false} size="small" variant="secondary" render={<Link href={`/deployments/${appId}/deploy`} />}>
{t('overview.viewDeployments')}
<span className="i-ri-arrow-right-up-line h-3.5 w-3.5" />
</Button>
)}
>
{deployments.length === 0
? (
<div className="flex flex-col items-center gap-3 rounded-lg border border-dashed border-components-panel-border bg-components-panel-bg-blur px-4 py-8 text-center">
<span className="i-ri-rocket-line h-5 w-5 text-text-quaternary" />
<div className="system-sm-regular text-text-tertiary">
{releaseRows.length === 0
? t(canCreateRelease ? 'overview.noReleaseYet' : 'overview.noReleaseSourceUnavailable')
: t('overview.notDeployedYet')}
</div>
{releaseRows.length === 0
? canCreateRelease
? (
<Button nativeButton={false} size="small" variant="primary" render={<Link href={`/deployments/${appId}/versions`} />}>
{t('overview.createRelease')}
</Button>
)
: (
<Button size="small" variant="primary" disabled>
{t('overview.createRelease')}
</Button>
)
: (
<DeployFromOverviewButton appId={appId} />
)}
</div>
)
: (
<div className="flex flex-col divide-y divide-divider-subtle">
{deployments.map((row) => {
const status = overviewDeploymentStatus(row.status)
return (
<div key={row.environment?.id} className="flex items-center justify-between gap-3 py-2">
<div className="flex min-w-0 flex-col">
<span className="system-sm-medium text-text-primary">{row.environment?.name || row.environment?.id}</span>
<span className="system-xs-regular text-text-tertiary">
{releaseLabel(row.release) || t('overview.emptyValue')}
</span>
</div>
<StatusBadge status={status} />
</div>
)
})}
</div>
)}
</Section>
)
}
function AccessStatusSection({ appId }: {
appId: string
}) {
const { t } = useTranslation('deployments')
const input = { params: { appInstanceId: appId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input,
}))
const { data: accessConfig } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceAccess.queryOptions({
input,
}))
const webappAccessUrl = webappUrl(overview?.access?.webappUrl)
const cliUrl = overview?.access?.cliUrl
const apiUrl = overview?.access?.apiUrl ?? accessConfig?.developerApi?.apiUrl
const apiKeysCount = overview?.access?.apiKeyCount ?? accessConfig?.developerApi?.apiKeys?.length ?? 0
return (
<Section
title={t('overview.accessStatus')}
action={(
<Button nativeButton={false} size="small" variant="secondary" render={<Link href={`/deployments/${appId}/access`} />}>
{t('overview.configureAccess')}
<span className="i-ri-arrow-right-up-line h-3.5 w-3.5" />
</Button>
)}
>
<div className="flex flex-col divide-y divide-divider-subtle">
<AccessOverviewRow
label={t('overview.webapp')}
enabled={overview?.access?.accessChannelsEnabled ?? false}
hint={webappAccessUrl || t('overview.notConfigured')}
/>
<AccessOverviewRow
label={t('overview.cli')}
enabled={overview?.access?.accessChannelsEnabled ?? false}
hint={cliUrl ?? t('overview.notConfigured')}
/>
<AccessOverviewRow
label={t('overview.api')}
enabled={overview?.access?.developerApiEnabled ?? false}
hint={overview?.access?.developerApiEnabled
? apiUrl || t('overview.notConfigured')
: t('overview.notConfigured')}
meta={overview?.access?.developerApiEnabled
? t('overview.apiKeysCount', { count: apiKeysCount })
: undefined}
/>
</div>
</Section>
)
}
export function OverviewTab({ instanceId: appId }: {
instanceId: string
}) {
return (
<div className="flex w-full max-w-[960px] flex-col gap-5 p-6">
<Section title={t('overview.basicInfo')}>
<div className="flex flex-col divide-y divide-divider-subtle">
<InfoRow label={t('overview.name')} value={appName} />
<InfoRow label={t('overview.description')} value={overviewApp.description ?? t('overview.emptyValue')} />
<InfoRow label={t('overview.sourceApp')} value={overviewApp.sourceAppName ?? appName} />
<InfoRow label={t('overview.appMode')} value={appModeLabel} />
</div>
</Section>
<Section
title={t('overview.deploymentStatus')}
action={(
<Button nativeButton={false} size="small" variant="secondary" render={<Link href={`/deployments/${appId}/deploy`} />}>
{t('overview.viewDeployments')}
<span className="i-ri-arrow-right-up-line h-3.5 w-3.5" />
</Button>
)}
>
{deployments.length === 0
? (
<div className="flex flex-col items-center gap-3 rounded-lg border border-dashed border-components-panel-border bg-components-panel-bg-blur px-4 py-8 text-center">
<span className="i-ri-rocket-line h-5 w-5 text-text-quaternary" />
<div className="system-sm-regular text-text-tertiary">
{releaseRows.length === 0
? t(canCreateRelease ? 'overview.noReleaseYet' : 'overview.noReleaseSourceUnavailable')
: t('overview.notDeployedYet')}
</div>
{releaseRows.length === 0
? canCreateRelease
? (
<Button nativeButton={false} size="small" variant="primary" render={<Link href={`/deployments/${appId}/versions`} />}>
{t('overview.createRelease')}
</Button>
)
: (
<Button size="small" variant="primary" disabled>
{t('overview.createRelease')}
</Button>
)
: (
<DeployFromOverviewButton appId={appId} />
)}
</div>
)
: (
<div className="flex flex-col divide-y divide-divider-subtle">
{deployments.map((row) => {
const status = overviewDeploymentStatus(row.status)
return (
<div key={row.environment?.id} className="flex items-center justify-between gap-3 py-2">
<div className="flex min-w-0 flex-col">
<span className="system-sm-medium text-text-primary">{row.environment?.name || row.environment?.id}</span>
<span className="system-xs-regular text-text-tertiary">
{releaseLabel(row.release) || t('overview.emptyValue')}
</span>
</div>
<StatusBadge status={status} />
</div>
)
})}
</div>
)}
</Section>
<Section
title={t('overview.accessStatus')}
action={(
<Button nativeButton={false} size="small" variant="secondary" render={<Link href={`/deployments/${appId}/access`} />}>
{t('overview.configureAccess')}
<span className="i-ri-arrow-right-up-line h-3.5 w-3.5" />
</Button>
)}
>
<div className="flex flex-col divide-y divide-divider-subtle">
<AccessOverviewRow
label={t('overview.webapp')}
enabled={overview?.access?.accessChannelsEnabled ?? false}
hint={webappAccessUrl || t('overview.notConfigured')}
/>
<AccessOverviewRow
label={t('overview.cli')}
enabled={overview?.access?.accessChannelsEnabled ?? false}
hint={cliUrl ?? t('overview.notConfigured')}
/>
<AccessOverviewRow
label={t('overview.api')}
enabled={overview?.access?.developerApiEnabled ?? false}
hint={overview?.access?.developerApiEnabled
? apiUrl || t('overview.notConfigured')
: t('overview.notConfigured')}
meta={overview?.access?.developerApiEnabled
? t('overview.apiKeysCount', { count: apiKeysCount })
: undefined}
/>
</div>
</Section>
<BasicInfoSection appId={appId} />
<DeploymentStatusSection appId={appId} />
<AccessStatusSection appId={appId} />
</div>
)
}

View File

@ -216,16 +216,13 @@ function SettingsForm({ app, settings }: SettingsFormProps) {
)
}
export function SettingsTab({ instanceId }: {
instanceId: string
function SettingsFormSection({ appId }: {
appId: string
}) {
const appInput = { params: { appInstanceId: instanceId } }
const appInput = { params: { appInstanceId: appId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input: appInput,
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
const app = overview?.instance
const settingsQuery = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceSettings.queryOptions({
input: appInput,
@ -234,22 +231,54 @@ export function SettingsTab({ instanceId }: {
if (!app?.id)
return null
const hasDeployments = deployedRows(environmentDeployments?.data).length > 0
const appName = app.name ?? app.id
const formKey = `${app.id}-${settingsQuery.data?.name ?? appName}-${settingsQuery.data?.description ?? app.description ?? ''}`
return (
<SettingsForm
key={formKey}
app={app}
settings={settingsQuery.data}
/>
)
}
function DeleteInstanceControlSection({ appId }: {
appId: string
}) {
const appInput = { params: { appInstanceId: appId } }
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
input: appInput,
}))
const { data: environmentDeployments } = useQuery(consoleQuery.enterprise.appDeploy.listRuntimeInstances.queryOptions({
input: appInput,
}))
const settingsQuery = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceSettings.queryOptions({
input: appInput,
}))
const app = overview?.instance
if (!app?.id)
return null
const hasDeployments = deployedRows(environmentDeployments?.data).length > 0
return (
<DeleteInstanceControl
app={app}
settings={settingsQuery.data}
hasDeployments={hasDeployments}
/>
)
}
export function SettingsTab({ instanceId: appId }: {
instanceId: string
}) {
return (
<div className="flex max-w-[640px] flex-col gap-5 p-6">
<SettingsForm
key={formKey}
app={app}
settings={settingsQuery.data}
/>
<DeleteInstanceControl
app={app}
settings={settingsQuery.data}
hasDeployments={hasDeployments}
/>
<SettingsFormSection appId={appId} />
<DeleteInstanceControlSection appId={appId} />
</div>
)
}

View File

@ -1,26 +1,15 @@
'use client'
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'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { consoleQuery } from '@/service/client'
import { DEPLOYMENT_PAGE_SIZE } from '../data'
import {
deployedRows,
formatDate,
releaseCommit,
releaseLabel,
} from '../utils'
import { DeployReleaseMenu } from './versions-tab/deploy-release-menu'
import { DeployedToBadge } from './versions-tab/deployed-to-badge'
import { getReleaseDeployments } from './versions-tab/release-deployments'
const GRID_TEMPLATE = 'grid-cols-[minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1.5fr)_96px]'
import { deployedRows } from '../utils'
import { ReleaseHistoryTable } from './versions-tab/release-history-table'
function CreateReleaseControl({ appId, canCreateRelease }: {
appId: string
@ -212,104 +201,11 @@ export function VersionsTab({ instanceId: appId }: {
</div>
)
: (
<div className="overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg">
<div className={cn(
'hidden items-center gap-4 border-b border-divider-subtle px-4 py-3 system-xs-medium-uppercase text-text-tertiary pc:grid',
GRID_TEMPLATE,
)}
>
<div>{t('versions.col.release')}</div>
<div>{t('versions.col.createdAt')}</div>
<div>{t('versions.col.author')}</div>
<div>{t('versions.col.deployedTo')}</div>
<div className="text-right">{t('versions.col.action')}</div>
</div>
{releaseRows.map((row) => {
const release = row
const releaseDeployments = getReleaseDeployments(row, deploymentRows)
return (
<div key={release.id} className="border-b border-divider-subtle last:border-b-0">
<div className="flex flex-col gap-3 p-4 pc:hidden">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<Tooltip>
<TooltipTrigger
render={(
<span className="inline-flex max-w-full cursor-default truncate font-mono system-sm-medium text-text-primary">
{releaseLabel(release)}
</span>
)}
/>
<TooltipContent>
{t('versions.commitTooltip', { commit: releaseCommit(release) })}
</TooltipContent>
</Tooltip>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 system-xs-regular text-text-secondary">
<span>{formatDate(release.createdAt)}</span>
<span aria-hidden>·</span>
<span>{row.createdBy?.name ?? '—'}</span>
</div>
</div>
<div className="flex shrink-0 justify-end gap-1">
<DeployReleaseMenu releaseId={release.id!} appInstanceId={appId} />
</div>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<div className="shrink-0 system-xs-medium-uppercase text-text-tertiary">
{t('versions.col.deployedTo')}
</div>
<div className="flex min-w-0 flex-wrap gap-1">
{releaseDeployments.length === 0
? <span className="system-sm-regular text-text-quaternary"></span>
: releaseDeployments.map(item => (
<DeployedToBadge
key={`${item.environmentId}-${item.state}`}
item={item}
/>
))}
</div>
</div>
</div>
<div className={cn(
'hidden items-center gap-4 px-4 py-3 pc:grid',
GRID_TEMPLATE,
)}
>
<div>
<Tooltip>
<TooltipTrigger
render={(
<span className="inline-flex cursor-default font-mono system-sm-medium text-text-primary">
{releaseLabel(release)}
</span>
)}
/>
<TooltipContent>
{t('versions.commitTooltip', { commit: releaseCommit(release) })}
</TooltipContent>
</Tooltip>
</div>
<div className="system-sm-regular text-text-secondary">{formatDate(release.createdAt)}</div>
<div className="system-sm-regular text-text-secondary">{row.createdBy?.name ?? '—'}</div>
<div className="flex flex-wrap gap-1">
{releaseDeployments.length === 0
? <span className="system-sm-regular text-text-quaternary"></span>
: releaseDeployments.map(item => (
<DeployedToBadge
key={`${item.environmentId}-${item.state}`}
item={item}
/>
))}
</div>
<div className="flex justify-end gap-1">
<DeployReleaseMenu releaseId={release.id!} appInstanceId={appId} />
</div>
</div>
</div>
)
})}
</div>
<ReleaseHistoryTable
appId={appId}
releaseRows={releaseRows}
deploymentRows={deploymentRows}
/>
)}
</div>
)

View File

@ -0,0 +1,125 @@
'use client'
import type { ReleaseRow, RuntimeInstanceRow } from '@dify/contracts/enterprise/types.gen'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useTranslation } from 'react-i18next'
import {
formatDate,
releaseCommit,
releaseLabel,
} from '../../utils'
import { DeployReleaseMenu } from './deploy-release-menu'
import { DeployedToBadge } from './deployed-to-badge'
import { getReleaseDeployments } from './release-deployments'
const GRID_TEMPLATE = 'grid-cols-[minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1.5fr)_96px]'
export function ReleaseHistoryTable({ appId, releaseRows, deploymentRows }: {
appId: string
releaseRows: ReleaseRow[]
deploymentRows: RuntimeInstanceRow[]
}) {
const { t } = useTranslation('deployments')
return (
<div className="overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg">
<div className={cn(
'hidden items-center gap-4 border-b border-divider-subtle px-4 py-3 system-xs-medium-uppercase text-text-tertiary pc:grid',
GRID_TEMPLATE,
)}
>
<div>{t('versions.col.release')}</div>
<div>{t('versions.col.createdAt')}</div>
<div>{t('versions.col.author')}</div>
<div>{t('versions.col.deployedTo')}</div>
<div className="text-right">{t('versions.col.action')}</div>
</div>
{releaseRows.map((row) => {
const release = row
const releaseDeployments = getReleaseDeployments(row, deploymentRows)
return (
<div key={release.id} className="border-b border-divider-subtle last:border-b-0">
<div className="flex flex-col gap-3 p-4 pc:hidden">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<Tooltip>
<TooltipTrigger
render={(
<span className="inline-flex max-w-full cursor-default truncate font-mono system-sm-medium text-text-primary">
{releaseLabel(release)}
</span>
)}
/>
<TooltipContent>
{t('versions.commitTooltip', { commit: releaseCommit(release) })}
</TooltipContent>
</Tooltip>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 system-xs-regular text-text-secondary">
<span>{formatDate(release.createdAt)}</span>
<span aria-hidden>·</span>
<span>{row.createdBy?.name ?? '—'}</span>
</div>
</div>
<div className="flex shrink-0 justify-end gap-1">
<DeployReleaseMenu releaseId={release.id!} appInstanceId={appId} />
</div>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<div className="shrink-0 system-xs-medium-uppercase text-text-tertiary">
{t('versions.col.deployedTo')}
</div>
<div className="flex min-w-0 flex-wrap gap-1">
{releaseDeployments.length === 0
? <span className="system-sm-regular text-text-quaternary"></span>
: releaseDeployments.map(item => (
<DeployedToBadge
key={`${item.environmentId}-${item.state}`}
item={item}
/>
))}
</div>
</div>
</div>
<div className={cn(
'hidden items-center gap-4 px-4 py-3 pc:grid',
GRID_TEMPLATE,
)}
>
<div>
<Tooltip>
<TooltipTrigger
render={(
<span className="inline-flex cursor-default font-mono system-sm-medium text-text-primary">
{releaseLabel(release)}
</span>
)}
/>
<TooltipContent>
{t('versions.commitTooltip', { commit: releaseCommit(release) })}
</TooltipContent>
</Tooltip>
</div>
<div className="system-sm-regular text-text-secondary">{formatDate(release.createdAt)}</div>
<div className="system-sm-regular text-text-secondary">{row.createdBy?.name ?? '—'}</div>
<div className="flex flex-wrap gap-1">
{releaseDeployments.length === 0
? <span className="system-sm-regular text-text-quaternary"></span>
: releaseDeployments.map(item => (
<DeployedToBadge
key={`${item.environmentId}-${item.state}`}
item={item}
/>
))}
</div>
<div className="flex justify-end gap-1">
<DeployReleaseMenu releaseId={release.id!} appInstanceId={appId} />
</div>
</div>
</div>
)
})}
</div>
)
}