Skip to content

Commit 436375f

Browse files
Setup data loading for repos list (#18935)
* rename for clarity * renaming for clarity * loading/listing first page of projects * Rendering list of configured repositories
1 parent 36071e6 commit 436375f

19 files changed

+256
-54
lines changed

components/dashboard/craco.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66
const { when } = require("@craco/craco");
7+
const path = require("path");
78
const webpack = require("webpack");
89

910
module.exports = {
@@ -18,6 +19,9 @@ module.exports = {
1819
webpack: {
1920
configure: {
2021
resolve: {
22+
alias: {
23+
"@podkit": path.resolve(__dirname, "./src/components/podkit/"),
24+
},
2125
fallback: {
2226
crypto: require.resolve("crypto-browserify"),
2327
stream: require.resolve("stream-browserify"),

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"react-router-dom": "^5.2.0",
4545
"setimmediate": "^1.0.5",
4646
"stream-browserify": "^2.0.1",
47+
"tailwind-merge": "^1.14.0",
4748
"url": "^0.11.1",
4849
"util": "^0.11.1",
4950
"validator": "^13.9.0",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
9+
type Props = {
10+
className?: string;
11+
children: string;
12+
};
13+
export const RepositoryURL: FC<Props> = ({ className, children }) => {
14+
const cleanURL = children.endsWith(".git") ? children.slice(0, -4) : children;
15+
16+
return <span className={className}>{cleanURL}</span>;
17+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 classNames, { Argument } from "classnames";
8+
import { twMerge } from "tailwind-merge";
9+
10+
// Helper type to add a className prop to a component
11+
export type PropsWithClassName<Props = {}> = {
12+
className?: string;
13+
} & Props;
14+
15+
// Helper fn to merge tailwind classes with a className prop
16+
export function cn(...inputs: Argument[]) {
17+
return twMerge(classNames(inputs));
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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, PropsWithChildren } from "react";
8+
import { PropsWithClassName, cn } from "@podkit/lib/cn";
9+
10+
type TextProps = PropsWithChildren<PropsWithClassName>;
11+
12+
export const Text: FC<TextProps> = ({ className, children }) => {
13+
return <span className={cn("text-black dark:text-white", className)}>{children}</span>;
14+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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, PropsWithChildren } from "react";
8+
import { PropsWithClassName, cn } from "@podkit/lib/cn";
9+
10+
type TextMutedProps = PropsWithChildren<PropsWithClassName>;
11+
12+
export const TextMuted: FC<TextMutedProps> = ({ className, children }) => {
13+
return <span className={cn("text-gray-500 dark:text-gray-400", className)}>{children}</span>;
14+
};

components/dashboard/src/data/projects/create-project-mutation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import { useMutation } from "@tanstack/react-query";
88
import { getGitpodService } from "../../service/service";
99
import { useCurrentOrg } from "../organizations/orgs-query";
10-
import { useRefreshProjects } from "./list-projects-query";
10+
import { useRefreshAllProjects } from "./list-all-projects-query";
1111
import { CreateProjectParams, Project } from "@gitpod/gitpod-protocol";
1212

1313
export type CreateProjectArgs = Omit<CreateProjectParams, "teamId">;
1414

1515
export const useCreateProject = () => {
16-
const refreshProjects = useRefreshProjects();
16+
const refreshProjects = useRefreshAllProjects();
1717
const { data: org } = useCurrentOrg();
1818

1919
return useMutation<Project, Error, CreateProjectArgs>(async ({ name, slug, cloneUrl, appInstallationId }) => {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 { Project } from "@gitpod/gitpod-protocol";
8+
import { useQuery, useQueryClient } from "@tanstack/react-query";
9+
import { useCallback } from "react";
10+
import { listAllProjects } from "../../service/public-api";
11+
import { useCurrentOrg } from "../organizations/orgs-query";
12+
13+
export type ListAllProjectsQueryResults = {
14+
projects: Project[];
15+
};
16+
17+
export const useListAllProjectsQuery = () => {
18+
const org = useCurrentOrg().data;
19+
const orgId = org?.id;
20+
return useQuery<ListAllProjectsQueryResults>({
21+
enabled: !!orgId,
22+
queryKey: getListAllProjectsQueryKey(orgId || ""),
23+
cacheTime: 1000 * 60 * 60 * 1, // 1 hour
24+
queryFn: async () => {
25+
if (!orgId) {
26+
return {
27+
projects: [],
28+
latestPrebuilds: new Map(),
29+
};
30+
}
31+
32+
const projects = await listAllProjects({ orgId });
33+
return {
34+
projects,
35+
};
36+
},
37+
});
38+
};
39+
40+
// helper to force a refresh of the list projects query
41+
export const useRefreshAllProjects = () => {
42+
const queryClient = useQueryClient();
43+
44+
return useCallback(
45+
async (orgId: string) => {
46+
// Don't refetch if no org is provided
47+
if (!orgId) {
48+
return;
49+
}
50+
51+
return await queryClient.refetchQueries({
52+
queryKey: getListAllProjectsQueryKey(orgId),
53+
});
54+
},
55+
[queryClient],
56+
);
57+
};
58+
59+
export const getListAllProjectsQueryKey = (orgId: string) => {
60+
return ["projects", "list-all", { orgId }];
61+
};

components/dashboard/src/data/projects/list-projects-query.ts

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

7-
import { Project } from "@gitpod/gitpod-protocol";
8-
import { useQuery, useQueryClient } from "@tanstack/react-query";
9-
import { useCallback } from "react";
10-
import { listAllProjects } from "../../service/public-api";
7+
import { useQuery } from "@tanstack/react-query";
118
import { useCurrentOrg } from "../organizations/orgs-query";
9+
import { projectsService } from "../../service/public-api";
1210

13-
export type ListProjectsQueryResults = {
14-
projects: Project[];
11+
type ListProjectsQueryArgs = {
12+
page: number;
13+
pageSize: number;
1514
};
1615

17-
export const useListProjectsQuery = () => {
18-
const org = useCurrentOrg().data;
19-
const orgId = org?.id;
20-
return useQuery<ListProjectsQueryResults>({
21-
enabled: !!orgId,
22-
queryKey: getListProjectsQueryKey(orgId || ""),
23-
cacheTime: 1000 * 60 * 60 * 1, // 1 hour
24-
queryFn: async () => {
25-
if (!orgId) {
26-
return {
27-
projects: [],
28-
latestPrebuilds: new Map(),
29-
};
30-
}
31-
32-
const projects = await listAllProjects({ orgId });
33-
return {
34-
projects,
35-
};
36-
},
37-
});
38-
};
39-
40-
// helper to force a refresh of the list projects query
41-
export const useRefreshProjects = () => {
42-
const queryClient = useQueryClient();
16+
export const useListProjectsQuery = ({ page, pageSize }: ListProjectsQueryArgs) => {
17+
const { data: org } = useCurrentOrg();
4318

44-
return useCallback(
45-
async (orgId: string) => {
46-
// Don't refetch if no org is provided
47-
if (!orgId) {
48-
return;
19+
return useQuery(
20+
getListProjectsQueryKey(org?.id || "", { page, pageSize }),
21+
async () => {
22+
if (!org) {
23+
throw new Error("No org currently selected");
4924
}
5025

51-
return await queryClient.refetchQueries({
52-
queryKey: getListProjectsQueryKey(orgId),
53-
});
26+
return projectsService.listProjects({ teamId: org.id, pagination: { page, pageSize } });
27+
},
28+
{
29+
enabled: !!org,
5430
},
55-
[queryClient],
5631
);
5732
};
5833

59-
export const getListProjectsQueryKey = (orgId: string) => {
60-
return ["projects", "list", { orgId }];
34+
export const getListProjectsQueryKey = (orgId: string, { page, pageSize }: ListProjectsQueryArgs) => {
35+
return ["projects", "list", { orgId, page, pageSize }];
6136
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
export const usePrettyRepoURL = (url: string) => {
8+
return url.endsWith(".git") ? url.slice(0, -4) : url;
9+
};

components/dashboard/src/menu/OrganizationSelector.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import { useCurrentOrg, useOrganizations } from "../data/organizations/orgs-quer
1212
import { useLocation } from "react-router";
1313
import { User } from "@gitpod/gitpod-protocol";
1414
import { useOrgBillingMode } from "../data/billing-mode/org-billing-mode-query";
15+
import { useFeatureFlag } from "../data/featureflag-query";
1516

1617
export default function OrganizationSelector() {
1718
const user = useCurrentUser();
1819
const orgs = useOrganizations();
1920
const currentOrg = useCurrentOrg();
2021
const { data: billingMode } = useOrgBillingMode();
2122
const getOrgURL = useGetOrgURL();
23+
const repoConfigListAndDetail = useFeatureFlag("repoConfigListAndDetail");
2224

2325
// we should have an API to ask for permissions, until then we duplicate the logic here
2426
const canCreateOrgs = user && !User.isOrganizationOwned(user);
@@ -56,6 +58,15 @@ export default function OrganizationSelector() {
5658

5759
// Show members if we have an org selected
5860
if (currentOrg.data) {
61+
if (repoConfigListAndDetail) {
62+
linkEntries.push({
63+
title: "Repositories",
64+
customContent: <LinkEntry>Repositories</LinkEntry>,
65+
active: false,
66+
separator: false,
67+
link: "/repositories",
68+
});
69+
}
5970
linkEntries.push({
6071
title: "Members",
6172
customContent: <LinkEntry>Members</LinkEntry>,

components/dashboard/src/projects/ProjectSettings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { RemoveProjectModal } from "./RemoveProjectModal";
1717
import SelectWorkspaceClassComponent from "../components/SelectWorkspaceClassComponent";
1818
import { TextInputField } from "../components/forms/TextInputField";
1919
import { Button } from "../components/Button";
20-
import { useRefreshProjects } from "../data/projects/list-projects-query";
20+
import { useRefreshAllProjects } from "../data/projects/list-all-projects-query";
2121
import { useToast } from "../components/toasts/Toasts";
2222
import classNames from "classnames";
2323
import { InputField } from "../components/forms/InputField";
@@ -54,7 +54,7 @@ export default function ProjectSettingsView() {
5454
}
5555
}
5656
const history = useHistory();
57-
const refreshProjects = useRefreshProjects();
57+
const refreshProjects = useRefreshAllProjects();
5858
const { toast } = useToast();
5959
const [prebuildBranchPattern, setPrebuildBranchPattern] = useState("");
6060

components/dashboard/src/projects/Projects.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Alert from "../components/Alert";
1212
import Header from "../components/Header";
1313
import { SpinnerLoader } from "../components/Loader";
1414
import { useCurrentOrg } from "../data/organizations/orgs-query";
15-
import { useListProjectsQuery } from "../data/projects/list-projects-query";
15+
import { useListAllProjectsQuery } from "../data/projects/list-all-projects-query";
1616
import search from "../icons/search.svg";
1717
import { Heading2 } from "../components/typography/headings";
1818
import projectsEmptyDark from "../images/projects-empty-dark.svg";
@@ -27,7 +27,7 @@ export default function ProjectsPage() {
2727
const createProjectModal = useFeatureFlag("createProjectModal");
2828
const history = useHistory();
2929
const team = useCurrentOrg().data;
30-
const { data, isLoading, isError, refetch } = useListProjectsQuery();
30+
const { data, isLoading, isError, refetch } = useListAllProjectsQuery();
3131
const { isDark } = useContext(ThemeContext);
3232
const [searchFilter, setSearchFilter] = useState<string | undefined>();
3333
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);

components/dashboard/src/projects/project-context.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useHistory, useLocation, useRouteMatch } from "react-router";
1010
import { useCurrentOrg, useOrganizations } from "../data/organizations/orgs-query";
1111
import { listAllProjects } from "../service/public-api";
1212
import { useCurrentUser } from "../user-context";
13-
import { useListProjectsQuery } from "../data/projects/list-projects-query";
13+
import { useListAllProjectsQuery } from "../data/projects/list-all-projects-query";
1414

1515
export const ProjectContext = createContext<{
1616
project?: Project;
@@ -36,7 +36,7 @@ export function useCurrentProject(): { project: Project | undefined; loading: bo
3636
const projectIdFromRoute = useRouteMatch<{ projectId?: string }>("/projects/:projectId")?.params?.projectId;
3737
const location = useLocation();
3838
const history = useHistory();
39-
const listProjects = useListProjectsQuery();
39+
const listProjects = useListAllProjectsQuery();
4040

4141
useEffect(() => {
4242
setLoading(true);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 { usePrettyRepoURL } from "../../hooks/use-pretty-repo-url";
9+
import { TextMuted } from "@podkit/typography/TextMuted";
10+
import { Text } from "@podkit/typography/Text";
11+
import { Link } from "react-router-dom";
12+
import { Button } from "../../components/Button";
13+
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
14+
15+
type Props = {
16+
project: Project;
17+
};
18+
export const RepositoryListItem: FC<Props> = ({ project }) => {
19+
const url = usePrettyRepoURL(project.cloneUrl);
20+
21+
return (
22+
<li key={project.id} className="flex flex-row w-full space-between items-center">
23+
<div className="flex flex-col flex-grow gap-1">
24+
<Text className="font-semibold">{project.name}</Text>
25+
<TextMuted className="text-sm">{url}</TextMuted>
26+
</div>
27+
28+
<div>
29+
<Link to={`/repositories/${project.id}`}>
30+
<Button type="secondary">View</Button>
31+
</Link>
32+
</div>
33+
</li>
34+
);
35+
};

0 commit comments

Comments
 (0)