Skip to content

Commit a1ec400

Browse files
Improve frontend handling of many projects (#20151)
* Remove project context fetching * limit projects returned from `ListSuggestedRepositories` * Get rid of all projects query * A WIP state * Enhance search and normalize links * Revert find project DB changes * Make repo finder responsible for current selection * remove debug * Comments * Re-use pagination flattening * Query improvements * limit pagination * Add test about keeping length * Fix test * Fix repo ordering * add normalize comment
1 parent b96fb60 commit a1ec400

16 files changed

+189
-203
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"react-scripts": "^5.0.1",
9898
"tailwind-underline-utils": "^1.1.2",
9999
"tailwindcss-filters": "^3.0.0",
100-
"typescript": "^5.4.5",
100+
"typescript": "^5.5.4",
101101
"web-vitals": "^1.1.1"
102102
},
103103
"scripts": {

components/dashboard/src/components/RepositoryFinder.tsx

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ import { ReactComponent as RepositoryIcon } from "../icons/RepositoryWithColor.s
1111
import { ReactComponent as GitpodRepositoryTemplate } from "../icons/GitpodRepositoryTemplate.svg";
1212
import GitpodRepositoryTemplateSVG from "../icons/GitpodRepositoryTemplate.svg";
1313
import { MiddleDot } from "./typography/MiddleDot";
14-
import { useUnifiedRepositorySearch } from "../data/git-providers/unified-repositories-search-query";
14+
import {
15+
deduplicateAndFilterRepositories,
16+
flattenPagedConfigurations,
17+
useUnifiedRepositorySearch,
18+
} from "../data/git-providers/unified-repositories-search-query";
1519
import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query";
1620
import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg";
1721
import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
1822
import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
1923
import { PREDEFINED_REPOS } from "../data/git-providers/predefined-repos";
24+
import { useConfiguration, useListConfigurations } from "../data/configurations/configuration-queries";
2025

2126
interface RepositoryFinderProps {
2227
selectedContextURL?: string;
@@ -41,7 +46,7 @@ export default function RepositoryFinder({
4146
}: RepositoryFinderProps) {
4247
const [searchString, setSearchString] = useState("");
4348
const {
44-
data: repos,
49+
data: unifiedRepos,
4550
isLoading,
4651
isSearching,
4752
hasMore,
@@ -52,6 +57,59 @@ export default function RepositoryFinder({
5257
showExamples,
5358
});
5459

60+
// We search for the current context URL in order to have data for the selected suggestion
61+
const selectedItemSearch = useListConfigurations({
62+
sortBy: "name",
63+
sortOrder: "desc",
64+
pageSize: 30,
65+
searchTerm: selectedContextURL,
66+
});
67+
const flattenedSelectedItem = useMemo(() => {
68+
if (excludeConfigurations) {
69+
return [];
70+
}
71+
72+
const flattened = flattenPagedConfigurations(selectedItemSearch.data);
73+
return flattened.map(
74+
(repo) =>
75+
new SuggestedRepository({
76+
configurationId: repo.id,
77+
configurationName: repo.name,
78+
url: repo.cloneUrl,
79+
}),
80+
);
81+
}, [excludeConfigurations, selectedItemSearch.data]);
82+
83+
// We get the configuration by ID if one is selected
84+
const selectedConfiguration = useConfiguration(selectedConfigurationId);
85+
const selectedConfigurationSuggestion = useMemo(() => {
86+
if (!selectedConfiguration.data) {
87+
return undefined;
88+
}
89+
90+
return new SuggestedRepository({
91+
configurationId: selectedConfiguration.data.id,
92+
configurationName: selectedConfiguration.data.name,
93+
url: selectedConfiguration.data.cloneUrl,
94+
});
95+
}, [selectedConfiguration.data]);
96+
97+
const repos = useMemo(() => {
98+
return deduplicateAndFilterRepositories(
99+
searchString,
100+
excludeConfigurations,
101+
onlyConfigurations,
102+
[unifiedRepos, selectedConfigurationSuggestion, flattenedSelectedItem].flat().filter((r) => !!r),
103+
);
104+
}, [
105+
searchString,
106+
excludeConfigurations,
107+
onlyConfigurations,
108+
selectedConfigurationSuggestion,
109+
flattenedSelectedItem,
110+
unifiedRepos,
111+
]);
112+
55113
const authProviders = useAuthProviderDescriptions();
56114

57115
// This approach creates a memoized Map of the predefined repos,
@@ -126,6 +184,11 @@ export default function RepositoryFinder({
126184
return repo.configurationId === selectedConfigurationId;
127185
}
128186

187+
// todo(ft): normalize this more centrally
188+
if (repo.url.endsWith(".git")) {
189+
repo.url = repo.url.slice(0, -4);
190+
}
191+
129192
return repo.url === selectedContextURL;
130193
});
131194

components/dashboard/src/data/featureflag-query.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
88
import { useQuery } from "@tanstack/react-query";
99
import { getExperimentsClient } from "../experiments/client";
10-
import { useCurrentProject } from "../projects/project-context";
1110
import { useCurrentUser } from "../user-context";
1211
import { useCurrentOrg } from "./organizations/orgs-query";
1312

@@ -32,17 +31,15 @@ type FeatureFlags = typeof featureFlags;
3231
export const useFeatureFlag = <K extends keyof FeatureFlags>(featureFlag: K): FeatureFlags[K] | boolean => {
3332
const user = useCurrentUser();
3433
const org = useCurrentOrg().data;
35-
const project = useCurrentProject().project;
3634

37-
const queryKey = ["featureFlag", featureFlag, user?.id || "", org?.id || "", project?.id || ""];
35+
const queryKey = ["featureFlag", featureFlag, user?.id || "", org?.id || ""];
3836

3937
const query = useQuery(queryKey, async () => {
4038
const flagValue = await getExperimentsClient().getValueAsync(featureFlag, featureFlags[featureFlag], {
4139
user: user && {
4240
id: user.id,
4341
email: getPrimaryEmail(user),
4442
},
45-
projectId: project?.id,
4643
teamId: org?.id,
4744
teamName: org?.name,
4845
gitpodHost: window.location.host,

components/dashboard/src/data/git-providers/suggested-repositories-query.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export const useSuggestedRepositories = ({ excludeConfigurations }: Props) => {
2424
const { repositories } = await scmClient.listSuggestedRepositories({
2525
organizationId: org.id,
2626
excludeConfigurations,
27+
pagination: {
28+
pageSize: 100,
29+
},
2730
});
2831
return repositories;
2932
},

components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ test("it should exclude project entries", () => {
4545
expect(deduplicated[1].repoName).toEqual("foo2");
4646
});
4747

48-
test("it should match entries in url as well as poject name", () => {
48+
test("it should match entries in url as well as project name", () => {
4949
const suggestedRepos: SuggestedRepository[] = [
5050
repo("somefOOtest"),
5151
repo("Footest"),
@@ -54,7 +54,7 @@ test("it should match entries in url as well as poject name", () => {
5454
repo("bar", "someFootest"),
5555
repo("bar", "FOOtest"),
5656
];
57-
var deduplicated = deduplicateAndFilterRepositories("foo", false, false, suggestedRepos);
57+
let deduplicated = deduplicateAndFilterRepositories("foo", false, false, suggestedRepos);
5858
expect(deduplicated.length).toEqual(6);
5959
deduplicated = deduplicateAndFilterRepositories("foot", false, false, suggestedRepos);
6060
expect(deduplicated.length).toEqual(4);
@@ -70,12 +70,16 @@ test("it keeps the order", () => {
7070
repo("bar", "somefOO"),
7171
repo("bar", "someFootest"),
7272
repo("bar", "FOOtest"),
73+
repo("bar", "somefOO"),
7374
];
7475
const deduplicated = deduplicateAndFilterRepositories("foot", false, false, suggestedRepos);
7576
expect(deduplicated[0].repoName).toEqual("somefOOtest");
7677
expect(deduplicated[1].repoName).toEqual("Footest");
7778
expect(deduplicated[2].configurationName).toEqual("someFootest");
7879
expect(deduplicated[3].configurationName).toEqual("FOOtest");
80+
81+
const deduplicatedNoSearch = deduplicateAndFilterRepositories("", false, false, suggestedRepos);
82+
expect(deduplicatedNoSearch.length).toEqual(6);
7983
});
8084

8185
test("it should return all repositories without duplicates when excludeProjects is true", () => {

components/dashboard/src/data/git-providers/unified-repositories-search-query.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ import { useSearchRepositories } from "./search-repositories-query";
99
import { useSuggestedRepositories } from "./suggested-repositories-query";
1010
import { PREDEFINED_REPOS } from "./predefined-repos";
1111
import { useMemo } from "react";
12+
import { useListConfigurations } from "../configurations/configuration-queries";
13+
import type { UseInfiniteQueryResult } from "@tanstack/react-query";
14+
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
15+
16+
export const flattenPagedConfigurations = (
17+
data: UseInfiniteQueryResult<{ configurations: Configuration[] }>["data"],
18+
): Configuration[] => {
19+
return data?.pages.flatMap((p) => p.configurations) ?? [];
20+
};
1221

1322
type UnifiedRepositorySearchArgs = {
1423
searchString: string;
@@ -26,9 +35,34 @@ export const useUnifiedRepositorySearch = ({
2635
onlyConfigurations = false,
2736
showExamples = false,
2837
}: UnifiedRepositorySearchArgs) => {
38+
// 1st data source: suggested SCM repos + up to 100 imported repos.
39+
// todo(ft): look into deduplicating and merging these on the server
2940
const suggestedQuery = useSuggestedRepositories({ excludeConfigurations });
3041
const searchLimit = 30;
42+
// 2nd data source: SCM repos according to `searchString`
3143
const searchQuery = useSearchRepositories({ searchString, limit: searchLimit });
44+
// 3rd data source: imported repos according to `searchString`
45+
const configurationSearch = useListConfigurations({
46+
sortBy: "name",
47+
sortOrder: "desc",
48+
pageSize: searchLimit,
49+
searchTerm: searchString,
50+
});
51+
const flattenedConfigurations = useMemo(() => {
52+
if (excludeConfigurations) {
53+
return [];
54+
}
55+
56+
const flattened = flattenPagedConfigurations(configurationSearch.data);
57+
return flattened.map(
58+
(repo) =>
59+
new SuggestedRepository({
60+
configurationId: repo.id,
61+
configurationName: repo.name,
62+
url: repo.cloneUrl,
63+
}),
64+
);
65+
}, [configurationSearch.data, excludeConfigurations]);
3266

3367
const filteredRepos = useMemo(() => {
3468
if (showExamples && searchString.length === 0) {
@@ -41,17 +75,25 @@ export const useUnifiedRepositorySearch = ({
4175
);
4276
}
4377

44-
const repos = [suggestedQuery.data || [], searchQuery.data || []].flat();
78+
const repos = [suggestedQuery.data || [], searchQuery.data || [], flattenedConfigurations ?? []].flat();
4579
return deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos);
46-
}, [excludeConfigurations, onlyConfigurations, showExamples, searchQuery.data, searchString, suggestedQuery.data]);
80+
}, [
81+
showExamples,
82+
searchString,
83+
suggestedQuery.data,
84+
searchQuery.data,
85+
flattenedConfigurations,
86+
excludeConfigurations,
87+
onlyConfigurations,
88+
]);
4789

4890
return {
4991
data: filteredRepos,
50-
hasMore: searchQuery.data?.length === searchLimit,
92+
hasMore: (searchQuery.data?.length ?? 0) >= searchLimit,
5193
isLoading: suggestedQuery.isLoading,
5294
isSearching: searchQuery.isFetching,
53-
isError: suggestedQuery.isError || searchQuery.isError,
54-
error: suggestedQuery.error || searchQuery.error,
95+
isError: suggestedQuery.isError || searchQuery.isError || configurationSearch.isError,
96+
error: suggestedQuery.error || searchQuery.error || configurationSearch.error,
5597
};
5698
};
5799

@@ -72,6 +114,11 @@ export function deduplicateAndFilterRepositories(
72114
});
73115
}
74116
for (const repo of suggestedRepos) {
117+
// normalize URLs
118+
if (repo.url.endsWith(".git")) {
119+
repo.url = repo.url.slice(0, -4);
120+
}
121+
75122
// filter out configuration-less entries if an entry with a configuration exists, and we're not excluding configurations
76123
if (!repo.configurationId) {
77124
if (reposWithConfiguration.has(repo.url) || onlyConfigurations) {

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

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

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

1514
export const useCreateProject = () => {
16-
const refreshProjects = useRefreshAllProjects();
1715
const { data: org } = useCurrentOrg();
1816

1917
return useMutation<Project, Error, CreateProjectArgs>(async ({ name, slug, cloneUrl, appInstallationId }) => {
@@ -32,11 +30,6 @@ export const useCreateProject = () => {
3230
appInstallationId,
3331
});
3432

35-
// TODO: remove this once we delete ProjectContext
36-
// wait for projects to refresh before returning
37-
// this ensures that the new project is included in the list before we navigate to it
38-
await refreshProjects(org.id);
39-
4033
return newProject;
4134
});
4235
};

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

Lines changed: 0 additions & 61 deletions
This file was deleted.

components/dashboard/src/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { ConfettiContextProvider } from "./contexts/ConfettiContext";
2323
import { setupQueryClientProvider } from "./data/setup";
2424
import "./index.css";
2525
import { PaymentContextProvider } from "./payment-context";
26-
import { ProjectContextProvider } from "./projects/project-context";
2726
import { ThemeContextProvider } from "./theme-context";
2827
import { UserContextProvider } from "./user-context";
2928
import { getURLHash, isGitpodIo, isWebsiteSlug } from "./utils";
@@ -69,9 +68,7 @@ const bootApp = () => {
6968
<ToastContextProvider>
7069
<UserContextProvider>
7170
<PaymentContextProvider>
72-
<ProjectContextProvider>
73-
<RootAppRouter />
74-
</ProjectContextProvider>
71+
<RootAppRouter />
7572
</PaymentContextProvider>
7673
</UserContextProvider>
7774
</ToastContextProvider>

0 commit comments

Comments
 (0)