127 lines
3.4 KiB
TypeScript
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}-`;
|
|
}
|