diff --git a/frontend/src/components/pages/ArcherTemplatePage.js b/frontend/src/components/pages/ArcherTemplatePage.js index af026cc..dcb5a11 100644 --- a/frontend/src/components/pages/ArcherTemplatePage.js +++ b/frontend/src/components/pages/ArcherTemplatePage.js @@ -6,7 +6,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { FileText, Plus, Edit, Copy, Trash2, ChevronDown, ChevronRight, - Loader, AlertCircle, RefreshCw, + Loader, AlertCircle, RefreshCw, Eye, EyeOff, Clipboard, Check, } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import TemplateFormModal from '../TemplateFormModal'; @@ -14,6 +14,18 @@ import DeleteConfirmModal from '../DeleteConfirmModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; +// Section field mapping — ordered: static first, then semi-static +const SECTIONS = [ + { key: 'environment_overview', label: 'Environment Overview' }, + { key: 'segmentation', label: 'Segmentation' }, + { key: 'mitigating_controls', label: 'Mitigating Controls' }, + { key: 'additional_info', label: 'Additional Info/Background' }, + { key: 'charter_network_banner', label: 'Charter Network Banner' }, + { key: 'data_classification', label: 'Data Classification' }, + { key: 'charter_network', label: 'Charter Network' }, + { key: 'additional_access_list', label: 'Additional Access List' }, +]; + // --------------------------------------------------------------------------- // Styles — dark theme tactical intelligence aesthetic // --------------------------------------------------------------------------- @@ -223,6 +235,11 @@ export default function ArcherTemplatePage() { // Modal state for create/edit/clone const [modalState, setModalState] = useState({ open: false, mode: 'create', template: null }); const [deleteTarget, setDeleteTarget] = useState(null); + // View panel state — which template ID is expanded for viewing + const [viewExpandedId, setViewExpandedId] = useState(null); + // Copy state for view panel + const [copiedSections, setCopiedSections] = useState({}); + const [copyAllCopied, setCopyAllCopied] = useState(false); // ------------------------------------------------------------------------- // Fetch templates @@ -265,6 +282,41 @@ export default function ArcherTemplatePage() { setExpandedVendors(prev => ({ ...prev, [vendor]: !prev[vendor] })); }; + // ------------------------------------------------------------------------- + // View panel toggle and copy handlers + // ------------------------------------------------------------------------- + const toggleView = (templateId) => { + setViewExpandedId(prev => prev === templateId ? null : templateId); + setCopiedSections({}); + setCopyAllCopied(false); + }; + + const handleCopySection = async (sectionKey, content) => { + if (!content) return; + try { + await navigator.clipboard.writeText(content); + setCopiedSections(prev => ({ ...prev, [sectionKey]: true })); + setTimeout(() => { + setCopiedSections(prev => ({ ...prev, [sectionKey]: false })); + }, 2000); + } catch (_err) { /* clipboard failed */ } + }; + + const handleCopyAll = async (template) => { + const parts = []; + for (const section of SECTIONS) { + const content = template[section.key]; + if (content && content.trim()) { + parts.push(`${section.label}\n${content}`); + } + } + try { + await navigator.clipboard.writeText(parts.join('\n\n')); + setCopyAllCopied(true); + setTimeout(() => setCopyAllCopied(false), 2000); + } catch (_err) { /* clipboard failed */ } + }; + // ------------------------------------------------------------------------- // Grouped data // ------------------------------------------------------------------------- @@ -363,36 +415,126 @@ export default function ArcherTemplatePage() {