diff --git a/web/src/app.tsx b/web/src/app.tsx index ff06402d7c..325638947e 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -31,10 +31,11 @@ import weekYear from 'dayjs/plugin/weekYear'; import weekday from 'dayjs/plugin/weekday'; import React from 'react'; import { RouterProvider } from 'react-router'; +import StarModal from '@/ee/components/star-modal'; +import { routers } from '@/ee/merge-routes'; import { ThemeProvider } from './components/theme-provider'; import { TooltipProvider } from './components/ui/tooltip'; import { ThemeEnum } from './constants/common'; -import { routers } from './routes'; import 'react-photo-view/dist/react-photo-view.css'; @@ -86,6 +87,7 @@ function Root({ children }: React.PropsWithChildren) { + ); } diff --git a/web/src/components/ui/modal/modal-manage.tsx b/web/src/components/ui/modal/modal-manage.tsx index 54af5a7c31..9a5b87718e 100644 --- a/web/src/components/ui/modal/modal-manage.tsx +++ b/web/src/components/ui/modal/modal-manage.tsx @@ -1,3 +1,4 @@ +import { EnterpriseModalProvider } from '@/ee/components/modal-provider'; import { ReactNode, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { createRoot } from 'react-dom/client'; @@ -72,7 +73,13 @@ export const createPortalModal = () => { }, ...props, }; - root?.render(isVisible ? : null); + root?.render( + isVisible ? ( + + + + ) : null, + ); }; const show = (props: PortalModalProps) => { diff --git a/web/src/ee/components/duplicate-dialog/index.tsx b/web/src/ee/components/duplicate-dialog/index.tsx new file mode 100644 index 0000000000..4e25b67839 --- /dev/null +++ b/web/src/ee/components/duplicate-dialog/index.tsx @@ -0,0 +1,12 @@ +import { IModalProps } from '@/interfaces/common'; + +/** + * Enterprise dialog for duplicating a resource. Open source has no duplicate + * action, so the slot renders nothing; the shared call sites still mount it + * with the same props. + */ +export default function DuplicateDialog( + _props: IModalProps & { initialName?: string }, +) { + return null; +} diff --git a/web/src/ee/components/modal-provider/index.tsx b/web/src/ee/components/modal-provider/index.tsx new file mode 100644 index 0000000000..2e4f29e4d2 --- /dev/null +++ b/web/src/ee/components/modal-provider/index.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +/** + * Provider wrapped around portal modals. The enterprise build replaces this + * file with the React Query provider those modals need; open source renders + * children directly. + */ +export function EnterpriseModalProvider({ children }: { children: ReactNode }) { + return <>{children}; +} diff --git a/web/src/ee/components/privilege-management/privilege-management-dialog.tsx b/web/src/ee/components/privilege-management/privilege-management-dialog.tsx new file mode 100644 index 0000000000..06ba8c3dcc --- /dev/null +++ b/web/src/ee/components/privilege-management/privilege-management-dialog.tsx @@ -0,0 +1,15 @@ +import { IModalProps } from '@/interfaces/common'; + +/** + * Enterprise dialog for managing a resource's collaborators. Open source has + * no collaborator permissions, so the slot renders nothing; the shared call + * sites still mount it with the same props. The prop type stays opaque here + * because the collaborator record only has a shape in the enterprise build. + */ +export function PrivilegeManagementDialog( + _props: IModalProps & { + initialValues: Record; + }, +) { + return null; +} diff --git a/web/src/ee/components/star-modal/index.tsx b/web/src/ee/components/star-modal/index.tsx new file mode 100644 index 0000000000..31b8acaca0 --- /dev/null +++ b/web/src/ee/components/star-modal/index.tsx @@ -0,0 +1,7 @@ +/** + * Enterprise slot inside the app root. The open-source build renders nothing; + * the enterprise build replaces this file with the GitHub star prompt. + */ +const StarModal = () => null; + +export default StarModal; diff --git a/web/src/ee/constants/team.ts b/web/src/ee/constants/team.ts new file mode 100644 index 0000000000..02bfb2c05b --- /dev/null +++ b/web/src/ee/constants/team.ts @@ -0,0 +1,81 @@ +/** + * Enterprise seam for collaborator permissions and team roles. + * + * The constants describe the enterprise permission bit set; open source keeps + * them so shared code can compare against `operator_permission`, but every + * resource is owned by its tenant, so the permission helpers treat the current + * user as owner. + */ + +export enum TeamRole { + Member = 'member', + Department = 'department', + Group = 'group', +} + +export const TeamRoleList = [ + TeamRole.Department, + TeamRole.Member, + TeamRole.Group, +]; + +export enum Permission { + Read = 1, + Write = 2, + Manage = 4, + Owner = 8, +} + +export const PermissionList = [ + Permission.Read, + Permission.Write, + Permission.Manage, +]; + +export function hasResourcePermission(permission: number, action: Permission) { + return (permission & action) === action; +} + +export function getResourcePermissionActions(permission: number) { + if (hasResourcePermission(permission, Permission.Owner)) { + return [Permission.Owner]; + } + return PermissionList.filter((action) => + hasResourcePermission(permission, action), + ); +} + +export function hasResourceOwnerPermission( + permissions?: Record, +) { + return Object.values(permissions ?? {}).some((permission) => + hasResourcePermission(permission, Permission.Owner), + ); +} + +export const LabelMap = { + [Permission.Read]: 'read', + [Permission.Write]: 'write', + [Permission.Manage]: 'manage', + [Permission.Owner]: 'owner', +}; + +export enum PermissionResourceType { + KnowledgeBase = 'kb', + LLM = 'llm', + Search = 'search', + Dialog = 'dialog', + Document = 'document', + Canvas = 'canvas', + Memory = 'memory', + MCP = 'mcp', + File = 'file', + Folder = 'folder', + ModelInstance = 'model_instance', +} + +export enum TenantRole { + Owner = 'owner', + Invite = 'invite', + Normal = 'normal', +} diff --git a/web/src/ee/hooks/auth-hooks-private.ts b/web/src/ee/hooks/auth-hooks-private.ts new file mode 100644 index 0000000000..e5df106641 --- /dev/null +++ b/web/src/ee/hooks/auth-hooks-private.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 The InfiniFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const useEnterpriseAuthRedirect = (): string | undefined => undefined; diff --git a/web/src/ee/hooks/logic-hooks/use-permission.ts b/web/src/ee/hooks/logic-hooks/use-permission.ts new file mode 100644 index 0000000000..aaf7f8b575 --- /dev/null +++ b/web/src/ee/hooks/logic-hooks/use-permission.ts @@ -0,0 +1,12 @@ +/** + * Enterprise permission seam. The enterprise build overlays this file with + * role-based gating; open source has no edit restrictions. + */ + +export function useDatasetEditButtonDisabled() { + return false; +} + +export function useDatasetManageButtonDisabled() { + return false; +} diff --git a/web/src/ee/hooks/use-agent-presence.ts b/web/src/ee/hooks/use-agent-presence.ts new file mode 100644 index 0000000000..95b09281aa --- /dev/null +++ b/web/src/ee/hooks/use-agent-presence.ts @@ -0,0 +1,36 @@ +/** + * Enterprise seam for the canvas presence tracking. The enterprise build + * replaces this file with the presence requests; open source has no shared + * canvas presence, so the presence is always empty and the actions are + * no-ops. + */ + +import type { ICanvasPresenceResponse } from '@/interfaces/database/agent'; + +const emptyPresence: ICanvasPresenceResponse = { + canvas_id: '', + online_user_count: 0, + users: [], +}; + +export const useFetchCanvasPresence = () => ({ + data: emptyPresence, + loading: false, + refetch: async () => {}, +}); + +export const useJoinCanvasPresence = (_canvasId?: string) => ({ + joinPresence: async (_payload: unknown) => {}, + loading: false, +}); + +export const useHeartbeatCanvasPresence = () => ({ + heartbeat: async (_payload: unknown) => null, + loading: false, + data: null, +}); + +export const useLeaveCanvasPresence = () => ({ + leavePresence: async (_payload: unknown) => {}, + loading: false, +}); diff --git a/web/src/ee/hooks/use-empty-model-warning.ts b/web/src/ee/hooks/use-empty-model-warning.ts new file mode 100644 index 0000000000..56122ebfd1 --- /dev/null +++ b/web/src/ee/hooks/use-empty-model-warning.ts @@ -0,0 +1,10 @@ +/** + * Enterprise seam for the empty-model warning. Open source has no admin + * approval flow, so the upstream modal is always the right outcome. + */ +export function useEmptyModelWarning() { + return { + isChecking: false, + showWarning: () => false, + }; +} diff --git a/web/src/ee/hooks/use-private-llm-request.ts b/web/src/ee/hooks/use-private-llm-request.ts new file mode 100644 index 0000000000..20a1b09e07 --- /dev/null +++ b/web/src/ee/hooks/use-private-llm-request.ts @@ -0,0 +1,27 @@ +/** + * Enterprise seam for the private-LLM admin flags. The enterprise build + * replaces this file with the private deployment requests; open source is not + * a private deployment, so the admin flags are off and the default-model + * action is a no-op. Model providers and the API page are part of the open + * source product, so their exposure flags stay on. + */ + +export const useFetchEnableAdmin = () => ({ data: false, loading: false }); + +export const useFetchIsAdmin = () => ({ data: false, loading: false }); + +export const useSetDefaultLlm = () => ({ + data: undefined, + loading: false, + setDefaultLlm: async (_params: { + llm_factory: string; + llm_name: string; + }) => {}, +}); + +export const useFetchExposeModelProvider = () => ({ + data: true, + loading: false, +}); + +export const useFetchExposeApi = () => ({ data: true, loading: false }); diff --git a/web/src/ee/hooks/use-upload-billing-error.ts b/web/src/ee/hooks/use-upload-billing-error.ts new file mode 100644 index 0000000000..31fffe569e --- /dev/null +++ b/web/src/ee/hooks/use-upload-billing-error.ts @@ -0,0 +1,18 @@ +export type UploadBillingResponse = { + code?: number; + message?: string; + detail?: { current?: number; limit?: number; [key: string]: any }; +}; + +/** + * Enterprise seam for the file-upload billing errors. Open source has no + * billing, so every upload failure is handled by the default error path. + */ +export function useUploadBillingError() { + const handleUploadError = (_ret?: UploadBillingResponse) => { + void _ret; + return false; + }; + + return { handleUploadError }; +} diff --git a/web/src/ee/layouts/components/notification-banner.tsx b/web/src/ee/layouts/components/notification-banner.tsx new file mode 100644 index 0000000000..5d2ea991a4 --- /dev/null +++ b/web/src/ee/layouts/components/notification-banner.tsx @@ -0,0 +1,7 @@ +/** + * Enterprise slot above the app shell. The enterprise build replaces this + * file with the notification banner; open source renders nothing. + */ +export function NotificationBanner() { + return null; +} diff --git a/web/src/ee/layouts/components/team-invitation-reminder-dialog/index.tsx b/web/src/ee/layouts/components/team-invitation-reminder-dialog/index.tsx new file mode 100644 index 0000000000..2184da0eb6 --- /dev/null +++ b/web/src/ee/layouts/components/team-invitation-reminder-dialog/index.tsx @@ -0,0 +1,19 @@ +import { Routes } from '@/routes'; +import { PropsWithChildren } from 'react'; +import { Link } from 'react-router'; + +/** + * Enterprise slot around the notification bell. The enterprise build replaces + * this file with the team invitation dialog; open source keeps the bell as a + * shortcut to the team page. + */ +export function TeamInvitationReminderDialog({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/web/src/ee/layouts/root-layout-shell.tsx b/web/src/ee/layouts/root-layout-shell.tsx new file mode 100644 index 0000000000..e2e5a1cecc --- /dev/null +++ b/web/src/ee/layouts/root-layout-shell.tsx @@ -0,0 +1,23 @@ +import React, { PropsWithChildren, useMemo } from 'react'; + +export type RootLayoutChrome = { + showHeader: boolean; + containerRef?: React.Ref; +}; + +/** + * Enterprise seams for the application shell. Open source has no notification + * banner, no billing wrapper and always shows the header, so every seam leaves + * the upstream layout untouched. + */ +export function RootLayoutBanner() { + return null; +} + +export function RootLayoutContent({ children }: PropsWithChildren) { + return <>{children}; +} + +export function useRootLayoutChrome(): RootLayoutChrome { + return useMemo(() => ({ showHeader: true }), []); +} diff --git a/web/src/ee/merge-routes.tsx b/web/src/ee/merge-routes.tsx new file mode 100644 index 0000000000..74f2724e75 --- /dev/null +++ b/web/src/ee/merge-routes.tsx @@ -0,0 +1,10 @@ +/** + * Enterprise route seam (open-source side). + * + * The enterprise build overlays this file with its own implementation, which + * merges the enterprise route tree into the tree upstream already built. + * Open source renders the upstream router unchanged. + */ +import { routers } from '@/routes'; + +export { routers }; diff --git a/web/src/ee/pages/admin/404.tsx b/web/src/ee/pages/admin/404.tsx new file mode 100644 index 0000000000..6259c90b3b --- /dev/null +++ b/web/src/ee/pages/admin/404.tsx @@ -0,0 +1,8 @@ +/** + * Enterprise admin 404 page. The enterprise build replaces this file with the + * "feature not enabled" page; open source renders nothing because no shared + * page is gated behind an enterprise admin toggle. + */ +export default function AdminNotFoundPage() { + return null; +} diff --git a/web/src/ee/pages/admin/hooks/useAdminVariables.ts b/web/src/ee/pages/admin/hooks/useAdminVariables.ts new file mode 100644 index 0000000000..b32277c19f --- /dev/null +++ b/web/src/ee/pages/admin/hooks/useAdminVariables.ts @@ -0,0 +1,19 @@ +/** + * Enterprise seam for the admin system variables. The enterprise build + * replaces this file with the request that reads and writes them; open source + * has no system-variable admin UI, so the values stay empty except for the + * whitelist flag that keeps the open source whitelist page reachable. + */ +const openSourceVariables: Record = { + enable_whitelist: { value: true }, +}; + +export default function useAdminVariables() { + return { + variables: openSourceVariables, + isFetching: false, + refetch: async () => {}, + setVariables: async (_variables: Record) => {}, + isUpdating: false, + }; +} diff --git a/web/src/ee/pages/admin/user-detail-assets.tsx b/web/src/ee/pages/admin/user-detail-assets.tsx new file mode 100644 index 0000000000..4d4d418594 --- /dev/null +++ b/web/src/ee/pages/admin/user-detail-assets.tsx @@ -0,0 +1,9 @@ +/** + * Enterprise slot inside the admin user detail card. The enterprise build + * replaces this file with the user's datasets/agents tabs; open source renders + * nothing. + */ +export default function UserDetailAssets({ userId }: { userId?: string }) { + void userId; + return null; +} diff --git a/web/src/ee/pages/agent/agent-edit-guard.tsx b/web/src/ee/pages/agent/agent-edit-guard.tsx new file mode 100644 index 0000000000..884d4539eb --- /dev/null +++ b/web/src/ee/pages/agent/agent-edit-guard.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from 'react'; + +type AgentEditGuardProps = { + operatorPermission: number; + children: ReactNode; +}; + +/** + * Enterprise seam for the agent canvas editing controls. Open source has no + * collaborator permissions, so every control stays visible. + */ +export function AgentEditGuard({ children }: AgentEditGuardProps) { + return <>{children}; +} diff --git a/web/src/ee/pages/agent/can-edit-agent.ts b/web/src/ee/pages/agent/can-edit-agent.ts new file mode 100644 index 0000000000..e5cc41a01d --- /dev/null +++ b/web/src/ee/pages/agent/can-edit-agent.ts @@ -0,0 +1,8 @@ +/** + * Enterprise seam for canvas edit permissions. Open source has no collaborator + * permissions, so every user may edit and autosave the agent. + */ +export function canEditAgent(_operatorPermission: number) { + void _operatorPermission; + return true; +} diff --git a/web/src/ee/pages/agent/online-users.tsx b/web/src/ee/pages/agent/online-users.tsx new file mode 100644 index 0000000000..67775e9c32 --- /dev/null +++ b/web/src/ee/pages/agent/online-users.tsx @@ -0,0 +1,7 @@ +/** + * Enterprise slot inside the agent debug header. The enterprise build replaces + * this file with the online-presence list; open source renders nothing. + */ +export function OnlineUsers() { + return null; +} diff --git a/web/src/ee/pages/agents/agent-dropdown-guard.tsx b/web/src/ee/pages/agents/agent-dropdown-guard.tsx new file mode 100644 index 0000000000..6bba708c40 --- /dev/null +++ b/web/src/ee/pages/agents/agent-dropdown-guard.tsx @@ -0,0 +1,25 @@ +import { IFlow } from '@/interfaces/database/agent'; +import { ReactNode } from 'react'; + +type AgentDropdownGuardProps = { + agent: IFlow; + showPrivilegeModal(): void; + manageItems: ReactNode; + deleteItems: ReactNode; +}; + +/** + * Enterprise seam for the agent dropdown. Open source has no collaborator + * permissions, so every action the shared file provides is rendered as-is. + */ +export function AgentDropdownGuard({ + manageItems, + deleteItems, +}: AgentDropdownGuardProps) { + return ( + <> + {manageItems} + {deleteItems} + + ); +} diff --git a/web/src/ee/pages/agents/use-show-privilege-dialog.ts b/web/src/ee/pages/agents/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..a2a449ca0b --- /dev/null +++ b/web/src/ee/pages/agents/use-show-privilege-dialog.ts @@ -0,0 +1,18 @@ +import { IFlow } from '@/interfaces/database/agent'; + +/** + * Enterprise seam for the agent permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModalVisible: false, + hidePrivilegeModal: () => {}, + handShowPrivilegeModal: (_item: IFlow) => () => {}, + recordWithSourceType: {}, + }; +} + +export type ShowPrivilegeDialogReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; diff --git a/web/src/ee/pages/dataset/components/metedata/hooks/use-save-builtin-metadata.ts b/web/src/ee/pages/dataset/components/metedata/hooks/use-save-builtin-metadata.ts new file mode 100644 index 0000000000..be87656cf6 --- /dev/null +++ b/web/src/ee/pages/dataset/components/metedata/hooks/use-save-builtin-metadata.ts @@ -0,0 +1,28 @@ +import { + IBuiltInMetadataItem, + IMetaDataReturnJSONSettings, +} from '@/pages/dataset/components/metedata/interface'; +import { useCallback } from 'react'; + +export type SaveBuiltinMetadataParams = { + kbId?: string; + documentId?: string; + builtInMetadata?: IBuiltInMetadataItem[]; + metadata: IMetaDataReturnJSONSettings; +}; + +/** + * Enterprise seam for persisting built-in metadata alongside a single + * document update. Open source has no collaborator permissions, so the shared + * save flow already covers it. + */ +export function useSaveBuiltinMetadata() { + const saveBuiltinMetadata = useCallback( + async (_params: SaveBuiltinMetadataParams) => { + void _params; + }, + [], + ); + + return { saveBuiltinMetadata }; +} diff --git a/web/src/ee/pages/dataset/dataset/dataset-action-cell-seams.tsx b/web/src/ee/pages/dataset/dataset/dataset-action-cell-seams.tsx new file mode 100644 index 0000000000..ae37cb595d --- /dev/null +++ b/web/src/ee/pages/dataset/dataset/dataset-action-cell-seams.tsx @@ -0,0 +1,25 @@ +import { IDocumentInfo } from '@/interfaces/database/document'; + +export type DocumentActionCellOverrides = { + actionClassName?: string; +}; + +export type DocumentPrivilegeActionProps = { + record: IDocumentInfo; + disabled: boolean; +}; + +const overrides: DocumentActionCellOverrides = {}; + +/** + * Enterprise seams for the document row actions. Open source keeps the upstream + * hover-only controls and exposes no permission entry. + */ +export function useDocumentActionCellOverrides(): DocumentActionCellOverrides { + return overrides; +} + +export function DocumentPrivilegeAction(_props: DocumentPrivilegeActionProps) { + void _props; + return null; +} diff --git a/web/src/ee/pages/dataset/dataset/use-extra-bulk-actions.tsx b/web/src/ee/pages/dataset/dataset/use-extra-bulk-actions.tsx new file mode 100644 index 0000000000..c108090f9e --- /dev/null +++ b/web/src/ee/pages/dataset/dataset/use-extra-bulk-actions.tsx @@ -0,0 +1,22 @@ +import { ShowPrivilegeModalReturnType } from '@/ee/pages/dataset/dataset/use-show-privilege-dialog'; +import { ReactNode } from 'react'; + +export type BulkOperationAction = { + id: string; + label: string; + icon: ReactNode; + onClick?: () => void; +}; + +/** + * Enterprise seam for dataset bulk-operation entries. Open source has no + * collaborator permissions, so no extra entry is appended. + */ +export function useExtraBulkActions( + _handShowPrivilegeModal: ShowPrivilegeModalReturnType['handShowPrivilegeModal'], + _selectedRowKeys: string[], +): BulkOperationAction[] { + void _handShowPrivilegeModal; + void _selectedRowKeys; + return []; +} diff --git a/web/src/ee/pages/dataset/dataset/use-show-privilege-dialog.ts b/web/src/ee/pages/dataset/dataset/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..0c3a912290 --- /dev/null +++ b/web/src/ee/pages/dataset/dataset/use-show-privilege-dialog.ts @@ -0,0 +1,23 @@ +import { IDocumentInfo } from '@/interfaces/database/document'; +import { createContext } from 'react'; + +/** + * Enterprise seam for the document permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModal: false, + hidePrivilegeModal: () => {}, + handShowPrivilegeModal: (_item: IDocumentInfo | string[]) => () => {}, + recordWithSourceType: {}, + }; +} + +export type ShowPrivilegeModalReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; + +export const ShowPrivilegeModalContext = createContext< + Partial +>({}); diff --git a/web/src/ee/pages/datasets/dataset-dropdown-guard.tsx b/web/src/ee/pages/datasets/dataset-dropdown-guard.tsx new file mode 100644 index 0000000000..13bd2820f1 --- /dev/null +++ b/web/src/ee/pages/datasets/dataset-dropdown-guard.tsx @@ -0,0 +1,25 @@ +import { IDataset } from '@/interfaces/database/dataset'; +import { ReactNode } from 'react'; + +type DatasetDropdownGuardProps = { + dataset: IDataset; + showPrivilegeModal(): void; + manageItems: ReactNode; + deleteItems: ReactNode; +}; + +/** + * Enterprise seam for the dataset dropdown. Open source has no collaborator + * permissions, so every action the shared file provides is rendered as-is. + */ +export function DatasetDropdownGuard({ + manageItems, + deleteItems, +}: DatasetDropdownGuardProps) { + return ( + <> + {manageItems} + {deleteItems} + + ); +} diff --git a/web/src/ee/pages/datasets/use-duplicate-dataset.ts b/web/src/ee/pages/datasets/use-duplicate-dataset.ts new file mode 100644 index 0000000000..3989fa0d5e --- /dev/null +++ b/web/src/ee/pages/datasets/use-duplicate-dataset.ts @@ -0,0 +1,18 @@ +import { IDataset } from '@/interfaces/database/dataset'; + +/** + * Enterprise seam for the dataset duplicate action. Open source has no + * duplicate dialog, so the hook reports a permanently closed modal. + */ +export default function useDuplicateDataset() { + return { + isModalVisible: false, + showModal: (_record: IDataset) => {}, + hideModal: () => {}, + initialName: '', + onOk: async (_name: string) => {}, + loading: false, + }; +} + +export type DuplicateDatasetReturnType = ReturnType; diff --git a/web/src/ee/pages/datasets/use-show-privilege-dialog.ts b/web/src/ee/pages/datasets/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..876025120e --- /dev/null +++ b/web/src/ee/pages/datasets/use-show-privilege-dialog.ts @@ -0,0 +1,18 @@ +import { IDataset } from '@/interfaces/database/dataset'; + +/** + * Enterprise seam for the dataset permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModal: false, + hidePrivilegeModal: () => {}, + handShowPrivilegeModal: (_item: IDataset) => () => {}, + recordWithSourceType: {}, + }; +} + +export type ShowPrivilegeDialogReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; diff --git a/web/src/ee/pages/files/file-privilege-button.tsx b/web/src/ee/pages/files/file-privilege-button.tsx new file mode 100644 index 0000000000..c832eb8da8 --- /dev/null +++ b/web/src/ee/pages/files/file-privilege-button.tsx @@ -0,0 +1,15 @@ +import { IFile } from '@/interfaces/database/file-manager'; + +type FilePrivilegeButtonProps = { + record: IFile; + showPrivilegeModal?(record: IFile): void; +}; + +/** + * Enterprise seam for the file permission entry point. Open source has no + * collaborator permissions, so the control is never rendered. + */ +export function FilePrivilegeButton(_props: FilePrivilegeButtonProps) { + void _props; + return null; +} diff --git a/web/src/ee/pages/memories/memory-dropdown-guard.tsx b/web/src/ee/pages/memories/memory-dropdown-guard.tsx new file mode 100644 index 0000000000..737075bcfa --- /dev/null +++ b/web/src/ee/pages/memories/memory-dropdown-guard.tsx @@ -0,0 +1,25 @@ +import { IMemory } from '@/pages/memories/interface'; +import { ReactNode } from 'react'; + +type MemoryDropdownGuardProps = { + memory: IMemory; + showPrivilegeModal(): void; + manageItems: ReactNode; + deleteItems: ReactNode; +}; + +/** + * Enterprise seam for the memory dropdown. Open source has no collaborator + * permissions, so every action the shared file provides is rendered as-is. + */ +export function MemoryDropdownGuard({ + manageItems, + deleteItems, +}: MemoryDropdownGuardProps) { + return ( + <> + {manageItems} + {deleteItems} + + ); +} diff --git a/web/src/ee/pages/memories/use-show-privilege-dialog.ts b/web/src/ee/pages/memories/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..06331324ca --- /dev/null +++ b/web/src/ee/pages/memories/use-show-privilege-dialog.ts @@ -0,0 +1,18 @@ +import { IMemory } from '@/pages/memories/interface'; + +/** + * Enterprise seam for the memory permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModalVisible: false, + hidePrivilegeModal: () => {}, + handShowPrivilegeModal: (_item: IMemory) => () => {}, + recordWithSourceType: {}, + }; +} + +export type ShowPrivilegeDialogReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; diff --git a/web/src/ee/pages/next-chats/chat-dropdown-guard.tsx b/web/src/ee/pages/next-chats/chat-dropdown-guard.tsx new file mode 100644 index 0000000000..767f6b5b1c --- /dev/null +++ b/web/src/ee/pages/next-chats/chat-dropdown-guard.tsx @@ -0,0 +1,25 @@ +import { IDialog } from '@/interfaces/database/chat'; +import { ReactNode } from 'react'; + +type ChatDropdownGuardProps = { + chat: IDialog; + showPrivilegeModal(): void; + manageItems: ReactNode; + deleteItems: ReactNode; +}; + +/** + * Enterprise seam for the chat dropdown. Open source has no collaborator + * permissions, so every action the shared file provides is rendered as-is. + */ +export function ChatDropdownGuard({ + manageItems, + deleteItems, +}: ChatDropdownGuardProps) { + return ( + <> + {manageItems} + {deleteItems} + + ); +} diff --git a/web/src/ee/pages/next-chats/chat/evaluation-button.tsx b/web/src/ee/pages/next-chats/chat/evaluation-button.tsx new file mode 100644 index 0000000000..cb39459b0b --- /dev/null +++ b/web/src/ee/pages/next-chats/chat/evaluation-button.tsx @@ -0,0 +1,8 @@ +/** + * Enterprise slot for the chat evaluation entry. The enterprise build replaces + * this file with the evaluation button; open source has no evaluation module, + * so the slot renders nothing. + */ +export function ChatEvaluationButton() { + return null; +} diff --git a/web/src/ee/pages/next-chats/use-show-privilege-dialog.ts b/web/src/ee/pages/next-chats/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..2f693d0f4a --- /dev/null +++ b/web/src/ee/pages/next-chats/use-show-privilege-dialog.ts @@ -0,0 +1,18 @@ +import { IDialog } from '@/interfaces/database/chat'; + +/** + * Enterprise seam for the chat permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModalVisible: false, + hidePrivilegeModal: () => {}, + privilegeRecord: {}, + handleShowPrivilegeModal: (_dialog: IDialog) => (_info?: any) => {}, + }; +} + +export type ShowPrivilegeDialogReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; diff --git a/web/src/ee/pages/next-searches/search-dropdown-guard.tsx b/web/src/ee/pages/next-searches/search-dropdown-guard.tsx new file mode 100644 index 0000000000..ba5567231c --- /dev/null +++ b/web/src/ee/pages/next-searches/search-dropdown-guard.tsx @@ -0,0 +1,25 @@ +import { ISearchAppProps } from '@/pages/next-searches/hooks'; +import { ReactNode } from 'react'; + +type SearchDropdownGuardProps = { + dataset: ISearchAppProps; + showPrivilegeModal(): void; + manageItems: ReactNode; + deleteItems: ReactNode; +}; + +/** + * Enterprise seam for the search-app dropdown. Open source has no collaborator + * permissions, so every action the shared file provides is rendered as-is. + */ +export function SearchDropdownGuard({ + manageItems, + deleteItems, +}: SearchDropdownGuardProps) { + return ( + <> + {manageItems} + {deleteItems} + + ); +} diff --git a/web/src/ee/pages/next-searches/use-show-privilege-dialog.ts b/web/src/ee/pages/next-searches/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..ed0f07c00c --- /dev/null +++ b/web/src/ee/pages/next-searches/use-show-privilege-dialog.ts @@ -0,0 +1,18 @@ +import { ISearchAppProps } from '@/pages/next-searches/hooks'; + +/** + * Enterprise seam for the search-app permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModalVisible: false, + hidePrivilegeModal: () => {}, + handShowPrivilegeModal: (_item: ISearchAppProps) => () => {}, + recordWithSourceType: {}, + }; +} + +export type ShowPrivilegeDialogReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; diff --git a/web/src/ee/pages/price/constant.ts b/web/src/ee/pages/price/constant.ts new file mode 100644 index 0000000000..78b14a32ea --- /dev/null +++ b/web/src/ee/pages/price/constant.ts @@ -0,0 +1,19 @@ +/** + * Enterprise seam for the plan names. The enterprise build replaces this file + * with the pricing catalogue; open source only needs the plan identifiers that + * shared billing-gated code compares against. + */ + +export enum PriceName { + Trial = 'Trial', + Starter = 'Starter', + Pro = 'Pro', + Enterprise = 'Enterprise', +} + +export const PriceNameMapValue = { + [PriceName.Trial]: 0, + [PriceName.Starter]: 1, + [PriceName.Pro]: 2, + [PriceName.Enterprise]: 3, +}; diff --git a/web/src/ee/pages/price/global/hook.ts b/web/src/ee/pages/price/global/hook.ts new file mode 100644 index 0000000000..5171296db5 --- /dev/null +++ b/web/src/ee/pages/price/global/hook.ts @@ -0,0 +1,30 @@ +/** + * Enterprise seam for the upgrade prompts. The enterprise build replaces this + * file with the events and modals that route the user to the price page; open + * source has no billing, so every entry point is a no-op. + */ + +export const showUpgradeTipsModal = (_options?: unknown) => {}; + +export const showPriceConfirmModal = (_options?: unknown) => {}; + +export const showFreeUpgradeTipsModal = (_options?: unknown) => {}; + +export const showPriceModal = (_price?: unknown) => {}; + +export const useShowUpgradeTipsModal = () => ({ + upgradeTips: undefined, + hideUpgradeTips: () => {}, +}); + +export const useShowFreeUpgradeTipsModal = () => ({ + freeUpgradeTips: undefined, + hideFreeUpgradeTips: () => {}, +}); + +export const useShowConfirmPriceModal = () => ({ + confirmPrice: undefined, + hideConfirmPrice: () => {}, +}); + +export const useComputedRouterChangeCount = () => 0; diff --git a/web/src/ee/pages/price/global/index.tsx b/web/src/ee/pages/price/global/index.tsx new file mode 100644 index 0000000000..f2b8a1eddd --- /dev/null +++ b/web/src/ee/pages/price/global/index.tsx @@ -0,0 +1,34 @@ +import { ReactNode, createContext, useContext } from 'react'; + +/** + * Enterprise seam for the global upgrade modal. The enterprise build replaces + * this file with the price modal providers; open source never enables billing, + * so the provider renders its children unchanged. + */ + +interface UpgradeModalContextType { + isModalOpen: boolean; + openModal: (modalType?: string, modalProps?: any) => void; + closeModal: () => void; +} + +const UpgradeModalContext = createContext( + undefined, +); + +export const UpgradeModalProvider = ({ + children, +}: { + children?: ReactNode; +}) => <>{children}; + +export const useUpgradeModal = () => { + const context = useContext(UpgradeModalContext); + return ( + context ?? { + isModalOpen: false, + openModal: () => {}, + closeModal: () => {}, + } + ); +}; diff --git a/web/src/ee/pages/price/hook/use-price-hooks.ts b/web/src/ee/pages/price/hook/use-price-hooks.ts new file mode 100644 index 0000000000..b72cb8c49c --- /dev/null +++ b/web/src/ee/pages/price/hook/use-price-hooks.ts @@ -0,0 +1,36 @@ +/** + * Enterprise seam for the billing plan requests. The enterprise build replaces + * this file with the price catalogue requests; open source has no billing, so + * the current plan is empty. + */ + +export const useFetchCurrentPlan = () => ({ + data: { plan_name: '' }, + loading: false, +}); + +export const useFetchPlanList = () => ({ + data: [], + loading: false, +}); + +export const useCharge = () => ({ + charge: async (_params: unknown) => {}, + loading: false, +}); + +export const useCancelPlan = () => ({ + cancelPlan: async (_params?: unknown) => {}, + loading: false, +}); + +export const useDirectPaymentConvergence = () => {}; + +export const useHandleTrialUpgradeSetupRetry = (_status: string | null) => {}; + +export const useSetupIntentPayment = () => ({ + setupIntentPayment: async (_params?: unknown) => {}, + loading: false, +}); + +export const getNextMonth = () => ''; diff --git a/web/src/ee/pages/user-setting/mcp/mcp-list-guard.tsx b/web/src/ee/pages/user-setting/mcp/mcp-list-guard.tsx new file mode 100644 index 0000000000..0275c4771d --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/mcp-list-guard.tsx @@ -0,0 +1,57 @@ +import { IMcpServer } from '@/interfaces/database/mcp'; +import { ReactNode } from 'react'; + +type McpToolbarGuardProps = { + mcpList: IMcpServer[]; + bulkManageButton: ReactNode; + children: ReactNode; +}; + +type McpSelectionBarGuardProps = { + mcpList: IMcpServer[]; + children: ReactNode; +}; + +type McpEmptyStateGuardProps = { + mcpList: IMcpServer[]; + addButton: ReactNode; + onClickAdd(): void; + children: ReactNode; +}; + +/** + * Enterprise seams for the MCP server list. Open source has no collaborator + * permissions, so the toolbar, the bulk-selection bar and the empty state are + * always available with every action. + */ +export function McpToolbarGuard({ + bulkManageButton, + children, +}: McpToolbarGuardProps) { + return ( + <> + {bulkManageButton} + {children} + + ); +} + +export function McpSelectionBarGuard({ children }: McpSelectionBarGuardProps) { + return <>{children}; +} + +export function McpEmptyStateGuard({ + addButton, + onClickAdd, + children, +}: McpEmptyStateGuardProps) { + return ( +
+ {children} + {addButton} +
+ ); +} diff --git a/web/src/ee/pages/user-setting/mcp/mcp-operation-shell.tsx b/web/src/ee/pages/user-setting/mcp/mcp-operation-shell.tsx new file mode 100644 index 0000000000..6794c5f043 --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/mcp-operation-shell.tsx @@ -0,0 +1,17 @@ +import { IMcpServer } from '@/interfaces/database/mcp'; +import { UseEditMcpReturnType } from '@/pages/user-setting/mcp/use-edit-mcp'; +import { PropsWithChildren } from 'react'; + +export type McpOperationShellProps = PropsWithChildren< + { mcp: IMcpServer } & Pick & { + showPrivilegeModal(): void; + } +>; + +/** + * Enterprise seam for the MCP server card actions. Open source renders the + * upstream hover controls unchanged. + */ +export function McpOperationShell({ children }: McpOperationShellProps) { + return <>{children}; +} diff --git a/web/src/ee/pages/user-setting/mcp/mcp-selection-guard.tsx b/web/src/ee/pages/user-setting/mcp/mcp-selection-guard.tsx new file mode 100644 index 0000000000..102194568b --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/mcp-selection-guard.tsx @@ -0,0 +1,15 @@ +import { IMcpServer } from '@/interfaces/database/mcp'; +import { ReactNode } from 'react'; + +type McpSelectionGuardProps = { + mcp: IMcpServer; + children: ReactNode; +}; + +/** + * Enterprise seam for the MCP card selection control. Open source has no + * collaborator permissions, so every listed server is selectable. + */ +export function McpSelectionGuard({ children }: McpSelectionGuardProps) { + return <>{children}; +} diff --git a/web/src/ee/pages/user-setting/mcp/use-exit-mcp-selection-without-owner.ts b/web/src/ee/pages/user-setting/mcp/use-exit-mcp-selection-without-owner.ts new file mode 100644 index 0000000000..6a6f3d6879 --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/use-exit-mcp-selection-without-owner.ts @@ -0,0 +1,16 @@ +import { IMcpServer } from '@/interfaces/database/mcp'; + +/** + * Enterprise seam for the MCP bulk-selection availability. Open source has no + * collaborator permissions, so a selection never has to be exited because the + * operator lost owner access. + */ +export function useExitMcpSelectionWithoutOwner( + _mcpList: IMcpServer[], + _isSelectionMode: boolean, + _exitSelectionMode: () => void, +) { + void _mcpList; + void _isSelectionMode; + void _exitSelectionMode; +} diff --git a/web/src/ee/pages/user-setting/mcp/use-mcp-read-only.ts b/web/src/ee/pages/user-setting/mcp/use-mcp-read-only.ts new file mode 100644 index 0000000000..0f703e5866 --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/use-mcp-read-only.ts @@ -0,0 +1,8 @@ +/** + * Enterprise seam for the MCP server edit permission. Open source has no + * collaborator permissions, so every visible server stays editable. + */ +export function useMcpReadOnly(_operatorPermission?: number) { + void _operatorPermission; + return false; +} diff --git a/web/src/ee/pages/user-setting/mcp/use-owner-mcp-list.ts b/web/src/ee/pages/user-setting/mcp/use-owner-mcp-list.ts new file mode 100644 index 0000000000..c6a1e6db52 --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/use-owner-mcp-list.ts @@ -0,0 +1,9 @@ +import { IMcpServer } from '@/interfaces/database/mcp'; + +/** + * Enterprise seam for the MCP bulk-selection scope. Open source has no + * collaborator permissions, so every listed server can be selected. + */ +export function useOwnerMcpList(mcpList: IMcpServer[]) { + return mcpList; +} diff --git a/web/src/ee/pages/user-setting/mcp/use-show-privilege-dialog.ts b/web/src/ee/pages/user-setting/mcp/use-show-privilege-dialog.ts new file mode 100644 index 0000000000..170b1dfd9c --- /dev/null +++ b/web/src/ee/pages/user-setting/mcp/use-show-privilege-dialog.ts @@ -0,0 +1,18 @@ +import { IMcpServer } from '@/interfaces/database/mcp'; + +/** + * Enterprise seam for the MCP server permission dialog. Open source has no + * collaborator permissions, so the modal stays closed and the record is empty. + */ +export function useShowPrivilegeDialog() { + return { + privilegeModal: false, + hidePrivilegeModal: () => {}, + handShowPrivilegeModal: (_item: IMcpServer) => () => {}, + recordWithSourceType: {}, + }; +} + +export type ShowPrivilegeDialogReturnType = ReturnType< + typeof useShowPrivilegeDialog +>; diff --git a/web/src/ee/pages/user-setting/setting-model/provider-instance-card-with-privilege.tsx b/web/src/ee/pages/user-setting/setting-model/provider-instance-card-with-privilege.tsx new file mode 100644 index 0000000000..5cc1560863 --- /dev/null +++ b/web/src/ee/pages/user-setting/setting-model/provider-instance-card-with-privilege.tsx @@ -0,0 +1,26 @@ +import { IProviderInstance } from '@/interfaces/database/llm'; +import { ProviderInstanceCard } from '@/pages/user-setting/setting-model/instance-card/provider-instance-card'; + +type ProviderInstanceCardWithPrivilegeProps = { + providerName: string; + instance: IProviderInstance; + defaultOpen: boolean; +}; + +/** + * Enterprise seam around a provider instance card. Open source has no + * collaborator permissions, so the card renders unchanged. + */ +export function ProviderInstanceCardWithPrivilege({ + providerName, + instance, + defaultOpen, +}: ProviderInstanceCardWithPrivilegeProps) { + return ( + + ); +} diff --git a/web/src/ee/pages/user-setting/setting-team/index.module.less b/web/src/ee/pages/user-setting/setting-team/index.module.less new file mode 100644 index 0000000000..12c572af90 --- /dev/null +++ b/web/src/ee/pages/user-setting/setting-team/index.module.less @@ -0,0 +1,9 @@ +.teamWrapper { + width: 100%; + display: flex; + flex-direction: column; + gap: 20px; + .teamCard { + // width: 100%; + } +} diff --git a/web/src/ee/pages/user-setting/setting-team/team-management/index.tsx b/web/src/ee/pages/user-setting/setting-team/team-management/index.tsx new file mode 100644 index 0000000000..2677071271 --- /dev/null +++ b/web/src/ee/pages/user-setting/setting-team/team-management/index.tsx @@ -0,0 +1,10 @@ +import UserSettingTeam from '@/pages/user-setting/setting-team/legacy-page'; + +/** + * Enterprise slot for the team management page. The enterprise build replaces + * this file with the department/group/member tabs; open source keeps the + * upstream team page. + */ +export function TeamManagement() { + return ; +} diff --git a/web/src/ee/pages/user-setting/sidebar/use-setting-menu.tsx b/web/src/ee/pages/user-setting/sidebar/use-setting-menu.tsx new file mode 100644 index 0000000000..6a5d61f190 --- /dev/null +++ b/web/src/ee/pages/user-setting/sidebar/use-setting-menu.tsx @@ -0,0 +1,19 @@ +import { ReactNode, useMemo } from 'react'; + +export type SettingMenuEntry = { + icon: ReactNode; + label: string; + key: string; + 'data-testid'?: string; +}; + +/** + * Enterprise seam for the settings sidebar entries. Open source has no billing + * page and no model-provider exposure rules, so the upstream menu is shown + * unchanged. + */ +export function useSettingMenuItems( + items: T[], +): T[] { + return useMemo(() => items, [items]); +} diff --git a/web/src/ee/private-routes.tsx b/web/src/ee/private-routes.tsx new file mode 100644 index 0000000000..8611dec9ac --- /dev/null +++ b/web/src/ee/private-routes.tsx @@ -0,0 +1,32 @@ +import type { ComponentType, ReactNode } from 'react'; + +/** + * Enterprise seam for the private route table. The enterprise build replaces + * this file with the enterprise routes merged over the upstream tree; open + * source only needs the path identifiers that shared code navigates to. + */ +export enum PrivateRoutes { + Price = '/price', + Billing = '/billing', + EvaluationDetail = '/evaluation-detail', + Evaluation = '/evaluation', + Tickets = '/support', + TicketCreate = '/support/create', + TicketDetail = '/support/:id', + AdminRoleConfigModelProviders = '/admin/roles/:roleName/model-providers', + AdminModelUsageStatistics = '/admin/model-usage-statistics', + AdminSettings = '/admin/settings', + AdminSSOProviders = '/admin/sso', + AdminModelProviders = '/admin/model-providers', +} + +export type PrivateRoute = { + path?: string; + Component?: () => Promise<{ default: ComponentType }>; + errorElement?: ReactNode; + children?: PrivateRoute[]; + replace?: string[]; + matchIndex?: number; +}; + +export const privateRoutes: PrivateRoute[] = []; diff --git a/web/src/ee/services/billingStatus.ts b/web/src/ee/services/billingStatus.ts new file mode 100644 index 0000000000..ae02b064b3 --- /dev/null +++ b/web/src/ee/services/billingStatus.ts @@ -0,0 +1,12 @@ +/** + * Enterprise seam for the runtime billing status lookup. + * + * The enterprise build replaces this file with a request to the billing + * endpoint so the header and price pages can gate on the server-side flag. + * Open source has no billing: the status is always disabled and the boot + * sequence does not wait on a request. + */ + +export const fetchBillingStatus = async (): Promise => false; + +export const isBillingEnabled = (): boolean | null => false; diff --git a/web/src/ee/utils/permission-util.ts b/web/src/ee/utils/permission-util.ts new file mode 100644 index 0000000000..c47e23ec91 --- /dev/null +++ b/web/src/ee/utils/permission-util.ts @@ -0,0 +1,43 @@ +/** + * Enterprise seam for resource-level permission checks. + * + * The enterprise build replaces this file with collaborator-aware checks based + * on `operator_permission`. Open source has no collaborator permissions: every + * resource belongs to its tenant and every user who can see it can edit and + * delete it. + */ + +export function hasPreviewPermission(operatorPermission: number) { + void operatorPermission; + return false; +} + +export function hasEditPermission(operatorPermission: number) { + void operatorPermission; + return true; +} + +export function hasManagementPermission(operatorPermission: number) { + void operatorPermission; + return true; +} + +export function hasOwnerPermission(operatorPermission: number) { + void operatorPermission; + return true; +} + +export function showEditButton(operatorPermission: number) { + void operatorPermission; + return true; +} + +export function disableEditButton(operatorPermission: number) { + void operatorPermission; + return false; +} + +export function hasManagePermissionPermission(operatorPermission: number) { + void operatorPermission; + return true; +} diff --git a/web/src/ee/utils/private-util.ts b/web/src/ee/utils/private-util.ts new file mode 100644 index 0000000000..bc8b9ee2e5 --- /dev/null +++ b/web/src/ee/utils/private-util.ts @@ -0,0 +1,14 @@ +/** + * Enterprise seam for the navigation helpers used by auth guards and the + * request interceptors. + * + * The enterprise build sends users back to login with a `redirect` query so + * they land on the page they were viewing. Open source keeps the upstream + * redirect target and has no post-login redirect hop. + */ + +export { redirectToLogin } from '@/utils/authorization-util'; + +export function redirectToSpecifiedPage(page: string) { + window.location.href = location.origin + page; +} diff --git a/web/src/ee/utils/response-private.ts b/web/src/ee/utils/response-private.ts new file mode 100644 index 0000000000..9615f1e700 --- /dev/null +++ b/web/src/ee/utils/response-private.ts @@ -0,0 +1,17 @@ +/** + * Enterprise seam for response-error handling. + * + * The enterprise build inspects the response code to open the billing + * upgrade modal and to keep license-related login errors quiet, then returns + * `true` so the shared interceptor skips its generic notification. + * Open source has neither, so every error keeps the upstream notification. + */ + +export type ResponseErrorContext = { + url?: string; +}; + +export const handleEnterpriseResponseError = async ( + _data: { code?: number; message?: string } | undefined, + _context?: ResponseErrorContext, +): Promise => false; diff --git a/web/src/hooks/auth-hooks.ts b/web/src/hooks/auth-hooks.ts index e744467270..7b714ad768 100644 --- a/web/src/hooks/auth-hooks.ts +++ b/web/src/hooks/auth-hooks.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { useEnterpriseAuthRedirect } from '@/ee/hooks/auth-hooks-private'; import authorizationUtil, { type CredentialStorage, } from '@/utils/authorization-util'; @@ -22,11 +23,13 @@ import { useEffect, useState } from 'react'; export const useAuth = ( credentialStorage: CredentialStorage = authorizationUtil, ) => { + const redirectUrl = useEnterpriseAuthRedirect(); + const [isLogin, setIsLogin] = useState>(null); useEffect(() => { setIsLogin(!!credentialStorage.getAuthorization()); }, [credentialStorage]); - return { isLogin }; + return { isLogin, redirectUrl }; }; diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 78fd16a0cb..7551ba874b 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -18,6 +18,7 @@ import { FileUploadProps } from '@/components/file-upload'; import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit'; import message from '@/components/ui/message'; import { AgentCategory, AgentGlobals } from '@/constants/agent'; +import { Permission } from '@/ee/constants/team'; import { ListDeletionKey } from '@/constants/list-deletion'; import { useFetchTenantInfo } from '@/hooks/use-user-setting-request'; import { @@ -105,6 +106,10 @@ export const enum AgentApiAction { CreateAgentSession = 'createAgentSession', DeleteAgentSession = 'deleteAgentSession', FetchSessionByIdManually = 'fetchSessionByIdManually', + FetchCanvasPresence = 'fetchCanvasPresence', + JoinCanvasPresence = 'joinCanvasPresence', + HeartbeatCanvasPresence = 'heartbeatCanvasPresence', + LeaveCanvasPresence = 'leaveCanvasPresence', FetchAgentLog = 'fetchAgentLog', FetchSharedAgent = 'fetchSharedAgent', FetchAgentTags = 'fetchAgentTags', @@ -379,7 +384,7 @@ export const useFetchAgentTags = (canvasCategory?: string) => { }, true, ); - return data?.data ?? []; + return Array.isArray(data?.data) ? data.data : []; }, }); return { data, loading }; @@ -1014,22 +1019,34 @@ export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => { export const AgentSessionKeys = { all: [AgentApiAction.FetchSessionsByCanvasId] as const, - list: (canvasId?: string, tenantId?: string) => - [...AgentSessionKeys.all, canvasId, tenantId] as const, + list: ( + canvasId?: string, + tenantId?: string, + operatorPermission?: Permission, + ) => + [...AgentSessionKeys.all, canvasId, tenantId, operatorPermission] as const, }; export const useFetchSessionsByCanvasId = () => { const { id: canvasId } = useParams(); const { data: tenantInfo } = useFetchTenantInfo(); + const { data: agentDetail } = useFetchAgent(); + const operatorPermission = agentDetail?.operator_permission; return useSessionList({ - queryKey: AgentSessionKeys.list(canvasId, tenantInfo.tenant_id), - enabled: !!canvasId && !isEmpty(tenantInfo), + queryKey: AgentSessionKeys.list( + canvasId, + tenantInfo.tenant_id, + operatorPermission, + ), + enabled: + !!canvasId && !isEmpty(tenantInfo) && operatorPermission !== undefined, fetchPage: async (params) => { + const shouldListAllSessions = operatorPermission !== Permission.Read; const { data } = await fetchAgentLogsByCanvasId(canvasId!, { ...params, include_history: false, - exp_user_id: tenantInfo.tenant_id, + ...(shouldListAllSessions ? {} : { exp_user_id: tenantInfo.tenant_id }), }); return { total: data?.total ?? 0, items: data?.data ?? [] }; diff --git a/web/src/hooks/use-file-request.ts b/web/src/hooks/use-file-request.ts index 8f1b4bab47..8b2285654e 100644 --- a/web/src/hooks/use-file-request.ts +++ b/web/src/hooks/use-file-request.ts @@ -92,6 +92,11 @@ export const useUploadFile = () => { formData.append('path', pathList[index]); }); try { + // Return the full response (code + message + detail) so the caller + // can dispatch the upgrade modal for billing codes 2000-2007. The + // global request interceptor already shows the modal for 2000-2005, + // but returning the full payload keeps the page-level handler + // consistent with the dataset upload flow. const ret = await fileManagerService.uploadFile(formData); if (ret?.data.code === 0) { message.success(t('message.uploaded')); @@ -100,9 +105,9 @@ export const useUploadFile = () => { queryKey: [FileApiAction.FetchFileList], }); } - return ret?.data?.code; + return ret?.data; } catch { - return; + return undefined; } }, }); diff --git a/web/src/hooks/use-user-setting-request.tsx b/web/src/hooks/use-user-setting-request.tsx index 80e52ea9a4..7914352432 100644 --- a/web/src/hooks/use-user-setting-request.tsx +++ b/web/src/hooks/use-user-setting-request.tsx @@ -15,6 +15,7 @@ */ import message from '@/components/ui/message'; +import { TenantRole } from '@/ee/constants/team'; import { ResponseGetType } from '@/interfaces/database/base'; import { IToken } from '@/interfaces/database/chat'; import { ITenantInfo } from '@/interfaces/database/dataset'; @@ -83,18 +84,27 @@ export const useFetchUserInfo = (): ResponseGetType => { return { data, loading }; }; -export const useFetchTenantData = (): ResponseGetType => { +/** + * @param enabled Set to `false` where the tenant endpoint is not part of the + * page's contract (e.g. the admin gateway, which serves every tenant and + * has no `/users/me/models`). Defaults to `true`, so existing callers are + * unchanged. + */ +export const useFetchTenantData = ( + enabled: boolean = true, +): ResponseGetType => { const { data, isFetching: loading } = useQuery({ queryKey: [UserSettingApiAction.TenantInfo], initialData: {}, gcTime: 0, + enabled, queryFn: async () => { const { data: res } = await userService.getTenantInfo(); if (res.code === 0) { return res.data ?? {}; } - return res; + return res ?? {}; }, }); return { data, loading }; @@ -311,9 +321,10 @@ export const useCreateSystemToken = () => { return { data, loading, createToken: mutateAsync }; }; -export const useListTenantUser = () => { - const { data: tenantInfo } = useFetchTenantInfo(); - const tenantId = tenantInfo.tenant_id; +export const useListTenantUser = ( + tenantId: string, + excludePendingInvitations = false, +) => { const { data, isFetching: loading, @@ -323,6 +334,10 @@ export const useListTenantUser = () => { initialData: [], gcTime: 0, enabled: !!tenantId, + select: (data) => + excludePendingInvitations + ? data.filter((x) => x.role !== TenantRole.Invite) + : data, queryFn: async () => { const { data } = await listTenantUser(tenantId); @@ -380,9 +395,6 @@ export const useDeleteTenantUser = () => { }); if (data.code === 0) { message.success(t('message.deleted')); - queryClient.invalidateQueries({ - queryKey: [UserSettingApiAction.ListTenantUser], - }); queryClient.invalidateQueries({ queryKey: [UserSettingApiAction.ListTenant], }); @@ -409,7 +421,7 @@ export const useListTenant = () => { queryFn: async () => { const { data } = await listTenant(); - return data?.data ?? []; + return Array.isArray(data?.data) ? data.data : []; }, }); diff --git a/web/src/hooks/use-warn-empty-model.tsx b/web/src/hooks/use-warn-empty-model.tsx index 447a8cb08e..123c3945ba 100644 --- a/web/src/hooks/use-warn-empty-model.tsx +++ b/web/src/hooks/use-warn-empty-model.tsx @@ -15,6 +15,7 @@ */ import { Modal } from '@/components/ui/modal/modal'; +import { useEmptyModelWarning } from '@/ee/hooks/use-empty-model-warning'; import DOMPurify from 'dompurify'; import { isEmpty } from 'lodash'; import { useEffect, useRef } from 'react'; @@ -32,6 +33,7 @@ export const useWarnEmptyModel = ( const { t } = useTranslation(); const warnedRef = useRef(false); const { navigateToModelSetting } = useNavigatePage(); + const { isChecking, showWarning } = useEmptyModelWarning(); useEffect(() => { if ( @@ -39,10 +41,15 @@ export const useWarnEmptyModel = ( !warnedRef.current && !isWarningVisible && !loading && + !isChecking && (isEmpty(embdId) || isEmpty(llmId)) && typeof embdId === 'string' && typeof llmId === 'string' ) { + if (showWarning()) { + return; + } + warnedRef.current = true; isWarningVisible = true; Modal.warning({ @@ -62,5 +69,14 @@ export const useWarnEmptyModel = ( }, }); } - }, [showEmptyModelWarn, embdId, llmId, loading, navigateToModelSetting, t]); + }, [ + showEmptyModelWarn, + embdId, + llmId, + loading, + navigateToModelSetting, + t, + isChecking, + showWarning, + ]); }; diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index fd74882a74..a859537c21 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -85,6 +85,31 @@ export declare interface IFlow { tags?: string; } +export interface ICanvasPresenceUser { + user_id: string; + display_name: string; + active_tab_count: number; + last_seen_at: number; + permission: number; +} + +export interface ICanvasPresenceResponse { + canvas_id: string; + online_user_count: number; + operator_permission?: number; + users: ICanvasPresenceUser[]; +} + +export interface ICanvasPresenceHeartbeatData { + ok: boolean; + code?: string; +} + +export interface IPresencePayload { + canvas_id: string; + tab_id: string; + presence_session_id: string; +} // GET /agents merges compilation template groups into the agent list when no // canvas_category is requested; every item carries this discriminator. export enum AgentListItemType { diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index 3ce9c65884..aa133b93c7 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -108,13 +108,16 @@ export interface IDialog { similarity_threshold: number; top_k: number; top_n: number; + meta_data_filter?: MetaDataFilter; + operator_permission: number; + nickname: string; + prefetch_size: number; rerank_candidates_count: number; rerank_id?: string; tenant_rerank_id?: string; - meta_data_filter: MetaDataFilter; } -interface MetaDataFilter { +export interface MetaDataFilter { manual: Manual[]; method: string; } @@ -158,11 +161,17 @@ export interface Message { export interface IReferenceChunk { citation_id?: string; id: string; + chunk_id?: string; content: null; + content_with_weight?: string; document_id: string; + doc_id?: string; document_name: string; + docnm_kwd?: string; dataset_id: string; + kb_id?: string; image_id: string; + img_id?: string; similarity: number; vector_similarity: number; term_similarity: number; diff --git a/web/src/interfaces/database/dataset.ts b/web/src/interfaces/database/dataset.ts index c699be51c5..e5a400f8ad 100644 --- a/web/src/interfaces/database/dataset.ts +++ b/web/src/interfaces/database/dataset.ts @@ -50,6 +50,7 @@ export interface IDataset { tenant_avatar: string; tenant_embd_id: number; tenant_id: string; + operator_permission: number; token_num: number; update_date: string; update_time: number; diff --git a/web/src/interfaces/database/file-manager.ts b/web/src/interfaces/database/file-manager.ts index 37f7d58560..ec70f9dba3 100644 --- a/web/src/interfaces/database/file-manager.ts +++ b/web/src/interfaces/database/file-manager.ts @@ -14,6 +14,7 @@ export interface IFile { update_time: number; source_type: string; has_child_folder?: boolean; + operator_permission?: number; } export interface IFolder { @@ -30,6 +31,7 @@ export interface IFolder { update_date: string; update_time: number; source_type: string; + operator_permission?: number; } export type IFetchFileListResult = { diff --git a/web/src/interfaces/database/llm.ts b/web/src/interfaces/database/llm.ts index 6ba8a36753..20e96cf043 100644 --- a/web/src/interfaces/database/llm.ts +++ b/web/src/interfaces/database/llm.ts @@ -62,6 +62,7 @@ export interface IProviderInstance { status: string; /** Whether the current user owns this model instance. */ is_owner?: boolean; + operator_permission?: number; /** Whether the server has a credential configured for this instance. */ api_key_configured?: boolean; /** Safe provider configuration values; secret values are never returned. */ diff --git a/web/src/interfaces/database/mcp.ts b/web/src/interfaces/database/mcp.ts index d8f0306e61..b065041e1c 100644 --- a/web/src/interfaces/database/mcp.ts +++ b/web/src/interfaces/database/mcp.ts @@ -5,9 +5,12 @@ export interface IMcpServer { id: string; name: string; server_type: string; + tenant_id: string; update_date: string; url: string; variables: Record & { tools?: IMCPToolObject }; + operator_permission: number; + nickname?: string; } export type IMCPToolObject = Record>; diff --git a/web/src/interfaces/database/memory.ts b/web/src/interfaces/database/memory.ts index 45ebe2cc78..f703bd4bd6 100644 --- a/web/src/interfaces/database/memory.ts +++ b/web/src/interfaces/database/memory.ts @@ -4,7 +4,9 @@ export interface IMemory { id: string; memory_type: string[]; name: string; - owner_name: string; + nickname?: string; + owner_name?: string; + operator_permission?: number; permissions: string; storage_type: string; tenant_id: string; diff --git a/web/src/layouts/components/bell-button.tsx b/web/src/layouts/components/bell-button.tsx index 6a64aeeb38..09ae205cf7 100644 --- a/web/src/layouts/components/bell-button.tsx +++ b/web/src/layouts/components/bell-button.tsx @@ -1,19 +1,19 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; -import { Routes } from '@/routes'; import { BellRing } from 'lucide-react'; +import { TeamInvitationReminderDialog } from '@/ee/layouts/components/team-invitation-reminder-dialog'; export function BellButton({ className }: { className?: string }) { return ( - + + + ); } diff --git a/web/src/layouts/root-layout.tsx b/web/src/layouts/root-layout.tsx index c26280e711..f30a835455 100644 --- a/web/src/layouts/root-layout.tsx +++ b/web/src/layouts/root-layout.tsx @@ -1,10 +1,24 @@ +import { + RootLayoutBanner, + RootLayoutContent, + useRootLayoutChrome, +} from '@/ee/layouts/root-layout-shell'; import { Outlet } from 'react-router'; import { Header } from './components/header'; export function RootLayoutContainer({ children }: React.PropsWithChildren) { + const { containerRef, showHeader } = useRootLayoutChrome(); + return ( -
-
+
+
+ +
+ + {showHeader &&
}
{children}
@@ -14,7 +28,9 @@ export function RootLayoutContainer({ children }: React.PropsWithChildren) { export default function RootLayout() { return ( - + + + ); } diff --git a/web/src/main.tsx b/web/src/main.tsx index cad2f00370..c64e62e906 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -21,12 +21,15 @@ import '../tailwind.css'; import App from './app'; import './global.less'; import { initLanguage } from './locales/config'; +import { fetchBillingStatus } from '@/ee/services/billingStatus'; -initLanguage().then(() => { - ReactDOM.createRoot(document.getElementById('root')!).render( - - - - , - ); -}); +initLanguage() + .then(() => fetchBillingStatus()) + .then(() => { + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + , + ); + }); diff --git a/web/src/pages/admin/user-detail.tsx b/web/src/pages/admin/user-detail.tsx index a79c55eff7..daedb7c867 100644 --- a/web/src/pages/admin/user-detail.tsx +++ b/web/src/pages/admin/user-detail.tsx @@ -16,6 +16,7 @@ import { Card, CardHeader } from '@/components/ui/card'; import { getUserDetails } from '@/services/admin-service'; import EnterpriseFeature from './components/enterprise-feature'; +import UserDetailAssets from '@/ee/pages/admin/user-detail-assets'; import { parseBooleanish } from './utils'; function AdminUserDetail() { @@ -126,6 +127,8 @@ function AdminUserDetail() {
+ + ); diff --git a/web/src/pages/admin/whitelist.tsx b/web/src/pages/admin/whitelist.tsx index 2c2fc93814..e96e93bd5e 100644 --- a/web/src/pages/admin/whitelist.tsx +++ b/web/src/pages/admin/whitelist.tsx @@ -68,6 +68,9 @@ import useImportExcelForm, { ImportExcelFormData, } from './forms/import-excel-form'; +import NotFound from '@/ee/pages/admin/404'; +import useAdminVariables from '@/ee/pages/admin/hooks/useAdminVariables'; + const columnHelper = createColumnHelper(); const globalFilterFn = createFuzzySearchFn([ 'email', @@ -541,4 +544,16 @@ function AdminWhitelist() { ); } -export default AdminWhitelist; +function AdminWhitelistGuard() { + const { variables } = useAdminVariables(); + + const isWhitelistEnabled = !!variables.enable_whitelist?.value; + + if (!isWhitelistEnabled) { + return ; + } + + return ; +} + +export default AdminWhitelistGuard; diff --git a/web/src/pages/agent/hooks/use-save-graph.ts b/web/src/pages/agent/hooks/use-save-graph.ts index 54e4b7df1f..31f0700201 100644 --- a/web/src/pages/agent/hooks/use-save-graph.ts +++ b/web/src/pages/agent/hooks/use-save-graph.ts @@ -7,11 +7,13 @@ import { useResetAgent, useSaveAgent, } from '@/hooks/use-agent-request'; +import { useFetchCanvasPresence } from '@/ee/hooks/use-agent-presence'; import { GlobalVariableType, RAGFlowNodeType, } from '@/interfaces/database/agent'; import { formatDate } from '@/utils/date'; +import { canEditAgent } from '@/ee/pages/agent/can-edit-agent'; import i18n from '@/locales/config'; import { useDebounceEffect } from 'ahooks'; import { useCallback, useEffect, useState } from 'react'; @@ -161,6 +163,7 @@ export const useWatchAgentChange = (chatDrawerVisible: boolean) => { const edges = useGraphStore((state) => state.edges); const { saveGraph } = useSaveGraph(false, true); const { data: flowDetail } = useFetchAgent(); + const { data: presence } = useFetchCanvasPresence(); const setSaveTime = useCallback((updateTime: number) => { setTime(formatDate(updateTime)); @@ -178,7 +181,8 @@ export const useWatchAgentChange = (chatDrawerVisible: boolean) => { agentLoaded: Boolean(flowDetail?.id), nodeCount: nodes.length, edgeCount: edges.length, - }) + }) || + !canEditAgent(presence.operator_permission ?? 0) ) { return; } @@ -194,6 +198,7 @@ export const useWatchAgentChange = (chatDrawerVisible: boolean) => { nodes.length, saveGraph, setSaveTime, + presence.operator_permission, ]); useDebounceEffect( diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index 96e74a5f91..895735e768 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -26,6 +26,8 @@ import { SharedFrom } from '@/constants/chat'; import { useSetModalState } from '@/hooks/common-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useSaveAgent } from '@/hooks/use-agent-request'; +import { useFetchCanvasPresence } from '@/ee/hooks/use-agent-presence'; +import { AgentEditGuard } from '@/ee/pages/agent/agent-edit-guard'; import { ReactFlowProvider } from '@xyflow/react'; import { ChevronDown, @@ -65,6 +67,7 @@ import { useSaveGraphBeforeOpeningDebugDrawer, useWatchAgentChange, } from './hooks/use-save-graph'; +import { OnlineUsers } from '@/ee/pages/agent/online-users'; import { PipelineLogSheet } from './pipeline-log-sheet'; import PipelineRunSheet from './pipeline-run-sheet'; import { SettingDialog } from './setting-dialog'; @@ -272,6 +275,8 @@ export default function Agent() { uploadedFileData, } = useRunDataflow({ showLogSheet: showPipelineLogSheet, setMessageId }); + const { data: presence } = useFetchCanvasPresence(); + const operatorPermission = presence.operator_permission ?? 0; const initialWidgetSettings = useMemo(() => { const widgetSettings = agentDetail?.dsl?.globals?.[AgentWidgetSettingsGlobalKey]; @@ -325,30 +330,40 @@ export default function Agent() {
- - {t('flow.saveDraft')} - - {showRunButton && - (runTooltipKey ? ( - {runButton} - ) : ( - runButton - ))} - {isConversationMode && ( - - )} - + + + + {t('flow.saveDraft')} + + + {showRunButton && + (runTooltipKey ? ( + {runButton} + ) : ( + runButton + ))} + {isConversationMode && ( + + )} + {/* saveGraph(undefined, undefined, true)} + onBeforeOpen={hasBlockingIssues} + /> */} + + @@ -357,11 +372,13 @@ export default function Agent() { - showGlobalParamSheet()}> - - {t('flow.conversationVariable')} - - + + showGlobalParamSheet()}> + + {t('flow.conversationVariable')} + + + {t('flow.historyVersion')} @@ -373,10 +390,12 @@ export default function Agent() { {t('flow.export')} - - - {t('flow.setting')} - + + + + {t('flow.setting')} + + {isPipeline || (location.hostname !== 'cloud.ragflow.io' && ( <> diff --git a/web/src/pages/agents/agent-card.tsx b/web/src/pages/agents/agent-card.tsx index a77b69d24d..86d1403c59 100644 --- a/web/src/pages/agents/agent-card.tsx +++ b/web/src/pages/agents/agent-card.tsx @@ -10,16 +10,18 @@ import { } from '@/components/ui/tooltip'; import { AgentCategory } from '@/constants/agent'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { useRef, useState } from 'react'; import { AgentListItemType, IFlow } from '@/interfaces/database/agent'; import { CanvasCategoryToFlowType, FlowType, FlowTypeConfig } from './constant'; import { AgentDropdown } from './agent-dropdown'; import { useRenameAgent } from './use-rename-agent'; -import { useRef, useState } from 'react'; import { Tag } from 'lucide-react'; export type DatasetCardProps = { - data: IFlow & { type?: AgentListItemType }; -} & Pick, 'showAgentRenameModal'>; + data: IFlow; +} & Pick, 'showAgentRenameModal'> & { + showPrivilegeModal(): void; + }; function AgentTypeIcon({ data, @@ -73,6 +75,7 @@ function AgentTags({ tags }: { tags?: string }) { @@ -99,7 +102,11 @@ function AgentTags({ tags }: { tags?: string }) { ); } -export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { +export function AgentCard({ + data, + showAgentRenameModal, + showPrivilegeModal, +}: DatasetCardProps) { const { navigateToAgent } = useNavigatePage(); return ( @@ -112,8 +119,12 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { release_time: data.release_time, }} moreDropdown={ - - + + } sharedBadge={{data.nickname}} diff --git a/web/src/pages/agents/agent-dropdown.tsx b/web/src/pages/agents/agent-dropdown.tsx index 4bb565dfca..9c392fb315 100644 --- a/web/src/pages/agents/agent-dropdown.tsx +++ b/web/src/pages/agents/agent-dropdown.tsx @@ -9,6 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { AgentDropdownGuard } from '@/ee/pages/agents/agent-dropdown-guard'; import { useDeleteAgent } from '@/hooks/use-agent-request'; import { IFlow } from '@/interfaces/database/agent'; import { PenLine, Tag, Trash2 } from 'lucide-react'; @@ -25,10 +26,12 @@ import { useRenameAgent } from './use-rename-agent'; export function AgentDropdown({ children, showAgentRenameModal, - agent: agent, + agent, + showPrivilegeModal, }: PropsWithChildren & Pick, 'showAgentRenameModal'> & { agent: IFlow; + showPrivilegeModal(): void; }) { const { t } = useTranslation(); const { deleteAgent } = useDeleteAgent(); @@ -57,37 +60,49 @@ export function AgentDropdown({ {children} - - {t('common.rename')} - - - {t('flow.editTags')} - - - - ), - }} - > - { - e.preventDefault(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - > - {t('common.delete')} - - + + + {t('common.rename')} + + + {t('flow.editTags')} + + + } + deleteItems={ + <> + + + ), + }} + > + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + } + /> ), )} @@ -297,6 +306,12 @@ export default function Agents() { onOk={onFileUploadOk} > )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx index 48c9c00d82..c716f7f904 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx @@ -7,6 +7,7 @@ import { TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; +import { useDatasetEditButtonDisabled } from '@/ee/hooks/logic-hooks/use-permission'; import type { ChunkDocType, IChunk } from '@/interfaces/database/dataset'; import { cn } from '@/lib/utils'; import { sanitizeHtmlWithImagesAsText } from '@/utils/dom-util'; @@ -41,6 +42,8 @@ const ChunkCard = ({ t: imageCacheKey, }: IProps) => { const { t } = useTranslation(); + const datasetEditButtonDisabled = useDatasetEditButtonDisabled(); + const available = Number(item.available_int); const [enabled, setEnabled] = useState(false); @@ -145,6 +148,7 @@ const ChunkCard = ({ onCheckedChange={onChange} aria-readonly className="!m-0" + disabled={datasetEditButtonDisabled} />
diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/checkbox-sets.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/checkbox-sets.tsx index e31d224731..0a24440689 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/checkbox-sets.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/checkbox-sets.tsx @@ -1,6 +1,7 @@ import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; +import { useDatasetEditButtonDisabled } from '@/ee/hooks/logic-hooks/use-permission'; import { cn } from '@/lib/utils'; import { LucideToggleLeft, @@ -18,7 +19,9 @@ type ICheckboxSetProps = { checked: boolean; selectedChunkIds: string[]; }; + export default function CheckboxSets(props: ICheckboxSetProps) { + const datasetEditButtonDisabled = useDatasetEditButtonDisabled(); const { className, selectAllChunk, @@ -51,7 +54,9 @@ export default function CheckboxSets(props: ICheckboxSetProps) { const isSelected = useMemo(() => { return selectedChunkIds?.length > 0; }, [selectedChunkIds]); - + if (datasetEditButtonDisabled) { + return null; + } return (
diff --git a/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts b/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts index 50ce8d64af..98369c50de 100644 --- a/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts +++ b/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts @@ -1,4 +1,5 @@ import message from '@/components/ui/message'; +import { useSaveBuiltinMetadata } from '@/ee/pages/dataset/components/metedata/hooks/use-save-builtin-metadata'; import { useSetModalState } from '@/hooks/common-hooks'; import { useSelectedIds } from '@/hooks/logic-hooks/use-row-selection'; import { DocumentApiAction } from '@/hooks/use-document-request'; @@ -294,6 +295,7 @@ export const useManageMetaDataModal = ( const { id } = useParams(); const { t } = useTranslation(); const { data, loading } = useFetchMetaDataManageData(type, documentIds); + const { saveBuiltinMetadata } = useSaveBuiltinMetadata(); const [tableData, setTableData] = useState(metaData); const queryClient = useQueryClient(); @@ -469,6 +471,12 @@ export const useManageMetaDataModal = ( case MetadataType.UpdateSingle: // handleSaveUpdateSingle(callback); handleSaveManage(callback); + await saveBuiltinMetadata({ + kbId: id, + documentId: documentIds?.[0], + builtInMetadata, + metadata: util.tableDataToMetaDataSettingJSON(tableData), + }); break; case MetadataType.Manage: handleSaveManage(callback); @@ -484,8 +492,12 @@ export const useManageMetaDataModal = ( } }, [ + id, + documentIds, + tableData, handleSaveManage, type, + saveBuiltinMetadata, // handleSaveUpdateSingle, handleSaveSettings, handleSaveSingleFileSettings, diff --git a/web/src/pages/dataset/dataset/dataset-action-cell.tsx b/web/src/pages/dataset/dataset/dataset-action-cell.tsx index bb4aa5601b..aa800bbd30 100644 --- a/web/src/pages/dataset/dataset/dataset-action-cell.tsx +++ b/web/src/pages/dataset/dataset/dataset-action-cell.tsx @@ -6,9 +6,14 @@ import { HoverCardTrigger, } from '@/components/ui/hover-card'; import { DocumentType } from '@/constants/knowledge'; +import { + DocumentPrivilegeAction, + useDocumentActionCellOverrides, +} from '@/ee/pages/dataset/dataset/dataset-action-cell-seams'; import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; import { useRemoveDocument } from '@/hooks/use-document-request'; import { IDocumentInfo } from '@/interfaces/database/document'; +import { cn } from '@/lib/utils'; import { downloadDatasetDocument } from '@/services/file-manager-service'; import { formatFileSize } from '@/utils/common-util'; import { formatDate } from '@/utils/date'; @@ -27,15 +32,23 @@ const FunctionMap = { update_time: formatDate, }; +type DatasetActionCellProps = { + record: IDocumentInfo; + datasetEditButtonDisabled?: boolean; +} & UseRenameDocumentShowType & + Pick; + export function DatasetActionCell({ record, showRenameModal, setRowSelection, -}: { record: IDocumentInfo } & UseRenameDocumentShowType & - Pick) { + datasetEditButtonDisabled, +}: DatasetActionCellProps) { const { id, type } = record; const isRunning = isDocumentProcessing(record); const isVirtualDocument = type === DocumentType.Virtual; + const actionDisabled = isRunning || Boolean(datasetEditButtonDisabled); + const { actionClassName } = useDocumentActionCellOverrides(); const { removeDocument } = useRemoveDocument(); @@ -69,14 +82,16 @@ export function DatasetActionCell({ return (
+ diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx index f2a56aea25..292e1a62d3 100644 --- a/web/src/pages/dataset/dataset/index.tsx +++ b/web/src/pages/dataset/dataset/index.tsx @@ -4,6 +4,7 @@ import { } from '@/components/bulk-operate-bar'; import { FileUploadDialog } from '@/components/file-upload-dialog'; import ListFilterBar from '@/components/list-filter-bar'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; @@ -11,9 +12,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { useDatasetEditButtonDisabled } from '@/ee/hooks/logic-hooks/use-permission'; import { useGoToPreviousPageOnEmpty } from '@/hooks/logic-hooks'; import { useClearSelectionOnPageChange } from '@/hooks/logic-hooks/use-clear-selection-on-page-change'; import { @@ -33,9 +34,15 @@ import { DatasetTable } from './dataset-table'; import { useBulkOperateDataset } from './use-bulk-operate-dataset'; import { useCreateEmptyDocument } from './use-create-empty-document'; import { useSelectDatasetFilters } from './use-select-filters'; +import { + ShowPrivilegeModalContext, + useShowPrivilegeDialog, +} from '@/ee/pages/dataset/dataset/use-show-privilege-dialog'; import { useHandleUploadDocument } from './use-upload-document'; export default function Dataset() { + const datasetEditButtonDisabled = useDatasetEditButtonDisabled(); + const { t } = useTranslation(); const { knowledgeBase } = useKnowledgeBaseContext(); const { @@ -91,10 +98,18 @@ export default function Dataset() { useClearSelectionOnPageChange(pagination, clearRowSelection); + const { + handShowPrivilegeModal, + hidePrivilegeModal, + privilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); + const { list } = useBulkOperateDataset({ documents, rowSelection, setRowSelection, + handShowPrivilegeModal, }); const { selectedIds: selectedRowKeys } = useSelectedIds( @@ -167,7 +182,7 @@ export default function Dataset() { - @@ -176,7 +191,6 @@ export default function Dataset() { {t('fileManager.uploadFile')} - {t('knowledgeDetails.emptyFiles')} @@ -195,16 +209,18 @@ export default function Dataset() { - + + + {documentUploadVisible && ( )} + {/* */} + {privilegeModal && ( + + )} + {/* */} ); } diff --git a/web/src/pages/dataset/dataset/parsing-status-cell.tsx b/web/src/pages/dataset/dataset/parsing-status-cell.tsx index ea47b338a6..afa180110e 100644 --- a/web/src/pages/dataset/dataset/parsing-status-cell.tsx +++ b/web/src/pages/dataset/dataset/parsing-status-cell.tsx @@ -140,10 +140,12 @@ export function ParseDropdownButton({ export function ParsingStatusCell({ record, showLog, + datasetEditButtonDisabled = false, showChangeParserModal, }: { record: IDocumentInfo; showLog: (record: IDocumentInfo) => void; + datasetEditButtonDisabled?: boolean; } & UseChangeDocumentParserShowType) { const { t } = useTranslation(); const { progress, chunk_count, id } = record; @@ -168,6 +170,9 @@ export function ParsingStatusCell({ delete: boolean; apply_kb: boolean; }) => { + if (datasetEditButtonDisabled) { + return; + } handleRunDocumentByIds(record, isRunning, option); }; diff --git a/web/src/pages/dataset/dataset/use-bulk-operate-dataset.tsx b/web/src/pages/dataset/dataset/use-bulk-operate-dataset.tsx index 6ef556bcfa..3002e218dd 100644 --- a/web/src/pages/dataset/dataset/use-bulk-operate-dataset.tsx +++ b/web/src/pages/dataset/dataset/use-bulk-operate-dataset.tsx @@ -1,5 +1,6 @@ import { Modal } from '@/components/ui/modal/modal'; import { FileType } from '@/constants/file'; +import { useExtraBulkActions } from '@/ee/pages/dataset/dataset/use-extra-bulk-actions'; import { UseRowSelectionType, useSelectedIds, @@ -23,6 +24,7 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { toast } from 'sonner'; +import { ShowPrivilegeModalReturnType } from '@/ee/pages/dataset/dataset/use-show-privilege-dialog'; import { DocumentType, ParserGapReason } from './constant'; import { buildParserGapModalContent } from './parser-gap-content'; import { useParserGapValidation } from './use-parser-gap-validation'; @@ -32,9 +34,10 @@ export function useBulkOperateDataset({ rowSelection, setRowSelection, documents, + handShowPrivilegeModal, }: Pick & { documents: IDocumentInfo[]; -}) { +} & Pick) { const { t } = useTranslation(); const { selectedIds: selectedRowKeys } = useSelectedIds( rowSelection, @@ -46,6 +49,10 @@ export function useBulkOperateDataset({ const { setDocumentStatus } = useSetDocumentStatus(); const { removeDocument } = useRemoveDocument(); const { findDocumentParseGaps } = useParserGapValidation(); + const extraBulkActions = useExtraBulkActions( + handShowPrivilegeModal, + selectedRowKeys, + ); const runDocument = useCallback( async (run: number) => { @@ -209,6 +216,7 @@ export function useBulkOperateDataset({ icon: , onClick: handleCancelClick, }, + ...extraBulkActions, { id: 'enabled', label: t('knowledgeDetails.enabled'), diff --git a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx index 2d74ad19ad..1273514687 100644 --- a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx +++ b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx @@ -8,6 +8,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { useDatasetEditButtonDisabled } from '@/ee/hooks/logic-hooks/use-permission'; import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; import { useSetDocumentStatus } from '@/hooks/use-document-request'; import { IDocumentInfo } from '@/interfaces/database/document'; @@ -39,6 +40,7 @@ export function useDatasetTableColumns({ showLog, setRowSelection, }: UseDatasetTableColumnsType) { + const datasetEditButtonDisabled = useDatasetEditButtonDisabled(); const { t } = useTranslation('translation', { keyPrefix: 'knowledgeDetails', }); @@ -61,6 +63,7 @@ export function useDatasetTableColumns({ table.toggleAllPageRowsSelected(!!value) } aria-label="Select all" + disabled={datasetEditButtonDisabled} /> ), cell: ({ row }) => ( @@ -68,6 +71,7 @@ export function useDatasetTableColumns({ checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" + disabled={datasetEditButtonDisabled} /> ), enableSorting: false, @@ -239,6 +243,7 @@ export function useDatasetTableColumns({ record={row.original} showChangeParserModal={showChangeParserModal} showLog={showLog} + datasetEditButtonDisabled={datasetEditButtonDisabled} /> ); }, @@ -255,6 +260,7 @@ export function useDatasetTableColumns({ record={record} showRenameModal={showRenameModal} setRowSelection={setRowSelection} + datasetEditButtonDisabled={datasetEditButtonDisabled} /> ); }, @@ -270,6 +276,7 @@ export function useDatasetTableColumns({ showManageMetadataModal, showLog, setRowSelection, + datasetEditButtonDisabled, ], ); diff --git a/web/src/pages/datasets/dataset-card.tsx b/web/src/pages/datasets/dataset-card.tsx index ca23988286..19daf51710 100644 --- a/web/src/pages/datasets/dataset-card.tsx +++ b/web/src/pages/datasets/dataset-card.tsx @@ -4,19 +4,29 @@ import { SharedBadge } from '@/components/shared-badge'; import { Card, CardContent } from '@/components/ui/card'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { IDataset } from '@/interfaces/database/dataset'; -import { t } from 'i18next'; import { ChevronRight } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + import { DatasetDropdown } from './dataset-dropdown'; +import useDuplicateDataset from '@/ee/pages/datasets/use-duplicate-dataset'; import { useRenameDataset } from './use-rename-dataset'; export type DatasetCardProps = { dataset: IDataset; -} & Pick, 'showDatasetRenameModal'>; + showPrivilegeModal(): void; +} & Pick, 'showDatasetRenameModal'> & { + showDatasetDuplicateModal: ReturnType< + typeof useDuplicateDataset + >['showModal']; + }; export function DatasetCard({ dataset, showDatasetRenameModal, + showDatasetDuplicateModal, + showPrivilegeModal, }: DatasetCardProps) { + const { t } = useTranslation(); const { navigateToDataset } = useNavigatePage(); return ( @@ -28,6 +38,8 @@ export function DatasetCard({ moreDropdown={ @@ -35,12 +47,13 @@ export function DatasetCard({ } sharedBadge={{dataset.nickname}} onClick={navigateToDataset(dataset.id)} - /> + > ); } export function SeeAllCard() { const { navigateToDatasetList } = useNavigatePage(); + const { t } = useTranslation(); return ( , 'showDatasetRenameModal'> & { + showDatasetDuplicateModal: ReturnType< + typeof useDuplicateDataset + >['showModal']; + }; + export function DatasetDropdown({ children, showDatasetRenameModal, dataset, -}: PropsWithChildren & + showPrivilegeModal, +}: PropsWithChildren & Pick, 'showDatasetRenameModal'> & { dataset: IDataset; }) { @@ -44,34 +56,44 @@ export function DatasetDropdown({ {children} - - {t('common.rename')} - - - - ), - }} - > - { - e.preventDefault(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - > - {t('common.delete')} - - + + {t('common.rename')} + + } + deleteItems={ + <> + + + ), + }} + title={t('deleteModal.delDataset')} + > + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + } + /> ); diff --git a/web/src/pages/datasets/index.tsx b/web/src/pages/datasets/index.tsx index c8f27fb3b5..7a95fcb47c 100644 --- a/web/src/pages/datasets/index.tsx +++ b/web/src/pages/datasets/index.tsx @@ -1,7 +1,9 @@ import { CardContainer } from '@/components/card-container'; +import DuplicateDialog from '@/ee/components/duplicate-dialog'; import { EmptyCardType } from '@/components/empty/constant'; import { EmptyAppCard } from '@/components/empty/empty'; import ListFilterBar from '@/components/list-filter-bar'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { Button } from '@/components/ui/button'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; @@ -18,8 +20,10 @@ import { useSearchParams } from 'react-router'; import { DatasetCard } from './dataset-card'; import { DatasetCreatingDialog } from './dataset-creating-dialog'; import { useSaveKnowledge } from './hooks'; +import useDuplicateDataset from '@/ee/pages/datasets/use-duplicate-dataset'; import { useRenameDataset } from './use-rename-dataset'; import { useSelectOwners } from './use-select-owners'; +import { useShowPrivilegeDialog } from '@/ee/pages/datasets/use-show-privilege-dialog'; export default function Datasets() { const { t } = useTranslation(); @@ -56,6 +60,15 @@ export default function Datasets() { showDatasetRenameModal, } = useRenameDataset(); + const { + isModalVisible: datasetDuplicateModalVisible, + showModal: showDatasetDuplicateModal, + hideModal: hideDatasetDuplicateModal, + initialName: initialDatasetDuplicateName, + onOk: onDatasetDuplicateOk, + loading: datasetDuplicateLoading, + } = useDuplicateDataset(); + const handlePageChange = useCallback( (page: number, pageSize?: number) => { setPagination({ page, pageSize }); @@ -81,6 +94,13 @@ export default function Datasets() { } }, [isCreate, showModal, searchUrl, setSearchUrl, queryClient]); + const { + privilegeModal, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); + return ( <> {loading && !kbs?.length ? ( @@ -120,6 +140,8 @@ export default function Datasets() { dataset={dataset} key={dataset.id} showDatasetRenameModal={showDatasetRenameModal} + showDatasetDuplicateModal={showDatasetDuplicateModal} + showPrivilegeModal={handShowPrivilegeModal(dataset)} /> ))} @@ -166,6 +188,14 @@ export default function Datasets() { loading={creatingLoading} > )} + {datasetDuplicateModalVisible && ( + + )} {datasetRenameVisible && ( )} + {privilegeModal && ( + + )} ); } diff --git a/web/src/pages/files/action-cell.tsx b/web/src/pages/files/action-cell.tsx index b28d8e5a2d..70ca431342 100644 --- a/web/src/pages/files/action-cell.tsx +++ b/web/src/pages/files/action-cell.tsx @@ -5,6 +5,7 @@ import { import { FileIcon } from '@/components/icon-font'; import NewDocumentLink from '@/components/new-document-link'; import { Button } from '@/components/ui/button'; +import { FilePrivilegeButton } from '@/ee/pages/files/file-privilege-button'; import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; import { useDownloadFile } from '@/hooks/use-file-request'; import { IFile } from '@/interfaces/database/file-manager'; @@ -36,7 +37,9 @@ type IProps = Pick, 'row'> & Pick & Pick & Pick & - UseMoveDocumentShowType; + UseMoveDocumentShowType & { + showPrivilegeModal?(record: IFile): void; + }; export function ActionCell({ row, @@ -44,6 +47,7 @@ export function ActionCell({ showFileRenameModal, showMoveFileModal, setRowSelection, + showPrivilegeModal, }: IProps) { const record = row.original; const documentId = record.id; @@ -106,6 +110,10 @@ export function ActionCell({ )} + {isKnowledgeBase || (
)} + {privilegeModalVisible && ( + + )} {/* */} ); diff --git a/web/src/pages/home/chat-list.tsx b/web/src/pages/home/chat-list.tsx index 486b1f62c2..1e9fa4e0ad 100644 --- a/web/src/pages/home/chat-list.tsx +++ b/web/src/pages/home/chat-list.tsx @@ -1,5 +1,6 @@ import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchChatList } from '@/hooks/use-chat-request'; @@ -7,6 +8,7 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ChatDropdown } from '../next-chats/chat-dropdown'; import { useRenameChat } from '../next-chats/hooks/use-rename-chat'; +import { useShowPrivilegeDialog } from '@/ee/pages/next-chats/use-show-privilege-dialog'; export function ChatList({ setListLength, @@ -31,23 +33,37 @@ export function ChatList({ setListLength(data?.chats?.length || 0); setLoading?.(loading || false); }, [data, setListLength, loading, setLoading]); + + const { + handleShowPrivilegeModal, + privilegeRecord, + hidePrivilegeModal, + privilegeModalVisible, + } = useShowPrivilegeDialog(); + return ( <> - {(data?.chats ?? []).slice(0, 10).map((x) => ( - - - - } - > - ))} + {data?.chats && + data.chats.length > 0 && + data.chats.slice(0, 10).map((x) => ( + + + + } + > + ))} {chatRenameVisible && ( )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/home/datasets.tsx b/web/src/pages/home/datasets.tsx index e5e27449a6..766cc41797 100644 --- a/web/src/pages/home/datasets.tsx +++ b/web/src/pages/home/datasets.tsx @@ -1,6 +1,8 @@ import { CardSineLineContainer } from '@/components/card-singleline-container'; +import DuplicateDialog from '@/ee/components/duplicate-dialog'; import { EmptyCardType } from '@/components/empty/constant'; import { EmptyAppCard } from '@/components/empty/empty'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { HomeIcon } from '@/components/svg-icon'; import { CardSkeleton } from '@/components/ui/skeleton'; @@ -8,7 +10,9 @@ import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; import { useTranslation } from 'react-i18next'; import { DatasetCard } from '../datasets/dataset-card'; +import useDuplicateDataset from '@/ee/pages/datasets/use-duplicate-dataset'; import { useRenameDataset } from '../datasets/use-rename-dataset'; +import { useShowPrivilegeDialog } from '@/ee/pages/datasets/use-show-privilege-dialog'; import { SeeAllAppCard } from './application-card'; export function Datasets() { @@ -22,8 +26,25 @@ export function Datasets() { hideDatasetRenameModal, showDatasetRenameModal, } = useRenameDataset(); + + const { + isModalVisible: datasetDuplicateModalVisible, + showModal: showDatasetDuplicateModal, + hideModal: hideDatasetDuplicateModal, + initialName: initialDatasetDuplicateName, + onOk: onDatasetDuplicateOk, + loading: datasetDuplicateLoading, + } = useDuplicateDataset(); + const { navigateToDatasetList } = useNavigatePage(); + const { + privilegeModal, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); + return (
@@ -41,13 +62,15 @@ export function Datasets() { ) : ( <> - {kbs?.length > 0 && ( + {(kbs?.length ?? 0) > 0 && ( {kbs?.slice(0, 6).map((dataset) => ( ))} { @@ -78,6 +101,20 @@ export function Datasets() { loading={datasetRenameLoading} /> )} + {datasetDuplicateModalVisible && ( + + )} + {privilegeModal && ( + + )}
); } diff --git a/web/src/pages/home/memory-list.tsx b/web/src/pages/home/memory-list.tsx index a5b52adfbf..fde4134dda 100644 --- a/web/src/pages/home/memory-list.tsx +++ b/web/src/pages/home/memory-list.tsx @@ -1,11 +1,13 @@ import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useEffect } from 'react'; import { AddOrEditModal } from '../memories/add-or-edit-modal'; import { useFetchMemoryList, useRenameMemory } from '../memories/hooks'; import { ICreateMemoryProps } from '../memories/interface'; import { MemoryDropdown } from '../memories/memory-dropdown'; +import { useShowPrivilegeDialog } from '@/ee/pages/memories/use-show-privilege-dialog'; export function MemoryList({ setListLength, @@ -32,6 +34,12 @@ export function MemoryList({ onMemoryRenameOk, initialMemory, } = useRenameMemory(); + const { + privilegeModalVisible, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); const onMemoryConfirm = (data: ICreateMemoryProps) => { onMemoryRenameOk(data, () => { refetchList(); @@ -58,6 +66,7 @@ export function MemoryList({ @@ -74,6 +83,12 @@ export function MemoryList({ onSubmit={onMemoryConfirm} /> )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/home/search-list.tsx b/web/src/pages/home/search-list.tsx index 9474a54a03..2abe34e3c1 100644 --- a/web/src/pages/home/search-list.tsx +++ b/web/src/pages/home/search-list.tsx @@ -1,11 +1,13 @@ import { HomeCard } from '@/components/home-card'; import { IconFont } from '@/components/icon-font'; import { MoreButton } from '@/components/more-button'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useEffect } from 'react'; import { useFetchSearchList, useRenameSearch } from '../next-searches/hooks'; import { SearchDropdown } from '../next-searches/search-dropdown'; +import { useShowPrivilegeDialog } from '@/ee/pages/next-searches/use-show-privilege-dialog'; export function SearchList({ setListLength, @@ -24,6 +26,12 @@ export function SearchList({ onSearchRenameOk, initialSearchName, } = useRenameSearch(); + const { + privilegeModalVisible, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); const onSearchRenameConfirm = (name: string) => { onSearchRenameOk(name, () => { refetchList(); @@ -45,6 +53,7 @@ export function SearchList({ @@ -60,6 +69,12 @@ export function SearchList({ title={} > )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/memories/index.tsx b/web/src/pages/memories/index.tsx index 4b59a5496a..91d68c44dd 100644 --- a/web/src/pages/memories/index.tsx +++ b/web/src/pages/memories/index.tsx @@ -2,6 +2,7 @@ import { CardContainer } from '@/components/card-container'; import { EmptyCardType } from '@/components/empty/constant'; import { EmptyAppCard } from '@/components/empty/empty'; import ListFilterBar from '@/components/list-filter-bar'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { Button } from '@/components/ui/button'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { ListDeletionKey } from '@/constants/list-deletion'; @@ -21,6 +22,7 @@ import { } from './hooks'; import { ICreateMemoryProps, IMemory } from './interface'; import { MemoryCard } from './memory-card'; +import { useShowPrivilegeDialog } from '@/ee/pages/memories/use-show-privilege-dialog'; export default function MemoryList() { // const { data } = useFetchFlowList(); @@ -50,6 +52,12 @@ export default function MemoryList() { onMemoryRenameOk, initialMemory, } = useRenameMemory(); + const { + privilegeModalVisible, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); const onMemoryConfirm = (data: ICreateMemoryProps) => { onMemoryRenameOk(data, () => { @@ -121,6 +129,7 @@ export default function MemoryList() { setAddOrEditType('edit'); showMemoryRenameModal(x); }} + showPrivilegeModal={handShowPrivilegeModal(x)} /> ))} @@ -177,6 +186,12 @@ export default function MemoryList() { onSubmit={onMemoryConfirm} /> )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/memories/interface.ts b/web/src/pages/memories/interface.ts index 35ab17f7f2..723731c675 100644 --- a/web/src/pages/memories/interface.ts +++ b/web/src/pages/memories/interface.ts @@ -26,8 +26,10 @@ export interface ICreateMemoryProps { export interface IMemory extends ICreateMemoryProps { id: string; avatar: string; + operator_permission?: number; tenant_id: string; - owner_name: string; + nickname?: string; + owner_name?: string; storage_type: StorageType; permissions: Permissions; description: string; diff --git a/web/src/pages/memories/memory-card.tsx b/web/src/pages/memories/memory-card.tsx index 5d75f6e678..bc3628f70a 100644 --- a/web/src/pages/memories/memory-card.tsx +++ b/web/src/pages/memories/memory-card.tsx @@ -8,8 +8,13 @@ import { MemoryDropdown } from './memory-dropdown'; interface IProps { data: IMemory; showMemoryRenameModal: (data: IMemory) => void; + showPrivilegeModal: () => void; } -export function MemoryCard({ data, showMemoryRenameModal }: IProps) { +export function MemoryCard({ + data, + showMemoryRenameModal, + showPrivilegeModal, +}: IProps) { const { navigateToMemory } = useNavigatePage(); return ( @@ -24,6 +29,7 @@ export function MemoryCard({ data, showMemoryRenameModal }: IProps) { diff --git a/web/src/pages/memories/memory-dropdown.tsx b/web/src/pages/memories/memory-dropdown.tsx index bb1c732b9b..2a3078799e 100644 --- a/web/src/pages/memories/memory-dropdown.tsx +++ b/web/src/pages/memories/memory-dropdown.tsx @@ -9,6 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { MemoryDropdownGuard } from '@/ee/pages/memories/memory-dropdown-guard'; import { PenLine, Trash2 } from 'lucide-react'; import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,12 +20,15 @@ export function MemoryDropdown({ children, memory, showMemoryRenameModal, + showPrivilegeModal, }: PropsWithChildren & { memory: IMemory; showMemoryRenameModal: (memory: IMemory) => void; + showPrivilegeModal(): void; }) { const { t } = useTranslation(); const { deleteMemory } = useDeleteMemory(); + const handleShowChatRenameModal: MouseEventHandler = useCallback( (e) => { @@ -41,35 +45,45 @@ export function MemoryDropdown({ {children} - - {t('common.rename')} - - - - ), - }} - > - { - e.preventDefault(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - > - {t('common.delete')} - - + + {t('common.rename')} + + } + deleteItems={ + <> + + + ), + }} + > + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + } + /> ); diff --git a/web/src/pages/next-chats/chat-card.tsx b/web/src/pages/next-chats/chat-card.tsx index 0e6a9eece9..8dcdeb688c 100644 --- a/web/src/pages/next-chats/chat-card.tsx +++ b/web/src/pages/next-chats/chat-card.tsx @@ -1,5 +1,6 @@ import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; +import { SharedBadge } from '@/components/shared-badge'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { IDialog } from '@/interfaces/database/chat'; import { ChatDropdown } from './chat-dropdown'; @@ -7,9 +8,14 @@ import { useRenameChat } from './hooks/use-rename-chat'; export type IProps = { data: IDialog; + showPrivilegeModal(): void; } & Pick, 'showChatRenameModal'>; -export function ChatCard({ data, showChatRenameModal }: IProps) { +export function ChatCard({ + data, + showChatRenameModal, + showPrivilegeModal, +}: IProps) { const { navigateToChat } = useNavigatePage(); return ( @@ -21,10 +27,15 @@ export function ChatCard({ data, showChatRenameModal }: IProps) { update_time: data.update_time, }} moreDropdown={ - + } + sharedBadge={{data.nickname}} onClick={navigateToChat(data?.id)} /> ); diff --git a/web/src/pages/next-chats/chat-dropdown.tsx b/web/src/pages/next-chats/chat-dropdown.tsx index 90cfa2a581..1b7f010339 100644 --- a/web/src/pages/next-chats/chat-dropdown.tsx +++ b/web/src/pages/next-chats/chat-dropdown.tsx @@ -9,6 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ChatDropdownGuard } from '@/ee/pages/next-chats/chat-dropdown-guard'; import { useDeleteChat } from '@/hooks/use-chat-request'; import { IDialog } from '@/interfaces/database/chat'; import { PenLine, Trash2 } from 'lucide-react'; @@ -17,12 +18,14 @@ import { useTranslation } from 'react-i18next'; import { useRenameChat } from './hooks/use-rename-chat'; export function ChatDropdown({ + chat, children, showChatRenameModal, - chat, + showPrivilegeModal, }: PropsWithChildren & Pick, 'showChatRenameModal'> & { chat: IDialog; + showPrivilegeModal(): void; }) { const { t } = useTranslation(); const { deleteChat } = useDeleteChat(); @@ -44,34 +47,44 @@ export function ChatDropdown({ {children} - - {t('common.rename')} - - - - ), - }} - > - { - e.preventDefault(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - > - {t('common.delete')} - - + + {t('common.rename')} + + } + deleteItems={ + <> + + + ), + }} + > + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + } + /> ); diff --git a/web/src/pages/next-chats/chat/index.tsx b/web/src/pages/next-chats/chat/index.tsx index ca23daf2d6..eca7eed9de 100644 --- a/web/src/pages/next-chats/chat/index.tsx +++ b/web/src/pages/next-chats/chat/index.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ChatEvaluationButton } from '@/ee/pages/next-chats/chat/evaluation-button'; import { useFetchSessionList, useFetchSessionManually, @@ -119,15 +120,17 @@ export default function Chat() { >
{currentConversationName}
- - +
+ + +
diff --git a/web/src/pages/next-chats/index.tsx b/web/src/pages/next-chats/index.tsx index 76d43cc3e8..5da5d635ee 100644 --- a/web/src/pages/next-chats/index.tsx +++ b/web/src/pages/next-chats/index.tsx @@ -2,6 +2,7 @@ import { CardContainer } from '@/components/card-container'; import { EmptyCardType } from '@/components/empty/constant'; import { EmptyAppCard } from '@/components/empty/empty'; import ListFilterBar from '@/components/list-filter-bar'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { Button } from '@/components/ui/button'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; @@ -18,6 +19,7 @@ import { useSearchParams } from 'react-router'; import { ChatCard } from './chat-card'; import { useCreateChatDialog } from './hooks/use-create-chat'; import { useRenameChat } from './hooks/use-rename-chat'; +import { useShowPrivilegeDialog } from '@/ee/pages/next-chats/use-show-privilege-dialog'; export default function ChatList() { const { @@ -79,6 +81,13 @@ export default function ChatList() { } }, [isCreate, handleShowCreateModal, searchParams, setSearchParams]); + const { + handleShowPrivilegeModal, + privilegeRecord, + hidePrivilegeModal, + privilegeModalVisible, + } = useShowPrivilegeDialog(); + const renameDialogProps = useMemo(() => { if (chatRenameVisible) { return { @@ -151,6 +160,7 @@ export default function ChatList() { key={x.id} data={x} showChatRenameModal={showChatRenameModal} + showPrivilegeModal={handleShowPrivilegeModal(x)} /> ))} @@ -195,6 +205,12 @@ export default function ChatList() { {renameDialogProps && ( )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/next-searches/hooks.ts b/web/src/pages/next-searches/hooks.ts index e6b6bfaafe..72b9251789 100644 --- a/web/src/pages/next-searches/hooks.ts +++ b/web/src/pages/next-searches/hooks.ts @@ -91,6 +91,7 @@ export interface ISearchAppProps { id: string; name: string; nickname: string; + operator_permission?: number; status: string; tenant_avatar: any; tenant_id: string; diff --git a/web/src/pages/next-searches/index.tsx b/web/src/pages/next-searches/index.tsx index 63943d2799..a187483c9f 100644 --- a/web/src/pages/next-searches/index.tsx +++ b/web/src/pages/next-searches/index.tsx @@ -18,6 +18,7 @@ import { CardContainer } from '@/components/card-container'; import { EmptyCardType } from '@/components/empty/constant'; import { EmptyAppCard } from '@/components/empty/empty'; import ListFilterBar from '@/components/list-filter-bar'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; import { RenameDialog } from '@/components/rename-dialog'; import { Button } from '@/components/ui/button'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; @@ -31,6 +32,7 @@ import { useCallback, useEffect } from 'react'; import { useSearchParams } from 'react-router'; import { useFetchSearchList, useRenameSearch } from './hooks'; import { SearchCard } from './search-card'; +import { useShowPrivilegeDialog } from '@/ee/pages/next-searches/use-show-privilege-dialog'; export default function SearchList() { // const { data } = useFetchFlowList(); @@ -61,6 +63,12 @@ export default function SearchList() { onSearchRenameOk, initialSearchName, } = useRenameSearch(); + const { + privilegeModalVisible, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); // const handleSearchChange = (value: string) => { // console.log(value); @@ -136,6 +144,7 @@ export default function SearchList() { showSearchRenameModal={() => { showSearchRenameModal(x); }} + showPrivilegeModal={handShowPrivilegeModal(x)} /> ); })} @@ -186,6 +195,12 @@ export default function SearchList() { title={initialSearchName || t('createSearch')} > )} + {privilegeModalVisible && ( + + )} ); } diff --git a/web/src/pages/next-searches/search-card.tsx b/web/src/pages/next-searches/search-card.tsx index a22e889ab5..1167d0ad80 100644 --- a/web/src/pages/next-searches/search-card.tsx +++ b/web/src/pages/next-searches/search-card.tsx @@ -16,6 +16,7 @@ import { HomeCard } from '@/components/home-card'; import { MoreButton } from '@/components/more-button'; +import { SharedBadge } from '@/components/shared-badge'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { ISearchAppProps } from './hooks'; import { SearchDropdown } from './search-dropdown'; @@ -23,8 +24,13 @@ import { SearchDropdown } from './search-dropdown'; interface IProps { data: ISearchAppProps; showSearchRenameModal: (data: ISearchAppProps) => void; + showPrivilegeModal(): void; } -export function SearchCard({ data, showSearchRenameModal }: IProps) { +export function SearchCard({ + data, + showSearchRenameModal, + showPrivilegeModal, +}: IProps) { const { navigateToSearch } = useNavigatePage(); return ( @@ -34,10 +40,12 @@ export function SearchCard({ data, showSearchRenameModal }: IProps) { } + sharedBadge={{data.nickname}} onClick={navigateToSearch(data?.id)} /> ); diff --git a/web/src/pages/next-searches/search-dropdown.tsx b/web/src/pages/next-searches/search-dropdown.tsx index 2cc91c92b8..1476432d43 100644 --- a/web/src/pages/next-searches/search-dropdown.tsx +++ b/web/src/pages/next-searches/search-dropdown.tsx @@ -25,6 +25,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { SearchDropdownGuard } from '@/ee/pages/next-searches/search-dropdown-guard'; import { PenLine, Trash2 } from 'lucide-react'; import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,9 +35,11 @@ export function SearchDropdown({ children, dataset, showSearchRenameModal, + showPrivilegeModal, }: PropsWithChildren & { dataset: ISearchAppProps; showSearchRenameModal: (dataset: ISearchAppProps) => void; + showPrivilegeModal(): void; }) { const { t } = useTranslation(); const { deleteSearch } = useDeleteSearch(); @@ -56,34 +59,44 @@ export function SearchDropdown({ {children} - - {t('common.rename')} - - - - ), - }} - > - { - e.preventDefault(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - > - {t('common.delete')} - - + + {t('common.rename')} + + } + deleteItems={ + <> + + + ), + }} + > + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + } + /> ); diff --git a/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx b/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx index bd9eb6aa3e..bd904cc6c1 100644 --- a/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx +++ b/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx @@ -16,6 +16,7 @@ import { Collapse } from '@/components/collapse'; import { Button, ButtonLoading } from '@/components/ui/button'; +import { useMcpReadOnly } from '@/ee/pages/user-setting/mcp/use-mcp-read-only'; import { Card, CardContent } from '@/components/ui/card'; import { DialogFooter } from '@/components/ui/dialog'; import { Modal } from '@/components/ui/modal/modal'; @@ -64,7 +65,8 @@ export function EditMcpDialog({ loading, onOk, id, -}: IModalProps & { id: string }) { + operatorPermission, +}: IModalProps & { id: string; operatorPermission?: number }) { const { t } = useTranslation(); const { testMcpServer, @@ -127,10 +129,19 @@ export function EditMcpDialog({ }, [data, form]); const disabled = !tools?.length || testLoading || fieldChanged; + const isReadOnly = useMcpReadOnly( + operatorPermission ?? data.operator_permission, + ); return ( @@ -169,19 +181,21 @@ export function EditMcpDialog({ open={collapseOpen} onOpenChange={setCollapseOpen} rightContent={ - + isReadOnly ? undefined : ( + + ) } >
diff --git a/web/src/pages/user-setting/mcp/edit-mcp-form.tsx b/web/src/pages/user-setting/mcp/edit-mcp-form.tsx index 299f078868..2c7a822b32 100644 --- a/web/src/pages/user-setting/mcp/edit-mcp-form.tsx +++ b/web/src/pages/user-setting/mcp/edit-mcp-form.tsx @@ -82,11 +82,14 @@ export function EditMcpForm({ form, onOk, setFieldChanged, + readOnly = false, }: IModalProps & { form: UseFormReturn; setFieldChanged: Dispatch>; + readOnly?: boolean; }) { const { t } = useTranslation(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const FormSchema = useBuildFormSchema(); function onSubmit(data: z.infer) { @@ -111,6 +114,7 @@ export function EditMcpForm({ placeholder={t('common.mcp.namePlaceholder')} {...field} autoComplete="off" + disabled={readOnly} /> @@ -128,6 +132,7 @@ export function EditMcpForm({ placeholder={t('common.mcp.urlPlaceholder')} {...field} autoComplete="off" + disabled={readOnly} onChange={(e) => { field.onChange(e.target.value.trim()); setFieldChanged(true); @@ -148,6 +153,7 @@ export function EditMcpForm({ { field.onChange(value); setFieldChanged(true); @@ -170,6 +176,7 @@ export function EditMcpForm({ {...field} autoComplete="off" type="password" + disabled={readOnly} onChange={(e) => { field.onChange(e.target.value.trim()); setFieldChanged(true); diff --git a/web/src/pages/user-setting/mcp/index.tsx b/web/src/pages/user-setting/mcp/index.tsx index 728417adeb..2411b24125 100644 --- a/web/src/pages/user-setting/mcp/index.tsx +++ b/web/src/pages/user-setting/mcp/index.tsx @@ -19,6 +19,12 @@ import { ConfirmDeleteDialog, ConfirmDeleteDialogNode, } from '@/components/confirm-delete-dialog'; +import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog'; +import { + McpEmptyStateGuard, + McpSelectionBarGuard, + McpToolbarGuard, +} from '@/ee/pages/user-setting/mcp/mcp-list-guard'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { SearchInput } from '@/components/ui/input'; @@ -27,6 +33,7 @@ import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { ListDeletionKey } from '@/constants/list-deletion'; import { useGoToPreviousPageOnEmpty } from '@/hooks/logic-hooks'; import { useClearSelectionOnPageChange } from '@/hooks/logic-hooks/use-clear-selection-on-page-change'; +import { useExitMcpSelectionWithoutOwner } from '@/ee/pages/user-setting/mcp/use-exit-mcp-selection-without-owner'; import { useListMcpServer } from '@/hooks/use-mcp-request'; import { pick } from 'lodash'; import { @@ -46,8 +53,18 @@ import { McpCard } from './mcp-card'; import { useBulkOperateMCP } from './use-bulk-operate-mcp'; import { useEditMcp } from './use-edit-mcp'; import { useImportMcp } from './use-import-mcp'; +import { useShowPrivilegeDialog } from '@/ee/pages/user-setting/mcp/use-show-privilege-dialog'; export default function McpServer() { + const { + editVisible, + showEditModal, + hideEditModal, + handleOk, + id, + loading, + operatorPermission, + } = useEditMcp(); const { data, setPagination, @@ -62,18 +79,23 @@ export default function McpServer() { searchString, setSearchString, }); - const { editVisible, showEditModal, hideEditModal, handleOk, id, loading } = - useEditMcp(); const { selectedList, handleSelectChange, handleDelete, handleExportMcp, handleSelectAll, + selectAllChecked, resetSelection, } = useBulkOperateMCP(data.mcp_servers); useClearSelectionOnPageChange(pagination, resetSelection); const { t } = useTranslation(); + const { + privilegeModal, + hidePrivilegeModal, + handShowPrivilegeModal, + recordWithSourceType, + } = useShowPrivilegeDialog(); const { importVisible, showImportModal, @@ -84,6 +106,16 @@ export default function McpServer() { const [isSelectionMode, setSelectionMode] = useState(false); + const exitSelectionMode = useCallback(() => { + setSelectionMode(false); + }, []); + + useExitMcpSelectionWithoutOwner( + data.mcp_servers, + isSelectionMode, + exitSelectionMode, + ); + const handlePageChange = useCallback( (page: number, pageSize?: number) => { setPagination({ page, pageSize }); @@ -117,21 +149,29 @@ export default function McpServer() { onChange={handleInputChange} placeholder={t('common.search')} /> - - - + + {isSelectionMode ? ( + + ) : ( + + )} + {t( + `mcp.${isSelectionMode ? 'exitBulkManage' : 'bulkManage'}`, + )} + + } + > + + +
} @@ -140,52 +180,47 @@ export default function McpServer() { {data.mcp_servers?.length ? ( <> {isSelectionMode && ( -
- 0 && - selectedList.length === data.mcp_servers.length - ? true - : selectedList.length > 0 - ? 'indeterminate' - : false - } - onCheckedChange={handleSelectAll} - /> - - - {t('mcp.selected')} {selectedList.length} - -
- - - ), - }} + +
+ +
-
+ + ), + }} + > + + + + + )} {data.mcp_servers.map((item) => ( @@ -195,8 +230,9 @@ export default function McpServer() { selectedList={selectedList} handleSelectChange={handleSelectChange} showEditModal={showEditModal} + showPrivilegeModal={handShowPrivilegeModal(item)} isSelectionMode={isSelectionMode} - > + /> ))}
@@ -208,18 +244,20 @@ export default function McpServer() {
) : ( -
+ + {t('empty.addNow')} + + } + onClickAdd={showEditModal('')} >
{t('empty.noMCP')}
- -
+ )} @@ -229,6 +267,7 @@ export default function McpServer() { onOk={handleOk} id={id} loading={loading} + operatorPermission={operatorPermission} > )} {importVisible && ( @@ -238,6 +277,12 @@ export default function McpServer() { loading={importLoading} > )} + {privilegeModal && ( + + )} ); } diff --git a/web/src/pages/user-setting/mcp/mcp-card.tsx b/web/src/pages/user-setting/mcp/mcp-card.tsx index 90205ec0e2..3c47c6cb03 100644 --- a/web/src/pages/user-setting/mcp/mcp-card.tsx +++ b/web/src/pages/user-setting/mcp/mcp-card.tsx @@ -14,8 +14,10 @@ * limitations under the License. */ +import { SharedBadge } from '@/components/shared-badge'; import { Card, CardContent } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; +import { McpSelectionGuard } from '@/ee/pages/user-setting/mcp/mcp-selection-guard'; import { IMcpServer } from '@/interfaces/database/mcp'; import { formatDate } from '@/utils/date'; import { isPlainObject } from 'lodash'; @@ -29,13 +31,16 @@ export type DatasetCardProps = { data: IMcpServer; isSelectionMode: boolean; } & Pick & - Pick; + Pick & { + showPrivilegeModal(): void; + }; export function McpCard({ data, selectedList, handleSelectChange, showEditModal, + showPrivilegeModal, isSelectionMode, }: DatasetCardProps) { const { t } = useTranslation(); @@ -61,18 +66,21 @@ export function McpCard({
{isSelectionMode ? ( - { - e.stopPropagation(); - }} - /> + + { + e.stopPropagation(); + }} + /> + ) : ( + showPrivilegeModal={showPrivilegeModal} + /> )}
@@ -81,7 +89,10 @@ export function McpCard({
{toolLength} {t('mcp.cachedTools')}
-

{formatDate(data.update_date)}

+
+

{formatDate(data.update_date)}

+ {data?.nickname} +
diff --git a/web/src/pages/user-setting/mcp/mcp-operation.tsx b/web/src/pages/user-setting/mcp/mcp-operation.tsx index 287400300b..bce0bffbe7 100644 --- a/web/src/pages/user-setting/mcp/mcp-operation.tsx +++ b/web/src/pages/user-setting/mcp/mcp-operation.tsx @@ -18,6 +18,7 @@ import { ConfirmDeleteDialog, ConfirmDeleteDialogNode, } from '@/components/confirm-delete-dialog'; +import { McpOperationShell } from '@/ee/pages/user-setting/mcp/mcp-operation-shell'; import { useDeleteMcpServer } from '@/hooks/use-mcp-request'; import { IMcpServer } from '@/interfaces/database/mcp'; import { PenLine, Trash2, Upload } from 'lucide-react'; @@ -29,7 +30,10 @@ import { useExportMcp } from './use-export-mcp'; export function McpOperation({ mcp, showEditModal, -}: { mcp: IMcpServer } & Pick) { + showPrivilegeModal, +}: { mcp: IMcpServer } & Pick & { + showPrivilegeModal(): void; + }) { const { t } = useTranslation(); const { deleteMcpServer } = useDeleteMcpServer(); const { handleExportMcpJson } = useExportMcp(mcp); @@ -39,32 +43,40 @@ export function McpOperation({ }, [deleteMcpServer, mcp.id]); return ( -
- {/* */} - - {/* + +
+ {/* */} + + {/* */} - - {/* + + {/* */} - - ), - }} - > - - - {/* */} -
+ + ), + }} + > + + + {/* */} +
+ ); } diff --git a/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx b/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx index 88d69faf42..c8abd0c1df 100644 --- a/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx +++ b/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { useOwnerMcpList } from '@/ee/pages/user-setting/mcp/use-owner-mcp-list'; import { useDeleteMcpServer } from '@/hooks/use-mcp-request'; import { IMcpServer } from '@/interfaces/database/mcp'; import { Trash2, Upload } from 'lucide-react'; @@ -35,9 +36,7 @@ export function useBulkOperateMCP(mcpList: IMcpServer[]) { const deletedIds = selectedList; const ret = await deleteMcpServer(deletedIds); if (ret.code === 0) { - setSelectedList((list) => - list.filter((id) => !deletedIds.includes(id)), - ); + setSelectedList((list) => list.filter((id) => !deletedIds.includes(id))); } }, [deleteMcpServer, selectedList]); @@ -47,13 +46,24 @@ export function useBulkOperateMCP(mcpList: IMcpServer[]) { }); }, []); + const ownerMcpList = useOwnerMcpList(mcpList); + const handleSelectAll = useCallback( (checked: boolean) => { - setSelectedList(() => (checked ? mcpList.map((item) => item.id) : [])); + setSelectedList(() => + checked ? ownerMcpList.map((item) => item.id) : [], + ); }, - [mcpList], + [ownerMcpList], ); + const selectAllChecked: boolean | 'indeterminate' = + selectedList.length === 0 + ? false + : selectedList.length === ownerMcpList.length + ? true + : 'indeterminate'; + const list = [ { id: 'export', @@ -76,6 +86,7 @@ export function useBulkOperateMCP(mcpList: IMcpServer[]) { handleDelete, handleExportMcp: handleExportMcpJson(selectedList), handleSelectAll, + selectAllChecked, resetSelection, }; } diff --git a/web/src/pages/user-setting/mcp/use-edit-mcp.ts b/web/src/pages/user-setting/mcp/use-edit-mcp.ts index fa84becd16..2a21466e8f 100644 --- a/web/src/pages/user-setting/mcp/use-edit-mcp.ts +++ b/web/src/pages/user-setting/mcp/use-edit-mcp.ts @@ -29,15 +29,19 @@ export const useEditMcp = () => { } = useSetModalState(); const { createMcpServer, loading } = useCreateMcpServer(); const [id, setId] = useState(''); + const [operatorPermission, setOperatorPermission] = useState< + number | undefined + >(); const { updateMcpServer, loading: updateLoading } = useUpdateMcpServer(); const handleShowModal = useCallback( - (id: string) => () => { + (id: string, operatorPermission?: number) => () => { setId(id); + setOperatorPermission(operatorPermission); showEditModal(); }, - [setId, showEditModal], + [setId, setOperatorPermission, showEditModal], ); const handleOk = useCallback( @@ -63,6 +67,7 @@ export const useEditMcp = () => { createMcpServer, handleOk, id, + operatorPermission, }; }; diff --git a/web/src/pages/user-setting/setting-model/index.tsx b/web/src/pages/user-setting/setting-model/index.tsx index 73cf9e3bb8..fc75213908 100644 --- a/web/src/pages/user-setting/setting-model/index.tsx +++ b/web/src/pages/user-setting/setting-model/index.tsx @@ -15,6 +15,7 @@ */ import Spotlight from '@/components/spotlight'; +import { ProviderInstanceCardWithPrivilege } from '@/ee/pages/user-setting/setting-model/provider-instance-card-with-privilege'; import { useTranslate } from '@/hooks/common-hooks'; import { useFetchAddedProviders, @@ -178,7 +179,7 @@ const SettingModelV2: FC = () => { )} {instances.map((instance, index) => ( - { showDeleteConfirm({ title: t('setting.sureDelete'), onOk: async () => { - const code = await deleteTenantUser({ userId }); - if (code === 0) { - } + deleteTenantUser({ userId }); + return; }, }); diff --git a/web/src/pages/user-setting/setting-team/index.tsx b/web/src/pages/user-setting/setting-team/index.tsx index 461726f22b..eaf4f6ce6d 100644 --- a/web/src/pages/user-setting/setting-team/index.tsx +++ b/web/src/pages/user-setting/setting-team/index.tsx @@ -14,108 +14,15 @@ * limitations under the License. */ -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - useFetchUserInfo, - useListTenantUser, -} from '@/hooks/use-user-setting-request'; -import { useTranslation } from 'react-i18next'; +import { TeamManagement } from '@/ee/pages/user-setting/setting-team/team-management'; -import Spotlight from '@/components/spotlight'; -import { SearchInput } from '@/components/ui/input'; -import { UserPlus } from 'lucide-react'; -import { useState } from 'react'; -import { ProfileSettingWrapperCard } from '../components/user-setting-header'; -import AddingUserModal from './add-user-modal'; -import { useAddUser } from './hooks'; -import TenantTable from './tenant-table'; -import UserTable from './user-table'; +import styles from '@/ee/pages/user-setting/setting-team/index.module.less'; const UserSettingTeam = () => { - const { data: userInfo } = useFetchUserInfo(); - const { t } = useTranslation(); - const [searchTerm, setSearchTerm] = useState(''); - const [searchUser, setSearchUser] = useState(''); - useListTenantUser(); - const { - addingTenantModalVisible, - hideAddingTenantModal, - showAddingTenantModal, - handleAddUserOk, - } = useAddUser(); - return ( - //
- // - // - -

- {userInfo?.nickname + ' ' + t('setting.workspace')} -

- - } - > - - -
- - - {/* */} - - {t('setting.teamMembers')} - - -
- setSearchUser(e.target.value)} - /> - -
-
- - - - -
- - - - {/* */} - - {t('setting.joinedTeams')} - - setSearchTerm(e.target.value)} - placeholder={t('common.search')} - /> - - - - - -
- - {addingTenantModalVisible && ( - - )} -
+
+ +
); }; diff --git a/web/src/pages/user-setting/setting-team/legacy-page.tsx b/web/src/pages/user-setting/setting-team/legacy-page.tsx new file mode 100644 index 0000000000..8257f971e0 --- /dev/null +++ b/web/src/pages/user-setting/setting-team/legacy-page.tsx @@ -0,0 +1,113 @@ +/* + * Copyright 2026 The InfiniFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; +import { useTranslation } from 'react-i18next'; + +import Spotlight from '@/components/spotlight'; +import { SearchInput } from '@/components/ui/input'; +import { UserPlus } from 'lucide-react'; +import { useState } from 'react'; +import { ProfileSettingWrapperCard } from '../components/user-setting-header'; +import AddingUserModal from './add-user-modal'; +import { useAddUser } from './hooks'; +import TenantTable from './tenant-table'; +import UserTable from './user-table'; + +const UserSettingTeam = () => { + const { data: userInfo } = useFetchUserInfo(); + const { t } = useTranslation(); + const [searchTerm, setSearchTerm] = useState(''); + const [searchUser, setSearchUser] = useState(''); + const { + addingTenantModalVisible, + hideAddingTenantModal, + showAddingTenantModal, + handleAddUserOk, + } = useAddUser(); + + return ( + +

+ {userInfo?.nickname + ' ' + t('setting.workspace')} +

+ + } + > + + +
+ + + {/* */} + + {t('setting.teamMembers')} + + +
+ setSearchUser(e.target.value)} + /> + +
+
+ + + + +
+ + + + {/* */} + + {t('setting.joinedTeams')} + + setSearchTerm(e.target.value)} + placeholder={t('common.search')} + /> + + + + + +
+ + {addingTenantModalVisible && ( + + )} +
+ ); +}; + +export default UserSettingTeam; diff --git a/web/src/pages/user-setting/setting-team/user-table.tsx b/web/src/pages/user-setting/setting-team/user-table.tsx index 44d940137e..60a59b0fca 100644 --- a/web/src/pages/user-setting/setting-team/user-table.tsx +++ b/web/src/pages/user-setting/setting-team/user-table.tsx @@ -30,7 +30,10 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { useListTenantUser } from '@/hooks/use-user-setting-request'; +import { + useFetchTenantInfo, + useListTenantUser, +} from '@/hooks/use-user-setting-request'; import { formatDate } from '@/utils/date'; import { upperFirst } from 'lodash'; import { ArrowDown, ArrowUp, ArrowUpDown, Trash2 } from 'lucide-react'; @@ -49,7 +52,9 @@ const ColorMap: Record = { }; const UserTable = ({ searchUser }: { searchUser: string }) => { - const { data, loading } = useListTenantUser(); + const { data: tenantInfo } = useFetchTenantInfo(); + const { data, loading } = useListTenantUser(tenantInfo.tenant_id); + // const { handleDeleteTenantUser } = useHandleDeleteUser(); const { deleteTenantUser } = useHandleDeleteUser(); const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null); const { t } = useTranslation(); diff --git a/web/src/pages/user-setting/sidebar/hooks.tsx b/web/src/pages/user-setting/sidebar/hooks.tsx index 68d9372b40..89640590dd 100644 --- a/web/src/pages/user-setting/sidebar/hooks.tsx +++ b/web/src/pages/user-setting/sidebar/hooks.tsx @@ -15,13 +15,14 @@ */ import { useLogout } from '@/hooks/use-login-request'; +import { PrivateRoutes } from '@/ee/private-routes'; import { Routes } from '@/routes'; import { useCallback, useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router'; export const useHandleMenuClick = () => { const navigate = useNavigate(); - const [active, setActive] = useState(); + const [active, setActive] = useState(); const { logout } = useLogout(); const location = useLocation(); useEffect(() => { @@ -32,7 +33,7 @@ export const useHandleMenuClick = () => { }, [location]); const handleMenuClick = useCallback( - (key: Routes) => () => { + (key: Routes | PrivateRoutes) => () => { if (key === Routes.Logout) { logout(); } else { diff --git a/web/src/pages/user-setting/sidebar/index.tsx b/web/src/pages/user-setting/sidebar/index.tsx index b0dd58f521..759badb86e 100644 --- a/web/src/pages/user-setting/sidebar/index.tsx +++ b/web/src/pages/user-setting/sidebar/index.tsx @@ -19,6 +19,7 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import ThemeSwitch from '@/components/theme-switch'; import { Button } from '@/components/ui/button'; import { Domain } from '@/constants/common'; +import { useSettingMenuItems } from '@/ee/pages/user-setting/sidebar/use-setting-menu'; import { useLogout } from '@/hooks/use-login-request'; import { useFetchSystemVersion, @@ -90,6 +91,7 @@ export function SideBar() { } }, [fetchSystemVersion]); const { logout } = useLogout(); + const items = useSettingMenuItems(menuItems(t)); return (