Skip to content

Commit

Permalink
Adding ConfigurationServiceAPI (#19020)
Browse files Browse the repository at this point in the history
* adding ConfigurationServiceAPI

* binding config service api to server

* use getConfiguration in dashboard

* adding missing binding

* use ApplicationError's

* add protobuf classes to query client hydration

* fixing pagination param & query

* changing to import statements for consistency and clarity on what the imports are for

* cleanup

* dropping config settings for create for now

* use protobuf field names in error messages

* removing optional on fields

* fixing converters to account for non-optional (undefined) fields

* update test

* adding more tests for findProjectsBySearchTerm

* fixing test to use offset correctly

* convert pagination args correctly
  • Loading branch information
selfcontained authored Nov 8, 2023
1 parent bf06755 commit df7929c
Show file tree
Hide file tree
Showing 21 changed files with 2,679 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* 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 { useQuery } from "@tanstack/react-query";
import { useCurrentOrg } from "../organizations/orgs-query";
import { configurationClient } from "../../service/public-api";
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";

const BASE_KEY = "configurations";

type ListConfigurationsArgs = {
searchTerm?: string;
page: number;
pageSize: number;
};

export const useListConfigurations = ({ searchTerm = "", page, pageSize }: ListConfigurationsArgs) => {
const { data: org } = useCurrentOrg();

return useQuery(
getListConfigurationsQueryKey(org?.id || "", { searchTerm, page, pageSize }),
async () => {
if (!org) {
throw new Error("No org currently selected");
}

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

return { configurations, pagination };
},
{
enabled: !!org,
},
);
};

export const getListConfigurationsQueryKey = (orgId: string, args?: ListConfigurationsArgs) => {
const key: any[] = [BASE_KEY, "list", { orgId }];
if (args) {
key.push(args);
}

return key;
};

export const useConfiguration = (configurationId: string) => {
return useQuery<Configuration | undefined, Error>(getConfigurationQueryKey(configurationId), async () => {
const { configuration } = await configurationClient.getConfiguration({
configurationId,
});

return configuration;
});
};

export const getConfigurationQueryKey = (configurationId: string) => {
const key: any[] = [BASE_KEY, { configurationId }];

return key;
};
12 changes: 9 additions & 3 deletions components/dashboard/src/data/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { Message } from "@bufbuild/protobuf";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { FunctionComponent } from "react";
import debounce from "lodash.debounce";
// Need to import all the protobuf classes we want to support for hydration
import * as OrganizationClasses from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import * as WorkspaceClasses from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import * as PaginationClasses from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";

// This is used to version the cache
// If data we cache changes in a non-backwards compatible way, increment this version
Expand Down Expand Up @@ -134,9 +139,10 @@ const supportedMessages = new Map<string, typeof Message>();

function initializeMessages() {
const constr = [
...Object.values(require("@gitpod/public-api/lib/gitpod/v1/organization_pb")),
...Object.values(require("@gitpod/public-api/lib/gitpod/v1/workspace_pb")),
...Object.values(require("@gitpod/public-api/lib/gitpod/v1/pagination_pb")),
...Object.values(OrganizationClasses),
...Object.values(WorkspaceClasses),
...Object.values(PaginationClasses),
...Object.values(ConfigurationClasses),
];
for (const c of constr) {
if ((c as any).prototype instanceof Message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
import { FC } from "react";
import Header from "../../components/Header";
import { useParams } from "react-router";
import { useProject } from "../../data/projects/project-queries";
import { Button } from "../../components/Button";
import { RepositoryNameForm } from "./RepositoryName";
import { Loader2 } from "lucide-react";
import Alert from "../../components/Alert";
import { useConfiguration } from "../../data/configurations/configuration-queries";

type PageRouteParams = {
id: string;
};
const RepositoryDetailPage: FC = () => {
const { id } = useParams<PageRouteParams>();
const { data, error, isLoading, refetch } = useProject({ id });
const { data, error, isLoading, refetch } = useConfiguration(id);

return (
<>
Expand All @@ -42,7 +42,7 @@ const RepositoryDetailPage: FC = () => {
// TODO: add a better not-found UI w/ link back to repositories
<div>Sorry, we couldn't find that repository configuration.</div>
) : (
<RepositoryNameForm project={data} />
<RepositoryNameForm configuration={data} />
))}
</div>
</>
Expand Down
15 changes: 7 additions & 8 deletions components/dashboard/src/repositories/detail/RepositoryName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@
* See License.AGPL.txt in the project root for license information.
*/

// TODO: fix mismatched project types when we build repo configuration apis
import { Project } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
import { Button } from "../../components/Button";
import { TextInputField } from "../../components/forms/TextInputField";
import { FC, useCallback, useState } from "react";
import { useUpdateProject } from "../../data/projects/project-queries";
import { useToast } from "../../components/toasts/Toasts";
import { useOnBlurError } from "../../hooks/use-onblur-error";
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";

const MAX_LENGTH = 100;

type Props = {
project: Project;
configuration: Configuration;
};

export const RepositoryNameForm: FC<Props> = ({ project }) => {
export const RepositoryNameForm: FC<Props> = ({ configuration }) => {
const { toast } = useToast();
const updateProject = useUpdateProject();
const [projectName, setProjectName] = useState(project.name);
const [projectName, setProjectName] = useState(configuration.name);

const nameError = useOnBlurError("Sorry, this name is too long.", projectName.length <= MAX_LENGTH);

Expand All @@ -37,7 +36,7 @@ export const RepositoryNameForm: FC<Props> = ({ project }) => {

updateProject.mutate(
{
id: project.id,
id: configuration.id,
name: projectName,
},
{
Expand All @@ -47,7 +46,7 @@ export const RepositoryNameForm: FC<Props> = ({ project }) => {
},
);
},
[nameError.isValid, updateProject, project.id, projectName, toast],
[nameError.isValid, updateProject, configuration.id, projectName, toast],
);

return (
Expand All @@ -63,7 +62,7 @@ export const RepositoryNameForm: FC<Props> = ({ project }) => {
<Button
className="mt-4"
htmlType="submit"
disabled={project.name === projectName}
disabled={configuration.name === projectName}
loading={updateProject.isLoading}
>
Update Name
Expand Down
14 changes: 7 additions & 7 deletions components/dashboard/src/repositories/list/RepoListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ 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";
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";

type Props = {
project: Project;
configuration: Configuration;
};
export const RepositoryListItem: FC<Props> = ({ project }) => {
const url = usePrettyRepoURL(project.cloneUrl);
export const RepositoryListItem: FC<Props> = ({ configuration }) => {
const url = usePrettyRepoURL(configuration.cloneUrl);

return (
<li key={project.id} className="flex flex-row w-full space-between items-center">
<li key={configuration.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>
<Text className="font-semibold">{configuration.name}</Text>
<TextMuted className="text-sm">{url}</TextMuted>
</div>

<div>
<Link to={`/repositories/${project.id}`}>
<Link to={`/repositories/${configuration.id}`}>
<Button type="secondary">View</Button>
</Link>
</div>
Expand Down
15 changes: 12 additions & 3 deletions components/dashboard/src/repositories/list/RepositoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@

import { FC, useCallback, useState } from "react";
import Header from "../../components/Header";
import { useListProjectsQuery } from "../../data/projects/project-queries";
import { Loader2 } from "lucide-react";
import { useHistory } from "react-router-dom";
import { Project } from "@gitpod/gitpod-protocol";
import { CreateProjectModal } from "../../projects/create-project-modal/CreateProjectModal";
import { Button } from "../../components/Button";
import { RepositoryListItem } from "./RepoListItem";
import { useListConfigurations } from "../../data/configurations/configuration-queries";
import { useStateWithDebounce } from "../../hooks/use-state-with-debounce";
import { TextInput } from "../../components/forms/TextInputField";

const RepositoryListPage: FC = () => {
const history = useHistory();
const { data, isLoading } = useListProjectsQuery({ page: 1, pageSize: 10 });
const [searchTerm, setSearchTerm, debouncedSearchTerm] = useStateWithDebounce("");
const { data, isLoading } = useListConfigurations({ searchTerm: debouncedSearchTerm, page: 0, pageSize: 10 });
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);

const handleProjectCreated = useCallback(
Expand All @@ -35,11 +38,17 @@ const RepositoryListPage: FC = () => {
<Button onClick={() => setShowCreateProjectModal(true)}>Configure Repository</Button>
</div>

<div>
<TextInput value={searchTerm} onChange={setSearchTerm} placeholder="Search repositories" />
</div>

{isLoading && <Loader2 className="animate-spin" />}

<ul className="space-y-2 mt-8">
{!isLoading &&
data?.projects.map((project) => <RepositoryListItem key={project.id} project={project} />)}
data?.configurations.map((configuration) => (
<RepositoryListItem key={configuration.id} configuration={configuration} />
))}
</ul>
</div>

Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/service/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tok
import { WorkspacesService as WorkspaceV1Service } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connect";
import { OrganizationService } from "@gitpod/public-api/lib/gitpod/v1/organization_connect";
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
import { ConfigurationService } from "@gitpod/public-api/lib/gitpod/v1/configuration_connect";
import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
import { getExperimentsClient } from "../experiments/client";
import { JsonRpcOrganizationClient } from "./json-rpc-organization-client";
Expand Down Expand Up @@ -45,6 +46,8 @@ export const organizationClient = createServiceClient(
new JsonRpcOrganizationClient(),
"organization",
);
// No jsonrcp client for the configuration service as it's only used in new UI of the dashboard
export const configurationClient = createServiceClient(ConfigurationService);

export async function listAllProjects(opts: { orgId: string }): Promise<ProtocolProject[]> {
let pagination = {
Expand Down
89 changes: 89 additions & 0 deletions components/gitpod-db/src/project-db.spec.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,95 @@ class ProjectDBSpec {
const foundProject = await this.projectDb.findProjectsBySearchTerm(0, 10, "creationTime", "DESC", searchTerm);

expect(foundProject.rows[0].id).to.eq(storedProject.id);

const foundProjectByName = await this.projectDb.findProjectsBySearchTerm(
0,
10,
"creationTime",
"DESC",
"some-proj",
);
expect(foundProjectByName.rows[0].id).to.eq(storedProject.id);

const foundProjectEmptySearch = await this.projectDb.findProjectsBySearchTerm(
0,
10,
"creationTime",
"DESC",
" ",
);
expect(foundProjectEmptySearch.rows[0].id).to.eq(storedProject.id);
}

@test()
public async findProjectBySearchTermPagniation() {
const user = await this.userDb.newUser();
user.identities.push({
authProviderId: "GitHub",
authId: "1234",
authName: "newUser",
primaryEmail: "[email protected]",
});
await this.userDb.storeUser(user);

const project1 = Project.create({
name: "some-project",
cloneUrl: "some-random-clone-url",
teamId: "team-1",
appInstallationId: "",
});
const project2 = Project.create({
name: "some-project-2",
cloneUrl: "some-random-clone-url-2",
teamId: "team-1",
appInstallationId: "",
});
const project3 = Project.create({
name: "some-project-3",
cloneUrl: "some-random-clone-url-1",
teamId: "team-1",
appInstallationId: "",
});
const project4 = Project.create({
name: "some-project-4",
cloneUrl: "some-random-clone-url-1",
teamId: "team-1",
appInstallationId: "",
});
const project5 = Project.create({
name: "some-project-5",
cloneUrl: "some-random-clone-url-1",
teamId: "team-1",
appInstallationId: "",
});
const storedProject1 = await this.projectDb.storeProject(project1);
const storedProject2 = await this.projectDb.storeProject(project2);
const storedProject3 = await this.projectDb.storeProject(project3);
const storedProject4 = await this.projectDb.storeProject(project4);
const storedProject5 = await this.projectDb.storeProject(project5);

const allResults = await this.projectDb.findProjectsBySearchTerm(0, 10, "name", "ASC", "");
expect(allResults.total).equals(5);
expect(allResults.rows.length).equal(5);
expect(allResults.rows[0].id).to.eq(storedProject1.id);
expect(allResults.rows[1].id).to.eq(storedProject2.id);
expect(allResults.rows[2].id).to.eq(storedProject3.id);
expect(allResults.rows[3].id).to.eq(storedProject4.id);
expect(allResults.rows[4].id).to.eq(storedProject5.id);

const pageSize = 3;
const page1 = await this.projectDb.findProjectsBySearchTerm(0, pageSize, "name", "ASC", "");
expect(page1.total).equals(5);
expect(page1.rows.length).equal(3);
expect(page1.rows[0].id).to.eq(storedProject1.id);
expect(page1.rows[1].id).to.eq(storedProject2.id);
expect(page1.rows[2].id).to.eq(storedProject3.id);

const page2 = await this.projectDb.findProjectsBySearchTerm(pageSize * 1, pageSize, "name", "ASC", "");
expect(page2.total).equals(5);
expect(page2.rows.length).equal(2);
expect(page2.rows[0].id).to.eq(storedProject4.id);
expect(page2.rows[1].id).to.eq(storedProject5.id);
}
}

Expand Down
Loading

0 comments on commit df7929c

Please sign in to comment.