feat: add ScriptConfigurePane — configure mode layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
213
frontend/src/components/scripts/ScriptConfigurePane.tsx
Normal file
213
frontend/src/components/scripts/ScriptConfigurePane.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Terminal, Download, Loader2, AlertTriangle, Copy, Check } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useScriptGeneratorStore } from '@/store/scriptGeneratorStore'
|
||||
import { ScriptParameterForm } from './ScriptParameterForm'
|
||||
|
||||
const COMPLEXITY_CLASSES = {
|
||||
beginner: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20',
|
||||
intermediate: 'text-amber-400 bg-amber-400/10 border-amber-400/20',
|
||||
advanced: 'text-rose-500 bg-rose-500/10 border-rose-500/20',
|
||||
} as const
|
||||
|
||||
interface Props {
|
||||
canGenerate: boolean
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function ScriptConfigurePane({ canGenerate, onBack }: Props) {
|
||||
const selectedTemplate = useScriptGeneratorStore(s => s.selectedTemplate)
|
||||
const isLoadingDetail = useScriptGeneratorStore(s => s.isLoadingDetail)
|
||||
const categories = useScriptGeneratorStore(s => s.categories)
|
||||
const generatedScript = useScriptGeneratorStore(s => s.generatedScript)
|
||||
const generationWarnings = useScriptGeneratorStore(s => s.generationWarnings)
|
||||
const isGenerating = useScriptGeneratorStore(s => s.isGenerating)
|
||||
const generateError = useScriptGeneratorStore(s => s.generateError)
|
||||
const generate = useScriptGeneratorStore(s => s.generate)
|
||||
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!generatedScript) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(generatedScript)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!generatedScript || !selectedTemplate) return
|
||||
const blob = new Blob([generatedScript], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${selectedTemplate.slug}.ps1`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (isLoadingDetail) {
|
||||
return (
|
||||
<div className="glass-card-static h-full flex flex-col p-4 overflow-y-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4 w-fit"
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
Back to library
|
||||
</button>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Loader2 size={28} className="text-primary animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// First-selection failure state
|
||||
if (!selectedTemplate) {
|
||||
return (
|
||||
<div className="glass-card-static h-full flex flex-col p-4 overflow-y-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4 w-fit"
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
Back to library
|
||||
</button>
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-center">
|
||||
<Terminal size={32} className="text-muted-foreground/40" />
|
||||
<p className="text-sm text-muted-foreground">Failed to load template.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const categoryName = categories.find(c => c.id === selectedTemplate.category_id)?.name
|
||||
const displayTags = selectedTemplate.tags.slice(0, 3)
|
||||
const extraTagCount = selectedTemplate.tags.length - 3
|
||||
|
||||
return (
|
||||
<div className="glass-card-static h-full flex flex-col p-4 overflow-y-auto">
|
||||
{/* Back button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4 w-fit"
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
Back to library
|
||||
</button>
|
||||
|
||||
{/* Template header */}
|
||||
<div className="mb-3">
|
||||
<h2 className="text-base font-semibold font-heading text-foreground">
|
||||
{selectedTemplate.name}
|
||||
</h2>
|
||||
{selectedTemplate.description && (
|
||||
<p className="text-sm text-muted-foreground mt-0.5">{selectedTemplate.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 flex-wrap mt-2">
|
||||
{selectedTemplate.requires_elevation && (
|
||||
<span
|
||||
title="Requires administrator elevation"
|
||||
className="font-label text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded border text-amber-400 bg-amber-400/10 border-amber-400/20"
|
||||
>
|
||||
⚠ Elevated
|
||||
</span>
|
||||
)}
|
||||
<span className={cn(
|
||||
'font-label text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded border',
|
||||
COMPLEXITY_CLASSES[selectedTemplate.complexity]
|
||||
)}>
|
||||
{selectedTemplate.complexity}
|
||||
</span>
|
||||
{categoryName && (
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded border border-border text-muted-foreground bg-white/5">
|
||||
{categoryName}
|
||||
</span>
|
||||
)}
|
||||
{displayTags.map(tag => (
|
||||
<span key={tag} className="font-label text-[0.625rem] px-1.5 py-0.5 rounded border border-border text-muted-foreground bg-white/5">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{extraTagCount > 0 && (
|
||||
<span className="font-label text-[0.625rem] text-muted-foreground">+{extraTagCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border mb-4" />
|
||||
|
||||
{/* Parameter form */}
|
||||
<ScriptParameterForm canGenerate={canGenerate} />
|
||||
|
||||
{/* Warnings */}
|
||||
{generationWarnings.length > 0 && (
|
||||
<div className="flex flex-col gap-1 rounded-lg border border-amber-400/20 bg-amber-400/5 p-3 mt-4">
|
||||
<div className="flex items-center gap-1.5 text-amber-400 text-xs font-medium mb-1">
|
||||
<AlertTriangle size={13} />
|
||||
Warnings
|
||||
</div>
|
||||
{generationWarnings.map((w, i) => (
|
||||
<p key={i} className="text-xs text-amber-400/80">{w}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action bar */}
|
||||
<div className="flex flex-col gap-2 mt-4 pt-1">
|
||||
<span title={!canGenerate ? 'Engineer access required' : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => generate()}
|
||||
disabled={isGenerating || !canGenerate}
|
||||
className="w-full flex items-center justify-center gap-1.5 bg-gradient-brand text-[#101114] font-semibold text-sm px-4 py-2 rounded-[10px] hover:opacity-90 active:scale-[0.97] transition-all shadow-lg shadow-primary/20 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
|
||||
>
|
||||
{isGenerating && <Loader2 size={14} className="animate-spin" />}
|
||||
Generate Script
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<span className="flex-1" title={!canGenerate ? 'Engineer access required' : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={!generatedScript || !canGenerate}
|
||||
className="w-full flex items-center justify-center gap-1.5 bg-white/5 border border-border text-foreground text-sm px-4 py-2 rounded-[10px] hover:border-[rgba(255,255,255,0.12)] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Download size={14} />
|
||||
Download .ps1
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span className="flex-1" title={!canGenerate ? 'Engineer access required' : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!generatedScript || !canGenerate}
|
||||
className="w-full flex items-center justify-center gap-1.5 bg-white/5 border border-border text-foreground text-sm px-4 py-2 rounded-[10px] hover:border-[rgba(255,255,255,0.12)] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{copied ? <Check size={14} className="text-emerald-400" /> : <Copy size={14} />}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate error */}
|
||||
{generateError && (
|
||||
<p className="text-xs text-rose-500 mt-2">{generateError}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user