Skip to content

[dashboard] Refactor What's New dialog #4607

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 2 commits into from
Jun 28, 2021
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
4 changes: 2 additions & 2 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Login } from './Login';
import { UserContext } from './user-context';
import { TeamsContext } from './teams/teams-context';
import { getGitpodService } from './service/service';
import { shouldSeeWhatsNew, WhatsNew } from './WhatsNew';
import { shouldSeeWhatsNew, WhatsNew } from './whatsnew/WhatsNew';
import gitpodIcon from './icons/gitpod.svg';
import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error';

Expand Down Expand Up @@ -234,7 +234,7 @@ function App() {
const isCreation = window.location.pathname === '/' && hash !== '';
const isWsStart = /\/start\/?/.test(window.location.pathname) && hash !== '';
if (isWhatsNewShown) {
toRender = <WhatsNew visible={true} onClose={() => setWhatsNewShown(false)} />;
toRender = <WhatsNew onClose={() => setWhatsNewShown(false)} />;
} else if (isCreation) {
toRender = <CreateWorkspace contextUrl={hash} />;
} else if (isWsStart) {
Expand Down
73 changes: 0 additions & 73 deletions components/dashboard/src/WhatsNew.tsx

This file was deleted.

9 changes: 9 additions & 0 deletions components/dashboard/src/components/CodeText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* 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 default function CodeText(p: { children?: React.ReactNode }) {
return <span className="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded-md text-sm font-mono font-medium">{p.children}</span>;
}
19 changes: 19 additions & 0 deletions components/dashboard/src/components/PillLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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.
*/

/**
* Renders a pill.
*
* **type**\
* info: Renders a blue pile label (default).\
* warn: Renders an orange pile label.
*/
export default function PillLabel(props: { children?: React.ReactNode, type?: "info" | "warn" }) {
const infoStyle = "bg-blue-50 text-blue-500 dark:bg-blue-500 dark:text-blue-100";
const warnStyle = "bg-orange-100 text-orange-700 dark:bg-orange-600 dark:text-orange-100";
const style = `ml-2 px-3 py-1 text-sm uppercase rounded-xl ${props.type === "warn" ? warnStyle : infoStyle}`;
return <span className={style}>{props.children}</span>;
}
3 changes: 2 additions & 1 deletion components/dashboard/src/settings/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
import { UserContext } from "../user-context";
import settingsMenu from "./settings-menu";
import ConfirmationModal from "../components/ConfirmationModal";
import CodeText from "../components/CodeText";

export default function Account() {
const { user } = useContext(UserContext);
Expand Down Expand Up @@ -46,7 +47,7 @@ export default function Account() {

<PageWithSubMenu subMenu={settingsMenu} title='Account' subtitle='Manage account and git configuration.'>
<h3>Profile</h3>
<p className="text-base text-gray-500 pb-4 max-w-2xl">The following information will be used to set up git configuration. You can override git author name and email per project by using the default environment variables <span className="bg-gray-100 dark:bg-gray-800 px-1.5 py-1 rounded-md text-sm font-mono font-medium">GIT_AUTHOR_NAME</span> and <span className="bg-gray-100 dark:bg-gray-800 px-1.5 py-1 rounded-md text-sm font-mono font-medium">GIT_COMMITTER_EMAIL</span>.</p>
<p className="text-base text-gray-500 pb-4 max-w-2xl">The following information will be used to set up git configuration. You can override git author name and email per project by using the default environment variables <CodeText>GIT_AUTHOR_NAME</CodeText> and <CodeText>GIT_COMMITTER_EMAIL</CodeText>.</p>
<div className="flex flex-col lg:flex-row">
<div>
<div className="mt-4">
Expand Down
5 changes: 3 additions & 2 deletions components/dashboard/src/settings/EnvironmentVariables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
*/

import { UserEnvVarValue } from "@gitpod/gitpod-protocol";
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import ConfirmationModal from "../components/ConfirmationModal";
import { Item, ItemField, ItemFieldContextMenu, ItemsList } from "../components/ItemsList";
import Modal from "../components/Modal";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { getGitpodService } from "../service/service";
import settingsMenu from "./settings-menu";
import CodeText from "../components/CodeText";

interface EnvVarModalProps {
envVar: UserEnvVarValue;
Expand Down Expand Up @@ -70,7 +71,7 @@ function AddEnvVarModal(p: EnvVarModalProps) {
onChange={(v) => { update({repositoryPattern: v.target.value}) }} />
</div>
<div className="mt-1">
<p className="text-gray-500">You can pass a variable for a specific project or use wildcard character (<span className="bg-gray-100 dark:bg-gray-800 px-1.5 py-1 rounded-md text-sm font-mono font-medium">*/*</span>) to make it available in more projects.</p>
<p className="text-gray-500">You can pass a variable for a specific project or use wildcard character (<CodeText>*/*</CodeText>) to make it available in more projects.</p>
</div>
</div>
<div className="flex justify-end mt-6">
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/settings/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Theme = 'light' | 'dark' | 'system';

export default function Preferences() {
const { user } = useContext(UserContext);
const [ defaultIde, setDefaultIde ] = useState<string>(user?.additionalData?.ideSettings?.defaultIde || 'code');
const [ defaultIde, setDefaultIde ] = useState<string>(user?.additionalData?.ideSettings?.defaultIde || 'theia');
const actuallySetDefaultIde = async (value: string) => {
const additionalData = user?.additionalData || {};
const settings = additionalData.ideSettings || {};
Expand Down
56 changes: 56 additions & 0 deletions components/dashboard/src/whatsnew/WhatsNew-2021-04.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* 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 { User } from "@gitpod/gitpod-protocol";
import { getGitpodService } from "../service/service";
import { WhatsNewEntry } from "./WhatsNew";

export const switchToVSCodeAction = async (user: User) => {
const additionalData = user.additionalData = user.additionalData || {};
// make sure code is set as the default IDE
const ideSettings = additionalData.ideSettings = additionalData.ideSettings || {};
ideSettings.defaultIde = 'code';
user = await getGitpodService().server.updateLoggedInUser({
additionalData
});
return user;
};

export const WhatsNewEntry202104: WhatsNewEntry = {
newsKey: 'April-2021',
maxUserCreationDate: '2021-04-08',
children: () => <>
<div className="border-t border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4">
<p className="pb-2 text-gray-900 dark:text-gray-100 text-base font-medium">New Dashboard</p>
<p className="pb-2 text-gray-500 dark:text-gray-400 text-sm">We have made some layout changes on the dashboard to improve the overall user experience of Gitpod.</p>
</div>
<div className="border-t border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4">
<p className="pb-2 text-gray-900 dark:text-gray-100 text-base font-medium">VS Code</p>
<p className="pb-4 text-gray-500 dark:text-gray-400 text-sm">We are changing the default IDE to VS Code.</p>
<ol className="pb-2 text-gray-500 dark:text-gray-400 text-sm list-outside list-decimal space-y-2">
<li className="ml-5">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">We're preserving most <span className="font-bold">user settings and extensions</span>.</p>
<p className="text-gray-400 dark:text-gray-500 text-sm">Extensions you have manually uploaded are not transferred. You'll need to search and install those extensions through the extension panel in VS Code.</p>
</div>
</li>
<li className="ml-5">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">We've reduced the number of <span className="font-bold">pre-installed extensions</span>.</p>
<p className="text-gray-400 dark:text-gray-500 text-sm">The Theia-based editor included pre-installed extensions for the most popular programming languages which was convenient for starters but added additional bloat. You can now install any extensions you need and leave out those you don't.</p>
</div>
</li>
<li className="ml-5">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">You can still <span className="font-bold">switch the IDE</span> back to Theia.</p>
<p className="text-gray-400 dark:text-gray-500 text-sm">In case you run into trouble with VS Code, you can go to the settings and switch back to the Theia.</p>
</div>
</li>
</ol>
</div>
</>,
actionAfterSeen: switchToVSCodeAction,
};
33 changes: 33 additions & 0 deletions components/dashboard/src/whatsnew/WhatsNew-2021-06.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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 { User } from "@gitpod/gitpod-protocol";
import { WhatsNewEntry } from "./WhatsNew";
import { switchToVSCodeAction } from "./WhatsNew-2021-04";
import CodeText from "../components/CodeText";
import PillLabel from "../components/PillLabel";

export const WhatsNewEntry202106: WhatsNewEntry = {
children: (user: User, setUser: React.Dispatch<User>) => {

return <>
<div className="border-t border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 pt-6 pb-4">
<p className="pb-2 text-gray-900 dark:text-gray-100 text-base font-medium">Exposing Ports <PillLabel>Configuration Update</PillLabel></p>
<p className="pb-2 text-gray-500 dark:text-gray-400 text-sm">We've changed the default behavior of exposed ports to improve the security of your dev environments.</p>
<p className="pb-2 text-gray-500 dark:text-gray-400 text-sm">Exposing ports are now private by default. You can still change port visibility through the editor or even configure this with the <CodeText>visibility</CodeText> property in <CodeText>.gitpod.yml</CodeText>.</p>
</div>
{ user.additionalData?.ideSettings?.defaultIde !== "code" && <>
<div className="border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 pt-6 pb-4">
<p className="pb-2 text-gray-900 dark:text-gray-100 text-base font-medium">New Editor <PillLabel type="warn">Deprecation Warning</PillLabel></p>
<p className="pb-2 text-gray-500 dark:text-gray-400 text-sm">We're deprecating the Theia editor. You can still switch back to Theia for the next few weeks but the preference will be removed by the end of August 2021.</p>
</div>
</>}
</>;
},
newsKey: 'June-2021',
maxUserCreationDate: '2021-07-01',
actionAfterSeen: switchToVSCodeAction,
};
98 changes: 98 additions & 0 deletions components/dashboard/src/whatsnew/WhatsNew.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* 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 { User } from "@gitpod/gitpod-protocol";
import Modal from "../components/Modal";
import { WhatsNewEntry202104 } from "./WhatsNew-2021-04";
import { WhatsNewEntry202106 } from "./WhatsNew-2021-06";
import { UserContext } from "../user-context";
import { useContext, useState } from "react";
import { getGitpodService } from "../service/service";

const allEntries: WhatsNewEntry[] = [
WhatsNewEntry202106,
WhatsNewEntry202104,
]

export const shouldSeeWhatsNew = (user: User, news: { newsKey: string, maxUserCreationDate: string }[] = allEntries) => {
const whatsNewSeen = user?.additionalData?.whatsNewSeen;
return news.some(x => user.creationDate <= x.maxUserCreationDate && (!whatsNewSeen || !whatsNewSeen[x.newsKey]));
}

export function WhatsNew(props: { onClose: () => void }) {
const { user, setUser } = useContext(UserContext);

const _unseenEntries = allEntries.filter(x => user && shouldSeeWhatsNew(user, [x])) || [];
const [visibleEntry, setVisibleEntry] = useState(_unseenEntries.pop());
const [unseenEntries, setUnseenEntries] = useState(_unseenEntries);

const markAsSeen = async (user?: User, ...news: (WhatsNewEntry | undefined)[]) => {
if (!user) {
return;
}

for (const n of news.filter(x => x && x.actionAfterSeen)) {
user = await n!.actionAfterSeen!(user);
};

const additionalData = user.additionalData = user.additionalData || {};
additionalData.whatsNewSeen = additionalData.whatsNewSeen || {};
const now = new Date().toISOString();
for (const newsKey of (news.filter(x => x !== undefined) as WhatsNewEntry[]).map(x => x.newsKey)) {
additionalData.whatsNewSeen[newsKey] = now;
}
user = await getGitpodService().server.updateLoggedInUser({
additionalData
});
setUser(user);
};

const internalClose = async () => {
await markAsSeen(user, ...unseenEntries, visibleEntry);
props.onClose();
};

const hasNext = () => unseenEntries.length > 0;

const next = async () => {
if (unseenEntries.length === 0) {
return;
}
visibleEntry && await markAsSeen(user, visibleEntry);
const _unseenEntries = unseenEntries;
setVisibleEntry(_unseenEntries.pop());
setUnseenEntries(_unseenEntries);
};

return <Modal
visible={!!visibleEntry}
onClose={internalClose}
>
<h3 className="pb-4">What's New 🎁</h3>
<>{visibleEntry && user ? visibleEntry.children(user, setUser) : <></>}</>
{
hasNext() ?
<>
<div className="flex items-center justify-end mt-6 space-x-2">
<div className="text-sm mr-auto italic">{unseenEntries.length} more update{unseenEntries.length > 1 ? "s" : ""}</div>
<button className="ml-2 secondary" onClick={internalClose}>Dismiss All</button>
<button className="ml-2" onClick={next}>Next</button>
</div>
</>
:
<div className="flex justify-end mt-6 space-x-2">
<button onClick={internalClose}>Continue</button>
</div>
}
</Modal>
}

export interface WhatsNewEntry {
newsKey: string,
maxUserCreationDate: string,
children: (user: User, setUser: React.Dispatch<User>) => React.ReactChild[] | React.ReactChild,
actionAfterSeen?: (user: User) => Promise<User>;
}