Skip to content

Commit

Permalink
Imported repos list empty state updates (#19096)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
selfcontained and filiptronicek authored Nov 20, 2023
1 parent 4ac04ee commit b9189bf
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 95 deletions.
2 changes: 1 addition & 1 deletion components/dashboard/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useLocation } from "react-router";
import { useDocumentTitle } from "../hooks/use-document-title";
import { Separator } from "./Separator";
import TabMenuItem from "./TabMenuItem";
import { Heading1, Subheading } from "./typography/headings";
import { Heading1, Subheading } from "@podkit/typography/Headings";

export interface HeaderProps {
title: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const Heading3: FC<HeadingProps> = ({ id, color, tracking, className, chi
*/
export const Subheading: FC<HeadingProps> = ({ id, tracking, className, children }) => {
return (
<p id={id} className={cn("text-base text-gray-500 dark:text-gray-500", getTracking(tracking), className)}>
<p id={id} className={cn("text-base text-gray-500 dark:text-gray-400", getTracking(tracking), className)}>
{children}
</p>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tansta
import { useCurrentOrg } from "../organizations/orgs-query";
import { configurationClient } from "../../service/public-api";
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { useStateWithDebounce } from "../../hooks/use-state-with-debounce";
import { useEffect } from "react";

const BASE_KEY = "configurations";

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

// Debounce searchTerm for query
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchTerm, debouncedSearchTerm] = useStateWithDebounce(searchTerm);
useEffect(() => {
setSearchTerm(searchTerm);
}, [searchTerm, setSearchTerm]);

return useInfiniteQuery(
getListConfigurationsQueryKey(org?.id || "", { searchTerm: debouncedSearchTerm, pageSize }),
getListConfigurationsQueryKey(org?.id || "", { searchTerm, pageSize }),
// QueryFn receives the past page's pageParam as it's argument
async ({ pageParam: nextToken }) => {
if (!org) {
Expand All @@ -38,7 +29,7 @@ export const useListConfigurations = ({ searchTerm = "", pageSize }: ListConfigu

const { configurations, pagination } = await configurationClient.listConfigurations({
organizationId: org.id,
searchTerm: debouncedSearchTerm,
searchTerm,
pagination: { pageSize, token: nextToken },
});

Expand Down
28 changes: 28 additions & 0 deletions components/dashboard/src/repositories/list/RepoListEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { FC } from "react";
import { Heading2, Subheading } from "@podkit/typography/Headings";
import { Button } from "@podkit/buttons/Button";
import { cn } from "@podkit/lib/cn";

type Props = {
onImport: () => void;
};
export const RepoListEmptyState: FC<Props> = ({ onImport }) => {
return (
<div className={cn("w-full flex justify-center mt-2 rounded-xl bg-gray-100 dark:bg-gray-800 px-4 py-20")}>
<div className="flex flex-col justify-center items-center text-center space-y-4">
<Heading2>No imported repositories yet</Heading2>
<Subheading className="max-w-md">
Importing and configuring repositories allows your team members to be coding at the click of a
button.
</Subheading>
<Button onClick={onImport}>Import a Repository</Button>
</div>
</div>
);
};
131 changes: 49 additions & 82 deletions components/dashboard/src/repositories/list/RepositoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,120 +4,87 @@
* See License.AGPL.txt in the project root for license information.
*/

import { FC, useCallback, useState } from "react";
import { LoaderIcon } from "lucide-react";
import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { useHistory } from "react-router-dom";
import { RepositoryListItem } from "./RepoListItem";
import { useListConfigurations } from "../../data/configurations/configuration-queries";
import { TextInput } from "../../components/forms/TextInputField";
import { PageHeading } from "@podkit/layout/PageHeading";
import { Button } from "@podkit/buttons/Button";
import { useDocumentTitle } from "../../hooks/use-document-title";
import { Table, TableBody, TableHead, TableHeader, TableRow } from "@podkit/tables/Table";
import { ImportRepositoryModal } from "../create/ImportRepositoryModal";
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { LoadingButton } from "@podkit/buttons/LoadingButton";
import { useQueryParams } from "../../hooks/use-query-params";
import { RepoListEmptyState } from "./RepoListEmptyState";
import { useStateWithDebounce } from "../../hooks/use-state-with-debounce";
import { RepositoryTable } from "./RepositoryTable";
import { LoadingState } from "@podkit/loading/LoadingState";

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

const history = useHistory();

// Search/Filter params tracked in url query params
const params = useQueryParams();
const searchTerm = params.get("search") || "";
const updateSearchTerm = useCallback(
(val: string) => {
history.replace({ search: `?search=${encodeURIComponent(val)}` });
},
[history],
);

const { data, isFetching, isFetchingNextPage, isPreviousData, hasNextPage, fetchNextPage } = useListConfigurations({
searchTerm,
});
const [searchTerm, setSearchTerm, searchTermDebounced] = useStateWithDebounce(params.get("search") || "");
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);

// Search/Filter params tracked in url query params
useEffect(() => {
const params = searchTermDebounced ? `?search=${encodeURIComponent(searchTermDebounced)}` : "";
history.replace({ search: params });
}, [history, searchTermDebounced]);

const { data, isLoading, isFetching, isFetchingNextPage, isPreviousData, hasNextPage, fetchNextPage } =
useListConfigurations({
searchTerm: searchTermDebounced,
});

const handleRepoImported = useCallback(
(configuration: Configuration) => {
history.push(`/repositories/${configuration.id}`);
},
[history],
);

const configurations = useMemo(() => {
return data?.pages.map((page) => page.configurations).flat() ?? [];
}, [data?.pages]);

const hasMoreThanOnePage = (data?.pages.length ?? 0) > 1;

// This tracks any filters/search params applied
const hasFilters = !!searchTermDebounced;

// Show the table once we're done loading and either have results, or have filters applied
const showTable = !isLoading && (configurations.length > 0 || hasFilters);

return (
<>
<div className="app-container mb-8">
<PageHeading
title="Imported repositories"
subtitle="Configure and refine the experience of working with a repository in Gitpod"
action={<Button onClick={() => setShowCreateProjectModal(true)}>Import Repository</Button>}
action={
showTable && <Button onClick={() => setShowCreateProjectModal(true)}>Import Repository</Button>
}
/>

{/* Search/Filter bar */}
<div className="flex flex-row flex-wrap justify-between items-center">
<div className="flex flex-row flex-wrap gap-2 items-center">
{/* TODO: Add search icon on left and decide on pulling Inputs into podkit */}
<TextInput
className="w-80"
value={searchTerm}
onChange={updateSearchTerm}
placeholder="Search imported repositories"
/>
{/* TODO: Add prebuild status filter dropdown */}
</div>
{/* Account for variation of message when totalRows is greater than smallest page size option (20?) */}
</div>

<div className="relative w-full overflow-auto mt-2">
<Table>
{/* TODO: Add sorting controls */}
<TableHeader>
<TableRow>
<TableHead className="w-52">Name</TableHead>
<TableHead hideOnSmallScreen>Repository</TableHead>
<TableHead className="w-32" hideOnSmallScreen>
Created
</TableHead>
<TableHead className="w-24" hideOnSmallScreen>
Prebuilds
</TableHead>
{/* Action column, loading status in header */}
<TableHead className="w-24 text-right">
{isFetching && isPreviousData && (
<div className="flex flex-right justify-end items-center">
{/* TODO: Make a LoadingIcon component */}
<LoaderIcon
className="animate-spin text-gray-500 dark:text-gray-300"
size={20}
/>
</div>
)}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.pages.map((page) => {
return page.configurations.map((configuration) => {
return <RepositoryListItem key={configuration.id} configuration={configuration} />;
});
})}
</TableBody>
</Table>

{hasNextPage && (
<div className="my-4 flex flex-row justify-center">
<LoadingButton
variant="secondary"
onClick={() => fetchNextPage()}
loading={isFetchingNextPage}
>
Load more
</LoadingButton>
</div>
)}
</div>
{isLoading && <LoadingState />}

{showTable && (
<RepositoryTable
searchTerm={searchTerm}
configurations={configurations}
// we check isPreviousData too so we don't show spinner if it's a background refresh
isSearching={isFetching && isPreviousData}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={!!hasNextPage}
hasMoreThanOnePage={hasMoreThanOnePage}
onLoadNextPage={() => fetchNextPage()}
onSearchTermChange={setSearchTerm}
/>
)}

{!showTable && !isLoading && <RepoListEmptyState onImport={() => setShowCreateProjectModal(true)} />}
</div>

{showCreateProjectModal && (
Expand Down
110 changes: 110 additions & 0 deletions components/dashboard/src/repositories/list/RepositoryTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { FC } from "react";
import { TextInput } from "../../components/forms/TextInputField";
import { Table, TableBody, TableHead, TableHeader, TableRow } from "@podkit/tables/Table";
import { LoaderIcon } from "lucide-react";
import { RepositoryListItem } from "./RepoListItem";
import { LoadingButton } from "@podkit/buttons/LoadingButton";
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { TextMuted } from "@podkit/typography/TextMuted";
import { Subheading } from "@podkit/typography/Headings";
import { cn } from "@podkit/lib/cn";

type Props = {
configurations: Configuration[];
searchTerm: string;
hasNextPage: boolean;
hasMoreThanOnePage: boolean;
isSearching: boolean;
isFetchingNextPage: boolean;
onSearchTermChange: (val: string) => void;
onLoadNextPage: () => void;
};

export const RepositoryTable: FC<Props> = ({
searchTerm,
configurations,
hasNextPage,
hasMoreThanOnePage,
isSearching,
isFetchingNextPage,
onSearchTermChange,
onLoadNextPage,
}) => {
return (
<>
{/* Search/Filter bar */}
<div className="flex flex-row flex-wrap justify-between items-center">
<div className="flex flex-row flex-wrap gap-2 items-center">
{/* TODO: Add search icon on left and decide on pulling Inputs into podkit */}
<TextInput
className="w-80"
value={searchTerm}
onChange={onSearchTermChange}
placeholder="Search imported repositories"
/>
{/* TODO: Add prebuild status filter dropdown */}
</div>
</div>
<div className="relative w-full overflow-auto mt-2">
{configurations.length > 0 ? (
<Table>
{/* TODO: Add sorting controls */}
<TableHeader>
<TableRow>
<TableHead className="w-52">Name</TableHead>
<TableHead hideOnSmallScreen>Repository</TableHead>
<TableHead className="w-32" hideOnSmallScreen>
Created
</TableHead>
<TableHead className="w-24" hideOnSmallScreen>
Prebuilds
</TableHead>
{/* Action column, loading status in header */}
<TableHead className="w-24 text-right">
{isSearching && (
<div className="flex flex-right justify-end items-center">
{/* TODO: Make a LoadingIcon component */}
<LoaderIcon
className="animate-spin text-gray-500 dark:text-gray-300"
size={20}
/>
</div>
)}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{configurations.map((configuration) => {
return <RepositoryListItem key={configuration.id} configuration={configuration} />;
})}
</TableBody>
</Table>
) : (
<div
className={cn(
"w-full flex justify-center rounded-xl bg-gray-100 dark:bg-gray-800 px-4 py-10 animate-fade-in-fast",
)}
>
<Subheading className="max-w-md">No results found. Try adjusting your search terms.</Subheading>
</div>
)}

<div className="mt-4 mb-8 flex flex-row justify-center">
{hasNextPage ? (
<LoadingButton variant="secondary" onClick={onLoadNextPage} loading={isFetchingNextPage}>
Load more
</LoadingButton>
) : (
hasMoreThanOnePage && <TextMuted>All repositories are loaded</TextMuted>
)}
</div>
</div>
</>
);
};

0 comments on commit b9189bf

Please sign in to comment.