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