[eric] tools: header search + POPULAR connect strip close the claude connectors parity

This commit is contained in:
ciregenz
2026-08-05 14:36:49 -07:00
parent ec6a0b0397
commit 2ae7461c26
3 changed files with 127 additions and 9 deletions
+30 -9
View File
@@ -21,7 +21,7 @@ import {
import { Skeleton } from '@/app/components/feedback/Loading';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration, INTEGRATIONS } from './integrations';
import { INTEGRATIONS } from './integrations';
import { CATEGORY_ORDER } from './toolsHelpers';
import ToolSection from './cards/ToolSection';
import BrowserPermissionCard from './cards/BrowserPermissionCard';
@@ -30,9 +30,14 @@ import RegistryBrowserDialog from './dialogs/RegistryBrowserDialog';
import ToolsAddMenu from './dialogs/ToolsAddMenu';
import ToolDialogs from './dialogs/ToolDialogs';
import CustomToolCard from './cards/CustomToolCard';
import PopularConnectorsRow from './cards/PopularConnectorsRow';
import TextField from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import SearchIcon from '@mui/icons-material/Search';
import IntegrationGalleryCard from './cards/IntegrationGalleryCard';
import { useToolsActions } from './hooks/useToolsActions';
import { useBuiltinSections } from './hooks/useBuiltinSections';
import { useConnectorFilters } from './hooks/useConnectorFilters';
import { useCuratedRegistry } from './hooks/useCuratedRegistry';
interface ToolsProps {
@@ -69,13 +74,7 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
const [browserSectionOpen, setBrowserSectionOpen] = useState(false);
const [browserCollapsed, setBrowserCollapsed] = useState<Record<string, boolean>>({ browser_delegation: true, browser_action: true });
// claude.ai's Connectors page tabs: All / Connected / Not connected.
const [connFilter, setConnFilter] = useState<'all' | 'connected' | 'not-connected'>('all');
const visibleTools = useMemo(() => {
if (connFilter === 'connected') return tools.filter((t) => t.enabled !== false);
if (connFilter === 'not-connected') return tools.filter((t) => t.enabled === false);
return tools;
}, [tools, connFilter]);
const visibleGallery = useMemo(() => (connFilter === 'connected' ? [] : uninstalledIntegrations), [uninstalledIntegrations, connFilter]);
const { connFilter, setConnFilter, searchOpen, setSearchOpen, searchQ, setSearchQ, q, visibleTools, visibleGallery, popularUninstalled } = useConnectorFilters(tools, uninstalledIntegrations);
useEffect(() => {
dispatch(fetchTools());
@@ -119,6 +118,22 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
</Box>
))}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{searchOpen ? (
<TextField
autoFocus
size="small"
placeholder="Search connectors..."
value={searchQ}
onChange={(e) => setSearchQ(e.target.value)}
onBlur={() => { if (!searchQ.trim()) setSearchOpen(false); }}
sx={{ width: 240, '& .MuiOutlinedInput-root': { fontSize: '0.875rem', borderRadius: 999, '& input': { py: 0.6 } } }}
/>
) : (
<IconButton size="small" onClick={() => setSearchOpen(true)} sx={{ color: c.text.tertiary, '&:hover': { color: c.text.primary } }}>
<SearchIcon sx={{ fontSize: 19 }} />
</IconButton>
)}
<ToolsAddMenu
devMode={!!devMode}
onBrowseConnectors={onBrowseConnectors}
@@ -126,6 +141,7 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
onOpenRegistry={a.openRegistryBrowser}
onSnackbar={(message, severity) => a.setSnackbar({ open: true, message, severity: severity === 'error' ? 'error' : undefined })}
/>
</Box>
</Box>
{loading ? (
@@ -135,13 +151,17 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
))}
</Box>
) : (
<>
{connFilter === 'all' && !q && (
<PopularConnectorsRow integrations={popularUninstalled} loading={a.integrationLoading} onConnect={a.handleIntegrationToggle} />
)}
<Box sx={{ border: `1px solid ${c.border.subtle}`, borderRadius: '12px', overflow: 'hidden', bgcolor: c.bg.surface }}>
<Box sx={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 130px 150px', gap: 2, px: 2, py: 1.1, borderBottom: `1px solid ${c.border.subtle}` }}>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.8125rem' }}>Connector</Typography>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.8125rem' }}>Type</Typography>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.8125rem', textAlign: 'right' }}>Status</Typography>
</Box>
{connFilter === 'all' && (
{connFilter === 'all' && !q && (
<>
{coreTools.length > 0 && (
<ToolSection label="Core Tools" icon={<LockIcon sx={{ fontSize: 14, color: c.text.tertiary }} />} count={coreTools.length} open={coreSectionOpen} onToggle={() => setCoreSectionOpen((v) => !v)} grouped={groupedCore} collapsedCategories={collapsedCategories} toggleCategory={toggleCategory} expandedBuiltin={expandedBuiltin} toggleBuiltinExpand={toggleBuiltinExpand} builtinPermissions={builtinPermissions} onPermissionChange={a.handleBuiltinPermissionChange} onCategoryPermissionChange={a.handleBuiltinCategoryPermissionChange} enabled={coreSectionEnabled} onEnabledChange={(v) => a.handleSectionEnabledChange(coreTools, v)} />
@@ -212,6 +232,7 @@ const Tools: React.FC<ToolsProps> = ({ onBrowseConnectors, expandToolId }) => {
</Box>
)}
</Box>
</>
)}
<Box sx={{ mt: 3 }}>
@@ -0,0 +1,63 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { Integration } from '../integrations';
interface Props {
integrations: Integration[];
loading: Record<string, boolean>;
onConnect: (ig: Integration) => void;
}
// claude.ai's POPULAR strip on the Connectors settings page: compact cards with a Connect button.
const PopularConnectorsRow: React.FC<Props> = ({ integrations, loading, onConnect }) => {
const c = useClaudeTokens();
if (integrations.length === 0) return null;
return (
<Box sx={{ mb: 2.5 }}>
<Typography sx={{ fontSize: '0.75rem', fontWeight: 600, letterSpacing: '0.06em', color: c.text.tertiary, textTransform: 'uppercase', mb: 1.25 }}>
Popular
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 1.5 }}>
{integrations.map((ig) => (
<Box key={ig.id} sx={{
display: 'flex', alignItems: 'center', gap: 1.25, px: 1.75, py: 1.25,
border: `1px solid ${c.border.subtle}`, borderRadius: '12px', bgcolor: c.bg.surface,
transition: 'border-color 0.12s, box-shadow 0.12s',
'&:hover': { borderColor: c.border.medium, boxShadow: c.shadow.sm },
}}>
<Box sx={{
width: 30, height: 30, borderRadius: '8px', flexShrink: 0,
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.surface,
display: 'flex', alignItems: 'center', justifyContent: 'center', color: ig.color,
}}>
{ig.icon}
</Box>
<Typography noWrap sx={{ fontSize: '0.9375rem', fontWeight: 600, color: c.text.primary, flex: 1 }}>{ig.name}</Typography>
{loading[ig.id] ? (
<CircularProgress size={16} sx={{ color: ig.color }} />
) : (
<Button
size="small"
variant="outlined"
onClick={() => onConnect(ig)}
sx={{
textTransform: 'none', fontSize: '0.8125rem', fontWeight: 600, px: 1.5, py: 0.3,
color: c.text.primary, borderColor: c.border.medium, borderRadius: 999, minWidth: 0,
'&:hover': { borderColor: c.border.strong, bgcolor: c.bg.elevated },
}}
>
Connect
</Button>
)}
</Box>
))}
</Box>
</Box>
);
};
export default PopularConnectorsRow;
@@ -0,0 +1,34 @@
import { useMemo, useState } from 'react';
import { ToolDefinition } from '@/shared/state/toolsSlice';
import { Integration } from '../integrations';
export type ConnFilter = 'all' | 'connected' | 'not-connected';
// The claude Connectors-page filter state: All/Connected/Not connected pills + the header search.
export function useConnectorFilters(tools: ToolDefinition[], uninstalledIntegrations: Integration[]) {
const [connFilter, setConnFilter] = useState<ConnFilter>('all');
const [searchOpen, setSearchOpen] = useState(false);
const [searchQ, setSearchQ] = useState('');
const q = searchQ.trim().toLowerCase();
const visibleTools = useMemo(() => {
let out = tools;
if (connFilter === 'connected') out = out.filter((t) => t.enabled !== false);
if (connFilter === 'not-connected') out = out.filter((t) => t.enabled === false);
if (q) out = out.filter((t) => (t.name || '').toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q));
return out;
}, [tools, connFilter, q]);
const visibleGallery = useMemo(() => {
if (connFilter === 'connected') return [];
if (!q) return uninstalledIntegrations;
return uninstalledIntegrations.filter((ig) => ig.name.toLowerCase().includes(q) || ig.description.toLowerCase().includes(q));
}, [uninstalledIntegrations, connFilter, q]);
const popularUninstalled = useMemo(
() => ['google-workspace', 'slack', 'notion'].map((id) => uninstalledIntegrations.find((ig) => ig.id === id)).filter((ig): ig is Integration => !!ig),
[uninstalledIntegrations],
);
return { connFilter, setConnFilter, searchOpen, setSearchOpen, searchQ, setSearchQ, q, visibleTools, visibleGallery, popularUninstalled };
}