Skip to content

Commit a929d76

Browse files
committed
feat(web): add provider filtering functionality
Implement interactive provider filter with search capability allowing users to filter models by provider. The filter includes a dropdown popover with searchable provider list, checkbox selection, and URL parameter persistence. Filter state is preserved in URL query params and synchronized with the search functionality. fix: adjust button placement and formatting in the provider filter section fix: adjust provider popover positioning to align with button fix: change provider popover position from absolute to fixed feat(web): add provider reset button and enhance provider filtering functionality fix: clear provider search input when closing the popover refactor(web): optimize provider filter performance and reduce code duplication - Unified filter logic by integrating provider filtering into filterTable function - Eliminated duplicate search filter code in filterByProviders (reduced from ~30 to 9 lines) - Cached DOM queries (providerCountSpan) and provider values (allProviderValues) for better performance - Optimized filter flow with early returns to avoid unnecessary operations - Reduced table iterations from 2 to 1 when both filters are active - Improved algorithmic efficiency by checking provider filter first (fast Set lookup) style: clean up provider filter styles and improve hover effects fix: improve provider popover closing logic to handle nested clicks style: optimize provider list and checkbox flex properties for better layout fix: adjust provider popover positioning to use fixed layout relative to viewport refactor: remove redundant comments Reverted css formatting fix: prevent direct checkbox interaction to ensure consistent provider selection behavior Clicking directly on the checkbox vs the label was causing inconsistent selection logic due to event handling order. By disabling pointer events on checkboxes, all clicks now flow through the label handler consistently.
1 parent 8660aaf commit a929d76

File tree

3 files changed

+365
-4
lines changed

3 files changed

+365
-4
lines changed

packages/web/src/index.css

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,150 @@ dialog {
546546
}
547547
}
548548

549-
}
549+
}
550+
551+
/* Provider Filter */
552+
.provider-filter-button {
553+
background-color: var(--color-background);
554+
color: var(--color-text);
555+
border: 1px solid var(--color-border);
556+
display: flex;
557+
align-items: center;
558+
gap: 0.375rem;
559+
560+
&:hover {
561+
background-color: var(--color-surface);
562+
}
563+
564+
#provider-count {
565+
color: var(--color-text-tertiary);
566+
}
567+
}
568+
569+
.provider-popover {
570+
position: fixed;
571+
z-index: 100;
572+
background-color: var(--color-background);
573+
border: 1px solid var(--color-border);
574+
border-radius: 0.375rem;
575+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.05),
576+
0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07);
577+
width: 20rem;
578+
max-height: 28rem;
579+
overflow: hidden;
580+
}
581+
582+
.provider-popover-content {
583+
display: flex;
584+
flex-direction: column;
585+
max-height: 28rem;
586+
}
587+
588+
.provider-search-container {
589+
padding: 0.75rem;
590+
border-bottom: 1px solid var(--color-border);
591+
flex: 0 0 auto;
592+
display: flex;
593+
gap: 0.5rem;
594+
align-items: center;
595+
596+
input {
597+
flex: 1 1 auto;
598+
font-size: 0.8125rem;
599+
line-height: 1.1;
600+
padding: 0.5rem 0.625rem;
601+
border-radius: 0.25rem;
602+
border: 1px solid var(--color-border);
603+
height: 2rem;
604+
background: none;
605+
color: var(--color-text);
606+
607+
&:focus {
608+
border-color: var(--color-brand);
609+
outline: none;
610+
}
611+
}
612+
}
613+
614+
.provider-reset-button {
615+
flex: 0 0 auto;
616+
cursor: pointer;
617+
border: 1px solid var(--color-border);
618+
background-color: var(--color-background);
619+
color: var(--color-text);
620+
font-size: 0.8125rem;
621+
line-height: 1.1;
622+
height: 2rem;
623+
padding: 0.5rem 0.75rem;
624+
border-radius: 0.25rem;
625+
white-space: nowrap;
626+
627+
&:hover:not(:disabled) {
628+
background-color: var(--color-surface);
629+
}
630+
631+
&:disabled {
632+
cursor: not-allowed;
633+
opacity: 0.5;
634+
color: var(--color-text-tertiary);
635+
}
636+
}
637+
638+
.provider-list {
639+
flex: 1;
640+
overflow-y: auto;
641+
padding: 0.375rem;
642+
overscroll-behavior: contain;
643+
644+
&::-webkit-scrollbar {
645+
width: 8px;
646+
}
647+
648+
&::-webkit-scrollbar-thumb {
649+
background-color: var(--color-border);
650+
border-radius: 4px;
651+
}
652+
}
653+
654+
.provider-item {
655+
display: flex;
656+
align-items: center;
657+
gap: 0.5rem;
658+
padding: 0.5rem 0.625rem;
659+
cursor: pointer;
660+
border-radius: 0.25rem;
661+
user-select: none;
662+
663+
&:hover {
664+
background-color: var(--color-surface);
665+
}
666+
}
667+
668+
.provider-checkbox {
669+
flex-shrink: 0;
670+
width: 1rem;
671+
height: 1rem;
672+
cursor: pointer;
673+
accent-color: var(--color-brand);
674+
pointer-events: none;
675+
}
676+
677+
.provider-icon {
678+
flex-shrink: 0;
679+
display: flex;
680+
align-items: center;
681+
justify-content: center;
682+
width: 1rem;
683+
height: 1rem;
684+
685+
svg {
686+
width: 100%;
687+
height: 100%;
688+
color: var(--color-text-secondary);
689+
}
690+
}
691+
692+
.provider-name {
693+
flex: 1;
694+
font-size: 0.875rem;
695+
}

packages/web/src/index.ts

Lines changed: 177 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,60 @@ document.querySelectorAll("th.sortable").forEach((header) => {
143143
});
144144
});
145145

146+
///////////////////////////////
147+
// Handle Provider Filter
148+
///////////////////////////////
149+
const providerFilterButton = document.getElementById("provider-filter")!;
150+
const providerPopover = document.getElementById("provider-popover")!;
151+
const providerSearch = document.getElementById(
152+
"provider-search"
153+
)! as HTMLInputElement;
154+
const providerResetButton = document.getElementById(
155+
"provider-reset"
156+
)! as HTMLButtonElement;
157+
const providerCheckboxes = document.querySelectorAll(
158+
".provider-checkbox"
159+
) as NodeListOf<HTMLInputElement>;
160+
const providerItems = document.querySelectorAll(
161+
".provider-item"
162+
) as NodeListOf<HTMLElement>;
163+
const providerCountSpan = document.getElementById("provider-count")!;
164+
165+
const allProviderValues = Array.from(providerCheckboxes).map((cb) => cb.value);
166+
let selectedProviders = new Set<string>(allProviderValues);
167+
146168
///////////////////
147169
// Handle Search
148170
///////////////////
149171
function filterTable(value: string) {
150-
const lowerCaseValues = value.toLowerCase().split(",").filter(str => str.trim() !== "");
172+
const lowerCaseValues = value
173+
.toLowerCase()
174+
.split(",")
175+
.filter((str) => str.trim() !== "");
151176
const rows = document.querySelectorAll(
152177
"table tbody tr"
153178
) as NodeListOf<HTMLTableRowElement>;
154179

155180
rows.forEach((row) => {
181+
const providerId = row.cells[2].textContent?.trim() || "";
182+
const isProviderSelected = selectedProviders.has(providerId);
183+
184+
if (!isProviderSelected) {
185+
row.style.display = "none";
186+
return;
187+
}
188+
189+
if (lowerCaseValues.length === 0) {
190+
row.style.display = "";
191+
return;
192+
}
193+
156194
const cellTexts = Array.from(row.cells).map((cell) =>
157195
cell.textContent!.toLowerCase()
158196
);
159-
const isVisible = lowerCaseValues.length === 0 ||
160-
lowerCaseValues.some((lowerCaseValue) => cellTexts.some((text) => text.includes(lowerCaseValue)));
197+
const isVisible = lowerCaseValues.some((lowerCaseValue) =>
198+
cellTexts.some((text) => text.includes(lowerCaseValue))
199+
);
161200
row.style.display = isVisible ? "" : "none";
162201
});
163202

@@ -182,6 +221,124 @@ search.addEventListener("keydown", (e) => {
182221
}
183222
});
184223

