From a35048b1740597dd6a9f17b5c1a3321a08effe1c Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 05:08:37 -0400 Subject: [PATCH] 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. --- decnet_web/src/components/CanaryTokens.tsx | 128 +----------------- .../src/components/CanaryTokens/helpers.ts | 28 ++++ .../src/components/CanaryTokens/types.ts | 44 ++++++ decnet_web/src/components/CanaryTokens/ui.tsx | 55 ++++++++ 4 files changed, 134 insertions(+), 121 deletions(-) create mode 100644 decnet_web/src/components/CanaryTokens/helpers.ts create mode 100644 decnet_web/src/components/CanaryTokens/types.ts create mode 100644 decnet_web/src/components/CanaryTokens/ui.tsx diff --git a/decnet_web/src/components/CanaryTokens.tsx b/decnet_web/src/components/CanaryTokens.tsx index dd344874..d1a77b45 100644 --- a/decnet_web/src/components/CanaryTokens.tsx +++ b/decnet_web/src/components/CanaryTokens.tsx @@ -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 }) => ( -
-
- {label.toUpperCase()} -
- {children} -
-); - -const Stat: React.FC<{ label: string; value: number | string; color: string }> = ({ label, value, color }) => ( -
-
{label}
-
{value}
-
-); - export default CanaryTokens; diff --git a/decnet_web/src/components/CanaryTokens/helpers.ts b/decnet_web/src/components/CanaryTokens/helpers.ts new file mode 100644 index 00000000..3435b400 --- /dev/null +++ b/decnet_web/src/components/CanaryTokens/helpers.ts @@ -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`; +} diff --git a/decnet_web/src/components/CanaryTokens/types.ts b/decnet_web/src/components/CanaryTokens/types.ts new file mode 100644 index 00000000..533bfc6e --- /dev/null +++ b/decnet_web/src/components/CanaryTokens/types.ts @@ -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', +}; diff --git a/decnet_web/src/components/CanaryTokens/ui.tsx b/decnet_web/src/components/CanaryTokens/ui.tsx new file mode 100644 index 00000000..f51da6c2 --- /dev/null +++ b/decnet_web/src/components/CanaryTokens/ui.tsx @@ -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 }) => ( +
+
+ {label.toUpperCase()} +
+ {children} +
+); + +export const Stat: React.FC<{ label: string; value: number | string; color: string }> = ({ label, value, color }) => ( +
+
{label}
+
{value}
+
+);