Skip to content

Commit 278ffca

Browse files
svenefftingeroboquat
authored andcommitted
[dashboard] allow show options on ws start
1 parent e82fe80 commit 278ffca

10 files changed

+232
-136
lines changed

components/dashboard/src/app/AppRoutes.tsx

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

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

5254
const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "../Setup"));
5355
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces"));
@@ -94,7 +96,7 @@ type AppRoutesProps = {
9496
};
9597
export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) => {
9698
const hash = getURLHash();
97-
99+
const { startWorkspaceModalProps, setStartWorkspaceModalProps } = useContext(StartWorkspaceModalContext);
98100
const [isWhatsNewShown, setWhatsNewShown] = useState(shouldSeeWhatsNew(user));
99101

100102
// Prefix with `/#referrer` will specify an IDE for workspace
@@ -126,6 +128,19 @@ export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) =>
126128
<SelectIDEModal location="workspace_start" onClose={() => setShowUserIdePreference(false)} />
127129
</StartPage>
128130
);
131+
} else if (new URLSearchParams(window.location.search).has("showOptions")) {
132+
const props = StartWorkspaceOptions.parseSearchParams(window.location.search);
133+
return (
134+
<StartWorkspaceModal
135+
{...{
136+
contextUrl: hash,
137+
ide: props?.ideSettings?.defaultIde,
138+
uselatestIde: props?.ideSettings?.useLatestVersion,
139+
workspaceClass: props.workspaceClass,
140+
onClose: undefined,
141+
}}
142+
/>
143+
);
129144
} else {
130145
// return <div>create workspace yay {hash}</div>;
131146
return <CreateWorkspace contextUrl={hash} />;
@@ -294,7 +309,12 @@ export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) =>
294309
}}
295310
></Route>
296311
</Switch>
297-
<StartWorkspaceModal />
312+
{startWorkspaceModalProps && (
313+
<StartWorkspaceModal
314+
{...startWorkspaceModalProps}
315+
onClose={startWorkspaceModalProps.onClose || (() => setStartWorkspaceModalProps(undefined))}
316+
/>
317+
)}
298318
</div>
299319
</Route>
300320
);

components/dashboard/src/components/RepositoryFinder.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const LOCAL_STORAGE_KEY = "open-in-gitpod-search-data";
1414
interface RepositoryFinderProps {
1515
initialValue?: string;
1616
maxDisplayItems?: number;
17-
setSelection: (selection: string) => void;
17+
setSelection?: (selection: string) => void;
18+
onError?: (error: string) => void;
1819
}
1920

2021
function stripOffProtocol(url: string): string {
@@ -67,26 +68,35 @@ export default function RepositoryFinder(props: RepositoryFinderProps) {
6768
[suggestedContextURLs],
6869
);
6970

71+
const element = (
72+
<div className="flex h-12" title="Repository">
73+
<div className="mx-2 my-2">
74+
<img className="w-8 filter-grayscale self-center" src={Repository} alt="logo" />
75+
</div>
76+
<div className="flex-col ml-1 mt-1 flex-grow">
77+
<div className="flex font-semibold text-gray-700">
78+
<div className="text-gray-700 dark:text-gray-300">Context URL</div>
79+
</div>
80+
<div className={"flex text-xs text-gray-500 dark:text-gray-400 font-semibold "}>
81+
{displayContextUrl(props.initialValue) || "Select a repository"}
82+
</div>
83+
</div>
84+
</div>
85+
);
86+
87+
if (!props.setSelection) {
88+
// readonly display value
89+
return <div className="m-2">{element}</div>;
90+
}
91+
7092
return (
7193
<DropDown2
7294
getElements={getElements}
7395
expanded={!props.initialValue}
7496
onSelectionChange={props.setSelection}
7597
searchPlaceholder="Paste repository URL or type to find suggestions"
7698
>
77-
<div className="flex h-12" title="Repository">
78-
<div className="mx-2 my-2">
79-
<img className="w-8 filter-grayscale self-center" src={Repository} alt="logo" />
80-
</div>
81-
<div className="flex-col ml-1 mt-1 flex-grow">
82-
<div className="flex font-semibold text-gray-700">
83-
<div className="text-gray-700 dark:text-gray-300">Repository</div>
84-
</div>
85-
<div className={"flex text-xs text-gray-500 dark:text-gray-400 font-semibold "}>
86-
{displayContextUrl(props.initialValue) || "Select a repository"}
87-
</div>
88-
</div>
89-
</div>
99+
{element}
90100
</DropDown2>
91101
);
92102
}

components/dashboard/src/components/SelectIDEComponent.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface SelectIDEComponentProps {
1414
selectedIdeOption?: string;
1515
useLatest?: boolean;
1616
onSelectionChange: (ide: string, latest: boolean) => void;
17+
setError?: (error?: string) => void;
1718
}
1819

