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:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user