diff --git a/chart/templates/server-ide-configmap.yaml b/chart/templates/server-ide-configmap.yaml index cefbee02164393..35b9e311c4d680 100644 --- a/chart/templates/server-ide-configmap.yaml +++ b/chart/templates/server-ide-configmap.yaml @@ -46,17 +46,10 @@ options: orderKey: "00" title: "VS Code" type: "browser" + label: "Browser" logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/vscode.svg" image: {{ (include "stable-image-full" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.codeImage)) }} - code-latest: - orderKey: "01" - title: "VS Code" - type: "browser" - logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/vscodeInsiders.svg" - tooltip: "Early access version, still subject to testing." - label: "Insiders" - image: {{ (include "insider-image-full" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.codeImage)) }} - resolveImageDigest: true + latestImage: {{ (include "insider-image-full" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.codeImage)) }} # Desktop IDEs code-desktop: @@ -65,14 +58,7 @@ options: type: "desktop" logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/vscode.svg" image: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.codeDesktop)) }} - code-desktop-insiders: - orderKey: "03" - title: "VS Code" - type: "desktop" - logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/vscodeInsiders.svg" - tooltip: "Visual Studio Code Insiders for early adopters." - label: "Insiders" - image: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.codeDesktopInsiders)) }} + latestImage: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.codeDesktopInsiders)) }} intellij: orderKey: "04" title: "IntelliJ IDEA" @@ -113,8 +99,8 @@ clients: "If you don't see an open dialog in your browser, make sure you have VS Code installed on your machine, and then click ${OPEN_LINK_LABEL} below.", ] vscode-insiders: - defaultDesktopIDE: "code-desktop-insiders" - desktopIDEs: ["code-desktop-insiders"] + defaultDesktopIDE: "code-desktop" + desktopIDEs: ["code-desktop"] installationSteps: [ "If you don't see an open dialog in your browser, make sure you have VS Code Insiders installed on your machine, and then click ${OPEN_LINK_LABEL} below.", ] diff --git a/components/dashboard/src/components/Alert.tsx b/components/dashboard/src/components/Alert.tsx new file mode 100644 index 00000000000000..e93dda95ae0594 --- /dev/null +++ b/components/dashboard/src/components/Alert.tsx @@ -0,0 +1,86 @@ +/** + * 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 { useState } from "react"; +import { ReactComponent as Exclamation } from "../images/exclamation.svg"; +import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg"; +import { ReactComponent as InfoSvg } from "../images/info.svg"; +import { ReactComponent as XSvg } from "../images/x.svg"; + +export type AlertType = + // Yellow + | "warning" + // Gray alert + | "info" + // Red + | "error" + // Blue + | "message"; + +export interface AlertProps { + className?: string; + type?: AlertType; + // Without background color, default false + light?: boolean; + closable?: boolean; + showIcon?: boolean; + icon?: React.ReactNode; + children?: React.ReactNode; +} + +interface AlertInfo { + bgCls: string; + txtCls: string; + icon: React.ReactNode; + iconColor?: string; +} + +const infoMap: Record = { + warning: { + bgCls: "bg-yellow-100 dark:bg-yellow-700", + txtCls: "text-yellow-600 dark:text-yellow-50", + icon: , + iconColor: "text-yellow-400 dark:text-yellow-900", + }, + info: { + bgCls: "bg-gray-100 dark:bg-gray-700", + txtCls: "text-gray-500 dark:text-gray-300", + icon: , + iconColor: "text-gray-400", + }, + message: { + bgCls: "bg-blue-50 dark:bg-blue-700", + txtCls: "text-blue-800 dark:text-blue-100", + icon: , + iconColor: "text-blue-400", + }, + error: { + bgCls: "bg-red-50 dark:bg-red-800 dark:bg-opacity-50", + txtCls: "text-red-600 dark:text-red-200", + icon: , + iconColor: "text-red-400", + }, +}; + +export default function Alert(props: AlertProps) { + const [visible, setVisible] = useState(true); + if (!visible) { + return null; + } + const type: AlertType = props.type ?? "info"; + const info = infoMap[type]; + const showIcon = props.showIcon ?? true; + const light = props.light ?? false; + return ( +
+ {showIcon && {props.icon ?? info.icon}} + {props.children} + {props.closable && ( + setVisible(false)} className="mt-1 ml-4 w-3 h-3 cursor-pointer"> + )} +
+ ); +} diff --git a/components/dashboard/src/components/SelectableCardSolid.tsx b/components/dashboard/src/components/SelectableCardSolid.tsx new file mode 100644 index 00000000000000..78c4be12fa6c6f --- /dev/null +++ b/components/dashboard/src/components/SelectableCardSolid.tsx @@ -0,0 +1,41 @@ +/** + * 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. + */ + +export interface SelectableCardSolidProps { + title: string; + selected: boolean; + className?: string; + onClick: () => void; + children?: React.ReactNode; +} + +function SelectableCardSolid(props: SelectableCardSolidProps) { + return ( +
+
+

+ {props.title} +

+ +
+ {props.children} +
+ ); +} + +export default SelectableCardSolid; diff --git a/components/dashboard/src/images/exclamation2.svg b/components/dashboard/src/images/exclamation2.svg new file mode 100644 index 00000000000000..074304b68a5d07 --- /dev/null +++ b/components/dashboard/src/images/exclamation2.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/dashboard/src/images/info.svg b/components/dashboard/src/images/info.svg index 7b7d5b337adb58..b92a31baca697b 100644 --- a/components/dashboard/src/images/info.svg +++ b/components/dashboard/src/images/info.svg @@ -1,3 +1,3 @@ - - + + diff --git a/components/dashboard/src/images/x.svg b/components/dashboard/src/images/x.svg new file mode 100644 index 00000000000000..df1083f8e9bdd4 --- /dev/null +++ b/components/dashboard/src/images/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/dashboard/src/settings/Preferences.tsx b/components/dashboard/src/settings/Preferences.tsx index 32156a5e4e950e..d628759f1dcb02 100644 --- a/components/dashboard/src/settings/Preferences.tsx +++ b/components/dashboard/src/settings/Preferences.tsx @@ -10,52 +10,62 @@ import InfoBox from "../components/InfoBox"; import { PageWithSubMenu } from "../components/PageWithSubMenu"; import PillLabel from "../components/PillLabel"; import SelectableCard from "../components/SelectableCard"; +import SelectableCardSolid from "../components/SelectableCardSolid"; import Tooltip from "../components/Tooltip"; import { getGitpodService } from "../service/service"; import { ThemeContext } from "../theme-context"; import { UserContext } from "../user-context"; import getSettingsMenu from "./settings-menu"; -import IDENone from "../icons/IDENone.svg"; -import IDENoneDark from "../icons/IDENoneDark.svg"; -import CheckBox from "../components/CheckBox"; import { trackEvent } from "../Analytics"; import { PaymentContext } from "../payment-context"; +import CheckBox from "../components/CheckBox"; +import { IDESettings } from "@gitpod/gitpod-protocol"; type Theme = "light" | "dark" | "system"; -const DesktopNoneId = "none"; -const DesktopNone: IDEOption = { - image: "", - logo: IDENone, - orderKey: "-1", - title: "None", - type: "desktop", -}; - export default function Preferences() { const { user } = useContext(UserContext); const { showPaymentUI } = useContext(PaymentContext); - const { setIsDark, isDark } = useContext(ThemeContext); + const { setIsDark } = useContext(ThemeContext); - const updateUserIDEInfo = async (defaultDesktopIde: string, defaultIde: string, useLatestVersion: boolean) => { - const useDesktopIde = defaultDesktopIde !== DesktopNoneId; - const desktopIde = useDesktopIde ? defaultDesktopIde : undefined; + const migrationIDESettings = () => { + if (!user?.additionalData?.ideSettings || user.additionalData.ideSettings.settingVersion === "2.0") { + return; + } + const newIDESettings: IDESettings = { + settingVersion: "2.0", + }; + const ideSettings = user.additionalData.ideSettings; + if (ideSettings.useDesktopIde) { + if (ideSettings.defaultDesktopIde === "code-desktop") { + newIDESettings.defaultIde = "code-desktop"; + } else if (ideSettings.defaultDesktopIde === "code-desktop-insiders") { + newIDESettings.defaultIde = "code-desktop"; + newIDESettings.useLatestVersion = true; + } else { + newIDESettings.defaultIde = ideSettings.defaultDesktopIde; + newIDESettings.useLatestVersion = ideSettings.useLatestVersion; + } + } else { + const useLatest = ideSettings.defaultIde === "code-latest"; + newIDESettings.defaultIde = "code"; + newIDESettings.useLatestVersion = useLatest; + } + user.additionalData.ideSettings = newIDESettings; + }; + migrationIDESettings(); + + const updateUserIDEInfo = async (selectedIde: string, useLatestVersion: boolean) => { const additionalData = user?.additionalData ?? {}; const settings = additionalData.ideSettings ?? {}; - settings.useDesktopIde = useDesktopIde; - settings.defaultIde = defaultIde; - settings.defaultDesktopIde = desktopIde; + settings.settingVersion = "2.0"; + settings.defaultIde = selectedIde; settings.useLatestVersion = useLatestVersion; additionalData.ideSettings = settings; getGitpodService() .server.trackEvent({ event: "ide_configuration_changed", - properties: { - useDesktopIde, - defaultIde, - defaultDesktopIde: desktopIde, - useLatestVersion, - }, + properties: settings, }) .then() .catch(console.error); @@ -64,24 +74,15 @@ export default function Preferences() { const [defaultIde, setDefaultIde] = useState(user?.additionalData?.ideSettings?.defaultIde || ""); const actuallySetDefaultIde = async (value: string) => { - await updateUserIDEInfo(defaultDesktopIde, value, useLatestVersion); + await updateUserIDEInfo(value, useLatestVersion); setDefaultIde(value); }; - const [defaultDesktopIde, setDefaultDesktopIde] = useState( - (user?.additionalData?.ideSettings?.useDesktopIde && user?.additionalData?.ideSettings?.defaultDesktopIde) || - DesktopNoneId, - ); - const actuallySetDefaultDesktopIde = async (value: string) => { - await updateUserIDEInfo(value, defaultIde, useLatestVersion); - setDefaultDesktopIde(value); - }; - const [useLatestVersion, setUseLatestVersion] = useState( user?.additionalData?.ideSettings?.useLatestVersion ?? false, ); const actuallySetUseLatestVersion = async (value: boolean) => { - await updateUserIDEInfo(defaultDesktopIde, defaultIde, value); + await updateUserIDEInfo(defaultIde, value); setUseLatestVersion(value); }; @@ -89,14 +90,14 @@ export default function Preferences() { useEffect(() => { (async () => { const ideopts = await getGitpodService().server.getIDEOptions(); - ideopts.options[DesktopNoneId] = DesktopNone; + // TODO: Compatible with ide-config not deployed, need revert after ide-config deployed + delete ideopts.options["code-latest"]; + delete ideopts.options["code-desktop-insiders"]; + setIdeOptions(ideopts); - if (!defaultIde) { + if (!defaultIde || ideopts.options[defaultIde] == null) { setDefaultIde(ideopts.defaultIde); } - if (!defaultDesktopIde) { - setDefaultDesktopIde(ideopts.defaultDesktopIde); - } })(); }, []); @@ -114,8 +115,7 @@ export default function Preferences() { setTheme(theme); }; - const browserIdeOptions = ideOptions && orderedIdeOptions(ideOptions, "browser"); - const desktopIdeOptions = ideOptions && orderedIdeOptions(ideOptions, "desktop"); + const allIdeOptions = ideOptions && orderedIdeOptions(ideOptions); const [dotfileRepo, setDotfileRepo] = useState(user?.additionalData?.dotfileRepo || ""); const actuallySetDotfileRepo = async (value: string) => { @@ -138,14 +138,14 @@ export default function Preferences() { > {ideOptions && ( <> - {browserIdeOptions && ( + {allIdeOptions && ( <> -

Browser Editor

+

Editor

- Choose the default editor for opening workspaces in the browser. + Choose the editor for opening workspaces.

-
- {browserIdeOptions.map(([id, option]) => { +
+ {allIdeOptions.map(([id, option]) => { const selected = defaultIde === id; const onSelect = () => actuallySetDefaultIde(id); return renderIdeOption(option, selected, onSelect); @@ -160,38 +160,6 @@ export default function Preferences() { )} - - )} - {desktopIdeOptions && ( - <> -

- Desktop Editor - - Beta - -

-

- Optionally, choose the default desktop editor for opening workspaces. -

-
- {desktopIdeOptions.map(([id, option]) => { - const selected = defaultDesktopIde === id; - const onSelect = () => actuallySetDefaultDesktopIde(id); - if (id === DesktopNoneId) { - option.logo = isDark ? IDENoneDark : IDENone; - } - return renderIdeOption(option, selected, onSelect); - })} -
- {ideOptions.options[defaultDesktopIde]?.notes && ( - -
    - {ideOptions.options[defaultDesktopIde].notes?.map((x, idx) => ( -
  • 0 ? "mt-2" : ""}>{x}
  • - ))} -
-
- )}

The JetBrains desktop IDEs are currently in beta.{" "} )} + Use the latest version for each editor.{" "} + + Insiders + {" "} + for VS Code,{" "} + + EAP + {" "} + for JetBrains IDEs. + + } checked={useLatestVersion} onChange={(e) => actuallySetUseLatestVersion(e.target.checked)} /> @@ -307,10 +295,10 @@ export default function Preferences() { ); } -function orderedIdeOptions(ideOptions: IDEOptions, type: "browser" | "desktop") { +function orderedIdeOptions(ideOptions: IDEOptions) { // TODO: Maybe convert orderKey to number before sort? return Object.entries(ideOptions.options) - .filter(([_, x]) => x.type === type && !x.hidden) + .filter(([_, x]) => !x.hidden) .sort((a, b) => { const keyA = a[1].orderKey || a[0]; const keyB = b[1].orderKey || b[0]; @@ -319,23 +307,24 @@ function orderedIdeOptions(ideOptions: IDEOptions, type: "browser" | "desktop") } function renderIdeOption(option: IDEOption, selected: boolean, onSelect: () => void): JSX.Element { + const label = option.type === "desktop" ? "" : option.type; const card = ( - +

logo
- {option.label ? ( + {label ? (
- {option.label} + {label}
) : ( <> )} - + ); if (option.tooltip) { diff --git a/components/dashboard/src/start/StartPage.tsx b/components/dashboard/src/start/StartPage.tsx index 93dc9536e38e1d..7b3add2e9f35ae 100644 --- a/components/dashboard/src/start/StartPage.tsx +++ b/components/dashboard/src/start/StartPage.tsx @@ -5,7 +5,9 @@ */ import { useEffect } from "react"; +import Alert from "../components/Alert"; import gitpodIconUA from "../icons/gitpod-ua.svg"; +import { gitpodHostUrl } from "../service/service"; export enum StartPhase { Checking = 0, @@ -74,6 +76,7 @@ export interface StartPageProps { error?: StartWorkspaceError; title?: string; children?: React.ReactNode; + showLatestIdeWarning?: boolean; } export interface StartWorkspaceError { @@ -105,6 +108,14 @@ export function StartPage(props: StartPageProps) { )} {error && } {props.children} + {props.showLatestIdeWarning && ( + + You are using the latest release (unstable) for the editor.{" "} + + Change Preferences + + + )}
); diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index a5a3866c0d3bb2..00a063b876d35b 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -524,13 +524,6 @@ export default class StartWorkspace extends React.Component -
- These IDE options are based on{" "} - - your user preferences - - . -
{this.state.isSSHModalVisible === true && this.state.ownerToken && ( + {statusMessage} ); diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index f8c29c862818e2..85c6f373701839 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -115,8 +115,11 @@ export interface EmailNotificationSettings { } export type IDESettings = { + settingVersion?: string; defaultIde?: string; + // DEPRECATED: Use defaultIde after `settingVersion: 2.0`, no more specialify desktop or browser. useDesktopIde?: boolean; + // DEPRECATED: Same with useDesktopIde. defaultDesktopIde?: string; useLatestVersion?: boolean; }; diff --git a/components/gitpod-protocol/src/workspace-instance.ts b/components/gitpod-protocol/src/workspace-instance.ts index ad2f4871afea25..c809c82a17a2ba 100644 --- a/components/gitpod-protocol/src/workspace-instance.ts +++ b/components/gitpod-protocol/src/workspace-instance.ts @@ -204,6 +204,11 @@ export interface WorkspaceInstanceRepoStatus { totalUnpushedCommits?: number; } +// ConfigurationIdeConfig ide config of WorkspaceInstanceConfiguration +export interface ConfigurationIdeConfig { + useLatest?: boolean; +} + // WorkspaceInstanceConfiguration contains all per-instance configuration export interface WorkspaceInstanceConfiguration { // theiaVersion is the version of Theia this workspace instance uses @@ -221,6 +226,8 @@ export interface WorkspaceInstanceConfiguration { // supervisorImage is the ref of the supervisor image this instance uses. supervisorImage?: string; + + ideConfig?: ConfigurationIdeConfig; } /** diff --git a/components/ide/jetbrains/image/startup.sh b/components/ide/jetbrains/image/startup.sh index 429a529224f7eb..f763dd95d1ed44 100755 --- a/components/ide/jetbrains/image/startup.sh +++ b/components/ide/jetbrains/image/startup.sh @@ -25,4 +25,4 @@ export IJ_HOST_SYSTEM_BASE_DIR=/workspace/.cache/JetBrains # Enable host status endpoint export CWM_HOST_STATUS_OVER_HTTP_TOKEN=gitpod -/ide-desktop/status "$1" "$2" \ No newline at end of file +/ide-desktop/status "$@" \ No newline at end of file diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 7652e2893fe0c9..17ca4dc765b7da 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -314,9 +314,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { delete res.status.ownerToken; // is an operational internal detail delete res.status.nodeName; - // configuration contains feature flags and theia version. - // we might want to share that in the future, but for the time being there's no need - delete res.configuration; // internal operation detail // @ts-ignore delete res.workspaceImage; diff --git a/components/server/src/workspace/workspace-starter.spec.ts b/components/server/src/workspace/workspace-starter.spec.ts new file mode 100644 index 00000000000000..5161477e8fcf6d --- /dev/null +++ b/components/server/src/workspace/workspace-starter.spec.ts @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2020 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 { User } from "@gitpod/gitpod-protocol"; +import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; +import * as chai from "chai"; +import { migrationIDESettings, chooseIDE } from "./workspace-starter"; +const expect = chai.expect; + +describe("workspace-starter", function () { + describe("migrationIDESettings", function () { + it("with no ideSettings should be undefined", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: {}, + }; + const result = migrationIDESettings(user); + expect(result).to.undefined; + }); + + it("with settingVersion 2.0 should be undefined", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + settingVersion: "2.0", + defaultIde: "code-latest", + useDesktopIde: false, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result).to.undefined; + }); + + it("with code-latest should be code latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code-latest", + useDesktopIde: false, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result?.defaultIde).to.equal("code"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + + it("with code-desktop-insiders should be code-desktop latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "code-desktop-insiders", + useDesktopIde: true, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result?.defaultIde).to.equal("code-desktop"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + + it("with code-desktop should be code-desktop", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "code-desktop", + useDesktopIde: true, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result?.defaultIde).to.equal("code-desktop"); + expect(result?.useLatestVersion ?? false).to.be.false; + }); + + it("with intellij should be intellij", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "intellij", + useLatestVersion: false, + useDesktopIde: true, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result?.defaultIde).to.equal("intellij"); + expect(result?.useLatestVersion ?? false).to.be.false; + }); + + it("with intellij latest version should be intellij latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code", + defaultDesktopIde: "intellij", + useLatestVersion: true, + useDesktopIde: true, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result?.defaultIde).to.equal("intellij"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + + it("with user desktopIde false should be code latest", function () { + const user: User = { + id: "string", + creationDate: "string", + identities: [], + additionalData: { + ideSettings: { + defaultIde: "code-latest", + defaultDesktopIde: "intellij", + useLatestVersion: false, + useDesktopIde: false, + }, + }, + }; + const result = migrationIDESettings(user); + expect(result?.defaultIde).to.equal("code"); + expect(result?.useLatestVersion ?? false).to.be.true; + }); + }); + describe("chooseIDE", async function () { + const baseOpt: IDEOption = { + title: "title", + type: "desktop", + logo: "", + image: "image", + latestImage: "latestImage", + }; + const ideOptions: IDEOptions = { + options: { + code: Object.assign({}, baseOpt, { type: "browser" }), + goland: Object.assign({}, baseOpt), + "code-desktop": Object.assign({}, baseOpt), + "no-latest": Object.assign({}, baseOpt), + }, + defaultIde: "code", + defaultDesktopIde: "code-desktop", + }; + delete ideOptions.options["no-latest"].latestImage; + + it("code with latest", function () { + const useLatest = true; + const hasPerm = false; + const result = chooseIDE("code", ideOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); + }); + + it("code without latest", function () { + const useLatest = false; + const hasPerm = false; + const result = chooseIDE("code", ideOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal(ideOptions.options["code"].image); + }); + + it("desktop ide with latest", function () { + const useLatest = true; + const hasPerm = false; + const result = chooseIDE("code-desktop", ideOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); + expect(result.desktopIdeImage).to.equal(ideOptions.options["code-desktop"].latestImage); + }); + + it("desktop ide (JetBrains) without latest", function () { + const useLatest = false; + const hasPerm = false; + const result = chooseIDE("goland", ideOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal(ideOptions.options["code"].image); + expect(result.desktopIdeImage).to.equal(ideOptions.options["goland"].image); + }); + + it("desktop ide with no latest image", function () { + const useLatest = true; + const hasPerm = false; + const result = chooseIDE("no-latest", ideOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); + expect(result.desktopIdeImage).to.equal(ideOptions.options["no-latest"].image); + }); + + it("unknown ide with custom permission should be unknown", function () { + const customOptions = Object.assign({}, ideOptions); + customOptions.options["unknown-custom"] = { + title: "unknown title", + type: "browser", + logo: "", + image: "", + }; + const useLatest = true; + const hasPerm = true; + const result = chooseIDE("unknown-custom", customOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal("unknown-custom"); + }); + + it("unknown desktop ide with custom permission desktop should be unknown", function () { + const customOptions = Object.assign({}, ideOptions); + customOptions.options["unknown-custom"] = { + title: "unknown title", + type: "desktop", + logo: "", + image: "", + }; + const useLatest = true; + const hasPerm = true; + const result = chooseIDE("unknown-custom", customOptions, useLatest, hasPerm); + expect(result.desktopIdeImage).to.equal("unknown-custom"); + }); + + it("unknown browser ide without custom permission should fallback to code", function () { + const customOptions = Object.assign({}, ideOptions); + customOptions.options["unknown-custom"] = { + title: "unknown title", + type: "browser", + logo: "", + image: "", + }; + const useLatest = true; + const hasPerm = false; + const result = chooseIDE("unknown-custom", customOptions, useLatest, hasPerm); + expect(result.ideImage).to.equal(ideOptions.options["code"].latestImage); + }); + }); +}); diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index dcca719f13fa10..1e0a9c585a520e 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -56,6 +56,7 @@ import { ImageConfigFile, ProjectEnvVar, ImageBuildLogInfo, + IDESettings, } from "@gitpod/gitpod-protocol"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @@ -103,7 +104,7 @@ import * as grpc from "@grpc/grpc-js"; import { IDEConfig, IDEConfigService } from "../ide-config"; import { EnvVarWithValue } from "@gitpod/gitpod-protocol/src/protocol"; import { WithReferrerContext } from "@gitpod/gitpod-protocol/lib/protocol"; -import { IDEOption } from "@gitpod/gitpod-protocol/lib/ide-protocol"; +import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; import { ExtendedUser } from "@gitpod/ws-manager/lib/constraints"; import { increaseFailedInstanceStartCounter, increaseSuccessfulInstanceStartCounter } from "../prometheus-metrics"; @@ -118,6 +119,63 @@ export interface StartWorkspaceOptions { const MAX_INSTANCE_START_RETRIES = 2; const INSTANCE_START_RETRY_INTERVAL_SECONDS = 2; +export const migrationIDESettings = (user: User) => { + if (!user?.additionalData?.ideSettings || user.additionalData.ideSettings.settingVersion === "2.0") { + return; + } + const newIDESettings: IDESettings = { + settingVersion: "2.0", + }; + const ideSettings = user.additionalData.ideSettings; + if (ideSettings.useDesktopIde) { + if (ideSettings.defaultDesktopIde === "code-desktop") { + newIDESettings.defaultIde = "code-desktop"; + } else if (ideSettings.defaultDesktopIde === "code-desktop-insiders") { + newIDESettings.defaultIde = "code-desktop"; + newIDESettings.useLatestVersion = true; + } else { + newIDESettings.defaultIde = ideSettings.defaultDesktopIde; + newIDESettings.useLatestVersion = ideSettings.useLatestVersion; + } + } else { + const useLatest = ideSettings.defaultIde === "code-latest"; + newIDESettings.defaultIde = "code"; + newIDESettings.useLatestVersion = useLatest; + } + return newIDESettings; +}; + +export const chooseIDE = ( + ideChoice: string, + ideOptions: IDEOptions, + useLatest: boolean, + hasIdeSettingPerm: boolean, +) => { + const defaultIDEOption = ideOptions.options[ideOptions.defaultIde]; + const defaultIdeImage = useLatest ? defaultIDEOption.latestImage ?? defaultIDEOption.image : defaultIDEOption.image; + const data: { desktopIdeImage?: string; ideImage: string } = { + ideImage: defaultIdeImage, + }; + const chooseOption = ideOptions.options[ideChoice]; + const isDesktopIde = chooseOption.type === "desktop"; + if (isDesktopIde) { + data.desktopIdeImage = useLatest ? chooseOption?.latestImage ?? chooseOption?.image : chooseOption?.image; + if (hasIdeSettingPerm) { + data.desktopIdeImage = data.desktopIdeImage || ideChoice; + } + } else { + data.ideImage = useLatest ? chooseOption?.latestImage ?? chooseOption?.image : chooseOption?.image; + if (hasIdeSettingPerm) { + data.ideImage = data.ideImage || ideChoice; + } + } + if (!data.ideImage) { + data.ideImage = defaultIdeImage; + // throw new Error("cannot choose correct browser ide"); + } + return data; +}; + @injectable() export class WorkspaceStarter { @inject(WorkspaceManagerClientProvider) protected readonly clientProvider: WorkspaceManagerClientProvider; @@ -533,26 +591,41 @@ export class WorkspaceStarter { excludeFeatureFlags: NamedWorkspaceFeatureFlag[], ideConfig: IDEConfig, ): Promise { + // TODO: Compatible with ide-config not deployed, need revert after ide-config deployed + delete ideConfig.ideOptions.options["code-latest"]; + delete ideConfig.ideOptions.options["code-desktop-insiders"]; + + const migratted = migrationIDESettings(user); + if (user.additionalData?.ideSettings && migratted) { + user.additionalData.ideSettings = migratted; + } + + const ideChoice = user.additionalData?.ideSettings?.defaultIde; + const useLatest = !!user.additionalData?.ideSettings?.useLatestVersion; + // TODO(cw): once we allow changing the IDE in the workspace config (i.e. .gitpod.yml), we must // give that value precedence over the default choice. const configuration: WorkspaceInstanceConfiguration = { ideImage: ideConfig.ideOptions.options[ideConfig.ideOptions.defaultIde].image, supervisorImage: ideConfig.supervisorImage, + ideConfig: { + // We only check user setting because if code(insider) but desktopIde has no latestImage + // it still need to notice user that this workspace is using latest IDE + useLatest: user.additionalData?.ideSettings?.useLatestVersion, + }, }; - const ideChoice = user.additionalData?.ideSettings?.defaultIde; if (!!ideChoice) { - const mappedImage = ideConfig.ideOptions.options[ideChoice]; - if (!!mappedImage && mappedImage.image) { - configuration.ideImage = mappedImage.image; - } else if (this.authService.hasPermission(user, "ide-settings")) { - // if the IDE choice isn't one of the preconfiured choices, we assume its the image name. - // For now, this feature requires special permissions. - configuration.ideImage = ideChoice; - } + const choose = chooseIDE( + ideChoice, + ideConfig.ideOptions, + useLatest, + this.authService.hasPermission(user, "ide-settings"), + ); + configuration.ideImage = choose.ideImage; + configuration.desktopIdeImage = choose.desktopIdeImage; } - const useLatest = !!user.additionalData?.ideSettings?.useLatestVersion; const referrerIde = this.resolveReferrerIDE(workspace, user, ideConfig); if (referrerIde) { configuration.desktopIdeImage = useLatest @@ -562,8 +635,8 @@ export class WorkspaceStarter { // A user does not have IDE settings configured yet configure it with a referrer ide as default. const additionalData = user?.additionalData || {}; const settings = additionalData.ideSettings || {}; - settings.useDesktopIde = true; - settings.defaultDesktopIde = referrerIde.id; + settings.settingVersion = "2.0"; + settings.defaultIde = referrerIde.id; additionalData.ideSettings = settings; user.additionalData = additionalData; this.userDB @@ -573,23 +646,6 @@ export class WorkspaceStarter { log.error({ userId: user.id }, "cannot configure default desktop ide", e); }); } - } else { - const useDesktopIdeChoice = user.additionalData?.ideSettings?.useDesktopIde || false; - if (useDesktopIdeChoice) { - const desktopIdeChoice = user.additionalData?.ideSettings?.defaultDesktopIde; - if (!!desktopIdeChoice) { - const mappedImage = ideConfig.ideOptions.options[desktopIdeChoice]; - if (!!mappedImage && mappedImage.image) { - configuration.desktopIdeImage = useLatest - ? mappedImage.latestImage ?? mappedImage.image - : mappedImage.image; - } else if (this.authService.hasPermission(user, "ide-settings")) { - // if the IDE choice isn't one of the preconfiured choices, we assume its the image name. - // For now, this feature requires special permissions. - configuration.desktopIdeImage = desktopIdeChoice; - } - } - } } let featureFlags: NamedWorkspaceFeatureFlag[] = workspace.config._featureFlags || []; diff --git a/install/installer/pkg/components/server/ide/configmap.go b/install/installer/pkg/components/server/ide/configmap.go index de898da8c9a1c6..757f85256e10b7 100644 --- a/install/installer/pkg/components/server/ide/configmap.go +++ b/install/installer/pkg/components/server/ide/configmap.go @@ -27,7 +27,6 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { typeDesktop := "desktop" codeDesktop := "code-desktop" - codeDesktopInsiders := "code-desktop-insiders" intellij := "intellij" goland := "goland" @@ -60,8 +59,8 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { }, }, "vscode-insiders": { - DefaultDesktopIDE: codeDesktopInsiders, - DesktopIDEs: []string{codeDesktopInsiders}, + DefaultDesktopIDE: codeDesktop, + DesktopIDEs: []string{codeDesktop}, InstallationSteps: []string{ "If you don't see an open dialog in your browser, make sure you have VS Code Insiders installed on your machine, and then click ${OPEN_LINK_LABEL} below.", }, @@ -76,37 +75,21 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { }, Options: map[string]IDEOption{ "code": { - OrderKey: pointer.String("00"), - Title: "VS Code", - Type: typeBrowser, - Logo: getIdeLogoPath("vscode"), - Image: common.ImageName(ctx.Config.Repository, ide.CodeIDEImage, ide.CodeIDEImageStableVersion), - }, - "code-latest": { - OrderKey: pointer.String("01"), - Title: "VS Code", - Type: typeBrowser, - Logo: getIdeLogoPath("vscodeInsiders"), - Tooltip: pointer.String("Early access version, still subject to testing."), - Label: pointer.String("Insiders"), - Image: resolveLatestImage(ide.CodeIDEImage, "nightly", ctx.VersionManifest.Components.Workspace.CodeImage), - ResolveImageDigest: pointer.Bool(true), + OrderKey: pointer.String("00"), + Title: "VS Code", + Type: typeBrowser, + Label: pointer.String("Browser"), + Logo: getIdeLogoPath("vscode"), + Image: common.ImageName(ctx.Config.Repository, ide.CodeIDEImage, ide.CodeIDEImageStableVersion), + LatestImage: resolveLatestImage(ide.CodeIDEImage, "nightly", ctx.VersionManifest.Components.Workspace.CodeImage), }, codeDesktop: { - OrderKey: pointer.String("02"), - Title: "VS Code", - Type: typeDesktop, - Logo: getIdeLogoPath("vscode"), - Image: common.ImageName(ctx.Config.Repository, ide.CodeDesktopIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.CodeDesktopImage.Version), - }, - codeDesktopInsiders: { - OrderKey: pointer.String("03"), - Title: "VS Code", - Type: typeDesktop, - Logo: getIdeLogoPath("vscodeInsiders"), - Tooltip: pointer.String("Visual Studio Code Insiders for early adopters."), - Label: pointer.String("Insiders"), - Image: common.ImageName(ctx.Config.Repository, ide.CodeDesktopInsidersIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.CodeDesktopImageInsiders.Version), + OrderKey: pointer.String("02"), + Title: "VS Code", + Type: typeDesktop, + Logo: getIdeLogoPath("vscode"), + Image: common.ImageName(ctx.Config.Repository, ide.CodeDesktopIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.CodeDesktopImage.Version), + LatestImage: common.ImageName(ctx.Config.Repository, ide.CodeDesktopInsidersIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.CodeDesktopImageInsiders.Version), }, intellij: { OrderKey: pointer.String("04"),