Unverified Commit 2c77a74c authored by Matri's avatar Matri Committed by GitHub

fix: frontend permission check (#784)

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