Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/user-settings/SSHKeys.tsx
2500 views
1
/**
2
* Copyright (c) 2022 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, useEffect, useState } from "react";
8
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
9
import Alert from "../components/Alert";
10
import { Item, ItemField, ItemFieldContextMenu } from "../components/ItemsList";
11
import ConfirmationModal from "../components/ConfirmationModal";
12
import { SSHPublicKeyValue } from "@gitpod/gitpod-protocol";
13
import dayjs from "dayjs";
14
import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
15
import { Heading2, Subheading } from "../components/typography/headings";
16
import { EmptyMessage } from "../components/EmptyMessage";
17
import { Button } from "@podkit/buttons/Button";
18
import { sshClient } from "../service/public-api";
19
import { SSHPublicKey } from "@gitpod/public-api/lib/gitpod/v1/ssh_pb";
20
import { InputField } from "../components/forms/InputField";
21
import { TextInputField } from "../components/forms/TextInputField";
22
23
interface AddModalProps {
24
value: SSHPublicKeyValue;
25
onClose: () => void;
26
onSave: () => void;
27
}
28
29
interface DeleteModalProps {
30
value: SSHPublicKey;
31
onConfirm: () => void;
32
onClose: () => void;
33
}
34
35
export function AddSSHKeyModal(props: AddModalProps) {
36
const [errorMsg, setErrorMsg] = useState("");
37
38
const [value, setValue] = useState({ ...props.value });
39
const update = (pev: Partial<SSHPublicKeyValue>) => {
40
setValue({ ...value, ...pev });
41
setErrorMsg("");
42
};
43
44
useEffect(() => {
45
setValue({ ...props.value });
46
setErrorMsg("");
47
}, [props.value]);
48
49
const save = async () => {
50
const tmp = SSHPublicKeyValue.validate(value);
51
if (tmp) {
52
setErrorMsg(tmp);
53
return;
54
}
55
try {
56
await sshClient.createSSHPublicKey(value);
57
props.onClose();
58
props.onSave();
59
} catch (e) {
60
setErrorMsg(e.message.replace("Request addSSHPublicKey failed with message: ", ""));
61
return;
62
}
63
};
64
65
return (
66
<Modal visible onClose={props.onClose} onSubmit={save}>
67
<ModalHeader>New SSH Key</ModalHeader>
68
<ModalBody>
69
{errorMsg.length > 0 && (
70
<Alert type="error" className="mb-2">
71
{errorMsg}
72
</Alert>
73
)}
74
<div className="text-gray-500 dark:text-gray-400 text-md">
75
Add an SSH key for secure access to workspaces via SSH.
76
</div>
77
<Alert type="info" className="mt-2">
78
SSH key are used to connect securely to workspaces.{" "}
79
<a
80
href="https://www.gitpod.io/docs/configure/user-settings/ssh#create-an-ssh-key"
81
target="gitpod-create-ssh-key-doc"
82
className="gp-link"
83
>
84
Learn how to create an SSH Key
85
</a>
86
</Alert>
87
<InputField label="Key">
88
<textarea
89
autoFocus
90
style={{ height: "160px" }}
91
className="w-full resize-none"
92
value={value.key}
93
placeholder="Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256',
94
'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521',
95
'ssh-ed25519',
96
'[email protected]', or
97
'[email protected]'"
98
onChange={(v) => update({ key: v.target.value })}
99
/>
100
</InputField>
101
102
<TextInputField
103
label="Title"
104
placeholder="e.g. laptop"
105
type="text"
106
value={value.name}
107
onChange={(val) => update({ name: val })}
108
/>
109
</ModalBody>
110
<ModalFooter>
111
<Button variant="secondary" onClick={props.onClose}>
112
Cancel
113
</Button>
114
<Button type="submit">Add SSH Key</Button>
115
</ModalFooter>
116
</Modal>
117
);
118
}
119
120
export function DeleteSSHKeyModal(props: DeleteModalProps) {
121
const confirmDelete = useCallback(async () => {
122
await sshClient.deleteSSHPublicKey({ sshKeyId: props.value.id! });
123
props.onConfirm();
124
props.onClose();
125
}, [props]);
126
127
return (
128
<ConfirmationModal
129
title="Delete SSH Key"
130
areYouSureText="Are you sure you want to delete this SSH Key?"
131
buttonText="Delete SSH Key"
132
onClose={props.onClose}
133
onConfirm={confirmDelete}
134
>
135
<Item solid>
136
<KeyItem sshKey={props.value}></KeyItem>
137
</Item>
138
</ConfirmationModal>
139
);
140
}
141
142
export default function SSHKeys() {
143
const [dataList, setDataList] = useState<SSHPublicKey[]>([]);
144
const [currentData, setCurrentData] = useState<SSHPublicKeyValue>({ name: "", key: "" });
145
const [currentDelData, setCurrentDelData] = useState<SSHPublicKey>();
146
const [showAddModal, setShowAddModal] = useState(false);
147
const [showDelModal, setShowDelModal] = useState(false);
148
149
const loadData = () => {
150
sshClient.listSSHPublicKeys({}).then((r) => setDataList(r.sshKeys));
151
};
152
153
useEffect(() => {
154
loadData();
155
}, []);
156
157
const addOne = () => {
158
setCurrentData({ name: "", key: "" });
159
setShowAddModal(true);
160
setShowDelModal(false);
161
};
162
163
const deleteOne = (value: SSHPublicKey) => {
164
setCurrentDelData(value);
165
setShowAddModal(false);
166
setShowDelModal(true);
167
};
168
169
return (
170
<PageWithSettingsSubMenu>
171
{showAddModal && (
172
<AddSSHKeyModal value={currentData} onSave={loadData} onClose={() => setShowAddModal(false)} />
173
)}
174
{showDelModal && (
175
<DeleteSSHKeyModal
176
value={currentDelData!}
177
onConfirm={loadData}
178
onClose={() => setShowDelModal(false)}
179
/>
180
)}
181
<div className="flex items-start sm:justify-between mb-2">
182
<div>
183
<Heading2>SSH Keys</Heading2>
184
<Subheading>
185
Create and manage SSH keys.{" "}
186
<a
187
className="gp-link"
188
href="https://www.gitpod.io/docs/configure/user-settings/ssh"
189
target="_blank"
190
rel="noreferrer"
191
>
192
Learn more
193
</a>
194
</Subheading>
195
</div>
196
{dataList.length !== 0 ? (
197
<div className="mt-3 flex">
198
<Button onClick={addOne} className="ml-2">
199
New SSH Key
200
</Button>
201
</div>
202
) : null}
203
</div>
204
{dataList.length === 0 ? (
205
<EmptyMessage
206
title="No SSH Keys"
207
subtitle={
208
<span>
209
SSH keys allow you to establish a <b>secure connection</b> between your <b>computer</b> and{" "}
210
<b>workspaces</b>.
211
</span>
212
}
213
buttonText="New SSH Key"
214
onClick={addOne}
215
/>
216
) : (
217
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
218
{dataList.map((key) => {
219
return (
220
<Item key={key.id} solid className="items-start">
221
<KeyItem sshKey={key}></KeyItem>
222
<ItemFieldContextMenu
223
position="start"
224
menuEntries={[
225
{
226
title: "Delete",
227
customFontStyle:
228
"text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",
229
onClick: () => deleteOne(key),
230
},
231
]}
232
/>
233
</Item>
234
);
235
})}
236
</div>
237
)}
238
</PageWithSettingsSubMenu>
239
);
240
}
241
242
function KeyItem(props: { sshKey: SSHPublicKey }) {
243
const key = props.sshKey;
244
return (
245
<ItemField className="flex flex-col gap-y box-border overflow-hidden">
246
<p className="truncate text-gray-400 dark:text-gray-600">SHA256:{key.fingerprint}</p>
247
<div className="truncate my-1 text-xl text-gray-800 dark:text-gray-100 font-semibold">{key.name}</div>
248
<p className="truncate mt-4">Added on {dayjs(key.creationTime!.toDate()).format("MMM D, YYYY, hh:mm A")}</p>
249
{!!key.lastUsedTime && (
250
<p className="truncate">
251
Last used on {dayjs(key.lastUsedTime!.toDate()).format("MMM D, YYYY, hh:mm A")}
252
</p>
253
)}
254
</ItemField>
255
);
256
}
257
258