aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js128
1 files changed, 55 insertions, 73 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index bde218b..29aa7eb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -22,21 +22,23 @@ export const INVITE_EMAIL_PREF = 'invite_email';
* One component, two modes, because the Create Group wizard and the Settings
* page were drifting apart while showing the same thing:
*
- * mode="live" — a hosted group. Every change is a signed operator op sent
- * over MNP, or the loopback API when the node is on this
- * machine and there is no live connection.
+ * mode="live" — a hosted group. What widens the node's sharing — adding
+ * a directory, its `writable` and `removable` switches — goes
+ * through the loopback API only, so only on the node's own
+ * machine. Removing, ejecting and plugging are signed ops
+ * over MNP, or the loopback API when there is no connection.
* mode="local" — the wizard, before the group exists. Changes are held in
* an array the caller owns; nothing is persisted until the
* group is attached.
*
- * **Both paths matter and neither is optional.** The operator of a node is not
- * necessarily sitting at it: they may be signing in from any browser, and the
- * only thing that reaches their node from there is MNP. An earlier version of
- * this read its roots exclusively from the loopback API, which resolves to
- * "not available" in a browser — so the section rendered for nobody on the
- * web, while the controls it replaced had worked there. `mnpRoots` is the
- * source whenever a connection exists; the loopback list is the fallback for
- * a local node that is not currently connected (a group still scanning, say).
+ * **The list is shown wherever the operator is.** They may be signing in from
+ * any browser, and the only thing that reaches their node from there is MNP.
+ * An earlier version of this read its roots exclusively from the loopback API,
+ * which resolves to "not available" in a browser — so the section rendered for
+ * nobody on the web. `mnpRoots` is the source whenever a connection exists; the
+ * loopback list is the fallback for a local node that is not currently
+ * connected (a group still scanning, say). From a browser the table is read
+ * only where it would widen anything (MNP 6.0).
*
* Props:
* roots — the node's current roots: { name, path, writable,
@@ -75,8 +77,6 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
<p class=${msg.error ? 'error-msg' : 'settings-hint'}
role=${msg.error ? 'alert' : 'status'}
style="margin-top:10px">${msg.text}</p>`;
- const [pathDraft, setPathDraft] = useState('');
- const [addingByPath, setAddingByPath] = useState(false);
// A toggle has to move under the finger, and the answer only comes back
// when the node has signed, written node.toml and pushed the new table.
@@ -115,12 +115,20 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
// left out otherwise, rather than printing a row of blanks.
const hasPaths = displayRoots.some((r) => r.path);
- // Which door a change goes through. MNP first: it is the only one that
- // exists for an operator on the web, and it is signed, which the loopback
- // API is not (it is authorized by being on localhost with the run token).
+ // Which door a change goes through.
+ //
+ // Widening what the node shares — a new directory, `writable`, `removable` —
+ // only through the loopback API, which exists only on the node's own
+ // machine. Over MNP these were signed ops, and a
+ // signature proves that the operator's key signed, not that the operator
+ // meant it: in a browser that key is driven by code the hub serves.
+ //
+ // Narrowing — remove, eject, plug — MNP first, then loopback.
const overMnp = !isLocal && transport && transport.connected;
const overLoopback = !isLocal && !overMnp && nodeAvail;
+ const onNodeMachine = !isLocal && nodeAvail;
const canEdit = isLocal || overMnp || overLoopback;
+ const canWiden = isLocal || onNodeMachine;
// Deliberately no index refresh after a root change.
//
@@ -158,9 +166,8 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
...prev, [rootName]: { ...(prev[rootName] || {}), ...updates },
}));
const ok = await run(async () => {
- if (overMnp) await transport.updateRoot(groupId, rootName, updates, signFn);
- else if (overLoopback) await platform.node.op('updateRoot', { groupId, rootName, updates });
- else throw new Error(t('node.root_no_route'));
+ if (onNodeMachine) await platform.node.op('updateRoot', { groupId, rootName, updates });
+ else throw new Error(t('settings_node.roots_local_only_hint'));
});
// Only a failure clears the patch here; a success waits for the node's
// own table, so the switch never travels backwards on its way forwards.
@@ -169,8 +176,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
const next = { ...prev }; delete next[rootName]; return next;
});
}
- }, [isLocal, localRoots, onLocalRootsChange, overMnp, overLoopback,
- transport, groupId, signFn, run]);
+ }, [isLocal, localRoots, onLocalRootsChange, onNodeMachine, groupId, run]);
const doEjectRoot = useCallback((rootName) => run(async () => {
if (overMnp) await transport.ejectRoot(groupId, rootName, signFn);
@@ -203,10 +209,9 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
}, [isLocal, localRoots, onLocalRootsChange, overMnp, overLoopback,
transport, groupId, signFn, run]);
- // Adding a root needs a directory that exists on the *node's* filesystem.
- // With the node on this machine that is a native folder picker; from any
- // other browser the operator has to type the path, because nothing in a web
- // page can browse a remote disk. Both end at the same signed op.
+ // Adding a root: a native folder picker on the node's own machine, and
+ // nothing anywhere else — a path typed into a page is a path a script in the
+ // page could have typed.
const addRootAtPath = useCallback(async (path, name) => {
if (isLocal) {
if ((localRoots || []).some(r => r.path === path)) return true;
@@ -222,16 +227,12 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
}
setIndexProgress(null);
return run(async () => {
- if (overMnp) {
- await transport.addRoot(groupId, path, { name }, signFn);
- } else if (overLoopback) {
- await platform.node.op('addRoot', { groupId, path, name });
- await platform.node.op('reload');
- await platform.watchIndexProgress(groupId, setIndexProgress);
- } else throw new Error(t('node.root_no_route'));
+ if (!onNodeMachine) throw new Error(t('settings_node.roots_local_only_hint'));
+ await platform.node.op('addRoot', { groupId, path, name });
+ await platform.node.op('reload');
+ await platform.watchIndexProgress(groupId, setIndexProgress);
});
- }, [isLocal, localRoots, onLocalRootsChange, overMnp, overLoopback,
- transport, groupId, signFn, run]);
+ }, [isLocal, localRoots, onLocalRootsChange, onNodeMachine, groupId, run]);
const doPickRoot = useCallback(async () => {
const chosen = await platform.rootPicker.choose();
@@ -240,48 +241,23 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
if (ok && !isLocal) say(t('node.root_added'));
}, [addRootAtPath, isLocal]);
- const doAddByPath = useCallback(async () => {
- const path = pathDraft.trim();
- if (!path) return;
- // The name is the node's business — it derives the basename and refuses a
- // duplicate. Sending one guessed from a string typed here would be a
- // second opinion about something already decided in one place.
- const ok = await addRootAtPath(path, '');
- if (ok) { setPathDraft(''); setAddingByPath(false); if (!isLocal) say(t('node.root_added')); }
- }, [pathDraft, addRootAtPath, isLocal]);
-
- const addControls = !canEdit ? '' : html`
- ${platform.rootPicker.available ? html`
- <button class="btn btn-small btn-secondary" style="margin-top:8px"
- disabled=${busy} onClick=${doPickRoot}>
- <${Icon} name="folder-plus" /> ${t('node.add_root')}
- </button>
- ` : addingByPath ? html`
- <div class="sdt-add-row">
- <input class="sdt-add-input" type="text" value=${pathDraft}
- placeholder=${t('node.root_path_placeholder')}
- disabled=${busy}
- onInput=${(e) => setPathDraft(e.target.value)}
- onKeyDown=${(e) => { if (e.key === 'Enter') doAddByPath(); }} />
- <button class="btn btn-small btn-secondary" disabled=${busy || !pathDraft.trim()}
- onClick=${doAddByPath}>${t('node.add_root')}</button>
- <button class="btn btn-small" disabled=${busy}
- onClick=${() => { setAddingByPath(false); setPathDraft(''); }}>
- ${t('settings.cancel')}</button>
- </div>
- <p class="settings-hint">${t('node.root_path_hint')}</p>
- ` : html`
- <button class="btn btn-small btn-secondary" style="margin-top:8px"
- disabled=${busy} onClick=${() => setAddingByPath(true)}>
- <${Icon} name="folder-plus" /> ${t('node.add_root')}
- </button>
- `}
+ const addControls = !canWiden || !platform.rootPicker.available ? '' : html`
+ <button class="btn btn-small btn-secondary" style="margin-top:8px"
+ disabled=${busy} onClick=${doPickRoot}>
+ <${Icon} name="folder-plus" /> ${t('node.add_root')}
+ </button>
`;
+ // Said once, under the table, rather than as a tooltip on each switch: the
+ // switches are not broken, they are somewhere else.
+ const localOnlyHint = canEdit && !canWiden && html`
+ <p class="settings-hint" style="margin-top:8px">
+ ${t('settings_node.roots_local_only_hint')}</p>`;
if (!displayRoots.length) {
return html`
<div class="shared-directories-table">
<p class="settings-hint">${t('settings_node.shared_directories_hint')}</p>
+ ${localOnlyHint}
${addControls}
${message}
</div>
@@ -326,14 +302,15 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
doing the job. */''}
${canEdit && html`
<td class="sdt-col-toggle">
- <${ToggleSwitch} checked=${!!r.writable} disabled=${busy || !!r.ejected}
+ <${ToggleSwitch} checked=${!!r.writable}
+ disabled=${busy || !!r.ejected || !canWiden}
label=${t('node.root_rw')}
onChange=${(v) => doUpdateRoot(r.name, { writable: v })} />
</td>
`}
${canEdit && !isLocal && html`
<td class="sdt-col-toggle">
- <${ToggleSwitch} checked=${!!r.removable} disabled=${busy}
+ <${ToggleSwitch} checked=${!!r.removable} disabled=${busy || !canWiden}
label=${t('node.removable')}
onChange=${(v) => doUpdateRoot(r.name, { removable: v })} />
</td>
@@ -360,6 +337,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
`; })}
</tbody>
</table>
+ ${localOnlyHint}
${addControls}
${message}
${indexProgress && indexProgress.scanning && html`
@@ -426,6 +404,9 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
// Node loopback state (Electron-only)
const [nodeDetected, setNodeDetected] = useState(false);
+ // A node on this machine is not necessarily the one hosting this group, and
+ // the table's loopback door is only a door to *that* node.
+ const [nodeHostsGroup, setNodeHostsGroup] = useState(false);
const [nodeRoots, setNodeRoots] = useState([]);
const [nodeGroupName, setNodeGroupName] = useState('');
const [nodeBusy, setNodeBusy] = useState(false);
@@ -464,6 +445,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
const data = await platform.node.op('groups');
const groups = data.groups || [];
const ng = groups.find(g => g.id === groupId);
+ setNodeHostsGroup(Boolean(ng));
if (ng) {
setNodeRoots(ng.roots || []);
setNodeGroupName(ng.name || '');
@@ -1182,7 +1164,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
groupId=${groupId}
transport=${transportRef.current}
signFn=${adminSignFn}
- nodeDetected=${nodeDetected}
+ nodeDetected=${nodeDetected && nodeHostsGroup}
onRootsChange=${loadNodeInfo}
onRefreshIndex=${onRefreshIndex} />
</${CollapsibleSection}>