release-tracker/src/components/steps/step-detail-panel.tsx

407 lines
14 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { X, RotateCcw, Pencil, Trash2, FileText, AlertCircle, CheckCircle, Check, SkipForward, Copy } from 'lucide-react';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import { CodeBlock } from './code-block';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
// Copy button with subtle feedback
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className={`h-7 px-2 transition-colors duration-200 ${
copied
? 'text-green-600 bg-green-50 hover:bg-green-100 hover:text-green-700'
: 'text-slate-500 hover:text-slate-700'
}`}
>
{copied ? (
<>
<Check className="w-3.5 h-3.5 mr-1" />
Copied
</>
) : (
<>
<Copy className="w-3.5 h-3.5 mr-1" />
Copy
</>
)}
</Button>
);
}
interface StepDetailPanelProps {
step: any;
template: any;
isOpen: boolean;
onClose: () => void;
onMarkDone: (id: number, notes?: string) => Promise<void>;
onSkip: (id: number, reason: string) => Promise<void>;
onRevert: (id: number, reason?: string) => Promise<void>;
onOverride: (id: number, content: string) => Promise<void>;
onResetToTemplate: (id: number) => Promise<void>;
onEditCustom?: (id: number, data: any) => Promise<void>;
onDeleteCustom?: (id: number) => Promise<void>;
}
const statusIcons = {
pending: <div className="w-5 h-5 rounded-full border-2 border-slate-300" />,
done: <CheckCircle className="w-5 h-5 text-green-500" />,
skipped: <SkipForward className="w-5 h-5 text-amber-500" />,
reverted: <RotateCcw className="w-5 h-5 text-red-500" />,
};
const statusLabels = {
pending: 'Pending',
done: 'Done',
skipped: 'Skipped',
reverted: 'Reverted',
};
const typeLabels = {
bash: 'Bash Script',
sql: 'SQL',
text: 'Text',
};
export function StepDetailPanel({
step,
template,
isOpen,
onClose,
onMarkDone,
onSkip,
onRevert,
onOverride,
onResetToTemplate,
onEditCustom,
onDeleteCustom,
}: StepDetailPanelProps) {
const [notes, setNotes] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState('');
const [showOriginal, setShowOriginal] = useState(false);
const [skipReason, setSkipReason] = useState('');
const [isSkipping, setIsSkipping] = useState(false);
// Sync state when step changes
useEffect(() => {
if (step) {
setNotes(step.notes || '');
setEditContent(step.content || '');
setIsEditing(false);
setShowOriginal(false);
setSkipReason('');
setIsSkipping(false);
}
}, [step?.id]);
if (!step) return null;
const isCustom = step.isCustom;
const isOverridden = step.isOverridden;
const hasTemplate = !!step.templateId;
const handleMarkDone = async () => {
await onMarkDone(step.id, notes);
onClose();
};
const handleSkip = async () => {
if (!skipReason.trim()) return;
await onSkip(step.id, skipReason);
onClose();
};
const handleRevert = async () => {
await onRevert(step.id, notes);
onClose();
};
const handleOverride = async () => {
await onOverride(step.id, editContent);
setIsEditing(false);
};
const handleResetToTemplate = async () => {
await onResetToTemplate(step.id);
onClose();
};
const handleDeleteCustom = async () => {
if (!onDeleteCustom) return;
if (confirm('Are you sure you want to delete this custom step?')) {
await onDeleteCustom(step.id);
onClose();
}
};
const renderSourceBadge = () => {
if (isCustom) {
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-purple-100 text-purple-800">
<FileText className="w-3 h-3 mr-1" />
Custom Step
</Badge>
{onEditCustom && onDeleteCustom && (
<div className="flex gap-1">
<Button variant="ghost" size="sm" onClick={() => setIsEditing(true)}>
<Pencil className="w-3 h-3 mr-1" />
Edit
</Button>
<Button variant="ghost" size="sm" className="text-red-600" onClick={handleDeleteCustom}>
<Trash2 className="w-3 h-3 mr-1" />
Delete
</Button>
</div>
)}
</div>
);
}
if (isOverridden) {
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-amber-100 text-amber-800">
<AlertCircle className="w-3 h-3 mr-1" />
Overridden from Template
</Badge>
<Button variant="ghost" size="sm" onClick={() => setShowOriginal(!showOriginal)}>
{showOriginal ? 'Hide Original' : 'View Original'}
</Button>
<Button variant="ghost" size="sm" onClick={handleResetToTemplate}>
<RotateCcw className="w-3 h-3 mr-1" />
Reset to Template
</Button>
</div>
);
}
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
<FileText className="w-3 h-3 mr-1" />
From Template
</Badge>
<Button variant="ghost" size="sm" onClick={() => setIsEditing(true)}>
<Pencil className="w-3 h-3 mr-1" />
Override Content
</Button>
</div>
);
};
return (
<Sheet open={isOpen} onOpenChange={onClose}>
<SheetContent key={step?.id} className="w-[800px] sm:max-w-[800px] overflow-hidden">
<SheetHeader className="px-6">
<div className="flex items-start justify-between">
<div>
<SheetTitle className="text-xl mb-2">{step.name}</SheetTitle>
<div className="flex items-center gap-2 text-sm text-slate-500">
<span>{step.customer?.name}</span>
<span></span>
<span>{step.customer?.cluster?.name}</span>
<span></span>
<span>{step.customer?.namespace}</span>
</div>
</div>
</div>
</SheetHeader>
<div style={{ width: '752px', maxWidth: '752px' }}>
<ScrollArea className="h-[calc(100vh-180px)] mt-6 px-6 w-full">
<div className="space-y-6" style={{ width: '704px', maxWidth: '704px' }}>
{/* Status & Type */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
{statusIcons[step.status as keyof typeof statusIcons]}
<span className="font-medium">{statusLabels[step.status as keyof typeof statusLabels]}</span>
</div>
<Badge variant="outline">{typeLabels[step.type as keyof typeof typeLabels]}</Badge>
<Badge variant="outline" className="capitalize">{step.category}</Badge>
</div>
<Separator />
{/* Source Info */}
<div>
<label className="text-sm font-medium text-slate-500 mb-2 block">Source</label>
{renderSourceBadge()}
</div>
{/* Content */}
<div className="min-w-0" style={{ maxWidth: '100%' }}>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-slate-500">Content</label>
{!isEditing && (
<CopyButton text={step.content} />
)}
</div>
{isEditing ? (
<div className="space-y-3">
<Textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="font-mono min-h-[200px]"
/>
<div className="flex gap-2">
<Button size="sm" onClick={handleOverride}>
<Check className="w-4 h-4 mr-1" />
Save
</Button>
<Button size="sm" variant="outline" onClick={() => {
setIsEditing(false);
setEditContent(step.content);
}}>
Cancel
</Button>
</div>
</div>
) : (
<div className="overflow-hidden" style={{ maxWidth: '100%', width: '100%' }}>
<CodeBlock code={step.content} type={step.type} />
</div>
)}
{/* Show original content if overridden */}
{showOriginal && template && (
<div className="mt-4">
<label className="text-sm font-medium text-slate-500 mb-2 block">Original Template Content</label>
<CodeBlock code={template.content} type={template.type} />
</div>
)}
</div>
<Separator />
{/* Execution Section */}
{step.status !== 'done' && step.status !== 'skipped' && (
<div className="space-y-3">
<label className="text-sm font-medium text-slate-500 block">Execution Notes</label>
<Textarea
placeholder="Add notes about this execution..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="min-h-[80px]"
/>
{isSkipping ? (
<div className="space-y-2">
<Textarea
placeholder="Reason for skipping..."
value={skipReason}
onChange={(e) => setSkipReason(e.target.value)}
className="min-h-[60px]"
/>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={handleSkip} disabled={!skipReason.trim()}>
Confirm Skip
</Button>
<Button size="sm" variant="ghost" onClick={() => setIsSkipping(false)}>
Cancel
</Button>
</div>
</div>
) : (
<div className="flex gap-2">
<Button onClick={handleMarkDone}>
<Check className="w-4 h-4 mr-1" />
Mark as Done
</Button>
<Button variant="outline" onClick={() => setIsSkipping(true)}>
<SkipForward className="w-4 h-4 mr-1" />
Skip
</Button>
</div>
)}
</div>
)}
{step.status === 'done' && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-green-600">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">Completed</span>
{step.executedAt && (
<span className="text-sm text-slate-500">
at {new Date(step.executedAt).toLocaleString()}
</span>
)}
</div>
{step.notes && (
<div className="bg-slate-50 p-3 rounded-lg">
<label className="text-sm font-medium text-slate-500">Notes</label>
<p className="text-slate-700 mt-1">{step.notes}</p>
</div>
)}
<Button variant="outline" onClick={handleRevert}>
<RotateCcw className="w-4 h-4 mr-1" />
Revert to Pending
</Button>
</div>
)}
{step.status === 'skipped' && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-amber-600">
<SkipForward className="w-5 h-5" />
<span className="font-medium">Skipped</span>
</div>
{step.skipReason && (
<div className="bg-amber-50 p-3 rounded-lg border border-amber-200">
<label className="text-sm font-medium text-amber-700">Skip Reason</label>
<p className="text-slate-700 mt-1">{step.skipReason}</p>
</div>
)}
</div>
)}
{/* History */}
{(step.executedAt || step.updatedAt) && (
<>
<Separator />
<div>
<label className="text-sm font-medium text-slate-500 mb-2 block">History</label>
<div className="space-y-1 text-sm text-slate-500">
<p> Created: {new Date(step.createdAt).toLocaleString()}</p>
{step.isOverridden && <p> Content overridden</p>}
{step.executedAt && <p> Executed: {new Date(step.executedAt).toLocaleString()}</p>}
{step.status === 'skipped' && <p> Skipped: {new Date(step.updatedAt).toLocaleString()}</p>}
</div>
</div>
</>
)}
</div>
</ScrollArea>
</div>
</SheetContent>
</Sheet>
);
}