diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx new file mode 100644 index 0000000000..85ce85bc39 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/access-config/page.tsx @@ -0,0 +1,16 @@ +import type { Locale } from '@/i18n-config' +import AppAccessConfigPage from '@/app/components/app/access-config' + +export type AccessConfigPageProps = { + params: Promise<{ locale: Locale, appId: string }> +} + +const AccessConfig = async (props: AccessConfigPageProps) => { + const params = await props.params + + const { appId } = params + + return +} + +export default AccessConfig diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 8a1a6fd131..3493240edb 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -12,6 +12,8 @@ import { RiTerminalBoxLine, RiTerminalWindowFill, RiTerminalWindowLine, + RiUserSettingsFill, + RiUserSettingsLine, } from '@remixicon/react' import { useUnmount } from 'ahooks' import * as React from 'react' @@ -100,6 +102,15 @@ const AppDetailLayout: FC = (props) => { icon: RiDashboard2Line, selectedIcon: RiDashboard2Fill, }, + ...(isCurrentWorkspaceEditor + ? [{ + name: 'Access Config', + href: `/app/${appId}/access-config`, + icon: RiUserSettingsLine, + selectedIcon: RiUserSettingsFill, + }] + : [] + ), ] return navConfig }, [t]) diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/access-config/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/access-config/page.tsx new file mode 100644 index 0000000000..66a809a795 --- /dev/null +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/access-config/page.tsx @@ -0,0 +1,15 @@ +import DatasetAccessConfigPage from '@/app/components/datasets/access-config' + +type Props = { + params: Promise<{ datasetId: string }> +} + +const AccessConfig = async (props: Props) => { + const params = await props.params + + const { datasetId } = params + + return +} + +export default AccessConfig diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index ba3272c1a7..0cc5bd7351 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -9,6 +9,8 @@ import { RiFileTextLine, RiFocus2Fill, RiFocus2Line, + RiUserSettingsFill, + RiUserSettingsLine, } from '@remixicon/react' import * as React from 'react' import { useEffect, useMemo, useState } from 'react' @@ -83,6 +85,13 @@ const DatasetDetailLayout: FC = (props) => { selectedIcon: RiEqualizer2Fill, disabled: false, }, + { + name: 'Access Config', + href: `/datasets/${datasetId}/access-config`, + icon: RiUserSettingsLine, + selectedIcon: RiUserSettingsFill, + disabled: false, + }, ] if (datasetRes?.provider !== 'external') { diff --git a/web/app/components/access-config-modal/index.tsx b/web/app/components/access-config-modal/index.tsx index 8514eb9e06..a0052bd397 100644 --- a/web/app/components/access-config-modal/index.tsx +++ b/web/app/components/access-config-modal/index.tsx @@ -1,9 +1,6 @@ 'use client' -import type { - AccessRule, - AssignedRole, -} from '@/app/components/header/account-setting/access-rules-page/access-rule-row' +import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row' import { Button } from '@langgenius/dify-ui/button' import { Dialog, @@ -14,8 +11,7 @@ import { } from '@langgenius/dify-ui/dialog' import { ScrollArea } from '@langgenius/dify-ui/scroll-area' import { useCallback, useState } from 'react' -import AccessRuleRow from '@/app/components/header/account-setting/access-rules-page/access-rule-row' -import AddRuleTargetsModal from '@/app/components/header/account-setting/access-rules-page/add-rule-targets-modal' +import AccessRulesEditor from '@/app/components/access-rules-editor' export type AccessConfigModalProps = { open: boolean @@ -46,38 +42,6 @@ const AccessConfigModalBody = ({ onSave, }: AccessConfigModalBodyProps) => { const [rules, setRules] = useState(initialRules) - const [addingRule, setAddingRule] = useState(null) - - const handleAddRole = useCallback((rule: AccessRule) => { - setAddingRule(rule) - }, []) - - const handleCloseAddModal = useCallback(() => { - setAddingRule(null) - }, []) - - const handleAddSubmit = useCallback( - (_selection: { roleIds: string[], memberIds: string[] }) => { - // TODO: wire up to API when backend is ready. - }, - [], - ) - - const handleRemoveRole = useCallback( - (target: AccessRule, role: AssignedRole) => { - setRules(prev => - prev.map(rule => - rule.id === target.id - ? { - ...rule, - assignedRoles: rule.assignedRoles.filter(r => r.id !== role.id), - } - : rule, - ), - ) - }, - [], - ) const handleSave = useCallback(() => { onSave?.(rules) @@ -105,17 +69,7 @@ const AccessConfigModalBody = ({ className="min-h-0 flex-1" slotClassNames={{ viewport: 'px-6 overscroll-contain' }} > -
- {rules.map(rule => ( - - ))} -
+
@@ -126,17 +80,6 @@ const AccessConfigModalBody = ({ {saveLabel}
- - {addingRule && ( - role.id)} - initialMemberIds={[]} - onClose={handleCloseAddModal} - onSubmit={handleAddSubmit} - /> - )} ) } @@ -159,15 +102,17 @@ const AccessConfigModal = ({ onClose() }} > - + {open && ( + + )} ) } diff --git a/web/app/components/access-rules-editor/index.tsx b/web/app/components/access-rules-editor/index.tsx new file mode 100644 index 0000000000..82a11ae4c5 --- /dev/null +++ b/web/app/components/access-rules-editor/index.tsx @@ -0,0 +1,103 @@ +'use client' + +import type { + AccessRule, + AssignedRole, +} from '@/app/components/header/account-setting/access-rules-page/access-rule-row' +import { cn } from '@langgenius/dify-ui/cn' +import { useCallback, useState } from 'react' +import AccessRuleRow from '@/app/components/header/account-setting/access-rules-page/access-rule-row' +import AddRuleTargetsModal from '@/app/components/header/account-setting/access-rules-page/add-rule-targets-modal' + +export type AccessRulesEditorProps = { + rules: AccessRule[] + /** + * Called whenever assigned roles/members are mutated. The editor is + * controlled when this callback is provided, uncontrolled (with internal + * state seeded from `rules`) otherwise. + */ + onRulesChange?: (rules: AccessRule[]) => void + className?: string +} + +const AccessRulesEditor = ({ + rules: rulesProp, + onRulesChange, + className, +}: AccessRulesEditorProps) => { + const isControlled = typeof onRulesChange === 'function' + const [internalRules, setInternalRules] = useState(rulesProp) + const rules = isControlled ? rulesProp : internalRules + + const updateRules = useCallback( + (updater: (prev: AccessRule[]) => AccessRule[]) => { + if (isControlled) { + onRulesChange(updater(rulesProp)) + return + } + setInternalRules(prev => updater(prev)) + }, + [isControlled, onRulesChange, rulesProp], + ) + + const [addingRule, setAddingRule] = useState(null) + + const handleAddRole = useCallback((rule: AccessRule) => { + setAddingRule(rule) + }, []) + + const handleCloseAddModal = useCallback(() => { + setAddingRule(null) + }, []) + + const handleAddSubmit = useCallback( + (_selection: { roleIds: string[], memberIds: string[] }) => { + // TODO: wire up to API when backend is ready. + }, + [], + ) + + const handleRemoveRole = useCallback( + (target: AccessRule, role: AssignedRole) => { + updateRules(prev => + prev.map(rule => + rule.id === target.id + ? { + ...rule, + assignedRoles: rule.assignedRoles.filter(r => r.id !== role.id), + } + : rule, + ), + ) + }, + [updateRules], + ) + + return ( +
+ {rules.map((rule, index) => ( + 0 && 'border-t border-divider-subtle')} + /> + ))} + + {addingRule && ( + role.id)} + initialMemberIds={[]} + onClose={handleCloseAddModal} + onSubmit={handleAddSubmit} + /> + )} +
+ ) +} + +export default AccessRulesEditor diff --git a/web/app/components/app/access-config/index.tsx b/web/app/components/app/access-config/index.tsx new file mode 100644 index 0000000000..a7dd2cc0dd --- /dev/null +++ b/web/app/components/app/access-config/index.tsx @@ -0,0 +1,74 @@ +'use client' + +import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row' +import { ScrollArea } from '@langgenius/dify-ui/scroll-area' +import AccessRulesEditor from '@/app/components/access-rules-editor' + +// TODO: replace with the per-app access rules fetched from the access-rules +// API once available. Mirrors the workspace-level App access rules catalog. +const DEFAULT_APP_ACCESS_RULES: AccessRule[] = [ + { + id: 'app-full-access', + name: 'Full access', + description: 'Highest level. Can edit, publish, delete apps, and manage access for this app.', + assignedRoles: [ + { id: 'owner', name: 'Owner' }, + { id: 'admin', name: 'Admin' }, + { id: 'app-admin', name: 'App Admin' }, + { id: 'executive', name: 'Executive' }, + ], + permissions: [], + }, + { + id: 'app-can-edit', + name: 'Can edit', + description: 'Modify Prompts, adjust workflows, change variables. Test and publish updates.', + assignedRoles: [ + { id: 'app-editor', name: 'App Editor' }, + { id: 'it-staff', name: 'IT Staff' }, + ], + permissions: [], + }, + { + id: 'app-can-view-and-use', + name: 'Can view & use', + description: 'View and use the app. Access Prompt and workflow logs. Cannot modify.', + assignedRoles: [ + { id: 'tester', name: 'Tester' }, + { id: 'ops-staff', name: 'Ops Staff' }, + { id: 'member', name: 'Member' }, + ], + permissions: [], + }, + { + id: 'app-can-preview', + name: 'Can preview', + description: 'View the app in the list only. Cannot open the editor or use the app.', + assignedRoles: [ + { id: 'partner', name: 'Partner' }, + ], + permissions: [], + }, +] + +type AppAccessConfigPageProps = { + appId: string +} + +const AppAccessConfigPage = ({ appId: _appId }: AppAccessConfigPageProps) => { + return ( + +
+

