fix: add api

This commit is contained in:
NFish 2025-04-09 16:14:12 +08:00
parent d1de872d86
commit ab262b8506
14 changed files with 102 additions and 63 deletions

View File

@ -437,7 +437,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
/>
)}
{showAccessControl && (
<AccessControl onClose={() => setShowAccessControl(false)} />
<AccessControl app={app} onClose={() => setShowAccessControl(false)} />
)}
</>
)

View File

@ -480,7 +480,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
/>
)}
{
showAccessControl && <AccessControl />
showAccessControl && <AccessControl app={appDetail} onClose={() => { setShowAccessControl(false) }} />
}
</div>
</PortalToFollowElem>

View File

@ -1,10 +1,10 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import type { AccessControlType } from './access-control-store'
import useAccessControlStore from './access-control-store'
import type { AccessMode } from '@/models/access-control'
type AccessControlItemProps = PropsWithChildren<{
type: AccessControlType
type: AccessMode
}>
const AccessControlItem: FC<AccessControlItemProps> = ({ type, children }) => {

View File

@ -1,26 +1,28 @@
import { create } from 'zustand'
export enum AccessControlType {
PUBLIC = 'PUBLIC',
SPECIFIC_GROUPS_MEMBERS = 'SPECIFIC_GROUPS_MEMBERS',
ORGANIZATION = 'ORGANIZATION',
}
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control'
import type { App } from '@/types/app'
type AccessControlStore = {
specificGroups: []
setSpecificGroups: (specificGroups: []) => void
specificMembers: []
setSpecificMembers: (specificMembers: []) => void
currentMenu: AccessControlType
setCurrentMenu: (currentMenu: AccessControlType) => void
appId: App['id']
setAppId: (appId: App['id']) => void
specificGroups: AccessControlGroup[]
setSpecificGroups: (specificGroups: AccessControlGroup[]) => void
specificMembers: AccessControlAccount[]
setSpecificMembers: (specificMembers: AccessControlAccount[]) => void
currentMenu: AccessMode
setCurrentMenu: (currentMenu: AccessMode) => void
}
const useAccessControlStore = create<AccessControlStore>((set) => {
return {
appId: '',
setAppId: appId => set({ appId }),
specificGroups: [],
setSpecificGroups: specificGroups => set({ specificGroups }),
specificMembers: [],
setSpecificMembers: specificMembers => set({ specificMembers }),
currentMenu: AccessControlType.SPECIFIC_GROUPS_MEMBERS,
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
setCurrentMenu: currentMenu => set({ currentMenu }),
}
})

View File

@ -13,6 +13,7 @@ import classNames from '@/utils/classnames'
import { useSearchForWhiteListCandidates } from '@/service/access-control'
import type { AccessControlAccount, AccessControlGroup, Subject, SubjectAccount, SubjectGroup } from '@/models/access-control'
import { SubjectType } from '@/models/access-control'
import { useSelector } from '@/context/app-context'
export default function AddMemberOrGroupDialog() {
const { t } = useTranslation()
@ -116,6 +117,8 @@ type MemberItemProps = {
member: AccessControlAccount
}
function MemberItem({ member }: MemberItemProps) {
const currentUser = useSelector(s => s.userProfile)
const { t } = useTranslation()
return <BaseItem className='pr-3'>
<Checkbox className='w-4 h-4 shrink-0' />
<div className='flex items-center grow'>
@ -125,7 +128,7 @@ function MemberItem({ member }: MemberItemProps) {
</div>
</div>
<p className='system-sm-medium text-text-secondary mr-1'>{member.name}</p>
<p className='system-xs-regular text-text-tertiary'>You</p>
{currentUser.email === member.email && <p className='system-xs-regular text-text-tertiary'>({t('common.you')})</p>}
</div>
<p className='system-xs-regular text-text-quaternary'>{member.email}</p>
</BaseItem>

View File

@ -2,22 +2,48 @@
import { Dialog } from '@headlessui/react'
import { RiBuildingLine, RiGlobalLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react'
import Button from '../../base/button'
import Toast from '../../base/toast'
import AccessControlDialog from './access-control-dialog'
import AccessControlItem from './access-control-item'
import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members'
import { AccessControlType } from './access-control-store'
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 { useUpdateAccessMode } from '@/service/access-control'
type AccessControlProps = {
app: App
onClose: () => void
}
export default function AccessControl(props: AccessControlProps) {
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 hideTip = systemFeatures.enable_web_sso_switch_component && systemFeatures.sso_enforced_for_web
useEffect(() => {
setAppId(props.app.id)
}, [props.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 })
Toast.notify({ type: 'success', message: t('app.accessControlDialog.updateSuccess') })
}, [updateAccessMode, props.app, specificGroups, specificMembers, t])
return <AccessControlDialog show onClose={props.onClose}>
<div className='flex flex-col gap-y-3'>
<div className='pt-6 pr-14 pb-3 pl-6'>
@ -28,7 +54,7 @@ export default function AccessControl(props: AccessControlProps) {
<div className='leading-6'>
<p className='system-sm-medium'>{t('app.accessControlDialog.accessLabel')}</p>
</div>
<AccessControlItem type={AccessControlType.ORGANIZATION}>
<AccessControlItem type={AccessMode.ORGANIZATION}>
<div className='flex items-center p-3'>
<div className='grow flex items-center gap-x-2'>
<RiBuildingLine className='w-4 h-4 text-text-primary' />
@ -37,10 +63,10 @@ export default function AccessControl(props: AccessControlProps) {
{!hideTip && <WebAppSSONotEnabledTip />}
</div>
</AccessControlItem>
<AccessControlItem type={AccessControlType.SPECIFIC_GROUPS_MEMBERS}>
<AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}>
<SpecificGroupsOrMembers />
</AccessControlItem>
<AccessControlItem type={AccessControlType.PUBLIC}>
<AccessControlItem type={AccessMode.PUBLIC}>
<div className='flex items-center p-3 gap-x-2'>
<RiGlobalLine className='w-4 h-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.anyone')}</p>
@ -49,7 +75,7 @@ export default function AccessControl(props: AccessControlProps) {
</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 variant='primary'>{t('common.operation.confirm')}</Button>
<Button disabled={isPending} loading={isPending} variant='primary' onClick={handleConfirm}>{t('common.operation.confirm')}</Button>
</div>
</div>
</AccessControlDialog>

View File

@ -1,20 +1,34 @@
'use client'
import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useEffect } from 'react'
import Avatar from '../../base/avatar'
import Divider from '../../base/divider'
import Tooltip from '../../base/tooltip'
import Loading from '../../base/loading'
import AddMemberOrGroupDialog from './add-member-or-group-pop'
import useAccessControlStore, { AccessControlType } from './access-control-store'
import useAccessControlStore from './access-control-store'
import { useGlobalPublicStore } from '@/context/global-public-context'
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects } from '@/service/access-control'
export default function SpecificGroupsOrMembers() {
const currentMenu = useAccessControlStore(s => s.currentMenu)
const appId = useAccessControlStore(s => s.appId)
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const hideTip = systemFeatures.enable_web_sso_switch_component && systemFeatures.sso_enforced_for_web
if (currentMenu !== AccessControlType.SPECIFIC_GROUPS_MEMBERS) {
const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS)
useEffect(() => {
setSpecificGroups(data?.groups ?? [])
setSpecificMembers(data?.members ?? [])
}, [data, setSpecificGroups, setSpecificMembers])
if (currentMenu !== AccessMode.SPECIFIC_GROUPS_MEMBERS) {
return <div className='flex items-center p-3'>
<div className='grow flex items-center gap-x-2'>
<RiLockLine className='w-4 h-4 text-text-primary' />
@ -40,7 +54,7 @@ export default function SpecificGroupsOrMembers() {
</div>
<div className='px-1 pb-1'>
<div className='bg-background-section rounded-lg p-2 flex flex-col gap-y-2'>
<RenderGroupsAndMembers />
{isPending ? <Loading /> : <RenderGroupsAndMembers />}
</div>
</div>
</div >
@ -65,21 +79,21 @@ function RenderGroupsAndMembers() {
}
type GroupItemProps = {
group: string
group: AccessControlGroup
}
function GroupItem({ group }: GroupItemProps) {
return <BaseItem icon={<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />}>
<p className='system-xs-regular text-text-primary'>Group Name</p>
<p className='system-xs-regular text-text-tertiary'>7</p>
<p className='system-xs-regular text-text-primary'>{group.name}</p>
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
</BaseItem>
}
type MemberItemProps = {
member: string
member: AccessControlAccount
}
function MemberItem({ member }: MemberItemProps) {
return <BaseItem icon={<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name='M' />}>
<p className='system-xs-regular text-text-primary'>Member Name</p>
return <BaseItem icon={<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />}>
<p className='system-xs-regular text-text-primary'>{member.name}</p>
</BaseItem>
}

View File

@ -21,14 +21,12 @@ import type { AppIconType, AppSSO, Language } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast'
import { LanguagesSupported, languages } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ISettingsModalProps = {
isChat: boolean
@ -66,8 +64,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose,
onSave,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { isCurrentWorkspaceEditor } = useAppContext()
const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false)
const {
@ -139,7 +135,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
setAppIcon(icon_type === 'image'
? { type: 'image', url: icon_url!, fileId: icon }
: { type: 'emoji', icon, background: icon_background! })
}, [appInfo])
}, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon])
const onHide = () => {
onClose()
@ -325,28 +321,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
<p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
</div>
{/* SSO */}
{systemFeatures.enable_web_sso_switch_component && (
<>
<Divider className="h-px my-0" />
<div className='w-full'>
<p className='mb-1 system-xs-medium-uppercase text-text-tertiary'>{t(`${prefixSettings}.sso.label`)}</p>
<div className='flex justify-between items-center'>
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.sso.title`)}</div>
<Tooltip
disabled={systemFeatures.sso_enforced_for_web}
popupContent={
<div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>
}
asChild={false}
>
<Switch disabled={!systemFeatures.sso_enforced_for_web || !isCurrentWorkspaceEditor} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
</Tooltip>
</div>
<p className='pb-0.5 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.sso.description`)}</p>
</div>
</>
)}
{/* more settings switch */}
<Divider className="h-px my-0" />
{!isShowMore && (

View File

@ -196,6 +196,7 @@ const translation = {
expand: 'Expand',
noResult: 'No result',
},
updateSuccess: 'Update successfully',
},
}

View File

@ -621,6 +621,7 @@ const translation = {
pagination: {
perPage: 'Items per page',
},
your: 'You',
}
export default translation

View File

@ -197,6 +197,7 @@ const translation = {
expand: '展开',
noResult: '没有结果',
},
updateSuccess: '更新成功',
},
}

View File

@ -621,6 +621,7 @@ const translation = {
pagination: {
perPage: '每页显示',
},
you: '你',
}
export default translation

View File

@ -3,9 +3,9 @@ export enum SubjectType {
Account = 'account',
}
export enum AccessModel {
export enum AccessMode {
PUBLIC = 'PUBLIC',
PRIVATE = 'PRIVATE',
SPECIFIC_GROUPS_MEMBERS = 'SPECIFIC_GROUPS_MEMBERS',
ORGANIZATION = 'ORGANIZATION',
}

View File

@ -1,13 +1,15 @@
import { useQuery } from '@tanstack/react-query'
import { get } from './base'
import { useMutation, useQuery } from '@tanstack/react-query'
import { get, post } from './base'
import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
import type { App } from '@/types/app'
const NAME_SPACE = 'access-control'
export const useAppWhiteListSubjects = (appId: string) => {
export const useAppWhiteListSubjects = (appId: string, enabled: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'app-whitelist-subjects', appId],
queryFn: () => get<{ groups: AccessControlGroup[]; members: AccessControlAccount[] }>(`/enterprise/webapp/app/subjects?appId=${appId}`),
enabled,
})
}
@ -33,3 +35,17 @@ export const useSearchForWhiteListCandidates = (query: { appId?: string; keyword
enabled,
})
}
type UpdateAccessModeParams = {
appId: App['id']
subjects: Subject['subjectId'][]
}
export const useUpdateAccessMode = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-access-mode'],
mutationFn: (params: UpdateAccessModeParams) => {
return post('/enterprise/webapp/app/access-mode', { body: params })
},
})
}