mirror of
https://github.com/langgenius/dify.git
synced 2026-05-30 05:37:48 +08:00
feat: refactor dataset permissions handling and remove legacy workspace role checks
This commit is contained in:
@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DatasetInfo from '@/app/components/app-sidebar/dataset-info'
|
||||
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { DatasetACLPermission } from '@/utils/permission'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
const mockInvalidDatasetList = vi.fn()
|
||||
@ -33,11 +34,6 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) =>
|
||||
selector({ isCurrentWorkspaceDatasetOperator: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-knowledge', () => ({
|
||||
useKnowledge: () => ({
|
||||
formatIndexingTechniqueAndMethod: () => 'indexing-technique',
|
||||
@ -153,6 +149,11 @@ const createDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
|
||||
enable_api: false,
|
||||
is_multimodal: false,
|
||||
is_published: true,
|
||||
permission_keys: [
|
||||
DatasetACLPermission.Edit,
|
||||
DatasetACLPermission.Delete,
|
||||
DatasetACLPermission.ImportExportDSL,
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
|
||||
@ -8,10 +8,10 @@ import {
|
||||
DataSourceType,
|
||||
} from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { DatasetACLPermission } from '@/utils/permission'
|
||||
import Dropdown from '../dropdown'
|
||||
|
||||
let mockDataset: DataSet
|
||||
let mockIsDatasetOperator = false
|
||||
const mockReplace = vi.fn()
|
||||
const mockInvalidDatasetList = vi.fn()
|
||||
const mockInvalidDatasetDetail = vi.fn()
|
||||
@ -78,6 +78,11 @@ const createDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
|
||||
runtime_mode: 'rag_pipeline',
|
||||
enable_api: false,
|
||||
is_multimodal: false,
|
||||
permission_keys: [
|
||||
DatasetACLPermission.Edit,
|
||||
DatasetACLPermission.Delete,
|
||||
DatasetACLPermission.ImportExportDSL,
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@ -89,11 +94,6 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ dataset: mockDataset }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) =>
|
||||
selector({ isCurrentWorkspaceDatasetOperator: mockIsDatasetOperator }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
datasetDetailQueryKeyPrefix: ['dataset', 'detail'],
|
||||
useInvalidDatasetList: () => mockInvalidDatasetList,
|
||||
@ -143,7 +143,6 @@ describe('Dropdown callback coverage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDataset = createDataset({ pipeline_id: 'pipeline-1', runtime_mode: 'rag_pipeline' })
|
||||
mockIsDatasetOperator = false
|
||||
mockExportPipeline.mockResolvedValue({ data: 'pipeline-content' })
|
||||
mockCheckIsUsedInApp.mockResolvedValue({ is_using: false })
|
||||
mockDeleteDataset.mockResolvedValue({})
|
||||
|
||||
@ -8,13 +8,13 @@ import {
|
||||
DataSourceType,
|
||||
} from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { DatasetACLPermission } from '@/utils/permission'
|
||||
import DatasetInfo from '..'
|
||||
import Dropdown from '../dropdown'
|
||||
import Menu from '../menu'
|
||||
import MenuItem from '../menu-item'
|
||||
|
||||
let mockDataset: DataSet
|
||||
let mockIsDatasetOperator = false
|
||||
const mockReplace = vi.fn()
|
||||
const mockInvalidDatasetList = vi.fn()
|
||||
const mockInvalidDatasetDetail = vi.fn()
|
||||
@ -87,6 +87,11 @@ const createDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
|
||||
runtime_mode: 'rag_pipeline',
|
||||
enable_api: false,
|
||||
is_multimodal: false,
|
||||
permission_keys: [
|
||||
DatasetACLPermission.Edit,
|
||||
DatasetACLPermission.Delete,
|
||||
DatasetACLPermission.ImportExportDSL,
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@ -100,11 +105,6 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ dataset: mockDataset }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) =>
|
||||
selector({ isCurrentWorkspaceDatasetOperator: mockIsDatasetOperator }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
datasetDetailQueryKeyPrefix: ['dataset', 'detail'],
|
||||
useInvalidDatasetList: () => mockInvalidDatasetList,
|
||||
@ -161,7 +161,6 @@ describe('DatasetInfo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDataset = createDataset()
|
||||
mockIsDatasetOperator = false
|
||||
})
|
||||
|
||||
// Rendering of dataset summary details based on expand and dataset state.
|
||||
@ -337,7 +336,6 @@ describe('Dropdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDataset = createDataset({ pipeline_id: 'pipeline-1', runtime_mode: 'rag_pipeline' })
|
||||
mockIsDatasetOperator = false
|
||||
mockExportPipeline.mockResolvedValue({ data: 'pipeline-content' })
|
||||
mockCheckIsUsedInApp.mockResolvedValue({ is_using: false })
|
||||
mockDeleteDataset.mockResolvedValue({})
|
||||
@ -355,12 +353,19 @@ describe('Dropdown', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Rendering behavior based on workspace role.
|
||||
// Rendering behavior based on dataset ACL permission keys.
|
||||
describe('Rendering', () => {
|
||||
it('should hide delete option when user is dataset operator', async () => {
|
||||
it('should hide delete option when dataset lacks delete ACL permission', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
mockIsDatasetOperator = true
|
||||
mockDataset = createDataset({
|
||||
pipeline_id: 'pipeline-1',
|
||||
runtime_mode: 'rag_pipeline',
|
||||
permission_keys: [
|
||||
DatasetACLPermission.Edit,
|
||||
DatasetACLPermission.ImportExportDSL,
|
||||
],
|
||||
})
|
||||
render(<Dropdown expand />)
|
||||
|
||||
// Act
|
||||
|
||||
@ -18,7 +18,6 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
|
||||
@ -64,7 +63,6 @@ const DropDown = ({
|
||||
const [confirmMessage, setConfirmMessage] = useState<string>('')
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
|
||||
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
|
||||
const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet
|
||||
const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(dataset?.permission_keys), [dataset?.permission_keys])
|
||||
const canShowOperations = datasetACLCapabilities.canEdit
|
||||
@ -155,7 +153,7 @@ const DropDown = ({
|
||||
>
|
||||
<Menu
|
||||
showEdit={datasetACLCapabilities.canEdit}
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator && datasetACLCapabilities.canDelete}
|
||||
showDelete={datasetACLCapabilities.canDelete}
|
||||
showExportPipeline={datasetACLCapabilities.canImportExportDSL}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
|
||||
@ -25,10 +25,6 @@ vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => boolean) => selector({ isCurrentWorkspaceDatasetOperator: false }),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-dataset-card-state', () => ({
|
||||
useDatasetCardState: () => ({
|
||||
modalState: {
|
||||
@ -55,6 +51,7 @@ vi.mock('../components/dataset-card-modals', () => ({
|
||||
default: () => <div data-testid="card-modals" />,
|
||||
}))
|
||||
const renderDatasetCardTags = vi.hoisted(() => vi.fn())
|
||||
const renderOperationsDropdown = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/features/tag-management/components/dataset-card-tags', () => ({
|
||||
DatasetCardTags: (props: { onClick: (e: React.MouseEvent) => void, canBindOrUnbindTags?: boolean }) => {
|
||||
@ -65,7 +62,10 @@ vi.mock('@/features/tag-management/components/dataset-card-tags', () => ({
|
||||
},
|
||||
}))
|
||||
vi.mock('../components/operations-dropdown', () => ({
|
||||
default: () => <div data-testid="operations-dropdown" />,
|
||||
default: (props: Record<string, unknown>) => {
|
||||
renderOperationsDropdown(props)
|
||||
return <div data-testid="operations-dropdown" />
|
||||
},
|
||||
}))
|
||||
|
||||
// Factory function for DataSet mock data
|
||||
@ -285,4 +285,18 @@ describe('DatasetCard Component', () => {
|
||||
canBindOrUnbindTags: true,
|
||||
}))
|
||||
})
|
||||
|
||||
it('should pass dataset operations without legacy workspace-role props', () => {
|
||||
const dataset = createMockDataset({
|
||||
permission_keys: [DatasetACLPermission.Delete],
|
||||
})
|
||||
render(<DatasetCard dataset={dataset} />)
|
||||
|
||||
expect(renderOperationsDropdown).toHaveBeenCalledWith(expect.objectContaining({
|
||||
dataset,
|
||||
}))
|
||||
expect(renderOperationsDropdown).toHaveBeenCalledWith(expect.not.objectContaining({
|
||||
isCurrentWorkspaceDatasetOperator: expect.any(Boolean),
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
|
||||
import { DatasetACLPermission } from '@/utils/permission'
|
||||
import OperationsDropdown from '../operations-dropdown'
|
||||
|
||||
describe('OperationsDropdown', () => {
|
||||
@ -25,12 +26,17 @@ describe('OperationsDropdown', () => {
|
||||
created_by: 'user-1',
|
||||
doc_form: ChunkingMode.text,
|
||||
runtime_mode: 'general',
|
||||
permission_keys: [
|
||||
DatasetACLPermission.Edit,
|
||||
DatasetACLPermission.Delete,
|
||||
DatasetACLPermission.ImportExportDSL,
|
||||
DatasetACLPermission.AccessConfig,
|
||||
],
|
||||
...overrides,
|
||||
} as DataSet)
|
||||
|
||||
const defaultProps = {
|
||||
dataset: createMockDataset(),
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
openRenameModal: vi.fn(),
|
||||
handleExportPipeline: vi.fn(),
|
||||
detectIsUsedByApp: vi.fn(),
|
||||
@ -66,38 +72,41 @@ describe('OperationsDropdown', () => {
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should show delete option when not workspace dataset operator', () => {
|
||||
render(<OperationsDropdown {...defaultProps} isCurrentWorkspaceDatasetOperator={false} />)
|
||||
it('should show delete option when dataset has delete ACL permission', () => {
|
||||
render(<OperationsDropdown {...defaultProps} />)
|
||||
|
||||
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
|
||||
if (triggerButton)
|
||||
fireEvent.click(triggerButton)
|
||||
fireEvent.click(screen.getByLabelText('Dataset operations'))
|
||||
|
||||
expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide delete option when is workspace dataset operator', () => {
|
||||
render(<OperationsDropdown {...defaultProps} isCurrentWorkspaceDatasetOperator={true} />)
|
||||
it('should hide delete option when dataset lacks delete ACL permission', () => {
|
||||
const dataset = createMockDataset({
|
||||
permission_keys: [DatasetACLPermission.Edit],
|
||||
})
|
||||
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
|
||||
|
||||
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
|
||||
if (triggerButton)
|
||||
fireEvent.click(triggerButton)
|
||||
fireEvent.click(screen.getByLabelText('Dataset operations'))
|
||||
|
||||
expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show export pipeline when runtime_mode is rag_pipeline', () => {
|
||||
const dataset = createMockDataset({ runtime_mode: 'rag_pipeline' })
|
||||
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
|
||||
|
||||
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
|
||||
if (triggerButton)
|
||||
fireEvent.click(triggerButton)
|
||||
fireEvent.click(screen.getByLabelText('Dataset operations'))
|
||||
|
||||
expect(screen.getByText('datasetPipeline.operations.exportPipeline')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide export pipeline when runtime_mode is not rag_pipeline', () => {
|
||||
const dataset = createMockDataset({ runtime_mode: 'general' })
|
||||
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
|
||||
|
||||
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
|
||||
if (triggerButton)
|
||||
fireEvent.click(triggerButton)
|
||||
fireEvent.click(screen.getByLabelText('Dataset operations'))
|
||||
|
||||
expect(screen.queryByText('datasetPipeline.operations.exportPipeline')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ import Operations from '../operations'
|
||||
|
||||
type OperationsDropdownProps = {
|
||||
dataset: DataSet
|
||||
isCurrentWorkspaceDatasetOperator: boolean
|
||||
openRenameModal: () => void
|
||||
handleExportPipeline: (include?: boolean) => void
|
||||
detectIsUsedByApp: () => void
|
||||
@ -20,7 +19,6 @@ type OperationsDropdownProps = {
|
||||
|
||||
const OperationsDropdown = ({
|
||||
dataset,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
openRenameModal,
|
||||
handleExportPipeline,
|
||||
detectIsUsedByApp,
|
||||
@ -65,7 +63,7 @@ const OperationsDropdown = ({
|
||||
>
|
||||
<Operations
|
||||
showEdit={datasetACLCapabilities.canEdit}
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator && datasetACLCapabilities.canDelete}
|
||||
showDelete={datasetACLCapabilities.canDelete}
|
||||
showExportPipeline={dataset.runtime_mode === 'rag_pipeline' && datasetACLCapabilities.canImportExportDSL}
|
||||
showAccessConfig={datasetACLCapabilities.canAccessConfig}
|
||||
openRenameModal={openRenameModal}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { useMemo } from 'react'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { DatasetCardTags } from '@/features/tag-management/components/dataset-card-tags'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { getDatasetACLCapabilities } from '@/utils/permission'
|
||||
@ -28,8 +27,6 @@ const DatasetCard = ({
|
||||
}: DatasetCardProps) => {
|
||||
const { push } = useRouter()
|
||||
|
||||
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
|
||||
|
||||
const datasetCard = useDatasetCardController({ dataset, onSuccess })
|
||||
const {
|
||||
modalState,
|
||||
@ -89,7 +86,6 @@ const DatasetCard = ({
|
||||
<DatasetCardFooter dataset={dataset} />
|
||||
<OperationsDropdown
|
||||
dataset={dataset}
|
||||
isCurrentWorkspaceDatasetOperator={isCurrentWorkspaceDatasetOperator}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
|
||||
Reference in New Issue
Block a user