web: hand-rolled modal overlays onto the Modal primitive
Five sites converted (LiveOperations, AgentUpdatesEnhanced x2, SecurityEvents, ProcessDetailModal, SecuritySettings); Layout's sidebar backdrop is a drawer, not a modal — left alone. Modal grows maxHeight + Body scrollable.
This commit is contained in:
parent
31c4ae74e5
commit
8c396c1f7a
6 changed files with 283 additions and 285 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { formatRelativeTime, formatBytes } from '@/lib/utils';
|
||||
import { packageSeverityColor, packageSeverityTextColor } from '@/components/primitives/statusColors';
|
||||
import Modal from '@/components/primitives/Modal';
|
||||
import { updateApi } from '@/lib/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -613,74 +614,68 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
|
|||
)}
|
||||
|
||||
{/* Dependency Confirmation Modal */}
|
||||
{confirmDepsUpdateId && confirmDepsData !== null && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-lg w-full max-h-[80vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
||||
<span>Confirm Dependencies</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleCancelConfirmDeps}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{confirmDepsData.length === 0 ? (
|
||||
<p className="text-sm text-gray-600">No additional dependencies detected. Proceed with installation.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-gray-600">
|
||||
The following additional packages will be installed:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-gray-700 space-y-1">
|
||||
{confirmDepsData.map((dep, i) => (
|
||||
<li key={i}>{dep}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-200 flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={handleCancelConfirmDeps}
|
||||
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExecuteConfirmDeps}
|
||||
disabled={confirmDepsMutation.isPending}
|
||||
className="px-3 py-1.5 text-sm bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50"
|
||||
>
|
||||
{confirmDepsMutation.isPending ? 'Confirming...' : 'Confirm & Install'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
open={confirmDepsUpdateId !== null && confirmDepsData !== null}
|
||||
onClose={handleCancelConfirmDeps}
|
||||
title={
|
||||
<span className="flex items-center space-x-2">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
||||
<span>Confirm Dependencies</span>
|
||||
</span>
|
||||
}
|
||||
maxWidth="lg"
|
||||
>
|
||||
<Modal.Body>
|
||||
{confirmDepsData !== null && (
|
||||
confirmDepsData.length === 0 ? (
|
||||
<p className="text-sm text-gray-600">No additional dependencies detected. Proceed with installation.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-gray-600">
|
||||
The following additional packages will be installed:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-gray-700 space-y-1 mt-2">
|
||||
{confirmDepsData.map((dep, i) => (
|
||||
<li key={i}>{dep}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
onClick={handleExecuteConfirmDeps}
|
||||
disabled={confirmDepsMutation.isPending}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm bg-orange-600 text-white border border-orange-700 rounded hover:bg-orange-700 disabled:opacity-50"
|
||||
>
|
||||
{confirmDepsMutation.isPending ? 'Confirming...' : 'Confirm & Install'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelConfirmDeps}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm text-gray-600 bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
{/* Logs Modal */}
|
||||
{showLogsModal && logsData && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[80vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-900 flex items-center space-x-2">
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span>Installation Logs</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowLogsModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 overflow-y-auto max-h-[60vh] space-y-3 text-xs">
|
||||
<Modal
|
||||
open={showLogsModal && logsData !== null}
|
||||
onClose={() => setShowLogsModal(false)}
|
||||
title={
|
||||
<span className="flex items-center space-x-2">
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span>Installation Logs</span>
|
||||
</span>
|
||||
}
|
||||
maxWidth="4xl"
|
||||
maxHeight="80vh"
|
||||
>
|
||||
<Modal.Body scrollable className="space-y-3 text-xs">
|
||||
{logsData && (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Result:</span>
|
||||
|
|
@ -720,10 +715,10 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
|
|||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import { X, Activity, Network, FileText, Key, Layers, Box } from 'lucide-react';
|
||||
import { Activity, Network, FileText, Key, Layers, Box } from 'lucide-react';
|
||||
import { useProcessDetail } from '@/hooks/useProcesses';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Modal from '@/components/primitives/Modal';
|
||||
|
||||
// Safe JSON parse — returns null on malformed data instead of crashing.
|
||||
const safeParse = (data: any): any => {
|
||||
|
|
@ -27,22 +28,8 @@ export const ProcessDetailModal: React.FC<ProcessDetailModalProps> = ({
|
|||
const [activeTab, setActiveTab] = useState<DetailTab>('overview');
|
||||
const { data, isLoading } = useProcessDetail(agentId, processId, true);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex items-center gap-3 text-gray-500">
|
||||
<Activity className="h-5 w-5 animate-spin" />
|
||||
Loading process detail...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const proc = data?.process;
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const proc = data.process;
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!bytes) return '0 B';
|
||||
const k = 1024;
|
||||
|
|
@ -57,56 +44,65 @@ export const ProcessDetailModal: React.FC<ProcessDetailModalProps> = ({
|
|||
return d.toLocaleString();
|
||||
};
|
||||
|
||||
const tabs: { key: DetailTab; label: string; icon: React.ReactNode; count?: number }[] = [
|
||||
const tabs: { key: DetailTab; label: string; icon: React.ReactNode; count?: number }[] = data ? [
|
||||
{ key: 'overview', label: 'Overview', icon: <Activity className="h-4 w-4" /> },
|
||||
{ key: 'network', label: 'Network', icon: <Network className="h-4 w-4" />, count: (data.open_sockets?.length ?? 0) + (data.listening_ports?.length ?? 0) },
|
||||
{ key: 'files', label: 'Files', icon: <FileText className="h-4 w-4" />, count: data.open_files?.length },
|
||||
{ key: 'environment', label: 'Env', icon: <Key className="h-4 w-4" />, count: data.environment?.length },
|
||||
{ key: 'memory', label: 'Memory', icon: <Layers className="h-4 w-4" />, count: data.memory_map?.length },
|
||||
{ key: 'namespaces', label: 'Namespaces', icon: <Box className="h-4 w-4" />, count: data.namespaces?.length },
|
||||
];
|
||||
] : [];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[85vh] flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b">
|
||||
<Modal
|
||||
open
|
||||
onClose={onClose}
|
||||
title={
|
||||
proc ? (
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900">{proc.name}</h3>
|
||||
<p className="text-xs text-gray-500 font-mono">PID {proc.pid} · {proc.user} · {proc.path || 'no path'}</p>
|
||||
<div className="text-lg font-medium text-gray-900">{proc.name}</div>
|
||||
<div className="text-xs text-gray-500 font-mono font-normal">PID {proc.pid} · {proc.user} · {proc.path || 'no path'}</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="h-5 w-5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
) : 'Process Detail'
|
||||
}
|
||||
maxWidth="4xl"
|
||||
maxHeight="85vh"
|
||||
>
|
||||
{isLoading && (
|
||||
<Modal.Body>
|
||||
<div className="flex items-center gap-3 text-gray-500 py-4">
|
||||
<Activity className="h-5 w-5 animate-spin" />
|
||||
Loading process detail...
|
||||
</div>
|
||||
</Modal.Body>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b px-6">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-2.5 text-sm border-b-2 -mb-px transition-colors',
|
||||
activeTab === tab.key
|
||||
? 'border-red-500 text-red-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
{tab.count !== undefined && tab.count > 0 && (
|
||||
<span className="ml-1 text-xs bg-gray-100 text-gray-600 rounded-full px-1.5">{tab.count}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isLoading && data && proc && (
|
||||
<>
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b px-6 flex-shrink-0">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-2.5 text-sm border-b-2 -mb-px transition-colors',
|
||||
activeTab === tab.key
|
||||
? 'border-red-500 text-red-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
{tab.count !== undefined && tab.count > 0 && (
|
||||
<span className="ml-1 text-xs bg-gray-100 text-gray-600 rounded-full px-1.5">{tab.count}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{/* Content */}
|
||||
<Modal.Body scrollable className="p-6">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field label="Command" value={proc.cmdline || '—'} mono fullWidth />
|
||||
|
|
@ -319,9 +315,10 @@ export const ProcessDetailModal: React.FC<ProcessDetailModalProps> = ({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,12 +19,18 @@ import { cn } from '@/lib/utils';
|
|||
*
|
||||
* Handles: Escape key, overlay click, focus trap, scroll lock.
|
||||
* Only renders when `open` is true.
|
||||
*
|
||||
* maxHeight: constrains the panel height and switches the panel to flex-col layout so
|
||||
* Modal.Body with scrollable=true can absorb remaining space. Use when inner content
|
||||
* may overflow (logs, long lists, tabbed detail views).
|
||||
*/
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: React.ReactNode;
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '4xl';
|
||||
/** Constrain panel height and switch to flex-col. Pair with Modal.Body scrollable. */
|
||||
maxHeight?: '80vh' | '85vh';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -37,11 +43,17 @@ const WIDTH_MAP: Record<string, string> = {
|
|||
'4xl': 'sm:max-w-4xl',
|
||||
};
|
||||
|
||||
const MAX_HEIGHT_MAP: Record<string, string> = {
|
||||
'80vh': 'max-h-[80vh]',
|
||||
'85vh': 'max-h-[85vh]',
|
||||
};
|
||||
|
||||
const Modal: React.FC<ModalProps> & { Body: typeof ModalBody; Footer: typeof ModalFooter } = ({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
maxWidth = '2xl',
|
||||
maxHeight,
|
||||
children,
|
||||
}) => {
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -94,7 +106,9 @@ const Modal: React.FC<ModalProps> & { Body: typeof ModalBody; Footer: typeof Mod
|
|||
tabIndex={-1}
|
||||
className={cn(
|
||||
'relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full border border-gray-200',
|
||||
WIDTH_MAP[maxWidth]
|
||||
WIDTH_MAP[maxWidth],
|
||||
maxHeight && MAX_HEIGHT_MAP[maxHeight],
|
||||
maxHeight && 'flex flex-col',
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
|
|
@ -124,10 +138,12 @@ const Modal: React.FC<ModalProps> & { Body: typeof ModalBody; Footer: typeof Mod
|
|||
interface ModalBodyProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
/** Flex-grow + overflow-y-auto. Use when the parent Modal has maxHeight set. */
|
||||
scrollable?: boolean;
|
||||
}
|
||||
|
||||
const ModalBody: React.FC<ModalBodyProps> = ({ children, className }) => (
|
||||
<div className={cn('bg-white px-6 py-4', className)}>{children}</div>
|
||||
const ModalBody: React.FC<ModalBodyProps> = ({ children, className, scrollable }) => (
|
||||
<div className={cn('bg-white px-6 py-4', scrollable && 'flex-1 overflow-y-auto', className)}>{children}</div>
|
||||
);
|
||||
|
||||
interface ModalFooterProps {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useSecurityEvents, useSecurityWebSocket } from '@/hooks/useSecuritySett
|
|||
import { SecurityEvent, EventFilters } from '@/types/security';
|
||||
import { clientLogger } from '@/lib/client-logger';
|
||||
import { securityEventSeverityColor } from '@/components/primitives/statusColors';
|
||||
import Modal from '@/components/primitives/Modal';
|
||||
|
||||
const SecurityEvents: React.FC = () => {
|
||||
const [filters, setFilters] = useState<EventFilters>({});
|
||||
|
|
@ -477,96 +478,84 @@ const SecurityEvents: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Event Detail Modal */}
|
||||
{selectedEvent && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Event Details</h3>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
</button>
|
||||
<Modal
|
||||
open={selectedEvent !== null}
|
||||
onClose={() => setSelectedEvent(null)}
|
||||
title="Event Details"
|
||||
maxWidth="2xl"
|
||||
maxHeight="80vh"
|
||||
>
|
||||
<Modal.Body scrollable>
|
||||
{selectedEvent && (
|
||||
<div className="space-y-4">
|
||||
{/* Event Header */}
|
||||
<div className="flex items-start gap-4 pb-4 border-b">
|
||||
<div className={`p-2 rounded-lg border ${securityEventSeverityColor(selectedEvent.severity)}`}>
|
||||
{getSeverityIcon(selectedEvent.severity)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900 mb-1">
|
||||
{selectedEvent.event_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{selectedEvent.message}</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
{formatTimestamp(selectedEvent.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Event Header */}
|
||||
<div className="flex items-start gap-4 pb-4 border-b">
|
||||
<div className={`p-2 rounded-lg border ${securityEventSeverityColor(selectedEvent.severity)}`}>
|
||||
{getSeverityIcon(selectedEvent.severity)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900 mb-1">
|
||||
{selectedEvent.event_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{selectedEvent.message}</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
{formatTimestamp(selectedEvent.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
{/* Event Information */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Severity</p>
|
||||
<p className="capitalize">{selectedEvent.severity}</p>
|
||||
</div>
|
||||
|
||||
{/* Event Information */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Severity</p>
|
||||
<p className="capitalize">{selectedEvent.severity}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Category</p>
|
||||
<p className="capitalize">{selectedEvent.category.replace('_', ' ')}</p>
|
||||
</div>
|
||||
{selectedEvent.agent_id && (
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Agent ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.agent_id}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.user_id && (
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">User ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.user_id}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.trace_id && (
|
||||
<div className="col-span-2">
|
||||
<p className="font-medium text-gray-900">Trace ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.trace_id}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Category</p>
|
||||
<p className="capitalize">{selectedEvent.category.replace('_', ' ')}</p>
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
{Object.keys(selectedEvent.details).length > 0 && (
|
||||
{selectedEvent.agent_id && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="font-medium text-gray-900">Additional Details</p>
|
||||
<button
|
||||
onClick={() => copyEventDetails(selectedEvent)}
|
||||
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 bg-gray-50 rounded border text-xs overflow-auto max-h-48">
|
||||
{JSON.stringify(selectedEvent.details, null, 2)}
|
||||
</pre>
|
||||
<p className="font-medium text-gray-900">Agent ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.agent_id}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.user_id && (
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">User ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.user_id}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.trace_id && (
|
||||
<div className="col-span-2">
|
||||
<p className="font-medium text-gray-900">Trace ID</p>
|
||||
<p className="font-mono text-xs">{selectedEvent.trace_id}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
{Object.keys(selectedEvent.details).length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="font-medium text-gray-900">Additional Details</p>
|
||||
<button
|
||||
onClick={() => copyEventDetails(selectedEvent)}
|
||||
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 bg-gray-50 rounded border text-xs overflow-auto max-h-48">
|
||||
{JSON.stringify(selectedEvent.details, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
Shield,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react';
|
||||
import { FilterBar, PageState, StatusBadge } from '@/components/primitives';
|
||||
import { FilterBar, PageState, StatusBadge, Modal } from '@/components/primitives';
|
||||
import { useFilterUrl, buildFilterPills } from '@/hooks/useFilterUrl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAgents } from '@/hooks/useAgents';
|
||||
|
|
@ -737,11 +737,13 @@ const LiveOperations: React.FC = () => {
|
|||
</PageState>
|
||||
|
||||
{/* Cleanup Confirmation Dialog */}
|
||||
{showCleanupDialog && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Archive Failed Operations</h3>
|
||||
|
||||
<Modal
|
||||
open={showCleanupDialog}
|
||||
onClose={() => setShowCleanupDialog(false)}
|
||||
title="Archive Failed Operations"
|
||||
maxWidth="md"
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className="mb-4 alert alert-info rounded-md p-3">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>INFO:</strong> This will remove failed commands from the active operations view, but all history will be preserved in the database for audit trails and continuity.
|
||||
|
|
@ -754,7 +756,7 @@ const LiveOperations: React.FC = () => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Clear operations older than
|
||||
|
|
@ -824,25 +826,23 @@ const LiveOperations: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowCleanupDialog(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClearFailedCommands}
|
||||
disabled={clearFailedMutation.isPending}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{clearFailedMutation.isPending ? 'Archiving...' : 'Archive Failed Commands'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
onClick={handleClearFailedCommands}
|
||||
disabled={clearFailedMutation.isPending}
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-red-600 border border-red-700 rounded hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{clearFailedMutation.isPending ? 'Archiving...' : 'Archive Failed Commands'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCleanupDialog(false)}
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import SecurityStatusCard from '@/components/security/SecurityStatusCard';
|
|||
import SecurityCategorySection from '@/components/security/SecurityCategorySection';
|
||||
import SecurityEvents from '@/components/security/SecurityEvents';
|
||||
import SigningKeyRoster from '@/components/security/SigningKeyRoster';
|
||||
import Modal from '@/components/primitives/Modal';
|
||||
|
||||
const SecuritySettings: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -595,57 +596,57 @@ const SecuritySettings: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{confirmationDialog.isOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{confirmationDialog.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{confirmationDialog.message}
|
||||
</p>
|
||||
{confirmationDialog.requiresConfirmation && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Type "{confirmationDialog.title === 'Rotate Security Key' ? 'CONFIRM' : 'RESET'}" to proceed
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
onChange={(e) => {
|
||||
const expected = confirmationDialog.title === 'Rotate Security Key' ? 'CONFIRM' : 'RESET';
|
||||
if (e.target.value === expected) {
|
||||
e.target.classList.remove('border-red-300');
|
||||
e.target.classList.add('border-green-300');
|
||||
} else {
|
||||
e.target.classList.remove('border-green-300');
|
||||
e.target.classList.add('border-red-300');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={confirmationDialog.onCancel}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmationDialog.onConfirm}
|
||||
className={`px-4 py-2 rounded-lg text-white ${
|
||||
confirmationDialog.severity === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<Modal
|
||||
open={confirmationDialog.isOpen}
|
||||
onClose={confirmationDialog.onCancel}
|
||||
title={confirmationDialog.title}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<Modal.Body>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{confirmationDialog.message}
|
||||
</p>
|
||||
{confirmationDialog.requiresConfirmation && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Type "{confirmationDialog.title === 'Rotate Security Key' ? 'CONFIRM' : 'RESET'}" to proceed
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
onChange={(e) => {
|
||||
const expected = confirmationDialog.title === 'Rotate Security Key' ? 'CONFIRM' : 'RESET';
|
||||
if (e.target.value === expected) {
|
||||
e.target.classList.remove('border-red-300');
|
||||
e.target.classList.add('border-green-300');
|
||||
} else {
|
||||
e.target.classList.remove('border-green-300');
|
||||
e.target.classList.add('border-red-300');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
onClick={confirmationDialog.onConfirm}
|
||||
className={`inline-flex items-center px-4 py-2 rounded border text-white ${
|
||||
confirmationDialog.severity === 'danger'
|
||||
? 'bg-red-600 border-red-700 hover:bg-red-700'
|
||||
: 'bg-blue-600 border-blue-700 hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmationDialog.onCancel}
|
||||
className="inline-flex items-center px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue