Skip to content

Commit 9fa6eaa

Browse files
authored
feat: add standardized search to all table views (#72)
Add reusable search hook and component, then integrate server-side search across all list views (devboxes, blueprints, snapshots, objects, network policies) using the shared code. - Add useListSearch hook for managing search state - Add SearchBar component for consistent UI - Refactor devbox list to use shared search components - Add search to blueprint, snapshot, object, and network-policy lists ## Description <!-- Provide a brief description of your changes --> **Note:** PR titles should follow [Conventional Commits](https://www.conventionalcommits.org/) format (e.g., `feat(devbox): add support for custom env vars` or `fix(snapshot): resolve pagination issue`) as they are used for automatic release notes generation. ## Type of Change <!-- Mark the relevant option with an 'x' --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test updates ## Related Issues <!-- Link to related issues using #issue-number --> Closes # ## Changes Made <!-- Describe the changes in detail --> ## Testing <!-- Describe how you tested your changes --> - [ ] I have tested locally - [ ] I have added/updated tests - [ ] All existing tests pass ## Checklist - [ ] My code follows the code style of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published ## Screenshots (if applicable) <!-- Add screenshots to help explain your changes --> ## Additional Notes <!-- Any additional information that reviewers should know -->
1 parent acda1b8 commit 9fa6eaa

8 files changed

Lines changed: 433 additions & 77 deletions

File tree

src/commands/blueprint/list.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { createTextColumn, Table } from "../../components/Table.js";
1414
import { Operation } from "../../components/OperationsMenu.js";
1515
import { ActionsPopup } from "../../components/ActionsPopup.js";
1616
import { formatTimeAgo } from "../../components/ResourceListView.js";
17+
import { SearchBar } from "../../components/SearchBar.js";
1718
import { output, outputError } from "../../utils/output.js";
1819
import { getBlueprintUrl } from "../../utils/url.js";
1920
import { colors } from "../../utils/theme.js";
@@ -22,6 +23,7 @@ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
2223
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
2324
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
2425
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
26+
import { useListSearch } from "../../hooks/useListSearch.js";
2527
import { useNavigation } from "../../store/navigationStore.js";
2628
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
2729

@@ -73,8 +75,14 @@ const ListBlueprintsUI = ({
7375
const [showPopup, setShowPopup] = React.useState(false);
7476
const { navigate } = useNavigation();
7577

78+
// Search state
79+
const search = useListSearch({
80+
onSearchSubmit: () => setSelectedIndex(0),
81+
onSearchClear: () => setSelectedIndex(0),
82+
});
83+
7684
// Calculate overhead for viewport height
77-
const overhead = 13;
85+
const overhead = 13 + search.getSearchOverhead();
7886
const { viewportHeight, terminalWidth } = useViewportHeight({
7987
overhead,
8088
minHeight: 5,
@@ -111,6 +119,9 @@ const ListBlueprintsUI = ({
111119
if (params.startingAt) {
112120
queryParams.starting_after = params.startingAt;
113121
}
122+
if (search.submittedSearchQuery) {
123+
queryParams.search = search.submittedSearchQuery;
124+
}
114125

115126
// Fetch ONE page only
116127
const page = (await client.blueprints.list(
@@ -137,7 +148,7 @@ const ListBlueprintsUI = ({
137148

138149
return result;
139150
},
140-
[],
151+
[search.submittedSearchQuery],
141152
);
142153

143154
// Use the shared pagination hook
@@ -161,8 +172,9 @@ const ListBlueprintsUI = ({
161172
!showPopup &&
162173
!showCreateDevbox &&
163174
!executingOperation &&
164-
!showDeleteConfirm,
165-
deps: [PAGE_SIZE],
175+
!showDeleteConfirm &&
176+
!search.searchMode,
177+
deps: [PAGE_SIZE, search.submittedSearchQuery],
166178
});
167179

168180
// Memoize columns array
@@ -416,6 +428,14 @@ const ListBlueprintsUI = ({
416428

417429
// Handle input for all views
418430
useInput((input, key) => {
431+
// Handle search mode input
432+
if (search.searchMode) {
433+
if (key.escape) {
434+
search.cancelSearch();
435+
}
436+
return;
437+
}
438+
419439
// Handle operation input mode
420440
if (executingOperation && !operationResult && !operationError) {
421441
// Allow escape/q to cancel any operation, even during loading
@@ -579,7 +599,12 @@ const ListBlueprintsUI = ({
579599
exec(openCommand);
580600
};
581601
openBrowser();
602+
} else if (input === "/") {
603+
search.enterSearchMode();
582604
} else if (key.escape) {
605+
if (search.handleEscape()) {
606+
return;
607+
}
583608
if (onBack) {
584609
onBack();
585610
} else if (onExit) {
@@ -764,6 +789,17 @@ const ListBlueprintsUI = ({
764789
<>
765790
<Breadcrumb items={[{ label: "Blueprints", active: true }]} />
766791

792+
{/* Search bar */}
793+
<SearchBar
794+
searchMode={search.searchMode}
795+
searchQuery={search.searchQuery}
796+
submittedSearchQuery={search.submittedSearchQuery}
797+
resultCount={totalCount}
798+
onSearchChange={search.setSearchQuery}
799+
onSearchSubmit={search.submitSearch}
800+
placeholder="Search blueprints..."
801+
/>
802+
767803
{/* Table */}
768804
{!showPopup && (
769805
<Table
@@ -814,6 +850,17 @@ const ListBlueprintsUI = ({
814850
<Text color={colors.textDim} dimColor>
815851
Showing {startIndex + 1}-{endIndex} of {totalCount}
816852
</Text>
853+
{search.submittedSearchQuery && (
854+
<>
855+
<Text color={colors.textDim} dimColor>
856+
{" "}
857+
{" "}
858+
</Text>
859+
<Text color={colors.warning}>
860+
Filtered: &quot;{search.submittedSearchQuery}&quot;
861+
</Text>
862+
</>
863+
)}
817864
</Box>
818865
)}
819866

@@ -856,6 +903,7 @@ const ListBlueprintsUI = ({
856903
{ key: "Enter", label: "Details" },
857904
{ key: "a", label: "Actions" },
858905
{ key: "o", label: "Browser" },
906+
{ key: "/", label: "Search" },
859907
{ key: "Esc", label: "Back" },
860908
]}
861909
/>

src/commands/devbox/list.tsx

Lines changed: 40 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React from "react";
22
import { Box, Text, useInput, useApp } from "ink";
3-
import TextInput from "ink-text-input";
43
import figures from "figures";
54
import type { DevboxesCursorIDPage } from "@runloop/api-client/pagination";
65
import { getClient } from "../../utils/client.js";
@@ -12,6 +11,7 @@ import { NavigationTips } from "../../components/NavigationTips.js";
1211
import type { Column } from "../../components/Table.js";
1312
import { Table, createTextColumn } from "../../components/Table.js";
1413
import { formatTimeAgo } from "../../components/ResourceListView.js";
14+
import { SearchBar } from "../../components/SearchBar.js";
1515
import { output, outputError } from "../../utils/output.js";
1616
import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
1717
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
@@ -21,6 +21,7 @@ import { getDevboxUrl } from "../../utils/url.js";
2121
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
2222
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
2323
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
24+
import { useListSearch } from "../../hooks/useListSearch.js";
2425
import { colors } from "../../utils/theme.js";
2526
import { useDevboxStore, type Devbox } from "../../store/devboxStore.js";
2627

@@ -50,9 +51,12 @@ const ListDevboxesUI = ({
5051
const [showActions, setShowActions] = React.useState(false);
5152
const [showPopup, setShowPopup] = React.useState(false);
5253
const [selectedOperation, setSelectedOperation] = React.useState(0);
53-
const [searchMode, setSearchMode] = React.useState(false);
54-
const [searchQuery, setSearchQuery] = React.useState("");
55-
const [submittedSearchQuery, setSubmittedSearchQuery] = React.useState("");
54+
55+
// Search state using shared hook
56+
const search = useListSearch({
57+
onSearchSubmit: () => setSelectedIndex(0),
58+
onSearchClear: () => setSelectedIndex(0),
59+
});
5660

5761
// Get devbox store setter to sync data for detail screen
5862
const setDevboxesInStore = useDevboxStore((state) => state.setDevboxes);
@@ -65,7 +69,7 @@ const ListDevboxesUI = ({
6569
// - Help bar (marginTop + content): 2 lines
6670
// - Safety buffer for edge cases: 1 line
6771
// Total: 13 lines base + 2 if searching
68-
const overhead = 13 + (searchMode || submittedSearchQuery ? 2 : 0);
72+
const overhead = 13 + search.getSearchOverhead();
6973
const { viewportHeight, terminalWidth } = useViewportHeight({
7074
overhead,
7175
minHeight: 5,
@@ -89,8 +93,8 @@ const ListDevboxesUI = ({
8993
if (status) {
9094
queryParams.status = status;
9195
}
92-
if (submittedSearchQuery) {
93-
queryParams.search = submittedSearchQuery;
96+
if (search.submittedSearchQuery) {
97+
queryParams.search = search.submittedSearchQuery;
9498
}
9599

96100
// Fetch ONE page only
@@ -113,7 +117,7 @@ const ListDevboxesUI = ({
113117

114118
return result;
115119
},
116-
[status, submittedSearchQuery],
120+
[status, search.submittedSearchQuery],
117121
);
118122

119123
// Use the shared pagination hook
@@ -134,8 +138,12 @@ const ListDevboxesUI = ({
134138
getItemId: (devbox: Devbox) => devbox.id,
135139
pollInterval: 2000,
136140
pollingEnabled:
137-
!showDetails && !showCreate && !showActions && !showPopup && !searchMode,
138-
deps: [status, submittedSearchQuery, PAGE_SIZE],
141+
!showDetails &&
142+
!showCreate &&
143+
!showActions &&
144+
!showPopup &&
145+
!search.searchMode,
146+
deps: [status, search.submittedSearchQuery, PAGE_SIZE],
139147
});
140148

141149
// Sync devboxes to store for detail screen
@@ -443,10 +451,9 @@ const ListDevboxesUI = ({
443451
const pageDevboxes = devboxes.length;
444452

445453
// Skip input handling when in search mode - let TextInput handle it
446-
if (searchMode) {
454+
if (search.searchMode) {
447455
if (key.escape) {
448-
setSearchMode(false);
449-
setSearchQuery("");
456+
search.cancelSearch();
450457
}
451458
return;
452459
}
@@ -542,20 +549,17 @@ const ListDevboxesUI = ({
542549
};
543550
openBrowser();
544551
} else if (input === "/") {
545-
setSearchMode(true);
552+
search.enterSearchMode();
546553
} else if (key.escape) {
547-
if (submittedSearchQuery) {
548-
setSubmittedSearchQuery("");
549-
setSearchQuery("");
550-
setSelectedIndex(0);
554+
if (search.handleEscape()) {
555+
return;
556+
}
557+
if (onBack) {
558+
onBack();
559+
} else if (onExit) {
560+
onExit();
551561
} else {
552-
if (onBack) {
553-
onBack();
554-
} else if (onExit) {
555-
onExit();
556-
} else {
557-
inkExit();
558-
}
562+
inkExit();
559563
}
560564
}
561565
});
@@ -634,39 +638,15 @@ const ListDevboxesUI = ({
634638
<Breadcrumb items={[{ label: "Devboxes", active: true }]} />
635639

636640
{/* Search bar */}
637-
{searchMode && (
638-
<Box marginBottom={1}>
639-
<Text color={colors.primary}>{figures.pointerSmall} Search: </Text>
640-
<TextInput
641-
value={searchQuery}
642-
onChange={setSearchQuery}
643-
placeholder="Type to search..."
644-
onSubmit={() => {
645-
setSearchMode(false);
646-
setSubmittedSearchQuery(searchQuery);
647-
setSelectedIndex(0);
648-
}}
649-
/>
650-
<Text color={colors.textDim} dimColor>
651-
{" "}
652-
[Enter to search, Esc to cancel]
653-
</Text>
654-
</Box>
655-
)}
656-
{!searchMode && submittedSearchQuery && (
657-
<Box marginBottom={1}>
658-
<Text color={colors.primary}>{figures.info} Searching for: </Text>
659-
<Text color={colors.warning} bold>
660-
{submittedSearchQuery.length > 50
661-
? submittedSearchQuery.substring(0, 50) + "..."
662-
: submittedSearchQuery}
663-
</Text>
664-
<Text color={colors.textDim} dimColor>
665-
{" "}
666-
({totalCount} results) [/ to edit, Esc to clear]
667-
</Text>
668-
</Box>
669-
)}
641+
<SearchBar
642+
searchMode={search.searchMode}
643+
searchQuery={search.searchQuery}
644+
submittedSearchQuery={search.submittedSearchQuery}
645+
resultCount={totalCount}
646+
onSearchChange={search.setSearchQuery}
647+
onSearchSubmit={search.submitSearch}
648+
placeholder="Search devboxes..."
649+
/>
670650

671651
{/* Table - hide when popup is shown */}
672652
{!showPopup && (
@@ -718,14 +698,14 @@ const ListDevboxesUI = ({
718698
<Text color={colors.textDim} dimColor>
719699
Showing {startIndex + 1}-{endIndex} of {totalCount}
720700
</Text>
721-
{submittedSearchQuery && (
701+
{search.submittedSearchQuery && (
722702
<>
723703
<Text color={colors.textDim} dimColor>
724704
{" "}
725705
{" "}
726706
</Text>
727707
<Text color={colors.warning}>
728-
Filtered: &quot;{submittedSearchQuery}&quot;
708+
Filtered: &quot;{search.submittedSearchQuery}&quot;
729709
</Text>
730710
</>
731711
)}

0 commit comments

Comments
 (0)