Skip to content

Commit 85fb744

Browse files
Adding prebuild status filter to repo configs list ui (#19205)
* Adding prebuildsEnabled filter to projects search query * adding prebuilds_enabled param * stubbing out UI for prebuild status filter * mark prebuilds_enabled as optional so we have an undefined state * ui stuff * whoopsie * drop animations for now * comments * cleanup * convert to string * consider prebuild filter in showing table * add prebuilds query param * fix issue w/ fragment around routes preventing 404 handler
1 parent 277c9da commit 85fb744

File tree

15 files changed

+470
-138
lines changed

15 files changed

+470
-138
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@radix-ui/react-label": "^2.0.2",
1515
"@radix-ui/react-popover": "^1.0.7",
1616
"@radix-ui/react-radio-group": "^1.1.3",
17+
"@radix-ui/react-select": "^2.0.0",
1718
"@radix-ui/react-switch": "^1.0.3",
1819
"@radix-ui/react-tooltip": "^1.0.7",
1920
"@stripe/react-stripe-js": "^1.7.2",

components/dashboard/src/app/AppRoutes.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,9 @@ export const AppRoutes = () => {
206206
<Route exact path={`/projects/:projectSlug/variables`} component={ProjectVariables} />
207207
<Route exact path={`/projects/:projectSlug/:prebuildId`} component={Prebuild} />
208208

209-
{repoConfigListAndDetail && (
210-
<>
211-
<Route exact path="/repositories" component={ConfigurationListPage} />
212-
{/* Handles all /repositories/:id/* routes in a nested router */}
213-
<Route path="/repositories/:id" component={ConfigurationDetailPage} />
214-
</>
215-
)}
209+
{repoConfigListAndDetail && <Route exact path="/repositories" component={ConfigurationListPage} />}
210+
{/* Handles all /repositories/:id/* routes in a nested router */}
211+
{repoConfigListAndDetail && <Route path="/repositories/:id" component={ConfigurationDetailPage} />}
216212
{/* basic redirect for old team slugs */}
217213
<Route path={["/t/"]} exact>
218214
<Redirect to="/projects" />
@@ -251,7 +247,6 @@ export const AppRoutes = () => {
251247
// delegate to our website to handle the request
252248
if (isGitpodIo()) {
253249
window.location.host = "www.gitpod.io";
254-
return;
255250
}
256251

257252
return (
@@ -261,7 +256,7 @@ export const AppRoutes = () => {
261256
</div>
262257
);
263258
}}
264-
></Route>
259+
/>
265260
</Switch>
266261
</div>
267262
<WebsocketClients />
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as React from "react";
8+
import * as SelectPrimitive from "@radix-ui/react-select";
9+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
10+
import { cn } from "@podkit/lib/cn";
11+
12+
const Select = SelectPrimitive.Root;
13+
14+
const SelectGroup = SelectPrimitive.Group;
15+
16+
const SelectValue = SelectPrimitive.Value;
17+
18+
const SelectTrigger = React.forwardRef<
19+
React.ElementRef<typeof SelectPrimitive.Trigger>,
20+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
21+
>(({ className, children, ...props }, ref) => (
22+
<SelectPrimitive.Trigger
23+
ref={ref}
24+
className={cn(
25+
"flex items-center justify-between w-full max-w-lg",
26+
"rounded-lg py-[7px] px-2",
27+
"text-sm text-pk-content-primary placeholder:text-pk-content-tertiary",
28+
"bg-pk-surface-primary",
29+
"border border-pk-border-base",
30+
"text-sm",
31+
"hover:[&_svg]:text-pk-content-primary",
32+
"focus:outline-none focus-visible:ring-4 focus-visible:ring-ring",
33+
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
34+
className,
35+
)}
36+
{...props}
37+
>
38+
{children}
39+
<SelectPrimitive.Icon asChild>
40+
<ChevronDown className="h-4 w-4 text-pk-content-disabled" />
41+
</SelectPrimitive.Icon>
42+
</SelectPrimitive.Trigger>
43+
));
44+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
45+
46+
const SelectScrollUpButton = React.forwardRef<
47+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
48+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
49+
>(({ className, ...props }, ref) => (
50+
<SelectPrimitive.ScrollUpButton
51+
ref={ref}
52+
className={cn("flex cursor-default items-center justify-center py-1", className)}
53+
{...props}
54+
>
55+
<ChevronUp className="h-4 w-4" />
56+
</SelectPrimitive.ScrollUpButton>
57+
));
58+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
59+
60+
const SelectScrollDownButton = React.forwardRef<
61+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
62+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
63+
>(({ className, ...props }, ref) => (
64+
<SelectPrimitive.ScrollDownButton
65+
ref={ref}
66+
className={cn("flex cursor-default items-center justify-center py-1", className)}
67+
{...props}
68+
>
69+
<ChevronDown className="h-4 w-4" />
70+
</SelectPrimitive.ScrollDownButton>
71+
));
72+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
73+
74+
const SelectContent = React.forwardRef<
75+
React.ElementRef<typeof SelectPrimitive.Content>,
76+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
77+
>(({ className, children, position = "popper", ...props }, ref) => (
78+
<SelectPrimitive.Portal>
79+
<SelectPrimitive.Content
80+
ref={ref}
81+
className={cn(
82+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden",
83+
"rounded-lg border border-pk-border-base",
84+
"bg-pk-surface-primary text-pk-content-primary shadow-md",
85+
position === "popper" &&
86+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
87+
className,
88+
)}
89+
position={position}
90+
{...props}
91+
>
92+
<SelectScrollUpButton />
93+
<SelectPrimitive.Viewport
94+
className={cn(
95+
"p-1",
96+
position === "popper" &&
97+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
98+
)}
99+
>
100+
{children}
101+
</SelectPrimitive.Viewport>
102+
<SelectScrollDownButton />
103+
</SelectPrimitive.Content>
104+
</SelectPrimitive.Portal>
105+
));
106+
SelectContent.displayName = SelectPrimitive.Content.displayName;
107+
108+
const SelectLabel = React.forwardRef<
109+
React.ElementRef<typeof SelectPrimitive.Label>,
110+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
111+
>(({ className, ...props }, ref) => (
112+
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
113+
));
114+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
115+
116+
const SelectItem = React.forwardRef<
117+
React.ElementRef<typeof SelectPrimitive.Item>,
118+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
119+
>(({ className, children, ...props }, ref) => (
120+
<SelectPrimitive.Item
121+
ref={ref}
122+
className={cn(
123+
"relative flex w-full cursor-default items-center select-none",
124+
"rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
125+
"focus:bg-pk-surface-tertiary focus:text-accent-foreground",
126+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
127+
className,
128+
)}
129+
{...props}
130+
>
131+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
132+
<SelectPrimitive.ItemIndicator>
133+
<Check className="h-4 w-4" />
134+
</SelectPrimitive.ItemIndicator>
135+
</span>
136+
137+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
138+
</SelectPrimitive.Item>
139+
));
140+
SelectItem.displayName = SelectPrimitive.Item.displayName;
141+
142+
const SelectSeparator = React.forwardRef<
143+
React.ElementRef<typeof SelectPrimitive.Separator>,
144+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
145+
>(({ className, ...props }, ref) => (
146+
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-pk-border-base", className)} {...props} />
147+
));
148+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
149+
150+
export {
151+
Select,
152+
SelectGroup,
153+
SelectValue,
154+
SelectTrigger,
155+
SelectContent,
156+
SelectLabel,
157+
SelectItem,
158+
SelectSeparator,
159+
SelectScrollUpButton,
160+
SelectScrollDownButton,
161+
};

components/dashboard/src/data/configurations/configuration-queries.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ const BASE_KEY = "configurations";
2222
type ListConfigurationsArgs = {
2323
pageSize?: number;
2424
searchTerm?: string;
25+
prebuildsEnabled?: boolean;
2526
sortBy: string;
2627
sortOrder: TableSortOrder;
2728
};
2829

29-
export const useListConfigurations = ({ searchTerm = "", pageSize, sortBy, sortOrder }: ListConfigurationsArgs) => {
30+
export const useListConfigurations = (options: ListConfigurationsArgs) => {
3031
const { data: org } = useCurrentOrg();
32+
const { searchTerm = "", prebuildsEnabled, pageSize, sortBy, sortOrder } = options;
3133

3234
return useInfiniteQuery(
33-
getListConfigurationsQueryKey(org?.id || "", { searchTerm, pageSize, sortBy, sortOrder }),
35+
getListConfigurationsQueryKey(org?.id || "", options),
3436
// QueryFn receives the past page's pageParam as it's argument
3537
async ({ pageParam: nextToken }) => {
3638
if (!org) {
@@ -40,6 +42,7 @@ export const useListConfigurations = ({ searchTerm = "", pageSize, sortBy, sortO
4042
const { configurations, pagination } = await configurationClient.listConfigurations({
4143
organizationId: org.id,
4244
searchTerm,
45+
prebuildsEnabled,
4346
pagination: { pageSize, token: nextToken },
4447
sort: [
4548
{

components/dashboard/src/repositories/list/RepositoryList.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ import { RepositoryTable } from "./RepositoryTable";
1818
import { LoadingState } from "@podkit/loading/LoadingState";
1919
import { TableSortOrder } from "@podkit/tables/SortableTable";
2020

21+
const PREBUILD_FILTERS = { all: undefined, enabled: true, disabled: false };
22+
2123
const RepositoryListPage: FC = () => {
2224
useDocumentTitle("Imported repositories");
2325

2426
const history = useHistory();
2527

2628
const params = useQueryParams();
2729
const [searchTerm, setSearchTerm, searchTermDebounced] = useStateWithDebounce(params.get("search") || "");
30+
const [prebuildsFilter, setPrebuildsFilter] = useState(parsePrebuilds(params));
2831
const [sortBy, setSortBy] = useState(parseSortBy(params));
2932
const [sortOrder, setSortOrder] = useState<TableSortOrder>(parseSortOrder(params));
3033
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);
@@ -42,16 +45,22 @@ const RepositoryListPage: FC = () => {
4245
if (sortOrder) {
4346
params.set("sortOrder", sortOrder);
4447
}
48+
// Since "all" is the default, we don't need to set it in the url
49+
if (prebuildsFilter !== "all") {
50+
params.set("prebuilds", prebuildsFilter);
51+
}
4552
params.toString();
4653
history.replace({ search: `?${params.toString()}` });
47-
}, [history, searchTermDebounced, sortBy, sortOrder]);
54+
}, [history, prebuildsFilter, searchTermDebounced, sortBy, sortOrder]);
4855

4956
// TODO: handle isError case
5057
const { data, isLoading, isFetching, isFetchingNextPage, isPreviousData, hasNextPage, fetchNextPage } =
5158
useListConfigurations({
5259
searchTerm: searchTermDebounced,
5360
sortBy: sortBy,
5461
sortOrder: sortOrder,
62+
// Map ui prebuildFilter state to the right api value
63+
prebuildsEnabled: { all: undefined, enabled: true, disabled: false }[prebuildsFilter],
5564
});
5665

5766
const handleRepoImported = useCallback(
@@ -76,7 +85,7 @@ const RepositoryListPage: FC = () => {
7685
const hasMoreThanOnePage = (data?.pages.length ?? 0) > 1;
7786

7887
// This tracks any filters/search params applied
79-
const hasFilters = !!searchTermDebounced;
88+
const hasFilters = !!searchTermDebounced || prebuildsFilter !== "all";
8089

8190
// Show the table once we're done loading and either have results, or have filters applied
8291
const showTable = !isLoading && (configurations.length > 0 || hasFilters);
@@ -94,6 +103,7 @@ const RepositoryListPage: FC = () => {
94103
{showTable && (
95104
<RepositoryTable
96105
searchTerm={searchTerm}
106+
prebuildsFilter={prebuildsFilter}
97107
configurations={configurations}
98108
sortBy={sortBy}
99109
sortOrder={sortOrder}
@@ -105,6 +115,7 @@ const RepositoryListPage: FC = () => {
105115
onImport={() => setShowCreateProjectModal(true)}
106116
onLoadNextPage={() => fetchNextPage()}
107117
onSearchTermChange={setSearchTerm}
118+
onPrebuildsFilterChange={setPrebuildsFilter}
108119
onSort={handleSort}
109120
/>
110121
)}
@@ -139,3 +150,11 @@ const parseSortBy = (params: URLSearchParams) => {
139150
}
140151
return "name";
141152
};
153+
154+
const parsePrebuilds = (params: URLSearchParams): keyof typeof PREBUILD_FILTERS => {
155+
const prebuilds = params.get("prebuilds");
156+
if (prebuilds === "enabled" || prebuilds === "disabled") {
157+
return prebuilds;
158+
}
159+
return "all";
160+
};

components/dashboard/src/repositories/list/RepositoryTable.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { cn } from "@podkit/lib/cn";
1616
import { SortableTableHead, TableSortOrder } from "@podkit/tables/SortableTable";
1717
import { LoadingState } from "@podkit/loading/LoadingState";
1818
import { Button } from "@podkit/buttons/Button";
19+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@podkit/select/Select";
1920

2021
type Props = {
2122
configurations: Configuration[];
2223
searchTerm: string;
24+
prebuildsFilter: string;
2325
sortBy: string;
2426
sortOrder: "asc" | "desc";
2527
hasNextPage: boolean;
@@ -28,12 +30,14 @@ type Props = {
2830
isFetchingNextPage: boolean;
2931
onImport: () => void;
3032
onSearchTermChange: (val: string) => void;
33+
onPrebuildsFilterChange: (val: "all" | "enabled" | "disabled") => void;
3134
onLoadNextPage: () => void;
3235
onSort: (columnName: string, direction: TableSortOrder) => void;
3336
};
3437

3538
export const RepositoryTable: FC<Props> = ({
3639
searchTerm,
40+
prebuildsFilter,
3741
configurations,
3842
sortOrder,
3943
sortBy,
@@ -43,22 +47,32 @@ export const RepositoryTable: FC<Props> = ({
4347
isFetchingNextPage,
4448
onImport,
4549
onSearchTermChange,
50+
onPrebuildsFilterChange,
4651
onLoadNextPage,
4752
onSort,
4853
}) => {
4954
return (
5055
<>
5156
{/* Search/Filter bar */}
5257
<div className="flex flex-col-reverse md:flex-row flex-wrap justify-between items-center gap-2">
53-
<div className="flex flex-row flex-wrap items-center w-full md:w-auto">
58+
<div className="flex flex-row flex-wrap gap-2 items-center w-full md:w-auto">
5459
{/* TODO: Add search icon on left - need to revisit TextInputs for podkit - and remove global styles */}
5560
<TextInput
5661
className="w-full max-w-none md:w-80"
5762
value={searchTerm}
5863
onChange={onSearchTermChange}
5964
placeholder="Search imported repositories"
6065
/>
61-
{/* TODO: Add prebuild status filter dropdown */}
66+
<Select value={prebuildsFilter} onValueChange={onPrebuildsFilterChange}>
67+
<SelectTrigger className="w-[180px]">
68+
<SelectValue placeholder="Prebuilds: All" />
69+
</SelectTrigger>
70+
<SelectContent>
71+
<SelectItem value="all">Prebuilds: All</SelectItem>
72+
<SelectItem value="enabled">Prebuilds: Enabled</SelectItem>
73+
<SelectItem value="disabled">Prebuilds: Disabled</SelectItem>
74+
</SelectContent>
75+
</Select>
6276
</div>
6377

6478
{/* TODO: Consider making all podkit buttons behave this way, full width on small screen */}

0 commit comments

Comments
 (0)