dify/web/features/deployments/detail/overview/release-summary/release-hero.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

189 lines
6.6 KiB
TypeScript

'use client'
import type { Release } from '@dify/contracts/enterprise/types.gen'
import type { ReactNode } from 'react'
import { ReleaseSource } from '@dify/contracts/enterprise/types.gen'
import { cn } from '@langgenius/dify-ui/cn'
import { useQuery } from '@tanstack/react-query'
import { useAtomValue } 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 { CreateReleaseControl } from '../../../create-release'
import { deploymentRouteAppInstanceIdAtom } from '../../../route-state'
import { DeploymentEmptyState } from '../../../shared/components/empty-state'
import { TitleTooltip } from '../../../shared/components/title-tooltip'
import { formatDate, releaseCommit } from '../../../shared/domain/release'
type ReleaseHeroProps = {
latestRelease?: Release
releaseCount: number
}
type ReleaseMetaItemProps = {
label?: string
showSeparator?: boolean
children: ReactNode
}
const OVERVIEW_CARD_CLASS_NAME =
'rounded-xl border border-components-panel-border bg-components-panel-bg p-4'
const OVERVIEW_ICON_CLASS_NAME =
'flex size-8 shrink-0 items-center justify-center rounded-lg bg-background-section-burn text-text-tertiary'
export function ReleaseHero({ latestRelease, releaseCount }: ReleaseHeroProps) {
const { t } = useTranslation('deployments')
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
const { formatTimeFromNow } = useFormatTimeFromNow()
if (!latestRelease) {
return (
<DeploymentEmptyState
variant="section"
icon="i-ri-stack-line"
title={t(($) => $['overview.hero.empty'])}
description={t(($) => $['overview.hero.emptyDescription'])}
action={
appInstanceId ? (
<CreateReleaseControl appInstanceId={appInstanceId} size="medium" />
) : undefined
}
className="min-h-44"
/>
)
}
const author = latestRelease.createdBy.displayName
const ago = formatTimeFromNow(new Date(latestRelease.createdAt).getTime())
const createdAtTitle = formatDate(latestRelease.createdAt)
const commit = releaseCommit(latestRelease)
return (
<div
className={cn(
OVERVIEW_CARD_CLASS_NAME,
'flex min-w-0 flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6',
)}
>
<div className="flex min-w-0 items-center gap-3">
<span aria-hidden className={OVERVIEW_ICON_CLASS_NAME}>
<span className="i-ri-stack-fill size-4" />
</span>
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex min-w-0 items-center gap-2">
<h4 className="truncate system-sm-semibold text-text-primary">
{latestRelease.displayName}
</h4>
{commit !== '—' && (
<TitleTooltip content={t(($) => $['versions.commitTooltip'], { commit })}>
<span className="shrink-0 rounded bg-background-section-burn px-1.5 py-0.5 font-mono system-xs-regular text-text-tertiary">
{commit}
</span>
</TitleTooltip>
)}
</div>
<p className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 system-xs-regular text-text-tertiary">
<ReleaseMetaItem label={t(($) => $['versions.col.sourceApp'])} showSeparator={false}>
<LatestReleaseSource release={latestRelease} />
</ReleaseMetaItem>
{author && (
<ReleaseMetaItem>
{t(($) => $['overview.hero.byName'], { name: author })}
</ReleaseMetaItem>
)}
{ago && (
<ReleaseMetaItem>
<TitleTooltip content={createdAtTitle}>
<span>{ago}</span>
</TitleTooltip>
</ReleaseMetaItem>
)}
<ReleaseMetaItem>
{t(($) => $['overview.latestRelease.releaseCount'], { count: releaseCount })}
</ReleaseMetaItem>
</p>
</div>
</div>
</div>
)
}
function ReleaseMetaItem({ label, showSeparator = true, children }: ReleaseMetaItemProps) {
return (
<span className="inline-flex min-w-0 items-center gap-1.5">
{showSeparator && (
<span aria-hidden className="text-text-quaternary">
·
</span>
)}
{label && <span className="shrink-0 text-text-quaternary">{label}</span>}
<span className="min-w-0 truncate">{children}</span>
</span>
)
}
function LatestReleaseSource({ release }: { release: Release }) {
const { t } = useTranslation('deployments')
const sourceAppId = release.sourceAppId
if (!sourceAppId) {
return (
<span>
{release.source === ReleaseSource.RELEASE_SOURCE_UPLOAD
? t(($) => $['versions.manualDslOption'])
: '—'}
</span>
)
}
return <LatestReleaseSourceLink sourceAppId={sourceAppId} />
}
function LatestReleaseSourceLink({ sourceAppId }: { sourceAppId: string }) {
const sourceAppQuery = useQuery(
consoleQuery.apps.byAppId.get.queryOptions({
input: {
params: { app_id: sourceAppId },
},
}),
)
const sourceAppName = sourceAppQuery.data?.name
const label = sourceAppName || sourceAppId
const title = sourceAppName ? `${sourceAppName} (${sourceAppId})` : sourceAppId
return (
<TitleTooltip content={title}>
<Link
href={`/app/${encodeURIComponent(sourceAppId)}/workflow`}
target="_blank"
rel="noreferrer"
className="inline-flex max-w-full min-w-0 items-center gap-1 text-text-secondary transition-colors hover:text-text-accent"
>
<span className="min-w-0 truncate">{label}</span>
<span className="i-ri-arrow-right-up-line size-3.5 shrink-0" aria-hidden="true" />
</Link>
</TitleTooltip>
)
}
export function ReleaseHeroSkeleton() {
return (
<div
data-slot="deployment-overview-release-hero-skeleton"
className={cn(OVERVIEW_CARD_CLASS_NAME, 'flex min-w-0 items-start gap-3')}
>
<SkeletonRectangle className="my-0 size-8 shrink-0 animate-pulse rounded-lg" />
<div className="flex min-w-0 flex-col gap-2">
<SkeletonRectangle className="my-0 h-4 w-40 animate-pulse" />
<div className="flex min-w-0 flex-wrap items-baseline gap-x-1.5 gap-y-1">
<SkeletonRectangle className="my-0 h-3 w-32 animate-pulse" />
<SkeletonRectangle className="my-0 h-3 w-14 animate-pulse" />
<SkeletonRectangle className="my-0 h-3 w-28 animate-pulse" />
</div>
</div>
</div>
)
}