mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
feat: add access configuration pages and editor components for app and dataset management
This commit is contained in:
parent
7b97789fd2
commit
34f1ed0ab7
@ -0,0 +1,16 @@
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import AppAccessConfigPage from '@/app/components/app/access-config'
|
||||
|
||||
export type AccessConfigPageProps = {
|
||||
params: Promise<{ locale: Locale, appId: string }>
|
||||
}
|
||||
|
||||
const AccessConfig = async (props: AccessConfigPageProps) => {
|
||||
const params = await props.params
|
||||
|
||||
const { appId } = params
|
||||
|
||||
return <AppAccessConfigPage appId={appId} />
|
||||
}
|
||||
|
||||
export default AccessConfig
|
||||
@ -12,6 +12,8 @@ import {
|
||||
RiTerminalBoxLine,
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
RiUserSettingsFill,
|
||||
RiUserSettingsLine,
|
||||
} from '@remixicon/react'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
@ -100,6 +102,15 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
},
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: 'Access Config',
|
||||
href: `/app/${appId}/access-config`,
|
||||
icon: RiUserSettingsLine,
|
||||
selectedIcon: RiUserSettingsFill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
]
|
||||
return navConfig
|
||||
}, [t])
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import DatasetAccessConfigPage from '@/app/components/datasets/access-config'
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ datasetId: string }>
|
||||
}
|
||||
|
||||
const AccessConfig = async (props: Props) => {
|
||||
const params = await props.params
|
||||
|
||||
const { datasetId } = params
|
||||
|
||||
return <DatasetAccessConfigPage datasetId={datasetId} />
|
||||
}
|
||||
|
||||
export default AccessConfig
|
||||
@ -9,6 +9,8 @@ import {
|
||||
RiFileTextLine,
|
||||
RiFocus2Fill,
|
||||
RiFocus2Line,
|
||||
RiUserSettingsFill,
|
||||
RiUserSettingsLine,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@ -83,6 +85,13 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
selectedIcon: RiEqualizer2Fill,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: 'Access Config',
|
||||
href: `/datasets/${datasetId}/access-config`,
|
||||
icon: RiUserSettingsLine,
|
||||
selectedIcon: RiUserSettingsFill,
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
|
||||
if (datasetRes?.provider !== 'external') {
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
AccessRule,
|
||||
AssignedRole,
|
||||
} from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import type { AccessRule } from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@ -14,8 +11,7 @@ import {
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
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 AccessRulesEditor from '@/app/components/access-rules-editor'
|
||||
|
||||
export type AccessConfigModalProps = {
|
||||
open: boolean
|
||||
@ -46,38 +42,6 @@ const AccessConfigModalBody = ({
|
||||
onSave,
|
||||
}: AccessConfigModalBodyProps) => {
|
||||
const [rules, setRules] = useState<AccessRule[]>(initialRules)
|
||||
const [addingRule, setAddingRule] = useState<AccessRule | null>(null)
|
||||
|
||||
const handleAddRole = useCallback((rule: AccessRule) => {
|
||||
setAddingRule(rule)
|
||||
}, [])
|
||||
|
||||
const handleCloseAddModal = useCallback(() => {
|
||||
setAddingRule(null)
|
||||
}, [])
|
||||
|
||||
const handleAddSubmit = useCallback(
|
||||
(_selection: { roleIds: string[], memberIds: string[] }) => {
|
||||
// TODO: wire up to API when backend is ready.
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const handleRemoveRole = useCallback(
|
||||
(target: AccessRule, role: AssignedRole) => {
|
||||
setRules(prev =>
|
||||
prev.map(rule =>
|
||||
rule.id === target.id
|
||||
? {
|
||||
...rule,
|
||||
assignedRoles: rule.assignedRoles.filter(r => r.id !== role.id),
|
||||
}
|
||||
: rule,
|
||||
),
|
||||
)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave?.(rules)
|
||||
@ -105,17 +69,7 @@ const AccessConfigModalBody = ({
|
||||
className="min-h-0 flex-1"
|
||||
slotClassNames={{ viewport: 'px-6 overscroll-contain' }}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{rules.map(rule => (
|
||||
<AccessRuleRow
|
||||
key={rule.id}
|
||||
rule={rule}
|
||||
showMenu={false}
|
||||
onAddRole={handleAddRole}
|
||||
onRemoveRole={handleRemoveRole}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AccessRulesEditor rules={rules} onRulesChange={setRules} />
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-divider-subtle px-6 py-4">
|
||||
@ -126,17 +80,6 @@ const AccessConfigModalBody = ({
|
||||
{saveLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{addingRule && (
|
||||
<AddRuleTargetsModal
|
||||
open
|
||||
ruleName={addingRule.name}
|
||||
initialRoleIds={addingRule.assignedRoles.map(role => role.id)}
|
||||
initialMemberIds={[]}
|
||||
onClose={handleCloseAddModal}
|
||||
onSubmit={handleAddSubmit}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
@ -159,15 +102,17 @@ const AccessConfigModal = ({
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<AccessConfigModalBody
|
||||
title={title}
|
||||
description={description}
|
||||
initialRules={initialRules}
|
||||
saveLabel={saveLabel}
|
||||
cancelLabel={cancelLabel}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
{open && (
|
||||
<AccessConfigModalBody
|
||||
title={title}
|
||||
description={description}
|
||||
initialRules={initialRules}
|
||||
saveLabel={saveLabel}
|
||||
cancelLabel={cancelLabel}
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
103
web/app/components/access-rules-editor/index.tsx
Normal file
103
web/app/components/access-rules-editor/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
AccessRule,
|
||||
AssignedRole,
|
||||
} from '@/app/components/header/account-setting/access-rules-page/access-rule-row'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
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'
|
||||
|
||||
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
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccessRulesEditor = ({
|
||||
rules: rulesProp,
|
||||
onRulesChange,
|
||||
className,
|
||||
}: AccessRulesEditorProps) => {
|
||||
const isControlled = typeof onRulesChange === 'function'
|
||||
const [internalRules, setInternalRules] = useState<AccessRule[]>(rulesProp)
|
||||
const rules = isControlled ? rulesProp : internalRules
|
||||
|
||||
const updateRules = useCallback(
|
||||
(updater: (prev: AccessRule[]) => AccessRule[]) => {
|
||||
if (isControlled) {
|
||||
onRulesChange(updater(rulesProp))
|
||||
return
|
||||
}
|
||||
setInternalRules(prev => updater(prev))
|
||||
},
|
||||
[isControlled, onRulesChange, rulesProp],
|
||||
)
|
||||
|
||||
const [addingRule, setAddingRule] = useState<AccessRule | null>(null)
|
||||
|
||||
const handleAddRole = useCallback((rule: AccessRule) => {
|
||||
setAddingRule(rule)
|
||||
}, [])
|
||||
|
||||
const handleCloseAddModal = useCallback(() => {
|
||||
setAddingRule(null)
|
||||
}, [])
|
||||
|
||||
const handleAddSubmit = useCallback(
|
||||
(_selection: { roleIds: string[], memberIds: string[] }) => {
|
||||
// TODO: wire up to API when backend is ready.
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
},
|
||||
[updateRules],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)}>
|
||||
{rules.map((rule, index) => (
|
||||
<AccessRuleRow
|
||||
key={rule.id}
|
||||
rule={rule}
|
||||
showMenu={false}
|
||||
onAddRole={handleAddRole}
|
||||
onRemoveRole={handleRemoveRole}
|
||||
className={cn(index > 0 && 'border-t border-divider-subtle')}
|
||||
/>
|
||||
))}
|
||||
|
||||
{addingRule && (
|
||||
<AddRuleTargetsModal
|
||||
open
|
||||
ruleName={addingRule.name}
|
||||
initialRoleIds={addingRule.assignedRoles.map(role => role.id)}
|
||||
initialMemberIds={[]}
|
||||
onClose={handleCloseAddModal}
|
||||
onSubmit={handleAddSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccessRulesEditor
|
||||
74
web/app/components/app/access-config/index.tsx
Normal file
74
web/app/components/app/access-config/index.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
'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: [],
|
||||
},
|
||||
]
|
||||
|
||||
type AppAccessConfigPageProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const AppAccessConfigPage = ({ appId: _appId }: AppAccessConfigPageProps) => {
|
||||
return (
|
||||
<ScrollArea
|
||||
className="h-full bg-components-panel-bg"
|
||||
slotClassNames={{ viewport: 'overscroll-contain' }}
|
||||
>
|
||||
<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={DEFAULT_APP_ACCESS_RULES} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppAccessConfigPage
|
||||
83
web/app/components/datasets/access-config/index.tsx
Normal file
83
web/app/components/datasets/access-config/index.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'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: [],
|
||||
},
|
||||
]
|
||||
|
||||
type DatasetAccessConfigPageProps = {
|
||||
datasetId: string
|
||||
}
|
||||
|
||||
const DatasetAccessConfigPage = ({ datasetId: _datasetId }: DatasetAccessConfigPageProps) => {
|
||||
return (
|
||||
<ScrollArea
|
||||
className="h-full bg-components-panel-bg"
|
||||
slotClassNames={{ viewport: 'overscroll-contain' }}
|
||||
>
|
||||
<div className="px-12 py-8">
|
||||
<h1 className="title-2xl-semi-bold text-text-primary">Access Config</h1>
|
||||
<div className="mt-6">
|
||||
<AccessRulesEditor rules={DEFAULT_KB_ACCESS_RULES} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetAccessConfigPage
|
||||
Loading…
Reference in New Issue
Block a user