Skip to content

Commit e56ef18

Browse files
committed
wip
1 parent 6813b77 commit e56ef18

File tree

4 files changed

+295
-2
lines changed

4 files changed

+295
-2
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 { useMemo, useState } from "react";
8+
import Arrow from "./Arrow";
9+
10+
export interface DropDown2Element {
11+
id: string;
12+
elementInDropDown?: JSX.Element;
13+
element: JSX.Element;
14+
searchableString?: string;
15+
isSelectable?: boolean;
16+
}
17+
18+
export interface DropDown2Props {
19+
elements: DropDown2Element[];
20+
selectedElement?: string;
21+
searchable?: boolean;
22+
onSelectionChange: (id: string) => void;
23+
}
24+
25+
export default function DropDown2(props: DropDown2Props) {
26+
const [selectedElement, setSelectedElement] = useState<string>(props.selectedElement || props.elements[0].id);
27+
const [showDropDown, setShowDropDown] = useState<boolean>(false);
28+
const onSelected = useMemo(
29+
() => (elementId: string) => {
30+
props.onSelectionChange(elementId);
31+
setSelectedElement(elementId);
32+
setShowDropDown(false);
33+
},
34+
[props],
35+
);
36+
const doShowDropDown = useMemo(() => () => setShowDropDown(true), []);
37+
if (props.elements.length === 0) {
38+
return <></>;
39+
}
40+
return (
41+
<div>
42+
{showDropDown ? (
43+
<div
44+
onKeyDown={(e) => {
45+
if (e.key === "Escape") {
46+
setShowDropDown(false);
47+
e.preventDefault();
48+
}
49+
}}
50+
>
51+
<DropDownMenu {...props} onSelected={onSelected} />
52+
</div>
53+
) : (
54+
<div
55+
className="rounded-xl hover:bg-gray-400 dark:hover:bg-gray-700 cursor-pointer flex items-center px-2"
56+
onClick={doShowDropDown}
57+
>
58+
{props.elements.find((e) => e.id === selectedElement)?.element}
59+
<div className="flex-grow" />
60+
<div>
61+
<Arrow direction={"down"} />
62+
</div>
63+
</div>
64+
)}
65+
</div>
66+
);
67+
}
68+
69+
interface DropDownMenuProps extends DropDown2Props {
70+
onSelected: (ide: string) => void;
71+
}
72+
73+
function DropDownMenu(props: DropDownMenuProps): JSX.Element {
74+
const { elements, selectedElement, searchable, onSelected } = props;
75+
const [search, setSearch] = useState<string>("");
76+
const filteredOptions = useMemo(
77+
() =>
78+
elements.filter(
79+
(o) => !o.isSelectable || (o.searchableString || "").toLowerCase().indexOf(search.toLowerCase()) !== -1,
80+
),
81+
[search, elements],
82+
);
83+
return (
84+
<div className="relative flex flex-col">
85+
{searchable && (
86+
<input
87+
type="text"
88+
autoFocus
89+
className="w-full focus"
90+
placeholder="Search IDE"
91+
value={search}
92+
onChange={(e) => setSearch(e.target.value)}
93+
/>
94+
)}
95+
<div className="absolute w-full top-11 bg-gray-900 max-h-72 overflow-auto rounded-xl mt-3">
96+
{filteredOptions.map((element) => {
97+
const selected = element.id === selectedElement;
98+
99+
let selectionClasses = `hover:bg-gray-400 dark:hover:bg-gray-700 cursor-pointer`;
100+
if (selected) {
101+
selectionClasses = `bg-gray-300 dark:bg-gray-800`;
102+
}
103+
if (!element.isSelectable) {
104+
selectionClasses = ``;
105+
}
106+
return (
107+
<div
108+
key={element.id}
109+
className={"rounded-md " + selectionClasses}
110+
onClick={() => element.isSelectable && onSelected(element.id)}
111+
>
112+
{element.elementInDropDown || element.element}
113+
</div>
114+
);
115+
})}
116+
</div>
117+
</div>
118+
);
119+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
8+
import { useContext, useEffect, useState } from "react";
9+
import { getGitpodService } from "../service/service";
10+
import { UserContext } from "../user-context";
11+
import CheckBox from "./CheckBox";
12+
import DropDown2, { DropDown2Element } from "./DropDown2";
13+
14+
interface SelectIDEComponentProps {
15+
ideOptions?: IDEOptions;
16+
selectedIdeOption?: string;
17+
useLatest?: boolean;
18+
setUseLatest?: (useLatestVersion: boolean) => void;
19+
onSelectionChange: (ide: string) => void;
20+
}
21+
22+
export default function SelectIDEComponent(props: SelectIDEComponentProps) {
23+
const { user } = useContext(UserContext);
24+
function options2Elements(
25+
ideOptions?: IDEOptions,
26+
useLatest?: boolean,
27+
setUseLatest?: (useLatest: boolean) => void,
28+
): DropDown2Element[] {
29+
if (!ideOptions) {
30+
return [];
31+
}
32+
return [
33+
...IDEOptions.asArray(ideOptions).map((ide) => ({
34+
id: ide.id,
35+
element: <IdeOptionElementSelected option={ide} useLatest={!!useLatest} />,
36+
elementInDropDown: <IdeOptionElementInDropDown option={ide} useLatest={!!useLatest} />,
37+
searchableString: `${ide.label}${ide.title}${ide.notes}${ide.id}`,
38+
isSelectable: true,
39+
})),
40+
{
41+
id: "non-selectable-checkbox",
42+
element: (
43+
<div className="ml-3">
44+
<CheckBox
45+
title="Use latest"
46+
desc="Use latest version of IDE"
47+
checked={!!useLatest}
48+
onChange={(e) => setUseLatest && setUseLatest(e.target.checked)}
49+
/>
50+
</div>
51+
),
52+
isSelectable: false,
53+
},
54+
];
55+
}
56+
57+
const [elements, setElements] = useState<DropDown2Element[]>(
58+
options2Elements(props.ideOptions, props.useLatest, props.setUseLatest),
59+
);
60+
const [selectedIdeOption, setSelectedIdeOption] = useState<string | undefined>(
61+
props.selectedIdeOption || user?.additionalData?.ideSettings?.defaultIde,
62+
);
63+
useEffect(() => {
64+
(async () => {
65+
let options = props.ideOptions;
66+
if (!options) {
67+
options = await getGitpodService().server.getIDEOptions();
68+
}
69+
setElements(options2Elements(options, props.useLatest, props.setUseLatest));
70+
if (!selectedIdeOption || !options.options[selectedIdeOption]) {
71+
setSelectedIdeOption(options.defaultIde);
72+
}
73+
})();
74+
}, [props.ideOptions, selectedIdeOption, props.useLatest, props.setUseLatest]);
75+
return (
76+
<DropDown2
77+
elements={elements}
78+
onSelectionChange={props.onSelectionChange}
79+
searchable={true}
80+
selectedElement={selectedIdeOption}
81+
/>
82+
);
83+
}
84+
85+
interface IdeOptionElementProps {
86+
option: IDEOption | undefined;
87+
useLatest: boolean;
88+
}
89+
90+
function IdeOptionElementSelected(p: IdeOptionElementProps): JSX.Element {
91+
const { option, useLatest } = p;
92+
if (!option) {
93+
return <></>;
94+
}
95+
const version = useLatest ? option.latestImageVersion : option.imageVersion;
96+
const label = option.type === "desktop" ? "" : option.type;
97+
98+
return (
99+
<div className="flex" title={option.title}>
100+
<div className="mx-2 my-3">
101+
<img className="w-8 filter-grayscale self-center" src={option.logo} alt="logo" />
102+
</div>
103+
<div className="flex-col ml-1 mt-1 flex-grow">
104+
<div className="flex">Editor</div>
105+
<div className="flex text-sm text-gray-100 dark:text-gray-600">
106+
<div>{option.title}</div>
107+
<div className="ml-1">{version}</div>
108+
<div className="ml-1">
109+
{label ? (
110+
<span className={`font-semibold text-sm text-gray-600 dark:text-gray-500"}`}>
111+
{label[0].toLocaleUpperCase() + label.slice(1)}
112+
</span>
113+
) : (
114+
<></>
115+
)}
116+
</div>
117+
</div>
118+
</div>
119+
</div>
120+
);
121+
}
122+
123+
function IdeOptionElementInDropDown(p: IdeOptionElementProps): JSX.Element {
124+
const { option, useLatest } = p;
125+
if (!option) {
126+
return <></>;
127+
}
128+
const version = useLatest ? option.latestImageVersion : option.imageVersion;
129+
const label = option.type === "desktop" ? "" : option.type;
130+
131+
return (
132+
<div className="flex" title={option.title}>
133+
<div className="mx-2 my-3">
134+
<img className="w-8 filter-grayscale self-center" src={option.logo} alt="logo" />
135+
</div>
136+
<div className="flex-col ml-1 mt-1 flex-grow">
137+
<div className="flex">
138+
<div>{option.title}</div>
139+
<div className="ml-1 text-gray-100 dark:text-gray-600">{version}</div>
140+
</div>
141+
<div className="">
142+
{label ? (
143+
<span className={`font-semibold text-sm text-gray-600 dark:text-gray-500"}`}>
144+
{label[0].toLocaleUpperCase() + label.slice(1)}
145+
</span>
146+
) : (
147+
<></>
148+
)}
149+
</div>
150+
</div>
151+
</div>
152+
);
153+
}

components/dashboard/src/workspaces/StartWorkspaceModal.tsx

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

7-
import { useContext, useEffect } from "react";
7+
import { useContext, useEffect, useState } from "react";
88
import { useLocation } from "react-router";
99
import Modal from "../components/Modal";
1010
import RepositoryFinder from "../components/RepositoryFinder";
11+
import SelectIDEComponent from "../components/SelectIDEComponent";
12+
import { UserContext } from "../user-context";
1113
import { StartWorkspaceModalContext } from "./start-workspace-modal-context";
1214

1315
export function StartWorkspaceModal() {
16+
const { user } = useContext(UserContext);
1417
const { isStartWorkspaceModalVisible, setIsStartWorkspaceModalVisible } = useContext(StartWorkspaceModalContext);
1518
const location = useLocation();
19+
const [useLatestIde, setUseLatestIde] = useState(!!user?.additionalData?.ideSettings?.useLatestVersion);
1620

1721
// Close the modal on navigation events.
1822
useEffect(() => {
@@ -26,10 +30,21 @@ export function StartWorkspaceModal() {
2630
onEnter={() => false}
2731
visible={!!isStartWorkspaceModalVisible}
2832
>
29-
<h3 className="pb-2">Open in Gitpod</h3>
33+
<h3 className="pb-2">New Workspace</h3>
3034
<div className="border-t border-gray-200 dark:border-gray-800 mt-2 -mx-6 px-6 pt-4">
3135
<RepositoryFinder />
3236
</div>
37+
38+
<div className="border-t border-gray-200 dark:border-gray-800 mt-2 -mx-6 px-6 pt-4">
39+
<div className="text-sm text-gray-100 dark:text-gray-600">Configure Workspace</div>
40+
<SelectIDEComponent
41+
onSelectionChange={(e) => {
42+
console.log(e);
43+
}}
44+
useLatest={useLatestIde}
45+
setUseLatest={setUseLatestIde}
46+
/>
47+
</div>
3348
</Modal>
3449
);
3550
}

components/gitpod-protocol/src/ide-protocol.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export interface IDEOptions {
3636
clients?: { [id: string]: IDEClient };
3737
}
3838

39+
export namespace IDEOptions {
40+
export function asArray(options: IDEOptions): (IDEOption & { id: string })[] {
41+
return Object.keys(options.options).map((id) => ({ ...options.options[id], id }));
42+
}
43+
}
44+
3945
export interface IDEClient {
4046
/**
4147
* The default desktop IDE when the user has not specified one.

0 commit comments

Comments
 (0)