feat(web): add enterprise (ee) seam layer for build-time overlays (#21908)

This commit is contained in:
chanx authored and GitHub committed 2026-11-03 18:05:27 +08:00
1 parent 5dc7683aa4
commit d543182eab
136 files changed
+2534 -572

No files matched your search

+3 -1
View File
@@ -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) {
<Sonner position="top-right" expand richColors closeButton />
<Toaster />
<StarModal></StarModal>
</>
);
}
+8 -1
View File
@@ -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 ? <PortalModal {...modalParam} /> : null);
root?.render(
isVisible ? (
<EnterpriseModalProvider>
<PortalModal {...modalParam} />
</EnterpriseModalProvider>
) : null,
);
};
const show = (props: PortalModalProps) => {
@@ -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<any> & { initialName?: string },
) {
return null;
}
@@ -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}</>;
}
@@ -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<any> & {
initialValues: Record<string, unknown>;
},
) {
return null;
}
@@ -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;
+81
View File
@@ -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<string, number>,
) {
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',
}
+17
View File
@@ -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;
@@ -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;
}
+36
View File
@@ -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,
});
@@ -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,
};
}
@@ -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 });
@@ -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 };
}
@@ -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;
}
@@ -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 (
<Link
to={`${Routes.UserSetting}${Routes.Team}`}
className="inline-flex items-center"
>
{children}
</Link>
);
}
+23
View File
@@ -0,0 +1,23 @@
import React, { PropsWithChildren, useMemo } from 'react';
export type RootLayoutChrome = {
showHeader: boolean;
containerRef?: React.Ref<HTMLDivElement>;
};
/**
* 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 }), []);
}
+10
View File
@@ -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 };
+8
View File
@@ -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;
}
@@ -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<string, { value?: unknown } | undefined> = {
enable_whitelist: { value: true },
};
export default function useAdminVariables() {
return {
variables: openSourceVariables,
isFetching: false,
refetch: async () => {},
setVariables: async (_variables: Record<string, unknown>) => {},
isUpdating: false,
};
}
@@ -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;
}
@@ -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}</>;
}
+8
View File
@@ -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;
}
+7
View File
@@ -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;
}
@@ -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}
</>
);
}
@@ -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
>;
@@ -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 };
}
@@ -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;
}
@@ -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 [];
}
@@ -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<ShowPrivilegeModalReturnType>
>({});
@@ -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}
</>
);
}
@@ -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<typeof useDuplicateDataset>;
@@ -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
>;
@@ -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;
}
@@ -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}
</>
);
}
@@ -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
>;
@@ -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}
</>
);
}
@@ -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;
}
@@ -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
>;
@@ -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}
</>
);
}
@@ -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
>;
+19
View File
@@ -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,
};
+30
View File
@@ -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;
+34
View File
@@ -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<UpgradeModalContextType | undefined>(
undefined,
);
export const UpgradeModalProvider = ({
children,
}: {
children?: ReactNode;
}) => <>{children}</>;
export const useUpgradeModal = () => {
const context = useContext(UpgradeModalContext);
return (
context ?? {
isModalOpen: false,
openModal: () => {},
closeModal: () => {},
}
);
};
@@ -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 = () => '';
@@ -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 (
<div
className="flex items-center justify-between border border-dashed border-border-button rounded-md p-10 cursor-pointer w-[590px]"
onClick={onClickAdd}
>
{children}
{addButton}
</div>
);
}
@@ -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<UseEditMcpReturnType, 'showEditModal'> & {
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}</>;
}
@@ -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}</>;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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
>;
@@ -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 (
<ProviderInstanceCard
providerName={providerName}
instance={instance}
defaultOpen={defaultOpen}
/>
);
}
@@ -0,0 +1,9 @@
.teamWrapper {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
.teamCard {
// width: 100%;
}
}
@@ -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 <UserSettingTeam />;
}
@@ -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<T extends SettingMenuEntry>(
items: T[],
): T[] {
return useMemo(() => items, [items]);
}
+32
View File
@@ -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<any> }>;
errorElement?: ReactNode;
children?: PrivateRoute[];
replace?: string[];
matchIndex?: number;
};
export const privateRoutes: PrivateRoute[] = [];
+12
View File
@@ -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<boolean> => false;
export const isBillingEnabled = (): boolean | null => false;
+43
View File
@@ -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;
}
+14
View File
@@ -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;
}
+17
View File
@@ -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<boolean> => false;
+4 -1
View File
@@ -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<Nullable<boolean>>(null);
useEffect(() => {
setIsLogin(!!credentialStorage.getAuthorization());
}, [credentialStorage]);
return { isLogin };
return { isLogin, redirectUrl };
};
+23 -6
View File
@@ -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<IAgentLogResponse>({
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 ?? [] };
+7 -2
View File
@@ -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;
}
},
});
+21 -9
View File
@@ -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<IUserInfo> => {
return { data, loading };
};
export const useFetchTenantData = (): ResponseGetType<ITenantInfo> => {
/**
* @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<ITenantInfo> => {
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 : [];
},
});
+17 -1
View File
@@ -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,
]);
};
+25
View File
@@ -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 {
+11 -2
View File
@@ -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;
+1
View File
@@ -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;
@@ -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 = {
+1
View File
@@ -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. */
+3
View File
@@ -5,9 +5,12 @@ export interface IMcpServer {
id: string;
name: string;
server_type: string;
tenant_id: string;
update_date: string;
url: string;
variables: Record<string, any> & { tools?: IMCPToolObject };
operator_permission: number;
nickname?: string;
}
export type IMCPToolObject = Record<string, Omit<IMCPTool, 'name'>>;
+3 -1
View File
@@ -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;
+11 -11
View File
@@ -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 (
<Button
asLink
to={`${Routes.UserSetting}${Routes.Team}`}
variant="ghost"
size="icon"
className={cn('group size-10 shrink-0 lg:size-8', className)}
dot
>
<BellRing className="size-5 animate-bell-shake group-hover:animate-none lg:size-4" />
</Button>
<TeamInvitationReminderDialog>
<Button
variant="ghost"
size="icon"
className={cn('group size-10 shrink-0 lg:size-8', className)}
dot
>
<BellRing className="size-5 animate-bell-shake group-hover:animate-none lg:size-4" />
</Button>
</TeamInvitationReminderDialog>
);
}
+19 -3
View File
@@ -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 (
<div className="size-full min-w-0 grid grid-flow-col grid-cols-1 grid-rows-[auto_1fr]">
<Header className="px-5 py-4" />
<div
className="size-full min-w-0 grid grid-flow-col grid-cols-1 grid-rows-[auto_auto_1fr]"
ref={containerRef}
>
<div>
<RootLayoutBanner />
</div>
{showHeader && <Header className="px-5 py-4" />}
<main className="size-full min-w-0 overflow-hidden">{children}</main>
</div>
@@ -14,7 +28,9 @@ export function RootLayoutContainer({ children }: React.PropsWithChildren) {
export default function RootLayout() {
return (
<RootLayoutContainer>
<Outlet />
<RootLayoutContent>
<Outlet />
</RootLayoutContent>
</RootLayoutContainer>
);
}
+11 -8
View File
@@ -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(
<React.StrictMode>
<Inspector keys={['alt', 'c']} />
<App />
</React.StrictMode>,
);
});
initLanguage()
.then(() => fetchBillingStatus())
.then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Inspector keys={['alt', 'c']} />
<App />
</React.StrictMode>,
);
});
+3
View File
@@ -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() {
</div>
</section>
</CardHeader>
<UserDetailAssets userId={id} />
</Card>
</section>
);
+16 -1
View File
@@ -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<AdminService.ListWhitelistItem>();
const globalFilterFn = createFuzzySearchFn<AdminService.ListWhitelistItem>([
'email',
@@ -541,4 +544,16 @@ function AdminWhitelist() {
);
}
export default AdminWhitelist;
function AdminWhitelistGuard() {
const { variables } = useAdminVariables();
const isWhitelistEnabled = !!variables.enable_whitelist?.value;
if (!isWhitelistEnabled) {
return <NotFound />;
}
return <AdminWhitelist />;
}
export default AdminWhitelistGuard;
+6 -1
View File
@@ -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(
+52 -33
View File
@@ -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<WidgetSettings>(() => {
const widgetSettings =
agentDetail?.dsl?.globals?.[AgentWidgetSettingsGlobalKey];
@@ -325,30 +330,40 @@ export default function Agent() {
</div>
</section>
<div className="flex items-center gap-5">
<ButtonLoading
variant={'secondary'}
onClick={handleSave}
loading={loading}
>
<LaptopMinimalCheck /> {t('flow.saveDraft')}
</ButtonLoading>
{showRunButton &&
(runTooltipKey ? (
<RunTooltip tooltip={runTooltipKey}>{runButton}</RunTooltip>
) : (
runButton
))}
{isConversationMode && (
<Button variant={'secondary'} onClick={handleExploreClick}>
<Compass />
{t('explore.title')}
</Button>
)}
<PublishConfirmDialog
agentDetail={agentDetail}
loading={loading}
onPublish={handlePublish}
/>
<OnlineUsers></OnlineUsers>
<AgentEditGuard operatorPermission={operatorPermission}>
<ButtonLoading
variant={'secondary'}
onClick={handleSave}
loading={loading}
>
<LaptopMinimalCheck /> {t('flow.saveDraft')}
</ButtonLoading>
{showRunButton &&
(runTooltipKey ? (
<RunTooltip tooltip={runTooltipKey}>{runButton}</RunTooltip>
) : (
runButton
))}
{isConversationMode && (
<Button variant={'secondary'} onClick={handleExploreClick}>
<Compass />
{t('explore.title')}
</Button>
)}
{/* <PublishConfirmDialog
agentDetail={agentDetail}
loading={loading}
onPublish={() => saveGraph(undefined, undefined, true)}
onBeforeOpen={hasBlockingIssues}
/> */}
<PublishConfirmDialog
agentDetail={agentDetail}
loading={loading}
onPublish={handlePublish}
/>
</AgentEditGuard>
<CanvasChecklist issues={issues} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -357,11 +372,13 @@ export default function Agent() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<AgentDropdownMenuItem onClick={() => showGlobalParamSheet()}>
<MessageSquareCode />
{t('flow.conversationVariable')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
<AgentEditGuard operatorPermission={operatorPermission}>
<AgentDropdownMenuItem onClick={() => showGlobalParamSheet()}>
<MessageSquareCode />
{t('flow.conversationVariable')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
</AgentEditGuard>
<AgentDropdownMenuItem onClick={showVersionDialog}>
<History />
{t('flow.historyVersion')}
@@ -373,10 +390,12 @@ export default function Agent() {
{t('flow.export')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showSettingDialog}>
<Settings />
{t('flow.setting')}
</AgentDropdownMenuItem>
<AgentEditGuard operatorPermission={operatorPermission}>
<AgentDropdownMenuItem onClick={showSettingDialog}>
<Settings />
{t('flow.setting')}
</AgentDropdownMenuItem>
</AgentEditGuard>
{isPipeline ||
(location.hostname !== 'cloud.ragflow.io' && (
<>
+17 -6
View File
@@ -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<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;
data: IFlow;
} & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'> & {
showPrivilegeModal(): void;
};
function AgentTypeIcon({
data,
@@ -73,6 +75,7 @@ function AgentTags({ tags }: { tags?: string }) {
<Badge
key={tag}
variant="secondary"
className="text-xs font-normal mr-1 space-x-1"
>
<Tag className="size-3" />
@@ -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={
<AgentDropdown showAgentRenameModal={showAgentRenameModal} agent={data}>
<MoreButton></MoreButton>
<AgentDropdown
showAgentRenameModal={showAgentRenameModal}
agent={data}
showPrivilegeModal={showPrivilegeModal}
>
<MoreButton />
</AgentDropdown>
}
sharedBadge={<SharedBadge>{data.nickname}</SharedBadge>}
+47 -32
View File
@@ -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<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'> & {
agent: IFlow;
showPrivilegeModal(): void;
}) {
const { t } = useTranslation();
const { deleteAgent } = useDeleteAgent();
@@ -57,37 +60,49 @@ export function AgentDropdown({
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowAgentRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
<DropdownMenuItem onClick={handleEditTags}>
{t('flow.editTags')} <Tag />
</DropdownMenuItem>
<DropdownMenuSeparator />
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('deleteModal.delAgent')}
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: agent.avatar, name: agent.title }}
name={agent.title}
/>
),
}}
>
<DropdownMenuItem
className="text-state-error"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
<AgentDropdownGuard
agent={agent}
showPrivilegeModal={showPrivilegeModal}
manageItems={
<>
<DropdownMenuItem onClick={handleShowAgentRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
<DropdownMenuItem onClick={handleEditTags}>
{t('flow.editTags')} <Tag />
</DropdownMenuItem>
</>
}
deleteItems={
<>
<DropdownMenuSeparator />
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('deleteModal.delAgent')}
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: agent.avatar, name: agent.title }}
name={agent.title}
/>
),
}}
>
<DropdownMenuItem
className="text-state-error"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
</>
}
/>
</DropdownMenuContent>
</DropdownMenu>
<AgentTagEditor
+15
View File
@@ -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 { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ListDeletionKey } from '@/constants/list-deletion';
@@ -34,6 +35,7 @@ import { useSelectFilters } from './hooks/use-select-filters';
import { UploadAgentDialog } from './upload-agent-dialog';
import { useHandleImportJsonFile } from './use-import-json';
import { useRenameAgent } from './use-rename-agent';
import { useShowPrivilegeDialog } from '@/ee/pages/agents/use-show-privilege-dialog';
export default function Agents() {
const {
@@ -108,6 +110,12 @@ export default function Agents() {
handleImportJson,
);
const {
privilegeModalVisible,
hidePrivilegeModal,
handShowPrivilegeModal,
recordWithSourceType,
} = useShowPrivilegeDialog();
const { deleteGroup } = useDeleteCompilationTemplateGroup();
const filters = useSelectFilters(activeCategory);
@@ -224,6 +232,7 @@ export default function Agents() {
key={x.id}
data={x}
showAgentRenameModal={showAgentRenameModal}
showPrivilegeModal={handShowPrivilegeModal(x)}
/>
),
)}
@@ -297,6 +306,12 @@ export default function Agents() {
onOk={onFileUploadOk}
></UploadAgentDialog>
)}
{privilegeModalVisible && (
<PrivilegeManagementDialog
hideModal={hidePrivilegeModal}
initialValues={recordWithSourceType}
></PrivilegeManagementDialog>
)}
</>
);
}
@@ -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}
/>
</div>
</div>
@@ -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 (
<div className={cn('flex gap-4 text-sm text-text-secondary', className)}>
<Label className="flex items-center gap-2">
@@ -7,6 +7,7 @@ import {
} from '@/components/ui/popover';
import { Radio } from '@/components/ui/radio';
import { Segmented } from '@/components/ui/segmented';
import { useDatasetEditButtonDisabled } from '@/ee/hooks/logic-hooks/use-permission';
import { cn } from '@/lib/utils';
import { LucideFilter, Plus } from 'lucide-react';
import { useState } from 'react';
@@ -32,6 +33,7 @@ export default function ChunkResultBar({
handleInputChange,
searchString,
}: ChunkResultBarProps) {
const datasetEditButtonDisabled = useDatasetEditButtonDisabled();
const { t } = useTranslation('translation', { keyPrefix: 'chunk' });
const [textSelectValue, setTextSelectValue] = useState<string | number>(
ChunkTextMode.Full,
@@ -71,12 +73,12 @@ export default function ChunkResultBar({
value={textSelectValue}
onChange={changeTextSelectValue}
/>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="icon"
disabled={datasetEditButtonDisabled}
>
<LucideFilter />
</Button>
@@ -93,7 +95,12 @@ export default function ChunkResultBar({
value={searchString}
/>
<Button variant="outline" size="icon" onClick={() => createChunk()}>
<Button
variant="outline"
size="icon"
onClick={() => createChunk()}
disabled={datasetEditButtonDisabled}
>
<Plus size={44} />
</Button>
</div>
@@ -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<IMetaDataTableData[]>(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,
@@ -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<UseRowSelectionType, 'setRowSelection'>;
export function DatasetActionCell({
record,
showRenameModal,
setRowSelection,
}: { record: IDocumentInfo } & UseRenameDocumentShowType &
Pick<UseRowSelectionType, 'setRowSelection'>) {
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 (
<div
className="
flex gap-2 items-center opacity-0
transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"
className={cn(
'flex gap-2 items-center opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100',
actionClassName,
)}
>
<DocumentPrivilegeAction record={record} disabled={actionDisabled} />
<Button
size="icon-xs"
variant="ghost"
disabled={isRunning}
disabled={actionDisabled}
onClick={handleRename}
>
<PenLine className="size-[1em]" />
@@ -123,7 +138,7 @@ export function DatasetActionCell({
data-testid="document-delete"
size="icon-xs"
variant="ghost"
disabled={isRunning}
disabled={actionDisabled}
>
<Trash2 className="size-[1em]" />
</Button>
+37 -13
View File
@@ -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() {
<CancelParsingDropdown />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="default">
<Button size="default" disabled={datasetEditButtonDisabled}>
<LucidePlus />
{t('knowledgeDetails.addFile')}
</Button>
@@ -176,7 +191,6 @@ export default function Dataset() {
<DropdownMenuItem onClick={showDocumentUploadModal}>
{t('fileManager.uploadFile')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={showCreateModal}>
{t('knowledgeDetails.emptyFiles')}
</DropdownMenuItem>
@@ -195,16 +209,18 @@ export default function Dataset() {
</CardHeader>
<CardContent className="px-5 py-0">
<DatasetTable
documents={documents}
pagination={pagination}
setPagination={setPagination}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
showManageMetadataModal={showManageMetadataModal}
loading={loading}
bulkOperateBarVisible={!rowSelectionIsEmpty}
/>
<ShowPrivilegeModalContext.Provider value={{ handShowPrivilegeModal }}>
<DatasetTable
documents={documents}
pagination={pagination}
setPagination={setPagination}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
showManageMetadataModal={showManageMetadataModal}
loading={loading}
bulkOperateBarVisible={!rowSelectionIsEmpty}
></DatasetTable>
</ShowPrivilegeModalContext.Provider>
{documentUploadVisible && (
<FileUploadDialog
@@ -256,6 +272,14 @@ export default function Dataset() {
/>
)}
</CardContent>
{/* </section> */}
{privilegeModal && (
<PrivilegeManagementDialog
hideModal={hidePrivilegeModal}
initialValues={recordWithSourceType}
></PrivilegeManagementDialog>
)}
{/* </> */}
</Card>
);
}
@@ -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);
};
@@ -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<UseRowSelectionType, 'rowSelection' | 'setRowSelection'> & {
documents: IDocumentInfo[];
}) {
} & Pick<ShowPrivilegeModalReturnType, 'handShowPrivilegeModal'>) {
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: <LucideCircleX />,
onClick: handleCancelClick,
},
...extraBulkActions,
{
id: 'enabled',
label: t('knowledgeDetails.enabled'),
@@ -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,
],
);
+16 -3
View File
@@ -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<ReturnType<typeof useRenameDataset>, 'showDatasetRenameModal'>;
showPrivilegeModal(): void;
} & Pick<ReturnType<typeof useRenameDataset>, '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={
<DatasetDropdown
showDatasetRenameModal={showDatasetRenameModal}
showDatasetDuplicateModal={showDatasetDuplicateModal}
showPrivilegeModal={showPrivilegeModal}
dataset={dataset}
>
<MoreButton></MoreButton>
@@ -35,12 +47,13 @@ export function DatasetCard({
}
sharedBadge={<SharedBadge>{dataset.nickname}</SharedBadge>}
onClick={navigateToDataset(dataset.id)}
/>
></HomeCard>
);
}
export function SeeAllCard() {
const { navigateToDatasetList } = useNavigatePage();
const { t } = useTranslation();
return (
<Card
+51 -29
View File
@@ -9,6 +9,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { DatasetDropdownGuard } from '@/ee/pages/datasets/dataset-dropdown-guard';
import useDuplicateDataset from '@/ee/pages/datasets/use-duplicate-dataset';
import { useDeleteKnowledge } from '@/hooks/use-knowledge-request';
import { IDataset } from '@/interfaces/database/dataset';
import { PenLine, Trash2 } from 'lucide-react';
@@ -16,11 +18,21 @@ import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useRenameDataset } from './use-rename-dataset';
type IDatasetDropdownProps = {
dataset: IDataset;
showPrivilegeModal(): void;
} & Pick<ReturnType<typeof useRenameDataset>, 'showDatasetRenameModal'> & {
showDatasetDuplicateModal: ReturnType<
typeof useDuplicateDataset
>['showModal'];
};
export function DatasetDropdown({
children,
showDatasetRenameModal,
dataset,
}: PropsWithChildren &
showPrivilegeModal,
}: PropsWithChildren<IDatasetDropdownProps> &
Pick<ReturnType<typeof useRenameDataset>, 'showDatasetRenameModal'> & {
dataset: IDataset;
}) {
@@ -44,34 +56,44 @@ export function DatasetDropdown({
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowDatasetRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
<DropdownMenuSeparator />
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('deleteModal.delDataset')}
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: dataset.avatar, name: dataset.name }}
name={dataset.name}
/>
),
}}
>
<DropdownMenuItem
className="text-state-error"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
<DatasetDropdownGuard
dataset={dataset}
showPrivilegeModal={showPrivilegeModal}
manageItems={
<DropdownMenuItem onClick={handleShowDatasetRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
}
deleteItems={
<>
<DropdownMenuSeparator />
<ConfirmDeleteDialog
onOk={handleDelete}
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: dataset.avatar, name: dataset.name }}
name={dataset.name}
/>
),
}}
title={t('deleteModal.delDataset')}
>
<DropdownMenuItem
className="text-state-error"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
</>
}
/>
</DropdownMenuContent>
</DropdownMenu>
);
+36
View File
@@ -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)}
/>
))}
</CardContainer>
@@ -166,6 +188,14 @@ export default function Datasets() {
loading={creatingLoading}
></DatasetCreatingDialog>
)}
{datasetDuplicateModalVisible && (
<DuplicateDialog
hideModal={hideDatasetDuplicateModal}
onOk={onDatasetDuplicateOk}
initialName={initialDatasetDuplicateName}
loading={datasetDuplicateLoading}
/>
)}
{datasetRenameVisible && (
<RenameDialog
hideModal={hideDatasetRenameModal}
@@ -174,6 +204,12 @@ export default function Datasets() {
loading={datasetRenameLoading}
></RenameDialog>
)}
{privilegeModal && (
<PrivilegeManagementDialog
hideModal={hidePrivilegeModal}
initialValues={recordWithSourceType}
></PrivilegeManagementDialog>
)}
</>
);
}
+9 -1
View File
@@ -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<CellContext<IFile, unknown>, 'row'> &
Pick<UseHandleConnectToKnowledgeReturnType, 'showConnectToKnowledgeModal'> &
Pick<UseRenameCurrentFileReturnType, 'showFileRenameModal'> &
Pick<UseRowSelectionType, 'setRowSelection'> &
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({
<Link2 />
</Button>
)}
<FilePrivilegeButton
record={record}
showPrivilegeModal={showPrivilegeModal}
/>
{isKnowledgeBase || (
<Button
variant="transparent"
+3
View File
@@ -61,6 +61,7 @@ type FilesTableProps = Pick<
Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'> &
UseMoveDocumentShowType & {
connectKnowledgeModal: UseHandleConnectToKnowledgeReturnType;
showPrivilegeModal?(record: IFile): void;
};
export function FilesTable({
@@ -73,6 +74,7 @@ export function FilesTable({
setRowSelection,
showMoveFileModal,
connectKnowledgeModal,
showPrivilegeModal,
}: FilesTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
@@ -273,6 +275,7 @@ export function FilesTable({
showFileRenameModal={showFileRenameModal}
showMoveFileModal={showMoveFileModal}
setRowSelection={setRowSelection}
showPrivilegeModal={showPrivilegeModal}
/>
);
},
+17 -5
View File
@@ -1,4 +1,5 @@
import { UploadFormSchemaType } from '@/components/file-upload-dialog';
import { useUploadBillingError } from '@/ee/hooks/use-upload-billing-error';
import { useSetModalState } from '@/hooks/common-hooks';
import { useUploadFile } from '@/hooks/use-file-request';
import { useCallback } from 'react';
@@ -11,19 +12,30 @@ export const useHandleUploadFile = () => {
showModal: showFileUploadModal,
} = useSetModalState();
const { uploadFile, loading } = useUploadFile();
const { handleUploadError } = useUploadBillingError();
const id = useGetFolderId();
const onFileUploadOk = useCallback(
async ({ fileList }: UploadFormSchemaType): Promise<number | undefined> => {
if (fileList.length > 0) {
const ret: number = await uploadFile({ fileList, parentId: id });
if (ret === 0) {
hideFileUploadModal();
const ret = await uploadFile({
fileList: fileList as File[],
parentId: id,
});
if (handleUploadError(ret)) {
return ret?.code;
}
return ret;
if (ret?.code === 0) {
hideFileUploadModal();
return 0;
}
return ret?.code;
}
},
[uploadFile, hideFileUploadModal, id],
[uploadFile, hideFileUploadModal, id, handleUploadError],
);
return {
+4
View File
@@ -3,6 +3,7 @@ import { MoreButton } from '@/components/more-button';
import { RenameDialog } from '@/components/rename-dialog';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
import { IFlow } from '@/interfaces/database/agent';
import { AgentListItem, AgentListItemType } from '@/interfaces/database/agent';
import { useEffect, useMemo } from 'react';
import { AgentDropdown } from '../agents/agent-dropdown';
@@ -11,9 +12,11 @@ import { useRenameAgent } from '../agents/use-rename-agent';
export function Agents({
setListLength,
setLoading,
showPrivilegeModal,
}: {
setListLength: (length: number) => void;
setLoading?: (loading: boolean) => void;
showPrivilegeModal: (agent: IFlow) => () => void;
}) {
const { data, loading } = useFetchAgentListByPage();
const { navigateToAgent } = useNavigatePage();
@@ -51,6 +54,7 @@ export function Agents({
<AgentDropdown
showAgentRenameModal={showAgentRenameModal}
agent={x}
showPrivilegeModal={showPrivilegeModal(x)}
>
<MoreButton></MoreButton>
</AgentDropdown>
+15
View File
@@ -1,12 +1,14 @@
import { CardSineLineContainer } from '@/components/card-singleline-container';
import { EmptyCardType } from '@/components/empty/constant';
import { EmptyAppCard } from '@/components/empty/empty';
import { PrivilegeManagementDialog } from '@/ee/components/privilege-management/privilege-management-dialog';
import { HomeIcon } from '@/components/svg-icon';
import { Segmented, SegmentedValue } from '@/components/ui/segmented';
import { Routes } from '@/routes';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { useShowPrivilegeDialog as useAgentShowPrivilegeDialog } from '@/ee/pages/agents/use-show-privilege-dialog';
import { Agents } from './agent-list';
import { SeeAllAppCard } from './application-card';
import { ChatList } from './chat-list';
@@ -33,6 +35,12 @@ export function Applications() {
const navigate = useNavigate();
const [listLength, setListLength] = useState(0);
const [loading, setLoading] = useState(false);
const {
privilegeModalVisible,
hidePrivilegeModal,
handShowPrivilegeModal,
recordWithSourceType,
} = useAgentShowPrivilegeDialog();
const handleNavigate = useCallback(
({ isCreate }: { isCreate?: boolean }) => {
@@ -89,6 +97,7 @@ export function Applications() {
<Agents
setListLength={(length: number) => setListLength(length)}
setLoading={(loading: boolean) => setLoading(loading)}
showPrivilegeModal={handShowPrivilegeModal}
/>
)}
{val === Routes.Chats && (
@@ -122,6 +131,12 @@ export function Applications() {
/>
</div>
)}
{privilegeModalVisible && (
<PrivilegeManagementDialog
hideModal={hidePrivilegeModal}
initialValues={recordWithSourceType}
/>
)}
{/* </div> */}
</section>
);
+37 -15
View File
@@ -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) => (
<HomeCard
key={x.id}
data={{
avatar: x.icon,
...x,
}}
onClick={navigateToChat(x.id)}
moreDropdown={
<ChatDropdown chat={x} showChatRenameModal={showChatRenameModal}>
<MoreButton></MoreButton>
</ChatDropdown>
}
></HomeCard>
))}
{data?.chats &&
data.chats.length > 0 &&
data.chats.slice(0, 10).map((x) => (
<HomeCard
key={x.id}
data={{
avatar: x.icon,
...x,
}}
onClick={navigateToChat(x.id)}
moreDropdown={
<ChatDropdown
chat={x}
showChatRenameModal={showChatRenameModal}
showPrivilegeModal={handleShowPrivilegeModal(x)}
>
<MoreButton></MoreButton>
</ChatDropdown>
}
></HomeCard>
))}
{chatRenameVisible && (
<RenameDialog
hideModal={hideChatRenameModal}
@@ -57,6 +73,12 @@ export function ChatList({
title={initialChatName || t('chat.createChat')}
></RenameDialog>
)}
{privilegeModalVisible && (
<PrivilegeManagementDialog
hideModal={hidePrivilegeModal}
initialValues={privilegeRecord}
></PrivilegeManagementDialog>
)}
</>
);
}
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff. Show more