refactor(web): migrate dataset access context (#38523)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-07-08 10:35:54 +08:00 committed by GitHub
parent 915655683c
commit ee0068eed4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 629 additions and 119 deletions

View File

@ -31,14 +31,44 @@ vi.mock('@/context/app-context', () => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: [],
}),
useSelector: (selector: (state: {
isCurrentWorkspaceDatasetOperator: boolean
isLoadingCurrentWorkspace: boolean
isLoadingWorkspacePermissionKeys: boolean
userProfile: { id: string }
workspacePermissionKeys: string[]
}) => unknown) => selector({
isCurrentWorkspaceDatasetOperator: false,
isLoadingCurrentWorkspace: false,
isLoadingWorkspacePermissionKeys: false,
userProfile: { id: 'user-1' },
workspacePermissionKeys: [],
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: [],
}), () => ({
isRbacEnabled: mockIsRbacEnabled,
}))
})
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: undefined,
}),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))

View File

@ -2,14 +2,19 @@
import type { FC } from 'react'
import type { DataSet } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import {
currentWorkspaceLoadingAtom,
datasetRbacEnabledAtom,
userProfileIdAtom,
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
} from '@/context/app-context-state'
import DatasetDetailContext from '@/context/dataset-detail'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import useDocumentTitle from '@/hooks/use-document-title'
import { usePathname, useRouter } from '@/next/navigation'
import { useDatasetDetail } from '@/service/knowledge/use-dataset'
@ -58,23 +63,20 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const {
isLoadingCurrentWorkspace,
isLoadingWorkspacePermissionKeys,
userProfile,
workspacePermissionKeys,
} = useAppContext()
const isRbacEnabled = systemFeatures.rbac_enabled
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const isRbacEnabled = useAtomValue(datasetRbacEnabledAtom)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: datasetRes, error, refetch: mutateDatasetRes } = useDatasetDetail(datasetId)
const shouldRedirect = shouldRedirectToDatasetList(error)
const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(datasetRes?.permission_keys, {
currentUserId: userProfile?.id,
currentUserId,
resourceMaintainer: datasetRes?.maintainer,
workspacePermissionKeys,
isRbacEnabled,
}), [datasetRes?.maintainer, datasetRes?.permission_keys, isRbacEnabled, userProfile?.id, workspacePermissionKeys])
}), [datasetRes?.maintainer, datasetRes?.permission_keys, isRbacEnabled, currentUserId, workspacePermissionKeys])
const isAccessConfigPath = pathname.endsWith('/access-config')
const isHitTestingPath = pathname.endsWith('/hitTesting')
const isPermissionControlledPath = isAccessConfigPath || isHitTestingPath

View File

