diff --git a/components/dashboard/src/components/Header.tsx b/components/dashboard/src/components/Header.tsx index af2838e1cfd9e6..026463d3d721d2 100644 --- a/components/dashboard/src/components/Header.tsx +++ b/components/dashboard/src/components/Header.tsx @@ -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; diff --git a/components/dashboard/src/components/podkit/typography/Headings.tsx b/components/dashboard/src/components/podkit/typography/Headings.tsx index 97922be5388ea0..81599cf8a768e2 100644 --- a/components/dashboard/src/components/podkit/typography/Headings.tsx +++ b/components/dashboard/src/components/podkit/typography/Headings.tsx @@ -57,7 +57,7 @@ export const Heading3: FC = ({ id, color, tracking, className, chi */ export const Subheading: FC = ({ id, tracking, className, children }) => { return ( -

+

{children}

); diff --git a/components/dashboard/src/data/configurations/configuration-queries.ts b/components/dashboard/src/data/configurations/configuration-queries.ts index 96c0c9014ef24c..143cc843c43780 100644 --- a/components/dashboard/src/data/configurations/configuration-queries.ts +++ b/components/dashboard/src/data/configurations/configuration-queries.ts @@ -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"; @@ -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) { @@ -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 }, }); diff --git a/components/dashboard/src/repositories/list/RepoListEmptyState.tsx b/components/dashboard/src/repositories/list/RepoListEmptyState.tsx new file mode 100644 index 00000000000000..fd3a66275b2b5e --- /dev/null +++ b/components/dashboard/src/repositories/list/RepoListEmptyState.tsx @@ -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 = ({ onImport }) => { + return ( +
+
+ No imported repositories yet + + Importing and configuring repositories allows your team members to be coding at the click of a + button. + + +
+
+ ); +}; diff --git a/components/dashboard/src/repositories/list/RepositoryList.tsx b/components/dashboard/src/repositories/list/RepositoryList.tsx index 773e27e04db08d..936af98bf492df 100644 --- a/components/dashboard/src/repositories/list/RepositoryList.tsx +++ b/components/dashboard/src/repositories/list/RepositoryList.tsx @@ -4,41 +4,40 @@ * 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}`); @@ -46,78 +45,46 @@ const RepositoryListPage: FC = () => { [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 ( <>
setShowCreateProjectModal(true)}>Import Repository} + action={ + showTable && + } /> - {/* Search/Filter bar */} -
-
- {/* TODO: Add search icon on left and decide on pulling Inputs into podkit */} - - {/* TODO: Add prebuild status filter dropdown */} -
- {/* Account for variation of message when totalRows is greater than smallest page size option (20?) */} -
- -
- - {/* TODO: Add sorting controls */} - - - Name - Repository - - Created - - - Prebuilds - - {/* Action column, loading status in header */} - - {isFetching && isPreviousData && ( -
- {/* TODO: Make a LoadingIcon component */} - -
- )} -
-
-
- - {data?.pages.map((page) => { - return page.configurations.map((configuration) => { - return ; - }); - })} - -
- - {hasNextPage && ( -
- fetchNextPage()} - loading={isFetchingNextPage} - > - Load more - -
- )} -
+ {isLoading && } + + {showTable && ( + fetchNextPage()} + onSearchTermChange={setSearchTerm} + /> + )} + + {!showTable && !isLoading && setShowCreateProjectModal(true)} />}
{showCreateProjectModal && ( diff --git a/components/dashboard/src/repositories/list/RepositoryTable.tsx b/components/dashboard/src/repositories/list/RepositoryTable.tsx new file mode 100644 index 00000000000000..fa6b3ed110928f --- /dev/null +++ b/components/dashboard/src/repositories/list/RepositoryTable.tsx @@ -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 = ({ + searchTerm, + configurations, + hasNextPage, + hasMoreThanOnePage, + isSearching, + isFetchingNextPage, + onSearchTermChange, + onLoadNextPage, +}) => { + return ( + <> + {/* Search/Filter bar */} +
+
+ {/* TODO: Add search icon on left and decide on pulling Inputs into podkit */} + + {/* TODO: Add prebuild status filter dropdown */} +
+
+
+ {configurations.length > 0 ? ( + + {/* TODO: Add sorting controls */} + + + Name + Repository + + Created + + + Prebuilds + + {/* Action column, loading status in header */} + + {isSearching && ( +
+ {/* TODO: Make a LoadingIcon component */} + +
+ )} +
+
+
+ + {configurations.map((configuration) => { + return ; + })} + +
+ ) : ( +
+ No results found. Try adjusting your search terms. +
+ )} + +
+ {hasNextPage ? ( + + Load more + + ) : ( + hasMoreThanOnePage && All repositories are loaded + )} +
+
+ + ); +};