Skip to content

Commit b9189bf

Browse files
Imported repos list empty state updates (#19096)
* Adding an empty state and loading * updating no results ux * Update components/dashboard/src/repositories/list/RepoListEmptyState.tsx Co-authored-by: Filip Troníček <[email protected]> * updating header colors for better contrast in dark mode --------- Co-authored-by: Filip Troníček <[email protected]>
1 parent 4ac04ee commit b9189bf

File tree

6 files changed

+191
-95
lines changed

6 files changed

+191
-95
lines changed

components/dashboard/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useLocation } from "react-router";
88
import { useDocumentTitle } from "../hooks/use-document-title";
99
import { Separator } from "./Separator";
1010
import TabMenuItem from "./TabMenuItem";
11-
import { Heading1, Subheading } from "./typography/headings";
11+
import { Heading1, Subheading } from "@podkit/typography/Headings";
1212

1313
export interface HeaderProps {
1414
title: string;

components/dashboard/src/components/podkit/typography/Headings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const Heading3: FC<HeadingProps> = ({ id, color, tracking, className, chi
5757
*/
5858
export const Subheading: FC<HeadingProps> = ({ id, tracking, className, children }) => {
5959
return (
60-
<p id={id} className={cn("text-base text-gray-500 dark:text-gray-500", getTracking(tracking), className)}>
60+
<p id={id} className={cn("text-base text-gray-500 dark:text-gray-400", getTracking(tracking), className)}>
6161
{children}
6262
</p>
6363
);

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tansta
88
import { useCurrentOrg } from "../organizations/orgs-query";
99
import { configurationClient } from "../../service/public-api";
1010
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
11-
import { useStateWithDebounce } from "../../hooks/use-state-with-debounce";
12-
import { useEffect } from "react";
1311

1412
const BASE_KEY = "configurations";
1513

@@ -21,15 +19,8 @@ type ListConfigurationsArgs = {
2119
export const useListConfigurations = ({ searchTerm = "", pageSize }: ListConfigurationsArgs) => {
2220
const { data: org } = useCurrentOrg();
2321

24-
// Debounce searchTerm for query
25-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26-
const [_, setSearchTerm, debouncedSearchTerm] = useStateWithDebounce(searchTerm);
27-
useEffect(() => {
28-
setSearchTerm(searchTerm);
29-
}, [searchTerm, setSearchTerm]);
30-
3122
return useInfiniteQuery(
32-
getListConfigurationsQueryKey(org?.id || "", { searchTerm: debouncedSearchTerm, pageSize }),
23+
getListConfigurationsQueryKey(org?.id || "", { searchTerm, pageSize }),
3324
// QueryFn receives the past page's pageParam as it's argument
3425
async ({ pageParam: nextToken }) => {
3526
if (!org) {
@@ -38,7 +29,7 @@ export const useListConfigurations = ({ searchTerm = "", pageSize }: ListConfigu
3829

3930
const { configurations, pagination } = await configurationClient.listConfigurations({
4031
organizationId: org.id,
41-
searchTerm: debouncedSearchTerm,
32+
searchTerm,
4233
pagination: { pageSize, token: nextToken },
4334
});
4435

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 { FC } from "react";
8+
import { Heading2, Subheading } from "@podkit/typography/Headings";
9+
import { Button } from "@podkit/buttons/Button";
10+
import { cn } from "@podkit/lib/cn";
11+
12+
type Props = {
13+
onImport: () => void;
14+
};
15+
export const RepoListEmptyState: FC<Props> = ({ onImport }) => {
16+
return (
17+
<div className={cn("w-full flex justify-center mt-2 rounded-xl bg-gray-100 dark:bg-gray-800 px-4 py-20")}>
18+
<div className="flex flex-col justify-center items-center text-center space-y-4">
19+
<Heading2>No imported repositories yet</Heading2>
20+
<Subheading className="max-w-md">
21+
Importing and configuring repositories allows your team members to be coding at the click of a
22+
button.
23+
</Subheading>
24+
<Button onClick={onImport}>Import a Repository</Button>
25+
</div>
26+
</div>
27+
);
28+
};

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

Lines changed: 49 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,120 +4,87 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { FC, useCallback, useState } from "react";
8-
import { LoaderIcon } from "lucide-react";
7+
import { FC, useCallback, useEffect, useMemo, useState } from "react";
98
import { useHistory } from "react-router-dom";
10-
import { RepositoryListItem } from "./RepoListItem";
119
import { useListConfigurations } from "../../data/configurations/configuration-queries";
12-
import { TextInput } from "../../components/forms/TextInputField";
1310
import { PageHeading } from "@podkit/layout/PageHeading";
1411
import { Button } from "@podkit/buttons/Button";
1512
import { useDocumentTitle } from "../../hooks/use-document-title";
16-
import { Table, TableBody, TableHead, TableHeader, TableRow } from "@podkit/tables/Table";
1713
import { ImportRepositoryModal } from "../create/ImportRepositoryModal";
1814
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
19-
import { LoadingButton } from "@podkit/buttons/LoadingButton";
2015
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";
2120

2221
const RepositoryListPage: FC = () => {
2322
useDocumentTitle("Imported repositories");
2423

2524
const history = useHistory();
2625

27-
// Search/Filter params tracked in url query params
2826
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") || "");
4028
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);
4129

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+
4241
const handleRepoImported = useCallback(
4342
(configuration: Configuration) => {
4443
history.push(`/repositories/${configuration.id}`);
4544
},
4645
[history],
4746
);
4847

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+
4960
return (
5061
<>
5162
<div className="app-container mb-8">
5263
<PageHeading
5364
title="Imported repositories"
5465
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+
}
5669
/>
5770

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)} />}
12188
</div>
12289

12390
{showCreateProjectModal && (
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 { FC } from "react";
8+
import { TextInput } from "../../components/forms/TextInputField";
9+
import { Table, TableBody, TableHead, TableHeader, TableRow } from "@podkit/tables/Table";
10+
import { LoaderIcon } from "lucide-react";
11+
import { RepositoryListItem } from "./RepoListItem";
12+
import { LoadingButton } from "@podkit/buttons/LoadingButton";
13+
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
14+
import { TextMuted } from "@podkit/typography/TextMuted";
15+
import { Subheading } from "@podkit/typography/Headings";
16+
import { cn } from "@podkit/lib/cn";
17+
18+
type Props = {
19+
configurations: Configuration[];
20+
searchTerm: string;
21+
hasNextPage: boolean;
22+
hasMoreThanOnePage: boolean;
23+
isSearching: boolean;
24+
isFetchingNextPage: boolean;
25+
onSearchTermChange: (val: string) => void;
26+
onLoadNextPage: () => void;
27+
};
28+
29+
export const RepositoryTable: FC<Props> = ({
30+
searchTerm,
31+
configurations,
32+
hasNextPage,
33+
hasMoreThanOnePage,
34+
isSearching,
35+
isFetchingNextPage,
36+
onSearchTermChange,
37+
onLoadNextPage,
38+
}) => {
39+
return (
40+
<>
41+
{/* Search/Filter bar */}
42+
<div className="flex flex-row flex-wrap justify-between items-center">
43+
<div className="flex flex-row flex-wrap gap-2 items-center">
44+
{/* TODO: Add search icon on left and decide on pulling Inputs into podkit */}
45+
<TextInput
46+
className="w-80"
47+
value={searchTerm}
48+
onChange={onSearchTermChange}
49+
placeholder="Search imported repositories"
50+
/>
51+
{/* TODO: Add prebuild status filter dropdown */}
52+
</div>
53+
</div>
54+
<div className="relative w-full overflow-auto mt-2">
55+
{configurations.length > 0 ? (
56+
<Table>
57+
{/* TODO: Add sorting controls */}
58+
<TableHeader>
59+
<TableRow>
60+
<TableHead className="w-52">Name</TableHead>
61+
<TableHead hideOnSmallScreen>Repository</TableHead>
62+
<TableHead className="w-32" hideOnSmallScreen>
63+
Created
64+
</TableHead>
65+
<TableHead className="w-24" hideOnSmallScreen>
66+
Prebuilds
67+
</TableHead>
68+
{/* Action column, loading status in header */}
69+
<TableHead className="w-24 text-right">
70+
{isSearching && (
71+
<div className="flex flex-right justify-end items-center">
72+
{/* TODO: Make a LoadingIcon component */}
73+
<LoaderIcon
74+
className="animate-spin text-gray-500 dark:text-gray-300"
75+
size={20}
76+
/>
77+
</div>
78+
)}
79+
</TableHead>
80+
</TableRow>
81+
</TableHeader>
82+
<TableBody>
83+
{configurations.map((configuration) => {
84+
return <RepositoryListItem key={configuration.id} configuration={configuration} />;
85+
})}
86+
</TableBody>
87+
</Table>
88+
) : (
89+
<div
90+
className={cn(
91+
"w-full flex justify-center rounded-xl bg-gray-100 dark:bg-gray-800 px-4 py-10 animate-fade-in-fast",
92+
)}
93+
>
94+
<Subheading className="max-w-md">No results found. Try adjusting your search terms.</Subheading>
95+
</div>
96+
)}
97+
98+
<div className="mt-4 mb-8 flex flex-row justify-center">
99+
{hasNextPage ? (
100+
<LoadingButton variant="secondary" onClick={onLoadNextPage} loading={isFetchingNextPage}>
101+
Load more
102+
</LoadingButton>
103+
) : (
104+
hasMoreThanOnePage && <TextMuted>All repositories are loaded</TextMuted>
105+
)}
106+
</div>
107+
</div>
108+
</>
109+
);
110+
};

0 commit comments

Comments
 (0)