mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 19:37:16 +08:00
tweaks
This commit is contained in:
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user