224+
function updateProviderCount() {
225+
const totalProviders = providerCheckboxes.length;
226+
const selectedCount = selectedProviders.size;
227+
providerCountSpan.textContent = `${selectedCount}/${totalProviders}`;
228+
providerResetButton.disabled = selectedCount === totalProviders;
229+
}
230+
231+
function filterByProviders() {
232+
filterTable(search.value);
233+
updateProviderCount();
234+
updateQueryParams({
235+
providers:
236+
selectedProviders.size === providerCheckboxes.length
237+
? null
238+
: Array.from(selectedProviders).sort().join(","),
239+
});
240+
}
241+
242+
function togglePopover() {
243+
const isVisible = providerPopover.style.display !== "none";
244+
providerPopover.style.display = isVisible ? "none" : "block";
245+
246+
if (!isVisible) {
247+
const buttonRect = providerFilterButton.getBoundingClientRect();
248+
providerPopover.style.top = `${buttonRect.bottom + 4}px`;
249+
providerPopover.style.left = `${
250+
buttonRect.right - providerPopover.offsetWidth
251+
}px`;
252+
}
253+
}
254+
255+
function closePopover() {
256+
providerPopover.style.display = "none";
257+
258+
providerSearch.value = "";
259+
filterProviderList("");
260+
}
261+
262+
function filterProviderList(searchValue: string) {
263+
const searchLower = searchValue.toLowerCase();
264+
providerItems.forEach((item) => {
265+
const providerName = item.getAttribute("data-provider-name") || "";
266+
if (providerName.includes(searchLower)) {
267+
item.style.display = "";
268+
} else {
269+
item.style.display = "none";
270+
}
271+
});
272+
}
273+
274+
providerFilterButton.addEventListener("click", (e) => {
275+
e.stopPropagation();
276+
togglePopover();
277+
});
278+
279+
document.addEventListener("click", (e) => {
280+
if (
281+
!providerPopover.contains(e.target as Node) &&
282+
!providerFilterButton.contains(e.target as Node)
283+
) {
284+
closePopover();
285+
}
286+
});
287+
288+
providerSearch.addEventListener("input", () => {
289+
filterProviderList(providerSearch.value);
290+
});
291+
292+
providerResetButton.addEventListener("click", (e) => {
293+
e.stopPropagation();
294+
295+
selectedProviders = new Set(allProviderValues);
296+
providerCheckboxes.forEach((cb) => {
297+
cb.checked = true;
298+
});
299+
300+
providerSearch.value = "";
301+
filterProviderList("");
302+
303+
filterByProviders();
304+
});
305+
306+
providerItems.forEach((item) => {
307+
item.addEventListener("click", (e) => {
308+
e.preventDefault();
309+
310+
const checkbox = item.querySelector(
311+
".provider-checkbox"
312+
) as HTMLInputElement;
313+
const providerId = checkbox.value;
314+
const wasChecked = checkbox.checked;
315+
const allSelected = selectedProviders.size === providerCheckboxes.length;
316+
317+
if (allSelected) {
318+
selectedProviders.clear();
319+
selectedProviders.add(providerId);
320+
providerCheckboxes.forEach((cb) => {
321+
cb.checked = cb.value === providerId;
322+
});
323+
} else if (wasChecked) {
324+
if (selectedProviders.size === 1) {
325+
selectedProviders = new Set(allProviderValues);
326+
providerCheckboxes.forEach((cb) => {
327+
cb.checked = true;
328+
});
329+
} else {
330+
selectedProviders.delete(providerId);
331+
checkbox.checked = false;
332+
}
333+
} else {
334+
selectedProviders.add(providerId);
335+
checkbox.checked = true;
336+
}
337+
338+
filterByProviders();
339+
});
340+
});
341+
185342
///////////////////////////////////
186343
// Handle Copy model ID function
187344
///////////////////////////////////
@@ -217,6 +374,19 @@ search.addEventListener("keydown", (e) => {
217374
function initializeFromURL() {
218375
const params = getQueryParams();
219376

377+
(() => {
378+
const providersParam = params.get("providers");
379+
if (providersParam) {
380+
const providerIds = providersParam.split(",");
381+
selectedProviders = new Set(providerIds);
382+
383+
providerCheckboxes.forEach((cb) => {
384+
cb.checked = selectedProviders.has(cb.value);
385+
});
386+
}
387+
updateProviderCount();
388+
})();
389+
220390
(() => {
221391
const searchQuery = params.get("search");
222392
if (!searchQuery) return;
@@ -234,6 +404,10 @@ function initializeFromURL() {
234404
const direction = (params.get("order") as "asc" | "desc") || "asc";
235405
sortTable(columnIndex, direction);
236406
})();
407+
408+
if (selectedProviders.size < providerCheckboxes.length) {
409+
filterByProviders();
410+
}
237411
}
238412

239413
document.addEventListener("DOMContentLoaded", initializeFromURL);

0 commit comments

Comments
 (0)