feat(ui): add search filter to provider grid (#10437)

This commit is contained in:
Harnoor Singh
2026-07-14 22:00:13 +05:30
committed by GitHub
parent 3f08de4444
commit c1eebf034d
17 changed files with 179 additions and 37 deletions
@@ -10,7 +10,8 @@ import {
acpDeleteCustomProvider,
acpUpdateCustomProviderFromRequest,
} from '../../../acp/providers';
import { Plus } from 'lucide-react';
import { Plus, Search } from 'lucide-react';
import { Input } from '../../ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
import { SwitchModelModal } from '../models/subcomponents/SwitchModelModal';
@@ -43,6 +44,14 @@ const i18n = defineMessages({
id: 'providerGrid.chooseModel',
defaultMessage: 'Choose Model',
},
searchPlaceholder: {
id: 'providerGrid.searchPlaceholder',
defaultMessage: 'Search providers...',
},
noMatch: {
id: 'providerGrid.noMatch',
defaultMessage: 'No providers match "{query}"',
},
});
const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
@@ -97,6 +106,7 @@ function ProviderCards({
onModelSelected?: (model?: string) => void;
}) {
const intl = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [configuringProvider, setConfiguringProvider] = useState<ProviderDetails | null>(null);
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
@@ -232,6 +242,8 @@ function ProviderCards({
[refreshProviders]
);
const query = searchQuery.trim().toLowerCase();
const providerCards = useMemo(() => {
// providers needs to be an array
const providersArray = Array.isArray(providers) ? providers : [];
@@ -239,7 +251,14 @@ function ProviderCards({
const sortedProviders = [...providersArray].sort((a, b) =>
a.metadata.display_name.localeCompare(b.metadata.display_name)
);
const cards = sortedProviders.map((provider) => (
const filteredProviders = query
? sortedProviders.filter(
(provider) =>
provider.metadata.display_name.toLowerCase().includes(query) ||
provider.metadata.description.toLowerCase().includes(query)
)
: sortedProviders;
const cards = filteredProviders.map((provider) => (
<ProviderCard
key={provider.name}
provider={provider}
@@ -254,7 +273,18 @@ function ProviderCards({
);
return cards;
}, [providers, isOnboarding, configureProviderViaModal, handleProviderLaunchWithModelSelection]);
}, [
providers,
query,
isOnboarding,
configureProviderViaModal,
handleProviderLaunchWithModelSelection,
]);
const hasNoMatches =
query.length > 0 &&
providerCards.length === 1 &&
(Array.isArray(providers) ? providers.length : 0) > 0;
const initialData = editingProvider && {
engine: editingProvider.config.engine,
@@ -277,7 +307,25 @@ function ProviderCards({
: intl.formatMessage(i18n.addProviderTitle);
return (
<>
{providerCards}
<div className="mx-auto mb-4 max-w-md px-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" />
<Input
type="search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={intl.formatMessage(i18n.searchPlaceholder)}
className="pl-9"
data-testid="provider-search-input"
/>
</div>
</div>
<GridLayout>{providerCards}</GridLayout>
{hasNoMatches && (
<div className="mt-2 text-center text-sm text-text-secondary">
{intl.formatMessage(i18n.noMatch, { query: searchQuery.trim() })}
</div>
)}
<Dialog open={showCustomProviderModal} onOpenChange={handleCloseModal}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
@@ -330,14 +378,12 @@ export default function ProviderGrid({
onModelSelected?: (model?: string) => void;
}) {
return (
<GridLayout>
<ProviderCards
providers={providers}
isOnboarding={isOnboarding}
refreshProviders={refreshProviders}
setView={setView}
onModelSelected={onModelSelected}
/>
</GridLayout>
<ProviderCards
providers={providers}
isOnboarding={isOnboarding}
refreshProviders={refreshProviders}
setView={setView}
onModelSelected={onModelSelected}
/>
);
}