mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
refactor: align deployment detail state ownership (#38008)
This commit is contained in:
parent
a66b1de477
commit
113d6d7e00
@ -49,6 +49,7 @@ Use this as the component decision guide for Dify web. Existing code is referenc
|
||||
- Use uncontrolled `@langgenius/dify-ui/form` and `@langgenius/dify-ui/field` controls for edit/create forms whose fields are read only at submit time. Initialize query-backed defaults with `defaultValue` and keyed remounts.
|
||||
- Promote form state to atoms only when another component must react to in-progress values, a draft must survive unmount/remount in the scoped workflow, or multiple steps share the same editable draft before submit.
|
||||
- Treat `useParams`, route args, and `nuqs` query state as framework-owned state. When atom logic needs those values, hydrate primitive atoms at the route or surface boundary, such as with `useHydrateAtoms(..., { dangerouslyForceHydrate: true })`; keep URL updates in the route/query-state APIs instead of write atoms.
|
||||
- Within a route-owned feature, choose one source for route identity. If route params are bridged into feature atoms, use that bridge consistently for route-derived queries and actions instead of also threading the same route id through page, tab, and section props.
|
||||
- For async work tied to atom state, use `atomWithQuery` or `atomWithMutation`; write atoms should update only the inputs that drive those atoms. This applies to pure frontend async work as well as network requests, so do not hand-roll loading/error/in-flight state with `useState` or `useRef` for atom-orchestrated async behavior. For component-owned remote work, use `useQuery` or `useMutation` directly.
|
||||
- Row-local async state belongs to the row owner unless it participates in a shared Jotai workflow or needs atom-scoped reset semantics.
|
||||
- Leave query and mutation atoms unscoped so they keep shared QueryClient cache and invalidation behavior. Scope resettable primitives and explicit hydration tuples; scope a derived atom only when every dependency should be private to that surface.
|
||||
@ -88,11 +89,13 @@ Use this as the component decision guide for Dify web. Existing code is referenc
|
||||
|
||||
- Keep `web/contract/*` as the API shape source of truth and follow the `{ params, query?, body? }` input shape.
|
||||
- Consume generated queries with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`.
|
||||
- If a generated query input comes from an atom, including a route-identity bridge atom, keep the query in `atomWithQuery`; do not unwrap the atom in a component just to call `useQuery`.
|
||||
- Consume owner-local mutations with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))` when pending/error state is not consumed by feature atoms.
|
||||
- In `atomWithQuery`, `atomWithInfiniteQuery`, and `atomWithMutation`, return generated `queryOptions()`, `infiniteOptions()`, or `mutationOptions()` directly. Pass `enabled`, `retry`, `placeholderData`, `select`, and pagination options into the generated call instead of spreading options into a hand-built object.
|
||||
- For generated oRPC options with missing required input, branch the whole input with `input: condition ? validInput : skipToken` and `enabled: Boolean(condition)`. Never place `skipToken` inside a nested placeholder payload or coerce required IDs to `''`.
|
||||
- When prefetch and render use the same request, extract local query options or a query-options atom so `prefetchQuery` and `useQuery`/`atomWithQuery` share the exact options.
|
||||
- For custom query or mutation functions, wrap options with TanStack `queryOptions(...)` or `mutationOptions(...)`.
|
||||
- Do not extract generated `queryOptions(...)` into a helper solely to share input construction; extract only when prefetch/render must share exact options or the helper owns real domain behavior.
|
||||
- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename generated options. Keep feature hooks for real orchestration, workflow state, or shared domain behavior.
|
||||
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`. Component or atom callbacks may handle local toasts, closing dialogs, and navigation, but should not replace shared invalidation or patch shared server state locally.
|
||||
- For overlays that may open heavier secondary content, prefetch from the trigger/menu open event with `queryClient.prefetchQuery(queryOptions)` when `onOpenChange` is available. Do not mount hidden subscribers just to warm cache.
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { DeployTab } from '@/features/deployments/detail/deploy-tab'
|
||||
|
||||
export default async function InstanceDetailInstancesPage({ params }: {
|
||||
params: Promise<{ appInstanceId: string }>
|
||||
}) {
|
||||
const { appInstanceId } = await params
|
||||
return <DeployTab appInstanceId={appInstanceId} />
|
||||
export default function InstanceDetailInstancesPage() {
|
||||
return <DeployTab />
|
||||
}
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { InstanceDetail } from '@/features/deployments/detail'
|
||||
|
||||
export default async function InstanceDetailLayout({ children, params }: {
|
||||
export default function InstanceDetailLayout({ children }: {
|
||||
children: ReactNode
|
||||
params: Promise<{ appInstanceId: string }>
|
||||
}) {
|
||||
const { appInstanceId } = await params
|
||||
|
||||
return (
|
||||
<InstanceDetail appInstanceId={appInstanceId}>
|
||||
<InstanceDetail>
|
||||
{children}
|
||||
</InstanceDetail>
|
||||
)
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { OverviewTab } from '@/features/deployments/detail/overview-tab'
|
||||
|
||||
export default async function InstanceDetailOverviewPage({ params }: {
|
||||
params: Promise<{ appInstanceId: string }>
|
||||
}) {
|
||||
const { appInstanceId } = await params
|
||||
return <OverviewTab appInstanceId={appInstanceId} />
|
||||
export default function InstanceDetailOverviewPage() {
|
||||
return <OverviewTab />
|
||||
}
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { VersionsTab } from '@/features/deployments/detail/versions-tab'
|
||||
|
||||
export default async function InstanceDetailReleasesPage({ params }: {
|
||||
params: Promise<{ appInstanceId: string }>
|
||||
}) {
|
||||
const { appInstanceId } = await params
|
||||
return <VersionsTab appInstanceId={appInstanceId} />
|
||||
export default function InstanceDetailReleasesPage() {
|
||||
return <VersionsTab />
|
||||
}
|
||||
|
||||
@ -24,16 +24,6 @@ vi.mock('jotai-tanstack-query', () => ({
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
byAppId: {
|
||||
get: {
|
||||
queryOptions: (options: QueryOptions) => ({
|
||||
...options,
|
||||
queryKey: ['appById', options.input],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
enterprise: {
|
||||
appInstanceService: {
|
||||
getAppInstance: {
|
||||
@ -89,18 +79,13 @@ describe('deployment detail state', () => {
|
||||
enabled: false,
|
||||
input: skipToken,
|
||||
})
|
||||
expect(store.get(state.deploymentSourceAppQueryAtom)).toMatchObject({
|
||||
enabled: false,
|
||||
input: skipToken,
|
||||
})
|
||||
})
|
||||
|
||||
it('should build detail query inputs from route and source identities', async () => {
|
||||
it('should build detail query inputs from route identity', async () => {
|
||||
const state = await loadState()
|
||||
const store = createStore()
|
||||
|
||||
setDeploymentRoute(store)
|
||||
store.set(state.deploymentSourceAppIdAtom, 'source-app-1')
|
||||
|
||||
expect(store.get(state.deploymentDetailAppInstanceQueryAtom)).toMatchObject({
|
||||
enabled: true,
|
||||
@ -117,10 +102,5 @@ describe('deployment detail state', () => {
|
||||
input: { params: { appInstanceId: 'app-instance-1' } },
|
||||
})
|
||||
expect(environmentDeploymentsQuery.refetchInterval).toEqual(expect.any(Function))
|
||||
|
||||
expect(store.get(state.deploymentSourceAppQueryAtom)).toMatchObject({
|
||||
enabled: true,
|
||||
input: { params: { app_id: 'source-app-1' } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -16,12 +16,17 @@ vi.mock('jotai', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockToggleAccessChannel,
|
||||
}),
|
||||
}))
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockToggleAccessChannel,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
|
||||
@ -20,22 +20,23 @@ vi.mock('jotai', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useInfiniteQuery: () => ({
|
||||
data: { pages: [] },
|
||||
fetchNextPage: vi.fn(),
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
}),
|
||||
useQuery: () => ({
|
||||
data: undefined,
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useInfiniteQuery: () => ({
|
||||
data: { pages: [] },
|
||||
fetchNextPage: vi.fn(),
|
||||
isFetchingNextPage: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
useMutation: () => ({
|
||||
isPending: false,
|
||||
mutate: mockMutate,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
|
||||
@ -72,8 +72,7 @@ function CurrentReleaseMobileSummary({ release }: {
|
||||
)
|
||||
}
|
||||
|
||||
function DeploymentEnvironmentMobileRow({ appInstanceId, row }: {
|
||||
appInstanceId: string
|
||||
function DeploymentEnvironmentMobileRow({ row }: {
|
||||
row: EnvironmentDeployment
|
||||
}) {
|
||||
const envId = row.environment.id
|
||||
@ -88,15 +87,14 @@ function DeploymentEnvironmentMobileRow({ appInstanceId, row }: {
|
||||
</div>
|
||||
{!isUndeployedDeploymentRow(row) && <CurrentReleaseMobileSummary release={release} />}
|
||||
<div className="flex min-w-0 items-center justify-start gap-2">
|
||||
<DeploymentRowActions appInstanceId={appInstanceId} envId={envId} row={row} />
|
||||
<DeploymentRowActions envId={envId} row={row} />
|
||||
</div>
|
||||
</div>
|
||||
</DetailTableCard>
|
||||
)
|
||||
}
|
||||
|
||||
function DeploymentEnvironmentDesktopRows({ appInstanceId, rows }: {
|
||||
appInstanceId: string
|
||||
function DeploymentEnvironmentDesktopRows({ rows }: {
|
||||
rows: EnvironmentDeployment[]
|
||||
}) {
|
||||
return (
|
||||
@ -116,7 +114,7 @@ function DeploymentEnvironmentDesktopRows({ appInstanceId, rows }: {
|
||||
</DetailTableCell>
|
||||
<DetailTableCell className={DEPLOYMENT_DETAIL_TABLE_COLUMN_CLASS_NAMES.actions}>
|
||||
<div className="flex min-h-8 justify-end">
|
||||
<DeploymentRowActions appInstanceId={appInstanceId} envId={envId} row={row} />
|
||||
<DeploymentRowActions envId={envId} row={row} />
|
||||
</div>
|
||||
</DetailTableCell>
|
||||
</DetailTableRow>
|
||||
@ -126,8 +124,7 @@ function DeploymentEnvironmentDesktopRows({ appInstanceId, rows }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function DeploymentEnvironmentList({ appInstanceId, rows }: {
|
||||
appInstanceId: string
|
||||
export function DeploymentEnvironmentList({ rows }: {
|
||||
rows: EnvironmentDeployment[]
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
@ -138,7 +135,6 @@ export function DeploymentEnvironmentList({ appInstanceId, rows }: {
|
||||
{rows.map(row => (
|
||||
<DeploymentEnvironmentMobileRow
|
||||
key={row.environment.id}
|
||||
appInstanceId={appInstanceId}
|
||||
row={row}
|
||||
/>
|
||||
))}
|
||||
@ -154,7 +150,7 @@ export function DeploymentEnvironmentList({ appInstanceId, rows }: {
|
||||
</DetailTableRow>
|
||||
</DetailTableHeader>
|
||||
<DetailTableBody>
|
||||
<DeploymentEnvironmentDesktopRows appInstanceId={appInstanceId} rows={rows} />
|
||||
<DeploymentEnvironmentDesktopRows rows={rows} />
|
||||
</DetailTableBody>
|
||||
</DetailTable>
|
||||
</div>
|
||||
|
||||
@ -3,23 +3,24 @@
|
||||
import type { EnvironmentDeployment } from '@dify/contracts/enterprise/types.gen'
|
||||
import { RuntimeInstanceStatus } from '@dify/contracts/enterprise/types.gen'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { openDeployDrawerAtom } from '../../deploy-drawer/state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { createDeploymentIdempotencyKey } from '../../shared/domain/idempotency'
|
||||
import { isRuntimeDeploymentInProgress, isUndeployedDeploymentRow } from '../../shared/domain/runtime-status'
|
||||
import { DeploymentErrorDialog } from './deployment-error-dialog'
|
||||
import { DeploymentActionsDropdown } from './deployment-row-actions-menu'
|
||||
import { UndeployDeploymentDialog } from './undeploy-deployment-dialog'
|
||||
|
||||
export function DeploymentRowActions({ appInstanceId, envId, row }: {
|
||||
appInstanceId: string
|
||||
export function DeploymentRowActions({ envId, row }: {
|
||||
envId: string
|
||||
row: EnvironmentDeployment
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const routeAppInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const openDeployDrawer = useSetAtom(openDeployDrawerAtom)
|
||||
const undeployDeployment = useMutation(consoleQuery.enterprise.deploymentService.undeploy.mutationOptions())
|
||||
const [showUndeployConfirm, setShowUndeployConfirm] = useState(false)
|
||||
@ -36,6 +37,11 @@ export function DeploymentRowActions({ appInstanceId, envId, row }: {
|
||||
? t('deployDrawer.deploy')
|
||||
: t('deployTab.deployOtherVersion')
|
||||
|
||||
if (!routeAppInstanceId)
|
||||
return null
|
||||
|
||||
const appInstanceId = routeAppInstanceId
|
||||
|
||||
function handleDeployAction(releaseId?: string) {
|
||||
openDeployDrawer({ appInstanceId, environmentId: envId, releaseId })
|
||||
}
|
||||
|
||||
@ -87,9 +87,7 @@ function DeploymentEnvironmentListSkeleton() {
|
||||
)
|
||||
}
|
||||
|
||||
export function DeployTab({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
export function DeployTab() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const environmentDeploymentsQuery = useAtomValue(deploymentEnvironmentDeploymentsQueryAtom)
|
||||
const environmentDeployments = environmentDeploymentsQuery.data
|
||||
@ -109,11 +107,11 @@ export function DeployTab({ appInstanceId }: {
|
||||
icon="i-ri-server-line"
|
||||
title={t('deployTab.emptyTitle')}
|
||||
description={t('deployTab.emptyDescription')}
|
||||
action={<NewDeploymentButton appInstanceId={appInstanceId} />}
|
||||
action={<NewDeploymentButton />}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<DeploymentEnvironmentList appInstanceId={appInstanceId} rows={rows} />
|
||||
<DeploymentEnvironmentList rows={rows} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -4,13 +4,13 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { openDeployDrawerAtom } from '../../deploy-drawer/state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { hasRuntimeInstanceDeployment } from '../../shared/domain/runtime-status'
|
||||
import { deploymentEnvironmentDeploymentsQueryAtom } from '../state'
|
||||
|
||||
export function NewDeploymentButton({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
export function NewDeploymentButton() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const openDeployDrawer = useSetAtom(openDeployDrawerAtom)
|
||||
|
||||
return (
|
||||
@ -18,7 +18,12 @@ export function NewDeploymentButton({ appInstanceId }: {
|
||||
size="medium"
|
||||
variant="primary"
|
||||
className="gap-1.5"
|
||||
onClick={() => openDeployDrawer({ appInstanceId })}
|
||||
disabled={!appInstanceId}
|
||||
onClick={() => {
|
||||
if (!appInstanceId)
|
||||
return
|
||||
openDeployDrawer({ appInstanceId })
|
||||
}}
|
||||
>
|
||||
<span className="i-ri-rocket-line size-4 shrink-0" aria-hidden="true" />
|
||||
{t('deployTab.newDeployment')}
|
||||
@ -26,14 +31,12 @@ export function NewDeploymentButton({ appInstanceId }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function NewDeploymentHeaderAction({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
export function NewDeploymentHeaderAction() {
|
||||
const environmentDeploymentsQuery = useAtomValue(deploymentEnvironmentDeploymentsQueryAtom)
|
||||
const rows = environmentDeploymentsQuery.data?.environmentDeployments.filter(hasRuntimeInstanceDeployment) ?? []
|
||||
|
||||
if (environmentDeploymentsQuery.isLoading || environmentDeploymentsQuery.isError || rows.length === 0)
|
||||
return null
|
||||
|
||||
return <NewDeploymentButton appInstanceId={appInstanceId} />
|
||||
return <NewDeploymentButton />
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import type { InstanceDetailTabKey } from './tabs'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { ScopeProvider } from 'jotai-scope'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useSelectedLayoutSegment } from '@/next/navigation'
|
||||
import { CreateReleaseControl } from '../create-release'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../route-state'
|
||||
import { DeveloperApiHeaderSwitch } from './access-tab/developer-api/section'
|
||||
import { NewDeploymentHeaderAction } from './deploy-tab/new-deployment-button'
|
||||
import { INSTANCE_DETAIL_TAB_KEYS, isInstanceDetailTabKey } from './tabs'
|
||||
@ -43,17 +45,20 @@ function MobileDetailTabs({ appInstanceId, activeTab }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function InstanceDetail({ appInstanceId, children }: {
|
||||
appInstanceId: string
|
||||
export function InstanceDetail({ children }: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const selectedSegment = useSelectedLayoutSegment()
|
||||
const selectedTab = selectedSegment ?? undefined
|
||||
const activeTab: InstanceDetailTabKey = isInstanceDetailTabKey(selectedTab) ? selectedTab : 'overview'
|
||||
|
||||
useDocumentTitle(t('documentTitle.detail'))
|
||||
|
||||
if (!appInstanceId)
|
||||
return null
|
||||
|
||||
return (
|
||||
<ScopeProvider
|
||||
key={appInstanceId}
|
||||
@ -82,7 +87,7 @@ export function InstanceDetail({ appInstanceId, children }: {
|
||||
{(activeTab === 'instances' || activeTab === 'releases') && (
|
||||
<div className="w-full shrink-0 pt-1 sm:w-auto sm:pt-1.5 [&_button]:w-full sm:[&_button]:w-auto">
|
||||
{activeTab === 'instances'
|
||||
? <NewDeploymentHeaderAction appInstanceId={appInstanceId} />
|
||||
? <NewDeploymentHeaderAction />
|
||||
: <CreateReleaseControl appInstanceId={appInstanceId} size="medium" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -3,19 +3,19 @@
|
||||
import type { AccessChannels, ApiKeySummary } from '@dify/contracts/enterprise/types.gen'
|
||||
import { RuntimeInstanceStatus } from '@dify/contracts/enterprise/types.gen'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
import Link from '@/next/link'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { DeploymentStatusBadge } from '../../shared/ui/deployment-status-badge'
|
||||
import { OVERVIEW_CARD_CLASS_NAME, OVERVIEW_ICON_CLASS_NAME, OVERVIEW_INTERACTIVE_CARD_CLASS_NAME } from './card-styles'
|
||||
|
||||
type AccessStatusSectionProps = {
|
||||
appInstanceId: string
|
||||
accessChannels?: AccessChannels
|
||||
}
|
||||
|
||||
type ApiTokenSummarySectionProps = {
|
||||
appInstanceId: string
|
||||
accessChannels?: AccessChannels
|
||||
apiKeySummary?: ApiKeySummary
|
||||
deployedEnvironmentCount: number
|
||||
@ -32,8 +32,13 @@ type AccessStatusItem = {
|
||||
|
||||
const ACCESS_STATUS_SKELETON_KEYS = ['webapp', 'cli']
|
||||
|
||||
export function AccessStatusSection({ appInstanceId, accessChannels }: AccessStatusSectionProps) {
|
||||
export function AccessStatusSection({ accessChannels }: AccessStatusSectionProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
|
||||
if (!appInstanceId)
|
||||
return null
|
||||
|
||||
const items: AccessStatusItem[] = [
|
||||
{
|
||||
key: 'webapp',
|
||||
@ -100,15 +105,18 @@ export function AccessStatusSection({ appInstanceId, accessChannels }: AccessSta
|
||||
}
|
||||
|
||||
export function ApiTokenSummarySection({
|
||||
appInstanceId,
|
||||
accessChannels,
|
||||
apiKeySummary,
|
||||
deployedEnvironmentCount,
|
||||
}: ApiTokenSummarySectionProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const apiEnabled = Boolean(accessChannels?.developerApiEnabled)
|
||||
const apiKeyCount = apiKeySummary?.apiKeyCount ?? 0
|
||||
|
||||
if (!appInstanceId)
|
||||
return null
|
||||
|
||||
return (
|
||||
<section className="flex min-w-0 flex-col gap-3">
|
||||
<h3 className="system-sm-semibold text-text-primary">
|
||||
|
||||
@ -3,12 +3,13 @@
|
||||
import type { EnvironmentDeployment, Release } from '@dify/contracts/enterprise/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||
import Link from '@/next/link'
|
||||
import { DeploymentEmptyState } from '../../components/empty-state'
|
||||
import { openDeployDrawerAtom } from '../../deploy-drawer/state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { hasRuntimeInstanceDeployment } from '../../shared/domain/runtime-status'
|
||||
import { OVERVIEW_CARD_CLASS_NAME } from './card-styles'
|
||||
import { EnvironmentTile } from './environment-tile'
|
||||
@ -16,13 +17,13 @@ import { EnvironmentTile } from './environment-tile'
|
||||
const OVERVIEW_RUNTIME_INSTANCE_LIMIT = 4
|
||||
|
||||
type EnvironmentStripProps = {
|
||||
appInstanceId: string
|
||||
rows: EnvironmentDeployment[]
|
||||
releaseRows: Release[]
|
||||
}
|
||||
|
||||
export function EnvironmentStrip({ appInstanceId, rows, releaseRows }: EnvironmentStripProps) {
|
||||
export function EnvironmentStrip({ rows, releaseRows }: EnvironmentStripProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const runtimeRows = rows.filter(hasRuntimeInstanceDeployment)
|
||||
const previewRows = runtimeRows.slice(0, OVERVIEW_RUNTIME_INSTANCE_LIMIT)
|
||||
const hasRuntimeRows = runtimeRows.length > 0
|
||||
@ -32,7 +33,7 @@ export function EnvironmentStrip({ appInstanceId, rows, releaseRows }: Environme
|
||||
<section className="flex 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.strip.title')}</h3>
|
||||
{hasRuntimeRows && (
|
||||
{hasRuntimeRows && appInstanceId && (
|
||||
<Link
|
||||
href={`/deployments/${appInstanceId}/instances`}
|
||||
className="inline-flex shrink-0 items-center gap-1 system-xs-medium text-text-tertiary transition-colors hover:text-text-secondary"
|
||||
@ -44,13 +45,12 @@ export function EnvironmentStrip({ appInstanceId, rows, releaseRows }: Environme
|
||||
</div>
|
||||
|
||||
{!hasRuntimeRows
|
||||
? <EnvironmentEmptyState appInstanceId={appInstanceId} canDeploy={hasRelease} />
|
||||
? <EnvironmentEmptyState canDeploy={hasRelease} />
|
||||
: (
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,360px),1fr))] gap-3">
|
||||
{previewRows.map(row => (
|
||||
<EnvironmentTile
|
||||
key={row.environment.id}
|
||||
appInstanceId={appInstanceId}
|
||||
row={row}
|
||||
releaseRows={releaseRows}
|
||||
/>
|
||||
@ -61,11 +61,11 @@ export function EnvironmentStrip({ appInstanceId, rows, releaseRows }: Environme
|
||||
)
|
||||
}
|
||||
|
||||
function EnvironmentEmptyState({ appInstanceId, canDeploy }: {
|
||||
appInstanceId: string
|
||||
function EnvironmentEmptyState({ canDeploy }: {
|
||||
canDeploy: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const openDeployDrawer = useSetAtom(openDeployDrawerAtom)
|
||||
|
||||
return (
|
||||
@ -75,7 +75,7 @@ function EnvironmentEmptyState({ appInstanceId, canDeploy }: {
|
||||
title={t('overview.strip.emptyTitle')}
|
||||
description={canDeploy ? t('overview.strip.emptyDeployableDescription') : t('overview.strip.emptyDescription')}
|
||||
className="min-h-44"
|
||||
action={canDeploy
|
||||
action={canDeploy && appInstanceId
|
||||
? (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@ -7,11 +7,12 @@ import type {
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import type { TileConfig } from './environment-tile-utils'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from '@/next/link'
|
||||
import { TitleTooltip } from '../../components/title-tooltip'
|
||||
import { openDeployDrawerAtom } from '../../deploy-drawer/state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { releaseCommit } from '../../shared/domain/release'
|
||||
import { DeploymentStatusBadge } from '../../shared/ui/deployment-status-badge'
|
||||
import {
|
||||
@ -27,13 +28,13 @@ import {
|
||||
import { computeDrift, latestReleaseId } from './overview-drift'
|
||||
|
||||
type EnvironmentTileProps = {
|
||||
appInstanceId: string
|
||||
row: EnvironmentDeployment
|
||||
releaseRows: Release[]
|
||||
}
|
||||
|
||||
export function EnvironmentTile({ appInstanceId, row, releaseRows }: EnvironmentTileProps) {
|
||||
export function EnvironmentTile({ row, releaseRows }: EnvironmentTileProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const openDeployDrawer = useSetAtom(openDeployDrawerAtom)
|
||||
|
||||
const envId = row.environment.id
|
||||
@ -55,7 +56,7 @@ export function EnvironmentTile({ appInstanceId, row, releaseRows }: Environment
|
||||
: undefined
|
||||
|
||||
function handleDrawerAction() {
|
||||
if (config.intent === 'disabled')
|
||||
if (config.intent === 'disabled' || !appInstanceId)
|
||||
return
|
||||
openDeployDrawer({ appInstanceId, environmentId: envId, releaseId: config.releaseId })
|
||||
}
|
||||
@ -66,7 +67,7 @@ export function EnvironmentTile({ appInstanceId, row, releaseRows }: Environment
|
||||
isDisabled && 'cursor-not-allowed opacity-60',
|
||||
)
|
||||
const actionLabel = renderActionLabel(config.kind, Boolean(currentReleaseId), t)
|
||||
const actionControl = config.intent === 'navigate'
|
||||
const actionControl = config.intent === 'navigate' && appInstanceId
|
||||
? (
|
||||
<Link
|
||||
href={`/deployments/${appInstanceId}/instances`}
|
||||
@ -78,7 +79,7 @@ export function EnvironmentTile({ appInstanceId, row, releaseRows }: Environment
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isDisabled}
|
||||
disabled={isDisabled || !appInstanceId}
|
||||
onClick={handleDrawerAction}
|
||||
className={actionClassName}
|
||||
>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from '@/next/link'
|
||||
import { DeploymentStateMessage } from '../../components/empty-state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { hasRuntimeInstanceDeployment } from '../../shared/domain/runtime-status'
|
||||
import { deploymentDetailOverviewQueryAtom } from '../state'
|
||||
import { AccessStatusSection, AccessStatusSectionSkeleton, ApiTokenSummarySection, ApiTokenSummarySectionSkeleton } from './access-status-section'
|
||||
@ -18,11 +19,11 @@ function OverviewLayout({ children }: { children: React.ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LatestReleaseSection({ appInstanceId, children }: {
|
||||
appInstanceId: string
|
||||
function LatestReleaseSection({ children }: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
|
||||
return (
|
||||
<section className="flex min-w-0 flex-col gap-3">
|
||||
@ -30,13 +31,15 @@ function LatestReleaseSection({ appInstanceId, children }: {
|
||||
<h3 className="system-sm-semibold text-text-primary">
|
||||
{t('overview.latestReleaseTitle')}
|
||||
</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>
|
||||
{appInstanceId && (
|
||||
<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}
|
||||
@ -45,13 +48,11 @@ function LatestReleaseSection({ appInstanceId, children }: {
|
||||
)
|
||||
}
|
||||
|
||||
function OverviewLoadingSkeleton({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
function OverviewLoadingSkeleton() {
|
||||
return (
|
||||
<OverviewLayout>
|
||||
<EnvironmentStripSkeleton />
|
||||
<LatestReleaseSection appInstanceId={appInstanceId}>
|
||||
<LatestReleaseSection>
|
||||
<ReleaseHeroSkeleton />
|
||||
</LatestReleaseSection>
|
||||
<AccessStatusSectionSkeleton />
|
||||
@ -60,15 +61,13 @@ function OverviewLoadingSkeleton({ appInstanceId }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function OverviewTab({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
export function OverviewTab() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const overviewQuery = useAtomValue(deploymentDetailOverviewQueryAtom)
|
||||
const overview = overviewQuery.data
|
||||
|
||||
if (overviewQuery.isLoading)
|
||||
return <OverviewLoadingSkeleton appInstanceId={appInstanceId} />
|
||||
return <OverviewLoadingSkeleton />
|
||||
|
||||
if (overviewQuery.isError) {
|
||||
return (
|
||||
@ -98,20 +97,17 @@ export function OverviewTab({ appInstanceId }: {
|
||||
return (
|
||||
<OverviewLayout>
|
||||
<EnvironmentStrip
|
||||
appInstanceId={appInstanceId}
|
||||
rows={runtimeRows}
|
||||
releaseRows={releaseRows}
|
||||
/>
|
||||
<LatestReleaseSection appInstanceId={appInstanceId}>
|
||||
<LatestReleaseSection>
|
||||
<ReleaseHero
|
||||
appInstanceId={appInstanceId}
|
||||
latestRelease={latestRelease}
|
||||
releaseCount={releaseCount}
|
||||
/>
|
||||
</LatestReleaseSection>
|
||||
<AccessStatusSection appInstanceId={appInstanceId} accessChannels={accessChannels} />
|
||||
<AccessStatusSection accessChannels={accessChannels} />
|
||||
<ApiTokenSummarySection
|
||||
appInstanceId={appInstanceId}
|
||||
accessChannels={accessChannels}
|
||||
apiKeySummary={apiKeySummary}
|
||||
deployedEnvironmentCount={deployedEnvironmentCount}
|
||||
|
||||
@ -4,24 +4,21 @@ 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 { ScopeProvider } from 'jotai-scope'
|
||||
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 { DeploymentEmptyState } from '../../components/empty-state'
|
||||
import { TitleTooltip } from '../../components/title-tooltip'
|
||||
import { CreateReleaseControl } from '../../create-release'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { formatDate, releaseCommit } from '../../shared/domain/release'
|
||||
import {
|
||||
deploymentSourceAppIdAtom,
|
||||
deploymentSourceAppQueryAtom,
|
||||
} from '../state'
|
||||
import { OVERVIEW_CARD_CLASS_NAME, OVERVIEW_ICON_CLASS_NAME } from './card-styles'
|
||||
|
||||
type ReleaseHeroProps = {
|
||||
appInstanceId: string
|
||||
latestRelease?: Release
|
||||
releaseCount: number
|
||||
}
|
||||
@ -32,8 +29,9 @@ type ReleaseMetaItemProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ReleaseHero({ appInstanceId, latestRelease, releaseCount }: ReleaseHeroProps) {
|
||||
export function ReleaseHero({ latestRelease, releaseCount }: ReleaseHeroProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
|
||||
if (!latestRelease) {
|
||||
@ -43,7 +41,7 @@ export function ReleaseHero({ appInstanceId, latestRelease, releaseCount }: Rele
|
||||
icon="i-ri-stack-line"
|
||||
title={t('overview.hero.empty')}
|
||||
description={t('overview.hero.emptyDescription')}
|
||||
action={<CreateReleaseControl appInstanceId={appInstanceId} size="medium" />}
|
||||
action={appInstanceId ? <CreateReleaseControl appInstanceId={appInstanceId} size="medium" /> : undefined}
|
||||
className="min-h-44"
|
||||
/>
|
||||
)
|
||||
@ -129,23 +127,17 @@ function LatestReleaseSource({ release }: {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopeProvider
|
||||
key={sourceAppId}
|
||||
atoms={[
|
||||
[deploymentSourceAppIdAtom, sourceAppId],
|
||||
]}
|
||||
name="DeploymentLatestReleaseSource"
|
||||
>
|
||||
<LatestReleaseSourceLink sourceAppId={sourceAppId} />
|
||||
</ScopeProvider>
|
||||
)
|
||||
return <LatestReleaseSourceLink sourceAppId={sourceAppId} />
|
||||
}
|
||||
|
||||
function LatestReleaseSourceLink({ sourceAppId }: {
|
||||
sourceAppId: string
|
||||
}) {
|
||||
const sourceAppQuery = useAtomValue(deploymentSourceAppQueryAtom)
|
||||
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
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { skipToken } from '@tanstack/react-query'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery } from 'jotai-tanstack-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../route-state'
|
||||
import { deploymentStatusPollingInterval } from '../shared/domain/runtime-status'
|
||||
|
||||
export const deploymentSourceAppIdAtom = atom<string | undefined>(undefined)
|
||||
|
||||
export const deploymentDetailAppInstanceQueryAtom = atomWithQuery((get) => {
|
||||
const appInstanceId = get(deploymentRouteAppInstanceIdAtom)
|
||||
|
||||
@ -48,14 +45,3 @@ export const deploymentEnvironmentDeploymentsQueryAtom = atomWithQuery((get) =>
|
||||
refetchInterval: query => deploymentStatusPollingInterval(query.state.data?.environmentDeployments),
|
||||
})
|
||||
})
|
||||
|
||||
export const deploymentSourceAppQueryAtom = atomWithQuery((get) => {
|
||||
const sourceAppId = get(deploymentSourceAppIdAtom)
|
||||
|
||||
return consoleQuery.apps.byAppId.get.queryOptions({
|
||||
input: sourceAppId
|
||||
? { params: { app_id: sourceAppId } }
|
||||
: skipToken,
|
||||
enabled: Boolean(sourceAppId),
|
||||
})
|
||||
})
|
||||
|
||||
@ -103,7 +103,6 @@ describe('DeployReleaseMenu', () => {
|
||||
|
||||
render(
|
||||
<DeployReleaseMenu
|
||||
appInstanceId="app-instance-1"
|
||||
releaseId={release.id}
|
||||
releaseRows={[release]}
|
||||
/>,
|
||||
|
||||
@ -8,14 +8,12 @@ vi.mock('../deploy-release-menu', () => ({
|
||||
DeployReleaseMenu: () => <button type="button">Actions</button>,
|
||||
}))
|
||||
|
||||
vi.mock('../../state', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../state')>()
|
||||
const { atom } = await import('jotai')
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
deploymentSourceAppIdAtom: atom<string | undefined>(undefined),
|
||||
deploymentSourceAppQueryAtom: atom({
|
||||
useQuery: () => ({
|
||||
data: {
|
||||
name: 'Source Workflow',
|
||||
},
|
||||
@ -23,6 +21,18 @@ vi.mock('../../state', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
byAppId: {
|
||||
get: {
|
||||
queryOptions: (options: unknown) => options,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
function createReleaseRow(overrides: Partial<ReleaseWithSummaryDeployments> = {}): ReleaseWithSummaryDeployments {
|
||||
return {
|
||||
id: 'release-1',
|
||||
@ -46,7 +56,6 @@ describe('ReleaseHistoryRows', () => {
|
||||
it('should render the desktop release list with the knowledge table style', () => {
|
||||
const { container } = render(
|
||||
<ReleaseHistoryRows
|
||||
appInstanceId="app-instance-1"
|
||||
releaseRows={[createReleaseRow()]}
|
||||
/>,
|
||||
)
|
||||
@ -67,7 +76,6 @@ describe('ReleaseHistoryRows', () => {
|
||||
it('should render release deployments with the dot status style', () => {
|
||||
const { container } = render(
|
||||
<ReleaseHistoryRows
|
||||
appInstanceId="app-instance-1"
|
||||
releaseRows={[
|
||||
createReleaseRow({
|
||||
summaryDeployments: [{
|
||||
@ -93,7 +101,6 @@ describe('ReleaseHistoryRows', () => {
|
||||
it('should render release source app links with scoped source app state', () => {
|
||||
const { container } = render(
|
||||
<ReleaseHistoryRows
|
||||
appInstanceId="app-instance-1"
|
||||
releaseRows={[
|
||||
createReleaseRow({
|
||||
sourceAppId: 'source-app-1',
|
||||
|
||||
@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { TitleTooltip } from '../../components/title-tooltip'
|
||||
import { openDeployDrawerAtom } from '../../deploy-drawer/state'
|
||||
import { deploymentRouteAppInstanceIdAtom } from '../../route-state'
|
||||
import { isUndeployedDeploymentRow } from '../../shared/domain/runtime-status'
|
||||
import { DETAIL_TABLE_ACTION_TRIGGER_CLASS_NAME } from '../components/detail-table-styles'
|
||||
import { DeleteReleaseDialog } from './delete-release-dialog'
|
||||
@ -38,13 +39,13 @@ type ExportReleaseDslInput = {
|
||||
appInstanceName?: string
|
||||
}
|
||||
|
||||
export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDeleted }: {
|
||||
appInstanceId: string
|
||||
export function DeployReleaseMenu({ releaseId, releaseRows, onDeleted }: {
|
||||
releaseId: string
|
||||
releaseRows: Release[]
|
||||
onDeleted?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appInstanceId = useAtomValue(deploymentRouteAppInstanceIdAtom)
|
||||
const openDeployDrawer = useSetAtom(openDeployDrawerAtom)
|
||||
const openReleaseMenuId = useAtomValue(deployReleaseMenuOpenReleaseIdAtom)
|
||||
const setDeployReleaseMenuOpen = useSetAtom(setDeployReleaseMenuOpenAtom)
|
||||
@ -194,7 +195,7 @@ export function DeployReleaseMenu({ appInstanceId, releaseId, releaseRows, onDel
|
||||
isDisabled && 'cursor-not-allowed opacity-60',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isDisabled)
|
||||
if (isDisabled || !appInstanceId)
|
||||
return
|
||||
handleOpenChange(false)
|
||||
openDeployDrawer({ appInstanceId, environmentId: row.environmentId, releaseId })
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
'use client'
|
||||
import { ReleaseHistoryTable } from './release-history-table'
|
||||
|
||||
export function VersionsTab({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
export function VersionsTab() {
|
||||
return (
|
||||
<div className="flex w-full min-w-0 flex-col gap-4 px-6 py-6">
|
||||
<ReleaseHistoryTable appInstanceId={appInstanceId} />
|
||||
<ReleaseHistoryTable />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,11 +4,11 @@ import type { Release } from '@dify/contracts/enterprise/types.gen'
|
||||
import type { ReleaseWithSummaryDeployments } from './release-deployments'
|
||||
import { ReleaseSource } from '@dify/contracts/enterprise/types.gen'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { ScopeProvider } from 'jotai-scope'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { TitleTooltip } from '../../components/title-tooltip'
|
||||
import {
|
||||
formatDate,
|
||||
@ -25,10 +25,6 @@ import {
|
||||
DetailTableRow,
|
||||
} from '../components/detail-table'
|
||||
import { RELEASE_DETAIL_TABLE_COLUMN_CLASS_NAMES } from '../components/detail-table-styles'
|
||||
import {
|
||||
deploymentSourceAppIdAtom,
|
||||
deploymentSourceAppQueryAtom,
|
||||
} from '../state'
|
||||
import { DeployReleaseMenu } from './deploy-release-menu'
|
||||
import {
|
||||
ReleaseDeploymentsContent,
|
||||
@ -90,23 +86,17 @@ function ReleaseSourceCell({ release }: {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopeProvider
|
||||
key={sourceAppId}
|
||||
atoms={[
|
||||
[deploymentSourceAppIdAtom, sourceAppId],
|
||||
]}
|
||||
name="DeploymentReleaseSource"
|
||||
>
|
||||
<ReleaseSourceLink sourceAppId={sourceAppId} />
|
||||
</ScopeProvider>
|
||||
)
|
||||
return <ReleaseSourceLink sourceAppId={sourceAppId} />
|
||||
}
|
||||
|
||||
function ReleaseSourceLink({ sourceAppId }: {
|
||||
sourceAppId: string
|
||||
}) {
|
||||
const sourceAppQuery = useAtomValue(deploymentSourceAppQueryAtom)
|
||||
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
|
||||
@ -126,8 +116,7 @@ function ReleaseSourceLink({ sourceAppId }: {
|
||||
)
|
||||
}
|
||||
|
||||
function ReleaseHistoryMobileRows({ appInstanceId, releaseRows, onReleaseDeleted }: {
|
||||
appInstanceId: string
|
||||
function ReleaseHistoryMobileRows({ releaseRows, onReleaseDeleted }: {
|
||||
releaseRows: ReleaseWithSummaryDeployments[]
|
||||
onReleaseDeleted?: () => void
|
||||
}) {
|
||||
@ -164,7 +153,6 @@ function ReleaseHistoryMobileRows({ appInstanceId, releaseRows, onReleaseDeleted
|
||||
<div className="flex shrink-0 justify-end gap-1">
|
||||
<DeployReleaseMenu
|
||||
releaseId={releaseId}
|
||||
appInstanceId={appInstanceId}
|
||||
releaseRows={releaseRows}
|
||||
onDeleted={onReleaseDeleted}
|
||||
/>
|
||||
@ -185,8 +173,7 @@ function ReleaseHistoryMobileRows({ appInstanceId, releaseRows, onReleaseDeleted
|
||||
)
|
||||
}
|
||||
|
||||
export function ReleaseHistoryRows({ appInstanceId, releaseRows, onReleaseDeleted }: {
|
||||
appInstanceId: string
|
||||
export function ReleaseHistoryRows({ releaseRows, onReleaseDeleted }: {
|
||||
releaseRows: ReleaseWithSummaryDeployments[]
|
||||
onReleaseDeleted?: () => void
|
||||
}) {
|
||||
@ -195,7 +182,6 @@ export function ReleaseHistoryRows({ appInstanceId, releaseRows, onReleaseDelete
|
||||
return (
|
||||
<>
|
||||
<ReleaseHistoryMobileRows
|
||||
appInstanceId={appInstanceId}
|
||||
releaseRows={releaseRows}
|
||||
onReleaseDeleted={onReleaseDeleted}
|
||||
/>
|
||||
@ -241,7 +227,6 @@ export function ReleaseHistoryRows({ appInstanceId, releaseRows, onReleaseDelete
|
||||
<div className="flex justify-end">
|
||||
<DeployReleaseMenu
|
||||
releaseId={releaseId}
|
||||
appInstanceId={appInstanceId}
|
||||
releaseRows={releaseRows}
|
||||
onDeleted={onReleaseDeleted}
|
||||
/>
|
||||
|
||||
@ -15,9 +15,7 @@ import {
|
||||
setReleaseHistoryCurrentPageAtom,
|
||||
} from './state'
|
||||
|
||||
export function ReleaseHistoryTable({ appInstanceId }: {
|
||||
appInstanceId: string
|
||||
}) {
|
||||
export function ReleaseHistoryTable() {
|
||||
const { t } = useTranslation('deployments')
|
||||
const currentPage = useAtomValue(releaseHistoryCurrentPageAtom)
|
||||
const setCurrentPage = useSetAtom(setReleaseHistoryCurrentPageAtom)
|
||||
@ -67,7 +65,6 @@ export function ReleaseHistoryTable({ appInstanceId }: {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<ReleaseHistoryRows
|
||||
appInstanceId={appInstanceId}
|
||||
releaseRows={releaseRows}
|
||||
onReleaseDeleted={handleReleaseDeleted}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user