refactor(ui): remove mock Ansible panel, settings in 3-column grid

BookingDetailsModal: remove static playbook template and fake simulator,
keep only the JSON REST response panel. Settings: drop max-w-2xl,
wrap integration cards in lg:grid-cols-3 so Azure, CheckMK and
Semaphore sit side by side on wide screens.
This commit is contained in:
Brückner
2026-06-05 09:54:54 +02:00
parent c428b12352
commit 7758bcaa02
2 changed files with 25 additions and 187 deletions

View File

@ -7,7 +7,7 @@ import React, { useState } from 'react';
import { Booking, LabTemplate, Device, User } from '../types';
import { authFetch } from '../lib/auth';
import {
X, Calendar, Clock, UserIcon, Database, Terminal, Cpu, Play, Check,
X, Calendar, Clock, UserIcon, Database, Terminal, Play, Check,
Trash2, Ban, Copy, CheckCircle, HelpCircle, HardDrive,
} from 'lucide-react';
@ -76,12 +76,7 @@ export default function BookingDetailsModal({
}
}
// Developer panel tabs ('rest', 'ansible', 'terminal')
const [activeTab, setActiveTab] = useState<'rest' | 'ansible' | 'terminal'>('ansible');
const [isCopied, setIsCopied] = useState(false);
const [isSimulating, setIsSimulating] = useState(false);
const [simulationLogs, setSimulationLogs] = useState<string[]>([]);
const [simStep, setSimStep] = useState(0);
const startFormatted = new Date(booking.startDateTime).toLocaleString('en-US', {
weekday: 'short',
@ -101,35 +96,6 @@ export default function BookingDetailsModal({
timeZoneName: 'short'
});
// Dynamic Ansible playbook string based on active nodes
const ipList = mappedDevices.map(d => d.ip);
const ansiblePlaybook = `---
- name: Reset GhostGrid Infrastructure Post-Reservation
hosts: localhost
gather_facts: false
vars:
target_nodes:
${ipList.map(ip => ` - "${ip}"`).join('\n') || ' - "No registered targets"'}
backup_repo: "https://git.ghostgrid.io/topology-configs"
tasks:
- name: Audit out-of-band diagnostic link states
ansible.builtin.ping:
register: ping_result
- name: Fetch designated golden config profile
ansible.builtin.get_url:
url: "{{ backup_repo }}/golden/${booking.labId}.cfg"
dest: "/tmp/golden_${booking.id}.cfg"
- name: Commit golden parameters & purge current stack
ansible.netcommon.net_config:
src: "/tmp/golden_${booking.id}.cfg"
replace: block
when: ping_result is succeeded
`;
// Dynamic REST Response
const mockJsonResponse = JSON.stringify({
retrievedAt: new Date().toISOString(),
apiEndpoint: `/api/bookings/${booking.id}`,
@ -145,57 +111,12 @@ ${ipList.map(ip => ` - "${ip}"`).join('\n') || ' - "No registered targ
}
}, null, 2);
const handleCopyText = (text: string) => {
navigator.clipboard.writeText(text);
const handleCopyJson = () => {
navigator.clipboard.writeText(mockJsonResponse);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
// Ansible Terminal simulation execution
const runAnsibleSimulation = () => {
if (isSimulating) return;
setIsSimulating(true);
setSimStep(1);
setSimulationLogs([
`[ansible-playbook -i localhost] Starting playbook: "Reset GhostGrid Infrastructure"`,
`[ansible-playbook] Configured target node list: ${ipList.join(', ') || 'None'}`
]);
setTimeout(() => {
setSimStep(2);
setSimulationLogs(prev => [
...prev,
`[localhost] TASK [Audit out-of-band diagnostic link states] **********************`,
...mappedDevices.map(d => `ok: [${d.hostname} (${d.ip})] ping_state=SUCCESS latency=1.2ms`)
]);
}, 1000);
setTimeout(() => {
setSimStep(3);
setSimulationLogs(prev => [
...prev,
`[localhost] TASK [Fetch designated golden config profile] *************************`,
`changed: [localhost] fetched golden profile for lab ID "${booking.labId}"`
]);
}, 2200);
setTimeout(() => {
setSimStep(4);
setSimulationLogs(prev => [
...prev,
`[localhost] TASK [Commit golden parameters & purge current stack] ******************`,
...mappedDevices.map(d => `changed: [${d.hostname}] configuration synced - cache invalidated - interfaces reset`),
`PLAY RECAP *************************************************************************`,
`localhost : ok=4 changed=2 unreachable=0 failed=0`
]);
setIsSimulating(false);
onAddLog({
type: 'maintenance',
message: `System Worker triggered an automated Ansible Golden Reset on reservation ${booking.id} (${lab?.name || 'Unknown'}). Checked ${mappedDevices.length} hosts.`
});
}, 3800);
};
return (
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" id="booking-details-modal">
<div className="bg-[#1E293B] border border-slate-705 w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150 flex flex-col max-h-[90vh]">
@ -392,115 +313,27 @@ ${ipList.map(ip => ` - "${ip}"`).join('\n') || ' - "No registered targ
</div>
)}
{/* BELOW BLOCK: Restful API & Automation Integration developer panel */}
{/* JSON REST Response panel */}
<div className="border border-slate-800 rounded-xl overflow-hidden bg-slate-950 font-mono">
{/* Panel Tabs Header */}
<div className="bg-slate-900 border-b border-slate-850 px-4 py-2 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
<div className="bg-slate-900 border-b border-slate-800 px-4 py-2 flex items-center justify-between">
<span className="text-[10px] uppercase tracking-wider font-bold text-indigo-400 font-sans flex items-center gap-2">
<Terminal className="w-4 h-4 text-emerald-400" />
Developer Restful API & Ansible Integration
<Database className="w-3.5 h-3.5" />
GET /api/bookings/{booking.id}
</span>
<div className="flex gap-1">
<button
onClick={() => setActiveTab('ansible')}
className={`px-2.5 py-1 text-xs rounded transition-all font-sans font-semibold flex items-center gap-1 ${
activeTab === 'ansible' ? 'bg-emerald-950/80 border border-emerald-500/50 text-emerald-400' : 'text-slate-400 hover:text-white'
}`}
>
<Cpu className="w-3 h-3" /> Ansible Playbook
</button>
<button
onClick={() => setActiveTab('terminal')}
className={`px-2.5 py-1 text-xs rounded transition-all font-sans font-semibold flex items-center gap-1 ${
activeTab === 'terminal' ? 'bg-emerald-950/80 border border-emerald-500/50 text-emerald-400' : 'text-slate-400 hover:text-white'
}`}
>
<Play className="w-3 h-3" /> Reset-Simulator {isSimulating && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse ml-1" />}
</button>
<button
onClick={() => setActiveTab('rest')}
className={`px-2.5 py-1 text-xs rounded transition-all font-sans font-semibold flex items-center gap-1 ${
activeTab === 'rest' ? 'bg-emerald-950/80 border border-emerald-500/50 text-emerald-400' : 'text-slate-400 hover:text-white'
}`}
>
<Database className="w-3 h-3" /> JSON REST Response
</button>
<span className="text-indigo-400 bg-indigo-950 border border-indigo-900 px-1.5 py-0.5 rounded font-mono text-[9px]">application/json</span>
</div>
</div>
{/* Panel Content Box */}
<div className="p-4 bg-slate-950 text-xs leading-normal font-mono relative overflow-x-auto min-h-[180px] max-h-[300px] overflow-y-auto">
{/* Copy Overlay button */}
{activeTab !== 'terminal' && (
<div className="p-4 relative overflow-x-auto max-h-[260px] overflow-y-auto">
<button
onClick={() => handleCopyText(activeTab === 'ansible' ? ansiblePlaybook : mockJsonResponse)}
className="absolute top-4 right-4 bg-slate-900 border border-slate-800 hover:border-slate-700 hover:bg-slate-850 p-1.5 text-slate-400 hover:text-white rounded flex items-center gap-1 text-[10px] font-sans transition-all"
onClick={handleCopyJson}
className="absolute top-4 right-4 bg-slate-900 border border-slate-800 hover:border-slate-700 p-1.5 text-slate-400 hover:text-white rounded flex items-center gap-1 text-[10px] font-sans transition-all"
>
{isCopied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
<span>{isCopied ? 'Copied' : 'Copy'}</span>
</button>
)}
{activeTab === 'ansible' && (
<div className="space-y-3">
<div className="text-[10px] text-slate-500 font-sans mb-1 uppercase tracking-wider">
Use this playbook in your local cron or Ansible Tower instance to automatically sync devices post-session:
</div>
<pre className="text-emerald-400/90 whitespace-pre text-[11px] leading-relaxed select-all">
{ansiblePlaybook}
</pre>
</div>
)}
{activeTab === 'rest' && (
<div className="space-y-3">
<div className="text-[10px] text-slate-500 font-sans mb-1 uppercase tracking-wider flex items-center justify-between">
<span>GET Endpoint: /api/bookings/{booking.id}</span>
<span className="text-indigo-400 bg-indigo-950 border border-indigo-900 px-1 py-0.5 rounded font-mono text-[9px]">application/json</span>
</div>
<pre className="text-slate-300 text-[11px] leading-relaxed select-all">
<pre className="text-slate-300 text-[11px] leading-relaxed select-all pr-16">
{mockJsonResponse}
</pre>
</div>
)}
{activeTab === 'terminal' && (
<div className="space-y-3 flex flex-col h-full justify-between">
<div className="text-[10px] text-slate-500 font-sans mb-2 uppercase tracking-wider flex justify-between items-center bg-slate-900/40 p-2 border border-slate-900 rounded">
<span>Manual trigger simulation to verify post-booking hardware reset tasks</span>
<button
onClick={runAnsibleSimulation}
disabled={isSimulating}
className="px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 hover:cursor-pointer disabled:bg-slate-800 text-slate-950 font-sans font-bold text-[10px] rounded flex items-center gap-1 transition"
>
<Play className="w-3 h-3 fill-slate-950" />
<span>{isSimulating ? 'SIMULATING...' : 'RUN SIMULATOR'}</span>
</button>
</div>
<div className="bg-slate-1000 border border-slate-900 p-3 rounded-lg text-[11px] leading-6 space-y-1 font-mono text-slate-305 max-h-[180px] overflow-y-auto">
{simulationLogs.length === 0 ? (
<p className="text-slate-600 italic">Playbook simulator offline. Press "Run Simulator" above to run the automated Ansible pipeline check on the active SQLite nodes...</p>
) : (
simulationLogs.map((logLine, lIdx) => (
<div key={lIdx} className={`${
logLine.includes('failed=0') || logLine.includes('sync') ? 'text-emerald-400 font-semibold' :
logLine.includes('TASK') ? 'text-indigo-400 font-semibold mt-3' :
logLine.includes('Starting') ? 'text-slate-400' : 'text-slate-300'
}`}>
{logLine}
</div>
))
)}
</div>
</div>
)}
</div>
</div>
</div>

View File

@ -315,7 +315,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
}
return (
<div className="space-y-6 max-w-2xl">
<div className="space-y-6">
{/* Page header */}
<div>
@ -343,6 +343,9 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
</div>
)}
{/* Integration cards: three columns on large screens */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
{/* ── Microsoft Entra ID ── */}
<SectionCard accentColor="bg-gradient-to-r from-blue-600 to-indigo-600">
<div className="flex items-center justify-between">
@ -629,6 +632,8 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
</div>
</SectionCard>
</div>{/* end grid */}
{/* Save bar */}
<div className="flex items-center justify-between pt-2 border-t border-slate-800/60">
<p className="text-[11px] text-slate-600">Changes are applied after saving and a server restart.</p>