Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/user-settings/Account.tsx
2500 views
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 { useCallback, useContext, useState } from "react";
8
import { getGitpodService, gitpodHostUrl } from "../service/service";
9
import { UserContext } from "../user-context";
10
import ConfirmationModal from "../components/ConfirmationModal";
11
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
12
import { Button } from "@podkit/buttons/Button";
13
import { Heading2, Subheading } from "../components/typography/headings";
14
import Alert from "../components/Alert";
15
import { TextInputField } from "../components/forms/TextInputField";
16
import isEmail from "validator/lib/isEmail";
17
import { useToast } from "../components/toasts/Toasts";
18
import { InputWithCopy } from "../components/InputWithCopy";
19
import { InputField } from "../components/forms/InputField";
20
import { getPrimaryEmail, isOrganizationOwned } from "@gitpod/public-api-common/lib/user-utils";
21
import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";
22
import { User as UserProtocol, ProfileDetails } from "@gitpod/gitpod-protocol";
23
import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation";
24
25
type UserProfile = Pick<ProfileDetails, "emailAddress"> & Required<Pick<UserProtocol, "name" | "avatarUrl">>;
26
function getProfile(user: User): UserProfile {
27
return {
28
name: user.name,
29
avatarUrl: user.avatarUrl,
30
emailAddress: getPrimaryEmail(user),
31
};
32
}
33
34
export default function Account() {
35
const { user, setUser } = useContext(UserContext);
36
const [modal, setModal] = useState(false);
37
const [typedEmail, setTypedEmail] = useState("");
38
const original = getProfile(user!);
39
const [profileState, setProfileState] = useState(original);
40
const [errorMessage, setErrorMessage] = useState("");
41
const canUpdateEmail = user && !isOrganizationOwned(user);
42
const { toast } = useToast();
43
const updateUser = useUpdateCurrentUserMutation();
44
45
const saveProfileState = useCallback(async () => {
46
if (!user || !profileState) {
47
return;
48
}
49
50
if (profileState.name.trim() === "") {
51
setErrorMessage("Name must not be empty.");
52
return;
53
}
54
if (canUpdateEmail) {
55
if (!profileState.emailAddress?.trim()) {
56
setErrorMessage("Email must not be empty.");
57
return;
58
}
59
// check valid email
60
if (!isEmail(profileState.emailAddress?.trim() || "")) {
61
setErrorMessage("Please enter a valid email.");
62
return;
63
}
64
} else {
65
profileState.emailAddress = getPrimaryEmail(user) || "";
66
}
67
68
const updatedUser = await updateUser.mutateAsync({
69
fullName: profileState.name,
70
additionalData: {
71
profile: profileState,
72
},
73
});
74
setUser(updatedUser);
75
toast("Your profile information has been updated.");
76
}, [updateUser, canUpdateEmail, profileState, setUser, toast, user]);
77
78
const deleteAccount = useCallback(async () => {
79
await getGitpodService().server.deleteAccount();
80
document.location.href = gitpodHostUrl.asApiLogout().toString();
81
}, []);
82
83
const close = () => setModal(false);
84
return (
85
<div>
86
<ConfirmationModal
87
title="Delete Account"
88
areYouSureText="You are about to permanently delete your account."
89
buttonText="Delete Account"
90
buttonDisabled={typedEmail !== (original.emailAddress || "")}
91
visible={modal}
92
onClose={close}
93
onConfirm={deleteAccount}
94
>
95
<ol className="text-gray-500 text-sm list-outside list-decimal">
96
<li className="ml-5">
97
All your workspaces and related data will be deleted and cannot be restored afterwards.
98
</li>
99
<li className="ml-5">
100
Your subscription will be cancelled. If you obtained a Gitpod subscription through the GitHub
101
marketplace, you need to cancel your plan there.
102
</li>
103
</ol>
104
<p className="pt-4 pb-2 text-gray-600 dark:text-gray-400 text-base font-semibold">
105
Type your email to confirm
106
</p>
107
<input autoFocus className="w-full" type="text" onChange={(e) => setTypedEmail(e.target.value)}></input>
108
</ConfirmationModal>
109
110
<PageWithSettingsSubMenu>
111
<Heading2>Profile</Heading2>
112
<form
113
onSubmit={(e) => {
114
e.preventDefault();
115
saveProfileState();
116
}}
117
>
118
<ProfileInformation
119
profileState={profileState}
120
setProfileState={(state) => {
121
setProfileState(state);
122
}}
123
errorMessage={errorMessage}
124
emailIsReadonly={!canUpdateEmail}
125
user={user}
126
>
127
<div className="flex flex-row mt-8">
128
<Button type="submit">Update Profile</Button>
129
</div>
130
</ProfileInformation>
131
</form>
132
<Heading2 className="mt-12">Delete Account</Heading2>
133
<Subheading className="mb-3">
134
This action will remove all the data associated with your account in Gitpod.
135
</Subheading>
136
<Button variant="destructive" onClick={() => setModal(true)}>
137
Delete Account
138
</Button>
139
</PageWithSettingsSubMenu>
140
</div>
141
);
142
}
143
144
function ProfileInformation(props: {
145
profileState: UserProfile;
146
setProfileState: (newState: UserProfile) => void;
147
errorMessage: string;
148
emailIsReadonly?: boolean;
149
user?: User;
150
children?: React.ReactChild[] | React.ReactChild;
151
}) {
152
return (
153
<div>
154
<p className="text-base text-gray-500 pb-4 max-w-xl">
155
The following information will be used to set up Git configuration. You can override Git author name and
156
email per project by using the default environment variables <code>GIT_AUTHOR_NAME</code>,{" "}
157
<code>GIT_COMMITTER_NAME</code>, <code>GIT_AUTHOR_EMAIL</code> and <code>GIT_COMMITTER_EMAIL</code>.
158
</p>
159
{props.errorMessage.length > 0 && (
160
<Alert type="error" closable={true} className="mb-2 max-w-xl rounded-md">
161
{props.errorMessage}
162
</Alert>
163
)}
164
<div className="flex flex-col lg:flex-row">
165
<fieldset>
166
<TextInputField
167
label="Name"
168
value={props.profileState.name}
169
onChange={(val) => props.setProfileState({ ...props.profileState, name: val })}
170
/>
171
<TextInputField
172
label="Email"
173
value={props.profileState.emailAddress || ""}
174
disabled={props.emailIsReadonly}
175
onChange={(val) => {
176
props.setProfileState({ ...props.profileState, emailAddress: val });
177
}}
178
/>
179
{props.user && (
180
<InputField label="User ID">
181
<InputWithCopy value={props.user.id} tip="Copy User ID" />
182
</InputField>
183
)}
184
</fieldset>
185
<div className="lg:pl-14">
186
<div className="mt-4">
187
<Subheading>Avatar</Subheading>
188
<img
189
className="rounded-full w-24 h-24"
190
src={props.profileState.avatarUrl}
191
alt={props.profileState.name}
192
/>
193
</div>
194
</div>
195
</div>
196
{props.children || null}
197
</div>
198
);
199
}
200
201