feat: add access rules management components including AccessRuleRow, AccessRuleSection, and AccessRulesPage

This commit is contained in:
twwu 2026-04-27 15:35:35 +08:00
parent 5907b3f809
commit 12b93290fa
8 changed files with 380 additions and 19 deletions

View File

@ -0,0 +1,69 @@
'use client'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useState } from 'react'
import ActionButton from '@/app/components/base/action-button'
export type AccessRuleRowMenuProps = {
onEdit?: () => void
onCopy?: () => void
onDelete?: () => void
}
const AccessRuleRowMenu = ({
onEdit,
onCopy,
onDelete,
}: AccessRuleRowMenuProps) => {
const [open, setOpen] = useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={(
<ActionButton
size="l"
className={open ? 'bg-state-base-hover' : ''}
aria-label="More actions"
/>
)}
>
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-end"
sideOffset={4}
popupClassName="min-w-[140px]"
>
<DropdownMenuItem
className="system-sm-semibold text-text-secondary"
onClick={onEdit}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem
className="system-sm-semibold text-text-secondary"
onClick={onCopy}
>
Copy
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
className="system-sm-semibold"
onClick={onDelete}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
export default AccessRuleRowMenu

View File

@ -0,0 +1,81 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { memo, useCallback } from 'react'
import AccessRuleRowMenu from './access-rule-row-menu'
import RoleTag from './role-tag'
export type AssignedRole = {
id: string
name: string
}
export type AccessRule = {
id: string
name: string
description: string
assignedRoles: AssignedRole[]
}
export type AccessRuleRowProps = {
rule: AccessRule
className?: string
onEdit?: (rule: AccessRule) => void
onCopy?: (rule: AccessRule) => void
onDelete?: (rule: AccessRule) => void
onAddRole?: (rule: AccessRule) => void
onRemoveRole?: (rule: AccessRule, role: AssignedRole) => void
}
const AccessRuleRow = ({
rule,
className,
onEdit,
onCopy,
onDelete,
onAddRole,
onRemoveRole,
}: AccessRuleRowProps) => {
const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule])
const handleCopy = useCallback(() => onCopy?.(rule), [onCopy, rule])
const handleDelete = useCallback(() => onDelete?.(rule), [onDelete, rule])
const handleAddRole = useCallback(() => onAddRole?.(rule), [onAddRole, rule])
return (
<div className={cn('flex items-start gap-2 py-3.5', className)}>
<div className="min-w-0 flex-1">
<div className="system-sm-semibold text-text-secondary">
{rule.name}
</div>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{rule.description}
</p>
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{rule.assignedRoles.map(role => (
<RoleTag
key={role.id}
label={role.name}
onRemove={onRemoveRole ? () => onRemoveRole(rule, role) : undefined}
/>
))}
<button
type="button"
onClick={handleAddRole}
className="inline-flex h-6 items-center gap-0.5 rounded-md border border-divider-deep px-1.5 system-xs-medium text-text-tertiary hover:border-divider-solid hover:text-text-secondary"
aria-label={`Add role to ${rule.name}`}
>
<span aria-hidden className="i-ri-add-line h-3 w-3" />
Add
</button>
</div>
</div>
<AccessRuleRowMenu
onEdit={handleEdit}
onCopy={handleCopy}
onDelete={handleDelete}
/>
</div>
)
}
export default memo(AccessRuleRow)

View File

@ -0,0 +1,62 @@
'use client'
import type { AccessRule, AssignedRole } from './access-rule-row'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { memo } from 'react'
import AccessRuleRow from './access-rule-row'
export type AccessRuleSectionProps = {
title: string
rules: AccessRule[]
createButtonLabel: string
onCreate?: () => void
onEditRule?: (rule: AccessRule) => void
onCopyRule?: (rule: AccessRule) => void
onDeleteRule?: (rule: AccessRule) => void
onAddRole?: (rule: AccessRule) => void
onRemoveRole?: (rule: AccessRule, role: AssignedRole) => void
className?: string
}
const AccessRuleSection = ({
title,
rules,
createButtonLabel,
onCreate,
onEditRule,
onCopyRule,
onDeleteRule,
onAddRole,
onRemoveRole,
className,
}: AccessRuleSectionProps) => {
return (
<section className={cn('flex flex-col', className)}>
<div className="mb-2 flex items-center justify-between gap-3">
<h3 className="pr-3 system-xs-medium-uppercase tracking-wide text-text-tertiary">
{title}
</h3>
<Button variant="secondary" size="medium" onClick={onCreate}>
{createButtonLabel}
</Button>
</div>
<div className="overflow-hidden">
{rules.map((rule, index) => (
<AccessRuleRow
key={rule.id}
rule={rule}
className={cn(index > 0 && 'border-t border-divider-subtle')}
onEdit={onEditRule}
onCopy={onCopyRule}
onDelete={onDeleteRule}
onAddRole={onAddRole}
onRemoveRole={onRemoveRole}
/>
))}
</div>
</section>
)
}
export default memo(AccessRuleSection)

