|
4 | 4 | * See License.AGPL.txt in the project root for license information.
|
5 | 5 | */
|
6 | 6 |
|
7 |
| -import { FC, useCallback, useState } from "react"; |
8 |
| -import { LoaderIcon } from "lucide-react"; |
| 7 | +import { FC, useCallback, useEffect, useMemo, useState } from "react"; |
9 | 8 | import { useHistory } from "react-router-dom";
|
10 |
| -import { RepositoryListItem } from "./RepoListItem"; |
11 | 9 | import { useListConfigurations } from "../../data/configurations/configuration-queries";
|
12 |
| -import { TextInput } from "../../components/forms/TextInputField"; |
13 | 10 | import { PageHeading } from "@podkit/layout/PageHeading";
|
14 | 11 | import { Button } from "@podkit/buttons/Button";
|
15 | 12 | import { useDocumentTitle } from "../../hooks/use-document-title";
|
16 |
| -import { Table, TableBody, TableHead, TableHeader, TableRow } from "@podkit/tables/Table"; |
17 | 13 | import { ImportRepositoryModal } from "../create/ImportRepositoryModal";
|
18 | 14 | import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
|
19 |
| -import { LoadingButton } from "@podkit/buttons/LoadingButton"; |
20 | 15 | import { useQueryParams } from "../../hooks/use-query-params";
|
| 16 | +import { RepoListEmptyState } from "./RepoListEmptyState"; |
| 17 | +import { useStateWithDebounce } from "../../hooks/use-state-with-debounce"; |
| 18 | +import { RepositoryTable } from "./RepositoryTable"; |
| 19 | +import { LoadingState } from "@podkit/loading/LoadingState"; |
21 | 20 |
|
22 | 21 | const RepositoryListPage: FC = () => {
|
23 | 22 | useDocumentTitle("Imported repositories");
|
24 | 23 |
|
25 | 24 | const history = useHistory();
|
26 | 25 |
|
27 |
| - // Search/Filter params tracked in url query params |
28 | 26 | const params = useQueryParams();
|
29 |
| - const searchTerm = params.get("search") || ""; |
30 |
| - const updateSearchTerm = useCallback( |
31 |
| - (val: string) => { |
32 |
| - history.replace({ search: `?search=${encodeURIComponent(val)}` }); |
33 |
| - }, |
34 |
| - [history], |
35 |
| - ); |
36 |
| - |
37 |
| - const { data, isFetching, isFetchingNextPage, isPreviousData, hasNextPage, fetchNextPage } = useListConfigurations({ |
38 |
| - searchTerm, |
39 |
| - }); |
| 27 | + const [searchTerm, setSearchTerm, searchTermDebounced] = useStateWithDebounce(params.get("search") || ""); |
40 | 28 | const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);
|
41 | 29 |
|
| 30 | + // Search/Filter params tracked in url query params |
| 31 | + useEffect(() => { |
| 32 | + const params = searchTermDebounced ? `?search=${encodeURIComponent(searchTermDebounced)}` : ""; |
| 33 | + history.replace({ search: params }); |
| 34 | + }, [history, searchTermDebounced]); |
| 35 | + |
| 36 | + const { data, isLoading, isFetching, isFetchingNextPage, isPreviousData, hasNextPage, fetchNextPage } = |
| 37 | + useListConfigurations({ |
| 38 | + searchTerm: searchTermDebounced, |
| 39 | + }); |
| 40 | + |
42 | 41 | const handleRepoImported = useCallback(
|
43 | 42 | (configuration: Configuration) => {
|
44 | 43 | history.push(`/repositories/${configuration.id}`);
|
45 | 44 | },
|
46 | 45 | [history],
|
47 | 46 | );
|
48 | 47 |
|
| 48 | + const configurations = useMemo(() => { |
| 49 | + return data?.pages.map((page) => page.configurations).flat() ?? []; |
| 50 | + }, [data?.pages]); |
| 51 | + |
| 52 | + const hasMoreThanOnePage = (data?.pages.length ?? 0) > 1; |
| 53 | + |
| 54 | + // This tracks any filters/search params applied |
| 55 | + const hasFilters = !!searchTermDebounced; |
| 56 | + |
| 57 | + // Show the table once we're done loading and either have results, or have filters applied |
| 58 | + const showTable = !isLoading && (configurations.length > 0 || hasFilters); |
| 59 | + |
49 | 60 | return (
|
50 | 61 | <>
|
51 | 62 | <div className="app-container mb-8">
|
52 | 63 | <PageHeading
|
53 | 64 | title="Imported repositories"
|
54 | 65 | subtitle="Configure and refine the experience of working with a repository in Gitpod"
|
55 |
| - action={<Button onClick={() => setShowCreateProjectModal(true)}>Import Repository</Button>} |
| 66 | + action={ |
| 67 | + showTable && <Button onClick={() => setShowCreateProjectModal(true)}>Import Repository</Button> |
| 68 | + } |
56 | 69 | />
|
57 | 70 |
|
58 |
| - {/* Search/Filter bar */} |
59 |
| - <div className="flex flex-row flex-wrap justify-between items-center"> |
60 |
| - <div className="flex flex-row flex-wrap gap-2 items-center"> |
61 |
| - {/* TODO: Add search icon on left and decide on pulling Inputs into podkit */} |
62 |
| - <TextInput |
63 |
| - className="w-80" |
64 |
| - value={searchTerm} |
65 |
| - onChange={updateSearchTerm} |
66 |
| - placeholder="Search imported repositories" |
67 |
| - /> |
68 |
| - {/* TODO: Add prebuild status filter dropdown */} |
69 |
| - </div> |
70 |
| - {/* Account for variation of message when totalRows is greater than smallest page size option (20?) */} |
71 |
| - </div> |
72 |
| - |
73 |
| - <div className="relative w-full overflow-auto mt-2"> |
74 |
| - <Table> |
75 |
| - {/* TODO: Add sorting controls */} |
76 |
| - <TableHeader> |
77 |
| - <TableRow> |
78 |
| - <TableHead className="w-52">Name</TableHead> |
79 |
| - <TableHead hideOnSmallScreen>Repository</TableHead> |
80 |
| - <TableHead className="w-32" hideOnSmallScreen> |
81 |
| - Created |
82 |
| - </TableHead> |
83 |
| - <TableHead className="w-24" hideOnSmallScreen> |
84 |
| - Prebuilds |
85 |
| - </TableHead> |
86 |
| - {/* Action column, loading status in header */} |
87 |
| - <TableHead className="w-24 text-right"> |
88 |
| - {isFetching && isPreviousData && ( |
89 |
| - <div className="flex flex-right justify-end items-center"> |
90 |
| - {/* TODO: Make a LoadingIcon component */} |
91 |
| - <LoaderIcon |
92 |
| - className="animate-spin text-gray-500 dark:text-gray-300" |
93 |
| - size={20} |
94 |
| - /> |
95 |
| - </div> |
96 |
| - )} |
97 |
| - </TableHead> |
98 |
| - </TableRow> |
99 |
| - </TableHeader> |
100 |
| - <TableBody> |
101 |
| - {data?.pages.map((page) => { |
102 |
| - return page.configurations.map((configuration) => { |
103 |
| - return <RepositoryListItem key={configuration.id} configuration={configuration} />; |
104 |
| - }); |
105 |
| - })} |
106 |
| - </TableBody> |
107 |
| - </Table> |
108 |
| - |
109 |
| - {hasNextPage && ( |
110 |
| - <div className="my-4 flex flex-row justify-center"> |
111 |
| - <LoadingButton |
112 |
| - variant="secondary" |
113 |
| - onClick={() => fetchNextPage()} |
114 |
| - loading={isFetchingNextPage} |
115 |
| - > |
116 |
| - Load more |
117 |
| - </LoadingButton> |
118 |
| - </div> |
119 |
| - )} |
120 |
| - </div> |
| 71 | + {isLoading && <LoadingState />} |
| 72 | + |
| 73 | + {showTable && ( |
| 74 | + <RepositoryTable |
| 75 | + searchTerm={searchTerm} |
| 76 | + configurations={configurations} |
| 77 | + // we check isPreviousData too so we don't show spinner if it's a background refresh |
| 78 | + isSearching={isFetching && isPreviousData} |
| 79 | + isFetchingNextPage={isFetchingNextPage} |
| 80 | + hasNextPage={!!hasNextPage} |
| 81 | + hasMoreThanOnePage={hasMoreThanOnePage} |
| 82 | + onLoadNextPage={() => fetchNextPage()} |
| 83 | + onSearchTermChange={setSearchTerm} |
| 84 | + /> |
| 85 | + )} |
| 86 | + |
| 87 | + {!showTable && !isLoading && <RepoListEmptyState onImport={() => setShowCreateProjectModal(true)} />} |
121 | 88 | </div>
|
122 | 89 |
|
123 | 90 | {showCreateProjectModal && (
|
|
0 commit comments