'use client' import type { AccessPolicyWithBindings, ResourceUserAccessSetting } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' import { Checkbox } from '@langgenius/dify-ui/checkbox' import { cn } from '@langgenius/dify-ui/cn' import { Pagination } from '@langgenius/dify-ui/pagination' import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { RESOURCE_ACCESS_SETTINGS_PAGE_SIZE_OPTIONS } from '@/service/access-control/constants' import AddAccessSubjectPopover from './add-access-subject-popover' import AutomaticIncludeWorkspaceMembersSection from './automatic-include-workspace-members-section' import AccessRulesBatchAction from './batch-action' import { ACCESS_RULE_TABLE_GRID, DEFAULT_ACCESS_POLICY_ID } from './constants' import UserAccessPolicyRow from './user-access-policy-row' export type AccessPolicyMemberBindingRemoval = { accessPolicyId: string accountIds: string[] } export type AccessRulesEditorProps = { rules: AccessPolicyWithBindings[] userAccessSettings: ResourceUserAccessSetting[] isLoadingRules: boolean isLoadingUserAccessSettings: boolean automaticIncludeWorkspaceMembers?: boolean isUpdatingAutomaticIncludeWorkspaceMembers: boolean existingAccountIds?: string[] currentPage?: number pageSize?: number totalCount?: number totalPages?: number isChangingPage?: boolean updatingAccountId: string | null maintainerId?: string | null className?: string onAutomaticIncludeWorkspaceMembersChange?: (checked: boolean) => void onPageChange?: (page: number) => void onPageSizeChange?: (pageSize: number) => void onUserAccessPoliciesChange?: (accountId: string, accessPolicyIds: string[]) => void onRemoveAccessPolicyMemberBinding?: (accountId: string, accessPolicyId: string) => void onBatchRemoveAccessPolicyMemberBindings?: ( removals: AccessPolicyMemberBindingRemoval[], ) => Promise onAddAccessSubject?: (accountId: string, accessPolicyIds: string[]) => void } function AccessRulesEditor({ rules, userAccessSettings, isLoadingRules, isLoadingUserAccessSettings, automaticIncludeWorkspaceMembers, isUpdatingAutomaticIncludeWorkspaceMembers, existingAccountIds, currentPage = 1, pageSize, totalCount, totalPages = 0, isChangingPage = false, updatingAccountId, maintainerId, className, onAutomaticIncludeWorkspaceMembersChange, onPageChange, onPageSizeChange, onUserAccessPoliciesChange, onRemoveAccessPolicyMemberBinding, onBatchRemoveAccessPolicyMemberBindings, onAddAccessSubject, }: AccessRulesEditorProps) { const { t } = useTranslation() const [selectedAccountIds, setSelectedAccountIds] = useState>(() => new Set()) const isLoading = isLoadingRules || isLoadingUserAccessSettings const shouldCenterTableBody = isLoading || userAccessSettings.length === 0 const areMembershipChangesDisabled = automaticIncludeWorkspaceMembers === true const policyOptions = useMemo(() => { return rules.map((rule) => ({ id: rule.policy.id, name: rule.policy.name, })) }, [rules]) const protectedAccountIds = useMemo(() => { const accountIds = new Set() for (const setting of userAccessSettings) { const accountId = setting.account.account_id const isWorkspaceOwner = setting.roles.some((role) => role.role_tag === 'owner') if (accountId === maintainerId || isWorkspaceOwner) accountIds.add(accountId) } return accountIds }, [maintainerId, userAccessSettings]) const existingOrProtectedAccountIds = useMemo(() => { if (existingAccountIds === undefined) return undefined return Array.from(new Set([...existingAccountIds, ...protectedAccountIds])) }, [existingAccountIds, protectedAccountIds]) const selectableAccountIds = useMemo( () => userAccessSettings .map((setting) => setting.account.account_id) .filter((accountId) => !protectedAccountIds.has(accountId)), [protectedAccountIds, userAccessSettings], ) const selectedAccountCount = selectableAccountIds.filter((accountId) => selectedAccountIds.has(accountId), ).length const areAllAccountsSelected = selectableAccountIds.length > 0 && selectedAccountCount === selectableAccountIds.length const areSomeAccountsSelected = selectedAccountCount > 0 && !areAllAccountsSelected const showPagination = totalPages > 0 && !!onPageChange const selectedBindingRemovals = useMemo(() => { const accountIdsByAccessPolicyId = new Map() for (const setting of userAccessSettings) { const accountId = setting.account.account_id if (!selectedAccountIds.has(accountId) || protectedAccountIds.has(accountId)) continue const accessPolicyId = setting.access_policies[0]?.id ?? DEFAULT_ACCESS_POLICY_ID const accountIds = accountIdsByAccessPolicyId.get(accessPolicyId) if (accountIds) accountIds.push(accountId) else accountIdsByAccessPolicyId.set(accessPolicyId, [accountId]) } return Array.from(accountIdsByAccessPolicyId, ([accessPolicyId, accountIds]) => ({ accessPolicyId, accountIds, })) }, [protectedAccountIds, selectedAccountIds, userAccessSettings]) const handleSelectAllAccounts = useCallback( (selected: boolean) => { setSelectedAccountIds((current) => { const next = new Set(current) for (const accountId of selectableAccountIds) { if (selected) next.add(accountId) else next.delete(accountId) } return next }) }, [selectableAccountIds], ) const handleAccountSelectedChange = useCallback((accountId: string, selected: boolean) => { setSelectedAccountIds((current) => { const next = new Set(current) if (selected) next.add(accountId) else next.delete(accountId) return next }) }, []) const handleRemoveAccessPolicyMemberBinding = useCallback( (accountId: string, accessPolicyId: string) => { setSelectedAccountIds((current) => { const next = new Set(current) next.delete(accountId) return next }) onRemoveAccessPolicyMemberBinding?.(accountId, accessPolicyId) }, [onRemoveAccessPolicyMemberBinding], ) const handleAutomaticIncludeWorkspaceMembersChange = useCallback( (checked: boolean) => { if (checked) setSelectedAccountIds(new Set()) onAutomaticIncludeWorkspaceMembersChange?.(checked) }, [onAutomaticIncludeWorkspaceMembersChange], ) const handlePageChange = useCallback( (page: number) => { setSelectedAccountIds(new Set()) onPageChange?.(page) }, [onPageChange], ) const handlePageSizeChange = useCallback( (nextPageSize: number) => { setSelectedAccountIds(new Set()) onPageSizeChange?.(nextPageSize) }, [onPageSizeChange], ) const handleBatchRemoveAccessPolicyMemberBindings = useCallback(async () => { if ( areMembershipChangesDisabled || !onBatchRemoveAccessPolicyMemberBindings || selectedBindingRemovals.length === 0 ) return await onBatchRemoveAccessPolicyMemberBindings(selectedBindingRemovals) setSelectedAccountIds(new Set()) }, [ areMembershipChangesDisabled, onBatchRemoveAccessPolicyMemberBindings, selectedBindingRemovals, ]) return (

