Skip to content

Commit d92f361

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

File tree

11 files changed

+278
-45
lines changed

11 files changed

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

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)