feat: app deployment v2 (#39829)

Co-authored-by: zhangx1n <zhangxin@dify.ai>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-08-12 01:55:36 +00:00
1 parent ef8544b173
commit 7aba539e82
542 files changed
+29450 -13824

No files matched your search

@@ -1,3 +1,4 @@
import type { SecretKeyScope } from '../secret-key-modal'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import SecretKeyButton from '../secret-key-button'
@@ -6,17 +7,19 @@ vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({
default: ({
isShow,
onClose,
appId,
canManage,
scope,
}: {
isShow: boolean
onClose: () => void
appId?: string
canManage: boolean
scope: SecretKeyScope
}) =>
isShow ? (
<div data-testid="secret-key-modal">
<span data-testid="modal-app-id">{`Modal for ${appId || 'no-app'}`}</span>
<span data-testid="modal-app-id">
{`Modal for ${scope.type === 'dataset' ? 'no-app' : scope.appId}`}
</span>
<span data-testid="modal-can-manage">{String(canManage)}</span>
<button onClick={onClose} data-testid="close-modal">
Close
@@ -1,14 +1,112 @@
import type { ApiKeyList as AppApiKeyList } from '@dify/contracts/api/console/apps/types.gen'
import type { ApiKeyList as DatasetApiKeyList } from '@dify/contracts/api/console/datasets/types.gen'
import type { EnvironmentApiKey } from '@dify/contracts/enterprise-app-deploy/types.gen'
import type { SecretKeyScope } from '../secret-key-modal'
import { QueryClientProvider } from '@tanstack/react-query'
import { act, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach } from 'vitest'
import { render } from '@/test/console/render'
import { createTestQueryClient } from '@/test/query-client'
import SecretKeyModal from '../secret-key-modal'
type MutationCallbacks<TData> = {
onSuccess?: (data: TData) => void
}
const apiMocks = vi.hoisted(() => ({
appKeys: [] as AppApiKeyList['data'],
datasetKeys: [] as DatasetApiKeyList['data'],
environmentKeys: [] as EnvironmentApiKey[],
listApp: vi.fn(),
createApp: vi.fn(),
deleteApp: vi.fn(),
listDataset: vi.fn(),
createDataset: vi.fn(),
deleteDataset: vi.fn(),
listEnvironment: vi.fn(),
createEnvironment: vi.fn(),
deleteEnvironment: vi.fn(),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
apps: {
byResourceId: {
apiKeys: {
get: {
queryOptions: ({ input }: { input: unknown }) => ({
queryKey: ['apps', 'api-keys', input],
queryFn: () => {
apiMocks.listApp(input)
return Promise.resolve({ data: apiMocks.appKeys })
},
}),
},
post: {
mutationOptions: () => ({
mutationFn: (variables: unknown) => apiMocks.createApp(variables),
}),
},
byApiKeyId: {
delete: {
mutationOptions: () => ({
mutationFn: (variables: unknown) => apiMocks.deleteApp(variables),
}),
},
},
},
},
},
datasets: {
apiKeys: {
get: {
queryOptions: () => ({
queryKey: ['datasets', 'api-keys'],
queryFn: () => {
apiMocks.listDataset()
return Promise.resolve({ data: apiMocks.datasetKeys })
},
}),
},
post: {
mutationOptions: () => ({
mutationFn: () => apiMocks.createDataset(),
}),
},
byApiKeyId: {
delete: {
mutationOptions: () => ({
mutationFn: (variables: unknown) => apiMocks.deleteDataset(variables),
}),
},
},
},
},
enterprise: {
appDeploy: {
accessService: {
listEnvironmentApiKeys: {
queryOptions: ({ input, enabled }: { input: unknown; enabled?: boolean }) => ({
queryKey: ['environment', 'api-keys', input],
queryFn: () => {
apiMocks.listEnvironment(input)
return Promise.resolve({ data: apiMocks.environmentKeys })
},
enabled,
}),
},
createEnvironmentApiKey: {
mutationOptions: () => ({
mutationFn: (variables: unknown) => apiMocks.createEnvironment(variables),
}),
},
deleteEnvironmentApiKey: {
mutationOptions: () => ({
mutationFn: (variables: unknown) => apiMocks.deleteEnvironment(variables),
}),
},
},
},
},
},
}))
const mockCurrentWorkspace = vi.fn().mockReturnValue({
id: 'workspace-1',
@@ -30,45 +128,27 @@ vi.mock('@/hooks/use-timestamp', () => ({
}),
}))
let appApiKeys: AppApiKeyList = { data: [] }
let datasetApiKeys: DatasetApiKeyList = { data: [] }
let appApiKeysLoading = false
let datasetApiKeysLoading = false
const appScope = { type: 'app', appId: 'app-123' } as const
const datasetScope = { type: 'dataset' } as const
const environmentScope = {
type: 'environment',
appId: 'app-123',
environmentId: 'staging',
} as const
const createAppApiKey = vi.fn(
(_variables: unknown, callbacks?: MutationCallbacks<{ token: string }>) =>
callbacks?.onSuccess?.({ token: 'new-app-token-123' }),
)
const deleteAppApiKey = vi.fn()
const createDatasetApiKey = vi.fn(
(_variables: unknown, callbacks?: MutationCallbacks<{ token: string }>) =>
callbacks?.onSuccess?.({ token: 'new-dataset-token-123' }),
)
const deleteDatasetApiKey = vi.fn()
let mutationHookIndex = 0
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useMutation: () => {
const mutations = [createAppApiKey, deleteAppApiKey, createDatasetApiKey, deleteDatasetApiKey]
const mutate = mutations[mutationHookIndex % mutations.length]
mutationHookIndex += 1
return { mutate }
},
useQuery: (options: { queryKey: readonly unknown[] }) => {
const isAppQuery = JSON.stringify(options.queryKey).includes('"apps"')
return isAppQuery
? { data: appApiKeys, isLoading: appApiKeysLoading }
: { data: datasetApiKeys, isLoading: datasetApiKeysLoading }
},
}
})
async function renderModal(appId?: string) {
async function renderModal(scope: SecretKeyScope, overrides: { canManage?: boolean } = {}) {
const queryClient = createTestQueryClient()
const onClose = vi.fn()
const result = render(<SecretKeyModal isShow appId={appId} canManage onClose={onClose} />)
const result = render(
<QueryClientProvider client={queryClient}>
<SecretKeyModal
isShow
canManage={overrides.canManage ?? true}
scope={scope}
onClose={onClose}
/>
</QueryClientProvider>,
)
await act(async () => {
vi.runAllTimers()
})
@@ -89,11 +169,21 @@ describe('SecretKeyModal', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers({ shouldAdvanceTime: true })
mutationHookIndex = 0
appApiKeys = { data: [] }
datasetApiKeys = { data: [] }
appApiKeysLoading = false
datasetApiKeysLoading = false
mockCurrentWorkspace.mockReturnValue({ id: 'workspace-1', name: 'Test Workspace' })
apiMocks.appKeys = []
apiMocks.datasetKeys = []
apiMocks.environmentKeys = []
apiMocks.createApp.mockResolvedValue({ token: 'new-app-token-123' })
apiMocks.deleteApp.mockResolvedValue(undefined)
apiMocks.createDataset.mockResolvedValue({ token: 'new-dataset-token-123' })
apiMocks.deleteDataset.mockResolvedValue(undefined)
apiMocks.createEnvironment.mockResolvedValue({
id: 'environment-key-2',
token: 'env-created-secret-key-abcdefghijklmnopqrst',
type: 'api',
created_at: 1,
})
apiMocks.deleteEnvironment.mockResolvedValue(undefined)
})
afterEach(() => {
@@ -101,119 +191,202 @@ describe('SecretKeyModal', () => {
vi.useRealTimers()
})
it('renders the app API key dataset selected by appId', async () => {
appApiKeys = {
data: [
{
id: 'app-key-1',
token: 'app-secret-token-123456789',
type: 'app',
created_at: 1,
},
],
}
it('loads and renders app API keys through the generated query input', async () => {
apiMocks.appKeys = [
{
id: 'app-key-1',
token: 'app-secret-token-123456789',
type: 'app',
created_at: 1,
},
]
await renderModal('app-123')
await renderModal(appScope)
expect(screen.getByText('app...cret-token-123456789')).toBeInTheDocument()
expect(await screen.findByText('app...cret-token-123456789')).toBeInTheDocument()
expect(apiMocks.listApp).toHaveBeenCalledWith({
params: { resource_id: 'app-123' },
})
})
it('renders the workspace dataset API keys without an appId', async () => {
datasetApiKeys = {
data: [
{
id: 'dataset-key-1',
token: 'dataset-secret-token-123456789',
type: 'dataset',
created_at: 1,
},
],
}
it('loads and renders workspace dataset API keys', async () => {
apiMocks.datasetKeys = [
{
id: 'dataset-key-1',
token: 'dataset-secret-token-123456789',
type: 'dataset',
created_at: 1,
},
]
await renderModal()
await renderModal(datasetScope)
expect(screen.getByText('dat...cret-token-123456789')).toBeInTheDocument()
expect(await screen.findByText('dat...cret-token-123456789')).toBeInTheDocument()
expect(apiMocks.listDataset).toHaveBeenCalled()
})
it('creates an app API key through the generated mutation input', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
await renderModal('app-123')
await renderModal(appScope)
await user.click(screen.getByText('appApi.apiKeyModal.createNewSecretKey'))
expect(createAppApiKey).toHaveBeenCalledWith(
{ params: { resource_id: 'app-123' } },
expect.objectContaining({ onSuccess: expect.any(Function) }),
)
await waitFor(() => {
expect(apiMocks.createApp).toHaveBeenCalledWith({
params: { resource_id: 'app-123' },
})
})
expect(await screen.findByText('new-app-token-123')).toBeInTheDocument()
})
it('creates a dataset API key through the generated mutation', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
await renderModal()
await renderModal(datasetScope)
await user.click(screen.getByText('appApi.apiKeyModal.createNewSecretKey'))
expect(createDatasetApiKey).toHaveBeenCalledWith(
undefined,
expect.objectContaining({ onSuccess: expect.any(Function) }),
)
await waitFor(() => {
expect(apiMocks.createDataset).toHaveBeenCalledWith()
})
expect(await screen.findByText('new-dataset-token-123')).toBeInTheDocument()
})
it('deletes an app API key through the generated mutation input', async () => {
appApiKeys = {
data: [
{
id: 'app-key-0',
token: 'other-app-secret-token-987654321',
type: 'app',
created_at: 1,
},
{
id: 'app-key-1',
token: 'app-secret-token-123456789',
type: 'app',
created_at: 1,
},
],
}
await renderModal('app-123')
apiMocks.appKeys = [
{
id: 'app-key-0',
token: 'other-app-secret-token-987654321',
type: 'app',
created_at: 1,
},
{
id: 'app-key-1',
token: 'app-secret-token-123456789',
type: 'app',
created_at: 1,
},
]
await renderModal(appScope)
await screen.findByText('app...cret-token-123456789')
await confirmKeyDeletion('common.operation.delete app...cret-token-123456789')
await waitFor(() => {
expect(deleteAppApiKey).toHaveBeenCalledWith({
expect(apiMocks.deleteApp).toHaveBeenCalledWith({
params: { resource_id: 'app-123', api_key_id: 'app-key-1' },
})
})
})
it('deletes a dataset API key through the generated mutation input', async () => {
datasetApiKeys = {
data: [
{
id: 'dataset-key-0',
token: 'other-dataset-secret-token-987654321',
type: 'dataset',
created_at: 1,
},
{
id: 'dataset-key-1',
token: 'dataset-secret-token-123456789',
type: 'dataset',
created_at: 1,
},
],
}
await renderModal()
apiMocks.datasetKeys = [
{
id: 'dataset-key-0',
token: 'other-dataset-secret-token-987654321',
type: 'dataset',
created_at: 1,
},
{
id: 'dataset-key-1',
token: 'dataset-secret-token-123456789',
type: 'dataset',
created_at: 1,
},
]
await renderModal(datasetScope)
await screen.findByText('dat...cret-token-123456789')
await confirmKeyDeletion('common.operation.delete dat...cret-token-123456789')
await waitFor(() => {
expect(deleteDatasetApiKey).toHaveBeenCalledWith({
expect(apiMocks.deleteDataset).toHaveBeenCalledWith({
params: { api_key_id: 'dataset-key-1' },
})
})
})
it('loads environment-scoped keys without requesting built-in app keys', async () => {
apiMocks.environmentKeys = [
{
id: 'environment-key-1',
token: 'env-existing-secret-key-abcdefghijklmnopqrst',
type: 'api',
created_at: 1,
},
]
await renderModal(environmentScope)
expect(await screen.findByText(/^env\.\.\./)).toBeInTheDocument()
expect(apiMocks.listEnvironment).toHaveBeenCalledWith({
params: {
app_id: 'app-123',
environment_id: 'staging',
},
})
expect(apiMocks.listApp).not.toHaveBeenCalled()
})
it('creates an environment-scoped API key', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
await renderModal(environmentScope)
await user.click(screen.getByText('appApi.apiKeyModal.createNewSecretKey'))
await waitFor(() => {
expect(apiMocks.createEnvironment).toHaveBeenCalledWith({
params: {
app_id: 'app-123',
environment_id: 'staging',
},
})
})
expect(
await screen.findByText('env-created-secret-key-abcdefghijklmnopqrst'),
).toBeInTheDocument()
})
it('deletes an environment-scoped API key', async () => {
apiMocks.environmentKeys = [
{
id: 'environment-key-1',
token: 'env-existing-secret-key-abcdefghijklmnopqrst',
type: 'api',
created_at: 1,
},
]
await renderModal(environmentScope)
await screen.findByText(/^env\.\.\./)
await confirmKeyDeletion('common.operation.delete env...abcdefghijklmnopqrst')
await waitFor(() => {
expect(apiMocks.deleteEnvironment).toHaveBeenCalledWith({
params: {
api_key_id: 'environment-key-1',
app_id: 'app-123',
environment_id: 'staging',
},
})
})
})
it('disables creation when the caller cannot manage keys', async () => {
await renderModal(datasetScope, { canManage: false })
expect(
screen.getByRole('button', {
name: 'appApi.apiKeyModal.createNewSecretKey',
}),
).toBeDisabled()
})
it('exposes an accessible close button', async () => {
const { onClose } = await renderModal(datasetScope)
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
expect(onClose).toHaveBeenCalled()
})
})
@@ -39,8 +39,8 @@ const SecretKeyButton = ({
<SecretKeyModal
isShow={isVisible}
onClose={() => setIsVisible(false)}
appId={appId}
canManage={canManage}
scope={appId ? { type: 'app', appId } : { type: 'dataset' }}
/>
</>
)
@@ -0,0 +1,154 @@
'use client'
import { skipToken, useMutation, useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { currentWorkspaceAtom } from '@/context/workspace-state'
import { consoleQuery } from '@/service/client'
import { SecretKeyModalView } from './view'
export type SecretKeyScope =
| { type: 'app'; appId: string }
| { type: 'dataset' }
| { type: 'environment'; appId: string; environmentId: string }
export type SecretKeyModalProps = {
isShow: boolean
canManage: boolean
scope: SecretKeyScope
onClose: () => void
}
type SecretKeyControllerProps = Omit<SecretKeyModalProps, 'scope'> & {
createDisabled: boolean
}
export default function SecretKeyModal({
isShow = false,
canManage,
scope,
onClose,
}: SecretKeyModalProps) {
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const controllerProps = {
canManage,
createDisabled: !currentWorkspace.id || !canManage,
isShow,
onClose,
}
switch (scope.type) {
case 'app':
return <AppSecretKeyController {...controllerProps} appId={scope.appId} />
case 'dataset':
return <DatasetSecretKeyController {...controllerProps} />
case 'environment':
return (
<EnvironmentSecretKeyController
{...controllerProps}
appId={scope.appId}
environmentId={scope.environmentId}
/>
)
}
}
function AppSecretKeyController({
appId,
...viewProps
}: SecretKeyControllerProps & { appId: string }) {
const apiKeysQuery = useQuery(
consoleQuery.apps.byResourceId.apiKeys.get.queryOptions({
input: viewProps.isShow ? { params: { resource_id: appId } } : skipToken,
}),
)
const createMutation = useMutation(consoleQuery.apps.byResourceId.apiKeys.post.mutationOptions())
const deleteMutation = useMutation(
consoleQuery.apps.byResourceId.apiKeys.byApiKeyId.delete.mutationOptions(),
)
return (
<SecretKeyModalView
{...viewProps}
apiKeys={apiKeysQuery.data?.data}
isCreating={createMutation.isPending}
isDeleting={deleteMutation.isPending}
isLoading={apiKeysQuery.isLoading}
onCreate={(onSuccess) =>
createMutation.mutate({ params: { resource_id: appId } }, { onSuccess })
}
onDelete={(keyId, onSuccess) =>
deleteMutation.mutate({ params: { resource_id: appId, api_key_id: keyId } }, { onSuccess })
}
/>
)
}
function DatasetSecretKeyController(viewProps: SecretKeyControllerProps) {
const apiKeysQuery = useQuery({
...consoleQuery.datasets.apiKeys.get.queryOptions(),
enabled: viewProps.isShow,
})
const createMutation = useMutation(consoleQuery.datasets.apiKeys.post.mutationOptions())
const deleteMutation = useMutation(
consoleQuery.datasets.apiKeys.byApiKeyId.delete.mutationOptions(),
)
return (
<SecretKeyModalView
{...viewProps}
apiKeys={apiKeysQuery.data?.data}
isCreating={createMutation.isPending}
isDeleting={deleteMutation.isPending}
isLoading={apiKeysQuery.isLoading}
onCreate={(onSuccess) => createMutation.mutate(undefined, { onSuccess })}
onDelete={(keyId, onSuccess) =>
deleteMutation.mutate({ params: { api_key_id: keyId } }, { onSuccess })
}
/>
)
}
function EnvironmentSecretKeyController({
appId,
environmentId,
...viewProps
}: SecretKeyControllerProps & { appId: string; environmentId: string }) {
const params = {
app_id: appId,
environment_id: environmentId,
}
const apiKeysQuery = useQuery(
consoleQuery.enterprise.appDeploy.accessService.listEnvironmentApiKeys.queryOptions({
input: { params },
enabled: viewProps.isShow,
}),
)
const createMutation = useMutation(
consoleQuery.enterprise.appDeploy.accessService.createEnvironmentApiKey.mutationOptions(),
)
const deleteMutation = useMutation(
consoleQuery.enterprise.appDeploy.accessService.deleteEnvironmentApiKey.mutationOptions(),
)
return (
<SecretKeyModalView
{...viewProps}
apiKeys={apiKeysQuery.data?.data}
isCreating={createMutation.isPending}
isDeleting={deleteMutation.isPending}
isLoading={apiKeysQuery.isLoading}
onCreate={(onSuccess) => createMutation.mutate({ params }, { onSuccess })}
onDelete={(keyId, onSuccess) =>
deleteMutation.mutate(
{
params: {
...params,
api_key_id: keyId,
},
},
{ onSuccess },
)
}
/>
)
}
@@ -1,5 +1,7 @@
'use client'
import type { ApiKeyItem } from '@dify/contracts/api/console/apps/types.gen'
import type { EnvironmentApiKey } from '@dify/contracts/enterprise-app-deploy/types.gen'
import {
AlertDialog,
AlertDialogActions,
@@ -12,120 +14,68 @@ import {
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { skipToken, useMutation, useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading'
import { currentWorkspaceAtom } from '@/context/workspace-state'
import useTimestamp from '@/hooks/use-timestamp'
import { consoleQuery } from '@/service/client'
import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css'
import SecretKeyGenerateModal from '../secret-key-generate'
import s from '../style.module.css'
type ISecretKeyModalProps = {
isShow: boolean
appId?: string
type SecretKeyItem = ApiKeyItem | EnvironmentApiKey
type CreatedSecretKey = Pick<ApiKeyItem, 'token'>
export type SecretKeyModalViewProps = {
apiKeys?: SecretKeyItem[]
canManage: boolean
createDisabled: boolean
isCreating: boolean
isDeleting: boolean
isLoading: boolean
isShow: boolean
onClose: () => void
onCreate: (onSuccess: (createdKey: CreatedSecretKey) => void) => void
onDelete: (keyId: string, onSuccess: () => void) => void
}
const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKeyModalProps) => {
export function SecretKeyModalView({
apiKeys,
canManage,
createDisabled,
isCreating,
isDeleting,
isLoading,
isShow,
onClose,
onCreate,
onDelete,
}: SecretKeyModalViewProps) {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [newKey, setNewKey] = useState<Pick<ApiKeyItem, 'token'> | undefined>(undefined)
const createAppApiKey = useMutation(consoleQuery.apps.byResourceId.apiKeys.post.mutationOptions())
const deleteAppApiKey = useMutation(
consoleQuery.apps.byResourceId.apiKeys.byApiKeyId.delete.mutationOptions(),
)
const createDatasetApiKey = useMutation(consoleQuery.datasets.apiKeys.post.mutationOptions())
const deleteDatasetApiKey = useMutation(
consoleQuery.datasets.apiKeys.byApiKeyId.delete.mutationOptions(),
)
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useQuery(
consoleQuery.apps.byResourceId.apiKeys.get.queryOptions({
input: appId && isShow ? { params: { resource_id: appId } } : skipToken,
}),
)
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading } = useQuery({
...consoleQuery.datasets.apiKeys.get.queryOptions(),
enabled: !appId && isShow,
})
const apiKeysList = appId ? appApiKeys : datasetApiKeys
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
const [deleteKeyId, setDeleteKeyId] = useState<string>()
const [newKey, setNewKey] = useState<CreatedSecretKey>()
const [delKeyID, setDelKeyId] = useState('')
const onDel = async () => {
setShowConfirmDelete(false)
if (!canManage) return
if (!delKeyID) return
try {
if (appId) {
deleteAppApiKey.mutate({
params: { resource_id: appId, api_key_id: delKeyID },
})
return
}
deleteDatasetApiKey.mutate({
params: { api_key_id: delKeyID },
})
} catch (error) {
console.error(error)
}
const handleClose = () => {
setDeleteKeyId(undefined)
setNewKey(undefined)
onClose()
}
const onCreate = async () => {
if (!currentWorkspace.id || !canManage) return
const handleCreate = () => {
if (createDisabled || isCreating) return
onCreate(setNewKey)
}
try {
if (appId) {
createAppApiKey.mutate(
{ params: { resource_id: appId } },
{
onSuccess: (apiKey) => {
setIsVisible(true)
setNewKey(apiKey)
},
},
)
return
}
createDatasetApiKey.mutate(undefined, {
onSuccess: (apiKey) => {
setIsVisible(true)
setNewKey(apiKey)
},
})
} catch (error) {
console.error(error)
}
const handleDelete = () => {
if (!deleteKeyId || isDeleting) return
onDelete(deleteKeyId, () => setDeleteKeyId(undefined))
}
const generateToken = (token: string) => {
return `${token.slice(0, 3)}...${token.slice(-20)}`
}
const handleDeleteConfirmOpenChange = (open: boolean) => {
if (open) return
setDelKeyId('')
setShowConfirmDelete(false)
}
const handleClose = () => {
setIsVisible(false)
onClose()
}
return (
<>
<Dialog
@@ -141,7 +91,7 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
)}
>
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t(($) => $['apiKeyModal.apiSecretKey'], { ns: 'appApi' })}`}
{t(($) => $['apiKeyModal.apiSecretKey'], { ns: 'appApi' })}
</DialogTitle>
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
@@ -160,12 +110,12 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
<p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">
{t(($) => $['apiKeyModal.apiSecretKeyTips'], { ns: 'appApi' })}
</p>
{isApiKeysLoading && (
{isLoading && (
<div className="mt-4">
<Loading />
</div>
)}
{!!apiKeysList?.data?.length && (
{!!apiKeys?.length && (
<div className="mt-4 flex grow flex-col overflow-hidden">
<div className="flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary">
<div className="w-64 shrink-0 px-3">
@@ -177,10 +127,10 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
<div className="w-50 shrink-0 px-3">
{t(($) => $['apiKeyModal.lastUsed'], { ns: 'appApi' })}
</div>
<div className="grow px-3"></div>
<div className="grow px-3" />
</div>
<div className="grow overflow-auto">
{apiKeysList.data.map((api) => (
{apiKeys.map((api) => (
<div
className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary"
key={api.id}
@@ -207,10 +157,7 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
{canManage && (
<ActionButton
aria-label={`${t(($) => $['operation.delete'], { ns: 'common' })} ${generateToken(api.token)}`}
onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}
onClick={() => setDeleteKeyId(api.id)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</ActionButton>
@@ -224,8 +171,9 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
<div className="flex">
<Button
className={`mt-4 flex shrink-0 ${s.autoWidth}`}
onClick={onCreate}
disabled={!currentWorkspace.id || !canManage}
onClick={handleCreate}
disabled={createDisabled || isCreating}
loading={isCreating}
>
<span className="i-heroicons-plus-20-solid flex size-4 shrink-0" />
<div className="text-xs font-medium text-text-secondary">
@@ -233,7 +181,12 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
</div>
</Button>
</div>
<AlertDialog open={showConfirmDelete} onOpenChange={handleDeleteConfirmOpenChange}>
<AlertDialog
open={deleteKeyId !== undefined}
onOpenChange={(open) => {
if (!open && !isDeleting) setDeleteKeyId(undefined)
}}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
@@ -244,10 +197,10 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
<AlertDialogCancelButton disabled={isDeleting}>
{t(($) => $['operation.cancel'], { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onDel}>
<AlertDialogConfirmButton loading={isDeleting} onClick={handleDelete}>
{t(($) => $['operation.confirm'], { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
@@ -255,16 +208,12 @@ const SecretKeyModal = ({ isShow = false, appId, canManage, onClose }: ISecretKe
</AlertDialog>
</DialogContent>
</Dialog>
{isShow && (
<SecretKeyGenerateModal
className="shrink-0"
isShow={isVisible}
onClose={() => setIsVisible(false)}
newKey={newKey}
/>
)}
<SecretKeyGenerateModal
className="shrink-0"
isShow={isShow && newKey !== undefined}
onClose={() => setNewKey(undefined)}
newKey={newKey}
/>
</>
)
}
export default SecretKeyModal