From fdb6839206c4cdafd2b207dc59b45f0ab16d438f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Mon, 26 Aug 2024 12:39:46 +0000 Subject: [PATCH 01/16] Remove project context fetching --- .../dashboard/src/data/featureflag-query.ts | 5 +- .../src/projects/project-context.tsx | 84 ------------------- 2 files changed, 1 insertion(+), 88 deletions(-) delete mode 100644 components/dashboard/src/projects/project-context.tsx diff --git a/components/dashboard/src/data/featureflag-query.ts b/components/dashboard/src/data/featureflag-query.ts index d42ebcb29f20c0..493fe9d12dc9e4 100644 --- a/components/dashboard/src/data/featureflag-query.ts +++ b/components/dashboard/src/data/featureflag-query.ts @@ -7,7 +7,6 @@ import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils"; import { useQuery } from "@tanstack/react-query"; import { getExperimentsClient } from "../experiments/client"; -import { useCurrentProject } from "../projects/project-context"; import { useCurrentUser } from "../user-context"; import { useCurrentOrg } from "./organizations/orgs-query"; @@ -32,9 +31,8 @@ type FeatureFlags = typeof featureFlags; export const useFeatureFlag = (featureFlag: K): FeatureFlags[K] | boolean => { const user = useCurrentUser(); const org = useCurrentOrg().data; - const project = useCurrentProject().project; - const queryKey = ["featureFlag", featureFlag, user?.id || "", org?.id || "", project?.id || ""]; + const queryKey = ["featureFlag", featureFlag, user?.id || "", org?.id || ""]; const query = useQuery(queryKey, async () => { const flagValue = await getExperimentsClient().getValueAsync(featureFlag, featureFlags[featureFlag], { @@ -42,7 +40,6 @@ export const useFeatureFlag = (featureFlag: K): Fe id: user.id, email: getPrimaryEmail(user), }, - projectId: project?.id, teamId: org?.id, teamName: org?.name, gitpodHost: window.location.host, diff --git a/components/dashboard/src/projects/project-context.tsx b/components/dashboard/src/projects/project-context.tsx deleted file mode 100644 index e9a3265442b891..00000000000000 --- a/components/dashboard/src/projects/project-context.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2021 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 React, { createContext, useContext, useEffect, useMemo, useState } from "react"; -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 { useListAllProjectsQuery } from "../data/projects/list-all-projects-query"; - -export const ProjectContext = createContext<{ - project?: Project; - setProject: React.Dispatch; -}>({ - setProject: () => null, -}); - -export const ProjectContextProvider: React.FC = ({ children }) => { - const [project, setProject] = useState(); - - const ctx = useMemo(() => ({ project, setProject }), [project]); - - return {children}; -}; - -export function useCurrentProject(): { project: Project | undefined; loading: boolean } { - const { project, setProject } = useContext(ProjectContext); - const [loading, setLoading] = useState(true); - const user = useCurrentUser(); - const org = useCurrentOrg(); - const orgs = useOrganizations(); - const projectIdFromRoute = useRouteMatch<{ projectId?: string }>("/projects/:projectId")?.params?.projectId; - const location = useLocation(); - const history = useHistory(); - const listProjects = useListAllProjectsQuery(); - - useEffect(() => { - setLoading(true); - if (!user) { - setProject(undefined); - // without a user we are still consider this loading - return; - } - if (!projectIdFromRoute) { - setProject(undefined); - setLoading(false); - return; - } - (async () => { - if (!org.data) { - return; - } - if (!listProjects.data) { - return; - } - const projects = listProjects.data?.projects || []; - - // Find project matching with slug, otherwise with name - const project = projects.find((p) => p.id === projectIdFromRoute); - if (!project && orgs.data) { - // check other orgs - for (const t of orgs.data || []) { - if (t.id === org.data?.id) { - continue; - } - const projects = await listAllProjects({ orgId: t.id }); - const project = projects.find((p) => p.id === projectIdFromRoute); - if (project) { - // redirect to the other org - history.push(location.pathname + "?org=" + t.id); - } - } - } - setProject(project); - setLoading(false); - })(); - }, [setProject, org.data, user, orgs.data, location, history, listProjects.data, projectIdFromRoute]); - - return { project, loading }; -} From 36780b964b74c0ee15cfdf2affaf61e68754416d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Mon, 26 Aug 2024 12:41:27 +0000 Subject: [PATCH 02/16] limit projects returned from `ListSuggestedRepositories` --- .../git-providers/suggested-repositories-query.ts | 3 +++ components/gitpod-db/src/project-db.ts | 2 +- .../gitpod-db/src/typeorm/project-db-impl.ts | 14 ++++++++++++-- components/server/src/api/scm-service-api.ts | 3 +-- components/server/src/projects/projects-service.ts | 4 ++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/components/dashboard/src/data/git-providers/suggested-repositories-query.ts b/components/dashboard/src/data/git-providers/suggested-repositories-query.ts index be16de3b0e1260..948ad1c5c74651 100644 --- a/components/dashboard/src/data/git-providers/suggested-repositories-query.ts +++ b/components/dashboard/src/data/git-providers/suggested-repositories-query.ts @@ -24,6 +24,9 @@ export const useSuggestedRepositories = ({ excludeConfigurations }: Props) => { const { repositories } = await scmClient.listSuggestedRepositories({ organizationId: org.id, excludeConfigurations, + pagination: { + pageSize: 100, + }, }); return repositories; }, diff --git a/components/gitpod-db/src/project-db.ts b/components/gitpod-db/src/project-db.ts index 92658f8a8f2ca1..2b07a3009d249d 100644 --- a/components/gitpod-db/src/project-db.ts +++ b/components/gitpod-db/src/project-db.ts @@ -11,7 +11,7 @@ export const ProjectDB = Symbol("ProjectDB"); export interface ProjectDB extends TransactionalDB { findProjectById(projectId: string): Promise; findProjectsByCloneUrl(cloneUrl: string, organizationId?: string): Promise; - findProjects(orgID: string): Promise; + findProjects(orgID: string, limit?: number): Promise; findProjectsBySearchTerm(args: FindProjectsBySearchTermArgs): Promise<{ total: number; rows: Project[] }>; storeProject(project: Project): Promise; updateProject(partialProject: PartialProject): Promise; diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts index 475bf9700de99b..8aab4feed8eb34 100644 --- a/components/gitpod-db/src/typeorm/project-db-impl.ts +++ b/components/gitpod-db/src/typeorm/project-db-impl.ts @@ -69,9 +69,19 @@ export class ProjectDBImpl extends TransactionalDBImpl implements Pro return repo.find(conditions); } - public async findProjects(orgId: string): Promise { + public async findProjects(orgId: string, limit?: number): Promise { const repo = await this.getRepo(); - return repo.find({ where: { teamId: orgId, markedDeleted: false }, order: { name: "ASC" } }); + + const queryBuilder = repo + .createQueryBuilder("project") + .where("project.teamId = :teamId", { teamId: orgId }) + .andWhere("project.markedDeleted = false"); + + if (limit) { + queryBuilder.take(limit); + } + + return queryBuilder.getMany(); } public async findProjectsBySearchTerm({ diff --git a/components/server/src/api/scm-service-api.ts b/components/server/src/api/scm-service-api.ts index b911d46b5ca464..dbbcbce4082341 100644 --- a/components/server/src/api/scm-service-api.ts +++ b/components/server/src/api/scm-service-api.ts @@ -81,7 +81,7 @@ export class ScmServiceAPI implements ServiceImpl { } const projectsPromise: Promise = !excludeConfigurations - ? this.projectService.getProjects(userId, organizationId) + ? this.projectService.getProjects(userId, organizationId, { limit: request.pagination?.pageSize }) : Promise.resolve([]); const workspacesPromise = this.workspaceService.getWorkspaces(userId, { organizationId }); const repos = await this.scmService.listSuggestedRepositories(userId, { projectsPromise, workspacesPromise }); @@ -89,7 +89,6 @@ export class ScmServiceAPI implements ServiceImpl { repositories: repos.map((r) => this.apiConverter.toSuggestedRepository(r)), pagination: new PaginationResponse({ nextToken: "", - total: repos.length, }), }); } diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 3b2136fce8fc29..769baa32d4f035 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -64,9 +64,9 @@ export class ProjectsService { return this.migratePrebuildSettingsOnDemand(project); } - async getProjects(userId: string, orgId: string): Promise { + async getProjects(userId: string, orgId: string, paginationOptions?: { limit?: number }): Promise { await this.auth.checkPermissionOnOrganization(userId, "read_info", orgId); - const projects = await this.projectDB.findProjects(orgId); + const projects = await this.projectDB.findProjects(orgId, paginationOptions?.limit); const filteredProjects = await this.filterByReadAccess(userId, projects); return Promise.all(filteredProjects.map((p) => this.migratePrebuildSettingsOnDemand(p))); } From 263b1b3211e37ea2146f5dc8623f212b0d980a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Mon, 26 Aug 2024 12:46:05 +0000 Subject: [PATCH 03/16] Get rid of all projects query --- .../data/projects/create-project-mutation.ts | 7 --- .../data/projects/list-all-projects-query.ts | 61 ------------------- components/dashboard/src/index.tsx | 5 +- .../src/workspaces/CreateWorkspacePage.tsx | 36 +++++++---- 4 files changed, 25 insertions(+), 84 deletions(-) delete mode 100644 components/dashboard/src/data/projects/list-all-projects-query.ts diff --git a/components/dashboard/src/data/projects/create-project-mutation.ts b/components/dashboard/src/data/projects/create-project-mutation.ts index 054fe37fb6010d..170b7563904c97 100644 --- a/components/dashboard/src/data/projects/create-project-mutation.ts +++ b/components/dashboard/src/data/projects/create-project-mutation.ts @@ -7,13 +7,11 @@ import { useMutation } from "@tanstack/react-query"; import { getGitpodService } from "../../service/service"; import { useCurrentOrg } from "../organizations/orgs-query"; -import { useRefreshAllProjects } from "./list-all-projects-query"; import { CreateProjectParams, Project } from "@gitpod/gitpod-protocol"; export type CreateProjectArgs = Omit; export const useCreateProject = () => { - const refreshProjects = useRefreshAllProjects(); const { data: org } = useCurrentOrg(); return useMutation(async ({ name, slug, cloneUrl, appInstallationId }) => { @@ -32,11 +30,6 @@ export const useCreateProject = () => { appInstallationId, }); - // TODO: remove this once we delete ProjectContext - // wait for projects to refresh before returning - // this ensures that the new project is included in the list before we navigate to it - await refreshProjects(org.id); - return newProject; }); }; diff --git a/components/dashboard/src/data/projects/list-all-projects-query.ts b/components/dashboard/src/data/projects/list-all-projects-query.ts deleted file mode 100644 index 0953869920234a..00000000000000 --- a/components/dashboard/src/data/projects/list-all-projects-query.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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({ - 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 }]; -}; diff --git a/components/dashboard/src/index.tsx b/components/dashboard/src/index.tsx index b8cfa7d1d59fba..d39c4efe24c260 100644 --- a/components/dashboard/src/index.tsx +++ b/components/dashboard/src/index.tsx @@ -23,7 +23,6 @@ import { ConfettiContextProvider } from "./contexts/ConfettiContext"; import { setupQueryClientProvider } from "./data/setup"; import "./index.css"; import { PaymentContextProvider } from "./payment-context"; -import { ProjectContextProvider } from "./projects/project-context"; import { ThemeContextProvider } from "./theme-context"; import { UserContextProvider } from "./user-context"; import { getURLHash, isGitpodIo, isWebsiteSlug } from "./utils"; @@ -69,9 +68,7 @@ const bootApp = () => { - - - + diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index c1b94913c8eff8..322b60deb5659f 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -22,7 +22,6 @@ import { InputField } from "../components/forms/InputField"; import { Heading1 } from "../components/typography/headings"; import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query"; import { useCurrentOrg } from "../data/organizations/orgs-query"; -import { useListAllProjectsQuery } from "../data/projects/list-all-projects-query"; import { useCreateWorkspaceMutation } from "../data/workspaces/create-workspace-mutation"; import { useListWorkspacesQuery } from "../data/workspaces/list-workspaces-query"; import { useWorkspaceContext } from "../data/workspaces/resolve-context-query"; @@ -55,6 +54,7 @@ import Menu from "../menu/Menu"; import { useOrgSettingsQuery } from "../data/organizations/org-settings-query"; import { useAllowedWorkspaceEditorsMemo } from "../data/ide-options/ide-options-query"; import { isGitpodIo } from "../utils"; +import { useListConfigurations } from "../data/configurations/configuration-queries"; type NextLoadOption = "searchParams" | "autoStart" | "allDone"; @@ -64,7 +64,6 @@ export function CreateWorkspacePage() { const { user, setUser } = useContext(UserContext); const updateUser = useUpdateCurrentUserMutation(); const currentOrg = useCurrentOrg().data; - const projects = useListAllProjectsQuery(); const workspaces = useListWorkspacesQuery({ limit: 50 }); const location = useLocation(); const history = useHistory(); @@ -123,11 +122,23 @@ export function CreateWorkspacePage() { setNextLoadOption("searchParams"); }, [location.hash]); + const cloneURL = workspaceContext.data?.cloneUrl; + + const paginatedProjects = useListConfigurations({ + sortBy: "name", + sortOrder: "desc", + pageSize: 100, + searchTerm: cloneURL, + }); + const projects = useMemo( + () => paginatedProjects.data?.pages.flatMap((p) => p.configurations) ?? [], + [paginatedProjects.data], + ); + const storeAutoStartOptions = useCallback(async () => { if (!workspaceContext.data || !user || !currentOrg) { return; } - const cloneURL = workspaceContext.data.cloneUrl; if (!cloneURL) { return; } @@ -160,20 +171,21 @@ export function CreateWorkspacePage() { }); setUser(updatedUser); }, [ - updateUser, + workspaceContext.data, + user, currentOrg, - selectedIde, + cloneURL, selectedWsClass, - setUser, + selectedIde, useLatestIde, preferToolbox, - user, - workspaceContext.data, + updateUser, + setUser, ]); // see if we have a matching project based on context url and project's repo url const project = useMemo(() => { - if (!workspaceContext.data || !projects.data) { + if (!workspaceContext.data || !projects) { return undefined; } const cloneUrl = workspaceContext.data.cloneUrl; @@ -181,8 +193,8 @@ export function CreateWorkspacePage() { return; } // TODO: Account for multiple projects w/ the same cloneUrl - return projects.data.projects.find((p) => p.cloneUrl === cloneUrl); - }, [projects.data, workspaceContext.data]); + return projects.find((p) => p.cloneUrl === cloneUrl); + }, [projects, workspaceContext.data]); // Handle the case where the context url in the hash matches a project and we don't have that project selected yet useEffect(() => { @@ -393,7 +405,7 @@ export function CreateWorkspacePage() { setPreferToolbox(defaultPreferToolbox); } if (!selectedWsClassIsDirty) { - const projectWsClass = project?.settings?.workspaceClasses?.regular; + const projectWsClass = project?.workspaceSettings?.workspaceClass; const targetClass = projectWsClass || defaultWorkspaceClass; if (allowedWorkspaceClasses.some((cls) => cls.id === targetClass && !cls.isDisabledInScope)) { setSelectedWsClass(targetClass, false); From 14806803e7aea587cdee73a229c12754b60153a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 27 Aug 2024 07:06:38 +0000 Subject: [PATCH 04/16] A WIP state --- .../src/components/RepositoryFinder.tsx | 6 +- .../unified-repositories-search-query.test.ts | 4 +- .../unified-repositories-search-query.ts | 62 ++++++++++++++++++- .../src/workspaces/CreateWorkspacePage.tsx | 11 ++-- .../gitpod-db/src/typeorm/project-db-impl.ts | 1 + 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 8154c424f4001d..8f49b64b7a5e81 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -50,6 +50,8 @@ export default function RepositoryFinder({ excludeConfigurations, onlyConfigurations, showExamples, + selectedContextURL, + selectedConfigurationId, }); const authProviders = useAuthProviderDescriptions(); @@ -172,12 +174,14 @@ export default function RepositoryFinder({ return; } + console.debug("Trying to display", { selectedSuggestion, selectedConfigurationId }); + if (!selectedSuggestion?.configurationName) { return displayContextUrl(selectedSuggestion?.repoName || selectedSuggestion?.url); } return selectedSuggestion?.configurationName; - }, [selectedSuggestion]); + }, [selectedConfigurationId, selectedSuggestion]); const handleSearchChange = (value: string) => { setSearchString(value); diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts index ab3d56c63410a5..d0d9fcdd2e0544 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts @@ -45,7 +45,7 @@ test("it should exclude project entries", () => { expect(deduplicated[1].repoName).toEqual("foo2"); }); -test("it should match entries in url as well as poject name", () => { +test("it should match entries in url as well as project name", () => { const suggestedRepos: SuggestedRepository[] = [ repo("somefOOtest"), repo("Footest"), @@ -54,7 +54,7 @@ test("it should match entries in url as well as poject name", () => { repo("bar", "someFootest"), repo("bar", "FOOtest"), ]; - var deduplicated = deduplicateAndFilterRepositories("foo", false, false, suggestedRepos); + let deduplicated = deduplicateAndFilterRepositories("foo", false, false, suggestedRepos); expect(deduplicated.length).toEqual(6); deduplicated = deduplicateAndFilterRepositories("foot", false, false, suggestedRepos); expect(deduplicated.length).toEqual(4); diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index cc3958273215d1..4b133dcc4fe299 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -9,6 +9,7 @@ import { useSearchRepositories } from "./search-repositories-query"; import { useSuggestedRepositories } from "./suggested-repositories-query"; import { PREDEFINED_REPOS } from "./predefined-repos"; import { useMemo } from "react"; +import { useListConfigurations } from "../configurations/configuration-queries"; type UnifiedRepositorySearchArgs = { searchString: string; @@ -18,6 +19,8 @@ type UnifiedRepositorySearchArgs = { onlyConfigurations?: boolean; // If true, only shows example repositories showExamples?: boolean; + selectedContextURL?: string; + selectedConfigurationId?: string; }; // Combines the suggested repositories and the search repositories query into one hook export const useUnifiedRepositorySearch = ({ @@ -25,10 +28,48 @@ export const useUnifiedRepositorySearch = ({ excludeConfigurations = false, onlyConfigurations = false, showExamples = false, + selectedContextURL, + selectedConfigurationId, }: UnifiedRepositorySearchArgs) => { const suggestedQuery = useSuggestedRepositories({ excludeConfigurations }); const searchLimit = 30; const searchQuery = useSearchRepositories({ searchString, limit: searchLimit }); + const configurationSearch = useListConfigurations({ + sortBy: "name", + sortOrder: "desc", + pageSize: searchLimit, + searchTerm: searchString, + }); + const flattenedConfigurations = useMemo( + () => + (configurationSearch.data?.pages.flatMap((p) => p.configurations) ?? []).map( + (repo) => + new SuggestedRepository({ + configurationId: repo.id, + configurationName: repo.name, + url: repo.cloneUrl, + }), + ), + [configurationSearch.data], + ); + const selectedItemSearch = useListConfigurations({ + sortBy: "name", + sortOrder: "desc", + pageSize: searchLimit, + searchTerm: selectedConfigurationId ?? selectedContextURL, + }); + const flattenedSelectedItem = useMemo( + () => + (selectedItemSearch.data?.pages.flatMap((p) => p.configurations) ?? []).map( + (repo) => + new SuggestedRepository({ + configurationId: repo.id, + configurationName: repo.name, + url: repo.cloneUrl, + }), + ), + [selectedItemSearch.data], + ); const filteredRepos = useMemo(() => { if (showExamples && searchString.length === 0) { @@ -41,13 +82,28 @@ export const useUnifiedRepositorySearch = ({ ); } - const repos = [suggestedQuery.data || [], searchQuery.data || []].flat(); + const repos = [ + suggestedQuery.data || [], + searchQuery.data || [], + flattenedConfigurations ?? [], + flattenedSelectedItem ?? [], + ].flat(); + console.log(deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos)); return deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos); - }, [excludeConfigurations, onlyConfigurations, showExamples, searchQuery.data, searchString, suggestedQuery.data]); + }, [ + showExamples, + searchString, + suggestedQuery.data, + searchQuery.data, + flattenedConfigurations, + flattenedSelectedItem, + excludeConfigurations, + onlyConfigurations, + ]); return { data: filteredRepos, - hasMore: searchQuery.data?.length === searchLimit, + hasMore: (searchQuery.data?.length ?? 0) >= searchLimit, isLoading: suggestedQuery.isLoading, isSearching: searchQuery.isFetching, isError: suggestedQuery.isError || searchQuery.isError, diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index 322b60deb5659f..8f9aea5c09f8ee 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -185,16 +185,15 @@ export function CreateWorkspacePage() { // see if we have a matching project based on context url and project's repo url const project = useMemo(() => { - if (!workspaceContext.data || !projects) { + if (!workspaceContext.data || projects.length === 0) { return undefined; } - const cloneUrl = workspaceContext.data.cloneUrl; - if (!cloneUrl) { + if (!cloneURL) { return; } // TODO: Account for multiple projects w/ the same cloneUrl - return projects.find((p) => p.cloneUrl === cloneUrl); - }, [projects, workspaceContext.data]); + return projects.find((p) => p.cloneUrl === cloneURL); + }, [workspaceContext.data, projects, cloneURL]); // Handle the case where the context url in the hash matches a project and we don't have that project selected yet useEffect(() => { @@ -203,6 +202,8 @@ export function CreateWorkspacePage() { } }, [project, selectedProjectID]); + console.log({ cloneURL, projects, project, setSelectedProjectID }); + // In addition to updating state, we want to update the url hash as well // This allows the contextURL to persist if user changes orgs, or copies/shares url const handleContextURLChange = useCallback( diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts index 8aab4feed8eb34..1724f7b8675af2 100644 --- a/components/gitpod-db/src/typeorm/project-db-impl.ts +++ b/components/gitpod-db/src/typeorm/project-db-impl.ts @@ -75,6 +75,7 @@ export class ProjectDBImpl extends TransactionalDBImpl implements Pro const queryBuilder = repo .createQueryBuilder("project") .where("project.teamId = :teamId", { teamId: orgId }) + .orderBy("project.creationTime", "DESC") .andWhere("project.markedDeleted = false"); if (limit) { From b76f4ae8f916aa5235442ad305ef2534989d5f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 27 Aug 2024 08:03:53 +0000 Subject: [PATCH 05/16] Enhance search and normalize links --- .../src/components/RepositoryFinder.tsx | 8 ++- .../unified-repositories-search-query.ts | 60 +++++++++++-------- .../gitpod-db/src/typeorm/project-db-impl.ts | 7 +-- 3 files changed, 43 insertions(+), 32 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 8f49b64b7a5e81..ed6d0047b1ed0c 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -128,6 +128,10 @@ export default function RepositoryFinder({ return repo.configurationId === selectedConfigurationId; } + if (repo.url.endsWith(".git")) { + repo.url = repo.url.slice(0, -4); + } + return repo.url === selectedContextURL; }); @@ -174,14 +178,12 @@ export default function RepositoryFinder({ return; } - console.debug("Trying to display", { selectedSuggestion, selectedConfigurationId }); - if (!selectedSuggestion?.configurationName) { return displayContextUrl(selectedSuggestion?.repoName || selectedSuggestion?.url); } return selectedSuggestion?.configurationName; - }, [selectedConfigurationId, selectedSuggestion]); + }, [selectedSuggestion]); const handleSearchChange = (value: string) => { setSearchString(value); diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 4b133dcc4fe299..11c7a66a34fe8b 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -10,6 +10,14 @@ import { useSuggestedRepositories } from "./suggested-repositories-query"; import { PREDEFINED_REPOS } from "./predefined-repos"; import { useMemo } from "react"; import { useListConfigurations } from "../configurations/configuration-queries"; +import type { UseInfiniteQueryResult } from "@tanstack/react-query"; +import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb"; + +const flattenPagedConfigurations = ( + data: UseInfiniteQueryResult<{ configurations: Configuration[] }>["data"], +): Configuration[] => { + return data?.pages.flatMap((p) => p.configurations) ?? []; +}; type UnifiedRepositorySearchArgs = { searchString: string; @@ -40,36 +48,34 @@ export const useUnifiedRepositorySearch = ({ pageSize: searchLimit, searchTerm: searchString, }); - const flattenedConfigurations = useMemo( - () => - (configurationSearch.data?.pages.flatMap((p) => p.configurations) ?? []).map( - (repo) => - new SuggestedRepository({ - configurationId: repo.id, - configurationName: repo.name, - url: repo.cloneUrl, - }), - ), - [configurationSearch.data], - ); + const flattenedConfigurations = useMemo(() => { + const flattened = flattenPagedConfigurations(configurationSearch.data); + return flattened.map( + (repo) => + new SuggestedRepository({ + configurationId: repo.id, + configurationName: repo.name, + url: repo.cloneUrl, + }), + ); + }, [configurationSearch.data]); const selectedItemSearch = useListConfigurations({ sortBy: "name", sortOrder: "desc", pageSize: searchLimit, searchTerm: selectedConfigurationId ?? selectedContextURL, }); - const flattenedSelectedItem = useMemo( - () => - (selectedItemSearch.data?.pages.flatMap((p) => p.configurations) ?? []).map( - (repo) => - new SuggestedRepository({ - configurationId: repo.id, - configurationName: repo.name, - url: repo.cloneUrl, - }), - ), - [selectedItemSearch.data], - ); + const flattenedSelectedItem = useMemo(() => { + const flattened = flattenPagedConfigurations(selectedItemSearch.data); + return flattened.map( + (repo) => + new SuggestedRepository({ + configurationId: repo.id, + configurationName: repo.name, + url: repo.cloneUrl, + }), + ); + }, [selectedItemSearch.data]); const filteredRepos = useMemo(() => { if (showExamples && searchString.length === 0) { @@ -88,7 +94,6 @@ export const useUnifiedRepositorySearch = ({ flattenedConfigurations ?? [], flattenedSelectedItem ?? [], ].flat(); - console.log(deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos)); return deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos); }, [ showExamples, @@ -128,6 +133,11 @@ export function deduplicateAndFilterRepositories( }); } for (const repo of suggestedRepos) { + // normalize URLs + if (repo.url.endsWith(".git")) { + repo.url = repo.url.slice(0, -4); + } + // filter out configuration-less entries if an entry with a configuration exists, and we're not excluding configurations if (!repo.configurationId) { if (reposWithConfiguration.has(repo.url) || onlyConfigurations) { diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts index 1724f7b8675af2..db304e0b84ddfa 100644 --- a/components/gitpod-db/src/typeorm/project-db-impl.ts +++ b/components/gitpod-db/src/typeorm/project-db-impl.ts @@ -111,10 +111,9 @@ export class ProjectDBImpl extends TransactionalDBImpl implements Pro if (normalizedSearchTerm) { queryBuilder.andWhere( new Brackets((qb) => { - qb.where("project.cloneUrl LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }).orWhere( - "project.name LIKE :searchTerm", - { searchTerm: `%${normalizedSearchTerm}%` }, - ); + qb.where("project.cloneUrl LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }) + .orWhere("project.name LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }) + .orWhere("project.id LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }); }), ); } From 1bb5fa82417a9876663df3de446f00eb9a797e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 28 Aug 2024 09:01:52 +0000 Subject: [PATCH 06/16] Revert find project DB changes --- components/gitpod-db/src/typeorm/project-db-impl.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts index db304e0b84ddfa..1724f7b8675af2 100644 --- a/components/gitpod-db/src/typeorm/project-db-impl.ts +++ b/components/gitpod-db/src/typeorm/project-db-impl.ts @@ -111,9 +111,10 @@ export class ProjectDBImpl extends TransactionalDBImpl implements Pro if (normalizedSearchTerm) { queryBuilder.andWhere( new Brackets((qb) => { - qb.where("project.cloneUrl LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }) - .orWhere("project.name LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }) - .orWhere("project.id LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }); + qb.where("project.cloneUrl LIKE :searchTerm", { searchTerm: `%${normalizedSearchTerm}%` }).orWhere( + "project.name LIKE :searchTerm", + { searchTerm: `%${normalizedSearchTerm}%` }, + ); }), ); } From 6397c198888c421eaec2ea9eda147d30177b95c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 28 Aug 2024 09:02:25 +0000 Subject: [PATCH 07/16] Make repo finder responsible for current selection --- components/dashboard/package.json | 2 +- .../src/components/RepositoryFinder.tsx | 64 +++++++++++++++++-- .../unified-repositories-search-query.ts | 39 +++-------- yarn.lock | 8 +-- 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/components/dashboard/package.json b/components/dashboard/package.json index f388ca7b71131b..274a91d2cc6158 100644 --- a/components/dashboard/package.json +++ b/components/dashboard/package.json @@ -97,7 +97,7 @@ "react-scripts": "^5.0.1", "tailwind-underline-utils": "^1.1.2", "tailwindcss-filters": "^3.0.0", - "typescript": "^5.4.5", + "typescript": "^5.5.4", "web-vitals": "^1.1.1" }, "scripts": { diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index ed6d0047b1ed0c..796230b0a186d9 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -11,12 +11,17 @@ import { ReactComponent as RepositoryIcon } from "../icons/RepositoryWithColor.s import { ReactComponent as GitpodRepositoryTemplate } from "../icons/GitpodRepositoryTemplate.svg"; import GitpodRepositoryTemplateSVG from "../icons/GitpodRepositoryTemplate.svg"; import { MiddleDot } from "./typography/MiddleDot"; -import { useUnifiedRepositorySearch } from "../data/git-providers/unified-repositories-search-query"; +import { + deduplicateAndFilterRepositories, + flattenPagedConfigurations, + useUnifiedRepositorySearch, +} from "../data/git-providers/unified-repositories-search-query"; import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query"; import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg"; import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb"; import { PREDEFINED_REPOS } from "../data/git-providers/predefined-repos"; +import { useConfiguration, useListConfigurations } from "../data/configurations/configuration-queries"; interface RepositoryFinderProps { selectedContextURL?: string; @@ -41,7 +46,7 @@ export default function RepositoryFinder({ }: RepositoryFinderProps) { const [searchString, setSearchString] = useState(""); const { - data: repos, + data: unifiedRepos, isLoading, isSearching, hasMore, @@ -50,10 +55,61 @@ export default function RepositoryFinder({ excludeConfigurations, onlyConfigurations, showExamples, - selectedContextURL, - selectedConfigurationId, }); + // We search for the current context URL in order to have data for the selected suggestion + const selectedItemSearch = useListConfigurations({ + sortBy: "name", + sortOrder: "desc", + pageSize: 30, + searchTerm: selectedContextURL, + }); + const flattenedSelectedItem = useMemo(() => { + if (excludeConfigurations) { + return []; + } + + const flattened = flattenPagedConfigurations(selectedItemSearch.data); + return flattened.map( + (repo) => + new SuggestedRepository({ + configurationId: repo.id, + configurationName: repo.name, + url: repo.cloneUrl, + }), + ); + }, [excludeConfigurations, selectedItemSearch.data]); + + // We get the configuration by ID if one is selected + const selectedConfiguration = useConfiguration(selectedConfigurationId); + const selectedConfigurationSuggestion = useMemo(() => { + if (!selectedConfiguration.data) { + return undefined; + } + + return new SuggestedRepository({ + configurationId: selectedConfiguration.data.id, + configurationName: selectedConfiguration.data.name, + url: selectedConfiguration.data.cloneUrl, + }); + }, [selectedConfiguration.data]); + + const repos = useMemo(() => { + return deduplicateAndFilterRepositories( + searchString, + excludeConfigurations, + onlyConfigurations, + [selectedConfigurationSuggestion, flattenedSelectedItem, unifiedRepos].flat().filter((r) => !!r), + ); + }, [ + searchString, + excludeConfigurations, + onlyConfigurations, + selectedConfigurationSuggestion, + flattenedSelectedItem, + unifiedRepos, + ]); + const authProviders = useAuthProviderDescriptions(); // This approach creates a memoized Map of the predefined repos, diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 11c7a66a34fe8b..0e6dad972cf6a5 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -13,7 +13,7 @@ import { useListConfigurations } from "../configurations/configuration-queries"; import type { UseInfiniteQueryResult } from "@tanstack/react-query"; import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb"; -const flattenPagedConfigurations = ( +export const flattenPagedConfigurations = ( data: UseInfiniteQueryResult<{ configurations: Configuration[] }>["data"], ): Configuration[] => { return data?.pages.flatMap((p) => p.configurations) ?? []; @@ -27,8 +27,6 @@ type UnifiedRepositorySearchArgs = { onlyConfigurations?: boolean; // If true, only shows example repositories showExamples?: boolean; - selectedContextURL?: string; - selectedConfigurationId?: string; }; // Combines the suggested repositories and the search repositories query into one hook export const useUnifiedRepositorySearch = ({ @@ -36,10 +34,8 @@ export const useUnifiedRepositorySearch = ({ excludeConfigurations = false, onlyConfigurations = false, showExamples = false, - selectedContextURL, - selectedConfigurationId, }: UnifiedRepositorySearchArgs) => { - const suggestedQuery = useSuggestedRepositories({ excludeConfigurations }); + const suggestedQuery = useSuggestedRepositories({ excludeConfigurations: true }); const searchLimit = 30; const searchQuery = useSearchRepositories({ searchString, limit: searchLimit }); const configurationSearch = useListConfigurations({ @@ -49,6 +45,10 @@ export const useUnifiedRepositorySearch = ({ searchTerm: searchString, }); const flattenedConfigurations = useMemo(() => { + if (excludeConfigurations) { + return []; + } + const flattened = flattenPagedConfigurations(configurationSearch.data); return flattened.map( (repo) => @@ -58,24 +58,7 @@ export const useUnifiedRepositorySearch = ({ url: repo.cloneUrl, }), ); - }, [configurationSearch.data]); - const selectedItemSearch = useListConfigurations({ - sortBy: "name", - sortOrder: "desc", - pageSize: searchLimit, - searchTerm: selectedConfigurationId ?? selectedContextURL, - }); - const flattenedSelectedItem = useMemo(() => { - const flattened = flattenPagedConfigurations(selectedItemSearch.data); - return flattened.map( - (repo) => - new SuggestedRepository({ - configurationId: repo.id, - configurationName: repo.name, - url: repo.cloneUrl, - }), - ); - }, [selectedItemSearch.data]); + }, [configurationSearch.data, excludeConfigurations]); const filteredRepos = useMemo(() => { if (showExamples && searchString.length === 0) { @@ -88,12 +71,7 @@ export const useUnifiedRepositorySearch = ({ ); } - const repos = [ - suggestedQuery.data || [], - searchQuery.data || [], - flattenedConfigurations ?? [], - flattenedSelectedItem ?? [], - ].flat(); + const repos = [suggestedQuery.data || [], searchQuery.data || [], flattenedConfigurations ?? []].flat(); return deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos); }, [ showExamples, @@ -101,7 +79,6 @@ export const useUnifiedRepositorySearch = ({ suggestedQuery.data, searchQuery.data, flattenedConfigurations, - flattenedSelectedItem, excludeConfigurations, onlyConfigurations, ]); diff --git a/yarn.lock b/yarn.lock index 6a2fb513d172f1..2afe416238920e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15193,10 +15193,10 @@ typescript@^4.2.4, typescript@~4.4.2, typescript@~4.4.4: resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz" integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== -typescript@^5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== ua-parser-js@^1.0.36: version "1.0.36" From b711f1b37efd84143773b94138b42e09a812dc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 28 Aug 2024 09:03:36 +0000 Subject: [PATCH 08/16] remove debug --- components/dashboard/src/workspaces/CreateWorkspacePage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index 8f9aea5c09f8ee..c17a06102eb0d4 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -202,8 +202,6 @@ export function CreateWorkspacePage() { } }, [project, selectedProjectID]); - console.log({ cloneURL, projects, project, setSelectedProjectID }); - // In addition to updating state, we want to update the url hash as well // This allows the contextURL to persist if user changes orgs, or copies/shares url const handleContextURLChange = useCallback( From 3357ed71f7024e45f4e9e5a3d1b66624aebc7bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 28 Aug 2024 16:18:17 +0000 Subject: [PATCH 09/16] Comments --- .../data/git-providers/unified-repositories-search-query.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 0e6dad972cf6a5..35eec2db83ba56 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -35,9 +35,13 @@ export const useUnifiedRepositorySearch = ({ onlyConfigurations = false, showExamples = false, }: UnifiedRepositorySearchArgs) => { + // 1st data source: suggested SCM repos + up to 100 imported repos. + // todo(ft): look into deduplicating and merging these on the server const suggestedQuery = useSuggestedRepositories({ excludeConfigurations: true }); const searchLimit = 30; + // 2nd data source: SCM repos according to `searchString` const searchQuery = useSearchRepositories({ searchString, limit: searchLimit }); + // 3rd data source: imported repos according to `searchString` const configurationSearch = useListConfigurations({ sortBy: "name", sortOrder: "desc", From 4c30e6a7e9fe8d90fd9fe45d3e7a88d0006c0e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 28 Aug 2024 16:24:48 +0000 Subject: [PATCH 10/16] Re-use pagination flattening --- .../src/workspaces/CreateWorkspacePage.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index c17a06102eb0d4..8d75dbdebe9d14 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -55,6 +55,8 @@ import { useOrgSettingsQuery } from "../data/organizations/org-settings-query"; import { useAllowedWorkspaceEditorsMemo } from "../data/ide-options/ide-options-query"; import { isGitpodIo } from "../utils"; import { useListConfigurations } from "../data/configurations/configuration-queries"; +import { flattenPagedConfigurations } from "../data/git-providers/unified-repositories-search-query"; +import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb"; type NextLoadOption = "searchParams" | "autoStart" | "allDone"; @@ -124,15 +126,15 @@ export function CreateWorkspacePage() { const cloneURL = workspaceContext.data?.cloneUrl; - const paginatedProjects = useListConfigurations({ + const paginatedConfigurations = useListConfigurations({ sortBy: "name", sortOrder: "desc", pageSize: 100, searchTerm: cloneURL, }); - const projects = useMemo( - () => paginatedProjects.data?.pages.flatMap((p) => p.configurations) ?? [], - [paginatedProjects.data], + const configurations = useMemo( + () => flattenPagedConfigurations(paginatedConfigurations.data), + [paginatedConfigurations.data], ); const storeAutoStartOptions = useCallback(async () => { @@ -183,24 +185,24 @@ export function CreateWorkspacePage() { setUser, ]); - // see if we have a matching project based on context url and project's repo url - const project = useMemo(() => { - if (!workspaceContext.data || projects.length === 0) { + // see if we have a matching configuration based on context url and configuration's repo url + const configuration = useMemo(() => { + if (!workspaceContext.data || configurations.length === 0) { return undefined; } if (!cloneURL) { return; } - // TODO: Account for multiple projects w/ the same cloneUrl - return projects.find((p) => p.cloneUrl === cloneURL); - }, [workspaceContext.data, projects, cloneURL]); + // TODO: Account for multiple configurations w/ the same cloneUrl + return configurations.find((p) => p.cloneUrl === cloneURL); + }, [workspaceContext.data, configurations, cloneURL]); // Handle the case where the context url in the hash matches a project and we don't have that project selected yet useEffect(() => { - if (project && !selectedProjectID) { - setSelectedProjectID(project.id); + if (configuration && !selectedProjectID) { + setSelectedProjectID(configuration.id); } - }, [project, selectedProjectID]); + }, [configuration, selectedProjectID]); // In addition to updating state, we want to update the url hash as well // This allows the contextURL to persist if user changes orgs, or copies/shares url @@ -211,7 +213,7 @@ export function CreateWorkspacePage() { // TODO: consider storing SuggestedRepository as state vs. discrete props setContextURL(repo?.url); setSelectedProjectID(repo?.configurationId); - // TOOD: consider dropping this - it's a lossy conversion + // TODO: consider dropping this - it's a lossy conversion history.replace(`#${repo?.url}`); // reset load options setNextLoadOption("searchParams"); @@ -404,7 +406,7 @@ export function CreateWorkspacePage() { setPreferToolbox(defaultPreferToolbox); } if (!selectedWsClassIsDirty) { - const projectWsClass = project?.workspaceSettings?.workspaceClass; + const projectWsClass = configuration?.workspaceSettings?.workspaceClass; const targetClass = projectWsClass || defaultWorkspaceClass; if (allowedWorkspaceClasses.some((cls) => cls.id === targetClass && !cls.isDisabledInScope)) { setSelectedWsClass(targetClass, false); @@ -415,7 +417,7 @@ export function CreateWorkspacePage() { setNextLoadOption("allDone"); // we only update the remembered options when the workspaceContext changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [workspaceContext.data, nextLoadOption, project, isLoadingWorkspaceClasses, allowedWorkspaceClasses]); + }, [workspaceContext.data, nextLoadOption, configuration, isLoadingWorkspaceClasses, allowedWorkspaceClasses]); // Need a wrapper here so we call createWorkspace w/o any arguments const onClickCreate = useCallback(() => createWorkspace(), [createWorkspace]); From bbd8559beae585c89f180dd80025a04d65133d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 29 Aug 2024 12:01:21 +0000 Subject: [PATCH 11/16] Query improvements --- .../data/git-providers/unified-repositories-search-query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 35eec2db83ba56..3f13bea4e13b81 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -37,7 +37,7 @@ export const useUnifiedRepositorySearch = ({ }: UnifiedRepositorySearchArgs) => { // 1st data source: suggested SCM repos + up to 100 imported repos. // todo(ft): look into deduplicating and merging these on the server - const suggestedQuery = useSuggestedRepositories({ excludeConfigurations: true }); + const suggestedQuery = useSuggestedRepositories({ excludeConfigurations }); const searchLimit = 30; // 2nd data source: SCM repos according to `searchString` const searchQuery = useSearchRepositories({ searchString, limit: searchLimit }); @@ -92,8 +92,8 @@ export const useUnifiedRepositorySearch = ({ hasMore: (searchQuery.data?.length ?? 0) >= searchLimit, isLoading: suggestedQuery.isLoading, isSearching: searchQuery.isFetching, - isError: suggestedQuery.isError || searchQuery.isError, - error: suggestedQuery.error || searchQuery.error, + isError: suggestedQuery.isError || searchQuery.isError || configurationSearch.isError, + error: suggestedQuery.error || searchQuery.error || configurationSearch.error, }; }; From 1c5a88db3cf967a5af8feabc9bfd3bec63a0aa2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 29 Aug 2024 12:18:45 +0000 Subject: [PATCH 12/16] limit pagination --- components/server/src/api/scm-service-api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/server/src/api/scm-service-api.ts b/components/server/src/api/scm-service-api.ts index dbbcbce4082341..8b9cbe51253969 100644 --- a/components/server/src/api/scm-service-api.ts +++ b/components/server/src/api/scm-service-api.ts @@ -80,6 +80,10 @@ export class ScmServiceAPI implements ServiceImpl { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId must be a valid UUID"); } + if (request.pagination?.pageSize && request.pagination?.pageSize > 100) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Pagesize must not exceed 100"); + } + const projectsPromise: Promise = !excludeConfigurations ? this.projectService.getProjects(userId, organizationId, { limit: request.pagination?.pageSize }) : Promise.resolve([]); From f246051489f746c2a62670d3c6c96b410a13b3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 29 Aug 2024 19:46:13 +0000 Subject: [PATCH 13/16] Add test about keeping length --- .../git-providers/unified-repositories-search-query.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts index d0d9fcdd2e0544..526da0c4d094d0 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts @@ -70,12 +70,15 @@ test("it keeps the order", () => { repo("bar", "somefOO"), repo("bar", "someFootest"), repo("bar", "FOOtest"), + repo("bar", "somefOO"), ]; const deduplicated = deduplicateAndFilterRepositories("foot", false, false, suggestedRepos); expect(deduplicated[0].repoName).toEqual("somefOOtest"); expect(deduplicated[1].repoName).toEqual("Footest"); expect(deduplicated[2].configurationName).toEqual("someFootest"); expect(deduplicated[3].configurationName).toEqual("FOOtest"); + + expect(deduplicated.length).toEqual(6); }); test("it should return all repositories without duplicates when excludeProjects is true", () => { From f30ace128dd1880b71aea9920dfd2e2059f9eb76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 29 Aug 2024 20:11:45 +0000 Subject: [PATCH 14/16] Fix test --- .../git-providers/unified-repositories-search-query.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts index 526da0c4d094d0..3f01e4a900b180 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.test.ts @@ -78,7 +78,8 @@ test("it keeps the order", () => { expect(deduplicated[2].configurationName).toEqual("someFootest"); expect(deduplicated[3].configurationName).toEqual("FOOtest"); - expect(deduplicated.length).toEqual(6); + const deduplicatedNoSearch = deduplicateAndFilterRepositories("", false, false, suggestedRepos); + expect(deduplicatedNoSearch.length).toEqual(6); }); test("it should return all repositories without duplicates when excludeProjects is true", () => { From 6124e2af72765ec08a52c2c4a0f888c62493e033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 29 Aug 2024 20:27:55 +0000 Subject: [PATCH 15/16] Fix repo ordering --- components/dashboard/src/components/RepositoryFinder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 796230b0a186d9..239aedf66d0b97 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -99,7 +99,7 @@ export default function RepositoryFinder({ searchString, excludeConfigurations, onlyConfigurations, - [selectedConfigurationSuggestion, flattenedSelectedItem, unifiedRepos].flat().filter((r) => !!r), + [unifiedRepos, selectedConfigurationSuggestion, flattenedSelectedItem].flat().filter((r) => !!r), ); }, [ searchString, From eea4d0f03b6e8dbf836f6e0df988dd058d58bde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Thu, 29 Aug 2024 20:31:27 +0000 Subject: [PATCH 16/16] add normalize comment --- components/dashboard/src/components/RepositoryFinder.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 239aedf66d0b97..56873704f9c31e 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -184,6 +184,7 @@ export default function RepositoryFinder({ return repo.configurationId === selectedConfigurationId; } + // todo(ft): normalize this more centrally if (repo.url.endsWith(".git")) { repo.url = repo.url.slice(0, -4); }