mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-02 23:57:14 +00:00
feat(tenancy): implement workspace isolation
This commit is contained in:
@@ -4,7 +4,11 @@ import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { systemInfo, httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import {
|
||||
clearUserInfo,
|
||||
getCloudServiceClientSync,
|
||||
useCurrentWorkspace,
|
||||
} from '@/app/infra/http';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Moon,
|
||||
@@ -380,6 +384,11 @@ function NavItems({
|
||||
const sidebarData = useSidebarData();
|
||||
const { state: sidebarState, isMobile } = useSidebar();
|
||||
const { t } = useTranslation();
|
||||
const currentWorkspace = useCurrentWorkspace();
|
||||
const canManageResources =
|
||||
currentWorkspace?.permissions.includes('resource.manage') ?? false;
|
||||
const canOperateRuntime =
|
||||
currentWorkspace?.permissions.includes('runtime.operate') ?? false;
|
||||
// Track which entity categories have their full list expanded
|
||||
const [expandedLists, setExpandedLists] = useState<SidebarListExpansionState>(
|
||||
loadListExpansionState,
|
||||
@@ -513,6 +522,9 @@ function NavItems({
|
||||
<>
|
||||
{sectionItems.map((config) => {
|
||||
if (!isEntityCategory(config.id)) {
|
||||
if (config.id === 'add-extension' && !canManageResources) {
|
||||
return null;
|
||||
}
|
||||
// Non-entity entries (e.g. monitoring, market, mcp) render as plain links
|
||||
return (
|
||||
<SidebarMenuItem key={config.id}>
|
||||
@@ -552,7 +564,8 @@ function NavItems({
|
||||
: sidebarData[entityKey];
|
||||
const routePrefix = ENTITY_ROUTE_MAP[categoryId];
|
||||
const hasDetailPages = DETAIL_PAGE_CATEGORIES.includes(categoryId);
|
||||
const canCreate = CREATABLE_CATEGORIES.includes(categoryId);
|
||||
const canCreate =
|
||||
canManageResources && CREATABLE_CATEGORIES.includes(categoryId);
|
||||
const isCollapseOnly = COLLAPSIBLE_ONLY_CATEGORIES.includes(categoryId);
|
||||
const isPlugin = categoryId === 'plugins';
|
||||
const isSkill = categoryId === 'skills';
|
||||
@@ -800,6 +813,7 @@ function NavItems({
|
||||
{itemIsPluginType && !item.debug && (
|
||||
<PluginItemMenu
|
||||
item={item}
|
||||
canManage={canManageResources}
|
||||
onUpdate={() => handlePluginUpdate(item)}
|
||||
onDelete={() => handlePluginDelete(item)}
|
||||
/>
|
||||
@@ -1063,7 +1077,7 @@ function NavItems({
|
||||
{config.name}
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-0.5 -mr-1">
|
||||
{isExtensionsCategory && (
|
||||
{isExtensionsCategory && canOperateRuntime && (
|
||||
<button
|
||||
type="button"
|
||||
title={t('common.refresh', '刷新')}
|
||||
@@ -1330,10 +1344,12 @@ function NavItems({
|
||||
// Dropdown menu for plugin sidebar sub-items (shown on hover)
|
||||
function PluginItemMenu({
|
||||
item,
|
||||
canManage,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: {
|
||||
item: SidebarEntityItem;
|
||||
canManage: boolean;
|
||||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
@@ -1344,6 +1360,8 @@ function PluginItemMenu({
|
||||
const isGithub = item.installSource === 'github';
|
||||
const hasSourceLink = isMarketplace || isGithub;
|
||||
|
||||
if (!canManage && !hasSourceLink) return null;
|
||||
|
||||
function handleViewSource() {
|
||||
const slashIdx = item.id.indexOf('/');
|
||||
const author = slashIdx >= 0 ? item.id.substring(0, slashIdx) : '';
|
||||
@@ -1384,7 +1402,7 @@ function PluginItemMenu({
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
{isMarketplace && (
|
||||
{canManage && isMarketplace && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -1413,16 +1431,18 @@ function PluginItemMenu({
|
||||
<span>{t('plugins.viewSource')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-red-600 focus:text-red-600"
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash className="size-4" />
|
||||
<span>{t('plugins.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
{canManage && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-red-600 focus:text-red-600"
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash className="size-4" />
|
||||
<span>{t('plugins.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
@@ -1612,6 +1632,7 @@ export default function HomeSidebar({
|
||||
useState<Record<string, boolean>>(loadSectionState);
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const currentWorkspace = useCurrentWorkspace();
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [settingsSection, setSettingsSection] =
|
||||
useState<SettingsSection>('models');
|
||||
@@ -1678,10 +1699,6 @@ export default function HomeSidebar({
|
||||
|
||||
useEffect(() => {
|
||||
initSelect();
|
||||
if (!localStorage.getItem('token')) {
|
||||
localStorage.setItem('token', 'test-token');
|
||||
localStorage.setItem('userEmail', 'test@example.com');
|
||||
}
|
||||
|
||||
const storedEmail = localStorage.getItem('userEmail');
|
||||
if (storedEmail) {
|
||||
@@ -1820,6 +1837,7 @@ export default function HomeSidebar({
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
clearUserInfo();
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userEmail');
|
||||
window.location.href = '/login';
|
||||
@@ -1948,18 +1966,20 @@ export default function HomeSidebar({
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
{/* API Integration entry */}
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => openSettings('apiIntegration')}
|
||||
tooltip={t('common.apiIntegration')}
|
||||
>
|
||||
<KeyRound className="size-4 text-blue-500" />
|
||||
<span>{t('common.apiIntegration')}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
{/* API-key management is available only to authorized Workspace roles. */}
|
||||
{currentWorkspace?.permissions.includes('api_key.manage') && (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => openSettings('apiIntegration')}
|
||||
tooltip={t('common.apiIntegration')}
|
||||
>
|
||||
<KeyRound className="size-4 text-blue-500" />
|
||||
<span>{t('common.apiIntegration')}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)}
|
||||
|
||||
{/* User menu using sidebar-07 nav-user DropdownMenu pattern */}
|
||||
<SidebarMenu>
|
||||
|
||||
Reference in New Issue
Block a user