Access Config

+
+ +
+
+
+ ) +} + +export default AppAccessConfigPage diff --git a/web/app/components/datasets/access-config/index.tsx b/web/app/components/datasets/access-config/index.tsx new file mode 100644 index 0000000000..a5c75bf738 --- /dev/null +++ b/web/app/components/datasets/access-config/index.tsx @@ -0,0 +1,83 @@ +'use client' + +import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row' +import { ScrollArea } from '@langgenius/dify-ui/scroll-area' +import AccessRulesEditor from '@/app/components/access-rules-editor' + +// TODO: replace with the per-knowledge-base access rules fetched from the +// access-rules API once available. Mirrors the workspace-level Knowledge Base +// access rules catalog. +const DEFAULT_KB_ACCESS_RULES: AccessRule[] = [ + { + id: 'kb-full-access', + name: 'Full access', + description: 'Highest level. Can edit, publish, delete, and manage access for this knowledge base.', + assignedRoles: [ + { id: 'owner', name: 'Owner' }, + { id: 'admin', name: 'Admin' }, + { id: 'kb-admin', name: 'KB Admin' }, + { id: 'executive', name: 'Executive' }, + ], + permissions: [], + }, + { + id: 'kb-can-edit', + name: 'Can edit', + description: 'Edit knowledge base content, modify settings, and run tests.', + assignedRoles: [ + { id: 'kb-editor', name: 'KB Editor' }, + { id: 'ops-staff', name: 'Ops Staff' }, + { id: 'it-staff', name: 'IT Staff' }, + ], + permissions: [], + }, + { + id: 'kb-can-view-and-use', + name: 'Can view & use', + description: 'View knowledge base sources, configs, and logs. Cannot modify content.', + assignedRoles: [ + { id: 'member', name: 'Member' }, + ], + permissions: [], + }, + { + id: 'kb-can-preview', + name: 'Can preview', + description: 'View in the list only. Cannot access the detail page.', + assignedRoles: [ + { id: 'partner', name: 'Partner' }, + ], + permissions: [], + }, + { + id: 'kb-can-test', + name: 'Can test', + description: 'Test knowledge base retrieval efficiency in sandbox.', + assignedRoles: [ + { id: 'tester', name: 'Tester' }, + ], + permissions: [], + }, +] + +type DatasetAccessConfigPageProps = { + datasetId: string +} + +const DatasetAccessConfigPage = ({ datasetId: _datasetId }: DatasetAccessConfigPageProps) => { + return ( + +
+

Access Config

+
+ +
+
+
+ ) +} + +export default DatasetAccessConfigPage