Skip to content

Commit df7929c

Browse files
Adding ConfigurationServiceAPI (#19020)
* 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
1 parent bf06755 commit df7929c

21 files changed

+2679
-28
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 { useQuery } from "@tanstack/react-query";
8+
import { useCurrentOrg } from "../organizations/orgs-query";
9+
import { configurationClient } from "../../service/public-api";
10+
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
11+
12+
const BASE_KEY = "configurations";
13+
14+
type ListConfigurationsArgs = {
15+
searchTerm?: string;
16+
page: number;
17+
pageSize: number;
18+
};
19+
20+
export const useListConfigurations = ({ searchTerm = "", page, pageSize }: ListConfigurationsArgs) => {
21+
const { data: org } = useCurrentOrg();
22+
23+
return useQuery(
24+
getListConfigurationsQueryKey(org?.id || "", { searchTerm, page, pageSize }),
25+
async () => {
26+
if (!org) {
27+
throw new Error("No org currently selected");
28+
}
29+
30+
const { configurations, pagination } = await configurationClient.listConfigurations({
31+
organizationId: org.id,
32+
searchTerm,
33+
pagination: { page, pageSize },
34+
});
35+
36+
return { configurations, pagination };
37+
},
38+
{
39+
enabled: !!org,
40+
},
41+
);
42+
};
43+
44+
export const getListConfigurationsQueryKey = (orgId: string, args?: ListConfigurationsArgs) => {
45+
const key: any[] = [BASE_KEY, "list", { orgId }];
46+
if (args) {
47+
key.push(args);
48+
}
49+
50+
return key;
51+
};
52+
53+
export const useConfiguration = (configurationId: string) => {
54+
return useQuery<Configuration | undefined, Error>(getConfigurationQueryKey(configurationId), async () => {
55+
const { configuration } = await configurationClient.getConfiguration({
56+
configurationId,
57+
});
58+
59+
return configuration;
60+
});
61+
};
62+
63+
export const getConfigurationQueryKey = (configurationId: string) => {
64+
const key: any[] = [BASE_KEY, { configurationId }];
65+
66+
return key;
67+
};

components/dashboard/src/data/setup.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import { Message } from "@bufbuild/protobuf";
1616
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
1717
import { FunctionComponent } from "react";
1818
import debounce from "lodash.debounce";
19+
// Need to import all the protobuf classes we want to support for hydration
20+
import * as OrganizationClasses from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
21+
import * as WorkspaceClasses from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
22+
import * as PaginationClasses from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
23+
import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
1924

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

135140
function initializeMessages() {
136141
const constr = [
137-
...Object.values(require("@gitpod/public-api/lib/gitpod/v1/organization_pb")),
138-
...Object.values(require("@gitpod/public-api/lib/gitpod/v1/workspace_pb")),
139-
...Object.values(require("@gitpod/public-api/lib/gitpod/v1/pagination_pb")),
142+
...Object.values(OrganizationClasses),
143+
...Object.values(WorkspaceClasses),
144+
...Object.values(PaginationClasses),
145+
...Object.values(ConfigurationClasses),
140146
];
141147
for (const c of constr) {
142148
if ((c as any).prototype instanceof Message) {

components/dashboard/src/repositories/detail/RepositoryDetail.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
import { FC } from "react";
88
import Header from "../../components/Header";
99
import { useParams } from "react-router";
10-
import { useProject } from "../../data/projects/project-queries";
1110
import { Button } from "../../components/Button";
1211
import { RepositoryNameForm } from "./RepositoryName";
1312
import { Loader2 } from "lucide-react";
1413
import Alert from "../../components/Alert";
14+
import { useConfiguration } from "../../data/configurations/configuration-queries";
1515

1616
type PageRouteParams = {
1717
id: string;
1818
};
1919
const RepositoryDetailPage: FC = () => {
2020
const { id } = useParams<PageRouteParams>();
21-
const { data, error, isLoading, refetch } = useProject({ id });
21+
const { data, error, isLoading, refetch } = useConfiguration(id);
2222

2323
return (
2424
<>
@@ -42,7 +42,7 @@ const RepositoryDetailPage: FC = () => {
4242
// TODO: add a better not-found UI w/ link back to repositories
4343
<div>Sorry, we couldn't find that repository configuration.</div>
4444
) : (
45-
<RepositoryNameForm project={data} />
45+
<RepositoryNameForm configuration={data} />
4646
))}
4747
</div>
4848
</>

components/dashboard/src/repositories/detail/RepositoryName.tsx

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

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

1615
const MAX_LENGTH = 100;
1716

1817
type Props = {
19-
project: Project;
18+
configuration: Configuration;
2019
};
2120

22-
export const RepositoryNameForm: FC<Props> = ({ project }) => {
21+
export const RepositoryNameForm: FC<Props> = ({ configuration }) => {
2322
const { toast } = useToast();
2423
const updateProject = useUpdateProject();
25-
const [projectName, setProjectName] = useState(project.name);
24+
const [projectName, setProjectName] = useState(configuration.name);
2625

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

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

3837
updateProject.mutate(
3938
{
40-
id: project.id,
39+
id: configuration.id,
4140
name: projectName,
4241
},
4342
{
@@ -47,7 +46,7 @@ export const RepositoryNameForm: FC<Props> = ({ project }) => {
4746
},
4847
);
4948
},
50-
[nameError.isValid, updateProject, project.id, projectName, toast],
49+
[nameError.isValid, updateProject, configuration.id, projectName, toast],
5150
);
5251

5352
return (
@@ -63,7 +62,7 @@ export const RepositoryNameForm: FC<Props> = ({ project }) => {
6362
<Button
6463
className="mt-4"
6564
htmlType="submit"
66-
disabled={project.name === projectName}
65+
disabled={configuration.name === projectName}
6766
loading={updateProject.isLoading}
6867
>
6968
Update Name

components/dashboard/src/repositories/list/RepoListItem.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@ import { TextMuted } from "@podkit/typography/TextMuted";
1010
import { Text } from "@podkit/typography/Text";
1111
import { Link } from "react-router-dom";
1212
import { Button } from "../../components/Button";
13-
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
13+
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
1414

1515
type Props = {
16-
project: Project;
16+
configuration: Configuration;
1717
};
18-
export const RepositoryListItem: FC<Props> = ({ project }) => {
19-
const url = usePrettyRepoURL(project.cloneUrl);
18+
export const RepositoryListItem: FC<Props> = ({ configuration }) => {
19+
const url = usePrettyRepoURL(configuration.cloneUrl);
2020

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

2828
<div>
29-
<Link to={`/repositories/${project.id}`}>
29+
<Link to={`/repositories/${configuration.id}`}>
3030
<Button type="secondary">View</Button>
3131
</Link>
3232
</div>

components/dashboard/src/repositories/list/RepositoryList.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66

77
import { FC, useCallback, useState } from "react";
88
import Header from "../../components/Header";
9-
import { useListProjectsQuery } from "../../data/projects/project-queries";
109
import { Loader2 } from "lucide-react";
1110
import { useHistory } from "react-router-dom";
1211
import { Project } from "@gitpod/gitpod-protocol";
1312
import { CreateProjectModal } from "../../projects/create-project-modal/CreateProjectModal";
1413
import { Button } from "../../components/Button";
1514
import { RepositoryListItem } from "./RepoListItem";
15+
import { useListConfigurations } from "../../data/configurations/configuration-queries";
16+
import { useStateWithDebounce } from "../../hooks/use-state-with-debounce";
17+
import { TextInput } from "../../components/forms/TextInputField";
1618

1719
const RepositoryListPage: FC = () => {
1820
const history = useHistory();
19-
const { data, isLoading } = useListProjectsQuery({ page: 1, pageSize: 10 });
21+
const [searchTerm, setSearchTerm, debouncedSearchTerm] = useStateWithDebounce("");
22+
const { data, isLoading } = useListConfigurations({ searchTerm: debouncedSearchTerm, page: 0, pageSize: 10 });
2023
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false);
2124

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

41+
<div>
42+
<TextInput value={searchTerm} onChange={setSearchTerm} placeholder="Search repositories" />
43+
</div>
44+
3845
{isLoading && <Loader2 className="animate-spin" />}
3946

4047
<ul className="space-y-2 mt-8">
4148
{!isLoading &&
42-
data?.projects.map((project) => <RepositoryListItem key={project.id} project={project} />)}
49+
data?.configurations.map((configuration) => (
50+
<RepositoryListItem key={configuration.id} configuration={configuration} />
51+
))}
4352
</ul>
4453
</div>
4554

components/dashboard/src/service/public-api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tok
1818
import { WorkspacesService as WorkspaceV1Service } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connect";
1919
import { OrganizationService } from "@gitpod/public-api/lib/gitpod/v1/organization_connect";
2020
import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect";
21+
import { ConfigurationService } from "@gitpod/public-api/lib/gitpod/v1/configuration_connect";
2122
import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
2223
import { getExperimentsClient } from "../experiments/client";
2324
import { JsonRpcOrganizationClient } from "./json-rpc-organization-client";
@@ -45,6 +46,8 @@ export const organizationClient = createServiceClient(
4546
new JsonRpcOrganizationClient(),
4647
"organization",
4748
);
49+
// No jsonrcp client for the configuration service as it's only used in new UI of the dashboard
50+
export const configurationClient = createServiceClient(ConfigurationService);
4851

4952
export async function listAllProjects(opts: { orgId: string }): Promise<ProtocolProject[]> {
5053
let pagination = {

components/gitpod-db/src/project-db.spec.db.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,95 @@ class ProjectDBSpec {
5454
const foundProject = await this.projectDb.findProjectsBySearchTerm(0, 10, "creationTime", "DESC", searchTerm);
5555

5656
expect(foundProject.rows[0].id).to.eq(storedProject.id);
57+
58+
const foundProjectByName = await this.projectDb.findProjectsBySearchTerm(
59+
0,
60+
10,
61+
"creationTime",
62+
"DESC",
63+
"some-proj",
64+
);
65+
expect(foundProjectByName.rows[0].id).to.eq(storedProject.id);
66+
67+
const foundProjectEmptySearch = await this.projectDb.findProjectsBySearchTerm(
68+
0,
69+
10,
70+
"creationTime",
71+
"DESC",
72+
" ",
73+
);
74+
expect(foundProjectEmptySearch.rows[0].id).to.eq(storedProject.id);
75+
}
76+
77+
@test()
78+
public async findProjectBySearchTermPagniation() {
79+
const user = await this.userDb.newUser();
80+
user.identities.push({
81+
authProviderId: "GitHub",
82+
authId: "1234",
83+
authName: "newUser",
84+
primaryEmail: "[email protected]",
85+
});
86+
await this.userDb.storeUser(user);
87+
88+
const project1 = Project.create({
89+
name: "some-project",
90+
cloneUrl: "some-random-clone-url",
91+
teamId: "team-1",
92+
appInstallationId: "",
93+
});
94+
const project2 = Project.create({
95+
name: "some-project-2",
96+
cloneUrl: "some-random-clone-url-2",
97+
teamId: "team-1",
98+
appInstallationId: "",
99+
});
100+
const project3 = Project.create({
101+
name: "some-project-3",
102+
cloneUrl: "some-random-clone-url-1",
103+
teamId: "team-1",
104+
appInstallationId: "",
105+
});
106+
const project4 = Project.create({
107+
name: "some-project-4",
108+
cloneUrl: "some-random-clone-url-1",
109+
teamId: "team-1",
110+
appInstallationId: "",
111+
});
112+
const project5 = Project.create({
113+
name: "some-project-5",
114+
cloneUrl: "some-random-clone-url-1",
115+
teamId: "team-1",
116+
appInstallationId: "",
117+
});
118+
const storedProject1 = await this.projectDb.storeProject(project1);
119+
const storedProject2 = await this.projectDb.storeProject(project2);
120+
const storedProject3 = await this.projectDb.storeProject(project3);
121+
const storedProject4 = await this.projectDb.storeProject(project4);
122+
const storedProject5 = await this.projectDb.storeProject(project5);
123+
124+
const allResults = await this.projectDb.findProjectsBySearchTerm(0, 10, "name", "ASC", "");
125+
expect(allResults.total).equals(5);
126+
expect(allResults.rows.length).equal(5);
127+
expect(allResults.rows[0].id).to.eq(storedProject1.id);
128+
expect(allResults.rows[1].id).to.eq(storedProject2.id);
129+
expect(allResults.rows[2].id).to.eq(storedProject3.id);
130+
expect(allResults.rows[3].id).to.eq(storedProject4.id);
131+
expect(allResults.rows[4].id).to.eq(storedProject5.id);
132+
133+
const pageSize = 3;
134+
const page1 = await this.projectDb.findProjectsBySearchTerm(0, pageSize, "name", "ASC", "");
135+
expect(page1.total).equals(5);
136+
expect(page1.rows.length).equal(3);
137+
expect(page1.rows[0].id).to.eq(storedProject1.id);
138+
expect(page1.rows[1].id).to.eq(storedProject2.id);
139+
expect(page1.rows[2].id).to.eq(storedProject3.id);
140+
141+
const page2 = await this.projectDb.findProjectsBySearchTerm(pageSize * 1, pageSize, "name", "ASC", "");
142+
expect(page2.total).equals(5);
143+
expect(page2.rows.length).equal(2);
144+
expect(page2.rows[0].id).to.eq(storedProject4.id);
145+
expect(page2.rows[1].id).to.eq(storedProject5.id);
57146
}
58147
}
59148

0 commit comments

Comments
 (0)