@ -20,10 +20,22 @@ vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: AppContextMock) => unknown) => selector(mockUseAppContext()),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockUseAppContext())
})
vi.mock('@/context/external-api-panel-context', () => ({
ExternalApiPanelProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
vi.mock('@/context/external-knowledge-api-context', () => ({
ExternalKnowledgeApiProvider: ({ children, enabled }: { children: ReactNode, enabled?: boolean }) => {
mockExternalKnowledgeApiProviderEnabled = enabled

View File

@ -1,8 +1,14 @@
'use client'
import { useAtomValue } from 'jotai'
import { useEffect } from 'react'
import Loading from '@/app/components/base/loading'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import {
currentWorkspaceIdAtom,
currentWorkspaceLoadingAtom,
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
} from '@/context/app-context-state'
import { ExternalApiPanelProvider } from '@/context/external-api-panel-context'
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
import { usePathname, useRouter } from '@/next/navigation'
@ -21,10 +27,10 @@ const isDatasetExternalConnectPath = (pathname: string) => {
}
export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
const currentWorkspaceId = useAppContextSelector(state => state.currentWorkspace.id)
const isLoadingCurrentWorkspace = useAppContextSelector(state => state.isLoadingCurrentWorkspace)
const isLoadingWorkspacePermissionKeys = useAppContextSelector(state => state.isLoadingWorkspacePermissionKeys)
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const router = useRouter()
const pathname = usePathname()
const isLoadingAccess = isLoadingCurrentWorkspace || !!isLoadingWorkspacePermissionKeys

View File

@ -0,0 +1,142 @@
const DATASET_ACCESS_ATOM_KIND = Symbol('dataset-access-atom-kind')
type DatasetAccessMockState = {
userProfile?: {
id?: string
name?: string
email?: string
avatar?: string
avatar_url?: string
is_password_set?: boolean
} | null
currentWorkspace?: {
id?: string
} | null
isCurrentWorkspaceOwner?: boolean
isLoadingCurrentWorkspace?: boolean
isLoadingWorkspacePermissionKeys?: boolean
workspacePermissionKeys?: string[]
}
type DatasetAccessMockOptions = {
isRbacEnabled?: boolean
}
type DatasetAccessAtomKind
= | 'userProfile'
| 'userProfileId'
| 'currentWorkspaceId'
| 'isCurrentWorkspaceOwner'
| 'workspacePermissionKeys'
| 'currentWorkspaceLoading'
| 'workspacePermissionKeysLoading'
| 'datasetRbacEnabled'
type DatasetAccessMockAtom = {
[DATASET_ACCESS_ATOM_KIND]: DatasetAccessAtomKind
}
type DatasetAccessMockRegistry = {
getState: () => DatasetAccessMockState
getOptions: () => DatasetAccessMockOptions
}
const defaultUserProfile = {
id: 'user-1',
name: 'User',
email: 'user@example.com',
avatar: '',
avatar_url: '',
is_password_set: true,
}
let datasetAccessMockRegistry: DatasetAccessMockRegistry | undefined
const createMockAtom = (
kind: DatasetAccessAtomKind,
): DatasetAccessMockAtom => ({
[DATASET_ACCESS_ATOM_KIND]: kind,
})
const isDatasetAccessMockAtom = (atom: unknown): atom is DatasetAccessMockAtom => {
return typeof atom === 'object' && atom !== null && DATASET_ACCESS_ATOM_KIND in atom
}
const getUserProfile = (state: DatasetAccessMockState) => ({
...defaultUserProfile,
...state.userProfile,
})
const getWorkspacePermissionKeys = (state: DatasetAccessMockState) => state.workspacePermissionKeys ?? []
export const createDatasetAccessAtomMock = async (
importOriginal: <T>() => Promise<T>,
getState: () => DatasetAccessMockState,
getOptions: () => DatasetAccessMockOptions = () => ({}),
) => {
const actual = await importOriginal<typeof import('@/context/app-context-state')>()
datasetAccessMockRegistry = {
getState,
getOptions,
}
return {
...actual,
userProfileAtom: createMockAtom('userProfile'),
userProfileIdAtom: createMockAtom('userProfileId'),
currentWorkspaceIdAtom: createMockAtom('currentWorkspaceId'),
isCurrentWorkspaceOwnerAtom: createMockAtom('isCurrentWorkspaceOwner'),
workspacePermissionKeysAtom: createMockAtom('workspacePermissionKeys'),
currentWorkspaceLoadingAtom: createMockAtom('currentWorkspaceLoading'),
workspacePermissionKeysLoadingAtom: createMockAtom('workspacePermissionKeysLoading'),
datasetRbacEnabledAtom: createMockAtom('datasetRbacEnabled'),
}
}
export const createDatasetAccessJotaiMock = async (
importOriginal: <T>() => Promise<T>,
) => {
const actual = await importOriginal<typeof import('jotai')>()
return {
...actual,
useAtomValue: (atom: unknown) => {
if (!isDatasetAccessMockAtom(atom))
return actual.useAtomValue(atom as Parameters<typeof actual.useAtomValue>[0])
if (!datasetAccessMockRegistry)
throw new Error('Dataset access atom mock is not initialized')
const state = datasetAccessMockRegistry.getState()
const options = datasetAccessMockRegistry.getOptions()
const userProfile = getUserProfile(state)
const workspacePermissionKeys = getWorkspacePermissionKeys(state)
if (atom[DATASET_ACCESS_ATOM_KIND] === 'userProfile')
return userProfile
if (atom[DATASET_ACCESS_ATOM_KIND] === 'userProfileId')
return userProfile.id
if (atom[DATASET_ACCESS_ATOM_KIND] === 'currentWorkspaceId')
return state.currentWorkspace?.id ?? 'workspace-1'
if (atom[DATASET_ACCESS_ATOM_KIND] === 'isCurrentWorkspaceOwner')
return state.isCurrentWorkspaceOwner ?? false
if (atom[DATASET_ACCESS_ATOM_KIND] === 'workspacePermissionKeys')
return workspacePermissionKeys
if (atom[DATASET_ACCESS_ATOM_KIND] === 'currentWorkspaceLoading')
return state.isLoadingCurrentWorkspace ?? false
if (atom[DATASET_ACCESS_ATOM_KIND] === 'workspacePermissionKeysLoading')
return state.isLoadingWorkspacePermissionKeys ?? false
if (atom[DATASET_ACCESS_ATOM_KIND] === 'datasetRbacEnabled')
return options.isRbacEnabled ?? true
throw new Error(`Unsupported dataset access atom: ${atom[DATASET_ACCESS_ATOM_KIND]}`)
},
}
}

View File

@ -79,6 +79,14 @@ vi.mock('@/context/app-context', () => ({
useSelector: vi.fn((selector: (state: typeof mockAppContextState) => unknown) => selector(mockAppContextState)),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState, () => ({
isRbacEnabled: mockIsRbacEnabled,
}))
})
vi.mock('@/app/components/access-rules-editor', () => ({
default: (props: AccessRulesEditorProps) => {
mockAccessRulesEditor.props = props
@ -88,6 +96,12 @@ vi.mock('@/app/components/access-rules-editor', () => ({
},
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
describe('DatasetAccessConfigPage', () => {
beforeEach(() => {
vi.clearAllMocks()

View File

@ -2,15 +2,18 @@
import type { ResourceOpenScope } from '@/models/access-control'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AccessRulesEditor from '@/app/components/access-rules-editor'
import Loading from '@/app/components/base/loading'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
datasetRbacEnabledAtom,
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useLocale } from '@/context/i18n'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { getAccessControlTemplateLanguage } from '@/i18n-config/language'
import {
useDatasetAccessRules,
@ -30,16 +33,15 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) =>
const locale = useLocale()
const language = useMemo(() => getAccessControlTemplateLanguage(locale), [locale])
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const isRbacEnabled = systemFeatures.rbac_enabled
const canAccessConfig = useMemo(() => getDatasetACLCapabilities(dataset?.permission_keys, {
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const isRbacEnabled = useAtomValue(datasetRbacEnabledAtom)
const canAccessConfig = getDatasetACLCapabilities(dataset?.permission_keys, {
currentUserId,
resourceMaintainer: dataset?.maintainer,
workspacePermissionKeys,
isRbacEnabled,
}).canAccessConfig, [currentUserId, dataset?.maintainer, dataset?.permission_keys, isRbacEnabled, workspacePermissionKeys])
}).canAccessConfig
const { data: datasetAccessRulesResponse, isLoading: isLoadingDatasetAccessRules } = useDatasetAccessRules(datasetId, language, { enabled: canAccessConfig })
const { data: datasetUserAccessSettingsResponse, isLoading: isLoadingDatasetUserAccessSettings } = useDatasetUserAccessSettings(datasetId, language, { enabled: canAccessConfig })
const { mutate: updateDatasetOpenScope, isPending: isUpdatingDatasetOpenScope } = useUpdateDatasetOpenScope(datasetId)

View File

@ -54,6 +54,16 @@ vi.mock('@/context/app-context', () => ({
},
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: { id: mockCurrentUserId },
workspacePermissionKeys: mockWorkspacePermissionKeys,
isLoadingWorkspacePermissionKeys: mockIsLoadingWorkspacePermissionKeys,
}))
})
// Mock modal context
const mockSetShowAccountSettingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
@ -68,6 +78,12 @@ vi.mock('@/context/modal-context', () => ({
},
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
// Mock dataset detail context
let mockDatasetDetail: DataSet | undefined
vi.mock('@/context/dataset-detail', () => ({

View File

@ -3,6 +3,7 @@ import type { NotionPage } from '@/models/common'
import type { CrawlOptions, CrawlResultItem, createDocumentResponse, FileItem } from '@/models/datasets'
import type { RETRIEVE_METHOD } from '@/types/app'
import { produce } from 'immer'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -10,7 +11,11 @@ import Loading from '@/app/components/base/loading'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { DataSourceProvider } from '@/models/common'
import { DataSourceType } from '@/models/datasets'
@ -43,9 +48,9 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
const router = useRouter()
const openIntegrationsSetting = useIntegrationsSetting()
const datasetDetail = useDatasetDetailContextWithSelector(state => state.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const isLoadingWorkspacePermissionKeys = useAppContextWithSelector(state => state.isLoadingWorkspacePermissionKeys)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: embeddingsDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
const canAddDocumentsToDataset = !datasetId || getDatasetACLCapabilities(datasetDetail?.permission_keys, {
currentUserId,

View File

@ -55,6 +55,15 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: { id: 'test-user' },
workspacePermissionKeys: ['dataset.create_and_management'],
}))
})
// Mock document service hooks
const mockInvalidDocumentList = vi.fn()
const mockInvalidDocumentDetail = vi.fn()
@ -92,6 +101,12 @@ vi.mock('@/service/knowledge/use-document', () => ({
useInvalidDocumentDetail: vi.fn(() => mockInvalidDocumentDetail),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
// Mock segment service hooks
vi.mock('@/service/knowledge/use-segment', () => ({
useSegmentListKey: 'segment-list-key',

View File

@ -42,6 +42,15 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: ['dataset.create_and_management'],
}))
})
vi.mock('@/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata', () => ({
default: () => ({
isShowEditModal: false,
@ -52,6 +61,12 @@ vi.mock('@/app/components/datasets/metadata/hooks/use-batch-edit-document-metada
}),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },

View File

@ -2,6 +2,7 @@ import type { SimpleDocumentDetail } from '@/models/datasets'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { pick } from 'es-toolkit/object'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@ -9,7 +10,10 @@ import ChunkingModeLabel from '@/app/components/datasets/common/chunking-mode-la
import Operations from '@/app/components/datasets/documents/components/operations'
import SummaryStatus from '@/app/components/datasets/documents/detail/completed/common/summary-status'
import StatusItem from '@/app/components/datasets/documents/status-item'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import useTimestamp from '@/hooks/use-timestamp'
import { DataSourceType } from '@/models/datasets'
@ -62,8 +66,8 @@ const DocumentTableRow = React.memo(({
const searchParams = useSearchParams()
const documentNameId = React.useId()
const dataset = useDatasetDetailContextWithSelector(s => s.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(dataset?.permission_keys, {
currentUserId,
resourceMaintainer: dataset?.maintainer,

View File

@ -4,11 +4,15 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useBoolean } from 'ahooks'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
import useBatchEditDocumentMetadata from '@/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '@/context/dataset-detail'
import { ChunkingMode, DocumentActionType } from '@/models/datasets'
import { getDatasetACLCapabilities } from '@/utils/permission'
@ -61,8 +65,8 @@ const DocumentList = ({
const pageSize = pagination.limit ?? 10
const totalPages = Math.max(Math.ceil(pagination.total / pageSize), 1)
const datasetConfig = useDatasetDetailContext(s => s.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const datasetACLCapabilities = useMemo(() => getDatasetACLCapabilities(datasetConfig?.permission_keys, {
currentUserId,
resourceMaintainer: datasetConfig?.maintainer,

View File

@ -63,6 +63,16 @@ vi.mock('@/context/app-context', () => ({
},
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: { id: mockCurrentUserId },
workspacePermissionKeys: mockWorkspacePermissionKeys,
isLoadingWorkspacePermissionKeys: mockIsLoadingWorkspacePermissionKeys,
}))
})
vi.mock('@/service/use-billing', () => ({
useCurrentPlanVectorSpace: () => ({
data: {
@ -73,6 +83,12 @@ vi.mock('@/service/use-billing', () => ({
}),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: (selector: (state: { dataset: { id: string, pipeline_id: string, permission_keys: string[] } }) => unknown) =>
selector({ dataset: { id: 'test-dataset-id', pipeline_id: 'test-pipeline-id', permission_keys: mockDatasetPermissionKeys } }),

View File

@ -5,11 +5,16 @@ import type { Node } from '@/app/components/workflow/types'
import type { FileIndexingEstimateResponse } from '@/models/datasets'
import type { InitialDocumentDetail } from '@/models/pipeline'
import { useBoolean } from 'ahooks'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContextSelector } from '@/context/provider-context'
import { DatasourceType } from '@/models/pipeline'
@ -40,9 +45,9 @@ const CreateFormPipeline = () => {
const enableBilling = useProviderContextSelector(state => state.enableBilling)
const dataset = useDatasetDetailContextWithSelector(s => s.dataset)
const pipelineId = dataset?.pipeline_id
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const isLoadingWorkspacePermissionKeys = useAppContextWithSelector(state => state.isLoadingWorkspacePermissionKeys)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const dataSourceStore = useDataSourceStore()
const canAddDocumentsToDataset = getDatasetACLCapabilities(dataset?.permission_keys, {
currentUserId,

View File

@ -4,6 +4,7 @@ import type { DocumentDisplayStatus, FileItem, FullDocumentDetail } from '@/mode
import type { SegmentImportStatus } from '@/types/dataset'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -11,7 +12,10 @@ import Divider from '@/app/components/base/divider'
import FloatRightContainer from '@/app/components/base/float-right-container'
import Loading from '@/app/components/base/loading'
import Metadata from '@/app/components/datasets/metadata/metadata-document'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { ChunkingMode, DisplayStatusList } from '@/models/datasets'
@ -49,8 +53,8 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => {
const isMobile = media === MediaType.mobile
const dataset = useDatasetDetailContextWithSelector(s => s.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const embeddingAvailable = !!dataset?.embedding_available
const datasetACLCapabilities = useMemo(
() => getDatasetACLCapabilities(dataset?.permission_keys, {

View File

@ -1,8 +1,12 @@
'use client'
import type { FC } from 'react'
import { useAtomValue } from 'jotai'
import { useCallback } from 'react'
import Loading from '@/app/components/base/loading'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import { DataSourceType } from '@/models/datasets'
@ -31,8 +35,8 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const isFreePlan = plan.type === 'sandbox'
const dataset = useDatasetDetailContextWithSelector(s => s.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const embeddingAvailable = !!dataset?.embedding_available
const datasetACLCapabilities = getDatasetACLCapabilities(dataset?.permission_keys, {
currentUserId,

View File

@ -1,10 +1,14 @@
import { cn } from '@langgenius/dify-ui/cn'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { Switch } from '@langgenius/dify-ui/switch'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
import Link from '@/next/link'
@ -22,8 +26,8 @@ const Card = ({
const datasetId = useDatasetDetailContextWithSelector(state => state.dataset?.id)
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
const mutateDatasetRes = useDatasetDetailContextWithSelector(state => state.mutateDatasetRes)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { mutateAsync: enableDatasetServiceApi } = useEnableDatasetServiceApi()
const { mutateAsync: disableDatasetServiceApi } = useDisableDatasetServiceApi()

View File

@ -15,6 +15,14 @@ vi.mock('@/context/app-context', () => ({
selector({ workspacePermissionKeys: mockWorkspacePermissionKeys }),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('@/next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
@ -24,6 +32,12 @@ vi.mock('@/next/navigation', () => ({
useSearchParams: () => new URLSearchParams(),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
// Mock next/link
vi.mock('@/next/link', () => ({
default: ({ children, href, ...props }: { children: React.ReactNode, href: string, [key: string]: unknown }) => (

View File

@ -1,11 +1,12 @@
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { hasPermission } from '@/utils/permission'
import Card from './card'
@ -19,7 +20,7 @@ const ServiceApi = ({
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [isSecretKeyModalVisible, setIsSecretKeyModalVisible] = useState(false)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageSecretKey = hasPermission(workspacePermissionKeys, 'dataset.api_key.manage')
const handleOpenSecretKeyModal = useCallback(() => {

View File

@ -83,6 +83,12 @@ vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector(mockAppContextState),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState)
})
const mockRecordsRefetch = vi.fn()
const mockHitTestingMutateAsync = vi.fn()
const mockExternalHitTestingMutateAsync = vi.fn()
@ -101,6 +107,12 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
})),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
vi.mock('@/service/knowledge/use-hit-testing', () => ({
useHitTesting: vi.fn(() => ({
mutateAsync: mockHitTestingMutateAsync,

View File

@ -20,6 +20,7 @@ import {
} from '@langgenius/dify-ui/drawer'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useBoolean } from 'ahooks'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -27,7 +28,10 @@ import { useContext } from 'use-context-selector'
import FloatRightContainer from '@/app/components/base/float-right-container'
import Loading from '@/app/components/base/loading'
import docStyle from '@/app/components/datasets/documents/detail/completed/style.module.css'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import DatasetDetailContext from '@/context/dataset-detail'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useDatasetTestingRecords } from '@/service/knowledge/use-dataset'
@ -63,13 +67,13 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
const [currPage, setCurrPage] = useState<number>(0)
const { dataset: currentDataset } = useContext(DatasetDetailContext)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const canRunRetrievalRecall = React.useMemo(() => getDatasetACLCapabilities(currentDataset?.permission_keys, {
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canRunRetrievalRecall = getDatasetACLCapabilities(currentDataset?.permission_keys, {
currentUserId,
resourceMaintainer: currentDataset?.maintainer,
workspacePermissionKeys,
}).canRetrievalRecall, [currentDataset?.maintainer, currentDataset?.permission_keys, currentUserId, workspacePermissionKeys])
}).canRetrievalRecall
const { data: recordsRes, refetch: recordsRefetch, isLoading: isRecordsLoading } = useDatasetTestingRecords(datasetId, { limit, page: currPage + 1 }, { enabled: canRunRetrievalRecall })
const total = recordsRes?.total || 0

View File

@ -8,6 +8,7 @@ const mockReplace = vi.fn()
let mockAppContextState = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'],
}
let mockIsCurrentWorkspaceOwner = true
@ -27,6 +28,12 @@ vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector(mockAppContextState),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState)
})
// Mock external api panel context
const mockSetShowExternalApiPanel = vi.fn()
vi.mock('@/context/external-api-panel-context', () => ({
@ -36,6 +43,12 @@ vi.mock('@/context/external-api-panel-context', () => ({
}),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
// Mock useDocumentTitle hook
vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
@ -132,6 +145,7 @@ describe('List', () => {
mockAppContextState = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'],
}
mockIsCurrentWorkspaceOwner = true
@ -170,6 +184,7 @@ describe('List', () => {
mockAppContextState = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management'],
}
@ -282,6 +297,7 @@ describe('List', () => {
mockAppContextState = {
isCurrentWorkspaceEditor: false,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management'],
}
const { useDatasetList } = await import('@/service/knowledge/use-dataset')
@ -303,6 +319,7 @@ describe('List', () => {
mockAppContextState = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: [],
}
const { useDatasetList } = await import('@/service/knowledge/use-dataset')
@ -367,6 +384,7 @@ describe('List', () => {
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector({
isCurrentWorkspaceEditor: false,
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'],
}),
}))
@ -417,6 +435,12 @@ describe('List', () => {
})
it('should not show ExternalAPIPanel without dataset.external.connect even when panel state is open', async () => {
mockAppContextState = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management'],
}
vi.doMock('@/context/app-context', () => ({
useAppContext: () => ({
currentWorkspace: { role: 'admin' },
@ -425,6 +449,7 @@ describe('List', () => {
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector({
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management'],
}),
}))
@ -452,6 +477,7 @@ describe('List', () => {
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector({
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'],
}),
}))
@ -481,6 +507,12 @@ describe('List', () => {
})
it('should not show include all checkbox when not workspace owner', async () => {
mockAppContextState = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'],
}
vi.doMock('@/context/app-context', () => ({
useAppContext: () => ({
currentWorkspace: { role: 'editor' },
@ -489,6 +521,7 @@ describe('List', () => {
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector({
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
workspacePermissionKeys: ['dataset.create_and_management', 'dataset.external.connect'],
}),
}))

View File

@ -53,6 +53,12 @@ vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: typeof mockAppContextState) => unknown) => selector(mockAppContextState),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState)
})
vi.mock('../hooks/use-dataset-card-state', () => ({
useDatasetCardState: () => ({
modalState: {
@ -72,6 +78,12 @@ vi.mock('../hooks/use-dataset-card-state', () => ({
}),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
vi.mock('../components/corner-labels', () => ({
default: () => <div data-testid="corner-labels" />,
}))

View File

@ -24,6 +24,20 @@ vi.mock('@/context/app-context', () => ({
useSelector: vi.fn((selector: (state: typeof mockAppContextState) => unknown) => selector(mockAppContextState)),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState, () => ({
isRbacEnabled: mockIsRbacEnabled,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
describe('OperationsDropdown', () => {
const createMockDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
id: 'dataset-1',

View File

@ -5,10 +5,13 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import {
datasetRbacEnabledAtom,
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { getDatasetACLCapabilities } from '@/utils/permission'
import Operations from '../operations'
@ -28,10 +31,9 @@ const OperationsDropdown = ({
openAccessConfig,
}: OperationsDropdownProps) => {
const [open, setOpen] = React.useState(false)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const isRbacEnabled = systemFeatures.rbac_enabled
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const isRbacEnabled = useAtomValue(datasetRbacEnabledAtom)
const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(dataset.permission_keys, {
currentUserId,
resourceMaintainer: dataset.maintainer,

View File

@ -3,9 +3,13 @@ import type { KeyboardEvent, MouseEvent } from 'react'
import type { DataSet } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { DatasetCardTags } from '@/features/tag-management/components/dataset-card-tags'
import { useRouter } from '@/next/navigation'
import { getDatasetACLCapabilities, hasOnlyDatasetPreviewPermission, hasPermission } from '@/utils/permission'
@ -32,8 +36,8 @@ const DatasetCard = ({
}: DatasetCardProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const datasetCard = useDatasetCardController({ dataset, onSuccess })
const {

View File

@ -1,11 +1,15 @@
'use client'
import { useBoolean, useDebounceFn } from 'ahooks'
import { useAtomValue } from 'jotai'
// Libraries
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
import {
isCurrentWorkspaceOwnerAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { TagManagementModal } from '@/features/tag-management/components/tag-management-modal'
import useDocumentTitle from '@/hooks/use-document-title'
@ -22,7 +26,7 @@ import DatasetListHeader from './header'
const List = () => {
const { t } = useTranslation()
const { push } = useRouter()
const { isCurrentWorkspaceOwner } = useAppContext()
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
const [showTagManagementModal, setShowTagManagementModal] = useState(false)
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false)
@ -48,7 +52,7 @@ const List = () => {
handleTagsUpdate()
}
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canCreateDataset = hasPermission(workspacePermissionKeys, 'dataset.create_and_management')
const canConnectExternalDataset = hasPermission(workspacePermissionKeys, 'dataset.external.connect')
const { data: apiBaseInfo } = useDatasetApiBaseUrl()

View File

@ -47,6 +47,17 @@ vi.mock('@/context/app-context', () => ({
},
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: mockUserProfile,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}), () => ({
isRbacEnabled: false,
}))
})
const createMockDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
id: 'dataset-1',
name: 'Test Dataset',
@ -129,6 +140,12 @@ vi.mock('@/context/dataset-detail', () => ({
},
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
// Mock services
vi.mock('@/service/datasets', () => ({
updateDatasetSetting: vi.fn().mockResolvedValue({}),

View File

@ -19,17 +19,29 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
}
})
// Mock app-context
vi.mock('@/context/app-context', () => ({
useSelector: () => ({
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
id: 'user-1',
name: 'Current User',
email: 'current@example.com',
avatar_url: '',
role: 'owner',
}),
},
}))
// Mock app-context
vi.mock('@/context/app-context', () => ({
useSelector: () => mockAppContextState.userProfile,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState, () => ({
isRbacEnabled: false,
}))
})
// Mock image uploader hooks for AppIconPicker
vi.mock('@/app/components/base/image-uploader/hooks', () => ({
useLocalFileUploader: () => ({
@ -47,6 +59,12 @@ vi.mock('@/app/components/base/image-uploader/hooks', () => ({
}),
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
describe('BasicInfoSection', () => {
const mockDataset: DataSet = {
id: 'dataset-1',

View File

@ -23,6 +23,15 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: [],
}))
})
const createDefaultMockDataset = (): DataSet => ({
id: 'dataset-1',
name: 'Test Dataset',
@ -104,6 +113,12 @@ vi.mock('@/context/dataset-detail', () => ({
},
}))
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
// Mock services
vi.mock('@/service/datasets', () => ({
updateDatasetSetting: vi.fn().mockResolvedValue({}),

View File

@ -5,12 +5,16 @@ import type { Member } from '@/models/common'
import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
userProfileIdAtom,
workspacePermissionKeysAtom,
} from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { DatasetPermission } from '@/models/datasets'
import { updateDatasetSetting } from '@/service/datasets'
@ -30,8 +34,8 @@ export const useFormState = () => {
const { t } = useTranslation()
const currentDataset = useDatasetDetailContextWithSelector(state => state.dataset)
const mutateDatasets = useDatasetDetailContextWithSelector(state => state.mutateDatasetRes)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const datasetACLCapabilities = useMemo(
() => getDatasetACLCapabilities(currentDataset?.permission_keys, {
currentUserId,

View File

@ -4,17 +4,32 @@ import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features
import { DatasetPermission } from '@/models/datasets'
import PermissionSelector from '../index'
// Mock app-context
vi.mock('@/context/app-context', () => ({
useSelector: () => ({
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
id: 'user-1',
name: 'Current User',
email: 'current@example.com',
avatar_url: '',
role: 'owner',
}),
},
}))
let mockIsRbacEnabled = false
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createDatasetAccessAtomMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessAtomMock(importOriginal, () => mockAppContextState, () => ({
isRbacEnabled: mockIsRbacEnabled,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createDatasetAccessJotaiMock } = await import('@/app/components/datasets/__tests__/mock-dataset-access')
return createDatasetAccessJotaiMock(importOriginal)
})
describe('PermissionSelector', () => {
const mockMemberList: Member[] = [
{ id: 'user-1', name: 'Current User', email: 'current@example.com', avatar: '', avatar_url: '', role: 'owner', roles: [], last_login_at: '', created_at: '', status: 'active' }!,
@ -33,6 +48,7 @@ describe('PermissionSelector', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsRbacEnabled = false
})
describe('Rendering', () => {
@ -409,6 +425,8 @@ describe('PermissionSelector', () => {
})
it('should show access config hint and remain closed when RBAC is enabled', () => {
mockIsRbacEnabled = true
renderWithSystemFeatures(<PermissionSelector {...defaultProps} />, {
systemFeatures: {
rbac_enabled: true,

View File

@ -7,12 +7,14 @@ import {
PopoverContent,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useDebounceFn } from 'ahooks'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import {
datasetRbacEnabledAtom,
userProfileAtom,
} from '@/context/app-context-state'
import { DatasetPermission } from '@/models/datasets'
import MemberItem from './member-item'
import Item from './permission-item'
@ -35,8 +37,8 @@ const PermissionSelector = ({
onMemberSelect,
}: RoleSelectorProps) => {
const { t } = useTranslation()
const userProfile = useAppContextWithSelector(state => state.userProfile)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const userProfile = useAtomValue(userProfileAtom)
const isRbacEnabled = useAtomValue(datasetRbacEnabledAtom)
const [open, setOpen] = useState(false)
const [keywords, setKeywords] = useState('')
@ -89,7 +91,7 @@ const PermissionSelector = ({
const isAllTeamMembers = permission === DatasetPermission.allTeamMembers
const isPartialMembers = permission === DatasetPermission.partialMembers
const selectedMemberNames = selectedMembers.map(member => member.name).join(', ')
const isDisabledByRBAC = systemFeatures.rbac_enabled
const isDisabledByRBAC = isRbacEnabled
const isDisabled = disabled || isDisabledByRBAC
return (

View File

@ -0,0 +1,34 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
export const userProfilePlaceholder: GetAccountProfileResponse = {
id: '',
name: '',
email: '',
avatar: '',
avatar_url: '',
is_password_set: false,
}
export const initialLangGeniusVersionInfo: LangGeniusVersionResponse = {
current_env: '',
current_version: '',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
}
export const initialWorkspaceInfo: ICurrentWorkspace = {
id: '',
name: '',
plan: '',
status: '',
created_at: 0,
role: 'normal',
providers: [],
trial_credits: 200,
trial_credits_used: 0,
next_credit_reset_date: 0,
}

View File

@ -8,6 +8,7 @@ import { atom } from 'jotai'
import { atomWithQuery, atomWithSuspenseQuery, queryClientAtom } from 'jotai-tanstack-query'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { defaultSystemFeatures } from '@/features/system-features/config'
import { workspacePermissionKeysQueryOptions } from '@/service/access-control/use-permission-keys'
import { consoleQuery } from '@/service/client'
import { langGeniusVersionQueryOptions } from '@/service/lang-genius-version'
@ -15,7 +16,7 @@ import {
initialLangGeniusVersionInfo,
initialWorkspaceInfo,
userProfilePlaceholder,
} from './app-context'
} from './app-context-defaults'
import {
emptyWorkspacePermissionKeys,
getLangGeniusVersionInfo,
@ -29,12 +30,22 @@ const accountProfileQueryAtom = atomWithSuspenseQuery(() => userProfileQueryOpti
const systemFeaturesQueryAtom = atomWithSuspenseQuery(() => systemFeaturesQueryOptions())
const systemFeaturesAtom = atom((get): GetSystemFeaturesResponse => {
const systemFeaturesQuery = get(systemFeaturesQueryAtom) as SuspenseQueryResult<GetSystemFeaturesResponse>
return systemFeaturesQuery.data ?? defaultSystemFeatures
})
export const userProfileAtom = atom((get): GetAccountProfileResponse => {
const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult<UserProfileWithMeta>
return accountProfileQuery.data?.profile || userProfilePlaceholder
})
export const userProfileIdAtom = atom((get) => {
return get(userProfileAtom).id
})
const profileMetaAtom = atom((get) => {
const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult<UserProfileWithMeta>
@ -58,18 +69,26 @@ export const currentWorkspaceAtom = atom((get) => {
return get(normalizedCurrentWorkspaceAtom)
})
export const currentWorkspaceIdAtom = atom((get) => {
return get(currentWorkspaceAtom).id
})
export const workspaceRoleFlagsAtom = atom((get) => {
return getWorkspaceRoleFlags(get(currentWorkspaceAtom))
})
export const isCurrentWorkspaceOwnerAtom = atom((get) => {
return get(workspaceRoleFlagsAtom).isCurrentWorkspaceOwner
})
const workspacePermissionKeysQueryAtom = atomWithQuery((get) => {
const workspaceId = get(currentWorkspaceAtom).id
const workspaceId = get(currentWorkspaceIdAtom)
return workspacePermissionKeysQueryOptions(workspaceId)
})
export const workspacePermissionKeysAtom = atom((get) => {
return get(workspacePermissionKeysQueryAtom).data?.workspace.permission_keys ?? emptyWorkspacePermissionKeys
return get(workspacePermissionKeysQueryAtom).data?.workspace?.permission_keys ?? emptyWorkspacePermissionKeys
})
export const workspacePermissionKeysLoadingAtom = atom((get) => {
@ -80,6 +99,10 @@ export const currentWorkspaceLoadingAtom = atom((get) => {
return get(currentWorkspaceQueryAtom).isPending
})
export const datasetRbacEnabledAtom = atom((get) => {
return get(systemFeaturesAtom).rbac_enabled
})
export const currentWorkspaceValidatingAtom = atom((get) => {
return get(currentWorkspaceQueryAtom).isFetching
})

View File

@ -4,6 +4,11 @@ import type { GetAccountProfileResponse } from '@dify/contracts/api/console/acco
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
import { noop } from 'es-toolkit/function'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import {
initialLangGeniusVersionInfo as defaultLangGeniusVersionInfo,
userProfilePlaceholder as defaultUserProfilePlaceholder,
initialWorkspaceInfo as defaultWorkspaceInfo,
} from './app-context-defaults'
export type AppContextValue = {
userProfile: GetAccountProfileResponse
@ -22,37 +27,11 @@ export type AppContextValue = {
workspacePermissionKeys: string[]
}
export const userProfilePlaceholder = {
id: '',
name: '',
email: '',
avatar: '',
avatar_url: '',
is_password_set: false,
}
export const userProfilePlaceholder = defaultUserProfilePlaceholder
export const initialLangGeniusVersionInfo = {
current_env: '',
current_version: '',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
}
export const initialLangGeniusVersionInfo = defaultLangGeniusVersionInfo
export const initialWorkspaceInfo: ICurrentWorkspace = {
id: '',
name: '',
plan: '',
status: '',
created_at: 0,
role: 'normal',
providers: [],
trial_credits: 200,
trial_credits_used: 0,
next_credit_reset_date: 0,
}
export const initialWorkspaceInfo = defaultWorkspaceInfo
export const AppContext = createContext<AppContextValue>({
userProfile: userProfilePlaceholder,