refactor(web): drop headless-ui, migrate overlay to dify-ui (#35963)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-05-09 10:33:25 +00:00
committed by GitHub
co-authored by CodingOnStar autofix-ci[bot] yyh yyh
parent f720a3bed2
commit 8581a68174
187 changed files with 5333 additions and 6395 deletions
@@ -88,7 +88,7 @@ describe('SecretKeyModal', () => {
beforeEach(() => {
vi.clearAllMocks()
// Suppress expected React act() warnings from Headless UI Dialog transitions and async API state updates
// Suppress expected React act() warnings from modal transitions and async API state updates.
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.useFakeTimers({ shouldAdvanceTime: true })
mockCurrentWorkspace.mockReturnValue({ id: 'workspace-1', name: 'Test Workspace' })
@@ -290,6 +290,36 @@ describe('SecretKeyModal', () => {
})
})
it('should place the generated key backdrop above the API keys modal', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
mockAppApiKeysData.mockReturnValue({
data: [
{ id: 'key-1', token: 'sk-abc123def456ghi789', created_at: 1700000000, last_used_at: null },
],
})
await renderModal(<SecretKeyModal {...defaultProps} appId="app-123" />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey')
await act(async () => {
await user.click(createButton)
})
await waitFor(() => {
expect(screen.getByText('appApi.apiKeyModal.generateTips')).toBeInTheDocument()
})
const parentDialog = screen.getByText('appApi.apiKeyModal.apiSecretKeyTips').closest('[role="dialog"]')
const generatedKeyDialog = screen.getByText('appApi.apiKeyModal.generateTips').closest('[role="dialog"]')
const backdrops = document.body.querySelectorAll('.bg-background-overlay')
const generatedKeyBackdrop = backdrops[1]
expect(parentDialog).toBeInTheDocument()
expect(generatedKeyDialog).toBeInTheDocument()
expect(backdrops).toHaveLength(2)
expect(parentDialog!.compareDocumentPosition(generatedKeyBackdrop!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(generatedKeyBackdrop!.compareDocumentPosition(generatedKeyDialog!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
})
it('should invalidate app API keys after creating', async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
await renderModal(<SecretKeyModal {...defaultProps} appId="app-123" />)
@@ -2,8 +2,9 @@
import type { CreateApiKeyResponse } from '@/models/app'
import { XMarkIcon } from '@heroicons/react/20/solid'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import InputCopy from './input-copy'
import s from './style.module.css'
@@ -22,21 +23,33 @@ const SecretKeyGenerateModal = ({
}: ISecretKeyGenerateModalProps) => {
const { t } = useTranslation()
return (
<Modal isShow={isShow} onClose={onClose} title={`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`} className={`px-8 ${className}`}>
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className="mt-1 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.generateTips', { ns: 'appApi' })}</p>
<div className="my-4">
<InputCopy className="w-full" value={newKey?.token} />
</div>
<div className="my-4 flex justify-end">
<Button className={`shrink-0 ${s.w64}`} onClick={onClose}>
<span className="text-xs font-medium text-text-secondary">{t('actionMsg.ok', { ns: 'appApi' })}</span>
</Button>
</div>
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<DialogContent className={cn('w-full max-w-[480px] overflow-hidden! border-none px-8 text-left align-middle', className)}>
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
</DialogTitle>
</Modal>
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className="mt-1 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.generateTips', { ns: 'appApi' })}</p>
<div className="my-4">
<InputCopy className="w-full" value={newKey?.token} />
</div>
<div className="my-4 flex justify-end">
<Button className={`shrink-0 ${s.w64}`} onClick={onClose}>
<span className="text-xs font-medium text-text-secondary">{t('actionMsg.ok', { ns: 'appApi' })}</span>
</Button>
</div>
</DialogContent>
</Dialog>
)
}
@@ -11,6 +11,8 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { RiDeleteBinLine } from '@remixicon/react'
import {
useState,
@@ -19,7 +21,6 @@ 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 Modal from '@/app/components/base/modal'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
import {
@@ -103,78 +104,99 @@ const SecretKeyModal = ({
setShowConfirmDelete(false)
}
const handleClose = () => {
setVisible(false)
onClose()
}
return (
<Modal isShow={isShow} onClose={onClose} title={`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`} className={`${s.customModal} flex flex-col px-8`}>
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
{isApiKeysLoading && <div className="mt-4"><Loading /></div>}
{
!!apiKeysList?.data?.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">{t('apiKeyModal.secretKey', { ns: 'appApi' })}</div>
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.created', { ns: 'appApi' })}</div>
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.lastUsed', { ns: 'appApi' })}</div>
<div className="grow px-3"></div>
</div>
<div className="grow overflow-auto">
{apiKeysList.data.map(api => (
<div className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary" key={api.id}>
<div className="w-64 shrink-0 truncate px-3 font-mono">{generateToken(api.token)}</div>
<div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
<div className="w-[200px] shrink-0 truncate px-3">{api.last_used_at ? formatTime(Number(api.last_used_at), t('dateTimeFormat', { ns: 'appLog' }) as string) : t('never', { ns: 'appApi' })}</div>
<div className="flex grow space-x-2 px-3">
<CopyFeedback content={api.token} />
{isCurrentWorkspaceManager && (
<ActionButton
onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}
>
<RiDeleteBinLine className="h-4 w-4" />
</ActionButton>
)}
</div>
</div>
))}
</div>
</div>
)
}
<div className="flex">
<Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace || !isCurrentWorkspaceEditor}>
<PlusIcon className="mr-1 flex h-4 w-4 shrink-0" />
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
</Button>
</div>
<SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setVisible(false)} newKey={newKey} />
<AlertDialog
open={showConfirmDelete}
onOpenChange={handleDeleteConfirmOpenChange}
<>
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
handleClose()
}}
>
<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">
{t('actionMsg.deleteConfirmTitle', { ns: 'appApi' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('actionMsg.deleteConfirmTips', { ns: 'appApi' })}
</AlertDialogDescription>
<DialogContent className={cn('max-h-[calc(100vh-80px)]! w-full max-w-[800px]! overflow-hidden! border-none text-left align-middle', `${s.customModal} flex flex-col px-8`)}>
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
</DialogTitle>
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={handleClose} />
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onDel}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</Modal>
<p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
{isApiKeysLoading && <div className="mt-4"><Loading /></div>}
{
!!apiKeysList?.data?.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">{t('apiKeyModal.secretKey', { ns: 'appApi' })}</div>
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.created', { ns: 'appApi' })}</div>
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.lastUsed', { ns: 'appApi' })}</div>
<div className="grow px-3"></div>
</div>
<div className="grow overflow-auto">
{apiKeysList.data.map(api => (
<div className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary" key={api.id}>
<div className="w-64 shrink-0 truncate px-3 font-mono">{generateToken(api.token)}</div>
<div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
<div className="w-[200px] shrink-0 truncate px-3">{api.last_used_at ? formatTime(Number(api.last_used_at), t('dateTimeFormat', { ns: 'appLog' }) as string) : t('never', { ns: 'appApi' })}</div>
<div className="flex grow space-x-2 px-3">
<CopyFeedback content={api.token} />
{isCurrentWorkspaceManager && (
<ActionButton
onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}
>
<RiDeleteBinLine className="h-4 w-4" />
</ActionButton>
)}
</div>
</div>
))}
</div>
</div>
)
}
<div className="flex">
<Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace || !isCurrentWorkspaceEditor}>
<PlusIcon className="mr-1 flex h-4 w-4 shrink-0" />
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
</Button>
</div>
<AlertDialog
open={showConfirmDelete}
onOpenChange={handleDeleteConfirmOpenChange}
>
<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">
{t('actionMsg.deleteConfirmTitle', { ns: 'appApi' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('actionMsg.deleteConfirmTips', { ns: 'appApi' })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onDel}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</DialogContent>
</Dialog>
{isShow && (
<SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setVisible(false)} newKey={newKey} />
)}
</>
)
}