{t(($) => $['accessRule.allowedMembers'], { ns: 'permission' })}

{totalCount ?? 0}
{onAddAccessSubject ? ( ) : ( )}
$['accessRule.allowedMembers'], { ns: 'permission', })} className="flex min-h-0 w-full flex-1 flex-col" > {isLoading ? ( ) : userAccessSettings.length === 0 ? ( ) : ( userAccessSettings.map((setting, index) => ( 0 && 'border-t border-divider-subtle')} onSelectedChange={handleAccountSelectedChange} onChange={onUserAccessPoliciesChange} onRemove={handleRemoveAccessPolicyMemberBinding} /> )) )}
$['operation.selectAll'], { ns: 'common' })} checked={areAllAccountsSelected} indeterminate={areSomeAccountsSelected} disabled={ isChangingPage || areMembershipChangesDisabled || !onBatchRemoveAccessPolicyMemberBindings || selectableAccountIds.length === 0 } onCheckedChange={handleSelectAllAccounts} /> {t(($) => $['accessRule.collaborator'], { ns: 'permission' })} {t(($) => $['accessRule.accessPermission'], { ns: 'permission' })} {t(($) => $['accessRule.actions'], { ns: 'permission' })}
{t(($) => $['accessRule.noUserAccessSettings'], { ns: 'permission' })}
{showPagination ? ( $['pagination.previous'], { ns: 'common' }), next: t(($) => $['pagination.next'], { ns: 'common' }), editPageNumber: (page, pageCount) => t(($) => $['pagination.editPageNumber'], { ns: 'common', page, totalPages: pageCount, }), pageNumberInput: t(($) => $['pagination.pageNumber'], { ns: 'common' }), }} pageSize={ pageSize !== undefined && onPageSizeChange ? { value: pageSize, options: RESOURCE_ACCESS_SETTINGS_PAGE_SIZE_OPTIONS, onValueChange: handlePageSizeChange, label: t(($) => $['pagination.perPage'], { ns: 'common' }), ariaLabel: t(($) => $['pagination.perPage'], { ns: 'common' }), } : undefined } /> ) : null}
{selectedAccountCount > 0 && !areMembershipChangesDisabled && onBatchRemoveAccessPolicyMemberBindings ? ( setSelectedAccountIds(new Set())} /> ) : null}
) } export default memo(AccessRulesEditor)