mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Migrate TrueNAS to supported JSON-RPC transport
This commit is contained in:
parent
0c17922055
commit
b81ba7dd06
19 changed files with 2145 additions and 371 deletions
|
|
@ -19,6 +19,7 @@ describe('TrueNASAPI', () => {
|
|||
host: ' truenas.local ',
|
||||
port: 443,
|
||||
apiKey: ' ******** ',
|
||||
username: ' pulse-readonly ',
|
||||
useHttps: true,
|
||||
insecureSkipVerify: false,
|
||||
enabled: true,
|
||||
|
|
@ -40,6 +41,15 @@ describe('TrueNASAPI', () => {
|
|||
disks: 8,
|
||||
recoveryArtifacts: 18,
|
||||
},
|
||||
transport: {
|
||||
mode: 'jsonrpc-websocket',
|
||||
endpoint: ' wss://truenas.local/api/current ',
|
||||
tls: true,
|
||||
connected: true,
|
||||
authMechanism: ' api-key-plain ',
|
||||
applianceVersion: ' TrueNAS-SCALE-25.10.4 ',
|
||||
reconnects: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -50,7 +60,7 @@ describe('TrueNASAPI', () => {
|
|||
host: 'truenas.local',
|
||||
port: 443,
|
||||
apiKey: '********',
|
||||
username: undefined,
|
||||
username: 'pulse-readonly',
|
||||
password: undefined,
|
||||
useHttps: true,
|
||||
insecureSkipVerify: false,
|
||||
|
|
@ -77,6 +87,18 @@ describe('TrueNASAPI', () => {
|
|||
disks: 8,
|
||||
recoveryArtifacts: 18,
|
||||
},
|
||||
transport: {
|
||||
mode: 'jsonrpc-websocket',
|
||||
endpoint: 'wss://truenas.local/api/current',
|
||||
tls: true,
|
||||
connected: true,
|
||||
authMechanism: 'api-key-plain',
|
||||
applianceVersion: 'TrueNAS-SCALE-25.10.4',
|
||||
legacyReason: undefined,
|
||||
reconnects: 1,
|
||||
lastError: undefined,
|
||||
lastConnectedAt: undefined,
|
||||
},
|
||||
monitorDatasets: false,
|
||||
monitorPools: false,
|
||||
monitorReplication: false,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type RawTrueNASConnection = Partial<TrueNASConnection>;
|
|||
type RawTrueNASConnectionPollError = Partial<TrueNASConnectionPollError>;
|
||||
type RawTrueNASConnectionPoll = Partial<TrueNASConnectionPollStatus>;
|
||||
type RawTrueNASConnectionObservedSummary = Partial<TrueNASConnectionObservedSummary>;
|
||||
type RawTrueNASConnectionTransportStatus = Partial<TrueNASConnectionTransportStatus>;
|
||||
|
||||
export interface TrueNASConnectionPollError {
|
||||
at?: string;
|
||||
|
|
@ -50,6 +51,19 @@ export interface TrueNASConnectionObservedSummary {
|
|||
recoveryArtifacts: number;
|
||||
}
|
||||
|
||||
export interface TrueNASConnectionTransportStatus {
|
||||
mode: 'negotiating' | 'jsonrpc-websocket' | 'legacy-rest';
|
||||
endpoint?: string;
|
||||
tls: boolean;
|
||||
connected: boolean;
|
||||
authMechanism?: string;
|
||||
applianceVersion?: string;
|
||||
legacyReason?: string;
|
||||
reconnects?: number;
|
||||
lastError?: string;
|
||||
lastConnectedAt?: string;
|
||||
}
|
||||
|
||||
export interface TrueNASConnection {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -68,6 +82,7 @@ export interface TrueNASConnection {
|
|||
monitorReplication: boolean;
|
||||
poll?: TrueNASConnectionPollStatus;
|
||||
observed?: TrueNASConnectionObservedSummary;
|
||||
transport?: TrueNASConnectionTransportStatus;
|
||||
}
|
||||
|
||||
export interface TrueNASConnectionInput {
|
||||
|
|
@ -134,6 +149,27 @@ const normalizeTrueNASConnectionObservedSummary = (
|
|||
};
|
||||
};
|
||||
|
||||
const normalizeTrueNASConnectionTransportStatus = (
|
||||
transport: RawTrueNASConnectionTransportStatus | undefined,
|
||||
): TrueNASConnectionTransportStatus | undefined => {
|
||||
if (!transport || typeof transport !== 'object') return undefined;
|
||||
const rawMode = optionalTrimmedString(transport.mode);
|
||||
const mode =
|
||||
rawMode === 'jsonrpc-websocket' || rawMode === 'legacy-rest' ? rawMode : 'negotiating';
|
||||
return {
|
||||
mode,
|
||||
endpoint: optionalTrimmedString(transport.endpoint),
|
||||
tls: strictBoolean(transport.tls),
|
||||
connected: strictBoolean(transport.connected),
|
||||
authMechanism: optionalTrimmedString(transport.authMechanism),
|
||||
applianceVersion: optionalTrimmedString(transport.applianceVersion),
|
||||
legacyReason: optionalTrimmedString(transport.legacyReason),
|
||||
reconnects: finiteNumberOrUndefined(transport.reconnects),
|
||||
lastError: optionalTrimmedString(transport.lastError),
|
||||
lastConnectedAt: optionalTrimmedString(transport.lastConnectedAt),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeTrueNASConnection = (connection: RawTrueNASConnection): TrueNASConnection => ({
|
||||
id: trimmedString(connection.id),
|
||||
name: optionalTrimmedString(connection.name) ?? '',
|
||||
|
|
@ -152,6 +188,7 @@ const normalizeTrueNASConnection = (connection: RawTrueNASConnection): TrueNASCo
|
|||
monitorReplication: strictBoolean(connection.monitorReplication),
|
||||
poll: normalizeTrueNASConnectionPoll(connection.poll),
|
||||
observed: normalizeTrueNASConnectionObservedSummary(connection.observed),
|
||||
transport: normalizeTrueNASConnectionTransportStatus(connection.transport),
|
||||
});
|
||||
|
||||
const serializeTrueNASConnectionInput = (input: TrueNASConnectionInput) => ({
|
||||
|
|
|
|||
|
|
@ -28,6 +28,22 @@ export interface TrueNASCredentialSlotProps {
|
|||
deleteError?: string | null;
|
||||
}
|
||||
|
||||
const describeTransport = (transport: NonNullable<TrueNASConnection['transport']>): string => {
|
||||
let description: string;
|
||||
if (transport.mode === 'jsonrpc-websocket') {
|
||||
description = `Transport: JSON-RPC WebSocket${transport.tls ? ' over TLS' : ''}; authentication: ${transport.authMechanism || 'session'}.`;
|
||||
} else if (transport.mode === 'legacy-rest') {
|
||||
description =
|
||||
`Transport: legacy REST (${transport.applianceVersion || 'legacy TrueNAS'}). ${transport.legacyReason || ''}`.trim();
|
||||
} else {
|
||||
description = 'Transport negotiation has not completed yet.';
|
||||
}
|
||||
if (transport.lastError) {
|
||||
description += ` Last error: ${transport.lastError}`;
|
||||
}
|
||||
return description;
|
||||
};
|
||||
|
||||
export const TrueNASCredentialSlot: Component<TrueNASCredentialSlotProps> = (props) => {
|
||||
let primed = false;
|
||||
|
||||
|
|
@ -132,21 +148,36 @@ export const TrueNASCredentialSlot: Component<TrueNASCredentialSlotProps> = (pro
|
|||
</div>
|
||||
|
||||
<Show when={props.state.form().authMode === 'apiKey'}>
|
||||
<label class={formField}>
|
||||
<span class={formLabel}>API key</span>
|
||||
<input
|
||||
class={formControl}
|
||||
type="password"
|
||||
value={props.state.form().apiKey}
|
||||
onInput={(event) => props.state.updateForm({ apiKey: event.currentTarget.value })}
|
||||
placeholder={
|
||||
props.state.form().hasStoredApiKey ? 'Saved API key retained unless replaced' : ''
|
||||
}
|
||||
/>
|
||||
<Show when={props.state.form().hasStoredApiKey}>
|
||||
<span class={formHelpText}>Leave this blank to keep the saved API key.</span>
|
||||
</Show>
|
||||
</label>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class={formField}>
|
||||
<span class={formLabel}>API key owner username</span>
|
||||
<input
|
||||
class={formControl}
|
||||
value={props.state.form().username}
|
||||
onInput={(event) => props.state.updateForm({ username: event.currentTarget.value })}
|
||||
placeholder="pulse-monitor"
|
||||
/>
|
||||
<span class={formHelpText}>
|
||||
Use the TrueNAS user that owns this key. Required by the supported login flow on
|
||||
current and future releases.
|
||||
</span>
|
||||
</label>
|
||||
<label class={formField}>
|
||||
<span class={formLabel}>API key</span>
|
||||
<input
|
||||
class={formControl}
|
||||
type="password"
|
||||
value={props.state.form().apiKey}
|
||||
onInput={(event) => props.state.updateForm({ apiKey: event.currentTarget.value })}
|
||||
placeholder={
|
||||
props.state.form().hasStoredApiKey ? 'Saved API key retained unless replaced' : ''
|
||||
}
|
||||
/>
|
||||
<Show when={props.state.form().hasStoredApiKey}>
|
||||
<span class={formHelpText}>Leave this blank to keep the saved API key.</span>
|
||||
</Show>
|
||||
</label>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.state.form().authMode === 'userpass'}>
|
||||
|
|
@ -236,6 +267,17 @@ export const TrueNASCredentialSlot: Component<TrueNASCredentialSlotProps> = (pro
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.editingConnection?.transport}>
|
||||
{(transport) => (
|
||||
<CalloutCard
|
||||
tone={transport().mode === 'legacy-rest' ? 'warning' : 'info'}
|
||||
scale="compact"
|
||||
padding="sm"
|
||||
description={describeTransport(transport())}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="space-y-3 rounded-md border border-border bg-surface p-4">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-base-content">Collection scope</div>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ describe('useTrueNASSettingsPanelState', () => {
|
|||
name: 'tower',
|
||||
host: 'truenas.local',
|
||||
apiKey: '********',
|
||||
username: 'pulse-readonly',
|
||||
pollIntervalSeconds: 90,
|
||||
useHttps: true,
|
||||
insecureSkipVerify: false,
|
||||
|
|
@ -91,6 +92,7 @@ describe('useTrueNASSettingsPanelState', () => {
|
|||
name: 'tower',
|
||||
host: 'truenas.local',
|
||||
apiKey: '********',
|
||||
username: 'pulse-readonly',
|
||||
pollIntervalSeconds: 90,
|
||||
useHttps: true,
|
||||
insecureSkipVerify: false,
|
||||
|
|
@ -105,6 +107,7 @@ describe('useTrueNASSettingsPanelState', () => {
|
|||
name: 'tower',
|
||||
host: 'truenas.local',
|
||||
apiKey: '********',
|
||||
username: 'pulse-readonly',
|
||||
pollIntervalSeconds: 90,
|
||||
useHttps: true,
|
||||
insecureSkipVerify: false,
|
||||
|
|
@ -128,6 +131,7 @@ describe('useTrueNASSettingsPanelState', () => {
|
|||
host: 'truenas.local',
|
||||
apiKey: '********',
|
||||
pollIntervalSeconds: 90,
|
||||
username: 'pulse-readonly',
|
||||
}),
|
||||
);
|
||||
expect(TrueNASAPI.updateConnection).toHaveBeenCalledWith(
|
||||
|
|
@ -136,7 +140,7 @@ describe('useTrueNASSettingsPanelState', () => {
|
|||
host: 'truenas.local',
|
||||
apiKey: '********',
|
||||
pollIntervalSeconds: 90,
|
||||
username: '',
|
||||
username: 'pulse-readonly',
|
||||
password: '',
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const buildFormStateFromConnection = (
|
|||
pollIntervalSeconds: String(connection.pollIntervalSeconds ?? 60),
|
||||
authMode,
|
||||
apiKey: '',
|
||||
username: authMode === 'userpass' ? connection.username || '' : '',
|
||||
username: connection.username || '',
|
||||
password: '',
|
||||
useHttps: connection.useHttps,
|
||||
insecureSkipVerify: connection.insecureSkipVerify,
|
||||
|
|
@ -163,7 +163,7 @@ const buildConnectionInput = (form: TrueNASConnectionFormState): TrueNASConnecti
|
|||
|
||||
if (form.authMode === 'apiKey') {
|
||||
input.apiKey = form.apiKey.trim() || (form.hasStoredApiKey ? REDACTED_SECRET : '');
|
||||
input.username = '';
|
||||
input.username = form.username.trim();
|
||||
input.password = '';
|
||||
} else {
|
||||
input.apiKey = '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue