mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-20 17:29:08 +08:00
fix: integrate with [app access control]
This commit is contained in:
parent
bfdcd78942
commit
0c492abfc9
@ -179,6 +179,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
setShowSwitchModal(false)
|
||||
}
|
||||
|
||||
const onUpdateAccessControl = useCallback(() => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
mutateApps()
|
||||
setShowAccessControl(false)
|
||||
}, [onRefresh, mutateApps, setShowAccessControl])
|
||||
|
||||
const Operations = (props: HtmlContentProps) => {
|
||||
const onMouseLeave = async () => {
|
||||
props.onClose?.()
|
||||
@ -316,13 +323,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 w-5 h-5 flex items-center justify-center'>
|
||||
{app.accessMode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.anyone')}>
|
||||
{app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.anyone')}>
|
||||
<RiGlobalLine className='text-text-accent w-4 h-4' />
|
||||
</Tooltip>}
|
||||
{app.accessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.specific')}>
|
||||
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.specific')}>
|
||||
<RiLockLine className='text-text-quaternary w-4 h-4' />
|
||||
</Tooltip>}
|
||||
{app.accessMode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.organization')}>
|
||||
{app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.organization')}>
|
||||
<RiBuildingLine className='text-text-quaternary w-4 h-4' />
|
||||
</Tooltip>}
|
||||
</div>
|
||||
@ -444,7 +451,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
/>
|
||||
)}
|
||||
{showAccessControl && (
|
||||
<AccessControl app={app} onClose={() => setShowAccessControl(false)} />
|
||||
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import { RiAddCircleFill, RiArrowRightSLine, RiOrganizationChart } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import Avatar from '../../base/avatar'
|
||||
import Button from '../../base/button'
|
||||
@ -9,6 +9,7 @@ import Checkbox from '../../base/checkbox'
|
||||
import Input from '../../base/input'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
||||
import Loading from '../../base/loading'
|
||||
import useAccessControlStore from './access-control-store'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useSearchForWhiteListCandidates } from '@/service/access-control'
|
||||
import type { AccessControlAccount, AccessControlGroup, Subject, SubjectAccount, SubjectGroup } from '@/models/access-control'
|
||||
@ -78,7 +79,7 @@ function renderGroupOrMember(data: GroupOrMemberData) {
|
||||
return data?.map((page) => {
|
||||
return <div key={`search_group_member_page_${page.currPage}`} className='p-1'>
|
||||
{page.subjects?.map((item, index) => {
|
||||
if (item.subjectType === SubjectType.Group)
|
||||
if (item.subjectType === SubjectType.GROUP)
|
||||
return <GroupItem key={index} group={(item as SubjectGroup).groupData} />
|
||||
return <MemberItem key={index} member={(item as SubjectAccount).accountData} />
|
||||
})}
|
||||
@ -91,8 +92,21 @@ type GroupItemProps = {
|
||||
}
|
||||
function GroupItem({ group }: GroupItemProps) {
|
||||
const { t } = useTranslation()
|
||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
|
||||
const isChecked = specificGroups.some(g => g.id === group.id)
|
||||
const handleCheckChange = useCallback(() => {
|
||||
if (!isChecked) {
|
||||
const newGroups = [...specificGroups, group]
|
||||
setSpecificGroups(newGroups)
|
||||
}
|
||||
else {
|
||||
const newGroups = specificGroups.filter(g => g.id !== group.id)
|
||||
setSpecificGroups(newGroups)
|
||||
}
|
||||
}, [specificGroups, setSpecificGroups, group, isChecked])
|
||||
return <BaseItem>
|
||||
<Checkbox className='w-4 h-4 shrink-0' />
|
||||
<Checkbox checked={isChecked} className='w-4 h-4 shrink-0' onCheck={handleCheckChange} />
|
||||
<div className='flex item-center grow'>
|
||||
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden mr-2'>
|
||||
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
|
||||
@ -115,8 +129,21 @@ type MemberItemProps = {
|
||||
function MemberItem({ member }: MemberItemProps) {
|
||||
const currentUser = useSelector(s => s.userProfile)
|
||||
const { t } = useTranslation()
|
||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
|
||||
const isChecked = specificMembers.some(m => m.id === member.id)
|
||||
const handleCheckChange = useCallback(() => {
|
||||
if (!isChecked) {
|
||||
const newMembers = [...specificMembers, member]
|
||||
setSpecificMembers(newMembers)
|
||||
}
|
||||
else {
|
||||
const newMembers = specificMembers.filter(m => m.id !== member.id)
|
||||
setSpecificMembers(newMembers)
|
||||
}
|
||||
}, [specificMembers, setSpecificMembers, member, isChecked])
|
||||
return <BaseItem className='pr-3'>
|
||||
<Checkbox className='w-4 h-4 shrink-0' />
|
||||
<Checkbox checked={isChecked} className='w-4 h-4 shrink-0' onCheck={handleCheckChange} />
|
||||
<div className='flex items-center grow'>
|
||||
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden mr-2'>
|
||||
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
|
||||
|
@ -11,40 +11,57 @@ import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-grou
|
||||
import useAccessControlStore from './access-control-store'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import type { App } from '@/types/app'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import type { Subject } from '@/models/access-control'
|
||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||
import { useUpdateAccessMode } from '@/service/access-control'
|
||||
|
||||
type AccessControlProps = {
|
||||
app: App
|
||||
onClose: () => void
|
||||
onConfirm?: () => void
|
||||
}
|
||||
|
||||
export default function AccessControl(props: AccessControlProps) {
|
||||
const { app, onClose, onConfirm } = props
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const setAppId = useAccessControlStore(s => s.setAppId)
|
||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||
const currentMenu = useAccessControlStore(s => s.currentMenu)
|
||||
const setCurrentMenu = useAccessControlStore(s => s.setCurrentMenu)
|
||||
const hideTip = systemFeatures.enable_web_sso_switch_component && systemFeatures.sso_enforced_for_web
|
||||
|
||||
useEffect(() => {
|
||||
setAppId(props.app.id)
|
||||
}, [props.app, setAppId])
|
||||
setAppId(app.id)
|
||||
setCurrentMenu(app.access_mode ?? AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||
}, [app, setAppId])
|
||||
|
||||
const { isPending, mutateAsync: updateAccessMode } = useUpdateAccessMode()
|
||||
const handleConfirm = useCallback(async () => {
|
||||
const subjectIds: string[] = []
|
||||
specificGroups.forEach((group) => {
|
||||
subjectIds.push(group.id)
|
||||
})
|
||||
specificMembers.forEach((member) => {
|
||||
subjectIds.push(member.id)
|
||||
},
|
||||
)
|
||||
await updateAccessMode({ appId: props.app.id, subjects: subjectIds })
|
||||
const submitData: {
|
||||
appId: string
|
||||
accessMode: AccessMode
|
||||
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
|
||||
} = { appId: app.id, accessMode: currentMenu }
|
||||
if (currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) {
|
||||
const subjects: Pick<Subject, 'subjectId' | 'subjectType'>[] = []
|
||||
specificGroups.forEach((group) => {
|
||||
subjects.push({ subjectId: group.id, subjectType: SubjectType.GROUP })
|
||||
})
|
||||
specificMembers.forEach((member) => {
|
||||
subjects.push({
|
||||
subjectId: member.id,
|
||||
subjectType: SubjectType.ACCOUNT,
|
||||
})
|
||||
})
|
||||
submitData.subjects = subjects
|
||||
}
|
||||
await updateAccessMode(submitData)
|
||||
Toast.notify({ type: 'success', message: t('app.accessControlDialog.updateSuccess') })
|
||||
}, [updateAccessMode, props.app, specificGroups, specificMembers, t])
|
||||
return <AccessControlDialog show onClose={props.onClose}>
|
||||
onConfirm?.()
|
||||
}, [updateAccessMode, app, specificGroups, specificMembers, t, onConfirm, currentMenu])
|
||||
return <AccessControlDialog show onClose={onClose}>
|
||||
<div className='flex flex-col gap-y-3'>
|
||||
<div className='pt-6 pr-14 pb-3 pl-6'>
|
||||
<Dialog.Title className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</Dialog.Title>
|
||||
@ -74,7 +91,7 @@ export default function AccessControl(props: AccessControlProps) {
|
||||
</AccessControlItem>
|
||||
</div>
|
||||
<div className='flex items-center justify-end p-6 pt-5 gap-x-2'>
|
||||
<Button onClick={props.onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button disabled={isPending} loading={isPending} variant='primary' onClick={handleConfirm}>{t('common.operation.confirm')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@ export default function SpecificGroupsOrMembers() {
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-1 pb-1'>
|
||||
<div className='bg-background-section rounded-lg p-2 flex flex-col gap-y-2'>
|
||||
<div className='bg-background-section rounded-lg p-2 flex flex-col gap-y-2 max-h-[400px] overflow-y-auto'>
|
||||
{isPending ? <Loading /> : <RenderGroupsAndMembers />}
|
||||
</div>
|
||||
</div>
|
||||
@ -67,11 +67,11 @@ function RenderGroupsAndMembers() {
|
||||
if (specificGroups.length <= 0 && specificMembers.length <= 0)
|
||||
return <div className='px-2 pt-5 pb-1.5'><p className='system-xs-regular text-text-tertiary text-center'>{t('app.accessControlDialog.noGroupsOrMembers')}</p></div>
|
||||
return <>
|
||||
<p className='system-2xs-medium-uppercase text-text-tertiary'>{t('app.accessControlDialog.groups', { count: specificGroups.length ?? 0 })}</p>
|
||||
<p className='system-2xs-medium-uppercase text-text-tertiary sticky top-0'>{t('app.accessControlDialog.groups', { count: specificGroups.length ?? 0 })}</p>
|
||||
<div className='flex flex-row flex-wrap gap-1'>
|
||||
{specificGroups.map((group, index) => <GroupItem key={index} group={group} />)}
|
||||
</div>
|
||||
<p className='system-2xs-medium-uppercase text-text-tertiary'>{t('app.accessControlDialog.members', { count: specificMembers.length ?? 0 })}</p>
|
||||
<p className='system-2xs-medium-uppercase text-text-tertiary sticky top-0'>{t('app.accessControlDialog.members', { count: specificMembers.length ?? 0 })}</p>
|
||||
<div className='flex flex-row flex-wrap gap-1'>
|
||||
{specificMembers.map((member, index) => <MemberItem key={index} member={member} />)}
|
||||
</div>
|
||||
|
@ -1,12 +1,12 @@
|
||||
export enum SubjectType {
|
||||
Group = 'group',
|
||||
Account = 'account',
|
||||
GROUP = 'group',
|
||||
ACCOUNT = 'account',
|
||||
}
|
||||
|
||||
export enum AccessMode {
|
||||
PUBLIC = 'PUBLIC',
|
||||
SPECIFIC_GROUPS_MEMBERS = 'SPECIFIC_GROUPS_MEMBERS',
|
||||
ORGANIZATION = 'ORGANIZATION',
|
||||
PUBLIC = 'public',
|
||||
SPECIFIC_GROUPS_MEMBERS = 'private',
|
||||
ORGANIZATION = 'private_all',
|
||||
}
|
||||
|
||||
export type AccessControlGroup = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { get, post } from './base'
|
||||
import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
|
||||
import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
|
||||
import type { App } from '@/types/app'
|
||||
|
||||
const NAME_SPACE = 'access-control'
|
||||
@ -45,7 +45,8 @@ export const useSearchForWhiteListCandidates = (query: { keyword?: string; resul
|
||||
|
||||
type UpdateAccessModeParams = {
|
||||
appId: App['id']
|
||||
subjects: Subject['subjectId'][]
|
||||
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
|
||||
accessMode: AccessMode
|
||||
}
|
||||
|
||||
export const useUpdateAccessMode = () => {
|
||||
|
@ -353,7 +353,7 @@ export type App = {
|
||||
api_base_url: string
|
||||
tags: Tag[]
|
||||
/** access control */
|
||||
accessMode: AccessMode
|
||||
access_mode: AccessMode
|
||||
}
|
||||
|
||||
export type AppSSO = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user