1920
export default function SelectIDEComponent(props: SelectIDEComponentProps) {
@@ -51,8 +52,20 @@ export default function SelectIDEComponent(props: SelectIDEComponentProps) {
5152
const internalOnSelectionChange = (id: string) => {
5253
const { ide, useLatest } = parseId(id);
5354
props.onSelectionChange(ide, useLatest);
55+
if (props.setError) {
56+
props.setError(undefined);
57+
}
5458
};
5559
const ide = props.selectedIdeOption || ideOptions?.defaultIde || "";
60+
useEffect(() => {
61+
if (!ideOptions) {
62+
return;
63+
}
64+
const option = ideOptions.options[ide];
65+
if (!option) {
66+
props.setError?.(`The editor '${ide}' is not supported.`);
67+
}
68+
}, [ide, ideOptions, props]);
5669
return (
5770
<DropDown2
5871
getElements={getElements}
@@ -80,21 +93,24 @@ function capitalize(label?: string) {
8093
}
8194

8295
function IdeOptionElementSelected({ option, useLatest }: IdeOptionElementProps): JSX.Element {
96+
let version: string | undefined, label: string | undefined, title: string;
8397
if (!option) {
84-
return <></>;
98+
title = "Select Editor";
99+
} else {
100+
version = useLatest ? option.latestImageVersion : option.imageVersion;
101+
label = option.type;
102+
title = option.title;
85103
}
86-
const version = useLatest ? option.latestImageVersion : option.imageVersion;
87-
const label = option.type;
88104

89105
return (
90-
<div className="flex" title={option.title}>
106+
<div className="flex" title={title}>
91107
<div className="mx-2 my-2">
92108
<img className="w-8 filter-grayscale self-center" src={Editor} alt="logo" />
93109
</div>
94110
<div className="flex-col ml-1 mt-1 flex-grow">
95111
<div className="text-gray-700 dark:text-gray-300 font-semibold">Editor</div>
96112
<div className="flex text-xs text-gray-500 dark:text-gray-400">
97-
<div className="font-semibold">{option.title}</div>
113+
<div className="font-semibold">{title}</div>
98114
{version && (
99115
<>
100116
<div className="mx-1">&middot;</div>

components/dashboard/src/components/SelectWorkspaceClassComponent.tsx

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
*/
66

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

1313
interface SelectWorkspaceClassProps {
1414
selectedWorkspaceClass?: string;
1515
onSelectionChange: (workspaceClass: string) => void;
16+
setError?: (error?: string) => void;
1617
}
1718

1819
export default function SelectWorkspaceClassComponent(props: SelectWorkspaceClassProps) {
@@ -37,35 +38,64 @@ export default function SelectWorkspaceClassComponent(props: SelectWorkspaceClas
3738
];
3839
};
3940
}, [workspaceClasses]);
41+
useEffect(() => {
42+
if (!workspaceClasses) {
43+
return;
44+
}
45+
// if the selected workspace class is not supported, we set an error and ask the user to pick one
46+
if (props.selectedWorkspaceClass && !workspaceClasses.find((c) => c.id === props.selectedWorkspaceClass)) {
47+
props.setError?.(`The workspace class '${props.selectedWorkspaceClass}' is not supported.`);
48+
}
49+
}, [workspaceClasses, props.selectedWorkspaceClass, props.setError, props]);
50+
const internalOnSelectionChange = useCallback(
51+
(id: string) => {
52+
props.onSelectionChange(id);
53+
if (props.setError) {
54+
props.setError(undefined);
55+
}
56+
},
57+
[props],
58+
);
59+
const selectedWsClass = useMemo(
60+
() =>
61+
workspaceClasses?.find(
62+
(ws) => ws.id === (props.selectedWorkspaceClass || workspaceClasses.find((ws) => ws.isDefault)?.id),
63+
),
64+
[props.selectedWorkspaceClass, workspaceClasses],
65+
);
4066
return (
4167
<DropDown2
4268
getElements={getElements}
43-
onSelectionChange={props.onSelectionChange}
69+
onSelectionChange={internalOnSelectionChange}
4470
searchPlaceholder="Select class"
4571
disableSearch={true}
4672
>
47-
<WorkspaceClassDropDownElementSelected
48-
wsClass={workspaceClasses?.find(
49-
(ws) => ws.id === (props.selectedWorkspaceClass || workspaceClasses.find((ws) => ws.isDefault)?.id),
50-
)}
51-
/>
73+
<WorkspaceClassDropDownElementSelected wsClass={selectedWsClass} />
5274
</DropDown2>
5375
);
5476
}
5577

5678
function WorkspaceClassDropDownElementSelected(props: { wsClass?: SupportedWorkspaceClass }): JSX.Element {
5779
const c = props.wsClass;
80+
let title = "Select class";
81+
if (c) {
82+
title = c.displayName;
83+
}
5884
return (
59-
<div className="flex h-12" title={c?.displayName}>
85+
<div className="flex h-12" title={title}>
6086
<div className="mx-2 my-2">
6187
<img className="w-8 filter-grayscale self-center" src={WorkspaceClass} alt="logo" />
6288
</div>
6389
<div className="flex-col ml-1 mt-1 flex-grow">
6490
<div className="text-gray-700 dark:text-gray-300 font-semibold">Class</div>
6591
<div className="flex text-xs text-gray-500 dark:text-gray-400">
66-
<div className="font-semibold">{c?.displayName}</div>
67-
<div className="mx-1">&middot;</div>
68-
<div>{c?.description}</div>
92+
<div className="font-semibold">{title}</div>
93+
{c?.description && (
94+
<>
95+
<div className="mx-1">&middot;</div>
96+
<div>{c?.description}</div>
97+
</>
98+
)}
6999
</div>
70100
</div>
71101
</div>

components/dashboard/src/start/CreateWorkspace.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { isGitpodIo } from "../utils";
2727
import { BillingAccountSelector } from "../components/BillingAccountSelector";
2828
import { FeatureFlagContext } from "../contexts/FeatureFlagContext";
2929
import { UsageLimitReachedModal } from "../components/UsageLimitReachedModal";
30-
import { StartOptions } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
30+
import { StartWorkspaceOptions } from "./start-workspace-options";
3131

3232
export interface CreateWorkspaceProps {
3333
contextUrl: string;
@@ -40,22 +40,6 @@ export interface CreateWorkspaceState {
4040
stillParsing: boolean;
4141
}
4242

43-
function parseSearchParams(search: string): GitpodServer.StartWorkspaceOptions {
44-
const params = new URLSearchParams(search);
45-
const options: GitpodServer.StartWorkspaceOptions = {};
46-
if (params.has(StartOptions.WORKSPACE_CLASS)) {
47-
options.workspaceClass = params.get(StartOptions.WORKSPACE_CLASS)!;
48-
}
49-
if (params.has(StartOptions.EDITOR)) {
50-
const useLatestVersion = params.get(StartOptions.USE_LATEST_EDITOR) === "true";
51-
options.ideSettings = {
52-
defaultIde: params.get(StartOptions.EDITOR)!,
53-
useLatestVersion,
54-
};
55-
}
56-
return options;
57-
}
58-
5943
export default class CreateWorkspace extends React.Component<CreateWorkspaceProps, CreateWorkspaceState> {
6044
constructor(props: CreateWorkspaceProps) {
6145
super(props);
@@ -72,7 +56,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
7256

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { GitpodServer } from "@gitpod/gitpod-protocol";
8+
9+
export namespace StartWorkspaceOptions {
10+
// The workspace class to use for the workspace. If not specified, the default workspace class is used.
11+
export const WORKSPACE_CLASS = "workspaceClass";
12+
13+
// The editor to use for the workspace. If not specified, the default editor is used.
14+
export const EDITOR = "editor";
15+
16+
export function parseSearchParams(search: string): GitpodServer.StartWorkspaceOptions {
17+
const params = new URLSearchParams(search);
18+
const options: GitpodServer.StartWorkspaceOptions = {};
19+
const workspaceClass = params.get(StartWorkspaceOptions.WORKSPACE_CLASS);
20+
if (workspaceClass) {
21+
options.workspaceClass = workspaceClass;
22+
}
23+
const editorParam = params.get(StartWorkspaceOptions.EDITOR);
24+
if (editorParam) {
25+
if (editorParam?.endsWith("-latest")) {
26+
options.ideSettings = {
27+
defaultIde: editorParam.slice(0, -7),
28+
useLatestVersion: true,
29+
};
30+
} else {
31+
options.ideSettings = {
32+
defaultIde: editorParam,
33+
useLatestVersion: false,
34+
};
35+
}
36+
}
37+
return options;
38+
}
39+
40+
export function toSearchParams(options: GitpodServer.StartWorkspaceOptions): string {
41+
const params = new URLSearchParams();
42+
if (options.workspaceClass) {
43+
params.set(StartWorkspaceOptions.WORKSPACE_CLASS, options.workspaceClass);
44+
}
45+
if (options.ideSettings && options.ideSettings.defaultIde) {
46+
const ide = options.ideSettings.defaultIde;
47+
const latest = options.ideSettings.useLatestVersion;
48+
params.set(StartWorkspaceOptions.EDITOR, latest ? ide + "-latest" : ide);
49+
}
50+
return params.toString();
51+
}
52+
}

0 commit comments

Comments
 (0)