Skip to content

Setup data loading for repos list #18935

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
Oct 17, 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
4 changes: 4 additions & 0 deletions components/dashboard/craco.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* See License.AGPL.txt in the project root for license information.
*/
const { when } = require("@craco/craco");
const path = require("path");
const webpack = require("webpack");

module.exports = {
Expand All @@ -18,6 +19,9 @@ module.exports = {
webpack: {
configure: {
resolve: {
alias: {
"@podkit": path.resolve(__dirname, "./src/components/podkit/"),
},
fallback: {
crypto: require.resolve("crypto-browserify"),
stream: require.resolve("stream-browserify"),
Expand Down
1 change: 1 addition & 0 deletions components/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"react-router-dom": "^5.2.0",
"setimmediate": "^1.0.5",
"stream-browserify": "^2.0.1",
"tailwind-merge": "^1.14.0",
"url": "^0.11.1",
"util": "^0.11.1",
"validator": "^13.9.0",
Expand Down
17 changes: 17 additions & 0 deletions components/dashboard/src/components/RepositoryURL.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* 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";

type Props = {
className?: string;
children: string;
};
export const RepositoryURL: FC<Props> = ({ className, children }) => {
const cleanURL = children.endsWith(".git") ? children.slice(0, -4) : children;

return <span className={className}>{cleanURL}</span>;
};
18 changes: 18 additions & 0 deletions components/dashboard/src/components/podkit/lib/cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* 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 classNames, { Argument } from "classnames";
import { twMerge } from "tailwind-merge";

// Helper type to add a className prop to a component
export type PropsWithClassName<Props = {}> = {
className?: string;
} & Props;

// Helper fn to merge tailwind classes with a className prop
export function cn(...inputs: Argument[]) {
return twMerge(classNames(inputs));
}
14 changes: 14 additions & 0 deletions components/dashboard/src/components/podkit/typography/Text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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, PropsWithChildren } from "react";
import { PropsWithClassName, cn } from "@podkit/lib/cn";

type TextProps = PropsWithChildren<PropsWithClassName>;

export const Text: FC<TextProps> = ({ className, children }) => {
return <span className={cn("text-black dark:text-white", className)}>{children}</span>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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, PropsWithChildren } from "react";
import { PropsWithClassName, cn } from "@podkit/lib/cn";

type TextMutedProps = PropsWithChildren<PropsWithClassName>;

export const TextMuted: FC<TextMutedProps> = ({ className, children }) => {
return <span className={cn("text-gray-500 dark:text-gray-400", className)}>{children}</span>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import { useMutation } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";
import { useCurrentOrg } from "../organizations/orgs-query";
import { useRefreshProjects } from "./list-projects-query";
import { useRefreshAllProjects } from "./list-all-projects-query";
import { CreateProjectParams, Project } from "@gitpod/gitpod-protocol";

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

export const useCreateProject = () => {
const refreshProjects = useRefreshProjects();
const refreshProjects = useRefreshAllProjects();
const { data: org } = useCurrentOrg();

return useMutation<Project, Error, CreateProjectArgs>(async ({ name, slug, cloneUrl, appInstallationId }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 { Project } from "@gitpod/gitpod-protocol";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { listAllProjects } from "../../service/public-api";
import { useCurrentOrg } from "../organizations/orgs-query";

export type ListAllProjectsQueryResults = {
projects: Project[];
};

export const useListAllProjectsQuery = () => {
const org = useCurrentOrg().data;
const orgId = org?.id;
return useQuery<ListAllProjectsQueryResults>({
enabled: !!orgId,
queryKey: getListAllProjectsQueryKey(orgId || ""),
cacheTime: 1000 * 60 * 60 * 1, // 1 hour
queryFn: async () => {
if (!orgId) {
return {
projects: [],
latestPrebuilds: new Map(),
};
}

const projects = await listAllProjects({ orgId });
return {
projects,
};
},
});
};

// helper to force a refresh of the list projects query
export const useRefreshAllProjects = () => {
const queryClient = useQueryClient();

return useCallback(
async (orgId: string) => {
// Don't refetch if no org is provided
if (!orgId) {
return;
}

return await queryClient.refetchQueries({
queryKey: getListAllProjectsQueryKey(orgId),
});
},
[queryClient],
);
};

export const getListAllProjectsQueryKey = (orgId: string) => {
return ["projects", "list-all", { orgId }];
};
61 changes: 18 additions & 43 deletions components/dashboard/src/data/projects/list-projects-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,33 @@
* See License.AGPL.txt in the project root for license information.
*/

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

export type ListProjectsQueryResults = {
projects: Project[];
type ListProjectsQueryArgs = {
page: number;
pageSize: number;
};

export const useListProjectsQuery = () => {
const org = useCurrentOrg().data;
const orgId = org?.id;
return useQuery<ListProjectsQueryResults>({
enabled: !!orgId,
queryKey: getListProjectsQueryKey(orgId || ""),
cacheTime: 1000 * 60 * 60 * 1, // 1 hour
queryFn: async () => {
if (!orgId) {
return {
projects: [],
latestPrebuilds: new Map(),
};
}

const projects = await listAllProjects({ orgId });
return {
projects,
};
},
});
};

// helper to force a refresh of the list projects query
export const useRefreshProjects = () => {
const queryClient = useQueryClient();
export const useListProjectsQuery = ({ page, pageSize }: ListProjectsQueryArgs) => {
const { data: org } = useCurrentOrg();

return useCallback(
async (orgId: string) => {
// Don't refetch if no org is provided
if (!orgId) {
return;
return useQuery(
getListProjectsQueryKey(org?.id || "", { page, pageSize }),
async () => {
if (!org) {
throw new Error("No org currently selected");
}

return await queryClient.refetchQueries({
queryKey: getListProjectsQueryKey(orgId),
});
return projectsService.listProjects({ teamId: org.id, pagination: { page, pageSize } });
},
{
enabled: !!org,
},
[queryClient],
);
};

export const getListProjectsQueryKey = (orgId: string) => {
return ["projects", "list", { orgId }];
export const getListProjectsQueryKey = (orgId: string, { page, pageSize }: ListProjectsQueryArgs) => {
return ["projects", "list", { orgId, page, pageSize }];
};
9 changes: 9 additions & 0 deletions components/dashboard/src/hooks/use-pretty-repo-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* 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.
*/

export const usePrettyRepoURL = (url: string) => {
return url.endsWith(".git") ? url.slice(0, -4) : url;
};
11 changes: 11 additions & 0 deletions components/dashboard/src/menu/OrganizationSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { useCurrentOrg, useOrganizations } from "../data/organizations/orgs-quer
import { useLocation } from "react-router";
import { User } from "@gitpod/gitpod-protocol";
import { useOrgBillingMode } from "../data/billing-mode/org-billing-mode-query";
import { useFeatureFlag } from "../data/featureflag-query";

export default function OrganizationSelector() {
const user = useCurrentUser();
const orgs = useOrganizations();
const currentOrg = useCurrentOrg();
const { data: billingMode } = useOrgBillingMode();
const getOrgURL = useGetOrgURL();
const repoConfigListAndDetail = useFeatureFlag("repoConfigListAndDetail");

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

// Show members if we have an org selected
if (currentOrg.data) {
if (repoConfigListAndDetail) {
linkEntries.push({
title: "Repositories",
customContent: <LinkEntry>Repositories</LinkEntry>,
active: false,
separator: false,
link: "/repositories",
});
}
linkEntries.push({
title: "Members",
customContent: <LinkEntry>Members</LinkEntry>,
Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/projects/ProjectSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { RemoveProjectModal } from "./RemoveProjectModal";
import SelectWorkspaceClassComponent from "../components/SelectWorkspaceClassComponent";
import { TextInputField } from "../components/forms/TextInputField";
import { Button } from "../components/Button";
import { useRefreshProjects } from "../data/projects/list-projects-query";
import { useRefreshAllProjects } from "../data/projects/list-all-projects-query";
import { useToast } from "../components/toasts/Toasts";
import classNames from "classnames";
import { InputField } from "../components/forms/InputField";
Expand Down Expand Up @@ -54,7 +54,7 @@ export default function ProjectSettingsView() {
}
}
const history = useHistory();
const refreshProjects = useRefreshProjects();
const refreshProjects = useRefreshAllProjects();
const { toast } = useToast();
const [prebuildBranchPattern, setPrebuildBranchPattern] = useState("");

Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/projects/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Alert from "../components/Alert";
import Header from "../components/Header";
import { SpinnerLoader } from "../components/Loader";
import { useCurrentOrg } from "../data/organizations/orgs-query";
import { useListProjectsQuery } from "../data/projects/list-projects-query";
import { useListAllProjectsQuery } from "../data/projects/list-all-projects-query";
import search from "../icons/search.svg";
import { Heading2 } from "../components/typography/headings";
import projectsEmptyDark from "../images/projects-empty-dark.svg";
Expand All @@ -27,7 +27,7 @@ export default function ProjectsPage() {
const createProjectModal = useFeatureFlag("createProjectModal");
const history = useHistory();
const team = useCurrentOrg().data;
const { data, isLoading, isError, refetch } = useListProjectsQuery();
const { data, isLoading, isError, refetch } = useListAllProjectsQuery();
const { isDark } = useContext(ThemeContext);
const [searchFilter, setSearchFilter] = useState<string | undefined>();
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);
Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/projects/project-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useHistory, useLocation, useRouteMatch } from "react-router";
import { useCurrentOrg, useOrganizations } from "../data/organizations/orgs-query";
import { listAllProjects } from "../service/public-api";
import { useCurrentUser } from "../user-context";
import { useListProjectsQuery } from "../data/projects/list-projects-query";
import { useListAllProjectsQuery } from "../data/projects/list-all-projects-query";

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

useEffect(() => {
setLoading(true);
Expand Down
35 changes: 35 additions & 0 deletions components/dashboard/src/repositories/list/RepoListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* 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 { usePrettyRepoURL } from "../../hooks/use-pretty-repo-url";
import { TextMuted } from "@podkit/typography/TextMuted";
import { Text } from "@podkit/typography/Text";
import { Link } from "react-router-dom";
import { Button } from "../../components/Button";
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";

type Props = {
project: Project;
};
export const RepositoryListItem: FC<Props> = ({ project }) => {
const url = usePrettyRepoURL(project.cloneUrl);
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 potentially change this to omit the scheme in the future, as I don't think it makes much of a difference with all of them being http/https.

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 call, I'll def do that in a followup, mostly an oversight 😄


return (
<li key={project.id} className="flex flex-row w-full space-between items-center">
<div className="flex flex-col flex-grow gap-1">
<Text className="font-semibold">{project.name}</Text>
<TextMuted className="text-sm">{url}</TextMuted>
</div>

<div>
<Link to={`/repositories/${project.id}`}>
<Button type="secondary">View</Button>
</Link>
</div>
</li>
);
};
Loading