mirror of
https://github.com/langgenius/dify.git
synced 2026-05-22 01:48:39 +08:00
feat: enhance access rule management with view functionality and permission modal updates
This commit is contained in:
@ -12,11 +12,13 @@ import { useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use
|
||||
|
||||
export type AccessRulesEditorProps = {
|
||||
rules: AccessPolicyWithBindings[]
|
||||
canManage: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccessRulesEditor = ({
|
||||
rules,
|
||||
canManage,
|
||||
className,
|
||||
}: AccessRulesEditorProps) => {
|
||||
const { appId } = useParams() as { appId: string }
|
||||
@ -102,6 +104,7 @@ const AccessRulesEditor = ({
|
||||
<AccessRuleRow
|
||||
key={rule.policy.id}
|
||||
rule={rule}
|
||||
canManage={canManage}
|
||||
showMenu={false}
|
||||
onAddRole={handleAddRole}
|
||||
onRemove={handleRemoveRole}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useMemo } from 'react'
|
||||
import AccessRulesEditor from '@/app/components/access-rules-editor'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useAppAccessRules } from '@/service/access-control/use-app-access-config'
|
||||
import { getAppACLCapabilities } from '@/utils/permission'
|
||||
|
||||
type AppAccessConfigPageProps = {
|
||||
appId: string
|
||||
@ -10,6 +13,11 @@ type AppAccessConfigPageProps = {
|
||||
|
||||
const AppAccessConfigPage = ({ appId }: AppAccessConfigPageProps) => {
|
||||
const { data: appAccessRulesResponse } = useAppAccessRules(appId)
|
||||
const appPermissionKeys = useAppStore(state => state.appDetail?.permission_keys)
|
||||
const appACLCapabilities = useMemo(
|
||||
() => getAppACLCapabilities(appPermissionKeys),
|
||||
[appPermissionKeys],
|
||||
)
|
||||
|
||||
const appAccessRules = appAccessRulesResponse?.items || []
|
||||
|
||||
@ -21,7 +29,7 @@ const AppAccessConfigPage = ({ appId }: AppAccessConfigPageProps) => {
|
||||
<div className="w-full px-16 py-8">
|
||||
<h1 className="title-2xl-semi-bold text-text-primary">Access Config</h1>
|
||||
<div className="mt-6">
|
||||
<AccessRulesEditor rules={appAccessRules} />
|
||||
<AccessRulesEditor rules={appAccessRules} canManage={appACLCapabilities.canAccessConfig} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useMemo } from 'react'
|
||||
import AccessRulesEditor from '@/app/components/access-rules-editor'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDatasetAccessRules } from '@/service/access-control/use-dataset-access-config'
|
||||
import { getDatasetACLCapabilities } from '@/utils/permission'
|
||||
|
||||
type DatasetAccessConfigPageProps = {
|
||||
datasetId: string
|
||||
@ -10,6 +13,11 @@ type DatasetAccessConfigPageProps = {
|
||||
|
||||
const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) => {
|
||||
const { data: datasetAccessRulesResponse } = useDatasetAccessRules(datasetId)
|
||||
const datasetPermissionKeys = useDatasetDetailContextWithSelector(state => state.dataset?.permission_keys)
|
||||
const datasetACLCapabilities = useMemo(
|
||||
() => getDatasetACLCapabilities(datasetPermissionKeys),
|
||||
[datasetPermissionKeys],
|
||||
)
|
||||
|
||||
const datasetAccessRules = datasetAccessRulesResponse?.items || []
|
||||
|
||||
@ -21,7 +29,7 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) =>
|
||||
<div className="px-12 py-8">
|
||||
<h1 className="title-2xl-semi-bold text-text-primary">Access Config</h1>
|
||||
<div className="mt-6">
|
||||
<AccessRulesEditor rules={datasetAccessRules} />
|
||||
<AccessRulesEditor rules={datasetAccessRules} canManage={datasetACLCapabilities.canAccessConfig} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -50,7 +50,7 @@ const DocumentList = ({
|
||||
}: DocumentListProps) => {
|
||||
const { t } = useTranslation()
|
||||
const datasetConfig = useDatasetDetailContext(s => s.dataset)
|
||||
const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(datasetConfig?.permission_keys), [datasetConfig?.permission_keys])
|
||||
const datasetACLCapabilities = useMemo(() => getDatasetACLCapabilities(datasetConfig?.permission_keys), [datasetConfig?.permission_keys])
|
||||
const chunkingMode = datasetConfig?.doc_form
|
||||
const isGeneralMode = chunkingMode !== ChunkingMode.parentChild
|
||||
const isQAMode = chunkingMode === ChunkingMode.qa
|
||||
|
||||
@ -24,11 +24,13 @@ import { useCopyAccessRule, useDeleteAccessRule } from '@/service/access-control
|
||||
|
||||
export type AccessRuleRowMenuProps = {
|
||||
rule: AccessPolicy
|
||||
onView?: () => void
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
const AccessRuleRowMenu = ({
|
||||
rule,
|
||||
onView,
|
||||
onEdit,
|
||||
}: AccessRuleRowMenuProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -37,6 +39,10 @@ const AccessRuleRowMenu = ({
|
||||
const { mutateAsync: copyAccessRule } = useCopyAccessRule(rule.resource_type)
|
||||
const { mutateAsync: deleteAccessRule, isPending: isDeletingAccessRule } = useDeleteAccessRule(rule.resource_type)
|
||||
|
||||
const handleView = useCallback(() => {
|
||||
onView?.()
|
||||
}, [onView])
|
||||
|
||||
const handleCopyRules = useCallback(() => {
|
||||
copyAccessRule(rule.id, {
|
||||
onSuccess: () => {
|
||||
@ -60,6 +66,8 @@ const AccessRuleRowMenu = ({
|
||||
})
|
||||
}, [deleteAccessRule, rule.id])
|
||||
|
||||
const isBuiltIn = rule.is_builtin
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
@ -79,26 +87,41 @@ const AccessRuleRowMenu = ({
|
||||
sideOffset={4}
|
||||
popupClassName="min-w-[140px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="system-sm-semibold text-text-secondary"
|
||||
onClick={onEdit}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
{isBuiltIn
|
||||
? (
|
||||
<DropdownMenuItem
|
||||
className="system-sm-semibold text-text-secondary"
|
||||
onClick={handleView}
|
||||
>
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
: (
|
||||
<DropdownMenuItem
|
||||
className="system-sm-semibold text-text-secondary"
|
||||
onClick={onEdit}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="system-sm-semibold text-text-secondary"
|
||||
onClick={handleCopyRules}
|
||||
>
|
||||
Copy
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="system-sm-semibold"
|
||||
onClick={openDeleteConfirm}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
{!isBuiltIn && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="system-sm-semibold"
|
||||
onClick={openDeleteConfirm}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && setShowDeleteConfirm(false)}>
|
||||
|
||||
@ -11,6 +11,7 @@ export type AccessRuleRowProps = {
|
||||
canManage: boolean
|
||||
className?: string
|
||||
showMenu?: boolean
|
||||
onView?: (rule: AccessPolicyWithBindings) => void
|
||||
onEdit?: (rule: AccessPolicyWithBindings) => void
|
||||
onAddRole?: (rule: AccessPolicyWithBindings) => void
|
||||
onRemove?: (payload: RemoveBindingPayload) => void
|
||||
@ -21,6 +22,7 @@ const AccessRuleRow = ({
|
||||
canManage,
|
||||
className,
|
||||
showMenu = true,
|
||||
onView,
|
||||
onEdit,
|
||||
onAddRole,
|
||||
onRemove,
|
||||
@ -28,6 +30,7 @@ const AccessRuleRow = ({
|
||||
const { policy, roles, accounts } = rule
|
||||
const { id: policyId, resource_type } = policy
|
||||
|
||||
const handleView = useCallback(() => onView?.(rule), [onView, rule])
|
||||
const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule])
|
||||
const handleAddRole = useCallback(() => onAddRole?.(rule), [onAddRole, rule])
|
||||
|
||||
@ -95,6 +98,7 @@ const AccessRuleRow = ({
|
||||
</div>
|
||||
{showMenu && canManage && (
|
||||
<AccessRuleRowMenu
|
||||
onView={handleView}
|
||||
onEdit={handleEdit}
|
||||
rule={policy}
|
||||
/>
|
||||
|
||||
@ -13,6 +13,7 @@ type AccessRuleSectionProps = {
|
||||
rules: AccessPolicyWithBindings[]
|
||||
isLoadingRules: boolean
|
||||
onCreate?: () => void
|
||||
onViewRule?: (rule: AccessPolicyWithBindings) => void
|
||||
onEditRule?: (rule: AccessPolicyWithBindings) => void
|
||||
onAddRole?: (rule: AccessPolicyWithBindings) => void
|
||||
onRemoveBinding?: (payload: RemoveBindingPayload) => void
|
||||
@ -24,6 +25,7 @@ const AccessRuleSection = ({
|
||||
rules,
|
||||
isLoadingRules,
|
||||
onCreate,
|
||||
onViewRule,
|
||||
onEditRule,
|
||||
onAddRole,
|
||||
onRemoveBinding,
|
||||
@ -57,6 +59,7 @@ const AccessRuleSection = ({
|
||||
rule={rule}
|
||||
canManage={canManage}
|
||||
className={cn(index > 0 && 'border-t border-divider-subtle')}
|
||||
onView={onViewRule}
|
||||
onEdit={onEditRule}
|
||||
onAddRole={onAddRole}
|
||||
onRemove={onRemoveBinding}
|
||||
|
||||
@ -50,6 +50,18 @@ const AppAccessRuleSection = ({
|
||||
setPermissionSetModalState({ mode: 'create' })
|
||||
}, [])
|
||||
|
||||
const handleView = useCallback((rule: AccessPolicyWithBindings) => {
|
||||
const { policy } = rule
|
||||
setPermissionSetModalState({
|
||||
mode: 'view',
|
||||
initialValues: {
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
permissionKeys: policy.permission_keys,
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleEdit = useCallback((rule: AccessPolicyWithBindings) => {
|
||||
const { policy } = rule
|
||||
setPermissionSetModalState({
|
||||
@ -136,6 +148,7 @@ const AppAccessRuleSection = ({
|
||||
rules={appAccessRules}
|
||||
isLoadingRules={isLoading}
|
||||
onCreate={handleCreate}
|
||||
onViewRule={handleView}
|
||||
onEditRule={handleEdit}
|
||||
onAddRole={handleAddRole}
|
||||
onRemoveBinding={handleRemoveBinding}
|
||||
|
||||
@ -50,6 +50,18 @@ const DatasetAccessRuleSection = ({
|
||||
setPermissionSetModalState({ mode: 'create' })
|
||||
}, [])
|
||||
|
||||
const handleView = useCallback((rule: AccessPolicyWithBindings) => {
|
||||
const { policy } = rule
|
||||
setPermissionSetModalState({
|
||||
mode: 'view',
|
||||
initialValues: {
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
permissionKeys: policy.permission_keys,
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleEdit = useCallback((rule: AccessPolicyWithBindings) => {
|
||||
const { policy } = rule
|
||||
setPermissionSetModalState({
|
||||
@ -136,6 +148,7 @@ const DatasetAccessRuleSection = ({
|
||||
rules={datasetAccessRules}
|
||||
isLoadingRules={isLoading}
|
||||
onCreate={handleCreate}
|
||||
onViewRule={handleView}
|
||||
onEditRule={handleEdit}
|
||||
onAddRole={handleAddRole}
|
||||
onRemoveBinding={handleRemoveBinding}
|
||||
|
||||
@ -14,7 +14,7 @@ import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import PermissionPicker from './permission-picker'
|
||||
|
||||
export type PermissionSetModalMode = 'create' | 'edit'
|
||||
export type PermissionSetModalMode = 'create' | 'edit' | 'view'
|
||||
|
||||
export type PermissionSetFormValues = {
|
||||
name: string
|
||||
@ -37,11 +37,13 @@ const RESOURCE_LABEL: Record<AccessPolicyResourceType, string> = {
|
||||
}
|
||||
|
||||
const buildTitle = (mode: PermissionSetModalMode, resource: AccessPolicyResourceType): string => {
|
||||
const verb = mode === 'create' ? 'Create' : 'Edit'
|
||||
const verb = mode === 'create' ? 'Create' : mode === 'edit' ? 'Edit' : 'View'
|
||||
return `${verb} ${RESOURCE_LABEL[resource]} permission set`
|
||||
}
|
||||
|
||||
const buildDescription = (mode: PermissionSetModalMode, resource: AccessPolicyResourceType): string => {
|
||||
if (mode === 'view')
|
||||
return 'View the name, description, and permissions granted for this permission set.'
|
||||
if (mode === 'edit')
|
||||
return 'Modify the name, description, and permissions granted for this permission set.'
|
||||
if (resource === 'app')
|
||||
@ -63,10 +65,11 @@ const PermissionSetModalBody = ({
|
||||
const [permissionKeys, setPermissionKeys] = useState<string[]>(initialValues?.permissionKeys ?? [])
|
||||
|
||||
const trimmedName = name.trim()
|
||||
const readonly = mode === 'view'
|
||||
const canSubmit = trimmedName.length > 0
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!canSubmit)
|
||||
if (readonly || !canSubmit)
|
||||
return
|
||||
onSubmit({
|
||||
name: trimmedName,
|
||||
@ -106,6 +109,7 @@ const PermissionSetModalBody = ({
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="e.g. Can export DSL"
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -119,6 +123,7 @@ const PermissionSetModalBody = ({
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Describe what this permission set grants"
|
||||
className="min-h-20 resize-none"
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -128,6 +133,7 @@ const PermissionSetModalBody = ({
|
||||
resourceType={resourceType}
|
||||
value={permissionKeys}
|
||||
onChange={setPermissionKeys}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,15 +150,17 @@ const PermissionSetModalBody = ({
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!canSubmit}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Confirm
|
||||
{readonly ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
{!readonly && (
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!canSubmit}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user