Feat/e permission (#18656)
parent
2259dfdc58
commit
fee51ba994
@ -0,0 +1,61 @@
|
|||||||
|
import { Fragment, useCallback } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type DialogProps = {
|
||||||
|
className?: string
|
||||||
|
children: ReactNode
|
||||||
|
show: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessControlDialog = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
show,
|
||||||
|
onClose,
|
||||||
|
}: DialogProps) => {
|
||||||
|
const close = useCallback(() => {
|
||||||
|
onClose?.()
|
||||||
|
}, [onClose])
|
||||||
|
return (
|
||||||
|
<Transition appear show={show} as={Fragment}>
|
||||||
|
<Dialog as="div" open={true} className="relative z-20" onClose={() => null}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-background-overlay bg-opacity-25" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className={cn('w-[600px] min-h-[323px] h-auto bg-components-panel-bg shadow-xl rounded-2xl transition-all transform relative p-0 overflow-y-auto', className)}>
|
||||||
|
<div onClick={() => close()} className="absolute top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer z-10">
|
||||||
|
<RiCloseLine className='w-5 h-5' />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccessControlDialog
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC, PropsWithChildren } from 'react'
|
||||||
|
import useAccessControlStore from '../../../../context/access-control-store'
|
||||||
|
import type { AccessMode } from '@/models/access-control'
|
||||||
|
|
||||||
|
type AccessControlItemProps = PropsWithChildren<{
|
||||||
|
type: AccessMode
|
||||||
|
}>
|
||||||
|
|
||||||
|
const AccessControlItem: FC<AccessControlItemProps> = ({ type, children }) => {
|
||||||
|
const { currentMenu, setCurrentMenu } = useAccessControlStore(s => ({ currentMenu: s.currentMenu, setCurrentMenu: s.setCurrentMenu }))
|
||||||
|
if (currentMenu !== type) {
|
||||||
|
return <div
|
||||||
|
className="rounded-[10px] border-[1px] cursor-pointer
|
||||||
|
border-components-option-card-option-border bg-components-option-card-option-bg
|
||||||
|
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover"
|
||||||
|
onClick={() => setCurrentMenu(type)} >
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="rounded-[10px] border-[1.5px]
|
||||||
|
border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessControlItem.displayName = 'AccessControlItem'
|
||||||
|
|
||||||
|
export default AccessControlItem
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
'use client'
|
||||||
|
import { RiAddCircleFill, RiArrowRightSLine, RiOrganizationChart } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useDebounce } from 'ahooks'
|
||||||
|
import Avatar from '../../base/avatar'
|
||||||
|
import Button from '../../base/button'
|
||||||
|
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 '../../../../context/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'
|
||||||
|
import { SubjectType } from '@/models/access-control'
|
||||||
|
import { useSelector } from '@/context/app-context'
|
||||||
|
|
||||||
|
export default function AddMemberOrGroupDialog() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
|
||||||
|
const debouncedKeyword = useDebounce(keyword, { wait: 500 })
|
||||||
|
|
||||||
|
const lastAvailableGroup = selectedGroupsForBreadcrumb[selectedGroupsForBreadcrumb.length - 1]
|
||||||
|
const { isPending, isFetchingNextPage, fetchNextPage, data } = useSearchForWhiteListCandidates({ keyword: debouncedKeyword, groupId: lastAvailableGroup?.id, resultsPerPage: 10 }, open)
|
||||||
|
const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setKeyword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorRef = useRef<HTMLDivElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
const hasMore = data?.pages?.[0].hasMore ?? false
|
||||||
|
let observer: IntersectionObserver | undefined
|
||||||
|
if (anchorRef.current) {
|
||||||
|
observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting && !isPending && hasMore)
|
||||||
|
fetchNextPage()
|
||||||
|
}, { rootMargin: '20px' })
|
||||||
|
observer.observe(anchorRef.current)
|
||||||
|
}
|
||||||
|
return () => observer?.disconnect()
|
||||||
|
}, [isPending, fetchNextPage, anchorRef, data])
|
||||||
|
|
||||||
|
return <PortalToFollowElem open={open} onOpenChange={setOpen} offset={{ crossAxis: 300 }} placement='bottom-end'>
|
||||||
|
<PortalToFollowElemTrigger asChild>
|
||||||
|
<Button variant='ghost-accent' size='small' className='shrink-0 flex items-center gap-x-0.5' onClick={() => setOpen(!open)}>
|
||||||
|
<RiAddCircleFill className='w-4 h-4' />
|
||||||
|
<span>{t('common.operation.add')}</span>
|
||||||
|
</Button>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[25]'>
|
||||||
|
<div className='w-[400px] max-h-[400px] relative overflow-y-auto flex flex-col border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur backdrop-blur-[5px] shadow-lg'>
|
||||||
|
<div className='p-2 pb-0.5 sticky top-0 bg-components-panel-bg-blur backdrop-blur-[5px] z-1'>
|
||||||
|
<Input value={keyword} onChange={handleKeywordChange} showLeftIcon placeholder={t('app.accessControlDialog.operateGroupAndMember.searchPlaceholder') as string} />
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
isPending
|
||||||
|
? <div className='p-1'><Loading /></div>
|
||||||
|
: (data?.pages?.length ?? 0) > 0
|
||||||
|
? <>
|
||||||
|
<div className='flex items-center h-7 px-2 py-0.5'>
|
||||||
|
<SelectedGroupsBreadCrumb />
|
||||||
|
</div>
|
||||||
|
<div className='p-1'>
|
||||||
|
{renderGroupOrMember(data?.pages ?? [])}
|
||||||
|
{isFetchingNextPage && <Loading />}
|
||||||
|
</div>
|
||||||
|
<div ref={anchorRef} className='h-0'> </div>
|
||||||
|
</>
|
||||||
|
: <div className='flex items-center justify-center h-7 px-2 py-0.5'>
|
||||||
|
<span className='system-xs-regular text-text-tertiary'>{t('app.accessControlDialog.operateGroupAndMember.noResult')}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupOrMemberData = { subjects: Subject[]; currPage: number }[]
|
||||||
|
function renderGroupOrMember(data: GroupOrMemberData) {
|
||||||
|
return data?.map((page) => {
|
||||||
|
return <div key={`search_group_member_page_${page.currPage}`}>
|
||||||
|
{page.subjects?.map((item, index) => {
|
||||||
|
if (item.subjectType === SubjectType.GROUP)
|
||||||
|
return <GroupItem key={index} group={(item as SubjectGroup).groupData} />
|
||||||
|
return <MemberItem key={index} member={(item as SubjectAccount).accountData} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectedGroupsBreadCrumb() {
|
||||||
|
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
|
||||||
|
const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleBreadCrumbClick = useCallback((index: number) => {
|
||||||
|
const newGroups = selectedGroupsForBreadcrumb.slice(0, index + 1)
|
||||||
|
setSelectedGroupsForBreadcrumb(newGroups)
|
||||||
|
}, [setSelectedGroupsForBreadcrumb, selectedGroupsForBreadcrumb])
|
||||||
|
const handleReset = useCallback(() => {
|
||||||
|
setSelectedGroupsForBreadcrumb([])
|
||||||
|
}, [setSelectedGroupsForBreadcrumb])
|
||||||
|
return <div className='flex items-center h-7 px-2 py-0.5 gap-x-0.5'>
|
||||||
|
<span className={classNames('system-xs-regular text-text-tertiary', selectedGroupsForBreadcrumb.length > 0 && 'text-text-accent cursor-pointer')} onClick={handleReset}>{t('app.accessControlDialog.operateGroupAndMember.allMembers')}</span>
|
||||||
|
{selectedGroupsForBreadcrumb.map((group, index) => {
|
||||||
|
return <div key={index} className='flex items-center gap-x-0.5 text-text-tertiary system-xs-regular'>
|
||||||
|
<span>/</span>
|
||||||
|
<span className={index === selectedGroupsForBreadcrumb.length - 1 ? '' : 'text-text-accent cursor-pointer'} onClick={() => handleBreadCrumbClick(index)}>{group.name}</span>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupItemProps = {
|
||||||
|
group: AccessControlGroup
|
||||||
|
}
|
||||||
|
function GroupItem({ group }: GroupItemProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||||
|
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
|
||||||
|
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
|
||||||
|
const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb)
|
||||||
|
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])
|
||||||
|
|
||||||
|
const handleExpandClick = useCallback(() => {
|
||||||
|
setSelectedGroupsForBreadcrumb([...selectedGroupsForBreadcrumb, group])
|
||||||
|
}, [selectedGroupsForBreadcrumb, setSelectedGroupsForBreadcrumb, group])
|
||||||
|
return <BaseItem>
|
||||||
|
<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'>
|
||||||
|
<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='system-sm-medium text-text-secondary mr-1'>{group.name}</p>
|
||||||
|
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
|
||||||
|
</div>
|
||||||
|
<Button size="small" disabled={isChecked} variant='ghost-accent'
|
||||||
|
className='py-1 px-1.5 shrink-0 flex items-center justify-between' onClick={handleExpandClick}>
|
||||||
|
<span className='px-[3px]'>{t('app.accessControlDialog.operateGroupAndMember.expand')}</span>
|
||||||
|
<RiArrowRightSLine className='w-4 h-4' />
|
||||||
|
</Button>
|
||||||
|
</BaseItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemberItemProps = {
|
||||||
|
member: AccessControlAccount
|
||||||
|
}
|
||||||
|
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 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'>
|
||||||
|
<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='system-sm-medium text-text-secondary mr-1'>{member.name}</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>
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseItemProps = {
|
||||||
|
className?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
function BaseItem({ children, className }: BaseItemProps) {
|
||||||
|
return <div className={classNames('p-1 pl-2 flex items-center space-x-2 hover:rounded-lg hover:bg-state-base-hover cursor-pointer', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
'use client'
|
||||||
|
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 useAccessControlStore from '../../../../context/access-control-store'
|
||||||
|
import AccessControlDialog from './access-control-dialog'
|
||||||
|
import AccessControlItem from './access-control-item'
|
||||||
|
import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import type { App } from '@/types/app'
|
||||||
|
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.webapp_auth.enabled
|
||||||
|
&& (systemFeatures.webapp_auth.allow_sso
|
||||||
|
|| systemFeatures.webapp_auth.allow_email_password_login
|
||||||
|
|| systemFeatures.webapp_auth.allow_email_code_login)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAppId(app.id)
|
||||||
|
setCurrentMenu(app.access_mode ?? AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||||
|
}, [app, setAppId, setCurrentMenu])
|
||||||
|
|
||||||
|
const { isPending, mutateAsync: updateAccessMode } = useUpdateAccessMode()
|
||||||
|
const handleConfirm = useCallback(async () => {
|
||||||
|
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') })
|
||||||
|
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>
|
||||||
|
<Dialog.Description className='mt-1 system-xs-regular text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
<div className='px-6 pb-3 flex flex-col gap-y-1'>
|
||||||
|
<div className='leading-6'>
|
||||||
|
<p className='system-sm-medium'>{t('app.accessControlDialog.accessLabel')}</p>
|
||||||
|
</div>
|
||||||
|
<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' />
|
||||||
|
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p>
|
||||||
|
</div>
|
||||||
|
{!hideTip && <WebAppSSONotEnabledTip />}
|
||||||
|
</div>
|
||||||
|
</AccessControlItem>
|
||||||
|
<AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}>
|
||||||
|
<SpecificGroupsOrMembers />
|
||||||
|
</AccessControlItem>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</AccessControlItem>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-end p-6 pt-5 gap-x-2'>
|
||||||
|
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||||
|
<Button disabled={isPending} loading={isPending} variant='primary' onClick={handleConfirm}>{t('common.operation.confirm')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccessControlDialog>
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
'use client'
|
||||||
|
import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useCallback, useEffect } from 'react'
|
||||||
|
import Avatar from '../../base/avatar'
|
||||||
|
import Divider from '../../base/divider'
|
||||||
|
import Tooltip from '../../base/tooltip'
|
||||||
|
import Loading from '../../base/loading'
|
||||||
|
import useAccessControlStore from '../../../../context/access-control-store'
|
||||||
|
import AddMemberOrGroupDialog from './add-member-or-group-pop'
|
||||||
|
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.webapp_auth.enabled
|
||||||
|
&& (systemFeatures.webapp_auth.allow_sso
|
||||||
|
|| systemFeatures.webapp_auth.allow_email_password_login
|
||||||
|
|| systemFeatures.webapp_auth.allow_email_code_login)
|
||||||
|
|
||||||
|
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' />
|
||||||
|
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
|
||||||
|
</div>
|
||||||
|
{!hideTip && <WebAppSSONotEnabledTip />}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<div className='flex items-center gap-x-1 p-3'>
|
||||||
|
<div className='grow flex items-center gap-x-1'>
|
||||||
|
<RiLockLine className='w-4 h-4 text-text-primary' />
|
||||||
|
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-x-1'>
|
||||||
|
{!hideTip && <>
|
||||||
|
<WebAppSSONotEnabledTip />
|
||||||
|
<Divider className='h-[14px] ml-2 mr-0' type="vertical" />
|
||||||
|
</>}
|
||||||
|
<AddMemberOrGroupDialog />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='px-1 pb-1'>
|
||||||
|
<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>
|
||||||
|
</div >
|
||||||
|
}
|
||||||
|
|
||||||
|
function RenderGroupsAndMembers() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||||
|
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||||
|
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 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 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupItemProps = {
|
||||||
|
group: AccessControlGroup
|
||||||
|
}
|
||||||
|
function GroupItem({ group }: GroupItemProps) {
|
||||||
|
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||||
|
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
|
||||||
|
const handleRemoveGroup = useCallback(() => {
|
||||||
|
setSpecificGroups(specificGroups.filter(g => g.id !== group.id))
|
||||||
|
}, [group, setSpecificGroups, specificGroups])
|
||||||
|
return <BaseItem icon={<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />}
|
||||||
|
onRemove={handleRemoveGroup}>
|
||||||
|
<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: AccessControlAccount
|
||||||
|
}
|
||||||
|
function MemberItem({ member }: MemberItemProps) {
|
||||||
|
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||||
|
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
|
||||||
|
const handleRemoveMember = useCallback(() => {
|
||||||
|
setSpecificMembers(specificMembers.filter(m => m.id !== member.id))
|
||||||
|
}, [member, setSpecificMembers, specificMembers])
|
||||||
|
return <BaseItem icon={<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />}
|
||||||
|
onRemove={handleRemoveMember}>
|
||||||
|
<p className='system-xs-regular text-text-primary'>{member.name}</p>
|
||||||
|
</BaseItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseItemProps = {
|
||||||
|
icon: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
onRemove?: () => void
|
||||||
|
}
|
||||||
|
function BaseItem({ icon, onRemove, children }: BaseItemProps) {
|
||||||
|
return <div className='rounded-full border-[0.5px] bg-components-badge-white-to-dark shadow-xs p-1 pr-1.5 group flex items-center flex-row gap-x-1'>
|
||||||
|
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden'>
|
||||||
|
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
<div className='flex items-center justify-center w-4 h-4 cursor-pointer' onClick={onRemove}>
|
||||||
|
<RiCloseCircleFill className='w-[14px] h-[14px] text-text-quaternary' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebAppSSONotEnabledTip() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return <Tooltip asChild={false} popupContent={t('app.accessControlDialog.webAppSSONotEnabledTip')}>
|
||||||
|
<RiAlertFill className='w-4 h-4 text-text-warning-secondary shrink-0' />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
|
import type { SiteInfo } from '@/models/share'
|
||||||
|
import { appDefaultIconBackground } from '@/config'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data?: SiteInfo
|
||||||
|
isShow: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoModal = ({
|
||||||
|
isShow,
|
||||||
|
onClose,
|
||||||
|
data,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isShow={isShow}
|
||||||
|
onClose={onClose}
|
||||||
|
className='min-w-[400px] max-w-[400px] !p-0'
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
<div className={cn('flex flex-col items-center gap-4 px-4 pb-8 pt-10')}>
|
||||||
|
<AppIcon
|
||||||
|
size='xxl'
|
||||||
|
iconType={data?.icon_type}
|
||||||
|
icon={data?.icon}
|
||||||
|
background={data?.icon_background || appDefaultIconBackground}
|
||||||
|
imageUrl={data?.icon_url}
|
||||||
|
/>
|
||||||
|
<div className='system-xl-semibold text-text-secondary'>{data?.title}</div>
|
||||||
|
<div className='system-xs-regular text-text-tertiary'>
|
||||||
|
{/* copyright */}
|
||||||
|
{data?.copyright && (
|
||||||
|
<div>© {(new Date()).getFullYear()} {data?.copyright}</div>
|
||||||
|
)}
|
||||||
|
{data?.custom_disclaimer && (
|
||||||
|
<div className='mt-2'>{data.custom_disclaimer}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InfoModal
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { Placement } from '@floating-ui/react'
|
||||||
|
import {
|
||||||
|
RiEqualizer2Line,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Divider from '../../base/divider'
|
||||||
|
import { removeAccessToken } from '../utils'
|
||||||
|
import InfoModal from './info-modal'
|
||||||
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import type { SiteInfo } from '@/models/share'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data?: SiteInfo
|
||||||
|
placement?: Placement
|
||||||
|
hideLogout?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuDropdown: FC<Props> = ({
|
||||||
|
data,
|
||||||
|
placement,
|
||||||
|
hideLogout,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, doSetOpen] = useState(false)
|
||||||
|
const openRef = useRef(open)
|
||||||
|
const setOpen = useCallback((v: boolean) => {
|
||||||
|
doSetOpen(v)
|
||||||
|
openRef.current = v
|
||||||
|
}, [doSetOpen])
|
||||||
|
|
||||||
|
const handleTrigger = useCallback(() => {
|
||||||
|
setOpen(!openRef.current)
|
||||||
|
}, [setOpen])
|
||||||
|
|
||||||
|
const handleLogout = useCallback(() => {
|
||||||
|
removeAccessToken()
|
||||||
|
router.replace(`/webapp-signin?redirect_url=${window.location.href}`)
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement={placement || 'bottom-end'}
|
||||||
|
offset={{
|
||||||
|
mainAxis: 4,
|
||||||
|
crossAxis: -4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||||
|
<div>
|
||||||
|
<ActionButton size='l' className={cn(open && 'bg-state-base-hover')}>
|
||||||
|
<RiEqualizer2Line className='h-[18px] w-[18px]' />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-50'>
|
||||||
|
<div className='w-[224px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
|
||||||
|
<div className='p-1'>
|
||||||
|
{data?.privacy_policy && (
|
||||||
|
<a href={data.privacy_policy} target='_blank' className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
|
||||||
|
<span className='grow'>{t('share.chat.privacyPolicyMiddle')}</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
handleTrigger()
|
||||||
|
setShow(true)
|
||||||
|
}}
|
||||||
|
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
|
||||||
|
>{t('common.userProfile.about')}</div>
|
||||||
|
{!hideLogout && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
handleLogout()
|
||||||
|
}}
|
||||||
|
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-destructive hover:bg-state-base-hover'
|
||||||
|
>{t('common.userProfile.logout')}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
{show && (
|
||||||
|
<InfoModal
|
||||||
|
isShow={show}
|
||||||
|
onClose={() => {
|
||||||
|
setShow(false)
|
||||||
|
}}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(MenuDropdown)
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
|
||||||
|
import { AccessMode } from '@/models/access-control'
|
||||||
|
import type { App } from '@/types/app'
|
||||||
|
|
||||||
|
type AccessControlStore = {
|
||||||
|
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
|
||||||
|
selectedGroupsForBreadcrumb: AccessControlGroup[]
|
||||||
|
setSelectedGroupsForBreadcrumb: (selectedGroupsForBreadcrumb: AccessControlGroup[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAccessControlStore = create<AccessControlStore>((set) => {
|
||||||
|
return {
|
||||||
|
appId: '',
|
||||||
|
setAppId: appId => set({ appId }),
|
||||||
|
specificGroups: [],
|
||||||
|
setSpecificGroups: specificGroups => set({ specificGroups }),
|
||||||
|
specificMembers: [],
|
||||||
|
setSpecificMembers: specificMembers => set({ specificMembers }),
|
||||||
|
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||||
|
setCurrentMenu: currentMenu => set({ currentMenu }),
|
||||||
|
selectedGroupsForBreadcrumb: [],
|
||||||
|
setSelectedGroupsForBreadcrumb: selectedGroupsForBreadcrumb => set({ selectedGroupsForBreadcrumb }),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useAccessControlStore
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
export enum SubjectType {
|
||||||
|
GROUP = 'group',
|
||||||
|
ACCOUNT = 'account',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccessMode {
|
||||||
|
PUBLIC = 'public',
|
||||||
|
SPECIFIC_GROUPS_MEMBERS = 'private',
|
||||||
|
ORGANIZATION = 'private_all',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccessControlGroup = {
|
||||||
|
'id': 'string'
|
||||||
|
'name': 'string'
|
||||||
|
'groupSize': 5
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccessControlAccount = {
|
||||||
|
'id': 'string'
|
||||||
|
'name': 'string'
|
||||||
|
'email': 'string'
|
||||||
|
'avatar': 'string'
|
||||||
|
'avatarUrl': 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubjectGroup = { subjectId: string; subjectType: SubjectType; groupData: AccessControlGroup }
|
||||||
|
export type SubjectAccount = { subjectId: string; subjectType: SubjectType; accountData: AccessControlAccount }
|
||||||
|
|
||||||
|
export type Subject = SubjectGroup | SubjectAccount
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { get, post } from './base'
|
||||||
|
import { getAppAccessMode, getUserCanAccess } from './share'
|
||||||
|
import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
|
||||||
|
import type { App } from '@/types/app'
|
||||||
|
|
||||||
|
const NAME_SPACE = 'access-control'
|
||||||
|
|
||||||
|
export const useAppWhiteListSubjects = (appId: string | undefined, enabled: boolean) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'app-whitelist-subjects', appId],
|
||||||
|
queryFn: () => get<{ groups: AccessControlGroup[]; members: AccessControlAccount[] }>(`/enterprise/webapp/app/subjects?appId=${appId}`),
|
||||||
|
enabled: !!appId && enabled,
|
||||||
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResults = {
|
||||||
|
currPage: number
|
||||||
|
totalPages: number
|
||||||
|
subjects: Subject[]
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSearchForWhiteListCandidates = (query: { keyword?: string; groupId?: AccessControlGroup['id']; resultsPerPage?: number }, enabled: boolean) => {
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'app-whitelist-candidates', query],
|
||||||
|
queryFn: ({ pageParam }) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
Object.keys(query).forEach((key) => {
|
||||||
|
const typedKey = key as keyof typeof query
|
||||||
|
if (query[typedKey])
|
||||||
|
params.append(key, `${query[typedKey]}`)
|
||||||
|
})
|
||||||
|
params.append('pageNumber', `${pageParam}`)
|
||||||
|
return get<SearchResults>(`/enterprise/webapp/app/subject/search?${new URLSearchParams(params).toString()}`)
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
if (lastPage.hasMore)
|
||||||
|
return lastPage.currPage + 1
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateAccessModeParams = {
|
||||||
|
appId: App['id']
|
||||||
|
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
|
||||||
|
accessMode: AccessMode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateAccessMode = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: [NAME_SPACE, 'update-access-mode'],
|
||||||
|
mutationFn: (params: UpdateAccessModeParams) => {
|
||||||
|
return post('/enterprise/webapp/app/access-mode', { body: params })
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [NAME_SPACE, 'app-whitelist-subjects'],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetAppAccessMode = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'app-access-mode', appId],
|
||||||
|
queryFn: () => getAppAccessMode(appId!, isInstalledApp),
|
||||||
|
enabled: !!appId,
|
||||||
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled = true }: { appId?: string; isInstalledApp?: boolean; enabled?: boolean }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'user-can-access-app', appId],
|
||||||
|
queryFn: () => getUserCanAccess(appId!, isInstalledApp),
|
||||||
|
enabled: !!appId && enabled,
|
||||||
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,35 +1,26 @@
|
|||||||
html[data-theme="dark"] {
|
html[data-theme="dark"] {
|
||||||
--color-chatbot-bg: linear-gradient(
|
--color-chatbot-bg: linear-gradient(180deg,
|
||||||
180deg,
|
rgba(34, 34, 37, 0.9) 0%,
|
||||||
rgba(34, 34, 37, 0.9) 0%,
|
rgba(29, 29, 32, 0.9) 90.48%);
|
||||||
rgba(29, 29, 32, 0.9) 90.48%
|
--color-chat-bubble-bg: linear-gradient(180deg,
|
||||||
);
|
rgba(200, 206, 218, 0.08) 0%,
|
||||||
--color-chat-bubble-bg: linear-gradient(
|
rgba(200, 206, 218, 0.02) 100%);
|
||||||
180deg,
|
--color-workflow-process-bg: linear-gradient(90deg,
|
||||||
rgba(200, 206, 218, 0.08) 0%,
|
rgba(24, 24, 27, 0.25) 0%,
|
||||||
rgba(200, 206, 218, 0.02) 100%
|
rgba(24, 24, 27, 0.04) 100%);
|
||||||
);
|
--color-account-teams-bg: linear-gradient(271deg,
|
||||||
--color-workflow-process-bg: linear-gradient(
|
rgba(34, 34, 37, 0.9) -0.1%,
|
||||||
90deg,
|
rgba(29, 29, 32, 0.9) 98.26%);
|
||||||
rgba(24, 24, 27, 0.25) 0%,
|
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
|
||||||
rgba(24, 24, 27, 0.04) 100%
|
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
|
||||||
);
|
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%);
|
||||||
--color-account-teams-bg: linear-gradient(
|
--color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%);
|
||||||
271deg,
|
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #24252E 0%, #1E1E21 100%);
|
||||||
rgba(34, 34, 37, 0.9) -0.1%,
|
--color-dataset-option-card-purple-gradient: linear-gradient(90deg, #25242E 0%, #1E1E21 100%);
|
||||||
rgba(29, 29, 32, 0.9) 98.26%
|
--color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%);
|
||||||
);
|
--color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%);
|
||||||
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
|
--mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg,
|
||||||
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
|
rgba(24, 24, 27, 0.08) 0%,
|
||||||
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%);
|
rgba(0, 0, 0, 0) 100%);
|
||||||
--color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%);
|
--color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.03) 100%);
|
||||||
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #24252E 0%, #1E1E21 100%);
|
|
||||||
--color-dataset-option-card-purple-gradient: linear-gradient(90deg, #25242E 0%, #1E1E21 100%);
|
|
||||||
--color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%);
|
|
||||||
--color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%);
|
|
||||||
--mask-top2bottom-gray-50-to-transparent: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(24, 24, 27, 0.08) 0%,
|
|
||||||
rgba(0, 0, 0, 0) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@ -1,35 +1,26 @@
|
|||||||
html[data-theme="light"] {
|
html[data-theme="light"] {
|
||||||
--color-chatbot-bg: linear-gradient(
|
--color-chatbot-bg: linear-gradient(180deg,
|
||||||
180deg,
|
rgba(249, 250, 251, 0.9) 0%,
|
||||||
rgba(249, 250, 251, 0.9) 0%,
|
rgba(242, 244, 247, 0.9) 90.48%);
|
||||||
rgba(242, 244, 247, 0.9) 90.48%
|
--color-chat-bubble-bg: linear-gradient(180deg,
|
||||||
);
|
#fff 0%,
|
||||||
--color-chat-bubble-bg: linear-gradient(
|
rgba(255, 255, 255, 0.6) 100%);
|
||||||
180deg,
|
--color-workflow-process-bg: linear-gradient(90deg,
|
||||||
#fff 0%,
|
rgba(200, 206, 218, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0.6) 100%
|
rgba(200, 206, 218, 0.04) 100%);
|
||||||
);
|
--color-account-teams-bg: linear-gradient(271deg,
|
||||||
--color-workflow-process-bg: linear-gradient(
|
rgba(249, 250, 251, 0.9) -0.1%,
|
||||||
90deg,
|
rgba(242, 244, 247, 0.9) 98.26%);
|
||||||
rgba(200, 206, 218, 0.2) 0%,
|
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
|
||||||
rgba(200, 206, 218, 0.04) 100%
|
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
|
||||||
);
|
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%);
|
||||||
--color-account-teams-bg: linear-gradient(
|
--color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%);
|
||||||
271deg,
|
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #F2F4F7 0%, #F9FAFB 100%);
|
||||||
rgba(249, 250, 251, 0.9) -0.1%,
|
--color-dataset-option-card-purple-gradient: linear-gradient(90deg, #F0EEFA 0%, #F9FAFB 100%);
|
||||||
rgba(242, 244, 247, 0.9) 98.26%
|
--color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%);
|
||||||
);
|
--color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%);
|
||||||
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
|
--mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg,
|
||||||
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
|
rgba(200, 206, 218, 0.2) 0%,
|
||||||
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%);
|
rgba(255, 255, 255, 0) 100%);
|
||||||
--color-dataset-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%);
|
--color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||||
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #F2F4F7 0%, #F9FAFB 100%);
|
|
||||||
--color-dataset-option-card-purple-gradient: linear-gradient(90deg, #F0EEFA 0%, #F9FAFB 100%);
|
|
||||||
--color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%);
|
|
||||||
--color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%);
|
|
||||||
--mask-top2bottom-gray-50-to-transparent: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(200, 206, 218, 0.2) 0%,
|
|
||||||
rgba(255, 255, 255, 0) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue