Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shell: More types than ever #21426

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkg/lib/cockpit.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,7 @@ declare module 'cockpit' {

/* === Session ====================== */
function logout(reload: boolean, reason?: string): void;

export let localStorage: Storage;
export let sessionStorage: Storage;
}
2 changes: 1 addition & 1 deletion pkg/lib/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class KeyLoadError extends Error {
}
}

class Keys extends EventTarget {
export class Keys extends EventTarget {
path: string | null = null;
items: Record<string, Key> = { };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* along with Cockpit; If not, see <https://www.gnu.org/licenses/>.
*/

// @cockpit-ts-relaxed

import cockpit from "cockpit";

import React, { useState } from "react";
Expand All @@ -27,9 +29,11 @@ import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
import { useInit } from "hooks";

import { ShellState } from "./state";

const _ = cockpit.gettext;

export const ActivePagesDialog = ({ dialogResult, state }) => {
export const ActivePagesDialog = ({ dialogResult, state } : { dialogResult, state: ShellState }) => {
function get_pages() {
const result = [];
for (const frame of Object.values(state.frames)) {
Expand Down
256 changes: 142 additions & 114 deletions pkg/shell/credentials.jsx → pkg/shell/credentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,120 +39,28 @@ import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
import { ListingTable } from 'cockpit-components-table.jsx';
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { useEvent, useObject } from 'hooks';
import { DialogResult } from "dialogs";

import "./credentials.scss";

const _ = cockpit.gettext;

export const CredentialsModal = ({ dialogResult }) => {
const keys = useObject(() => credentials.keys_instance(), null, []);
const [addNewKey, setAddNewKey] = useState(false);
const [dialogError, setDialogError] = useState();
const [unlockKey, setUnlockKey] = useState();

useEvent(keys, "changed");

if (!keys)
return null;

function onToggleKey(id, enable) {
const key = keys.items[id];

if (!key || !key.name)
return;

/* Key needs to be loaded, show load UI */
if (enable && !key.loaded) {
setUnlockKey(key.name);
/* Key needs to be unloaded, do that directly */
} else if (!enable && key.loaded) {
keys.unload(key).catch(ex => setDialogError(ex.message));
}
}

return (
<>
<Modal isOpen position="top" variant="medium"
onClose={() => dialogResult.resolve()}
title={_("SSH keys")}
id="credentials-modal"
footer={<Button variant='secondary' onClick={() => dialogResult.resolve()}>{_("Close")}</Button>}
>
<Stack hasGutter>
{dialogError && <ModalError dialogError={dialogError} />}
<Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
<FlexItem>{_("Use the following keys to authenticate against other systems")}</FlexItem>
<Button variant='secondary'
id="ssh-file-add-custom"
onClick={() => setAddNewKey(true)}>
{_("Add key")}
</Button>
</Flex>
{addNewKey && <AddNewKey keys={keys} unlockKey={setUnlockKey} onClose={() => setAddNewKey(false)} />}
<ListingTable
aria-label={ _("SSH keys") }
gridBreakPoint=''
id="credential-keys"
showHeader={false}
variant="compact"
columns={ [
{ title: _("Name"), header: true },
{ title: _("Toggle") },
] }
rows={ Object.keys(keys.items).map((currentKeyId, index) => {
const currentKey = keys.items[currentKeyId] || { name: 'test' };
const tabRenderers = [
{
data: { currentKey },
name: _("Details"),
renderer: KeyDetails,
},
{
data: { currentKey },
name: _("Public key"),
renderer: PublicKey,
},
{
data: { currentKey, keys, setDialogError },
name: _("Password"),
renderer: KeyPassword,
},
];
const expandedContent = (
<ListingPanel tabRenderers={tabRenderers} />
);

return ({
columns: [
{
title: currentKey.name || currentKey.comment,
},
{
title: <Switch aria-label={_("Use key")}
isChecked={!!currentKey.loaded}
key={"switch-" + index}
onChange={(_event, value) => onToggleKey(currentKeyId, value)} />,
}
],
expandedContent,
props: { key: currentKey.fingerprint, 'data-name': currentKey.name || currentKey.comment, 'data-loaded': !!currentKey.loaded },
});
})} />
</Stack>
</Modal>
{unlockKey && <UnlockKey keyName={unlockKey} keys={keys} onClose={() => { setUnlockKey(undefined); setAddNewKey(false) }} />}
</>
);
};

const AddNewKey = ({ keys, unlockKey, onClose }) => {
const AddNewKey = ({
keys,
unlockKey,
onClose
} : {
keys: credentials.Keys,
unlockKey: (name: string) => void,
onClose: () => void
}) => {
const [addNewKeyLoading, setAddNewKeyLoading] = useState(false);
const [newKeyPath, setNewKeyPath] = useState("");
const [newKeyPathError, setNewKeyPathError] = useState();
const [newKeyPathError, setNewKeyPathError] = useState("");

const addCustomKey = () => {
setAddNewKeyLoading(true);
keys.load(newKeyPath)
keys.load(newKeyPath, "")
.then(onClose)
.catch(ex => {
if (!(ex instanceof credentials.KeyLoadError) || !ex.sent_password)
Expand Down Expand Up @@ -186,7 +94,7 @@ const AddNewKey = ({ keys, unlockKey, onClose }) => {
);
};

const KeyDetails = ({ currentKey }) => {
const KeyDetails = ({ currentKey } : { currentKey: credentials.Key }) => {
return (
<DescriptionList className="pf-m-horizontal-on-sm">
<DescriptionListGroup>
Expand All @@ -205,17 +113,25 @@ const KeyDetails = ({ currentKey }) => {
);
};

const PublicKey = ({ currentKey }) => {
const PublicKey = ({ currentKey } : { currentKey: credentials.Key }) => {
return (
<ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")} variant={ClipboardCopyVariant.expansion}>
{currentKey.data.trim()}
</ClipboardCopy>
);
};

const KeyPassword = ({ currentKey, keys, setDialogError }) => {
const KeyPassword = ({
currentKey,
keys,
setDialogError
} : {
currentKey: credentials.Key,
keys: credentials.Keys,
setDialogError: (msg: string | null) => void
}) => {
const [confirmPassword, setConfirmPassword] = useState('');
const [inProgress, setInProgress] = useState(undefined);
const [inProgress, setInProgress] = useState<boolean | undefined>(undefined);
const [newPassword, setNewPassword] = useState('');
const [oldPassword, setOldPassword] = useState('');

Expand All @@ -224,7 +140,7 @@ const KeyPassword = ({ currentKey, keys, setDialogError }) => {
return;

setInProgress(true);
setDialogError();
setDialogError(null);

if (oldPassword === undefined || newPassword === undefined || confirmPassword === undefined)
setDialogError("Invalid password fields");
Expand All @@ -245,8 +161,7 @@ const KeyPassword = ({ currentKey, keys, setDialogError }) => {
const changePasswordBtn = (
<Button variant="primary"
id={(currentKey.name || currentKey.comment) + "-change-password"}
isDisabled={inProgress}
isLoading={inProgress}
{... inProgress !== undefined ? { isDisabled: inProgress, isLoading: inProgress } : {} }
onClick={() => changePassword()}>{_("Change password")}</Button>
);

Expand Down Expand Up @@ -285,9 +200,17 @@ const KeyPassword = ({ currentKey, keys, setDialogError }) => {
);
};

const UnlockKey = ({ keyName, keys, onClose }) => {
const [password, setPassword] = useState();
const [dialogError, setDialogError] = useState();
const UnlockKey = ({
keyName,
keys,
onClose
} : {
keyName: string,
keys: credentials.Keys,
onClose: () => void
}) => {
const [password, setPassword] = useState("");
const [dialogError, setDialogError] = useState("");

function load_key() {
if (!keyName)
Expand Down Expand Up @@ -320,3 +243,108 @@ const UnlockKey = ({ keyName, keys, onClose }) => {
</Modal>
);
};

export const CredentialsModal = ({
dialogResult
} : {
dialogResult: DialogResult<void>
}) => {
const keys = useObject(() => credentials.keys_instance(), null, []);
const [addNewKey, setAddNewKey] = useState(false);
const [dialogError, setDialogError] = useState();
const [unlockKey, setUnlockKey] = useState<string | undefined>();

useEvent(keys as unknown as cockpit.EventSource<cockpit.EventMap>, "changed");

if (!keys)
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.


function onToggleKey(id: string, enable: boolean) {
const key = keys.items[id];

if (!key || !key.name)
return;

/* Key needs to be loaded, show load UI */
if (enable && !key.loaded) {
setUnlockKey(key.name);
/* Key needs to be unloaded, do that directly */
} else if (!enable && key.loaded) {
keys.unload(key).catch(ex => setDialogError(ex.message));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

}
}

return (
<>
<Modal isOpen position="top" variant="medium"
onClose={() => dialogResult.resolve()}
title={_("SSH keys")}
id="credentials-modal"
footer={<Button variant='secondary' onClick={() => dialogResult.resolve()}>{_("Close")}</Button>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

>
<Stack hasGutter>
{dialogError && <ModalError dialogError={dialogError} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

<Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
<FlexItem>{_("Use the following keys to authenticate against other systems")}</FlexItem>
<Button variant='secondary'
id="ssh-file-add-custom"
onClick={() => setAddNewKey(true)}>
{_("Add key")}
</Button>
</Flex>
{addNewKey && <AddNewKey keys={keys} unlockKey={setUnlockKey} onClose={() => setAddNewKey(false)} />}
<ListingTable
aria-label={ _("SSH keys") }
gridBreakPoint=''
id="credential-keys"
showHeader={false}
variant="compact"
columns={ [
{ title: _("Name"), header: true },
{ title: _("Toggle") },
] }
rows={ Object.keys(keys.items).map((currentKeyId, index) => {
const currentKey = keys.items[currentKeyId] || { name: 'test' };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

const tabRenderers = [
{
data: { currentKey },
name: _("Details"),
renderer: KeyDetails,
},
{
data: { currentKey },
name: _("Public key"),
renderer: PublicKey,
},
{
data: { currentKey, keys, setDialogError },
name: _("Password"),
renderer: KeyPassword,
},
];
const expandedContent = (
<ListingPanel tabRenderers={tabRenderers} />
);

return ({
columns: [
{
title: currentKey.name || currentKey.comment,
},
{
title: <Switch aria-label={_("Use key")}
isChecked={!!currentKey.loaded}
key={"switch-" + index}
onChange={(_event, value) => onToggleKey(currentKeyId, value)} />,
}
],
expandedContent,
props: { key: currentKey.fingerprint, 'data-name': currentKey.name || currentKey.comment, 'data-loaded': !!currentKey.loaded },
});
})} />
</Stack>
</Modal>
{unlockKey && <UnlockKey keyName={unlockKey} keys={keys} onClose={() => { setUnlockKey(undefined); setAddNewKey(false) }} />}
</>
);
};
Loading
Loading