mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
feat: add access rules management components including AccessRuleRow, AccessRuleSection, and AccessRulesPage
This commit is contained in:
parent
5907b3f809
commit
12b93290fa
@ -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
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -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 />}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user