This commit is contained in:
Stephen Zhou
2026-05-22 15:48:07 +08:00
parent d7028421a6
commit 0378ecb6ba
2 changed files with 171 additions and 19 deletions

View File

@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DeveloperApiSection } from '../developer-api-section'
type QueryOptions = {
queryKey?: string[]
}
type QueryResult = {
data?: unknown
isLoading: boolean
isError: boolean
}
const mockUseQuery = vi.fn<(options: QueryOptions) => QueryResult>()
vi.mock('@tanstack/react-query', () => ({
useMutation: () => ({ mutate: vi.fn() }),
useQuery: (options: QueryOptions) => mockUseQuery(options),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
enterprise: {
appDeployAccessService: {
createDeveloperApiKey: {
mutationOptions: () => ({ mutationFn: vi.fn() }),
},
deleteDeveloperApiKey: {
mutationOptions: () => ({ mutationFn: vi.fn() }),
},
getAppInstanceAccess: {
queryOptions: () => ({ queryKey: ['app-instance-access'] }),
},
updateDeveloperApi: {
mutationOptions: () => ({ mutationFn: vi.fn() }),
},
},
},
},
}))
function queryResult(overrides: Partial<QueryResult> = {}): QueryResult {
return {
data: undefined,
isLoading: false,
isError: false,
...overrides,
}
}
describe('DeveloperApiSection', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Loading should reserve the same shape as the enabled API page: endpoint copy row plus API key table.
describe('Loading state', () => {
it('should render the updated API tab skeleton while access config is loading', () => {
// Arrange
mockUseQuery.mockReturnValue(queryResult({ isLoading: true }))
// Act
const { container } = render(<DeveloperApiSection appInstanceId="instance-1" />)
// Assert
expect(screen.getByText('deployments.access.api.endpoint')).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'deployments.access.api.table.name' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'deployments.access.api.table.environment' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'deployments.access.api.table.key' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'deployments.access.api.table.action' })).toBeInTheDocument()
expect(container.querySelectorAll('[data-slot="deployment-developer-api-skeleton"]')).toHaveLength(1)
expect(container.querySelectorAll('[data-slot="deployment-developer-api-desktop-row-skeleton"]')).toHaveLength(2)
expect(container.querySelectorAll('[data-slot="deployment-developer-api-mobile-row-skeleton"]')).toHaveLength(2)
})
})
})

View File

