mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
refactor(web): use generated dataset card queries (#39203)
This commit is contained in:
parent
5c6372d2f7
commit
a7ef41a8f5
@ -1,341 +1,84 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { createElement } from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { useDatasetCardState } from '../use-dataset-card-state'
|
||||
|
||||
const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({
|
||||
mockToastSuccess: vi.fn(),
|
||||
mockToastError: vi.fn(),
|
||||
const mocks = vi.hoisted(() => ({
|
||||
request: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
success: mockToastSuccess,
|
||||
error: mockToastError,
|
||||
},
|
||||
}))
|
||||
|
||||
const mockCheckUsage = vi.fn()
|
||||
const mockDeleteDataset = vi.fn()
|
||||
const mockExportPipeline = vi.fn()
|
||||
const mockPush = vi.fn()
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-dataset-card', () => ({
|
||||
useCheckDatasetUsage: () => ({ mutateAsync: mockCheckUsage }),
|
||||
useDeleteDataset: () => ({ mutateAsync: mockDeleteDataset }),
|
||||
vi.mock('@/service/base', () => ({
|
||||
request: (...args: unknown[]) => mocks.request(...args),
|
||||
sseGeneratorPost: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useExportPipelineDSL: () => ({ mutateAsync: mockExportPipeline }),
|
||||
useExportPipelineDSL: () => ({ mutateAsync: vi.fn() }),
|
||||
}))
|
||||
|
||||
const dataset = {
|
||||
id: 'dataset-1',
|
||||
name: 'Test Dataset',
|
||||
} as DataSet
|
||||
|
||||
function renderDatasetCardState() {
|
||||
const onSuccess = vi.fn()
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
mutations: { retry: false },
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children)
|
||||
const rendered = renderHook(useDatasetCardState, {
|
||||
initialProps: { dataset, onSuccess },
|
||||
wrapper,
|
||||
})
|
||||
|
||||
return { ...rendered, onSuccess }
|
||||
}
|
||||
|
||||
describe('useDatasetCardState', () => {
|
||||
const createMockDataset = (overrides: Partial<DataSet> = {}): DataSet =>
|
||||
({
|
||||
id: 'dataset-1',
|
||||
name: 'Test Dataset',
|
||||
description: 'Test description',
|
||||
provider: 'vendor',
|
||||
permission: DatasetPermission.allTeamMembers,
|
||||
data_source_type: DataSourceType.FILE,
|
||||
indexing_technique: IndexingType.QUALIFIED,
|
||||
embedding_available: true,
|
||||
app_count: 5,
|
||||
document_count: 10,
|
||||
word_count: 1000,
|
||||
created_at: 1609459200,
|
||||
updated_at: 1609545600,
|
||||
tags: [{ id: 'tag-1', name: 'Tag 1', type: 'knowledge', binding_count: '' }],
|
||||
embedding_model: 'text-embedding-ada-002',
|
||||
embedding_model_provider: 'openai',
|
||||
created_by: 'user-1',
|
||||
doc_form: ChunkingMode.text,
|
||||
pipeline_id: 'pipeline-1',
|
||||
...overrides,
|
||||
}) as DataSet
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCheckUsage.mockResolvedValue({ is_using: false })
|
||||
mockDeleteDataset.mockResolvedValue({})
|
||||
mockExportPipeline.mockResolvedValue({ data: 'yaml content' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have initial modal state closed', () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
expect(result.current.modalState.showRenameModal).toBe(false)
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(false)
|
||||
expect(result.current.modalState.confirmMessage).toBe('')
|
||||
})
|
||||
|
||||
it('should not be exporting initially', () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
expect(result.current.exporting).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Modal Handlers', () => {
|
||||
it('should open rename modal when openRenameModal is called', () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
act(() => {
|
||||
result.current.openRenameModal()
|
||||
})
|
||||
|
||||
expect(result.current.modalState.showRenameModal).toBe(true)
|
||||
})
|
||||
|
||||
it('should close rename modal when closeRenameModal is called', () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
act(() => {
|
||||
result.current.openRenameModal()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.closeRenameModal()
|
||||
})
|
||||
|
||||
expect(result.current.modalState.showRenameModal).toBe(false)
|
||||
})
|
||||
|
||||
it('should close confirm delete modal when closeConfirmDelete is called', async () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
// First trigger show confirm delete
|
||||
act(() => {
|
||||
result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(true)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.closeConfirmDelete()
|
||||
})
|
||||
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectIsUsedByApp', () => {
|
||||
it('should check usage and show confirm modal with not-in-use message', async () => {
|
||||
mockCheckUsage.mockResolvedValue({ is_using: false })
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
expect(mockCheckUsage).toHaveBeenCalledWith('dataset-1')
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(true)
|
||||
expect(result.current.modalState.confirmMessage).toContain('deleteDatasetConfirmContent')
|
||||
})
|
||||
|
||||
it('should show in-use message when dataset is used by app', async () => {
|
||||
mockCheckUsage.mockResolvedValue({ is_using: true })
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
expect(result.current.modalState.confirmMessage).toContain('datasetUsedByApp')
|
||||
})
|
||||
})
|
||||
|
||||
describe('onConfirmDelete', () => {
|
||||
it('should delete dataset and call onSuccess', async () => {
|
||||
const onSuccess = vi.fn()
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onConfirmDelete()
|
||||
})
|
||||
|
||||
expect(mockDeleteDataset).toHaveBeenCalledWith('dataset-1')
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close confirm modal after delete', async () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
// First open confirm modal
|
||||
await act(async () => {
|
||||
await result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onConfirmDelete()
|
||||
})
|
||||
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleExportPipeline', () => {
|
||||
it('should not export if pipeline_id is missing', async () => {
|
||||
const dataset = createMockDataset({ pipeline_id: undefined })
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportPipeline()
|
||||
})
|
||||
|
||||
expect(mockExportPipeline).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should export pipeline with correct parameters', async () => {
|
||||
const dataset = createMockDataset({ pipeline_id: 'pipeline-1', name: 'Test Pipeline' })
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportPipeline(true)
|
||||
})
|
||||
|
||||
expect(mockExportPipeline).toHaveBeenCalledWith({
|
||||
pipelineId: 'pipeline-1',
|
||||
include: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined onSuccess', async () => {
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset }))
|
||||
|
||||
// Should not throw when onSuccess is undefined
|
||||
await act(async () => {
|
||||
await result.current.onConfirmDelete()
|
||||
})
|
||||
|
||||
expect(mockDeleteDataset).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should show error toast when export pipeline fails', async () => {
|
||||
const { toast } = await import('@langgenius/dify-ui/toast')
|
||||
mockExportPipeline.mockRejectedValue(new Error('Export failed'))
|
||||
|
||||
const dataset = createMockDataset({ pipeline_id: 'pipeline-1' })
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportPipeline()
|
||||
})
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.any(String))
|
||||
})
|
||||
|
||||
it('should handle Response error in detectIsUsedByApp', async () => {
|
||||
const { toast } = await import('@langgenius/dify-ui/toast')
|
||||
const mockResponse = new Response(JSON.stringify({ message: 'API Error' }), {
|
||||
status: 400,
|
||||
})
|
||||
mockCheckUsage.mockRejectedValue(mockResponse)
|
||||
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('API Error'))
|
||||
})
|
||||
|
||||
it('should handle generic Error in detectIsUsedByApp', async () => {
|
||||
const { toast } = await import('@langgenius/dify-ui/toast')
|
||||
mockCheckUsage.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Network error')
|
||||
})
|
||||
|
||||
it('should handle error without message in detectIsUsedByApp', async () => {
|
||||
const { toast } = await import('@langgenius/dify-ui/toast')
|
||||
mockCheckUsage.mockRejectedValue({})
|
||||
|
||||
const dataset = createMockDataset()
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.detectIsUsedByApp()
|
||||
})
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('dataset.unknownError')
|
||||
})
|
||||
|
||||
it('should handle exporting state correctly', async () => {
|
||||
const dataset = createMockDataset({ pipeline_id: 'pipeline-1' })
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
// Exporting should initially be false
|
||||
expect(result.current.exporting).toBe(false)
|
||||
|
||||
// Export should work when not exporting
|
||||
await act(async () => {
|
||||
await result.current.handleExportPipeline()
|
||||
})
|
||||
|
||||
expect(mockExportPipeline).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reset exporting state after export completes', async () => {
|
||||
const dataset = createMockDataset({ pipeline_id: 'pipeline-1' })
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportPipeline()
|
||||
})
|
||||
|
||||
expect(result.current.exporting).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset exporting state even when export fails', async () => {
|
||||
mockExportPipeline.mockRejectedValue(new Error('Export failed'))
|
||||
|
||||
const dataset = createMockDataset({ pipeline_id: 'pipeline-1' })
|
||||
const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportPipeline()
|
||||
})
|
||||
|
||||
expect(result.current.exporting).toBe(false)
|
||||
})
|
||||
it('uses the latest usage state before deleting the dataset', async () => {
|
||||
mocks.request
|
||||
.mockResolvedValueOnce(Response.json({ is_using: false }))
|
||||
.mockResolvedValueOnce(Response.json({ is_using: true }))
|
||||
.mockResolvedValueOnce(new Response(null, { status: 204 }))
|
||||
const { onSuccess, result } = renderDatasetCardState()
|
||||
|
||||
await act(result.current.detectIsUsedByApp)
|
||||
expect(result.current.modalState.confirmMessage).toContain('deleteDatasetConfirmContent')
|
||||
|
||||
act(result.current.closeConfirmDelete)
|
||||
await act(result.current.detectIsUsedByApp)
|
||||
|
||||
expect(result.current.modalState.confirmMessage).toContain('datasetUsedByApp')
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(true)
|
||||
expect(mocks.request).toHaveBeenCalledTimes(2)
|
||||
expect(
|
||||
mocks.request.mock.calls.every(([url]) =>
|
||||
String(url).endsWith('/datasets/dataset-1/use-check'),
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
await act(result.current.onConfirmDelete)
|
||||
|
||||
expect(mocks.request).toHaveBeenCalledTimes(3)
|
||||
const deleteRequest = mocks.request.mock.calls[2]?.[2]?.request as Request
|
||||
expect(deleteRequest.method).toBe('DELETE')
|
||||
expect(deleteRequest.url).toContain('/datasets/dataset-1')
|
||||
expect(result.current.modalState.showConfirmDelete).toBe(false)
|
||||
expect(onSuccess).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
|
||||
@ -22,6 +23,7 @@ type UseDatasetCardStateOptions = {
|
||||
export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateOptions) => {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Modal state
|
||||
const [modalState, setModalState] = useState<ModalState>({
|
||||
@ -56,8 +58,9 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO
|
||||
}, [])
|
||||
|
||||
// API mutations
|
||||
const { mutateAsync: checkUsage } = useCheckDatasetUsage()
|
||||
const { mutateAsync: deleteDatasetMutation } = useDeleteDataset()
|
||||
const { mutateAsync: deleteDatasetMutation } = useMutation(
|
||||
consoleQuery.datasets.byDatasetId.delete.mutationOptions(),
|
||||
)
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
|
||||
// Export pipeline handler
|
||||
@ -86,7 +89,18 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO
|
||||
// Delete flow handlers
|
||||
const detectIsUsedByApp = useCallback(async () => {
|
||||
try {
|
||||
const { is_using: isUsedByApp } = await checkUsage(dataset.id)
|
||||
const { is_using: isUsedByApp } = await queryClient.fetchQuery(
|
||||
consoleQuery.datasets.byDatasetId.useCheck.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
dataset_id: dataset.id,
|
||||
},
|
||||
},
|
||||
staleTime: 0,
|
||||
retry: false,
|
||||
context: { silent: true },
|
||||
}),
|
||||
)
|
||||
const message = isUsedByApp
|
||||
? t(($) => $.datasetUsedByApp, { ns: 'dataset' })!
|
||||
: t(($) => $.deleteDatasetConfirmContent, { ns: 'dataset' })!
|
||||
@ -103,11 +117,15 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO
|
||||
toast.error((e as Error)?.message || t(($) => $.unknownError, { ns: 'dataset' }))
|
||||
}
|
||||
}
|
||||
}, [dataset.id, checkUsage, t])
|
||||
}, [dataset.id, queryClient, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDatasetMutation(dataset.id)
|
||||
await deleteDatasetMutation({
|
||||
params: {
|
||||
dataset_id: dataset.id,
|
||||
},
|
||||
})
|
||||
toast.success(t(($) => $.datasetDeleted, { ns: 'dataset' }))
|
||||
onSuccess?.()
|
||||
} finally {
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { checkIsUsedInApp, deleteDataset } from './datasets'
|
||||
|
||||
const NAME_SPACE = 'dataset-card'
|
||||
|
||||
export const useCheckDatasetUsage = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'check-usage'],
|
||||
mutationFn: (datasetId: string) => checkIsUsedInApp(datasetId),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteDataset = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'delete'],
|
||||
mutationFn: (datasetId: string) => deleteDataset(datasetId),
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user