diff --git a/web/app/components/access-config-modal/index.tsx b/web/app/components/access-config-modal/index.tsx deleted file mode 100644 index a0052bd397..0000000000 --- a/web/app/components/access-config-modal/index.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client' - -import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row' -import { Button } from '@langgenius/dify-ui/button' -import { - Dialog, - DialogCloseButton, - DialogContent, - DialogDescription, - DialogTitle, -} from '@langgenius/dify-ui/dialog' -import { ScrollArea } from '@langgenius/dify-ui/scroll-area' -import { useCallback, useState } from 'react' -import AccessRulesEditor from '@/app/components/access-rules-editor' - -export type AccessConfigModalProps = { - open: boolean - title: string - description: string - initialRules: AccessRule[] - /** - * Optional override label for the primary action. Defaults to "Save". - */ - saveLabel?: string - /** - * Optional override label for the cancel action. Defaults to "Cancel". - */ - cancelLabel?: string - onClose: () => void - onSave?: (rules: AccessRule[]) => void -} - -type AccessConfigModalBodyProps = Omit - -const AccessConfigModalBody = ({ - title, - description, - initialRules, - saveLabel = 'Save', - cancelLabel = 'Cancel', - onClose, - onSave, -}: AccessConfigModalBodyProps) => { - const [rules, setRules] = useState(initialRules) - - const handleSave = useCallback(() => { - onSave?.(rules) - onClose() - }, [onClose, onSave, rules]) - - return ( - -
- -
- - {title} - - - {description} - -
-
- - - - - -
- - -
-
- ) -} - -const AccessConfigModal = ({ - open, - title, - description, - initialRules, - saveLabel, - cancelLabel, - onClose, - onSave, -}: AccessConfigModalProps) => { - return ( - { - if (!nextOpen) - onClose() - }} - > - {open && ( - - )} - - ) -} - -export default AccessConfigModal diff --git a/web/app/components/access-rules-editor/index.tsx b/web/app/components/access-rules-editor/index.tsx index 82a11ae4c5..1e1249ccab 100644 --- a/web/app/components/access-rules-editor/index.tsx +++ b/web/app/components/access-rules-editor/index.tsx @@ -1,97 +1,119 @@ 'use client' -import type { - AccessRule, - AssignedRole, -} from '@/app/components/header/account-setting/access-rules-page/access-rule-row' +import type { AccessPolicyWithBindings, RemoveBindingPayload } from '@/models/access-control' import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' 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 { useParams } from '@/next/navigation' +import { useUpdateAppAccessRuleBindings } from '@/service/access-control/use-app-access-config' +import { useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use-dataset-access-config' 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 + rules: AccessPolicyWithBindings[] className?: string } const AccessRulesEditor = ({ - rules: rulesProp, - onRulesChange, + rules, className, }: AccessRulesEditorProps) => { - const isControlled = typeof onRulesChange === 'function' - const [internalRules, setInternalRules] = useState(rulesProp) - const rules = isControlled ? rulesProp : internalRules + const { appId } = useParams() as { appId: string } + const [currentRule, setCurrentRule] = useState(null) - 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 handleAddRole = useCallback((rule: AccessPolicyWithBindings) => { + setCurrentRule(rule) }, []) const handleCloseAddModal = useCallback(() => { - setAddingRule(null) + setCurrentRule(null) }, []) + const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings() + const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings() + const handleAddSubmit = useCallback( - (_selection: { roleIds: string[], memberIds: string[] }) => { - // TODO: wire up to API when backend is ready. + (selection: { roleIds: string[], memberIds: string[] }) => { + const { policy } = currentRule || {} + const { id: policyId, resource_type } = policy || {} + if (resource_type === 'app') { + updateAppAccessRuleBindings({ + appId, + policyId: policyId || '', + role_ids: selection.roleIds, + account_ids: selection.memberIds, + }, { + onSuccess: () => { + toast.success('Rule binding updated successfully') + }, + }) + } + else if (resource_type === 'dataset') { + updateDatasetAccessRuleBindings({ + datasetId: appId, + policyId: policyId || '', + role_ids: selection.roleIds, + account_ids: selection.memberIds, + }, { + onSuccess: () => { + toast.success('Rule binding updated successfully') + }, + }) + } }, - [], + [appId, currentRule, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings], ) 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, - ), - ) + (payload: RemoveBindingPayload) => { + const { policy_id, role_ids, account_ids, resource_type } = payload + if (resource_type === 'app') { + updateAppAccessRuleBindings({ + appId, + policyId: policy_id, + role_ids, + account_ids, + }, { + onSuccess: () => { + toast.success('Rule binding removed successfully') + }, + }) + } + else if (resource_type === 'dataset') { + updateDatasetAccessRuleBindings({ + datasetId: appId, + policyId: policy_id, + role_ids, + account_ids, + }, { + onSuccess: () => { + toast.success('Rule binding removed successfully') + }, + }) + } }, - [updateRules], + [appId, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings], ) return (
{rules.map((rule, index) => ( 0 && 'border-t border-divider-subtle')} /> ))} - {addingRule && ( + {currentRule && ( role.id)} - initialMemberIds={[]} + ruleName={currentRule.policy.name} + initialRoleIds={currentRule.roles.map(role => role.role_id)} + initialMemberIds={currentRule.accounts.map(account => account.account_id)} onClose={handleCloseAddModal} onSubmit={handleAddSubmit} /> diff --git a/web/app/components/app/access-config/index.tsx b/web/app/components/app/access-config/index.tsx index a7dd2cc0dd..ac1d6052ff 100644 --- a/web/app/components/app/access-config/index.tsx +++ b/web/app/components/app/access-config/index.tsx @@ -1,61 +1,18 @@ '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: [], - }, -] +import { useAppAccessRules } from '@/service/access-control/use-app-access-config' type AppAccessConfigPageProps = { appId: string } -const AppAccessConfigPage = ({ appId: _appId }: AppAccessConfigPageProps) => { +const AppAccessConfigPage = ({ appId }: AppAccessConfigPageProps) => { + const { data: appAccessRulesResponse } = useAppAccessRules(appId) + + const appAccessRules = appAccessRulesResponse?.items || [] + return ( {

Access Config

- +
diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index 5d32eef6d2..aac97bc5db 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -4,7 +4,6 @@ import type { App } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' -import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -79,7 +78,7 @@ export default function AccessControl(props: AccessControlProps) {
- +

{t('accessControlDialog.accessItems.organization', { ns: 'app' })}

@@ -90,7 +89,7 @@ export default function AccessControl(props: AccessControlProps) {
- +

{t('accessControlDialog.accessItems.external', { ns: 'app' })}

{!hideTip && } @@ -98,7 +97,7 @@ export default function AccessControl(props: AccessControlProps) {
- +

{t('accessControlDialog.accessItems.anyone', { ns: 'app' })}

diff --git a/web/app/components/apps/app-access-config-modal/index.tsx b/web/app/components/apps/app-access-config-modal/index.tsx deleted file mode 100644 index 528a7f4be6..0000000000 --- a/web/app/components/apps/app-access-config-modal/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -'use client' - -import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row' -import type { App } from '@/types/app' -import AccessConfigModal from '@/app/components/access-config-modal' - -// TODO: replace with the per-app access rules fetched from the access-rules API -// once available. The catalog mirrors the workspace-level App access rules and -// adds app-specific rules that can only be assigned per-app. -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: 'marketing-lead', name: 'Marketing Lead' }, - { id: 'kb-admin', name: 'KB 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: 'owner', name: 'Owner' }, - { id: 'admin', name: 'Admin' }, - { id: 'marketing-lead', name: 'Marketing Lead' }, - { id: 'kb-admin', name: 'KB Admin' }, - { id: 'app-admin', name: 'App Admin' }, - { id: 'executive', name: 'Executive' }, - ], - 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: 'owner', name: 'Owner' }, - { id: 'admin', name: 'Admin' }, - { id: 'marketing-lead', name: 'Marketing Lead' }, - { id: 'kb-admin', name: 'KB Admin' }, - { id: 'app-admin', name: 'App Admin' }, - { id: 'executive', name: 'Executive' }, - ], - 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: 'owner', name: 'Owner' }, - { id: 'admin', name: 'Admin' }, - { id: 'marketing-lead', name: 'Marketing Lead' }, - { id: 'kb-admin', name: 'KB Admin' }, - { id: 'app-admin', name: 'App Admin' }, - { id: 'executive', name: 'Executive' }, - ], - permissions: [], - }, - { - id: 'app-can-optimize-prompt', - name: 'Can optimize prompt', - description: 'Dedicated prompt optimization access.', - assignedRoles: [ - { id: 'owner', name: 'Owner' }, - { id: 'admin', name: 'Admin' }, - { id: 'marketing-lead', name: 'Marketing Lead' }, - { id: 'kb-admin', name: 'KB Admin' }, - { id: 'app-admin', name: 'App Admin' }, - { id: 'executive', name: 'Executive' }, - ], - permissions: [], - }, -] - -export type AppAccessConfigModalProps = { - open: boolean - app: Pick - onClose: () => void - onSave?: (rules: AccessRule[]) => void -} - -const AppAccessConfigModal = ({ - open, - app: _app, - onClose, - onSave, -}: AppAccessConfigModalProps) => { - return ( - - ) -} - -export default AppAccessConfigModal diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index d18970b35f..10f7a18111 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -71,9 +71,6 @@ const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/ds const AccessControl = dynamic(() => import('@/app/components/app/app-access-control'), { ssr: false, }) -const AppAccessConfigModal = dynamic(() => import('@/app/components/apps/app-access-config-modal'), { - ssr: false, -}) type AppCardProps = { app: App @@ -93,7 +90,6 @@ type AppCardOperationsMenuProps = { onSwitch: () => void onDelete: () => void onAccessControl: () => void - onAccessConfig: () => void } const AppCardOperationsMenu: React.FC = ({ @@ -107,10 +103,10 @@ const AppCardOperationsMenu: React.FC = ({ onSwitch, onDelete, onAccessControl, - onAccessConfig, }) => { const { t } = useTranslation() const openAsyncWindow = useAsyncWindowOpen() + const { push } = useRouter() const handleMenuAction = useCallback((e: React.MouseEvent, action: () => void) => { e.stopPropagation() @@ -139,6 +135,11 @@ const AppCardOperationsMenu: React.FC = ({ } }, [app.id, openAsyncWindow]) + const handleOpenAccessConfig = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + push(`/app/${app.id}/access-config`) + }, [app.id, push]) + return ( <> handleMenuAction(e, onEdit)}> @@ -176,7 +177,7 @@ const AppCardOperationsMenu: React.FC = ({ )} - handleMenuAction(e, onAccessConfig)}> + Access Config @@ -230,7 +231,6 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () => const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [confirmDeleteInput, setConfirmDeleteInput] = useState('') const [showAccessControl, setShowAccessControl] = useState(false) - const [showAccessConfig, setShowAccessConfig] = useState(false) const [isOperationsMenuOpen, setIsOperationsMenuOpen] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation() @@ -303,13 +303,6 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () => }) }, []) - const handleShowAccessConfig = useCallback(() => { - setIsOperationsMenuOpen(false) - queueMicrotask(() => { - setShowAccessConfig(true) - }) - }, []) - const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, icon_type, @@ -698,13 +691,6 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () => {showAccessControl && ( setShowAccessControl(false)} /> )} - {showAccessConfig && ( - setShowAccessConfig(false)} - /> - )} ) } diff --git a/web/app/components/datasets/access-config/index.tsx b/web/app/components/datasets/access-config/index.tsx index a5c75bf738..34786dbd4e 100644 --- a/web/app/components/datasets/access-config/index.tsx +++ b/web/app/components/datasets/access-config/index.tsx @@ -1,70 +1,18 @@ '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: [], - }, -] +import { useDatasetAccessRules } from '@/service/access-control/use-dataset-access-config' type DatasetAccessConfigPageProps = { datasetId: string } -const DatasetAccessConfigPage = ({ datasetId: _datasetId }: DatasetAccessConfigPageProps) => { +const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) => { + const { data: datasetAccessRulesResponse } = useDatasetAccessRules(datasetId) + + const datasetAccessRules = datasetAccessRulesResponse?.items || [] + return (

