From 3b6dd42172c54c077841c88cedfebf46bc0079e3 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Mon, 18 Sep 2023 18:46:12 +0000 Subject: [PATCH] handle selected projects better --- .../dashboard/src/components/DropDown2.tsx | 11 +- .../src/components/RepositoryFinder.tsx | 192 ++++++++++++------ .../src/components/SelectIDEComponent.tsx | 2 +- .../SelectWorkspaceClassComponent.tsx | 2 +- .../src/components/typography/text.tsx | 16 -- components/dashboard/src/start/Open.tsx | 2 +- .../src/workspaces/CreateWorkspacePage.tsx | 18 +- 7 files changed, 155 insertions(+), 88 deletions(-) delete mode 100644 components/dashboard/src/components/typography/text.tsx diff --git a/components/dashboard/src/components/DropDown2.tsx b/components/dashboard/src/components/DropDown2.tsx index 315aa9a7ef32d3..70fa8a042d24a5 100644 --- a/components/dashboard/src/components/DropDown2.tsx +++ b/components/dashboard/src/components/DropDown2.tsx @@ -250,7 +250,8 @@ export const DropDown2: FunctionComponent = ({ }; type DropDown2SelectedElementProps = { - iconSrc: string; + // Either a string of the icon source or an element + icon: ReactNode; loading?: boolean; title: ReactNode; subtitle: ReactNode; @@ -258,7 +259,7 @@ type DropDown2SelectedElementProps = { }; export const DropDown2SelectedElement: FC = ({ - iconSrc, + icon, loading = false, title, subtitle, @@ -272,7 +273,11 @@ export const DropDown2SelectedElement: FC = ({ aria-busy={loading} >
- logo + {typeof icon === "string" ? ( + logo + ) : ( + <>{icon} + )}
{loading ? ( diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 90aab938e71c6d..4f1206d2d9caef 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -4,42 +4,37 @@ * See License.AGPL.txt in the project root for license information. */ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { getGitpodService } from "../service/service"; import { DropDown2, DropDown2Element, DropDown2SelectedElement } from "./DropDown2"; -import Repository from "../icons/Repository.svg"; import { ReactComponent as RepositoryIcon } from "../icons/RepositoryWithColor.svg"; import { useSuggestedRepositories } from "../data/git-providers/suggested-repositories-query"; import { useFeatureFlag } from "../data/featureflag-query"; import { SuggestedRepository } from "@gitpod/gitpod-protocol"; -import { TextLight } from "./typography/text"; import classNames from "classnames"; import { MiddleDot } from "./typography/MiddleDot"; +// TODO: Remove this once we've fully enabled `includeProjectsOnCreateWorkspace` +// flag (caches w/ react-query instead of local storage) const LOCAL_STORAGE_KEY = "open-in-gitpod-search-data"; interface RepositoryFinderProps { - initialValue?: string; + selectedContextURL?: string; + selectedProjectID?: string; maxDisplayItems?: number; - setSelection?: (selection: string) => void; + setSelection: (repoUrl: string, projectID?: string) => void; onError?: (error: string) => void; disabled?: boolean; loading?: boolean; } -function stripOffProtocol(url: string): string { - if (!url.startsWith("http")) { - return url; - } - return url.substring(url.indexOf("//") + 2); -} - export default function RepositoryFinder(props: RepositoryFinderProps) { const includeProjectsOnCreateWorkspace = useFeatureFlag("includeProjectsOnCreateWorkspace"); const [suggestedContextURLs, setSuggestedContextURLs] = useState(loadSearchData()); const { data: suggestedRepos, isLoading } = useSuggestedRepositories(); + // TODO: remove this once includeProjectsOnCreateWorkspace is fully enabled const suggestedRepoURLs = useMemo(() => { // If the flag is disabled continue to use suggestedContextURLs, but convert into SuggestedRepository objects if (!includeProjectsOnCreateWorkspace) { @@ -53,6 +48,7 @@ export default function RepositoryFinder(props: RepositoryFinderProps) { return suggestedRepos || []; }, [suggestedContextURLs, suggestedRepos, includeProjectsOnCreateWorkspace]); + // TODO: remove this once includeProjectsOnCreateWorkspace is fully enabled useEffect(() => { getGitpodService() .server.getSuggestedContextURLs() @@ -62,6 +58,56 @@ export default function RepositoryFinder(props: RepositoryFinderProps) { }); }, []); + const handleSelectionChange = useCallback( + (selectedID: string) => { + // selectedId is either projectId or repo url + const matchingSuggestion = suggestedRepos?.find((repo) => { + if (repo.projectId) { + return repo.projectId === selectedID; + } + + return repo.url === selectedID; + }); + if (matchingSuggestion) { + props.setSelection(matchingSuggestion.url, matchingSuggestion.projectId); + return; + } + + // If we have no matching suggestion, it's a context URL they typed/pasted in, so just use that as the url + props.setSelection(selectedID); + }, + [props, suggestedRepos], + ); + + // Resolve the selected context url & project id props to a suggestion entry + const selectedSuggestion = useMemo(() => { + let match = suggestedRepos?.find((repo) => { + if (props.selectedProjectID) { + return repo.projectId === props.selectedProjectID; + } + + return repo.url === props.selectedContextURL; + }); + + // If no match, it's a context url that was typed/pasted in, so treat it like a suggestion w/ just a url + if (!match && props.selectedContextURL) { + match = { + url: props.selectedContextURL, + }; + } + + // This means we found a matching project, but the context url is different + // user may be using a pr or branch url, so we want to make sure and use that w/ the matching project + if (match && match.projectId && props.selectedContextURL && match.url !== props.selectedContextURL) { + match = { + ...match, + url: props.selectedContextURL, + }; + } + + return match; + }, [props.selectedProjectID, props.selectedContextURL, suggestedRepos]); + const getElements = useCallback( (searchString: string) => { const results = filterRepos(searchString, suggestedRepoURLs); @@ -90,39 +136,9 @@ export default function RepositoryFinder(props: RepositoryFinderProps) { // Otherwise we show the suggestedRepos return results.map((repo) => { - const name = repo.projectName || repo.repositoryName; - return { - id: repo.url, - element: ( -
- - - - - {name && ( - <> - {name} - - - - - )} - - {/* TODO: refine some Text* components a bit to make it easy to set the right colors for dark/light mode */} - - {stripOffProtocol(repo.url)} - -
- ), + id: repo.projectId || repo.url, + element: , isSelectable: true, } as DropDown2Element; }); @@ -130,37 +146,78 @@ export default function RepositoryFinder(props: RepositoryFinderProps) { [includeProjectsOnCreateWorkspace, suggestedRepoURLs], ); - const element = ( - {displayContextUrl(props.initialValue) || "Select a repository"}
- } - subtitle="Context URL" - loading={props.loading || isLoading} - /> - ); - - if (!props.setSelection) { - // readonly display value - return
{element}
; - } - return ( - {element} + + } + htmlTitle={displayContextUrl(props.selectedContextURL) || "Repository"} + title={ +
+ {displayContextUrl( + selectedSuggestion?.projectName || + selectedSuggestion?.repositoryName || + selectedSuggestion?.url, + ) || "Select a repository"} +
+ } + subtitle={ + // Only show the url if we have a project or repo name, otherwise it's redundant w/ the title + selectedSuggestion?.projectName || selectedSuggestion?.repositoryName + ? selectedSuggestion?.url + : undefined + } + loading={props.loading || isLoading} + />
); } +type SuggestedRepositoryOptionProps = { + repo: SuggestedRepository; +}; +const SuggestedRepositoryOption: FC = ({ repo }) => { + const name = repo.projectName || repo.repositoryName; + + return ( +
+ + + + + {name && ( + <> + {name} + + + )} + + {/* TODO: refine some Text* components a bit to make it easy to set the right colors for dark/light mode */} + + {stripOffProtocol(repo.url)} + +
+ ); +}; + function displayContextUrl(contextUrl?: string) { if (!contextUrl) { return undefined; @@ -211,3 +268,10 @@ function filterRepos(searchString: string, suggestedRepos: SuggestedRepository[] // Limit what we show to 200 results return results.length > 200 ? results.slice(0, 200) : results; } + +function stripOffProtocol(url: string): string { + if (!url.startsWith("http")) { + return url; + } + return url.substring(url.indexOf("//") + 2); +} diff --git a/components/dashboard/src/components/SelectIDEComponent.tsx b/components/dashboard/src/components/SelectIDEComponent.tsx index cf3a396158300b..7abfaa60b4cf89 100644 --- a/components/dashboard/src/components/SelectIDEComponent.tsx +++ b/components/dashboard/src/components/SelectIDEComponent.tsx @@ -142,7 +142,7 @@ const IdeOptionElementSelected: FC = ({ option, useLatest return ( {title}} diff --git a/components/dashboard/src/components/typography/text.tsx b/components/dashboard/src/components/typography/text.tsx deleted file mode 100644 index 4628aa6bc35f25..00000000000000 --- a/components/dashboard/src/components/typography/text.tsx +++ /dev/null @@ -1,16 +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 classNames from "classnames"; -import { FC, PropsWithChildren } from "react"; - -type Props = PropsWithChildren<{ - className?: string; -}>; - -export const TextLight: FC = ({ className, children }) => { - return {children}; -}; diff --git a/components/dashboard/src/start/Open.tsx b/components/dashboard/src/start/Open.tsx index 955a24790c51b8..2856369c86fd50 100644 --- a/components/dashboard/src/start/Open.tsx +++ b/components/dashboard/src/start/Open.tsx @@ -29,7 +29,7 @@ export default function Open() {

Open in Gitpod

- {}} /> + {}} />
); diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index d615b4fb74ea57..cd74c4428facf2 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -68,6 +68,10 @@ export function CreateWorkspacePage() { const [contextURL, setContextURL] = useState( StartWorkspaceOptions.parseContextUrl(location.hash), ); + // Currently this tracks if the user has selected a project from the dropdown + // Need to make sure we initialize this to a project if the url hash value maps to a project's repo url + // Will need to handle multiple projects w/ same repo url + const [selectedProjectID, setSelectedProjectID] = useState(undefined); const workspaceContext = useWorkspaceContext(contextURL); const [rememberOptions, setRememberOptions] = useState(false); const needsGitAuthorization = useNeedsGitAuthorization(); @@ -117,10 +121,18 @@ 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]); + // 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); + } + }, [project, selectedProjectID]); + // Apply project ws class settings useEffect(() => { // If URL has a ws class set, we don't override it w/ project settings @@ -147,10 +159,11 @@ export function CreateWorkspacePage() { // 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( - (newContextURL: string) => { + (newContextURL: string, projectID?: string) => { // we disable auto start if the user changes the context URL setAutostart(false); setContextURL(newContextURL); + setSelectedProjectID(projectID); history.replace(`#${newContextURL}`); }, [history], @@ -380,7 +393,8 @@ export function CreateWorkspacePage() {