Skip to content

[dashboard] support start-with-options URL #15567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions components/dashboard/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License.AGPL.txt in the project root for license information.
*/

import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, useContext, useState } from "react";
import { ContextURL, User, Team } from "@gitpod/gitpod-protocol";
import SelectIDEModal from "../settings/SelectIDEModal";
import { StartPage, StartPhase } from "../start/StartPage";
Expand Down Expand Up @@ -48,6 +48,8 @@ import { Blocked } from "./Blocked";
// TODO: Can we bundle-split/lazy load these like other pages?
import { BlockedRepositories } from "../admin/BlockedRepositories";
import PersonalAccessTokenCreateView from "../settings/PersonalAccessTokensCreateView";
import { StartWorkspaceModalContext } from "../workspaces/start-workspace-modal-context";
import { StartWorkspaceOptions } from "../start/start-workspace-options";

const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "../Setup"));
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces"));
Expand Down Expand Up @@ -94,7 +96,7 @@ type AppRoutesProps = {
};
export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) => {
const hash = getURLHash();

const { startWorkspaceModalProps, setStartWorkspaceModalProps } = useContext(StartWorkspaceModalContext);
const [isWhatsNewShown, setWhatsNewShown] = useState(shouldSeeWhatsNew(user));

// Prefix with `/#referrer` will specify an IDE for workspace
Expand Down Expand Up @@ -126,6 +128,19 @@ export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) =>
<SelectIDEModal location="workspace_start" onClose={() => setShowUserIdePreference(false)} />
</StartPage>
);
} else if (new URLSearchParams(window.location.search).has("showOptions")) {
const props = StartWorkspaceOptions.parseSearchParams(window.location.search);
return (
<StartWorkspaceModal
{...{
contextUrl: hash,
ide: props?.ideSettings?.defaultIde,
uselatestIde: props?.ideSettings?.useLatestVersion,
workspaceClass: props.workspaceClass,
onClose: undefined,
}}
/>
);
} else {
// return <div>create workspace yay {hash}</div>;
return <CreateWorkspace contextUrl={hash} />;
Expand Down Expand Up @@ -294,7 +309,12 @@ export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) =>
}}
></Route>
</Switch>
<StartWorkspaceModal />
{startWorkspaceModalProps && (
<StartWorkspaceModal
{...startWorkspaceModalProps}
onClose={startWorkspaceModalProps.onClose || (() => setStartWorkspaceModalProps(undefined))}
/>
)}
</div>
</Route>
);
Expand Down
38 changes: 24 additions & 14 deletions components/dashboard/src/components/RepositoryFinder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const LOCAL_STORAGE_KEY = "open-in-gitpod-search-data";
interface RepositoryFinderProps {
initialValue?: string;
maxDisplayItems?: number;
setSelection: (selection: string) => void;
setSelection?: (selection: string) => void;
onError?: (error: string) => void;
}

function stripOffProtocol(url: string): string {
Expand Down Expand Up @@ -67,26 +68,35 @@ export default function RepositoryFinder(props: RepositoryFinderProps) {
[suggestedContextURLs],
);

const element = (
<div className="flex h-12" title="Repository">
<div className="mx-2 my-2">
<img className="w-8 filter-grayscale self-center" src={Repository} alt="logo" />
</div>
<div className="flex-col ml-1 mt-1 flex-grow">
<div className="flex font-semibold text-gray-700">
<div className="text-gray-700 dark:text-gray-300">Context URL</div>
</div>
<div className={"flex text-xs text-gray-500 dark:text-gray-400 font-semibold "}>
{displayContextUrl(props.initialValue) || "Select a repository"}
</div>
</div>
</div>
);

if (!props.setSelection) {
// readonly display value
return <div className="m-2">{element}</div>;
}

return (
<DropDown2
getElements={getElements}
expanded={!props.initialValue}
onSelectionChange={props.setSelection}
searchPlaceholder="Paste repository URL or type to find suggestions"
>
<div className="flex h-12" title="Repository">
<div className="mx-2 my-2">
<img className="w-8 filter-grayscale self-center" src={Repository} alt="logo" />
</div>
<div className="flex-col ml-1 mt-1 flex-grow">
<div className="flex font-semibold text-gray-700">
<div className="text-gray-700 dark:text-gray-300">Repository</div>
</div>
<div className={"flex text-xs text-gray-500 dark:text-gray-400 font-semibold "}>
{displayContextUrl(props.initialValue) || "Select a repository"}
</div>
</div>
</div>
{element}
</DropDown2>
);
}
Expand Down
26 changes: 21 additions & 5 deletions components/dashboard/src/components/SelectIDEComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface SelectIDEComponentProps {
selectedIdeOption?: string;
useLatest?: boolean;
onSelectionChange: (ide: string, latest: boolean) => void;
setError?: (error?: string) => void;
}