Access Config

- +
diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx index dce7e9af54..8dd82ec195 100644 --- a/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx +++ b/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx @@ -1,13 +1,8 @@ 'use client' -import type { AccessPolicyWithBindings, BindingType } from '@/models/access-control' +import type { AccessPolicyWithBindings, BindingType, RemoveBindingPayload } from '@/models/access-control' import { cn } from '@langgenius/dify-ui/cn' -import { toast } from '@langgenius/dify-ui/toast' import { memo, useCallback } from 'react' -import { - useUpdateAppAccessRuleBindings, - useUpdateDatasetAccessRuleBindings, -} from '@/service/access-control/use-workspace-access-rules' import AccessRuleRowMenu from './access-rule-row-menu' import RoleTag from './role-tag' @@ -17,6 +12,7 @@ export type AccessRuleRowProps = { showMenu?: boolean onEdit?: (rule: AccessPolicyWithBindings) => void onAddRole?: (rule: AccessPolicyWithBindings) => void + onRemove?: (payload: RemoveBindingPayload) => void } const AccessRuleRow = ({ @@ -25,6 +21,7 @@ const AccessRuleRow = ({ showMenu = true, onEdit, onAddRole, + onRemove, }: AccessRuleRowProps) => { const { policy, roles, accounts } = rule const { id: policyId, resource_type } = policy @@ -32,12 +29,13 @@ const AccessRuleRow = ({ const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule]) const handleAddRole = useCallback(() => onAddRole?.(rule), [onAddRole, rule]) - const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings() - const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings() + const handleRemove = useCallback((id: string, type: BindingType) => { + if (!onRemove) + return - const handleRemoveRole = useCallback((id: string, type: BindingType) => { - const payload = { - id: policyId, + const payload: RemoveBindingPayload = { + policy_id: policyId, + resource_type, role_ids: roles.map(role => role.role_id), account_ids: accounts.map(account => account.account_id), } @@ -47,21 +45,8 @@ const AccessRuleRow = ({ else if (type === 'account') { payload.account_ids = payload.account_ids.filter(accountId => accountId !== id) } - if (resource_type === 'app') { - updateAppAccessRuleBindings(payload, { - onSuccess: () => { - toast.success('Access rule updated successfully') - }, - }) - } - else if (resource_type === 'dataset') { - updateDatasetAccessRuleBindings(payload, { - onSuccess: () => { - toast.success('Access rule updated successfully') - }, - }) - } - }, [accounts, policyId, resource_type, roles, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings]) + onRemove(payload) + }, [accounts, onRemove, policyId, resource_type, roles]) return (
@@ -79,7 +64,7 @@ const AccessRuleRow = ({ id={role.role_id} label={role.role_name} type="role" - onRemove={handleRemoveRole} + onRemove={handleRemove} /> ))} {accounts.map(account => ( @@ -88,7 +73,7 @@ const AccessRuleRow = ({ id={account.account_id} label={account.account_name} type="account" - onRemove={handleRemoveRole} + onRemove={handleRemove} /> ))}