mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-12 00:36:03 +00:00
feat(web): refactor MCP servers as sidebar entities and improve sidebar footer
- Refactor MCP servers to be managed as collapsible sidebar sub-items with ?id= detail routing and inline form (matching bots/pipelines pattern) - Add MCPDetailContent with create/edit modes, enable toggle, and danger zone - Extract MCPForm as standalone inline form from MCPFormDialog - Move API Integration to standalone sidebar footer button - Add GitHub star CTA with live star count badge in user dropdown menu - Add MCP server status dot indicators in sidebar (green/gray for enabled/disabled) - Add i18n keys for MCP detail page and GitHub star CTA in all 4 locales
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
LogOut,
|
||||
KeyRound,
|
||||
Settings,
|
||||
Star,
|
||||
Ellipsis,
|
||||
ArrowUp,
|
||||
ExternalLink,
|
||||
@@ -115,6 +116,7 @@ const ENTITY_CATEGORY_IDS = [
|
||||
'pipelines',
|
||||
'knowledge',
|
||||
'plugins',
|
||||
'mcp',
|
||||
] as const;
|
||||
type EntityCategoryId = (typeof ENTITY_CATEGORY_IDS)[number];
|
||||
|
||||
@@ -124,6 +126,7 @@ const DETAIL_PAGE_CATEGORIES: EntityCategoryId[] = [
|
||||
'pipelines',
|
||||
'knowledge',
|
||||
'plugins',
|
||||
'mcp',
|
||||
];
|
||||
|
||||
// Categories that support creating new entities from the sidebar
|
||||
@@ -131,6 +134,7 @@ const CREATABLE_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
];
|
||||
|
||||
// Categories where clicking the parent only toggles collapse (no list page)
|
||||
@@ -138,6 +142,7 @@ const COLLAPSIBLE_ONLY_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
];
|
||||
|
||||
function isEntityCategory(id: string): id is EntityCategoryId {
|
||||
@@ -147,12 +152,13 @@ function isEntityCategory(id: string): id is EntityCategoryId {
|
||||
// Map sidebar config IDs to SidebarDataContext keys
|
||||
const ENTITY_KEY_MAP: Record<
|
||||
EntityCategoryId,
|
||||
'bots' | 'pipelines' | 'knowledgeBases' | 'plugins'
|
||||
'bots' | 'pipelines' | 'knowledgeBases' | 'plugins' | 'mcpServers'
|
||||
> = {
|
||||
bots: 'bots',
|
||||
pipelines: 'pipelines',
|
||||
knowledge: 'knowledgeBases',
|
||||
plugins: 'plugins',
|
||||
mcp: 'mcpServers',
|
||||
};
|
||||
|
||||
// Route prefix map for entity detail pages
|
||||
@@ -161,6 +167,7 @@ const ENTITY_ROUTE_MAP: Record<EntityCategoryId, string> = {
|
||||
pipelines: '/home/pipelines',
|
||||
knowledge: '/home/knowledge',
|
||||
plugins: '/home/plugins',
|
||||
mcp: '/home/mcp',
|
||||
};
|
||||
|
||||
// localStorage key for collapsible section open/closed state
|
||||
@@ -324,6 +331,7 @@ function NavItems({
|
||||
const isCollapseOnly = COLLAPSIBLE_ONLY_CATEGORIES.includes(config.id);
|
||||
const isPlugin = config.id === 'plugins';
|
||||
const isBot = config.id === 'bots';
|
||||
const isMCP = config.id === 'mcp';
|
||||
const isActive =
|
||||
selectedChild?.id === config.id ||
|
||||
pathname === routePrefix ||
|
||||
@@ -400,7 +408,7 @@ function NavItems({
|
||||
alt=""
|
||||
className="size-4 rounded"
|
||||
/>
|
||||
{isBot && (
|
||||
{(isBot || isMCP) && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-popover',
|
||||
@@ -411,6 +419,15 @@ function NavItems({
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
) : isMCP ? (
|
||||
<span
|
||||
className={cn(
|
||||
'size-2 shrink-0 rounded-full',
|
||||
item.enabled === false
|
||||
? 'bg-muted-foreground/40'
|
||||
: 'bg-green-500',
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<span className="truncate">{item.name}</span>
|
||||
</button>
|
||||
@@ -447,7 +464,7 @@ function NavItems({
|
||||
alt=""
|
||||
className="size-4 rounded"
|
||||
/>
|
||||
{isBot && (
|
||||
{(isBot || isMCP) && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-sidebar',
|
||||
@@ -458,6 +475,15 @@ function NavItems({
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
) : isMCP ? (
|
||||
<span
|
||||
className={cn(
|
||||
'size-2 shrink-0 rounded-full',
|
||||
item.enabled === false
|
||||
? 'bg-muted-foreground/40'
|
||||
: 'bg-green-500',
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<span className="truncate">{item.name}</span>
|
||||
{item.debug && (
|
||||
@@ -914,7 +940,8 @@ export default function HomeSidebar({
|
||||
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
|
||||
const [modelsDialogOpen, setModelsDialogOpen] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
|
||||
const [starCount, setStarCount] = useState<number | null>(null);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
function handleModelsDialogChange(open: boolean) {
|
||||
setModelsDialogOpen(open);
|
||||
if (open) {
|
||||
@@ -985,6 +1012,15 @@ export default function HomeSidebar({
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch releases:', error);
|
||||
});
|
||||
|
||||
getCloudServiceClientSync()
|
||||
.getGitHubRepoInfo()
|
||||
.then((info) => {
|
||||
if (info?.repo?.stargazers_count != null) {
|
||||
setStarCount(info.repo.stargazers_count);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Update selected state + notify parent without navigating
|
||||
@@ -1122,6 +1158,19 @@ export default function HomeSidebar({
|
||||
|
||||
{/* Footer */}
|
||||
<SidebarFooter>
|
||||
{/* API Integration entry */}
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => setApiKeyDialogOpen(true)}
|
||||
tooltip={t('common.apiIntegration')}
|
||||
>
|
||||
<KeyRound className="size-4" />
|
||||
<span>{t('common.apiIntegration')}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
{/* Models entry */}
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
@@ -1145,7 +1194,7 @@ export default function HomeSidebar({
|
||||
{/* User menu using sidebar-07 nav-user DropdownMenu pattern */}
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={userMenuOpen} onOpenChange={setUserMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
@@ -1226,10 +1275,6 @@ export default function HomeSidebar({
|
||||
<Settings />
|
||||
{t('account.settings')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setApiKeyDialogOpen(true)}>
|
||||
<KeyRound />
|
||||
{t('common.apiIntegration')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -1266,6 +1311,29 @@ export default function HomeSidebar({
|
||||
<Lightbulb />
|
||||
{t('common.featureRequest')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://github.com/langbot-app/LangBot',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
className={cn(
|
||||
'text-yellow-500',
|
||||
userMenuOpen && 'animate-twinkle',
|
||||
)}
|
||||
/>
|
||||
<span className="flex-1">{t('common.starOnGitHub')}</span>
|
||||
{starCount != null && (
|
||||
<Badge variant="secondary" className="ml-auto text-xs">
|
||||
{starCount >= 1000
|
||||
? `${(starCount / 1000).toFixed(1)}k`
|
||||
: starCount}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
@@ -34,10 +34,12 @@ export interface SidebarDataContextValue {
|
||||
pipelines: SidebarEntityItem[];
|
||||
knowledgeBases: SidebarEntityItem[];
|
||||
plugins: SidebarEntityItem[];
|
||||
mcpServers: SidebarEntityItem[];
|
||||
refreshBots: () => Promise<void>;
|
||||
refreshPipelines: () => Promise<void>;
|
||||
refreshKnowledgeBases: () => Promise<void>;
|
||||
refreshPlugins: () => Promise<void>;
|
||||
refreshMCPServers: () => Promise<void>;
|
||||
refreshAll: () => Promise<void>;
|
||||
// Breadcrumb: entity name shown when viewing a detail page
|
||||
detailEntityName: string | null;
|
||||
@@ -55,6 +57,7 @@ export function SidebarDataProvider({
|
||||
const [pipelines, setPipelines] = useState<SidebarEntityItem[]>([]);
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<SidebarEntityItem[]>([]);
|
||||
const [plugins, setPlugins] = useState<SidebarEntityItem[]>([]);
|
||||
const [mcpServers, setMCPServers] = useState<SidebarEntityItem[]>([]);
|
||||
const [detailEntityName, setDetailEntityName] = useState<string | null>(null);
|
||||
|
||||
const refreshBots = useCallback(async () => {
|
||||
@@ -158,14 +161,36 @@ export function SidebarDataProvider({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshMCPServers = useCallback(async () => {
|
||||
try {
|
||||
const resp = await httpClient.getMCPServers();
|
||||
setMCPServers(
|
||||
resp.servers.map((server) => ({
|
||||
id: server.name,
|
||||
name: server.name,
|
||||
enabled: server.enable,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch MCP servers for sidebar:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
await Promise.all([
|
||||
refreshBots(),
|
||||
refreshPipelines(),
|
||||
refreshKnowledgeBases(),
|
||||
refreshPlugins(),
|
||||
refreshMCPServers(),
|
||||
]);
|
||||
}, [refreshBots, refreshPipelines, refreshKnowledgeBases, refreshPlugins]);
|
||||
}, [
|
||||
refreshBots,
|
||||
refreshPipelines,
|
||||
refreshKnowledgeBases,
|
||||
refreshPlugins,
|
||||
refreshMCPServers,
|
||||
]);
|
||||
|
||||
// Fetch all entity lists on mount
|
||||
useEffect(() => {
|
||||
@@ -179,10 +204,12 @@ export function SidebarDataProvider({
|
||||
pipelines,
|
||||
knowledgeBases,
|
||||
plugins,
|
||||
mcpServers,
|
||||
refreshBots,
|
||||
refreshPipelines,
|
||||
refreshKnowledgeBases,
|
||||
refreshPlugins,
|
||||
refreshMCPServers,
|
||||
refreshAll,
|
||||
detailEntityName,
|
||||
setDetailEntityName,
|
||||
|
||||
Reference in New Issue
Block a user