@ -8,9 +8,20 @@ import { Switch, SwitchSkeleton } from '@langgenius/dify-ui/switch'
import { useMutation, useQuery } from '@tanstack/react-query'
import { atom, useAtom, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { SkeletonRectangle } from '@/app/components/base/skeleton'
import { consoleQuery } from '@/service/client'
import { SectionState } from '../../common'
import {
DetailTable,
DetailTableBody,
DetailTableCard,
DetailTableCardList,
DetailTableCell,
DetailTableHead,
DetailTableHeader,
DetailTableRow,
} from '../../table'
import { API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES } from '../../table-styles'
import { ApiKeyGenerateMenu, ApiKeyList } from './api-keys'
import { CopyPill } from './common'
@ -127,31 +138,95 @@ function CreatedApiTokenCard({ token, onDismiss }: {
}
function DeveloperApiSkeleton() {
const { t } = useTranslation('deployments')
return (
<div className="flex flex-col gap-4">
<SkeletonRectangle className="my-0 h-8 w-full animate-pulse rounded-lg" />
<SkeletonRow className="items-center justify-between gap-3 rounded-lg border border-divider-subtle bg-background-default-subtle px-4 py-3">
<div className="flex min-w-0 flex-col gap-1.5">
<SkeletonRectangle className="h-3.5 w-28 animate-pulse" />
<SkeletonRectangle className="h-3 w-40 animate-pulse" />
<div className="flex flex-col gap-4" data-slot="deployment-developer-api-skeleton">
<div className="flex h-8 items-center gap-1 rounded-lg border border-components-input-border-active bg-components-input-bg-normal pr-1 pl-1.5">
<div className="flex h-5 shrink-0 items-center rounded-md border border-divider-subtle px-1.5 system-2xs-medium text-text-tertiary">
{t('access.api.endpoint')}
</div>
<SkeletonRectangle className="my-0 h-8 w-24 animate-pulse rounded-lg" />
</SkeletonRow>
<div className="flex flex-col gap-2">
{DEVELOPER_API_KEY_SKELETON_KEYS.map(key => (
<SkeletonRow key={key} className="items-center gap-3 rounded-lg border border-divider-subtle bg-background-default-subtle px-3 py-3">
<div className="flex min-w-35 flex-col gap-1.5">
<SkeletonRectangle className="h-3 w-24 animate-pulse" />
<SkeletonRectangle className="h-2.5 w-18 animate-pulse" />
</div>
<SkeletonRectangle className="my-0 h-8 min-w-0 flex-1 animate-pulse rounded-lg" />
</SkeletonRow>
))}
<SkeletonRectangle className="my-0 h-3 min-w-0 flex-1 animate-pulse" />
<div className="h-3.5 w-px shrink-0 bg-divider-regular" />
<SkeletonRectangle className="my-0 size-6 shrink-0 animate-pulse rounded-md" />
</div>
<ApiKeyTableSkeleton />
</div>
)
}
function ApiKeyTableSkeleton() {
return (
<>
<DetailTableCardList className="pc:hidden">
{DEVELOPER_API_KEY_SKELETON_KEYS.map(key => (
<DetailTableCard key={key} data-slot="deployment-developer-api-mobile-row-skeleton">
<div className="flex flex-col gap-3 p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<SkeletonRectangle className="my-0 h-3.5 w-32 animate-pulse" />
<SkeletonRectangle className="mt-2 h-5 w-20 animate-pulse rounded-md" />
</div>
<SkeletonRectangle className="my-0 size-8 shrink-0 animate-pulse rounded-md" />
</div>
<div className="flex min-w-0 flex-col gap-1">
<SkeletonRectangle className="my-0 h-2.5 w-14 animate-pulse" />
<SkeletonRectangle className="my-0 h-8 w-full animate-pulse rounded-lg" />
</div>
</div>
</DetailTableCard>
))}
</DetailTableCardList>
<div className="hidden pc:block">
<DetailTable>
<ApiKeyTableHeaderSkeleton />
<DetailTableBody>
{DEVELOPER_API_KEY_SKELETON_KEYS.map(key => (
<ApiKeyDesktopRowSkeleton key={key} />
))}
</DetailTableBody>
</DetailTable>
</div>
</>
)
}
function ApiKeyTableHeaderSkeleton() {
const { t } = useTranslation('deployments')
return (
<DetailTableHeader>
<DetailTableRow>
<DetailTableHead className={API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.name}>{t('access.api.table.name')}</DetailTableHead>
<DetailTableHead className={API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.environment}>{t('access.api.table.environment')}</DetailTableHead>
<DetailTableHead className={API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.key}>{t('access.api.table.key')}</DetailTableHead>
<DetailTableHead className={`${API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.action} text-right`}>{t('access.api.table.action')}</DetailTableHead>
</DetailTableRow>
</DetailTableHeader>
)
}
function ApiKeyDesktopRowSkeleton() {
return (
<DetailTableRow data-slot="deployment-developer-api-desktop-row-skeleton">
<DetailTableCell>
<SkeletonRectangle className="my-0 h-3.5 w-32 animate-pulse" />
</DetailTableCell>
<DetailTableCell>
<SkeletonRectangle className="my-0 h-5 w-20 animate-pulse rounded-md" />
</DetailTableCell>
<DetailTableCell>
<SkeletonRectangle className="my-0 h-8 w-full animate-pulse rounded-lg" />
</DetailTableCell>
<DetailTableCell>
<div className="flex justify-end">
<SkeletonRectangle className="my-0 size-8 animate-pulse rounded-md" />
</div>
</DetailTableCell>
</DetailTableRow>
)
}
export function DeveloperApiSection({
appInstanceId,
}: {