mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Route connection edit-mode into NodeCredentialSlot (pve/pbs/pmg)
The drawer had pause/remove after cc963391a, but edit still lived
only in the legacy per-type panels. This wires the drawer's new
Edit button into the existing NodeCredentialSlot via the editor's
mode="edit" path.
InfrastructureWorkspace gains an editingConnection signal and a
three-way Switch (ledger | add | edit). Edit route looks up the
underlying NodeConfigWithStatus by name from the existing pveNodes/
pbsNodes/pmgNodes accessors and hands it to NodeCredentialSlot as
editingNode — which already handles the redacted-secret convention
via useNodeModalState.
VMware + TrueNAS edit is a separate commit: their credential slots
currently call openCreateDialog() on mount and need an analogous
editingConnection branch. Agent edit has no obvious surface today
so the drawer simply omits the button for it.
Vitest adds: Edit-present for pve, Edit-absent for agent.
This commit is contained in:
parent
cc963391a3
commit
1f1bc57e2c
3 changed files with 159 additions and 10 deletions
|
|
@ -7,8 +7,11 @@ interface ConnectionDetailDrawerProps {
|
|||
connection: () => Connection | undefined;
|
||||
onClose: () => void;
|
||||
onMutated?: () => void;
|
||||
onEdit?: (connection: Connection) => void;
|
||||
}
|
||||
|
||||
const EDITABLE_CONNECTION_TYPES: readonly Connection['type'][] = ['pve', 'pbs', 'pmg'];
|
||||
|
||||
const REMOVE_CONFIRM_TIMEOUT_MS = 4000;
|
||||
|
||||
const errorMessage = (err: unknown): string => {
|
||||
|
|
@ -114,6 +117,8 @@ export const ConnectionDetailDrawer: Component<ConnectionDetailDrawerProps> = (p
|
|||
const canPause = connection.capabilities.supportsPause;
|
||||
const canRemove =
|
||||
connection.type !== 'docker' && connection.type !== 'kubernetes';
|
||||
const canEdit =
|
||||
Boolean(props.onEdit) && EDITABLE_CONNECTION_TYPES.includes(connection.type);
|
||||
const pauseLabel = connection.enabled ? 'Pause' : 'Resume';
|
||||
const pauseBusy = () => pendingAction() === 'pause';
|
||||
const removeBusy = () => pendingAction() === 'remove';
|
||||
|
|
@ -234,7 +239,7 @@ export const ConnectionDetailDrawer: Component<ConnectionDetailDrawerProps> = (p
|
|||
</dl>
|
||||
</div>
|
||||
|
||||
<Show when={canPause || canRemove}>
|
||||
<Show when={canEdit || canPause || canRemove}>
|
||||
<div class="space-y-2 border-t border-border px-5 py-4">
|
||||
<Show when={actionError()}>
|
||||
<div
|
||||
|
|
@ -245,6 +250,16 @@ export const ConnectionDetailDrawer: Component<ConnectionDetailDrawerProps> = (p
|
|||
</div>
|
||||
</Show>
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<Show when={canEdit}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={anyBusy()}
|
||||
onClick={() => props.onEdit?.(connection)}
|
||||
class="inline-flex items-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={canPause}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { Component, Match, Show, Switch, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { useLocation, useNavigate, useSearchParams } from '@solidjs/router';
|
||||
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
|
||||
import { AgentProfilesPanel } from './AgentProfilesPanel';
|
||||
|
|
@ -9,7 +9,8 @@ import { ConnectionEditor } from './ConnectionEditor/ConnectionEditor';
|
|||
import { NodeCredentialSlot } from './ConnectionEditor/CredentialSlots/NodeCredentialSlot';
|
||||
import { TrueNASCredentialSlot } from './ConnectionEditor/CredentialSlots/TrueNASCredentialSlot';
|
||||
import { VMwareCredentialSlot } from './ConnectionEditor/CredentialSlots/VMwareCredentialSlot';
|
||||
import type { ConnectionType } from '@/api/connections';
|
||||
import type { Connection, ConnectionType } from '@/api/connections';
|
||||
import type { NodeConfigWithStatus } from '@/types/nodes';
|
||||
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
|
||||
import {
|
||||
buildInfrastructureWorkspacePath,
|
||||
|
|
@ -43,12 +44,26 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
const [initialAddType, setInitialAddType] = createSignal<ConnectionType | null>(null);
|
||||
const [showAgentProfiles, setShowAgentProfiles] = createSignal(false);
|
||||
const [selectedConnectionId, setSelectedConnectionId] = createSignal<string | null>(null);
|
||||
const [editingConnection, setEditingConnection] = createSignal<Connection | null>(null);
|
||||
const readOnly = createMemo(() => presentationPolicyIsReadOnly());
|
||||
const selectedConnection = createMemo(() => {
|
||||
const id = selectedConnectionId();
|
||||
return id ? ledger.findById(id) : undefined;
|
||||
});
|
||||
|
||||
const findEditableNode = (connection: Connection): NodeConfigWithStatus | null => {
|
||||
const accessor =
|
||||
connection.type === 'pve'
|
||||
? props.pveNodes
|
||||
: connection.type === 'pbs'
|
||||
? props.pbsNodes
|
||||
: connection.type === 'pmg'
|
||||
? props.pmgNodes
|
||||
: null;
|
||||
if (!accessor) return null;
|
||||
return accessor().find((node) => node.name === connection.name) ?? null;
|
||||
};
|
||||
|
||||
// Redirect legacy deep links and pre-select the matching type in the editor.
|
||||
createEffect(() => {
|
||||
const path = location.pathname;
|
||||
|
|
@ -76,11 +91,14 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
setShowAgentProfiles(false);
|
||||
});
|
||||
|
||||
// Drop add mode in read-only sessions.
|
||||
// Drop add/edit mode in read-only sessions.
|
||||
createEffect(() => {
|
||||
if (readOnly() && addMode()) {
|
||||
setAddMode(false);
|
||||
}
|
||||
if (readOnly() && editingConnection()) {
|
||||
setEditingConnection(null);
|
||||
}
|
||||
});
|
||||
|
||||
const rows = createMemo<InfrastructureSystemRow[]>(() => ledger.rows());
|
||||
|
|
@ -113,21 +131,46 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
setShowAgentProfiles(false);
|
||||
};
|
||||
|
||||
const renderNodeSlot = (type: 'pve' | 'pbs' | 'pmg') => {
|
||||
const exitEditMode = () => {
|
||||
setEditingConnection(null);
|
||||
};
|
||||
|
||||
const handleEditConnection = (connection: Connection) => {
|
||||
setSelectedConnectionId(null);
|
||||
setEditingConnection(connection);
|
||||
};
|
||||
|
||||
const handleEditSaved = () => {
|
||||
ledger.reload();
|
||||
exitEditMode();
|
||||
};
|
||||
|
||||
const renderNodeSlot = (
|
||||
type: 'pve' | 'pbs' | 'pmg',
|
||||
editingNode?: NodeConfigWithStatus | null,
|
||||
) => {
|
||||
const onExit = editingNode ? exitEditMode : exitAddMode;
|
||||
const onSaved = editingNode ? handleEditSaved : exitAddMode;
|
||||
return (
|
||||
<NodeCredentialSlot
|
||||
nodeType={type}
|
||||
settings={props}
|
||||
onCancel={exitAddMode}
|
||||
onSaved={exitAddMode}
|
||||
editingNode={editingNode ?? null}
|
||||
onCancel={onExit}
|
||||
onSaved={onSaved}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mode = createMemo<'ledger' | 'add' | 'edit'>(() => {
|
||||
if (editingConnection()) return 'edit';
|
||||
if (addMode()) return 'add';
|
||||
return 'ledger';
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="space-y-8">
|
||||
<Show
|
||||
when={addMode()}
|
||||
<Switch
|
||||
fallback={
|
||||
<ConnectionsTable
|
||||
rows={rows}
|
||||
|
|
@ -136,6 +179,65 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
/>
|
||||
}
|
||||
>
|
||||
<Match when={mode() === 'edit' && editingConnection()}>
|
||||
{(accessor) => {
|
||||
const connection = accessor();
|
||||
const editingNode = findEditableNode(connection);
|
||||
if (!editingNode) {
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-base font-semibold text-base-content">
|
||||
Edit connection
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={exitEditMode}
|
||||
class="inline-flex items-center gap-1 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
← Back to systems
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
role="alert"
|
||||
class="rounded-md border border-rose-300 bg-rose-50 px-4 py-3 text-sm text-rose-800 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-200"
|
||||
>
|
||||
Couldn't find the underlying configuration for {connection.name}. It may have
|
||||
been removed. Reload and try again.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-base font-semibold text-base-content">
|
||||
Edit {connection.name}
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-muted">{connection.address}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={exitEditMode}
|
||||
class="inline-flex items-center gap-1 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
← Back to systems
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-border bg-surface p-4">
|
||||
{renderNodeSlot(
|
||||
connection.type as 'pve' | 'pbs' | 'pmg',
|
||||
editingNode,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Match>
|
||||
|
||||
<Match when={mode() === 'add'}>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
|
|
@ -219,12 +321,14 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<ConnectionDetailDrawer
|
||||
connection={selectedConnection}
|
||||
onClose={() => setSelectedConnectionId(null)}
|
||||
onMutated={() => ledger.reload()}
|
||||
onEdit={handleEditConnection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,36 @@ describe('ConnectionDetailDrawer', () => {
|
|||
expect(screen.getByText(/Removing stops recording this agent/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Edit for pve connections and invokes onEdit with the connection', () => {
|
||||
const onEdit = vi.fn();
|
||||
render(() => (
|
||||
<ConnectionDetailDrawer
|
||||
connection={() => pveConnection()}
|
||||
onClose={() => {}}
|
||||
onMutated={() => {}}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
));
|
||||
|
||||
const editButton = screen.getByRole('button', { name: 'Edit' });
|
||||
fireEvent.click(editButton);
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
expect(onEdit.mock.calls[0][0]).toMatchObject({ id: 'pve:tower', type: 'pve' });
|
||||
});
|
||||
|
||||
it('omits Edit for agent connections (edit is not yet supported)', () => {
|
||||
render(() => (
|
||||
<ConnectionDetailDrawer
|
||||
connection={() => agentConnection()}
|
||||
onClose={() => {}}
|
||||
onMutated={() => {}}
|
||||
onEdit={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles pause via ConnectionsAPI and calls onMutated on success', async () => {
|
||||
setEnabled.mockResolvedValueOnce(undefined);
|
||||
const onMutated = vi.fn();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue