From faef3f96887f2762b2c37ac6750a3ea5d5c18284 Mon Sep 17 00:00:00 2001 From: mustard Date: Wed, 27 Apr 2022 17:52:16 +0000 Subject: [PATCH] feat: ide awareness during onboarding Co-authored-by: mustard Co-authored-by: andreafalzetti --- .../dashboard/src/settings/Preferences.tsx | 199 +----------------- .../dashboard/src/settings/SelectIDE.tsx | 194 +++++++++++++++++ .../dashboard/src/settings/SelectIDEModal.tsx | 56 +++++ .../dashboard/src/workspaces/Workspaces.tsx | 8 + .../gitpod-db/src/typeorm/user-db-impl.ts | 2 +- components/gitpod-protocol/src/protocol.ts | 41 ++++ 6 files changed, 305 insertions(+), 195 deletions(-) create mode 100644 components/dashboard/src/settings/SelectIDE.tsx create mode 100644 components/dashboard/src/settings/SelectIDEModal.tsx diff --git a/components/dashboard/src/settings/Preferences.tsx b/components/dashboard/src/settings/Preferences.tsx index d628759f1dcb02..07f6559fd3491d 100644 --- a/components/dashboard/src/settings/Preferences.tsx +++ b/components/dashboard/src/settings/Preferences.tsx @@ -4,22 +4,17 @@ * See License-AGPL.txt in the project root for license information. */ -import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; -import { useContext, useEffect, useState } from "react"; -import InfoBox from "../components/InfoBox"; +import { useContext, useState } from "react"; 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 { trackEvent } from "../Analytics"; import { PaymentContext } from "../payment-context"; -import CheckBox from "../components/CheckBox"; -import { IDESettings } from "@gitpod/gitpod-protocol"; +import SelectIDE from "./SelectIDE"; type Theme = "light" | "dark" | "system"; @@ -28,79 +23,6 @@ export default function Preferences() { const { showPaymentUI } = useContext(PaymentContext); const { setIsDark } = useContext(ThemeContext); - 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.settingVersion = "2.0"; - settings.defaultIde = selectedIde; - settings.useLatestVersion = useLatestVersion; - additionalData.ideSettings = settings; - getGitpodService() - .server.trackEvent({ - event: "ide_configuration_changed", - properties: settings, - }) - .then() - .catch(console.error); - await getGitpodService().server.updateLoggedInUser({ additionalData }); - }; - - const [defaultIde, setDefaultIde] = useState(user?.additionalData?.ideSettings?.defaultIde || ""); - const actuallySetDefaultIde = async (value: string) => { - await updateUserIDEInfo(value, useLatestVersion); - setDefaultIde(value); - }; - - const [useLatestVersion, setUseLatestVersion] = useState( - user?.additionalData?.ideSettings?.useLatestVersion ?? false, - ); - const actuallySetUseLatestVersion = async (value: boolean) => { - await updateUserIDEInfo(defaultIde, value); - setUseLatestVersion(value); - }; - - const [ideOptions, setIdeOptions] = useState(undefined); - useEffect(() => { - (async () => { - const ideopts = await getGitpodService().server.getIDEOptions(); - // 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 || ideopts.options[defaultIde] == null) { - setDefaultIde(ideopts.defaultIde); - } - })(); - }, []); - const [theme, setTheme] = useState(localStorage.theme || "system"); const actuallySetTheme = (theme: Theme) => { if (theme === "dark" || theme === "light") { @@ -115,8 +37,6 @@ export default function Preferences() { setTheme(theme); }; - const allIdeOptions = ideOptions && orderedIdeOptions(ideOptions); - const [dotfileRepo, setDotfileRepo] = useState(user?.additionalData?.dotfileRepo || ""); const actuallySetDotfileRepo = async (value: string) => { const additionalData = user?.additionalData || {}; @@ -136,80 +56,9 @@ export default function Preferences() { title="Preferences" subtitle="Configure user preferences." > - {ideOptions && ( - <> - {allIdeOptions && ( - <> -

Editor

-

- Choose the editor for opening workspaces. -

-
- {allIdeOptions.map(([id, option]) => { - const selected = defaultIde === id; - const onSelect = () => actuallySetDefaultIde(id); - return renderIdeOption(option, selected, onSelect); - })} -
- {ideOptions.options[defaultIde]?.notes && ( - -
    - {ideOptions.options[defaultIde].notes?.map((x, idx) => ( -
  • 0 ? "mt-2" : ""}>{x}
  • - ))} -
-
- )} -

- The JetBrains desktop IDEs are currently in beta.{" "} - - Send feedback - {" "} - ·{" "} - - Documentation - -

- - )} - - Use the latest version for each editor.{" "} - - Insiders - {" "} - for VS Code,{" "} - - EAP - {" "} - for JetBrains IDEs. - - } - checked={useLatestVersion} - onChange={(e) => actuallySetUseLatestVersion(e.target.checked)} - /> - - )} +

