From 2c77a74c40f8e1f9d156b22ecb03a8d32c520e8d Mon Sep 17 00:00:00 2001 From: Matri Date: Tue, 15 Aug 2023 13:35:47 +0800 Subject: [PATCH] fix: frontend permission check (#784) --- api/services/workspace_service.py | 17 +++++- .../app/(appDetailLayout)/[appId]/layout.tsx | 21 ++++--- web/app/(commonLayout)/apps/AppCard.tsx | 6 +- web/app/(commonLayout)/apps/Apps.tsx | 6 +- web/app/(commonLayout)/datasets/Datasets.tsx | 5 +- web/app/components/app/overview/appCard.tsx | 34 +++++++---- .../components/app/overview/share-link.tsx | 6 +- .../develop/secret-key/secret-key-modal.tsx | 18 +++--- .../data-source-notion/index.tsx | 20 +++++-- .../account-setting/key-validator/Operate.tsx | 18 +++--- .../account-setting/key-validator/index.tsx | 5 +- .../account-setting/members-page/index.tsx | 16 +++--- .../plugin-page/SerpapiPlugin.tsx | 3 + .../components/header/app-selector/index.tsx | 5 +- web/app/components/header/index.tsx | 6 +- .../header/nav/nav-selector/index.tsx | 6 +- web/context/app-context.tsx | 56 ++++++++++++++++--- web/models/common.ts | 7 +++ web/service/common.ts | 5 ++ 19 files changed, 186 insertions(+), 74 deletions(-) diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py index 96319818c0..975c311a3d 100644 --- a/api/services/workspace_service.py +++ b/api/services/workspace_service.py @@ -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 diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx index 97b0164ea1..a3af3c68e9 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx @@ -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 = (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) diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 2b432c0527..2645859bb5 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -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 = ({
{app.name}
- + { isCurrentWorkspaceManager + && }
{app.model_config?.pre_prompt}
diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index e264cc2301..973afb6a3c 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -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 => ( )))} - + { isCurrentWorkspaceManager + && } ) } diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx index eaabf8b569..fc9e83a026 100644 --- a/web/app/(commonLayout)/datasets/Datasets.tsx +++ b/web/app/(commonLayout)/datasets/Datasets.tsx @@ -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 => ( ), ))} - + { isCurrentWorkspaceManager && } ) } diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index 067dddafe2..3c3f013d1f 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/web/app/components/app/overview/appCard.tsx @@ -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({ {runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')} - +
@@ -200,6 +209,7 @@ function AppCard({ onClose={() => setShowShareModal(false)} linkUrl={appUrl} onGenerateCode={onGenerateCode} + regeneratable={isCurrentWorkspaceManager} /> void onGenerateCode: () => Promise linkUrl: string + regeneratable?: boolean } const prefixShare = 'appOverview.overview.appInfo.share' @@ -26,6 +27,7 @@ const ShareLinkModal: FC = ({ isShow, onClose, onGenerateCode, + regeneratable, }) => { const [genLoading, setGenLoading] = useState(false) const [isCopied, setIsCopied] = useState(false) @@ -51,7 +53,7 @@ const ShareLinkModal: FC = ({ { t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) } - + }
} diff --git a/web/app/components/develop/secret-key/secret-key-modal.tsx b/web/app/components/develop/secret-key/secret-key-modal.tsx index 4cafad9de5..c93f1ce816 100644 --- a/web/app/components/develop/secret-key/secret-key-modal.tsx +++ b/web/app/components/develop/secret-key/secret-key-modal.tsx @@ -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(undefined) @@ -118,11 +120,13 @@ const SecretKeyModal = ({ setCopyValue(api.token) }}> -
{ - setDelKeyId(api.id) - setShowConfirmDelete(true) - }}> -
+ { isCurrentWorkspaceManager + &&
{ + setDelKeyId(api.id) + setShowConfirmDelete(true) + }}> +
+ } ))} @@ -131,9 +135,7 @@ const SecretKeyModal = ({ ) }
- diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx index 276df6a15f..ae19a5fc53 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx @@ -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 = ({ }
{ - !connected + connected ? ( + 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')} ) : ( + 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'}` + }> {t('common.dataSource.notion.addWorkspace')} diff --git a/web/app/components/header/account-setting/key-validator/Operate.tsx b/web/app/components/header/account-setting/key-validator/Operate.tsx index 285fd7a75c..99863fc43c 100644 --- a/web/app/components/header/account-setting/key-validator/Operate.tsx +++ b/web/app/components/header/account-setting/key-validator/Operate.tsx @@ -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 ( -
+
!disabled && onAdd()}> {t('common.provider.addKey')}
) @@ -69,10 +71,10 @@ const Operate = ({ ) } -
+
!disabled && onEdit()}> {t('common.provider.editKey')}
diff --git a/web/app/components/header/account-setting/key-validator/index.tsx b/web/app/components/header/account-setting/key-validator/index.tsx index 713ec81fbe..59a6c53a57 100644 --- a/web/app/components/header/account-setting/key-validator/index.tsx +++ b/web/app/components/header/account-setting/key-validator/index.tsx @@ -13,6 +13,7 @@ export type KeyValidatorProps = { forms: Form[] keyFrom: KeyFrom onSave: (v: ValidateValue) => Promise + 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} />
{ - isOpen && ( + isOpen && !disabled && (
{ forms.map(form => ( diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index af1a0440c4..33b19707a4 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -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 = () => {
-
{currentWrokspace?.name}
+
{currentWorkspace?.name}
{t('common.userProfile.workspace')}
-
setInviteModalVisible(true)}> + shadow-xs rounded-lg ${isCurrentWorkspaceManager ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` + } onClick={() => isCurrentWorkspaceManager && setInviteModalVisible(true)}> {t('common.members.invite')}
diff --git a/web/app/components/header/account-setting/plugin-page/SerpapiPlugin.tsx b/web/app/components/header/account-setting/plugin-page/SerpapiPlugin.tsx index 75a4276042..86b7db0370 100644 --- a/web/app/components/header/account-setting/plugin-page/SerpapiPlugin.tsx +++ b/web/app/components/header/account-setting/plugin-page/SerpapiPlugin.tsx @@ -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} /> ) } diff --git a/web/app/components/header/app-selector/index.tsx b/web/app/components/header/app-selector/index.tsx index 66d219a830..947884de8f 100644 --- a/web/app/components/header/app-selector/index.tsx +++ b/web/app/components/header/app-selector/index.tsx @@ -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) { )) }
)} - + {isCurrentWorkspaceManager &&
setShowNewAppDialog(true)}>
+ } diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 2c59453cf4..be2c16eef6 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -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 ( <>
@@ -29,7 +33,7 @@ const Header = () => { - + {isCurrentWorkspaceManager && }
diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index b7a647990c..f6108661da 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -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 )) }
- + {isCurrentWorkspaceManager &&
{createText}
-
+
}
diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 90b4dc00bc..793e3a5a35 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -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 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({ apps: [], mutateApps: () => { }, @@ -40,7 +54,10 @@ const AppContext = createContext({ avatar: '', is_password_set: false, }, + currentWorkspace: initialWorkspaceInfo, + isCurrentWorkspaceManager: false, mutateUserProfile: () => { }, + mutateCurrentWorkspace: () => { }, pageContainerRef: createRef(), langeniusVersionInfo: initialLangeniusVersionInfo, useSelector, @@ -59,10 +76,14 @@ export const AppContextProvider: FC = ({ 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() const [langeniusVersionInfo, setLangeniusVersionInfo] = useState(initialLangeniusVersionInfo) - const updateUserProfileAndVersion = async () => { + const [currentWorkspace, setCurrentWorkspace] = useState(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 = ({ 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 return ( - +
{children}
diff --git a/web/models/common.ts b/web/models/common.ts index 617df80e92..7482668e7b 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -118,6 +118,13 @@ export type IWorkspace = { current: boolean } +export type ICurrentWorkspace = Omit & { + role: 'normal' | 'admin' | 'owner' + providers: Provider[] + in_trail: boolean + trial_end_reason?: string +} + export type DataSourceNotionPage = { page_icon: null | { type: string | null diff --git a/web/service/common.ts b/web/service/common.ts index d66fe166aa..4b98ea003d 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -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 }> = ({ url, params }) => { + return get(url, { params }) as Promise +} + export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record }> = ({ url, params }) => { return get(url, { params }) as Promise<{ workspaces: IWorkspace[] }> }