refactor(decnet_web/CanaryTokens): extract types + helpers + ui
Foundation for the CanaryTokens split. Types, error/format helpers, and the inline style + small primitives move out of the page so the upcoming modal/list extractions can import without reaching back through CanaryTokens.tsx. - New CanaryTokens/types.ts (BlobRow, DeckyOption, TopologyOption, Scope, KNOWN_GENERATORS / GeneratorName, KIND_OPTIONS, STATE_COLOR) - New CanaryTokens/helpers.ts (extractError, fmt, fmtBytes) - New CanaryTokens/ui.tsx (INPUT_STYLE, BTN_PRIMARY, BTN_GHOST, Field, Stat) - CanaryTokens.tsx loses ~110 LOC of inline definitions; behavior unchanged.
This commit is contained in:
@@ -2,78 +2,20 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Plus, Upload, X, AlertTriangle, Search, Target,
|
||||
} from '../icons';
|
||||
import api, { type ApiError } from '../utils/api';
|
||||
import api from '../utils/api';
|
||||
import { useEscapeKey } from '../hooks/useEscapeKey';
|
||||
import { useFocusTrap } from '../hooks/useFocusTrap';
|
||||
import CanaryTokenDrawer from './CanaryTokenDrawer';
|
||||
import type { CanaryTokenRow } from './CanaryTokenDrawer';
|
||||
|
||||
interface BlobRow {
|
||||
uuid: string;
|
||||
sha256: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size_bytes: number;
|
||||
uploaded_by: string;
|
||||
uploaded_at: string;
|
||||
token_count: number;
|
||||
}
|
||||
|
||||
const KNOWN_GENERATORS = [
|
||||
'git_config', 'env_file', 'ssh_key', 'aws_creds',
|
||||
'honeydoc', 'honeydoc_docx', 'honeydoc_pdf',
|
||||
] as const;
|
||||
type GeneratorName = typeof KNOWN_GENERATORS[number];
|
||||
|
||||
const KIND_OPTIONS: Array<{ value: 'http' | 'dns' | 'aws_passive'; label: string }> = [
|
||||
{ value: 'http', label: 'HTTP callback' },
|
||||
{ value: 'dns', label: 'DNS callback' },
|
||||
{ value: 'aws_passive', label: 'AWS passive (no callback)' },
|
||||
];
|
||||
|
||||
function extractError(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.';
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function fmt(iso: string | null): string {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function fmtBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MiB`;
|
||||
}
|
||||
|
||||
const STATE_COLOR = {
|
||||
planted: '#00ff88',
|
||||
revoked: 'var(--dim-color)',
|
||||
failed: '#ff5555',
|
||||
};
|
||||
import {
|
||||
KNOWN_GENERATORS, KIND_OPTIONS, STATE_COLOR,
|
||||
type BlobRow, type DeckyOption, type TopologyOption, type Scope, type GeneratorName,
|
||||
} from './CanaryTokens/types';
|
||||
import { extractError, fmt, fmtBytes } from './CanaryTokens/helpers';
|
||||
import { INPUT_STYLE, BTN_PRIMARY, BTN_GHOST, Field, Stat } from './CanaryTokens/ui';
|
||||
|
||||
// ─── CREATE MODAL ──────────────────────────────────────────────────────────
|
||||
|
||||
interface DeckyOption {
|
||||
name: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
interface TopologyOption {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
type Scope = 'fleet' | 'topology';
|
||||
|
||||
interface CreateModalProps {
|
||||
blobs: BlobRow[];
|
||||
deckies: DeckyOption[];
|
||||
@@ -1275,60 +1217,4 @@ const CanaryTokens: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// ─── small style helpers ───────────────────────────────────────────────────
|
||||
|
||||
const INPUT_STYLE: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
marginBottom: '12px',
|
||||
background: 'var(--matrix-tint-5)',
|
||||
border: '1px solid var(--border-color, #30363d)',
|
||||
color: 'var(--text-color)',
|
||||
fontSize: '0.85rem',
|
||||
};
|
||||
|
||||
const BTN_PRIMARY: React.CSSProperties = {
|
||||
padding: '8px 14px',
|
||||
border: '1px solid var(--accent-color, #00ff88)',
|
||||
background: 'var(--accent-color, #00ff88)',
|
||||
color: 'var(--bg-color, #0d1117)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
const BTN_GHOST: React.CSSProperties = {
|
||||
padding: '8px 14px',
|
||||
border: '1px solid var(--text-color)',
|
||||
background: 'transparent',
|
||||
color: 'var(--text-color)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
};
|
||||
|
||||
const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
|
||||
<div>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em', marginBottom: '4px' }}>
|
||||
{label.toUpperCase()}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Stat: React.FC<{ label: string; value: number | string; color: string }> = ({ label, value, color }) => (
|
||||
<div style={{
|
||||
flex: '1 1 120px',
|
||||
padding: '12px 16px',
|
||||
border: '1px solid var(--border-color, #30363d)',
|
||||
background: 'var(--matrix-tint-5)',
|
||||
}}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em' }}>{label}</div>
|
||||
<div style={{ fontSize: '1.4rem', fontWeight: 'bold', color, marginTop: '4px' }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CanaryTokens;
|
||||
|
||||
28
decnet_web/src/components/CanaryTokens/helpers.ts
Normal file
28
decnet_web/src/components/CanaryTokens/helpers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ApiError } from '../../utils/api';
|
||||
|
||||
/** Normalize an axios error into operator-friendly text, with role
|
||||
* hints when the wire shape carries no detail. */
|
||||
export function extractError(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.';
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** Compact local-time YYYY-MM-DD HH:mm; returns "—" for null and the
|
||||
* raw input back when it's not a parseable ISO string. */
|
||||
export function fmt(iso: string | null): string {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** B / KiB / MiB binary-prefix file-size renderer. */
|
||||
export function fmtBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MiB`;
|
||||
}
|
||||
44
decnet_web/src/components/CanaryTokens/types.ts
Normal file
44
decnet_web/src/components/CanaryTokens/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/** Wire + UI types for the CanaryTokens page surface. */
|
||||
|
||||
export interface BlobRow {
|
||||
uuid: string;
|
||||
sha256: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size_bytes: number;
|
||||
uploaded_by: string;
|
||||
uploaded_at: string;
|
||||
token_count: number;
|
||||
}
|
||||
|
||||
export interface DeckyOption {
|
||||
name: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
export interface TopologyOption {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export type Scope = 'fleet' | 'topology';
|
||||
|
||||
export const KNOWN_GENERATORS = [
|
||||
'git_config', 'env_file', 'ssh_key', 'aws_creds',
|
||||
'honeydoc', 'honeydoc_docx', 'honeydoc_pdf',
|
||||
] as const;
|
||||
|
||||
export type GeneratorName = typeof KNOWN_GENERATORS[number];
|
||||
|
||||
export const KIND_OPTIONS: Array<{ value: 'http' | 'dns' | 'aws_passive'; label: string }> = [
|
||||
{ value: 'http', label: 'HTTP callback' },
|
||||
{ value: 'dns', label: 'DNS callback' },
|
||||
{ value: 'aws_passive', label: 'AWS passive (no callback)' },
|
||||
];
|
||||
|
||||
export const STATE_COLOR: Record<'planted' | 'revoked' | 'failed', string> = {
|
||||
planted: '#00ff88',
|
||||
revoked: 'var(--dim-color)',
|
||||
failed: '#ff5555',
|
||||
};
|
||||
55
decnet_web/src/components/CanaryTokens/ui.tsx
Normal file
55
decnet_web/src/components/CanaryTokens/ui.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
export const INPUT_STYLE: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '8px 10px',
|
||||
marginBottom: '12px',
|
||||
background: 'var(--matrix-tint-5)',
|
||||
border: '1px solid var(--border-color, #30363d)',
|
||||
color: 'var(--text-color)',
|
||||
fontSize: '0.85rem',
|
||||
};
|
||||
|
||||
export const BTN_PRIMARY: React.CSSProperties = {
|
||||
padding: '8px 14px',
|
||||
border: '1px solid var(--accent-color, #00ff88)',
|
||||
background: 'var(--accent-color, #00ff88)',
|
||||
color: 'var(--bg-color, #0d1117)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
export const BTN_GHOST: React.CSSProperties = {
|
||||
padding: '8px 14px',
|
||||
border: '1px solid var(--text-color)',
|
||||
background: 'transparent',
|
||||
color: 'var(--text-color)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
};
|
||||
|
||||
export const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
|
||||
<div>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em', marginBottom: '4px' }}>
|
||||
{label.toUpperCase()}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Stat: React.FC<{ label: string; value: number | string; color: string }> = ({ label, value, color }) => (
|
||||
<div style={{
|
||||
flex: '1 1 120px',
|
||||
padding: '12px 16px',
|
||||
border: '1px solid var(--border-color, #30363d)',
|
||||
background: 'var(--matrix-tint-5)',
|
||||
}}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--dim-color)', letterSpacing: '0.1em' }}>{label}</div>
|
||||
<div style={{ fontSize: '1.4rem', fontWeight: 'bold', color, marginTop: '4px' }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
Reference in New Issue
Block a user