export default function SelectIDEComponent(props: SelectIDEComponentProps) {
Expand Down Expand Up @@ -51,8 +52,20 @@ export default function SelectIDEComponent(props: SelectIDEComponentProps) {
const internalOnSelectionChange = (id: string) => {
const { ide, useLatest } = parseId(id);
props.onSelectionChange(ide, useLatest);
if (props.setError) {
props.setError(undefined);
}
};
const ide = props.selectedIdeOption || ideOptions?.defaultIde || "";
useEffect(() => {
if (!ideOptions) {
return;
}
const option = ideOptions.options[ide];
if (!option) {
props.setError?.(`The editor '${ide}' is not supported.`);
}
}, [ide, ideOptions, props]);
return (
<DropDown2
getElements={getElements}
Expand Down Expand Up @@ -80,21 +93,24 @@ function capitalize(label?: string) {
}

function IdeOptionElementSelected({ option, useLatest }: IdeOptionElementProps): JSX.Element {
let version: string | undefined, label: string | undefined, title: string;
if (!option) {
return <></>;
title = "Select Editor";
} else {
version = useLatest ? option.latestImageVersion : option.imageVersion;
label = option.type;
title = option.title;
}
const version = useLatest ? option.latestImageVersion : option.imageVersion;
const label = option.type;

return (
<div className="flex" title={option.title}>
<div className="flex" title={title}>
<div className="mx-2 my-2">
<img className="w-8 filter-grayscale self-center" src={Editor} alt="logo" />
</div>
<div className="flex-col ml-1 mt-1 flex-grow">
<div className="text-gray-700 dark:text-gray-300 font-semibold">Editor</div>
<div className="flex text-xs text-gray-500 dark:text-gray-400">
<div className="font-semibold">{option.title}</div>
<div className="font-semibold">{title}</div>
{version && (
<>
<div className="mx-1">&middot;</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
*/

import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { getGitpodService } from "../service/service";
import WorkspaceClass from "../icons/WorkspaceClass.svg";
import { DropDown2, DropDown2Element } from "./DropDown2";

interface SelectWorkspaceClassProps {
selectedWorkspaceClass?: string;
onSelectionChange: (workspaceClass: string) => void;
setError?: (error?: string) => void;
}

export default function SelectWorkspaceClassComponent(props: SelectWorkspaceClassProps) {
Expand All @@ -37,35 +38,64 @@ export default function SelectWorkspaceClassComponent(props: SelectWorkspaceClas
];
};
}, [workspaceClasses]);
useEffect(() => {
if (!workspaceClasses) {
return;
}
// if the selected workspace class is not supported, we set an error and ask the user to pick one
if (props.selectedWorkspaceClass && !workspaceClasses.find((c) => c.id === props.selectedWorkspaceClass)) {
props.setError?.(`The workspace class '${props.selectedWorkspaceClass}' is not supported.`);
}
}, [workspaceClasses, props.selectedWorkspaceClass, props.setError, props]);
const internalOnSelectionChange = useCallback(
(id: string) => {
props.onSelectionChange(id);
if (props.setError) {
props.setError(undefined);
}
},
[props],
);
const selectedWsClass = useMemo(
() =>
workspaceClasses?.find(
(ws) => ws.id === (props.selectedWorkspaceClass || workspaceClasses.find((ws) => ws.isDefault)?.id),
),
[props.selectedWorkspaceClass, workspaceClasses],
);
return (
<DropDown2
getElements={getElements}
onSelectionChange={props.onSelectionChange}
onSelectionChange={internalOnSelectionChange}
searchPlaceholder="Select class"
disableSearch={true}
>
<WorkspaceClassDropDownElementSelected
wsClass={workspaceClasses?.find(
(ws) => ws.id === (props.selectedWorkspaceClass || workspaceClasses.find((ws) => ws.isDefault)?.id),
)}
/>
<WorkspaceClassDropDownElementSelected wsClass={selectedWsClass} />
</DropDown2>
);
}

function WorkspaceClassDropDownElementSelected(props: { wsClass?: SupportedWorkspaceClass }): JSX.Element {
const c = props.wsClass;
let title = "Select class";
if (c) {
title = c.displayName;
}
return (
<div className="flex h-12" title={c?.displayName}>
<div className="flex h-12" title={title}>
<div className="mx-2 my-2">
<img className="w-8 filter-grayscale self-center" src={WorkspaceClass} alt="logo" />
</div>
<div className="flex-col ml-1 mt-1 flex-grow">
<div className="text-gray-700 dark:text-gray-300 font-semibold">Class</div>
<div className="flex text-xs text-gray-500 dark:text-gray-400">
<div className="font-semibold">{c?.displayName}</div>
<div className="mx-1">&middot;</div>
<div>{c?.description}</div>
<div className="font-semibold">{title}</div>
{c?.description && (
<>
<div className="mx-1">&middot;</div>
<div>{c?.description}</div>
</>
)}
</div>
</div>
</div>
Expand Down
20 changes: 2 additions & 18 deletions components/dashboard/src/start/CreateWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { isGitpodIo } from "../utils";
import { BillingAccountSelector } from "../components/BillingAccountSelector";
import { FeatureFlagContext } from "../contexts/FeatureFlagContext";
import { UsageLimitReachedModal } from "../components/UsageLimitReachedModal";
import { StartOptions } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
import { StartWorkspaceOptions } from "./start-workspace-options";

export interface CreateWorkspaceProps {
contextUrl: string;
Expand All @@ -40,22 +40,6 @@ export interface CreateWorkspaceState {
stillParsing: boolean;
}

function parseSearchParams(search: string): GitpodServer.StartWorkspaceOptions {
const params = new URLSearchParams(search);
const options: GitpodServer.StartWorkspaceOptions = {};
if (params.has(StartOptions.WORKSPACE_CLASS)) {
options.workspaceClass = params.get(StartOptions.WORKSPACE_CLASS)!;
}
if (params.has(StartOptions.EDITOR)) {
const useLatestVersion = params.get(StartOptions.USE_LATEST_EDITOR) === "true";
options.ideSettings = {
defaultIde: params.get(StartOptions.EDITOR)!,
useLatestVersion,
};
}
return options;
}

export default class CreateWorkspace extends React.Component<CreateWorkspaceProps, CreateWorkspaceState> {
constructor(props: CreateWorkspaceProps) {
super(props);
Expand All @@ -72,7 +56,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp

// add options from search params
const opts = options || {};
Object.assign(opts, parseSearchParams(window.location.search));
Object.assign(opts, StartWorkspaceOptions.parseSearchParams(window.location.search));
// We assume anything longer than 3 seconds is no longer just parsing the context URL (i.e. it's now creating a workspace).
let timeout = setTimeout(() => this.setState({ stillParsing: false }), 3000);

Expand Down
52 changes: 52 additions & 0 deletions components/dashboard/src/start/start-workspace-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2022 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 { GitpodServer } from "@gitpod/gitpod-protocol";

export namespace StartWorkspaceOptions {
// The workspace class to use for the workspace. If not specified, the default workspace class is used.
export const WORKSPACE_CLASS = "workspaceClass";

// The editor to use for the workspace. If not specified, the default editor is used.
export const EDITOR = "editor";

export function parseSearchParams(search: string): GitpodServer.StartWorkspaceOptions {
const params = new URLSearchParams(search);
const options: GitpodServer.StartWorkspaceOptions = {};
const workspaceClass = params.get(StartWorkspaceOptions.WORKSPACE_CLASS);
if (workspaceClass) {
options.workspaceClass = workspaceClass;
}
const editorParam = params.get(StartWorkspaceOptions.EDITOR);
if (editorParam) {
if (editorParam?.endsWith("-latest")) {
options.ideSettings = {
defaultIde: editorParam.slice(0, -7),
useLatestVersion: true,
};
} else {
options.ideSettings = {
defaultIde: editorParam,
useLatestVersion: false,
};
}
}
return options;
}

export function toSearchParams(options: GitpodServer.StartWorkspaceOptions): string {
const params = new URLSearchParams();
if (options.workspaceClass) {
params.set(StartWorkspaceOptions.WORKSPACE_CLASS, options.workspaceClass);
}
if (options.ideSettings && options.ideSettings.defaultIde) {
const ide = options.ideSettings.defaultIde;
const latest = options.ideSettings.useLatestVersion;
params.set(StartWorkspaceOptions.EDITOR, latest ? ide + "-latest" : ide);
}
return params.toString();
}
}
Loading