Line data Source code
1 : /*
2 : * Copyright (C) 2020 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 15 : import cockpit from 'cockpit';
7 15 : import React from 'react';
8 : import { useObject, useEvent } from 'hooks.js';
9 :
10 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
11 : import { Card, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
12 : import { OverflowMenu, OverflowMenuContent, OverflowMenuControl, OverflowMenuDropdownItem, OverflowMenuGroup, OverflowMenuItem } from "@patternfly/react-core/dist/esm/components/OverflowMenu/index.js";
13 : import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
14 : import { show_modal_dialog } from "cockpit-components-dialog.jsx";
15 : import { ListingTable } from 'cockpit-components-table.jsx';
16 : import { KebabDropdown } from 'cockpit-components-dropdown.jsx';
17 : import { show_unexpected_error } from "./dialog-utils.js";
18 : import * as authorized_keys from './authorized-keys.js';
19 :
20 15 : const _ = cockpit.gettext;
21 :
22 0 : function AddAuthorizedKeyDialogBody({ state, change }) {
23 0 : const { text } = state;
24 :
25 0 : return (
26 0 : <TextArea id="authorized-keys-text"
27 0 : placeholder={_("Paste the contents of your public SSH key file here")}
28 0 : className="form-control"
29 0 : value={text} onChange={(_event, value) => change("text", value)} />
30 : );
31 0 : }
32 :
33 0 : function add_authorized_key_dialog(keys) {
34 0 : let dlg = null;
35 0 : const state = {
36 0 : text: ""
37 0 : };
38 :
39 0 : function change(field, value) {
40 0 : state[field] = value;
41 0 : update();
42 0 : }
43 :
44 0 : function update() {
45 0 : const props = {
46 0 : id: "add-authorized-key-dialog",
47 0 : title: _("Add public key"),
48 0 : body: <AddAuthorizedKeyDialogBody state={state} change={change} />
49 0 : };
50 :
51 0 : const footer = {
52 0 : actions: [
53 0 : {
54 0 : caption: _("Add"),
55 0 : style: "primary",
56 0 : clicked: () => {
57 0 : return keys.add_key(state.text);
58 0 : }
59 0 : }
60 0 : ]
61 0 : };
62 :
63 0 : if (!dlg)
64 0 : dlg = show_modal_dialog(props, footer);
65 0 : else {
66 0 : dlg.setProps(props);
67 0 : dlg.setFooterProps(footer);
68 0 : }
69 0 : }
70 :
71 0 : update();
72 0 : }
73 :
74 10 : export function AuthorizedKeys({ name, home, allow_mods }) {
75 10 : const manager = useObject(() => authorized_keys.instance(name, home),
76 3 : manager => manager.close(),
77 10 : [name, home]);
78 10 : useEvent(manager, "changed");
79 :
80 1 : function remove_key(raw) {
81 1 : manager.remove_key(raw).catch(show_unexpected_error);
82 1 : }
83 :
84 10 : const { state, keys } = manager;
85 10 : let error = "";
86 :
87 10 : if (state == "access-denied")
88 2 : error = _("You do not have permission to view the authorized public keys for this account.");
89 10 : else if (state == "failed")
90 2 : error = _("Failed to load authorized keys.");
91 10 : else if (state == "ready")
92 10 : error = _("There are no authorized public keys for this account.");
93 : else
94 10 : return null;
95 :
96 10 : const actions = allow_mods && (
97 0 : <Button variant="secondary" id="authorized-key-add" onClick={() => add_authorized_key_dialog(manager)}>
98 10 : {_("Add key")}
99 10 : </Button>
100 : );
101 :
102 10 : return (
103 10 : <Card isPlain id="account-authorized-keys">
104 10 : <CardHeader actions={{ actions }}>
105 10 : <CardTitle component="h2">{_("Authorized public SSH keys")}</CardTitle>
106 10 : </CardHeader>
107 10 : <ListingTable
108 10 : aria-label={ _("Authorized public SSH keys") }
109 10 : id="account-authorized-keys-list"
110 10 : showHeader={false}
111 10 : emptyCaption={error}
112 10 : columns={ [
113 10 : { title: _("Name"), header: true },
114 10 : { title: _("Fingerprint") },
115 10 : { title: "" },
116 10 : ] }
117 5 : rows={keys.map(k => {
118 5 : return {
119 5 : columns: [
120 1 : { title: k.comment || _("Unnamed"), props: { width: 20 } },
121 1 : { title: k.fp || _("Invalid key"), props: { width: 80 } },
122 5 : {
123 5 : title: <OverflowMenu breakpoint="lg">
124 5 : <OverflowMenuContent>
125 5 : <OverflowMenuGroup groupType="button">
126 5 : <OverflowMenuItem>
127 1 : <Button key={k.fp} variant="secondary" onClick={() => remove_key(k.raw)}>
128 5 : {_("Remove")}
129 5 : </Button>
130 5 : </OverflowMenuItem>
131 5 : </OverflowMenuGroup>
132 5 : </OverflowMenuContent>
133 5 : <OverflowMenuControl>
134 5 : <KebabDropdown position="right" dropdownItems={[
135 0 : <OverflowMenuDropdownItem key="delete" isShared onClick={() => remove_key(k.raw)}>
136 5 : {_("Remove")}
137 5 : </OverflowMenuDropdownItem>]}
138 5 : />
139 5 : </OverflowMenuControl>
140 5 : </OverflowMenu>,
141 5 : props: { className: "pf-v6-c-table__action" }
142 5 : }
143 5 : ],
144 5 : props: { key: k.fp }
145 5 : };
146 5 : })} />
147 10 : </Card>
148 : );
149 10 : }
|