Skip to content

Commit ad017ac

Browse files
committed
[dashboard] allow editing user information
fixes #10999
1 parent ba78bd4 commit ad017ac

File tree

11 files changed

+275
-45
lines changed

11 files changed

+275
-45
lines changed

components/dashboard/src/Menu.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,9 @@ export default function Menu() {
342342
>
343343
<path
344344
fill="currentColor"
345-
fill-rule="evenodd"
345+
fillRule="evenodd"
346346
d="M7 0a1 1 0 011 1v5h5a1 1 0 110 2H8v5a1 1 0 11-2 0V8H1a1 1 0 010-2h5V1a1 1 0 011-1z"
347-
clip-rule="evenodd"
347+
clipRule="evenodd"
348348
/>
349349
</svg>
350350
</div>
@@ -356,8 +356,8 @@ export default function Menu() {
356356
<div className="flex h-full pl-0 pr-1 py-1.5 text-gray-50">
357357
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg">
358358
<path
359-
fill-rule="evenodd"
360-
clip-rule="evenodd"
359+
fillRule="evenodd"
360+
clipRule="evenodd"
361361
d="M5.293 7.293a1 1 0 0 1 1.414 0L10 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z"
362362
fill="#78716C"
363363
/>
@@ -385,8 +385,8 @@ export default function Menu() {
385385
<div className="flex pl-0 pr-1 py-1.5">
386386
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg">
387387
<path
388-
fill-rule="evenodd"
389-
clip-rule="evenodd"
388+
fillRule="evenodd"
389+
clipRule="evenodd"
390390
d="M7.293 14.707a1 1 0 0 1 0-1.414L10.586 10 7.293 6.707a1 1 0 1 1 1.414-1.414l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414 0Z"
391391
fill="#78716C"
392392
/>

components/dashboard/src/components/ConfirmationModal.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,31 @@ export default function ConfirmationModal(props: {
1919
onClose: () => void;
2020
onConfirm: () => void;
2121
}) {
22-
const child: React.ReactChild[] = [<p className="mt-3 mb-3 text-base text-gray-500">{props.areYouSureText}</p>];
22+
const children: React.ReactChild[] = [
23+
<p key="areYouSure" className="mt-3 mb-3 text-base text-gray-500">
24+
{props.areYouSureText}
25+
</p>,
26+
];
2327

2428
if (props.warningText) {
25-
child.unshift(<AlertBox>{props.warningText}</AlertBox>);
29+
children.unshift(<AlertBox>{props.warningText}</AlertBox>);
2630
}
2731

2832
const isEntity = (x: any): x is Entity => typeof x === "object" && "name" in x;
2933
if (props.children) {
3034
if (isEntity(props.children)) {
31-
child.push(
32-
<div className="w-full p-4 mb-2 bg-gray-100 dark:bg-gray-700 rounded-xl group">
35+
children.push(
36+
<div key="entity" className="w-full p-4 mb-2 bg-gray-100 dark:bg-gray-700 rounded-xl group">
3337
<p className="text-base text-gray-800 dark:text-gray-100 font-semibold">{props.children.name}</p>
3438
{props.children.description && (
3539
<p className="text-gray-500 truncate">{props.children.description}</p>
3640
)}
3741
</div>,
3842
);
3943
} else if (Array.isArray(props.children)) {
40-
child.push(...props.children);
44+
children.push(...props.children);
4145
} else {
42-
child.push(props.children);
46+
children.push(props.children);
4347
}
4448
}
4549
const cancelButtonRef = useRef<HTMLButtonElement>(null);
@@ -76,7 +80,7 @@ export default function ConfirmationModal(props: {
7680
return true;
7781
}}
7882
>
79-
{child}
83+
{children}
8084
</Modal>
8185
);
8286
}

components/dashboard/src/settings/Account.tsx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,32 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
1111
import { UserContext } from "../user-context";
1212
import getSettingsMenu from "./settings-menu";
1313
import ConfirmationModal from "../components/ConfirmationModal";
14-
import CodeText from "../components/CodeText";
1514
import { PaymentContext } from "../payment-context";
15+
import ProfileInformation, { ProfileState } from "./ProfileInformation";
1616

1717
export default function Account() {
18-
const { user } = useContext(UserContext);
18+
const { user, setUser } = useContext(UserContext);
1919
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);
20-
2120
const [modal, setModal] = useState(false);
21+
const primaryEmail = User.getPrimaryEmail(user!) || "";
2222
const [typedEmail, setTypedEmail] = useState("");
23+
const original = ProfileState.getProfileState(user!);
24+
const [profileState, setProfileState] = useState(original);
25+
const [errorMessage, setErrorMessage] = useState("");
26+
const [updated, setUpdated] = useState(false);
2327

24-
const primaryEmail = User.getPrimaryEmail(user!) || "---";
28+
const saveProfileState = () => {
29+
const error = ProfileState.validate(profileState);
30+
setErrorMessage(error);
31+
if (error) {
32+
return;
33+
}
34+
const updatedUser = ProfileState.setProfileState(user!, profileState);
35+
setUser(updatedUser);
36+
getGitpodService().server.updateLoggedInUser(updatedUser);
37+
setUpdated(true);
38+
setTimeout(() => setUpdated(false), 5000);
39+
};
2540

2641
const deleteAccount = async () => {
2742
await getGitpodService().server.deleteAccount();
@@ -61,30 +76,18 @@ export default function Account() {
6176
subtitle="Manage account and Git configuration."
6277
>
6378
<h3>Profile</h3>
64-
<p className="text-base text-gray-500 pb-4 max-w-2xl">
65-
The following information will be used to set up Git configuration. You can override Git author name
66-
and email per project by using the default environment variables{" "}
67-
<CodeText>GIT_AUTHOR_NAME</CodeText>, <CodeText>GIT_COMMITTER_NAME</CodeText>,{" "}
68-
<CodeText>GIT_AUTHOR_EMAIL</CodeText> and <CodeText>GIT_COMMITTER_EMAIL</CodeText>.
69-
</p>
70-
<div className="flex flex-col lg:flex-row">
71-
<div>
72-
<div className="mt-4">
73-
<h4>Name</h4>
74-
<input type="text" disabled={true} value={user?.fullName || user?.name} />
75-
</div>
76-
<div className="mt-4">
77-
<h4>Email</h4>
78-
<input type="text" disabled={true} value={primaryEmail} />
79-
</div>
80-
</div>
81-
<div className="lg:pl-14">
82-
<div className="mt-4">
83-
<h4>Avatar</h4>
84-
<img className="rounded-full w-24 h-24" src={user!.avatarUrl} alt={user!.name} />
85-
</div>
79+
<ProfileInformation
80+
profileState={profileState}
81+
setProfileState={setProfileState}
82+
errorMessage={errorMessage}
83+
updated={updated}
84+
>
85+
<div className="flex flex-row mt-8">
86+
<button className="primary" onClick={saveProfileState}>
87+
Update Profile
88+
</button>
8689
</div>
87-
</div>
90+
</ProfileInformation>
8891
<h3 className="mt-12">Delete Account</h3>
8992
<p className="text-base text-gray-500 pb-4">
9093
This action will remove all the data associated with your account in Gitpod.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/**
2+
* Copyright (c) 2021 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 { User } from "@gitpod/gitpod-protocol";
8+
import { hoursBefore, isDateSmaller } from "@gitpod/gitpod-protocol/lib/util/timeutil";
9+
import React, { useContext, useState } from "react";
10+
import CodeText from "../components/CodeText";
11+
import Modal from "../components/Modal";
12+
import { getGitpodService } from "../service/service";
13+
import { UserContext } from "../user-context";
14+
15+
export namespace ProfileState {
16+
export interface ProfileState {
17+
name: string;
18+
email: string;
19+
company?: string;
20+
avatarURL?: string;
21+
}
22+
23+
export function getProfileState(user: User): ProfileState {
24+
return {
25+
name: User.getName(user!) || "",
26+
email: User.getPrimaryEmail(user!) || "",
27+
company: user?.additionalData?.companyName,
28+
avatarURL: user?.avatarUrl,
29+
};
30+
}
31+
32+
export function setProfileState(user: User, profileState: ProfileState): User {
33+
user.fullName = profileState.name;
34+
user.avatarUrl = profileState.avatarURL;
35+
36+
if (!user.additionalData) {
37+
user.additionalData = {};
38+
}
39+
user.additionalData.emailAddress = profileState.email;
40+
user.additionalData.companyName = profileState.company;
41+
user.additionalData.lastUpdatedDetailsNudge = new Date().toISOString();
42+
43+
return user;
44+
}
45+
46+
export function hasChanges(before: ProfileState, after: ProfileState) {
47+
return (
48+
before.name !== after.name ||
49+
before.email !== after.email ||
50+
before.company !== after.company ||
51+
before.avatarURL !== after.avatarURL
52+
);
53+
}
54+
55+
function shouldNudgeForUpdate(user: User): boolean {
56+
if (!user.additionalData) {
57+
// never updated profile information and account is older tha 24 hours (i.e. ask on second day).
58+
return !isDateSmaller(hoursBefore(new Date().toISOString(), 24), user.creationDate);
59+
}
60+
// if the profile wasn't updated for 12 months ask again.
61+
return !(
62+
!!user.additionalData.lastUpdatedDetailsNudge &&
63+
isDateSmaller(hoursBefore(new Date().toISOString(), 24 * 365), user.additionalData.lastUpdatedDetailsNudge)
64+
);
65+
}
66+
67+
/**
68+
* @param state
69+
* @returns error message or empty string when valid
70+
*/
71+
export function validate(state: ProfileState): string {
72+
if (state.name.trim() === "") {
73+
return "Name must not be empty.";
74+
}
75+
if (state.email.trim() === "") {
76+
return "Email must not be empty.";
77+
}
78+
if (
79+
!/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
80+
state.email.trim(),
81+
)
82+
) {
83+
return "Please enter a valid email.";
84+
}
85+
return "";
86+
}
87+
88+
export function NudgeForProfileUpdateModal() {
89+
const { user, setUser } = useContext(UserContext);
90+
const original = ProfileState.getProfileState(user!);
91+
const [profileState, setProfileState] = useState(original);
92+
const [errorMessage, setErrorMessage] = useState("");
93+
const [visible, setVisible] = useState(shouldNudgeForUpdate(user!));
94+
95+
const saveProfileState = () => {
96+
const error = ProfileState.validate(profileState);
97+
setErrorMessage(error);
98+
if (error) {
99+
return;
100+
}
101+
const updatedUser = ProfileState.setProfileState(user!, profileState);
102+
setUser(updatedUser);
103+
getGitpodService().server.updateLoggedInUser(updatedUser);
104+
setVisible(shouldNudgeForUpdate(updatedUser!));
105+
};
106+
107+
const cancelProfileUpdate = () => {
108+
setProfileState(original);
109+
saveProfileState();
110+
};
111+
112+
return (
113+
<Modal
114+
title="Update Profile Information"
115+
visible={visible}
116+
onClose={cancelProfileUpdate}
117+
closeable={true}
118+
className="_max-w-xl"
119+
buttons={
120+
<div>
121+
<button className="secondary" onClick={cancelProfileUpdate}>
122+
Dismiss
123+
</button>
124+
<button onClick={saveProfileState}>Update Profile</button>
125+
</div>
126+
}
127+
>
128+
<ProfileInformation
129+
profileState={profileState}
130+
errorMessage={errorMessage}
131+
updated={false}
132+
setProfileState={setProfileState}
133+
/>
134+
</Modal>
135+
);
136+
}
137+
}
138+
139+
export default function ProfileInformation(props: {
140+
profileState: ProfileState.ProfileState;
141+
setProfileState: (newState: ProfileState.ProfileState) => void;
142+
errorMessage: string;
143+
updated: boolean;
144+
children?: React.ReactChild[] | React.ReactChild;
145+
}) {
146+
return (
147+
<div>
148+
<p className="text-base text-gray-500 pb-4 max-w-2xl">
149+
The following information will be used to set up Git configuration. You can override Git author name and
150+
email per project by using the default environment variables <CodeText>GIT_AUTHOR_NAME</CodeText>,{" "}
151+
<CodeText>GIT_COMMITTER_NAME</CodeText>, <CodeText>GIT_AUTHOR_EMAIL</CodeText> and{" "}
152+
<CodeText>GIT_COMMITTER_EMAIL</CodeText>.
153+
</p>
154+
{props.errorMessage.length > 0 ? (
155+
<div className="dark:bg-gray-800 bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">
156+
{props.errorMessage}
157+
</div>
158+
) : null}
159+
{props.updated ? (
160+
<div className="dark:bg-gray-800 bg-gray-300 rounded-md p-3 text-gray-700 dark:text-gray-200 text-sm mb-2">
161+
Profile information has been updated.
162+
</div>
163+
) : null}
164+
<div className="flex flex-col lg:flex-row">
165+
<div>
166+
<div className="mt-4">
167+
<h4>Name</h4>
168+
<input
169+
type="text"
170+
value={props.profileState.name}
171+
onChange={(e) => props.setProfileState({ ...props.profileState, name: e.target.value })}
172+
/>
173+
</div>
174+
<div className="mt-4">
175+
<h4>Email</h4>
176+
<input
177+
type="text"
178+
value={props.profileState.email}
179+
onChange={(e) => props.setProfileState({ ...props.profileState, email: e.target.value })}
180+
/>
181+
</div>
182+
<div className="mt-4">
183+
<h4>Company</h4>
184+
<input
185+
type="text"
186+
value={props.profileState.company}
187+
onChange={(e) => props.setProfileState({ ...props.profileState, company: e.target.value })}
188+
/>
189+
</div>
190+
</div>
191+
<div className="lg:pl-14">
192+
<div className="mt-4">
193+
<h4>Avatar</h4>
194+
<img
195+
className="rounded-full w-24 h-24"
196+
src={props.profileState.avatarURL}
197+
alt={props.profileState.name}
198+
/>
199+
</div>
200+
</div>
201+
</div>
202+
{props.children || null}
203+
</div>
204+
);
205+
}

components/dashboard/src/workspaces/Workspaces.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { StartWorkspaceModalContext, StartWorkspaceModalKeyBinding } from "./sta
2020
import SelectIDEModal from "../settings/SelectIDEModal";
2121
import Arrow from "../components/Arrow";
2222
import ConfirmationModal from "../components/ConfirmationModal";
23+
import { ProfileState } from "../settings/ProfileInformation";
2324

2425
export interface WorkspacesProps {}
2526

@@ -68,7 +69,11 @@ export default function () {
6869
}}
6970
></ConfirmationModal>
7071

71-
{isOnboardingUser && <SelectIDEModal location={"workspace_list"} />}
72+
{isOnboardingUser ? (
73+
<SelectIDEModal location={"workspace_list"} />
74+
) : (
75+
<ProfileState.NudgeForProfileUpdateModal />
76+
)}
7277

7378
{workspaceModel?.initialized &&
7479
(activeWorkspaces.length > 0 || inactiveWorkspaces.length > 0 || workspaceModel.searchTerm ? (

0 commit comments

Comments
 (0)