| @@ -1,11 +1,14 @@ | |||
| from flask_login import current_user | |||
| from extensions.ext_database import db | |||
| from models.account import Tenant | |||
| from models.account import Tenant, TenantAccountJoin | |||
| from models.provider import Provider | |||
| class WorkspaceService: | |||
| @classmethod | |||
| def get_tenant_info(cls, tenant: Tenant): | |||
| if not tenant: | |||
| return None | |||
| tenant_info = { | |||
| 'id': tenant.id, | |||
| 'name': tenant.name, | |||
| @@ -13,10 +16,18 @@ class WorkspaceService: | |||
| 'status': tenant.status, | |||
| 'created_at': tenant.created_at, | |||
| 'providers': [], | |||
| 'in_trial': True, | |||
| 'trial_end_reason': None | |||
| 'in_trail': True, | |||
| 'trial_end_reason': None, | |||
| 'role': 'normal', | |||
| } | |||
| # Get role of user | |||
| tenant_account_join = db.session.query(TenantAccountJoin).filter( | |||
| TenantAccountJoin.tenant_id == tenant.id, | |||
| TenantAccountJoin.account_id == current_user.id | |||
| ).first() | |||
| tenant_info['role'] = tenant_account_join.role | |||
| # Get providers | |||
| providers = db.session.query(Provider).filter( | |||
| Provider.tenant_id == tenant.id | |||
| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect } from 'react' | |||
| import React, { useEffect, useMemo } from 'react' | |||
| import cn from 'classnames' | |||
| import useSWR from 'swr' | |||
| import { useTranslation } from 'react-i18next' | |||
| @@ -19,6 +19,7 @@ import { | |||
| import s from './style.module.css' | |||
| import AppSideBar from '@/app/components/app-sidebar' | |||
| import { fetchAppDetail } from '@/service/apps' | |||
| import { useAppContext } from '@/context/app-context' | |||
| export type IAppDetailLayoutProps = { | |||
| children: React.ReactNode | |||
| @@ -31,15 +32,21 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| params: { appId }, // get appId in path | |||
| } = props | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const detailParams = { url: '/apps', id: appId } | |||
| const { data: response } = useSWR(detailParams, fetchAppDetail) | |||
| const navigation = [ | |||
| { name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon }, | |||
| { name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon }, | |||
| { name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon }, | |||
| { name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon }, | |||
| ] | |||
| const navigation = useMemo(() => { | |||
| const navs = [ | |||
| { name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon }, | |||
| { name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon }, | |||
| { name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon }, | |||
| ] | |||
| if (isCurrentWorkspaceManager) | |||
| navs.push({ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon }) | |||
| return navs | |||
| }, [appId, isCurrentWorkspaceManager, t]) | |||
| const appModeName = response?.mode?.toUpperCase() === 'COMPLETION' ? t('common.appModes.completionApp') : t('common.appModes.chatApp') | |||
| useEffect(() => { | |||
| if (response?.name) | |||
| @@ -12,7 +12,7 @@ import Confirm from '@/app/components/base/confirm' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import { deleteApp } from '@/service/apps' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import AppsContext from '@/context/app-context' | |||
| import AppsContext, { useAppContext } from '@/context/app-context' | |||
| export type AppCardProps = { | |||
| app: App | |||
| @@ -25,6 +25,7 @@ const AppCard = ({ | |||
| }: AppCardProps) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useContext(ToastContext) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const mutateApps = useContextSelector(AppsContext, state => state.mutateApps) | |||
| @@ -55,7 +56,8 @@ const AppCard = ({ | |||
| <div className={style.listItemHeading}> | |||
| <div className={style.listItemHeadingContent}>{app.name}</div> | |||
| </div> | |||
| <span className={style.deleteAppIcon} onClick={onDeleteClick} /> | |||
| { isCurrentWorkspaceManager | |||
| && <span className={style.deleteAppIcon} onClick={onDeleteClick} />} | |||
| </div> | |||
| <div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div> | |||
| <div className={style.listItemFooter}> | |||
| @@ -8,7 +8,7 @@ import AppCard from './AppCard' | |||
| import NewAppCard from './NewAppCard' | |||
| import type { AppListResponse } from '@/models/app' | |||
| import { fetchAppList } from '@/service/apps' | |||
| import { useSelector } from '@/context/app-context' | |||
| import { useAppContext, useSelector } from '@/context/app-context' | |||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | |||
| const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | |||
| @@ -19,6 +19,7 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | |||
| const Apps = () => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) | |||
| const loadingStateRef = useRef(false) | |||
| const pageContainerRef = useSelector(state => state.pageContainerRef) | |||
| @@ -55,7 +56,8 @@ const Apps = () => { | |||
| {data?.map(({ data: apps }) => apps.map(app => ( | |||
| <AppCard key={app.id} app={app} onDelete={mutate} /> | |||
| )))} | |||
| <NewAppCard ref={anchorRef} onSuccess={mutate} /> | |||
| { isCurrentWorkspaceManager | |||
| && <NewAppCard ref={anchorRef} onSuccess={mutate} />} | |||
| </nav> | |||
| ) | |||
| } | |||
| @@ -7,7 +7,7 @@ import NewDatasetCard from './NewDatasetCard' | |||
| import DatasetCard from './DatasetCard' | |||
| import type { DataSetListResponse } from '@/models/datasets' | |||
| import { fetchDatasets } from '@/service/datasets' | |||
| import { useSelector } from '@/context/app-context' | |||
| import { useAppContext, useSelector } from '@/context/app-context' | |||
| const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { | |||
| if (!pageIndex || previousPageData.has_more) | |||
| @@ -16,6 +16,7 @@ const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { | |||
| } | |||
| const Datasets = () => { | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false }) | |||
| const loadingStateRef = useRef(false) | |||
| const pageContainerRef = useSelector(state => state.pageContainerRef) | |||
| @@ -44,7 +45,7 @@ const Datasets = () => { | |||
| {data?.map(({ data: datasets }) => datasets.map(dataset => ( | |||
| <DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />), | |||
| ))} | |||
| <NewDatasetCard ref={anchorRef} /> | |||
| { isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> } | |||
| </nav> | |||
| ) | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useState } from 'react' | |||
| import React, { useMemo, useState } from 'react' | |||
| import { | |||
| Cog8ToothIcon, | |||
| DocumentTextIcon, | |||
| @@ -22,6 +22,7 @@ import Switch from '@/app/components/base/switch' | |||
| import type { AppDetailResponse } from '@/models/app' | |||
| import './style.css' | |||
| import { AppType } from '@/types/app' | |||
| import { useAppContext } from '@/context/app-context' | |||
| export type IAppCardProps = { | |||
| className?: string | |||
| @@ -48,22 +49,30 @@ function AppCard({ | |||
| }: IAppCardProps) { | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||
| const [showSettingsModal, setShowSettingsModal] = useState(false) | |||
| const [showShareModal, setShowShareModal] = useState(false) | |||
| const [showEmbedded, setShowEmbedded] = useState(false) | |||
| const [showCustomizeModal, setShowCustomizeModal] = useState(false) | |||
| const { t } = useTranslation() | |||
| const OPERATIONS_MAP = { | |||
| webapp: [ | |||
| { opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon }, | |||
| { opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon }, | |||
| appInfo.mode === AppType.chat ? { opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon } : false, | |||
| { opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon }, | |||
| ].filter(item => !!item), | |||
| api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }], | |||
| app: [], | |||
| } | |||
| const OPERATIONS_MAP = useMemo(() => { | |||
| const operationsMap = { | |||
| webapp: [ | |||
| { opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon }, | |||
| { opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon }, | |||
| ] as { opName: string; opIcon: any }[], | |||
| api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }], | |||
| app: [], | |||
| } | |||
| if (appInfo.mode === AppType.chat) | |||
| operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon }) | |||
| if (isCurrentWorkspaceManager) | |||
| operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon }) | |||
| return operationsMap | |||
| }, [isCurrentWorkspaceManager, appInfo, t]) | |||
| const isApp = cardType === 'app' || cardType === 'webapp' | |||
| const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title') | |||
| @@ -129,7 +138,7 @@ function AppCard({ | |||
| <Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}> | |||
| {runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')} | |||
| </Tag> | |||
| <Switch defaultValue={runningStatus} onChange={onChangeStatus} /> | |||
| <Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} /> | |||
| </div> | |||
| </div> | |||
| <div className="flex flex-col justify-center py-2"> | |||
| @@ -200,6 +209,7 @@ function AppCard({ | |||
| onClose={() => setShowShareModal(false)} | |||
| linkUrl={appUrl} | |||
| onGenerateCode={onGenerateCode} | |||
| regeneratable={isCurrentWorkspaceManager} | |||
| /> | |||
| <SettingsModal | |||
| appInfo={appInfo} | |||
| @@ -17,6 +17,7 @@ type IShareLinkProps = { | |||
| onClose: () => void | |||
| onGenerateCode: () => Promise<void> | |||
| linkUrl: string | |||
| regeneratable?: boolean | |||
| } | |||
| const prefixShare = 'appOverview.overview.appInfo.share' | |||
| @@ -26,6 +27,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({ | |||
| isShow, | |||
| onClose, | |||
| onGenerateCode, | |||
| regeneratable, | |||
| }) => { | |||
| const [genLoading, setGenLoading] = useState(false) | |||
| const [isCopied, setIsCopied] = useState(false) | |||
| @@ -51,7 +53,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({ | |||
| <LinkIcon className='w-4 h-4 mr-2' /> | |||
| { t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) } | |||
| </Button> | |||
| <Button className='w-32 !px-0' onClick={async () => { | |||
| {regeneratable && <Button className='w-32 !px-0' onClick={async () => { | |||
| setGenLoading(true) | |||
| await onGenerateCode() | |||
| setGenLoading(false) | |||
| @@ -59,7 +61,7 @@ const ShareLinkModal: FC<IShareLinkProps> = ({ | |||
| }}> | |||
| <ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} /> | |||
| {t(`${prefixShare}.regenerate`)} | |||
| </Button> | |||
| </Button>} | |||
| </div> | |||
| </Modal> | |||
| } | |||
| @@ -18,6 +18,7 @@ import Tooltip from '@/app/components/base/tooltip' | |||
| import Loading from '@/app/components/base/loading' | |||
| import Confirm from '@/app/components/base/confirm' | |||
| import I18n from '@/context/i18n' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type ISecretKeyModalProps = { | |||
| isShow: boolean | |||
| @@ -31,6 +32,7 @@ const SecretKeyModal = ({ | |||
| onClose, | |||
| }: ISecretKeyModalProps) => { | |||
| const { t } = useTranslation() | |||
| const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||
| const [showConfirmDelete, setShowConfirmDelete] = useState(false) | |||
| const [isVisible, setVisible] = useState(false) | |||
| const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined) | |||
| @@ -118,11 +120,13 @@ const SecretKeyModal = ({ | |||
| setCopyValue(api.token) | |||
| }}></div> | |||
| </Tooltip> | |||
| <div className={`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`} onClick={() => { | |||
| setDelKeyId(api.id) | |||
| setShowConfirmDelete(true) | |||
| }}> | |||
| </div> | |||
| { isCurrentWorkspaceManager | |||
| && <div className={`flex items-center justify-center flex-shrink-0 w-6 h-6 rounded-lg cursor-pointer ${s.trashIcon}`} onClick={() => { | |||
| setDelKeyId(api.id) | |||
| setShowConfirmDelete(true) | |||
| }}> | |||
| </div> | |||
| } | |||
| </div> | |||
| </div> | |||
| ))} | |||
| @@ -131,9 +135,7 @@ const SecretKeyModal = ({ | |||
| ) | |||
| } | |||
| <div className='flex'> | |||
| <Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={() => | |||
| onCreate() | |||
| }> | |||
| <Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || currentWorkspace.role === 'normal'}> | |||
| <PlusIcon className='flex flex-shrink-0 w-4 h-4' /> | |||
| <div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div> | |||
| </Button> | |||
| @@ -8,6 +8,7 @@ import s from './style.module.css' | |||
| import NotionIcon from '@/app/components/base/notion-icon' | |||
| import { apiPrefix } from '@/config' | |||
| import type { DataSourceNotion as TDataSourceNotion } from '@/models/common' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type DataSourceNotionProps = { | |||
| workspaces: TDataSourceNotion[] | |||
| @@ -16,6 +17,8 @@ const DataSourceNotion = ({ | |||
| workspaces, | |||
| }: DataSourceNotionProps) => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const connected = !!workspaces.length | |||
| return ( | |||
| @@ -35,18 +38,25 @@ const DataSourceNotion = ({ | |||
| } | |||
| </div> | |||
| { | |||
| !connected | |||
| connected | |||
| ? ( | |||
| <Link | |||
| className='flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 rounded-md text-xs font-medium text-gray-700 cursor-pointer' | |||
| href={`${apiPrefix}/oauth/data-source/notion`}> | |||
| className={ | |||
| `flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 | |||
| rounded-md text-xs font-medium text-gray-700 | |||
| ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` | |||
| } | |||
| href={isCurrentWorkspaceManager ? `${apiPrefix}/oauth/data-source/notion` : '/'}> | |||
| {t('common.dataSource.connect')} | |||
| </Link> | |||
| ) | |||
| : ( | |||
| <Link | |||
| href={`${apiPrefix}/oauth/data-source/notion`} | |||
| className='flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md cursor-pointer'> | |||
| href={isCurrentWorkspaceManager ? `${apiPrefix}/oauth/data-source/notion` : '/' } | |||
| className={ | |||
| `flex items-center px-3 h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md | |||
| ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` | |||
| }> | |||
| <PlusIcon className='w-[14px] h-[14px] mr-[5px]' /> | |||
| {t('common.dataSource.notion.addWorkspace')} | |||
| </Link> | |||
| @@ -5,6 +5,7 @@ import type { Status } from './declarations' | |||
| type OperateProps = { | |||
| isOpen: boolean | |||
| status: Status | |||
| disabled?: boolean | |||
| onCancel: () => void | |||
| onSave: () => void | |||
| onAdd: () => void | |||
| @@ -14,6 +15,7 @@ type OperateProps = { | |||
| const Operate = ({ | |||
| isOpen, | |||
| status, | |||
| disabled, | |||
| onCancel, | |||
| onSave, | |||
| onAdd, | |||
| @@ -44,10 +46,10 @@ const Operate = ({ | |||
| if (status === 'add') { | |||
| return ( | |||
| <div className=' | |||
| px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer | |||
| text-xs font-medium text-gray-700 flex items-center | |||
| ' onClick={onAdd}> | |||
| <div className={ | |||
| `px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer | |||
| text-xs font-medium text-gray-700 flex items-center ${disabled && 'opacity-50 cursor-default'}}` | |||
| } onClick={() => !disabled && onAdd()}> | |||
| {t('common.provider.addKey')} | |||
| </div> | |||
| ) | |||
| @@ -69,10 +71,10 @@ const Operate = ({ | |||
| <Indicator color='green' className='mr-4' /> | |||
| ) | |||
| } | |||
| <div className=' | |||
| px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer | |||
| text-xs font-medium text-gray-700 flex items-center | |||
| ' onClick={onEdit}> | |||
| <div className={ | |||
| `px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer | |||
| text-xs font-medium text-gray-700 flex items-center ${disabled && 'opacity-50 cursor-default'}}` | |||
| } onClick={() => !disabled && onEdit()}> | |||
| {t('common.provider.editKey')} | |||
| </div> | |||
| </div> | |||
| @@ -13,6 +13,7 @@ export type KeyValidatorProps = { | |||
| forms: Form[] | |||
| keyFrom: KeyFrom | |||
| onSave: (v: ValidateValue) => Promise<boolean | undefined> | |||
| disabled?: boolean | |||
| } | |||
| const KeyValidator = ({ | |||
| @@ -22,6 +23,7 @@ const KeyValidator = ({ | |||
| forms, | |||
| keyFrom, | |||
| onSave, | |||
| disabled, | |||
| }: KeyValidatorProps) => { | |||
| const triggerKey = `plugins/${type}` | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| @@ -85,10 +87,11 @@ const KeyValidator = ({ | |||
| onSave={handleSave} | |||
| onAdd={handleAdd} | |||
| onEdit={handleEdit} | |||
| disabled={disabled} | |||
| /> | |||
| </div> | |||
| { | |||
| isOpen && ( | |||
| isOpen && !disabled && ( | |||
| <div className='px-4 py-3'> | |||
| { | |||
| forms.map(form => ( | |||
| @@ -16,9 +16,9 @@ import { fetchMembers } from '@/service/common' | |||
| import I18n from '@/context/i18n' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import Avatar from '@/app/components/base/avatar' | |||
| import { useWorkspacesContext } from '@/context/workspace-context' | |||
| dayjs.extend(relativeTime) | |||
| const MembersPage = () => { | |||
| const { t } = useTranslation() | |||
| const RoleMap = { | |||
| @@ -27,15 +27,13 @@ const MembersPage = () => { | |||
| normal: t('common.members.normal'), | |||
| } | |||
| const { locale } = useContext(I18n) | |||
| const { userProfile } = useAppContext() | |||
| const { userProfile, currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||
| const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) | |||
| const [inviteModalVisible, setInviteModalVisible] = useState(false) | |||
| const [invitationLink, setInvitationLink] = useState('') | |||
| const [invitedModalVisible, setInvitedModalVisible] = useState(false) | |||
| const accounts = data?.accounts || [] | |||
| const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email | |||
| const { workspaces } = useWorkspacesContext() | |||
| const currentWrokspace = workspaces.filter(item => item.current)?.[0] | |||
| return ( | |||
| <> | |||
| @@ -43,14 +41,14 @@ const MembersPage = () => { | |||
| <div className='flex items-center mb-4 p-3 bg-gray-50 rounded-2xl'> | |||
| <div className={cn(s['logo-icon'], 'shrink-0')}></div> | |||
| <div className='grow mx-2'> | |||
| <div className='text-sm font-medium text-gray-900'>{currentWrokspace?.name}</div> | |||
| <div className='text-sm font-medium text-gray-900'>{currentWorkspace?.name}</div> | |||
| <div className='text-xs text-gray-500'>{t('common.userProfile.workspace')}</div> | |||
| </div> | |||
| <div className=' | |||
| shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200 | |||
| <div className={ | |||
| `shrink-0 flex items-center py-[7px] px-3 border-[0.5px] border-gray-200 | |||
| text-[13px] font-medium text-primary-600 bg-white | |||
| shadow-xs rounded-lg cursor-pointer | |||
| ' onClick={() => setInviteModalVisible(true)}> | |||
| shadow-xs rounded-lg ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` | |||
| } onClick={() => isCurrentWorkspaceManager && setInviteModalVisible(true)}> | |||
| <UserPlusIcon className='w-4 h-4 mr-2 ' /> | |||
| {t('common.members.invite')} | |||
| </div> | |||
| @@ -6,6 +6,7 @@ import type { Form, ValidateValue } from '../key-validator/declarations' | |||
| import { updatePluginKey, validatePluginKey } from './utils' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import type { PluginProvider } from '@/models/common' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type SerpapiPluginProps = { | |||
| plugin: PluginProvider | |||
| @@ -16,6 +17,7 @@ const SerpapiPlugin = ({ | |||
| onUpdate, | |||
| }: SerpapiPluginProps) => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { notify } = useToastContext() | |||
| const forms: Form[] = [{ | |||
| @@ -70,6 +72,7 @@ const SerpapiPlugin = ({ | |||
| link: 'https://serpapi.com/manage-api-key', | |||
| }} | |||
| onSave={handleSave} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -8,6 +8,7 @@ import Indicator from '../indicator' | |||
| import type { AppDetailResponse } from '@/models/app' | |||
| import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type IAppSelectorProps = { | |||
| appItems: AppDetailResponse[] | |||
| @@ -16,6 +17,7 @@ type IAppSelectorProps = { | |||
| export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | |||
| const router = useRouter() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const [showNewAppDialog, setShowNewAppDialog] = useState(false) | |||
| const { t } = useTranslation() | |||
| @@ -77,7 +79,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | |||
| )) | |||
| } | |||
| </div>)} | |||
| <Menu.Item> | |||
| {isCurrentWorkspaceManager && <Menu.Item> | |||
| <div className='p-1' onClick={() => setShowNewAppDialog(true)}> | |||
| <div | |||
| className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | |||
| @@ -95,6 +97,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | |||
| </div> | |||
| </div> | |||
| </Menu.Item> | |||
| } | |||
| </Menu.Items> | |||
| </Transition> | |||
| </Menu> | |||
| @@ -1,3 +1,5 @@ | |||
| 'use client' | |||
| import Link from 'next/link' | |||
| import AccountDropdown from './account-dropdown' | |||
| import AppNav from './app-nav' | |||
| @@ -8,6 +10,7 @@ import GithubStar from './github-star' | |||
| import PluginNav from './plugin-nav' | |||
| import s from './index.module.css' | |||
| import { WorkspaceProvider } from '@/context/workspace-context' | |||
| import { useAppContext } from '@/context/app-context' | |||
| const navClassName = ` | |||
| flex items-center relative mr-3 px-3 h-8 rounded-xl | |||
| @@ -16,6 +19,7 @@ const navClassName = ` | |||
| ` | |||
| const Header = () => { | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| return ( | |||
| <> | |||
| <div className='flex items-center'> | |||
| @@ -29,7 +33,7 @@ const Header = () => { | |||
| <ExploreNav className={navClassName} /> | |||
| <AppNav /> | |||
| <PluginNav className={navClassName} /> | |||
| <DatasetNav /> | |||
| {isCurrentWorkspaceManager && <DatasetNav />} | |||
| </div> | |||
| <div className='flex items-center flex-shrink-0'> | |||
| <EnvNav /> | |||
| @@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation' | |||
| import { debounce } from 'lodash-es' | |||
| import Indicator from '../../indicator' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type NavItem = { | |||
| id: string | |||
| @@ -29,6 +30,7 @@ const itemClassName = ` | |||
| const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSelectorProps) => { | |||
| const router = useRouter() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const handleScroll = useCallback(debounce((e) => { | |||
| if (typeof onLoadmore === 'function') { | |||
| @@ -81,7 +83,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel | |||
| )) | |||
| } | |||
| </div> | |||
| <Menu.Item> | |||
| {isCurrentWorkspaceManager && <Menu.Item> | |||
| <div className='p-1' onClick={onCreate}> | |||
| <div | |||
| className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | |||
| @@ -98,7 +100,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSel | |||
| <div className='font-normal text-[14px] text-gray-700'>{createText}</div> | |||
| </div> | |||
| </div> | |||
| </Menu.Item> | |||
| </Menu.Item>} | |||
| </Menu.Items> | |||
| </Menu> | |||
| </div> | |||
| @@ -1,20 +1,23 @@ | |||
| 'use client' | |||
| import { createRef, useEffect, useRef, useState } from 'react' | |||
| import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' | |||
| import useSWR from 'swr' | |||
| import { createContext, useContext, useContextSelector } from 'use-context-selector' | |||
| import type { FC, ReactNode } from 'react' | |||
| import { fetchAppList } from '@/service/apps' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { fetchLanggeniusVersion, fetchUserProfile } from '@/service/common' | |||
| import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common' | |||
| import type { App } from '@/types/app' | |||
| import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' | |||
| import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' | |||
| export type AppContextValue = { | |||
| apps: App[] | |||
| mutateApps: () => void | |||
| mutateApps: VoidFunction | |||
| userProfile: UserProfileResponse | |||
| mutateUserProfile: () => void | |||
| mutateUserProfile: VoidFunction | |||
| currentWorkspace: ICurrentWorkspace | |||
| isCurrentWorkspaceManager: boolean | |||
| mutateCurrentWorkspace: VoidFunction | |||
| pageContainerRef: React.RefObject<HTMLDivElement> | |||
| langeniusVersionInfo: LangGeniusVersionResponse | |||
| useSelector: typeof useSelector | |||
| @@ -30,6 +33,17 @@ const initialLangeniusVersionInfo = { | |||
| can_auto_update: false, | |||
| } | |||
| const initialWorkspaceInfo: ICurrentWorkspace = { | |||
| id: '', | |||
| name: '', | |||
| plan: '', | |||
| status: '', | |||
| created_at: 0, | |||
| role: 'normal', | |||
| providers: [], | |||
| in_trail: true, | |||
| } | |||
| const AppContext = createContext<AppContextValue>({ | |||
| apps: [], | |||
| mutateApps: () => { }, | |||
| @@ -40,7 +54,10 @@ const AppContext = createContext<AppContextValue>({ | |||
| avatar: '', | |||
| is_password_set: false, | |||
| }, | |||
| currentWorkspace: initialWorkspaceInfo, | |||
| isCurrentWorkspaceManager: false, | |||
| mutateUserProfile: () => { }, | |||
| mutateCurrentWorkspace: () => { }, | |||
| pageContainerRef: createRef(), | |||
| langeniusVersionInfo: initialLangeniusVersionInfo, | |||
| useSelector, | |||
| @@ -59,10 +76,14 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList) | |||
| const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) | |||
| const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace) | |||
| const [userProfile, setUserProfile] = useState<UserProfileResponse>() | |||
| const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo) | |||
| const updateUserProfileAndVersion = async () => { | |||
| const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) | |||
| const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role]) | |||
| const updateUserProfileAndVersion = useCallback(async () => { | |||
| if (userProfileResponse && !userProfileResponse.bodyUsed) { | |||
| const result = await userProfileResponse.json() | |||
| setUserProfile(result) | |||
| @@ -71,16 +92,33 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } }) | |||
| setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env }) | |||
| } | |||
| } | |||
| }, [userProfileResponse]) | |||
| useEffect(() => { | |||
| updateUserProfileAndVersion() | |||
| }, [userProfileResponse]) | |||
| }, [updateUserProfileAndVersion, userProfileResponse]) | |||
| useEffect(() => { | |||
| if (currentWorkspaceResponse) | |||
| setCurrentWorkspace(currentWorkspaceResponse) | |||
| }, [currentWorkspaceResponse]) | |||
| if (!appList || !userProfile) | |||
| return <Loading type='app' /> | |||
| return ( | |||
| <AppContext.Provider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile, pageContainerRef, langeniusVersionInfo, useSelector }}> | |||
| <AppContext.Provider value={{ | |||
| apps: appList.data, | |||
| mutateApps, | |||
| userProfile, | |||
| mutateUserProfile, | |||
| pageContainerRef, | |||
| langeniusVersionInfo, | |||
| useSelector, | |||
| currentWorkspace, | |||
| isCurrentWorkspaceManager, | |||
| mutateCurrentWorkspace, | |||
| }}> | |||
| <div ref={pageContainerRef} className='relative flex flex-col h-full overflow-auto bg-gray-100'> | |||
| {children} | |||
| </div> | |||
| @@ -118,6 +118,13 @@ export type IWorkspace = { | |||
| current: boolean | |||
| } | |||
| export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & { | |||
| role: 'normal' | 'admin' | 'owner' | |||
| providers: Provider[] | |||
| in_trail: boolean | |||
| trial_end_reason?: string | |||
| } | |||
| export type DataSourceNotionPage = { | |||
| page_icon: null | { | |||
| type: string | null | |||
| @@ -2,6 +2,7 @@ import type { Fetcher } from 'swr' | |||
| import { del, get, patch, post, put } from './base' | |||
| import type { | |||
| AccountIntegrate, CommonResponse, DataSourceNotion, | |||
| ICurrentWorkspace, | |||
| IWorkspace, LangGeniusVersionResponse, Member, | |||
| OauthResponse, PluginProvider, Provider, ProviderAnthropicToken, ProviderAzureToken, | |||
| SetupStatusResponse, TenantInfoResponse, UserProfileOriginResponse, | |||
| @@ -87,6 +88,10 @@ export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> | |||
| return get(`/files/${fileID}/preview`) as Promise<{ content: string }> | |||
| } | |||
| export const fetchCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; params: Record<string, any> }> = ({ url, params }) => { | |||
| return get(url, { params }) as Promise<ICurrentWorkspace> | |||
| } | |||
| export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => { | |||
| return get(url, { params }) as Promise<{ workspaces: IWorkspace[] }> | |||
| } | |||