-
+ if (systemFeatures.webapp_auth.enabled) {
+ if (systemFeatures.webapp_auth.allow_sso) {
+ return (
+
+ )
+ }
+ return
+
+
+
+
+
{t('login.webapp.noLoginMethod')}
+
{t('login.webapp.noLoginMethodTip')}
+
+
- )
+ }
+ else {
+ return
+
{t('login.webapp.disabled')}
+
+ }
}
export default React.memo(WebSSOForm)
diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx
index 72d2648c23..19c1e44236 100644
--- a/web/app/account/account-page/index.tsx
+++ b/web/app/account/account-page/index.tsx
@@ -20,6 +20,7 @@ import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input'
import PremiumBadge from '@/app/components/base/premium-badge'
+import { useGlobalPublicStore } from '@/context/global-public-context'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -32,7 +33,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function AccountPage() {
const { t } = useTranslation()
- const { systemFeatures } = useAppContext()
+ const { systemFeatures } = useGlobalPublicStore()
const { mutateUserProfile, userProfile, apps } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { notify } = useContext(ToastContext)
@@ -138,7 +139,7 @@ export default function AccountPage() {
{t('common.account.myAccount')}
-
+
{userProfile.name}
diff --git a/web/app/account/layout.tsx b/web/app/account/layout.tsx
index 9ee7435ac0..e74716fb3b 100644
--- a/web/app/account/layout.tsx
+++ b/web/app/account/layout.tsx
@@ -32,9 +32,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
>
)
}
-
-export const metadata = {
- title: 'Dify',
-}
-
export default Layout
diff --git a/web/app/account/page.tsx b/web/app/account/page.tsx
index baf386e2ba..e4896cdeb6 100644
--- a/web/app/account/page.tsx
+++ b/web/app/account/page.tsx
@@ -1,6 +1,11 @@
+'use client'
+import { useTranslation } from 'react-i18next'
import AccountPage from './account-page'
+import useDocumentTitle from '@/hooks/use-document-title'
export default function Account() {
+ const { t } = useTranslation()
+ useDocumentTitle(t('common.menus.account'))
return
diff --git a/web/app/activate/activateForm.tsx b/web/app/activate/activateForm.tsx
index 782b24be6e..d9d07cbfa1 100644
--- a/web/app/activate/activateForm.tsx
+++ b/web/app/activate/activateForm.tsx
@@ -7,8 +7,10 @@ import Button from '@/app/components/base/button'
import { invitationCheck } from '@/service/common'
import Loading from '@/app/components/base/loading'
+import useDocumentTitle from '@/hooks/use-document-title'
const ActivateForm = () => {
+ useDocumentTitle('')
const router = useRouter()
const { t } = useTranslation()
const searchParams = useSearchParams()
diff --git a/web/app/activate/page.tsx b/web/app/activate/page.tsx
index 221559ff28..cfb1e6b149 100644
--- a/web/app/activate/page.tsx
+++ b/web/app/activate/page.tsx
@@ -1,17 +1,20 @@
+'use client'
import React from 'react'
import Header from '../signin/_header'
import ActivateForm from './activateForm'
import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
const Activate = () => {
+ const { systemFeatures } = useGlobalPublicStore()
return (
-
+ {!systemFeatures.branding.enabled &&
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
-
+
}
)
diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx
index 18a9ac8bc8..27a6e352a8 100644
--- a/web/app/components/app-sidebar/app-info.tsx
+++ b/web/app/components/app-sidebar/app-info.tsx
@@ -6,19 +6,19 @@ import {
RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
- RiFileCopy2Line,
RiFileDownloadLine,
RiFileUploadLine,
} from '@remixicon/react'
import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal'
+import AccessControl from '../app/app-access-control'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import AppsContext, { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
-import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
+import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps'
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import CreateAppModal from '@/app/components/explore/create-app-modal'
@@ -32,6 +32,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
import ContentDialog from '@/app/components/base/content-dialog'
import Button from '@/app/components/base/button'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
+import Divider from '../base/divider'
export type IAppInfoProps = {
expand: boolean
@@ -50,6 +51,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showSwitchModal, setShowSwitchModal] = useState
(false)
const [showImportDSLModal, setShowImportDSLModal] = useState(false)
+ const [showAccessControl, setShowAccessControl] = useState(false)
const [secretEnvList, setSecretEnvList] = useState([])
const mutateApps = useContextSelector(
@@ -177,6 +179,19 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t])
+ const handleClickAccessControl = useCallback(() => {
+ if (!appDetail)
+ return
+ setShowAccessControl(true)
+ setOpen(false)
+ }, [appDetail])
+ const handleAccessControlUpdate = useCallback(() => {
+ fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
+ setAppDetail(res)
+ setShowAccessControl(false)
+ })
+ }, [appDetail, setAppDetail])
+
const { isCurrentWorkspaceEditor } = useAppContext()
if (!appDetail)
@@ -262,10 +277,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onClick={() => {
setOpen(false)
setShowDuplicateModal(true)
- }}
- >
-
- {t('app.duplicate')}
+ }}>
+ {t('app.duplicate')}
{
className='flex grow flex-col gap-2 overflow-auto px-2 py-1'
/>
+
+ {/* TODO update style figma */}
+
+ {t('app.accessControl')}
+
{
onClose={() => setSecretEnvList([])}
/>
)}
+ {
+ showAccessControl && { setShowAccessControl(false) }} />
+ }
)
}
diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx
index 3276a1c0a7..f58985ed96 100644
--- a/web/app/components/app-sidebar/index.tsx
+++ b/web/app/components/app-sidebar/index.tsx
@@ -16,7 +16,7 @@ export type IAppDetailNavProps = {
desc: string
isExternal?: boolean
icon: string
- icon_background: string
+ icon_background: string | null
navigation: Array<{
name: string
href: string
diff --git a/web/app/components/app/app-access-control/access-control-dialog.tsx b/web/app/components/app/app-access-control/access-control-dialog.tsx
new file mode 100644
index 0000000000..e3e013fbd4
--- /dev/null
+++ b/web/app/components/app/app-access-control/access-control-dialog.tsx
@@ -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 (
+
+ null}>
+
+
+
+
+
+
+
+ close()} className="absolute right-5 top-5 z-10 flex h-8 w-8 cursor-pointer items-center justify-center">
+
+
+ {children}
+
+
+
+
+
+ )
+}
+
+export default AccessControlDialog
diff --git a/web/app/components/app/app-access-control/access-control-item.tsx b/web/app/components/app/app-access-control/access-control-item.tsx
new file mode 100644
index 0000000000..0840902371
--- /dev/null
+++ b/web/app/components/app/app-access-control/access-control-item.tsx
@@ -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
= ({ type, children }) => {
+ const { currentMenu, setCurrentMenu } = useAccessControlStore(s => ({ currentMenu: s.currentMenu, setCurrentMenu: s.setCurrentMenu }))
+ if (currentMenu !== type) {
+ return setCurrentMenu(type)} >
+ {children}
+
+ }
+
+ return
+ {children}
+
+}
+
+AccessControlItem.displayName = 'AccessControlItem'
+
+export default AccessControlItem
diff --git a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx
new file mode 100644
index 0000000000..75a147f803
--- /dev/null
+++ b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx
@@ -0,0 +1,204 @@
+'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 { FloatingOverlay } from '@floating-ui/react'
+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 { isLoading, isFetchingNextPage, fetchNextPage, data } = useSearchForWhiteListCandidates({ keyword: debouncedKeyword, groupId: lastAvailableGroup?.id, resultsPerPage: 10 }, open)
+ const handleKeywordChange = (e: React.ChangeEvent) => {
+ setKeyword(e.target.value)
+ }
+
+ const anchorRef = useRef(null)
+ useEffect(() => {
+ const hasMore = data?.pages?.[0].hasMore ?? false
+ let observer: IntersectionObserver | undefined
+ if (anchorRef.current) {
+ observer = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && !isLoading && hasMore)
+ fetchNextPage()
+ }, { rootMargin: '20px' })
+ observer.observe(anchorRef.current)
+ }
+ return () => observer?.disconnect()
+ }, [isLoading, fetchNextPage, anchorRef, data])
+
+ return
+
+ setOpen(!open)}>
+
+ {t('common.operation.add')}
+
+
+ {open && }
+
+
+
+
+
+ {
+ isLoading
+ ?
+ : (data?.pages?.length ?? 0) > 0
+ ? <>
+
+
+
+
+ {renderGroupOrMember(data?.pages ?? [])}
+ {isFetchingNextPage && }
+
+
+ >
+ :
+ {t('app.accessControlDialog.operateGroupAndMember.noResult')}
+
+ }
+
+
+
+}
+
+type GroupOrMemberData = { subjects: Subject[]; currPage: number }[]
+function renderGroupOrMember(data: GroupOrMemberData) {
+ return data?.map((page) => {
+ return
+ {page.subjects?.map((item, index) => {
+ if (item.subjectType === SubjectType.GROUP)
+ return
+ return
+ })}
+
+ }) ?? 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
+
0 && 'text-text-accent cursor-pointer')} onClick={handleReset}>{t('app.accessControlDialog.operateGroupAndMember.allMembers')}
+ {selectedGroupsForBreadcrumb.map((group, index) => {
+ return
+ /
+ handleBreadCrumbClick(index)}>{group.name}
+
+ })}
+
+}
+
+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
+
+
+
+
{group.name}
+
{group.groupSize}
+
+
+ {t('app.accessControlDialog.operateGroupAndMember.expand')}
+
+
+
+}
+
+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
+
+
+
+
{member.name}
+ {currentUser.email === member.email &&
({t('common.you')})
}
+
+ {member.email}
+
+}
+
+type BaseItemProps = {
+ className?: string
+ children: React.ReactNode
+}
+function BaseItem({ children, className }: BaseItemProps) {
+ return
+ {children}
+
+}
diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx
new file mode 100644
index 0000000000..2f15c8ec48
--- /dev/null
+++ b/web/app/components/app/app-access-control/index.tsx
@@ -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[]
+ } = { appId: app.id, accessMode: currentMenu }
+ if (currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) {
+ const subjects: Pick[] = []
+ 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
+
+
+ {t('app.accessControlDialog.title')}
+ {t('app.accessControlDialog.description')}
+
+
+
+
{t('app.accessControlDialog.accessLabel')}
+
+
+
+
+
+
{t('app.accessControlDialog.accessItems.organization')}
+
+ {!hideTip &&
}
+
+
+
+
+
+
+
+
+
{t('app.accessControlDialog.accessItems.anyone')}
+
+
+
+
+ {t('common.operation.cancel')}
+ {t('common.operation.confirm')}
+
+
+
+}
diff --git a/web/app/components/app/app-access-control/specific-groups-or-members.tsx b/web/app/components/app/app-access-control/specific-groups-or-members.tsx
new file mode 100644
index 0000000000..f4872f8c99
--- /dev/null
+++ b/web/app/components/app/app-access-control/specific-groups-or-members.tsx
@@ -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
+
+
+
{t('app.accessControlDialog.accessItems.specific')}
+
+ {!hideTip &&
}
+
+ }
+
+ return
+
+
+
+
{t('app.accessControlDialog.accessItems.specific')}
+
+
+ {!hideTip && <>
+
+
+ >}
+
+
+
+
+
+}
+
+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 {t('app.accessControlDialog.noGroupsOrMembers')}
+ return <>
+ {t('app.accessControlDialog.groups', { count: specificGroups.length ?? 0 })}
+
+ {specificGroups.map((group, index) => )}
+
+ {t('app.accessControlDialog.members', { count: specificMembers.length ?? 0 })}
+
+ {specificMembers.map((member, index) => )}
+
+ >
+}
+
+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 }
+ onRemove={handleRemoveGroup}>
+ {group.name}
+ {group.groupSize}
+
+}
+
+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 }
+ onRemove={handleRemoveMember}>
+ {member.name}
+
+}
+
+type BaseItemProps = {
+ icon: React.ReactNode
+ children: React.ReactNode
+ onRemove?: () => void
+}
+function BaseItem({ icon, onRemove, children }: BaseItemProps) {
+ return
+}
+
+export function WebAppSSONotEnabledTip() {
+ const { t } = useTranslation()
+ return
+
+
+}
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index d4357a0955..8d905fba48 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -1,21 +1,28 @@
import {
memo,
useCallback,
+ useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import {
RiArrowDownSLine,
+ RiArrowRightSLine,
+ RiLockLine,
RiPlanetLine,
RiPlayCircleLine,
RiPlayList2Line,
RiTerminalBoxLine,
} from '@remixicon/react'
import { useKeyPress } from 'ahooks'
+import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
import Toast from '../../base/toast'
import type { ModelAndParameter } from '../configuration/debug/types'
-import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
+import Divider from '../../base/divider'
+import AccessControl from '../app-access-control'
+import Loading from '../../base/loading'
+import Tooltip from '../../base/tooltip'
import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model'
import Button from '@/app/components/base/button'
@@ -34,6 +41,9 @@ import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/co
import type { InputVar } from '@/app/components/workflow/types'
import { appDefaultIconBackground } from '@/config'
import type { PublishWorkflowParams } from '@/types/workflow'
+import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
+import { AccessMode } from '@/models/access-control'
+import { fetchAppDetail } from '@/service/apps'
export type AppPublisherProps = {
disabled?: boolean
@@ -74,11 +84,32 @@ const AppPublisher = ({
const [published, setPublished] = useState(false)
const [open, setOpen] = useState(false)
const appDetail = useAppStore(state => state.appDetail)
+ const setAppDetail = useAppStore(s => s.setAppDetail)
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
const appURL = `${appBaseURL}/${basePath}/${appMode}/${accessToken}`
const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '')
+ const { data: useCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
+ const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
+ useEffect(() => {
+ if (open && appDetail)
+ refetch()
+ }, [open, appDetail, refetch])
+
+ const [showAppAccessControl, setShowAppAccessControl] = useState(false)
+ const [isAppAccessSet, setIsAppAccessSet] = useState(true)
+ useEffect(() => {
+ if (appDetail && appAccessSubjects) {
+ if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0)
+ setIsAppAccessSet(false)
+ else
+ setIsAppAccessSet(true)
+ }
+ else {
+ setIsAppAccessSet(true)
+ }
+ }, [appAccessSubjects, appDetail])
const language = useGetLanguage()
const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
@@ -99,7 +130,7 @@ const AppPublisher = ({
await onRestore?.()
setOpen(false)
}
- catch {}
+ catch { }
}, [onRestore])
const handleTrigger = useCallback(() => {
@@ -130,6 +161,13 @@ const AppPublisher = ({
}
}, [appDetail?.id])
+ const handleAccessControlUpdate = useCallback(() => {
+ fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
+ setAppDetail(res)
+ setShowAppAccessControl(false)
+ })
+ }, [appDetail, setAppDetail])
+
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
@@ -138,7 +176,7 @@ const AppPublisher = ({
return
handlePublish()
},
- { exactMatch: true, useCapture: true })
+ { exactMatch: true, useCapture: true })
return (
<>
@@ -223,70 +261,106 @@ const AppPublisher = ({
)
}
+
+
{t('app.publishApp.title')}
+
+
{
- setEmbeddingModalOpen(true)
- handleTrigger()
- }}
- disabled={!publishedAt}
- icon={ }
- >
- {t('workflow.common.embedIntoSite')}
-
- )}
- {
- publishedAt && handleOpenInExplore()
- }}
- disabled={!publishedAt}
- icon={ }
- >
- {t('workflow.common.openInExplore')}
-
- }
- >
- {t('workflow.common.accessAPIReference')}
-
- {appDetail?.mode === 'workflow' && (
-
- )}
-
+ setShowAppAccessControl(true)
+ }}>
+
+
+ {appDetail?.access_mode === AccessMode.ORGANIZATION &&
{t('app.accessControlDialog.accessItems.organization')}
}
+ {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS &&
{t('app.accessControlDialog.accessItems.specific')}
}
+ {appDetail?.access_mode === AccessMode.PUBLIC &&
{t('app.accessControlDialog.accessItems.anyone')}
}
+
+ {!isAppAccessSet &&
{t('app.publishApp.notSet')}
}
+
+
+
+