fix: integrate with [app access control]

This commit is contained in:
NFish 2025-04-10 16:56:03 +08:00
parent bfdcd78942
commit 0c492abfc9
7 changed files with 86 additions and 34 deletions

View File

@ -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)} />
)}
</>
)

View File

@ -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'>

View File

@ -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>

View File

@ -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>

View File

@ -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 = {

View File

@ -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 = () => {

View File

@ -353,7 +353,7 @@ export type App = {
api_base_url: string
tags: Tag[]
/** access control */
accessMode: AccessMode
access_mode: AccessMode
}
export type AppSSO = {