This commit is contained in:
Stephen Zhou
2026-05-15 13:47:18 +08:00
parent 142d46fd03
commit 12faa2aff9
3 changed files with 106 additions and 153 deletions

View File

@ -2,11 +2,11 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import { SectionState } from './common'
import { EnvironmentStrip, EnvironmentStripSkeleton } from './overview-tab/environment-strip'
import { computeOverviewStats } from './overview-tab/overview-drift'
import { PreviousReleases } from './overview-tab/previous-releases'
import { ReleaseHero, ReleaseHeroSkeleton } from './overview-tab/release-hero'
import { useSourceAppAvailability } from './source-app-availability'
@ -41,6 +41,33 @@ function SourceAppDeletedNotice() {
)
}
function ReleaseOverviewSection({ appInstanceId, children }: {
appInstanceId: string
children: React.ReactNode
}) {
const { t } = useTranslation('deployments')
return (
<section className="flex min-w-0 flex-col gap-3">
<div className="flex min-w-0 items-baseline justify-between gap-3">
<h3 className="system-sm-semibold text-text-primary">
{t('overview.recentReleases')}
</h3>
<Link
href={`/deployments/${appInstanceId}/releases`}
className="inline-flex shrink-0 items-center gap-1 system-xs-medium text-text-tertiary transition-colors hover:text-text-secondary"
>
{t('overview.previousReleases.viewAll')}
<span aria-hidden className="i-ri-arrow-right-line size-3.5" />
</Link>
</div>
<div className="flex min-w-0 flex-col gap-3">
{children}
</div>
</section>
)
}
export function OverviewTab({ appInstanceId }: {
appInstanceId: string
}) {
@ -60,7 +87,9 @@ export function OverviewTab({ appInstanceId }: {
if (overviewQuery.isLoading) {
return (
<OverviewLayout>
<ReleaseHeroSkeleton />
<ReleaseOverviewSection appInstanceId={appInstanceId}>
<ReleaseHeroSkeleton />
</ReleaseOverviewSection>
</OverviewLayout>
)
}
@ -84,7 +113,10 @@ export function OverviewTab({ appInstanceId }: {
if (releasesQuery.isLoading) {
return (
<OverviewLayout>
<ReleaseHeroSkeleton />
{sourceAppAvailability.sourceAppUnavailable && <SourceAppDeletedNotice />}
<ReleaseOverviewSection appInstanceId={appInstanceId}>
<ReleaseHeroSkeleton />
</ReleaseOverviewSection>
<EnvironmentStripSkeleton />
</OverviewLayout>
)
@ -105,25 +137,24 @@ export function OverviewTab({ appInstanceId }: {
return (
<OverviewLayout>
<ReleaseHero
appInstanceId={appInstanceId}
latestRelease={latestRelease}
stats={stats}
/>
{sourceAppAvailability.sourceAppUnavailable && <SourceAppDeletedNotice />}
<EnvironmentStrip
appInstanceId={appInstanceId}
rows={runtimeRows}
releaseRows={releaseRows}
stats={stats}
isLoading={runtimeInstancesQuery.isLoading}
isError={runtimeInstancesQuery.isError}
/>
<PreviousReleases
appInstanceId={appInstanceId}
releaseRows={releaseRows}
stats={stats}
/>
<div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(320px,420px)] xl:items-start">
<ReleaseOverviewSection appInstanceId={appInstanceId}>
<ReleaseHero
appInstanceId={appInstanceId}
latestRelease={latestRelease}
stats={stats}
/>
</ReleaseOverviewSection>
<EnvironmentStrip
appInstanceId={appInstanceId}
rows={runtimeRows}
releaseRows={releaseRows}
stats={stats}
isLoading={runtimeInstancesQuery.isLoading}
isError={runtimeInstancesQuery.isError}
/>
</div>
</OverviewLayout>
)
}

View File

@ -1,86 +0,0 @@
'use client'
import type { ReleaseRow } from '@dify/contracts/enterprise/types.gen'
import type { OverviewStats } from './overview-drift'
import { useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import Link from '@/next/link'
import { formatDate, releaseLabel } from '../../release'
import { openDeployDrawerAtom } from '../../store'
type PreviousReleasesProps = {
appInstanceId: string
releaseRows: ReleaseRow[]
stats: OverviewStats
}
const PREVIOUS_RELEASES_LIMIT = 5
export function PreviousReleases({ appInstanceId, releaseRows, stats }: PreviousReleasesProps) {
const { t } = useTranslation('deployments')
const { formatTimeFromNow } = useFormatTimeFromNow()
const openDeployDrawer = useSetAtom(openDeployDrawerAtom)
const previous = releaseRows.slice(1, 1 + PREVIOUS_RELEASES_LIMIT).filter(row => row.id)
if (previous.length === 0)
return null
return (
<section className="flex flex-col gap-2">
<div className="flex min-w-0 items-baseline justify-between gap-3">
<h3 className="system-sm-semibold text-text-primary">
{t('overview.previousReleases.title')}
</h3>
<Link
href={`/deployments/${appInstanceId}/releases`}
className="inline-flex shrink-0 items-center gap-1 system-xs-medium text-text-tertiary transition-colors hover:text-text-secondary"
>
{t('overview.previousReleases.viewAll')}
<span aria-hidden className="i-ri-arrow-right-line size-3.5" />
</Link>
</div>
<ul className="divide-y divide-divider-subtle rounded-xl border border-components-panel-border bg-components-panel-bg">
{previous.map((row) => {
const author = row.createdBy?.name ?? ''
const ago = row.createdAt ? formatTimeFromNow(new Date(row.createdAt).getTime()) : ''
const deployedCount = row.deployedTo?.length ?? 0
const propagation = stats.total === 0
? t('overview.hero.untargeted')
: deployedCount === 0
? t('overview.previousReleases.retired')
: t('overview.hero.propagation', { count: deployedCount, total: stats.total })
return (
<li key={row.id}>
<button
type="button"
onClick={() => openDeployDrawer({ appInstanceId, releaseId: row.id })}
className="group flex w-full min-w-0 items-center gap-4 px-4 py-3 text-left transition-colors first:rounded-t-xl last:rounded-b-xl hover:bg-state-base-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-components-button-primary-bg"
>
<span className="min-w-0 shrink-0 truncate font-mono system-sm-semibold text-text-primary">
{releaseLabel(row)}
</span>
<span
className="min-w-0 grow truncate system-xs-regular text-text-tertiary"
title={row.createdAt ? formatDate(row.createdAt) : undefined}
>
{[author && t('overview.hero.byName', { name: author }), ago].filter(Boolean).join(' · ')}
</span>
<span className="shrink-0 system-xs-regular text-text-tertiary">
{propagation}
</span>
<span
aria-hidden
className="i-ri-arrow-right-line size-4 shrink-0 text-text-quaternary transition-colors group-hover:text-text-tertiary"
/>
</button>
</li>
)
})}
</ul>
</section>
)
}

View File

@ -15,68 +15,76 @@ type ReleaseHeroProps = {
}
export function ReleaseHero({ appInstanceId, latestRelease, stats }: ReleaseHeroProps) {
const { t } = useTranslation('deployments')
const { t, i18n } = useTranslation('deployments')
const { formatTimeFromNow } = useFormatTimeFromNow()
const hasRelease = Boolean(latestRelease?.id)
const author = latestRelease?.createdBy?.name ?? ''
const ago = latestRelease?.createdAt ? formatTimeFromNow(new Date(latestRelease.createdAt).getTime()) : ''
const deployedEnvironmentNames = Array.from(new Set(
latestRelease?.deployedTo
?.map(item => item.environmentName || item.environmentId)
.filter((name): name is string => Boolean(name)) ?? [],
))
const deployedTargets = deployedEnvironmentNames.join(i18n.language.startsWith('zh') ? '、' : ', ')
const metaParts: { key: string, value: string }[] = []
if (author)
metaParts.push({ key: 'author', value: t('overview.hero.byName', { name: author }) })
if (ago)
metaParts.push({ key: 'ago', value: ago })
if (stats.total > 0)
metaParts.push({ key: 'propagation', value: t('overview.hero.propagation', { count: stats.ready, total: stats.total }) })
else if (hasRelease)
if (deployedTargets)
metaParts.push({ key: 'deployedTo', value: `${t('versions.col.deployedTo')} ${deployedTargets}` })
else if (hasRelease && stats.total === 0)
metaParts.push({ key: 'untargeted', value: t('overview.hero.untargeted') })
return (
<div className="flex flex-col gap-4 rounded-xl border border-components-panel-border bg-components-panel-bg p-5 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
<div className="flex min-w-0 flex-col gap-2">
{hasRelease
? (
<>
<div className="flex min-w-0 items-center gap-3">
<span
aria-hidden
className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-util-colors-blue-blue-50 text-util-colors-blue-blue-700"
>
<span className="i-ri-stack-fill size-5" />
</span>
<h2 className="truncate font-mono text-2xl font-semibold text-text-primary">
{releaseLabel(latestRelease)}
<div className="overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg">
<div className="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
<div className="flex min-w-0 flex-col gap-2">
{hasRelease
? (
<>
<div className="flex min-w-0 items-center gap-3">
<span
aria-hidden
className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-util-colors-blue-blue-50 text-util-colors-blue-blue-700"
>
<span className="i-ri-stack-fill size-5" />
</span>
<h2 className="truncate font-mono text-2xl font-semibold text-text-primary">
{releaseLabel(latestRelease)}
</h2>
</div>
{metaParts.length > 0 && (
<p
className="flex min-w-0 flex-wrap items-baseline gap-x-1.5 gap-y-1 system-sm-regular text-text-tertiary"
title={latestRelease?.createdAt ? formatDate(latestRelease.createdAt) : undefined}
>
{metaParts.map((part, index) => (
<span key={part.key} className="inline-flex items-baseline gap-1.5">
{index > 0 && <span aria-hidden className="text-text-quaternary">·</span>}
<span>{part.value}</span>
</span>
))}
</p>
)}
</>
)
: (
<>
<h2 className="system-xl-semibold text-text-primary">
{t('overview.hero.empty')}
</h2>
</div>
{metaParts.length > 0 && (
<p
className="flex min-w-0 flex-wrap items-baseline gap-x-1.5 gap-y-1 system-sm-regular text-text-tertiary"
title={latestRelease?.createdAt ? formatDate(latestRelease.createdAt) : undefined}
>
{metaParts.map((part, index) => (
<span key={part.key} className="inline-flex items-baseline gap-1.5">
{index > 0 && <span aria-hidden className="text-text-quaternary">·</span>}
<span>{part.value}</span>
</span>
))}
<p className="max-w-[640px] system-sm-regular text-text-tertiary">
{t('overview.hero.emptyDescription')}
</p>
)}
</>
)
: (
<>
<h2 className="system-xl-semibold text-text-primary">
{t('overview.hero.empty')}
</h2>
<p className="max-w-[640px] system-sm-regular text-text-tertiary">
{t('overview.hero.emptyDescription')}
</p>
</>
)}
</div>
<div className="shrink-0">
<CreateReleaseControl appInstanceId={appInstanceId} size="medium" />
</>
)}
</div>
<div className="shrink-0">
<CreateReleaseControl appInstanceId={appInstanceId} size="medium" />
</div>
</div>
</div>
)