Editor

+

Choose the editor for opening workspaces.

+

Theme

Early bird or night owl? Choose your side.

@@ -294,41 +143,3 @@ export default function Preferences() {
); } - -function orderedIdeOptions(ideOptions: IDEOptions) { - // TODO: Maybe convert orderKey to number before sort? - return Object.entries(ideOptions.options) - .filter(([_, x]) => !x.hidden) - .sort((a, b) => { - const keyA = a[1].orderKey || a[0]; - const keyB = b[1].orderKey || b[0]; - return keyA.localeCompare(keyB); - }); -} - -function renderIdeOption(option: IDEOption, selected: boolean, onSelect: () => void): JSX.Element { - const label = option.type === "desktop" ? "" : option.type; - const card = ( - -
- logo -
- {label ? ( -
- {label} -
- ) : ( - <> - )} -
- ); - - if (option.tooltip) { - return {card}; - } - return card; -} diff --git a/components/dashboard/src/settings/SelectIDE.tsx b/components/dashboard/src/settings/SelectIDE.tsx new file mode 100644 index 00000000000000..66cb8740e62034 --- /dev/null +++ b/components/dashboard/src/settings/SelectIDE.tsx @@ -0,0 +1,194 @@ +/** + * 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 { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; +import { useContext, useEffect, useState } from "react"; +import InfoBox from "../components/InfoBox"; +import SelectableCardSolid from "../components/SelectableCardSolid"; +import Tooltip from "../components/Tooltip"; +import { getGitpodService } from "../service/service"; +import { UserContext } from "../user-context"; +import CheckBox from "../components/CheckBox"; +import { User } from "@gitpod/gitpod-protocol"; + +interface SelectIDEProps { + updateUserContext?: boolean; +} + +export const updateUserIDEInfo = async (user: User, selectedIde: string, useLatestVersion: boolean) => { + const additionalData = user?.additionalData ?? {}; + const settings = additionalData.ideSettings ?? {}; + settings.settingVersion = "2.0"; + settings.defaultIde = selectedIde; + settings.useLatestVersion = useLatestVersion; + additionalData.ideSettings = settings; + getGitpodService() + .server.trackEvent({ + event: "ide_configuration_changed", + properties: settings, + }) + .then() + .catch(console.error); + return getGitpodService().server.updateLoggedInUser({ additionalData }); +}; + +export default function SelectIDE(props: SelectIDEProps) { + const { user, setUser } = useContext(UserContext); + + // Only exec once when we access this component + useEffect(() => { + user && User.migrationIDESettings(user); + }, []); + + const actualUpdateUserIDEInfo = async (user: User, selectedIde: string, useLatestVersion: boolean) => { + const newUserData = await updateUserIDEInfo(user, selectedIde, useLatestVersion); + props.updateUserContext && setUser({ ...newUserData }); + }; + + const [defaultIde, setDefaultIde] = useState(user?.additionalData?.ideSettings?.defaultIde || "code"); + const actuallySetDefaultIde = async (value: string) => { + await actualUpdateUserIDEInfo(user!, value, useLatestVersion); + setDefaultIde(value); + }; + + const [useLatestVersion, setUseLatestVersion] = useState( + user?.additionalData?.ideSettings?.useLatestVersion ?? false, + ); + const actuallySetUseLatestVersion = async (value: boolean) => { + await actualUpdateUserIDEInfo(user!, defaultIde, value); + setUseLatestVersion(value); + }; + + const [ideOptions, setIdeOptions] = useState(undefined); + useEffect(() => { + (async () => { + const ideopts = await getGitpodService().server.getIDEOptions(); + // 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); + })(); + }, []); + + const allIdeOptions = ideOptions && orderedIdeOptions(ideOptions); + + return ( + <> + {ideOptions && ( + <> + {allIdeOptions && ( + <> +
+ {allIdeOptions.map(([id, option]) => { + const selected = defaultIde === id; + const onSelect = () => actuallySetDefaultIde(id); + return renderIdeOption(option, selected, onSelect); + })} +
+ {ideOptions.options[defaultIde]?.notes && ( + +
    + {ideOptions.options[defaultIde].notes?.map((x, idx) => ( +
  • 0 ? "mt-2" : ""}>{x}
  • + ))} +
+
+ )} + +

+ The JetBrains desktop IDEs are currently in beta.{" "} + + Send feedback + {" "} + ·{" "} + + Documentation + +

+ + )} + + Use the latest version for each editor.{" "} + + Insiders + {" "} + for VS Code,{" "} + + EAP + {" "} + for JetBrains IDEs. + + } + checked={useLatestVersion} + onChange={(e) => actuallySetUseLatestVersion(e.target.checked)} + /> + + )} + + ); +} + +function orderedIdeOptions(ideOptions: IDEOptions) { + // TODO: Maybe convert orderKey to number before sort? + return Object.entries(ideOptions.options) + .filter(([_, x]) => !x.hidden) + .sort((a, b) => { + const keyA = a[1].orderKey || a[0]; + const keyB = b[1].orderKey || b[0]; + return keyA.localeCompare(keyB); + }); +} + +function renderIdeOption(option: IDEOption, selected: boolean, onSelect: () => void): JSX.Element { + const label = option.type === "desktop" ? "" : option.type; + const card = ( + +
+ logo +
+ {label ? ( +
+ {label} +
+ ) : ( + <> + )} +
+ ); + + if (option.tooltip) { + return {card}; + } + return card; +} diff --git a/components/dashboard/src/settings/SelectIDEModal.tsx b/components/dashboard/src/settings/SelectIDEModal.tsx new file mode 100644 index 00000000000000..30380a2e5c0dc1 --- /dev/null +++ b/components/dashboard/src/settings/SelectIDEModal.tsx @@ -0,0 +1,56 @@ +/** + * 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 { useState, useContext } from "react"; +import { Link } from "react-router-dom"; +import { User } from "@gitpod/gitpod-protocol"; +import SelectIDE, { updateUserIDEInfo } from "./SelectIDE"; +import Modal from "../components/Modal"; +import { UserContext } from "../user-context"; + +export default function () { + const { user, setUser } = useContext(UserContext); + const [visible, setVisible] = useState(true); + + const actualUpdateUserIDEInfo = async (user: User, selectedIde: string, useLatestVersion: boolean) => { + const newUserData = await updateUserIDEInfo(user, selectedIde, useLatestVersion); + setUser({ ...newUserData }); + }; + + const handleContinue = async () => { + setVisible(false); + if (!user || User.hasPreferredIde(user)) { + return; + } + // TODO: We need to get defaultIde in ideOptions.. + const defaultIde = "code"; + await actualUpdateUserIDEInfo(user, defaultIde, false); + }; + + return ( + +

Select Editor

+
+

+ Choose the editor for opening workspaces. You can always change later the editor in{" "} + + user preferences + + . +

+ +
+
+ +
+
+ ); +} diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index 47806667f0f91b..2ca855c2187b9c 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -13,8 +13,11 @@ import { WorkspaceEntry } from "./WorkspaceEntry"; import { getGitpodService } from "../service/service"; import { ItemsList } from "../components/ItemsList"; import { TeamsContext } from "../teams/teams-context"; +import { UserContext } from "../user-context"; +import { User } from "@gitpod/gitpod-protocol"; import { useLocation } from "react-router"; import { StartWorkspaceModalContext, StartWorkspaceModalKeyBinding } from "./start-workspace-modal-context"; +import SelectIDEModal from "../settings/SelectIDEModal"; export interface WorkspacesProps {} @@ -27,6 +30,7 @@ export interface WorkspacesState { export default function () { const location = useLocation(); + const { user } = useContext(UserContext); const { teams } = useContext(TeamsContext); const [activeWorkspaces, setActiveWorkspaces] = useState([]); const [inactiveWorkspaces, setInactiveWorkspaces] = useState([]); @@ -40,10 +44,14 @@ export default function () { })(); }, [teams, location]); + const isOnboardingUser = user && User.isOnboardingUser(user); + return ( <>
+ {isOnboardingUser && } + {workspaceModel?.initialized && (activeWorkspaces.length > 0 || inactiveWorkspaces.length > 0 || workspaceModel.searchTerm ? ( <> diff --git a/components/gitpod-db/src/typeorm/user-db-impl.ts b/components/gitpod-db/src/typeorm/user-db-impl.ts index 7ed68b39e63513..ccdd8402fb67e6 100644 --- a/components/gitpod-db/src/typeorm/user-db-impl.ts +++ b/components/gitpod-db/src/typeorm/user-db-impl.ts @@ -101,7 +101,7 @@ export class TypeORMUserDBImpl implements UserDB { creationDate: new Date().toISOString(), identities: [], additionalData: { - ideSettings: { defaultIde: "code" }, + ideSettings: {}, emailNotificationSettings: { allowsChangelogMail: true, allowsDevXMail: true, diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index c8e10922a6e9c5..84388565b790f1 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -95,6 +95,47 @@ export namespace User { } return undefined; } + + export function hasPreferredIde(user: User) { + return ( + typeof user?.additionalData?.ideSettings?.defaultIde !== "undefined" || + typeof user?.additionalData?.ideSettings?.useLatestVersion !== "undefined" + ); + } + + export function isOnboardingUser(user: User) { + return !hasPreferredIde(user); + } + + export function migrationIDESettings(user: User) { + if ( + !user?.additionalData?.ideSettings || + Object.keys(user.additionalData.ideSettings).length === 0 || + 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; + } } export interface AdditionalUserData {