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

Allow adding multiple ssh keys at once #1309

Closed
wants to merge 1 commit into from

Conversation

jelly
Copy link
Member

@jelly jelly commented Nov 14, 2023

The initial version of the ssh key functionality would only add the first key if multiple where provided. Allow adding multiple keys in via a new helper, the validation now needs to happen via SshKeysRow as they are added dynamically and the value provided via properties.

Closes #1300

The initial version of the ssh key functionality would only add the
first key if multiple where provided. Allow adding multiple keys in via
a new helper, the validation now needs to happen via SshKeysRow as they
are added dynamically and the value provided via properties.

Closes cockpit-project#1300
Comment on lines +783 to +784
if (part.trim() === "") {
continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

These 2 added lines are not executed by any test. Details

Comment on lines +786 to +787
if (index !== idx) {
additem();
Copy link
Contributor

Choose a reason for hiding this comment

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

These 2 added lines are not executed by any test. Details

Copy link
Member

Choose a reason for hiding this comment

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

This is true. The test with a \n between two SSH keys never creates a second item. The \n does not make it into the textrea with our set_input_text function on Chromium, one has to use \r ro produce a line break.

The test still passes since the check for expected keys is pretty sloppy and doesn't consider line breaks either.

@jelly jelly requested a review from KKoukiou November 14, 2023 19:40
useEffect(() => {
let timeoutId = null;
if (keyValue !== "" && !keyObject) {
timeoutId = setTimeout(() => parseKey(keyValue, setKeyObject, setKeyInvalid), 500);
Copy link
Member

Choose a reason for hiding this comment

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

I think the original debounce comment still applies here -- this useEffect() re-runs on every input, so it creates multiple parallel timers and calls parseKey() for each one as well. Some debouncing still needs to happen.

Copy link
Member Author

Choose a reason for hiding this comment

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

The issue with a global debounce is that it is used for multiple components, so if you add two keys quickly only one will get parsed as the other call is debounced. So debouncing needs to happen per Component and we can't keep the debounce function in the component as every re-render it would be re-instantiated. So I believe the current approach with setTimeout debounces correctly, when a new as a new value would cause a re-render so clearTimeout() to be called?

If I'm wrong I'm happy to fix it if there is a better way.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not saying that the original wrapping of parseKey() into debounce was right -- it's not, it's a hack which doesn't belong there at all. I'm saying that I dislike useEffect() here. It's too disconnected from the actual input that it wants to debounce, it duplicates debouncing logic, has setState() calls in it which are prone to recursion, is too hard to understand, and above of all, it really shouldn't be necessary here.

I mean, what is this trying to do? It's a controlled component, so onChange must immediately call setKeyValue() so that the UI doesn't lag. The expensive bit is the validation because of parseKey(), so the onChange handler should wrap that part into a debunce(). None of that requires useEffect()?

(That said: of course I may miss something here)

Copy link
Member Author

Choose a reason for hiding this comment

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

We can not make onChange call setKeyValue simply because onChange will add a new row if multiple keys are added using additem(). That is the hard part, dynamically creating a new row component.

Copy link
Member

Choose a reason for hiding this comment

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

That sounds weird -- in JSX it's just a single TextArea. If this somehow multiplies the TextArea, this needs fixing -- either we shouldn't multiply it, or have a full-blown dynamic set of input lines where each one just has a single key, and ➕ 🗑️ buttons

Copy link
Member

Choose a reason for hiding this comment

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

we can't keep the debounce function in the component as every re-render it would be re-instantiated.

We can use a hook to avoid the re-instantiation on re-render. Like this:

diff --git a/src/components/create-vm-dialog/createVmDialog.jsx b/src/components/create-vm-dialog/createVmDialog.jsx
index 8a2dd72..88ce41a 100644
--- a/src/components/create-vm-dialog/createVmDialog.jsx
+++ b/src/components/create-vm-dialog/createVmDialog.jsx
@@ -37,6 +37,7 @@ import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner";
 import { ExternalLinkAltIcon, TrashIcon } from '@patternfly/react-icons';
 
 import { DialogsContext } from 'dialogs.jsx';
+import { useInit } from "hooks.js";
 import cockpit from 'cockpit';
 import store from "../../store.js";
 import { MachinesConnectionSelector } from '../common/machinesConnectionSelector.jsx';
@@ -752,13 +753,16 @@ const UsersConfigurationRow = ({
     );
 };
 
-// This method needs to be outside of component as re-render would create a new instance of debounce
-const parseKey = debounce(500, (key, setKeyObject, setKeyInvalid) => {
-    if (isEmpty(key))
-        return;
+const useParsedKey = (value) => {
+    const [keyObject, setKeyObject] = useState();
+    const [keyInvalid, setKeyInvalid] = useState(false);
+
+    const parseKey = (key) => {
+        if (isEmpty(key))
+            return;
 
-    // Validate correctness of the key
-    return cockpit.spawn(["ssh-keygen", "-l", "-f", "-"], { err: "message" })
+        // Validate correctness of the key
+        return cockpit.spawn(["ssh-keygen", "-l", "-f", "-"], { err: "message" })
             .input(key)
             .then(() => {
                 setKeyInvalid(false);
@@ -776,21 +780,33 @@ const parseKey = debounce(500, (key, setKeyObject, setKeyInvalid) => {
                 setKeyInvalid(true);
                 console.warn("Could not validate the public key");
             });
-});
+    };
+
+    const debouncedParseKey = useInit(() => debounce(500, parseKey));
+    useInit(() => debouncedParseKey(value), [ value ]);
+
+    return [keyObject, keyInvalid];
+};
 
 const SshKeysRow = ({
-    id, item, onChange, idx, removeitem,
+    id, item, onChange, idx, removeitem, additem
 }) => {
-    const [keyObject, setKeyObject] = useState();
-    const [keyInvalid, setKeyInvalid] = useState(false);
+    const [keyObject, keyInvalid] = useParsedKey(item.value);
 
     const onChangeHelper = (value) => {
-        // Some users might want to input multiple keys into the same field
-        // While handling that in the future might be a nice user experience, now we only parse one key out of input
-        value = value.split(/\r?\n/)[0];
+        const lines = value.split(/\r?\n/);
 
+        // The first line is for us.
+        value = lines[0];
         onChange(idx, "value", value);
-        parseKey(value, setKeyObject, setKeyInvalid);
+
+        // If there are more lines, create additional entries for them.
+        for (let i = 1; i < lines.length; i++) {
+            if (lines[i].length > 0) {
+                additem();
+                onChange(idx + i, "value", lines[i]);
+            }
+        }
     };
 
     return (

Copy link
Collaborator

@KKoukiou KKoukiou left a comment

Choose a reason for hiding this comment

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

Does this allow to input multiple keys in one text area field? If so, this seems a bit off by design perspective.

@mvollmer mvollmer self-assigned this Mar 25, 2024
@mvollmer
Copy link
Member

My version of this: #1515

let index = idx;
for (const part of value.split("\n")) {
if (part.trim() === "") {
continue;
Copy link
Member

Choose a reason for hiding this comment

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

This means that the last character in the textarea can never be deleted.

@jelly
Copy link
Member Author

jelly commented Mar 28, 2024

Fixed in #1515

@jelly jelly closed this Mar 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

There should be an hint to clarify that only the first SSH key could take effect for each "Public key" input
5 participants