| @@ -15,13 +15,14 @@ import { | |||
| } from '@remixicon/react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useShallow } from 'zustand/react/shallow' | |||
| import { useContextSelector } from 'use-context-selector' | |||
| import s from './style.module.css' | |||
| import cn from '@/utils/classnames' | |||
| import { useStore } from '@/app/components/app/store' | |||
| import AppSideBar from '@/app/components/app-sidebar' | |||
| import type { NavIcon } from '@/app/components/app-sidebar/navLink' | |||
| import { fetchAppDetail } from '@/service/apps' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { fetchAppDetail, fetchAppSSO } from '@/service/apps' | |||
| import AppContext, { useAppContext } from '@/context/app-context' | |||
| import Loading from '@/app/components/base/loading' | |||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| @@ -52,6 +53,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| icon: NavIcon | |||
| selectedIcon: NavIcon | |||
| }>>([]) | |||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||
| const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { | |||
| const navs = [ | |||
| @@ -114,8 +116,13 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| router.replace(`/app/${appId}/configuration`) | |||
| } | |||
| else { | |||
| setAppDetail(res) | |||
| setAppDetail({ ...res, enable_sso: false }) | |||
| setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode)) | |||
| if (systemFeatures.enable_web_sso_switch_component) { | |||
| fetchAppSSO({ appId }).then((ssoRes) => { | |||
| setAppDetail({ ...res, enable_sso: ssoRes.enabled }) | |||
| }) | |||
| } | |||
| } | |||
| }).catch((e: any) => { | |||
| if (e.status === 404) | |||
| @@ -2,22 +2,25 @@ | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import { useContext, useContextSelector } from 'use-context-selector' | |||
| import AppCard from '@/app/components/app/overview/appCard' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import { | |||
| fetchAppDetail, | |||
| fetchAppSSO, | |||
| updateAppSSO, | |||
| updateAppSiteAccessToken, | |||
| updateAppSiteConfig, | |||
| updateAppSiteStatus, | |||
| } from '@/service/apps' | |||
| import type { App } from '@/types/app' | |||
| import type { App, AppSSO } from '@/types/app' | |||
| import type { UpdateAppSiteCodeResponse } from '@/models/app' | |||
| import { asyncRunSafe } from '@/utils' | |||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | |||
| import type { IAppCardProps } from '@/app/components/app/overview/appCard' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| import AppContext from '@/context/app-context' | |||
| export type ICardViewProps = { | |||
| appId: string | |||
| @@ -28,11 +31,20 @@ const CardView: FC<ICardViewProps> = ({ appId }) => { | |||
| const { notify } = useContext(ToastContext) | |||
| const appDetail = useAppStore(state => state.appDetail) | |||
| const setAppDetail = useAppStore(state => state.setAppDetail) | |||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||
| const updateAppDetail = async () => { | |||
| fetchAppDetail({ url: '/apps', id: appId }).then((res) => { | |||
| setAppDetail(res) | |||
| }) | |||
| try { | |||
| const res = await fetchAppDetail({ url: '/apps', id: appId }) | |||
| if (systemFeatures.enable_web_sso_switch_component) { | |||
| const ssoRes = await fetchAppSSO({ appId }) | |||
| setAppDetail({ ...res, enable_sso: ssoRes.enabled }) | |||
| } | |||
| else { | |||
| setAppDetail({ ...res }) | |||
| } | |||
| } | |||
| catch (error) { console.error(error) } | |||
| } | |||
| const handleCallbackResult = (err: Error | null, message?: string) => { | |||
| @@ -81,6 +93,16 @@ const CardView: FC<ICardViewProps> = ({ appId }) => { | |||
| if (!err) | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| if (systemFeatures.enable_web_sso_switch_component) { | |||
| const [sso_err] = await asyncRunSafe<AppSSO>( | |||
| updateAppSSO({ id: appId, enabled: params.enable_sso }) as Promise<AppSSO>, | |||
| ) | |||
| if (sso_err) { | |||
| handleCallbackResult(sso_err) | |||
| return | |||
| } | |||
| } | |||
| handleCallbackResult(err) | |||
| } | |||
| @@ -27,10 +27,11 @@ import ShareQRCode from '@/app/components/base/qrcode' | |||
| import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button' | |||
| import type { AppDetailResponse } from '@/models/app' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import type { AppSSO } from '@/types/app' | |||
| export type IAppCardProps = { | |||
| className?: string | |||
| appInfo: AppDetailResponse | |||
| appInfo: AppDetailResponse & Partial<AppSSO> | |||
| cardType?: 'api' | 'webapp' | |||
| customBgColor?: string | |||
| onChangeStatus: (val: boolean) => Promise<void> | |||
| @@ -4,21 +4,25 @@ import React, { useEffect, useState } from 'react' | |||
| import { ChevronRightIcon } from '@heroicons/react/20/solid' | |||
| import Link from 'next/link' | |||
| import { Trans, useTranslation } from 'react-i18next' | |||
| import { useContextSelector } from 'use-context-selector' | |||
| import s from './style.module.css' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import Switch from '@/app/components/base/switch' | |||
| import { SimpleSelect } from '@/app/components/base/select' | |||
| import type { AppDetailResponse } from '@/models/app' | |||
| import type { AppIconType, Language } from '@/types/app' | |||
| import type { AppIconType, AppSSO, Language } from '@/types/app' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import { languages } from '@/i18n/language' | |||
| import TooltipPlus from '@/app/components/base/tooltip-plus' | |||
| import AppContext from '@/context/app-context' | |||
| import type { AppIconSelection } from '@/app/components/base/app-icon-picker' | |||
| import AppIconPicker from '@/app/components/base/app-icon-picker' | |||
| export type ISettingsModalProps = { | |||
| isChat: boolean | |||
| appInfo: AppDetailResponse | |||
| appInfo: AppDetailResponse & Partial<AppSSO> | |||
| isShow: boolean | |||
| defaultValue?: string | |||
| onClose: () => void | |||
| @@ -39,6 +43,7 @@ export type ConfigParams = { | |||
| icon: string | |||
| icon_background?: string | |||
| show_workflow_steps: boolean | |||
| enable_sso?: boolean | |||
| } | |||
| const prefixSettings = 'appOverview.overview.appInfo.settings' | |||
| @@ -50,6 +55,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| onClose, | |||
| onSave, | |||
| }) => { | |||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||
| const { notify } = useToastContext() | |||
| const [isShowMore, setIsShowMore] = useState(false) | |||
| const { | |||
| @@ -76,6 +82,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| privacyPolicy: privacy_policy, | |||
| customDisclaimer: custom_disclaimer, | |||
| show_workflow_steps, | |||
| enable_sso: appInfo.enable_sso, | |||
| }) | |||
| const [language, setLanguage] = useState(default_language) | |||
| const [saveLoading, setSaveLoading] = useState(false) | |||
| @@ -98,6 +105,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| privacyPolicy: privacy_policy, | |||
| customDisclaimer: custom_disclaimer, | |||
| show_workflow_steps, | |||
| enable_sso: appInfo.enable_sso, | |||
| }) | |||
| setLanguage(default_language) | |||
| setAppIcon(icon_type === 'image' | |||
| @@ -149,6 +157,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||
| show_workflow_steps: inputInfo.show_workflow_steps, | |||
| enable_sso: inputInfo.enable_sso, | |||
| } | |||
| await onSave?.(params) | |||
| setSaveLoading(false) | |||
| @@ -219,9 +228,19 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`} | |||
| value={inputInfo.chatColorTheme ?? ''} | |||
| onChange={onChange('chatColorTheme')} | |||
| placeholder= 'E.g #A020F0' | |||
| placeholder='E.g #A020F0' | |||
| /> | |||
| </>} | |||
| {systemFeatures.enable_web_sso_switch_component && <div className='w-full mt-8'> | |||
| <p className='system-xs-medium text-gray-500'>{t(`${prefixSettings}.sso.label`)}</p> | |||
| <div className='flex justify-between items-center'> | |||
| <div className='font-medium system-sm-semibold flex-grow text-gray-900'>{t(`${prefixSettings}.sso.title`)}</div> | |||
| <TooltipPlus disabled={systemFeatures.sso_enforced_for_web} popupContent={<div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>}> | |||
| <Switch disabled={!systemFeatures.sso_enforced_for_web} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch> | |||
| </TooltipPlus> | |||
| </div> | |||
| <p className='body-xs-regular text-gray-500'>{t(`${prefixSettings}.sso.description`)}</p> | |||
| </div>} | |||
| {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}> | |||
| <div className='flex justify-between'> | |||
| <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div> | |||
| @@ -1,9 +1,9 @@ | |||
| import { create } from 'zustand' | |||
| import type { App } from '@/types/app' | |||
| import type { App, AppSSO } from '@/types/app' | |||
| import type { IChatItem } from '@/app/components/base/chat/chat/type' | |||
| type State = { | |||
| appDetail?: App | |||
| appDetail?: App & Partial<AppSSO> | |||
| appSidebarExpand: string | |||
| currentLogItem?: IChatItem | |||
| currentLogModalActiveTab: string | |||
| @@ -13,7 +13,7 @@ type State = { | |||
| } | |||
| type Action = { | |||
| setAppDetail: (appDetail?: App) => void | |||
| setAppDetail: (appDetail?: App & Partial<AppSSO>) => void | |||
| setAppSiderbarExpand: (state: string) => void | |||
| setCurrentLogItem: (item?: IChatItem) => void | |||
| setCurrentLogModalActiveTab: (tab: string) => void | |||
| @@ -6,16 +6,19 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec | |||
| import type { FC, ReactNode } from 'react' | |||
| import { fetchAppList } from '@/service/apps' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common' | |||
| import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common' | |||
| import type { App } from '@/types/app' | |||
| import { Theme } from '@/types/app' | |||
| import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' | |||
| import MaintenanceNotice from '@/app/components/header/maintenance-notice' | |||
| import type { SystemFeatures } from '@/types/feature' | |||
| import { defaultSystemFeatures } from '@/types/feature' | |||
| export type AppContextValue = { | |||
| theme: Theme | |||
| setTheme: (theme: Theme) => void | |||
| apps: App[] | |||
| systemFeatures: SystemFeatures | |||
| mutateApps: VoidFunction | |||
| userProfile: UserProfileResponse | |||
| mutateUserProfile: VoidFunction | |||
| @@ -53,6 +56,7 @@ const initialWorkspaceInfo: ICurrentWorkspace = { | |||
| const AppContext = createContext<AppContextValue>({ | |||
| theme: Theme.light, | |||
| systemFeatures: defaultSystemFeatures, | |||
| setTheme: () => { }, | |||
| apps: [], | |||
| mutateApps: () => { }, | |||
| @@ -90,6 +94,10 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) | |||
| const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace) | |||
| const { data: systemFeatures } = useSWR({ url: '/console/system-features' }, getSystemFeatures, { | |||
| fallbackData: defaultSystemFeatures, | |||
| }) | |||
| const [userProfile, setUserProfile] = useState<UserProfileResponse>() | |||
| const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo) | |||
| const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) | |||
| @@ -136,6 +144,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| theme, | |||
| setTheme: handleSetTheme, | |||
| apps: appList.data, | |||
| systemFeatures, | |||
| mutateApps, | |||
| userProfile, | |||
| mutateUserProfile, | |||
| @@ -53,6 +53,12 @@ const translation = { | |||
| chatColorThemeDesc: 'Set the color theme of the chatbot', | |||
| chatColorThemeInverted: 'Inverted', | |||
| invalidHexMessage: 'Invalid hex value', | |||
| sso: { | |||
| label: 'SSO Authentication', | |||
| title: 'WebApp SSO', | |||
| description: 'All users are required to login with SSO before using WebApp', | |||
| tooltip: 'Contact the administrator to enable WebApp SSO', | |||
| }, | |||
| more: { | |||
| entry: 'Show more settings', | |||
| copyright: 'Copyright', | |||
| @@ -53,6 +53,12 @@ const translation = { | |||
| chatColorThemeDesc: '设置聊天机器人的颜色主题', | |||
| chatColorThemeInverted: '反转', | |||
| invalidHexMessage: '无效的十六进制值', | |||
| sso: { | |||
| label: '单点登录认证', | |||
| title: 'WebApp SSO 认证', | |||
| description: '启用后,所有用户都需要先进行 SSO 认证才能访问', | |||
| tooltip: '联系管理员以开启 WebApp SSO 认证', | |||
| }, | |||
| more: { | |||
| entry: '展示更多设置', | |||
| copyright: '版权', | |||
| @@ -1,5 +1,5 @@ | |||
| import type { LangFuseConfig, LangSmithConfig, TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' | |||
| import type { App, AppTemplate, SiteConfig } from '@/types/app' | |||
| import type { App, AppSSO, AppTemplate, SiteConfig } from '@/types/app' | |||
| /* export type App = { | |||
| id: string | |||
| @@ -67,6 +67,7 @@ export type AppListResponse = { | |||
| } | |||
| export type AppDetailResponse = App | |||
| export type AppSSOResponse = { enabled: AppSSO['enable_sso'] } | |||
| export type AppTemplatesResponse = { | |||
| data: AppTemplate[] | |||
| @@ -1,6 +1,6 @@ | |||
| import type { Fetcher } from 'swr' | |||
| import { del, get, patch, post, put } from './base' | |||
| import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' | |||
| import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppSSOResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' | |||
| import type { CommonResponse } from '@/models/common' | |||
| import type { AppIconType, AppMode, ModelConfig } from '@/types/app' | |||
| import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' | |||
| @@ -13,6 +13,13 @@ export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => { | |||
| return get<AppDetailResponse>(`${url}/${id}`) | |||
| } | |||
| export const fetchAppSSO = async ({ appId }: { appId: string }) => { | |||
| return get<AppSSOResponse>(`/enterprise/app-setting/sso?appID=${appId}`) | |||
| } | |||
| export const updateAppSSO = async ({ id, enabled }: { id: string; enabled: boolean }) => { | |||
| return post('/enterprise/app-setting/sso', { body: { app_id: id, enabled } }) | |||
| } | |||
| export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => { | |||
| return get<AppTemplatesResponse>(url) | |||
| } | |||
| @@ -348,6 +348,10 @@ export type App = { | |||
| tags: Tag[] | |||
| } | |||
| export type AppSSO = { | |||
| enable_sso: boolean | |||
| } | |||
| /** | |||
| * App Template | |||
| */ | |||
| @@ -3,6 +3,7 @@ export type SystemFeatures = { | |||
| sso_enforced_for_signin_protocol: string | |||
| sso_enforced_for_web: boolean | |||
| sso_enforced_for_web_protocol: string | |||
| enable_web_sso_switch_component: boolean | |||
| } | |||
| export const defaultSystemFeatures: SystemFeatures = { | |||
| @@ -10,4 +11,5 @@ export const defaultSystemFeatures: SystemFeatures = { | |||
| sso_enforced_for_signin_protocol: '', | |||
| sso_enforced_for_web: false, | |||
| sso_enforced_for_web_protocol: '', | |||
| enable_web_sso_switch_component: false, | |||
| } | |||