Watch
1
0
Fork
You've already forked RedFlag
0

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:
Fimeg 2026-06-12 14:18:52 -04:00
commit 8c396c1f7a
6 changed files with 283 additions and 285 deletions

View file

@ -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>
);
}

View file

@ -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>
);
};

View file

@ -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 {

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};