View File

@ -1,22 +1,130 @@
import { Button } from '@langgenius/dify-ui/button'
'use client'
import type { AccessRule } from './access-rule-row'
import { useCallback } from 'react'
import AccessRuleSection from './access-rule-section'
// todo: replace with API data when backend is ready
const 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' },
],
},
{
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' },
],
},
{
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' },
],
},
{
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' },
],
},
]
// todo: replace with API data when backend is ready
const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
{
id: 'kb-full-access',
name: 'Full access',
description: 'Highest level. Can edit, publish, delete apps, 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' },
],
},
{
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' },
],
},
{
id: 'kb-can-view',
name: 'Can view',
description: 'View knowledge base sources and logs. Cannot modify content.',
assignedRoles: [
{ id: 'member', name: 'Member' },
],
},
{
id: 'kb-can-preview',
name: 'Can preview',
description: 'View in the list only. Cannot access the detail page.',
assignedRoles: [
{ id: 'partner', name: 'Partner' },
],
},
{
id: 'kb-can-test',
name: 'Can test',
description: 'Test knowledge base retrieval efficiency in sandbox.',
assignedRoles: [
{ id: 'tester', name: 'Tester' },
],
},
]
const AccessRulesPage = () => {
const noop = useCallback(() => {
// TODO: wire up to API when backend is ready
}, [])
return (
<>
<div className="flex flex-col">
<div className="mb-8 flex items-center gap-3">
<div className="system-sm-semibold-uppercase text-text-secondary">
App Access Rules
</div>
<Button
variant="secondary"
size="medium"
>
Create App permission set
</Button>
</div>
</div>
</>
<div className="flex flex-col gap-6">
<AccessRuleSection
title="App Access Rules"
rules={APP_ACCESS_RULES}
createButtonLabel="Create App permission set"
onCreate={noop}
onEditRule={noop}
onCopyRule={noop}
onDeleteRule={noop}
onAddRole={noop}
onRemoveRole={noop}
/>
<AccessRuleSection
title="Knowledge Base Access Rules"
rules={KNOWLEDGE_BASE_ACCESS_RULES}
createButtonLabel="Create KB permission set"
onCreate={noop}
onEditRule={noop}
onCopyRule={noop}
onDeleteRule={noop}
onAddRole={noop}
onRemoveRole={noop}
/>
</div>
)
}

View File

@ -0,0 +1,39 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { memo } from 'react'
export type RoleTagProps = {
label: string
onRemove?: () => void
className?: string
}
const RoleTag = ({ label, onRemove, className }: RoleTagProps) => {
return (
<span
className={cn(
'inline-flex h-6 max-w-full items-center gap-0.5 rounded-md bg-components-badge-bg-gray-soft px-1.5 system-xs-medium text-text-secondary shadow-xs',
className,
)}
data-testid="access-rule-role-tag"
>
<span className="truncate">{label}</span>
{onRemove && (
<button
type="button"
aria-label={`Remove ${label}`}
onClick={(e) => {
e.stopPropagation()
onRemove()
}}
className="flex h-4 w-4 items-center justify-center rounded text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
>
<span aria-hidden className="i-ri-close-line h-3 w-3" />
</button>
)}
</span>
)
}
export default memo(RoleTag)

View File

@ -16,6 +16,7 @@ import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import AccessRulesPage from './access-rules-page'
import ApiBasedExtensionPage from './api-based-extension-page'
import DataSourcePage from './data-source-page-new'
import LanguagePage from './language-page'
@ -247,6 +248,7 @@ export default function AccountSetting({
{activeMenu === ACCOUNT_SETTING_TAB.PROVIDER && <ModelProviderPage searchText={searchValue} />}
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSIONS && <PermissionsPage />}
{activeMenu === ACCOUNT_SETTING_TAB.ACCESS_RULES && <AccessRulesPage />}
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
{activeMenu === ACCOUNT_SETTING_TAB.DATA_SOURCE && <DataSourcePage />}
{activeMenu === ACCOUNT_SETTING_TAB.API_BASED_EXTENSION && <ApiBasedExtensionPage />}

View File

@ -41,10 +41,10 @@ const RoleList = ({
key={group.id}
className={cn(groupIndex > 0 && 'mt-6')}
>
<h3 className="mb-2 px-3 system-xs-medium-uppercase tracking-wide text-text-tertiary">
<h3 className="mb-2 pr-3 system-xs-medium-uppercase tracking-wide text-text-tertiary">
{group.title}
</h3>
<div className="overflow-hidden rounded-xl border-[0.5px] border-divider-subtle bg-background-section-burn">
<div className="overflow-hidden">
{group.items.map((row, rowIndex) => (
<Row
key={row.id}

View File

@ -27,7 +27,7 @@ const Row = ({
return (
<div
className={cn(
'flex items-start gap-3 px-4 py-3.5',
'flex items-start gap-3 py-3.5',
className,
)}
>