diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx index 1a566074505..7bb628997df 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx @@ -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(), })) diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 8a505ee3077..36a9a792ea5 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -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 = (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 diff --git a/web/app/(commonLayout)/datasets/layout.spec.tsx b/web/app/(commonLayout)/datasets/layout.spec.tsx index 4db962ff301..98ac56c6fce 100644 --- a/web/app/(commonLayout)/datasets/layout.spec.tsx +++ b/web/app/(commonLayout)/datasets/layout.spec.tsx @@ -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 diff --git a/web/app/(commonLayout)/datasets/layout.tsx b/web/app/(commonLayout)/datasets/layout.tsx index 8f6777dedfb..fa6d3d0569a 100644 --- a/web/app/(commonLayout)/datasets/layout.tsx +++ b/web/app/(commonLayout)/datasets/layout.tsx @@ -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 diff --git a/web/app/components/datasets/__tests__/mock-dataset-access.ts b/web/app/components/datasets/__tests__/mock-dataset-access.ts new file mode 100644 index 00000000000..fb6aff4650d --- /dev/null +++ b/web/app/components/datasets/__tests__/mock-dataset-access.ts @@ -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: () => Promise, + getState: () => DatasetAccessMockState, + getOptions: () => DatasetAccessMockOptions = () => ({}), +) => { + const actual = await importOriginal() + 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: () => Promise, +) => { + const actual = await importOriginal() + + return { + ...actual, + useAtomValue: (atom: unknown) => { + if (!isDatasetAccessMockAtom(atom)) + return actual.useAtomValue(atom as Parameters[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]}`) + }, + } +} diff --git a/web/app/components/datasets/access-config/__tests__/index.spec.tsx b/web/app/components/datasets/access-config/__tests__/index.spec.tsx index eeeec0dcf88..df6d3d7c1fa 100644 --- a/web/app/components/datasets/access-config/__tests__/index.spec.tsx +++ b/web/app/components/datasets/access-config/__tests__/index.spec.tsx @@ -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() diff --git a/web/app/components/datasets/access-config/index.tsx b/web/app/components/datasets/access-config/index.tsx index b1df8e3eea7..86b2aa061f1 100644 --- a/web/app/components/datasets/access-config/index.tsx +++ b/web/app/components/datasets/access-config/index.tsx @@ -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) diff --git a/web/app/components/datasets/create/__tests__/index.spec.tsx b/web/app/components/datasets/create/__tests__/index.spec.tsx index 7668e778d94..04a8c68f9c9 100644 --- a/web/app/components/datasets/create/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/__tests__/index.spec.tsx @@ -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', () => ({ diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index 1f41f7b456c..ac9653cdff7 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -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, diff --git a/web/app/components/datasets/documents/__tests__/index.spec.tsx b/web/app/components/datasets/documents/__tests__/index.spec.tsx index fa09e9c0766..de6e84e2ee5 100644 --- a/web/app/components/datasets/documents/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/__tests__/index.spec.tsx @@ -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', diff --git a/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx b/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx index 3840223e2db..632e9b16fcb 100644 --- a/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/components/document-list/__tests__/index.spec.tsx @@ -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 }, diff --git a/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx b/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx index 7fedde10168..0961eb2bcd9 100644 --- a/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx +++ b/web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx @@ -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, diff --git a/web/app/components/datasets/documents/components/list.tsx b/web/app/components/datasets/documents/components/list.tsx index ef965554b9c..d2d56f57cba 100644 --- a/web/app/components/datasets/documents/components/list.tsx +++ b/web/app/components/datasets/documents/components/list.tsx @@ -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, diff --git a/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx index b6ec26b923c..8e699008b3b 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/__tests__/index.spec.tsx @@ -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 } }), diff --git a/web/app/components/datasets/documents/create-from-pipeline/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/index.tsx index d83e9ae501d..6ee21ae9d7d 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/index.tsx @@ -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, diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 1d7ad17d07e..31de23ccef1 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -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 = ({ 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, { diff --git a/web/app/components/datasets/documents/index.tsx b/web/app/components/datasets/documents/index.tsx index 33b27c1c46e..d64fefe1325 100644 --- a/web/app/components/datasets/documents/index.tsx +++ b/web/app/components/datasets/documents/index.tsx @@ -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 = ({ 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, diff --git a/web/app/components/datasets/extra-info/api-access/card.tsx b/web/app/components/datasets/extra-info/api-access/card.tsx index 3a5685e8fbb..13b65ca612d 100644 --- a/web/app/components/datasets/extra-info/api-access/card.tsx +++ b/web/app/components/datasets/extra-info/api-access/card.tsx @@ -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() diff --git a/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx b/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx index 06bb88e91ef..1a5ac70964a 100644 --- a/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx +++ b/web/app/components/datasets/extra-info/service-api/__tests__/index.spec.tsx @@ -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 }) => ( diff --git a/web/app/components/datasets/extra-info/service-api/index.tsx b/web/app/components/datasets/extra-info/service-api/index.tsx index 679e80c5df3..e48b3c2eb7a 100644 --- a/web/app/components/datasets/extra-info/service-api/index.tsx +++ b/web/app/components/datasets/extra-info/service-api/index.tsx @@ -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(() => { diff --git a/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx b/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx index 50ba6fc7d6e..fa20d559603 100644 --- a/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx +++ b/web/app/components/datasets/hit-testing/__tests__/index.spec.tsx @@ -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, diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index 3324a497838..92332e97e95 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -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 = ({ datasetId }: Props) => { const [currPage, setCurrPage] = useState(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 diff --git a/web/app/components/datasets/list/__tests__/index.spec.tsx b/web/app/components/datasets/list/__tests__/index.spec.tsx index 76dfb03a9ed..9b6a71a0407 100644 --- a/web/app/components/datasets/list/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/__tests__/index.spec.tsx @@ -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'], }), })) diff --git a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx index bbe58038d6e..d1735b299e0 100644 --- a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx @@ -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: () =>
, })) diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx index 6e087158bc6..9620202a47a 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx @@ -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 => ({ id: 'dataset-1', diff --git a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx index d5ec29b233e..d340b2e974f 100644 --- a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx +++ b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx @@ -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, diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 0e03449aaf5..e568247a486 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -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 { diff --git a/web/app/components/datasets/list/index.tsx b/web/app/components/datasets/list/index.tsx index ba1d7cb5565..f079aa3414b 100644 --- a/web/app/components/datasets/list/index.tsx +++ b/web/app/components/datasets/list/index.tsx @@ -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() diff --git a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx index b39f4e54706..64795665890 100644 --- a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx @@ -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 => ({ 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({}), diff --git a/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx b/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx index 3c0e997ad57..2466445435a 100644 --- a/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx +++ b/web/app/components/datasets/settings/form/components/__tests__/basic-info-section.spec.tsx @@ -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', diff --git a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts index 78db31af095..bd047dd469e 100644 --- a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts +++ b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts @@ -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({}), diff --git a/web/app/components/datasets/settings/form/hooks/use-form-state.ts b/web/app/components/datasets/settings/form/hooks/use-form-state.ts index 8892d982636..fb3541fb573 100644 --- a/web/app/components/datasets/settings/form/hooks/use-form-state.ts +++ b/web/app/components/datasets/settings/form/hooks/use-form-state.ts @@ -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, diff --git a/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx b/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx index 7793c50acc2..f37728f2724 100644 --- a/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/permission-selector/__tests__/index.spec.tsx @@ -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(, { systemFeatures: { rbac_enabled: true, diff --git a/web/app/components/datasets/settings/permission-selector/index.tsx b/web/app/components/datasets/settings/permission-selector/index.tsx index afff19320e5..8289c9698a5 100644 --- a/web/app/components/datasets/settings/permission-selector/index.tsx +++ b/web/app/components/datasets/settings/permission-selector/index.tsx @@ -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 ( diff --git a/web/context/app-context-defaults.ts b/web/context/app-context-defaults.ts new file mode 100644 index 00000000000..475d7e474f5 --- /dev/null +++ b/web/context/app-context-defaults.ts @@ -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, +} diff --git a/web/context/app-context-state.ts b/web/context/app-context-state.ts index 780138f4d0f..04676f51099 100644 --- a/web/context/app-context-state.ts +++ b/web/context/app-context-state.ts @@ -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 + + return systemFeaturesQuery.data ?? defaultSystemFeatures +}) + export const userProfileAtom = atom((get): GetAccountProfileResponse => { const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult return accountProfileQuery.data?.profile || userProfilePlaceholder }) +export const userProfileIdAtom = atom((get) => { + return get(userProfileAtom).id +}) + const profileMetaAtom = atom((get) => { const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult @@ -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 }) diff --git a/web/context/app-context.ts b/web/context/app-context.ts index 5b8fcf22e69..e89546a1e1d 100644 --- a/web/context/app-context.ts +++ b/web/context/app-context.ts @@ -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({ userProfile: userProfilePlaceholder,