soteria/web/src/soteria-ui-helpers.ts

127 lines
3.4 KiB
TypeScript

export function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
function looksLikeHTML(value: string): boolean {
const sample = value.trim().slice(0, 512).toLowerCase();
return sample.startsWith('<!doctype html') || sample.includes('<html');
}
function extractHTMLTitle(value: string): string {
const match = value.match(/<title>\s*([^<]+)\s*<\/title>/i);
return match?.[1]?.trim() || '';
}
function extractRequestID(value: string): string {
const match = value.match(/Request ID:\s*([0-9a-f-]+)/i);
return match?.[1]?.trim() || '';
}
export async function fetchJSON<T>(input: string, init?: RequestInit): Promise<T> {
const response = await fetch(input, init);
const text = await response.text();
let payload: unknown = {};
if (text.trim() !== '') {
try {
payload = JSON.parse(text);
} catch {
payload = { error: text };
}
}
if (!response.ok) {
let message = typeof payload === 'object' && payload !== null && 'error' in payload
? String((payload as { error: unknown }).error)
: `${response.status} ${response.statusText}`;
if (looksLikeHTML(text)) {
const title = extractHTMLTitle(text);
const requestID = extractRequestID(text);
message = `upstream gateway error (${response.status}${title ? ` ${title}` : ''})`;
if (requestID) {
message = `${message}; request id ${requestID}`;
}
}
throw new Error(message);
}
return payload as T;
}
export function formatBytes(value?: number): string {
if (value === undefined || value === null || Number.isNaN(value)) {
return 'n/a';
}
if (value <= 0) {
return '0 B';
}
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
let size = value;
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit += 1;
}
return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`;
}
export function formatTimestamp(value?: string): string {
if (!value) {
return 'n/a';
}
const parsed = new Date(value);
if (Number.isNaN(parsed.valueOf())) {
return value;
}
return parsed.toLocaleString();
}
export function formatLabel(value?: string): string {
if (!value) {
return 'Needs attention';
}
return value.replace(/_/g, ' ');
}
export function progressChipClass(state?: string): 'good' | 'warn' | 'bad' {
switch ((state || '').toLowerCase()) {
case 'completed':
return 'good';
case 'failed':
return 'bad';
default:
return 'warn';
}
}
export function suggestTargetPVCName(sourcePVC: string): string {
const now = new Date();
const pad = (item: number) => String(item).padStart(2, '0');
const stamp = [
now.getUTCFullYear(),
pad(now.getUTCMonth() + 1),
pad(now.getUTCDate()),
pad(now.getUTCHours()),
pad(now.getUTCMinutes())
].join('');
return (`restore-${sourcePVC}-${stamp}`)
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 63)
.replace(/-+$/g, '');
}
export function suggestNamespacePrefix(): string {
const now = new Date();
const pad = (item: number) => String(item).padStart(2, '0');
const stamp = [
now.getUTCFullYear(),
pad(now.getUTCMonth() + 1),
pad(now.getUTCDate()),
pad(now.getUTCHours()),
pad(now.getUTCMinutes())
].join('');
return `restore-${stamp}-`;
}