refactor(decnet_web/SwarmHosts): extract types + helpers with tests
This commit is contained in:
54
decnet_web/src/components/SwarmHosts/helpers.test.ts
Normal file
54
decnet_web/src/components/SwarmHosts/helpers.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
AGENT_NAME_RE, bundleSecondsLeft, formatMmSs, shortFp,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
describe('shortFp', () => {
|
||||||
|
it('truncates long fingerprints with an ellipsis', () => {
|
||||||
|
expect(shortFp('a'.repeat(64))).toBe('aaaaaaaaaaaaaaaa…');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns em-dash for empty input', () => {
|
||||||
|
expect(shortFp('')).toBe('—');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AGENT_NAME_RE', () => {
|
||||||
|
it('accepts lowercase / digits / dashes within 1..63 chars', () => {
|
||||||
|
expect(AGENT_NAME_RE.test('agent-1')).toBe(true);
|
||||||
|
expect(AGENT_NAME_RE.test('a')).toBe(true);
|
||||||
|
expect(AGENT_NAME_RE.test('1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects uppercase, leading dash, and oversized names', () => {
|
||||||
|
expect(AGENT_NAME_RE.test('Agent-1')).toBe(false);
|
||||||
|
expect(AGENT_NAME_RE.test('-leading')).toBe(false);
|
||||||
|
expect(AGENT_NAME_RE.test('a'.repeat(64))).toBe(false);
|
||||||
|
expect(AGENT_NAME_RE.test('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bundleSecondsLeft', () => {
|
||||||
|
it('returns the floor of (expires - now) / 1000 in seconds', () => {
|
||||||
|
const now = new Date('2026-05-09T08:00:00Z').getTime();
|
||||||
|
const exp = new Date('2026-05-09T08:04:30Z').toISOString();
|
||||||
|
expect(bundleSecondsLeft(exp, now)).toBe(270);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps to zero when already expired', () => {
|
||||||
|
const now = new Date('2026-05-09T08:10:00Z').getTime();
|
||||||
|
expect(bundleSecondsLeft('2026-05-09T08:05:00Z', now)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for unparseable timestamps', () => {
|
||||||
|
expect(bundleSecondsLeft('not-a-date', Date.now())).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatMmSs', () => {
|
||||||
|
it('zero-pads minutes and seconds', () => {
|
||||||
|
expect(formatMmSs(75)).toEqual({ mm: '01', ss: '15' });
|
||||||
|
expect(formatMmSs(9)).toEqual({ mm: '00', ss: '09' });
|
||||||
|
expect(formatMmSs(0)).toEqual({ mm: '00', ss: '00' });
|
||||||
|
});
|
||||||
|
});
|
||||||
29
decnet_web/src/components/SwarmHosts/helpers.ts
Normal file
29
decnet_web/src/components/SwarmHosts/helpers.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ApiError } from '../../utils/api';
|
||||||
|
|
||||||
|
export const AGENT_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
||||||
|
|
||||||
|
export const shortFp = (fp: string): string =>
|
||||||
|
(fp ? fp.slice(0, 16) + '…' : '—');
|
||||||
|
|
||||||
|
export function extractErrorDetail(err: unknown, fallback: string): string {
|
||||||
|
const e = err as ApiError;
|
||||||
|
if (e?.response?.data?.detail) return e.response.data.detail;
|
||||||
|
if (e?.response?.status === 403) return 'Insufficient permissions (admin only)';
|
||||||
|
if (e?.response?.status === 401) return 'Session expired — please log in again';
|
||||||
|
if (e?.message) return e.message;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seconds remaining until the bundle expires, clamped to >= 0. */
|
||||||
|
export function bundleSecondsLeft(expiresAt: string, now: number): number {
|
||||||
|
const t = new Date(expiresAt).getTime();
|
||||||
|
if (Number.isNaN(t)) return 0;
|
||||||
|
return Math.max(0, Math.floor((t - now) / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMmSs(seconds: number): { mm: string; ss: string } {
|
||||||
|
return {
|
||||||
|
mm: Math.floor(seconds / 60).toString().padStart(2, '0'),
|
||||||
|
ss: (seconds % 60).toString().padStart(2, '0'),
|
||||||
|
};
|
||||||
|
}
|
||||||
27
decnet_web/src/components/SwarmHosts/types.ts
Normal file
27
decnet_web/src/components/SwarmHosts/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export interface SwarmHost {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
agent_port: number;
|
||||||
|
status: string;
|
||||||
|
last_heartbeat: string | null;
|
||||||
|
client_cert_fingerprint: string;
|
||||||
|
updater_cert_fingerprint: string | null;
|
||||||
|
enrolled_at: string;
|
||||||
|
notes: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleResult {
|
||||||
|
token: string;
|
||||||
|
host_uuid: string;
|
||||||
|
command: string;
|
||||||
|
expires_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleRequest {
|
||||||
|
master_host: string;
|
||||||
|
agent_name: string;
|
||||||
|
with_updater: boolean;
|
||||||
|
use_ipvlan: boolean;
|
||||||
|
services_ini: string | null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user