Skip to content

Imported repos list empty state updates #19096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we change the color in dark mode for more contrast?
image

Copy link
Member

@filiptronicek filiptronicek Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It applies to this as well πŸ™

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think I'm going to look at swapping all our headings to the podkit ones to at least get that stuff cleaned up and using the updated components.

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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get really cheesy here one day πŸ˜„

)}
</div>
</div>
</>
);
};