30 references to removed CSS variables: border-brand-border → border-[#1e2130], text-brand-text-muted → text-[#4f5666], text-brand-dark → text-white, bg-white/[0.04] → bg-[#191c25], hover:border-white/[0.12] → hover:border-[#2a2f3d]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
218 lines
9.2 KiB
TypeScript
218 lines
9.2 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Plus, Search, Pencil, Trash2, Users, User as UserIcon, Loader2, FileCode, ArrowLeft } from 'lucide-react'
|
|
import { Link } from 'react-router-dom'
|
|
import { cn } from '@/lib/utils'
|
|
import { usePermissions } from '@/hooks/usePermissions'
|
|
import { scriptsApi } from '@/api'
|
|
import type { ScriptTemplateListItem, ScriptCategoryResponse } from '@/types'
|
|
|
|
const COMPLEXITY_CLASSES = {
|
|
beginner: 'text-emerald-400 bg-emerald-400/10',
|
|
intermediate: 'text-amber-400 bg-amber-400/10',
|
|
advanced: 'text-rose-500 bg-rose-500/10',
|
|
} as const
|
|
|
|
interface Props {
|
|
onEdit: (id: string) => void
|
|
onCreate: () => void
|
|
}
|
|
|
|
export function ScriptTemplateListView({ onEdit, onCreate }: Props) {
|
|
const [templates, setTemplates] = useState<ScriptTemplateListItem[]>([])
|
|
const [categories, setCategories] = useState<ScriptCategoryResponse[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
|
|
|
|
const { canManageScriptTemplate, canCreateScriptTemplate } = usePermissions()
|
|
|
|
const loadData = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const [tpls, cats] = await Promise.all([
|
|
scriptsApi.getManagedTemplates(searchQuery ? { search: searchQuery } : undefined),
|
|
scriptsApi.getCategories(),
|
|
])
|
|
setTemplates(tpls)
|
|
setCategories(cats)
|
|
} catch {
|
|
// silently fail
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
loadData()
|
|
}, 300)
|
|
return () => clearTimeout(timer)
|
|
}, [searchQuery]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await scriptsApi.deleteTemplate(id)
|
|
setTemplates(prev => prev.filter(t => t.id !== id))
|
|
setDeleteConfirm(null)
|
|
} catch {
|
|
// silently fail
|
|
}
|
|
}
|
|
|
|
const getCategoryName = (categoryId: string) =>
|
|
categories.find(c => c.id === categoryId)?.name ?? 'Unknown'
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
{/* Back link */}
|
|
<Link
|
|
to="/scripts"
|
|
className="flex items-center gap-1.5 text-xs text-[#848b9b] hover:text-[#e2e5eb] transition-colors w-fit"
|
|
>
|
|
<ArrowLeft size={12} />
|
|
Back to Script Library
|
|
</Link>
|
|
|
|
{/* Header row */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-heading font-bold text-[#e2e5eb]">Manage Templates</h1>
|
|
<p className="text-sm text-[#848b9b] mt-1">
|
|
Create and edit PowerShell script templates.
|
|
</p>
|
|
</div>
|
|
{canCreateScriptTemplate && (
|
|
<button
|
|
type="button"
|
|
onClick={onCreate}
|
|
className="flex items-center gap-1.5 bg-[#22d3ee] text-white font-semibold text-sm px-4 py-2 rounded-lg hover:brightness-110 active:scale-[0.98] transition-all"
|
|
>
|
|
<Plus size={16} />
|
|
New Template
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="relative w-64">
|
|
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#848b9b] pointer-events-none" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={e => setSearchQuery(e.target.value)}
|
|
placeholder="Search templates…"
|
|
className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-[#1e2130] bg-[#14161d] text-[#e2e5eb] placeholder:text-[#848b9b] focus:outline-none focus:border-[rgba(6,182,212,0.3)] focus:ring-1 focus:ring-[rgba(6,182,212,0.2)]"
|
|
/>
|
|
</div>
|
|
|
|
{/* Template list */}
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 size={28} className="text-[#22d3ee] animate-spin" />
|
|
</div>
|
|
) : templates.length === 0 ? (
|
|
<div className="card-flat flex flex-col items-center justify-center gap-3 py-12 text-center">
|
|
<FileCode size={32} className="text-[#848b9b]/40" />
|
|
<p className="text-sm text-[#848b9b]">
|
|
{searchQuery ? 'No templates match your search' : 'No templates yet. Create your first one!'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="card-flat overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-[#1e2130]">
|
|
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Name</th>
|
|
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Category</th>
|
|
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Complexity</th>
|
|
<th className="text-left font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Scope</th>
|
|
<th className="text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Uses</th>
|
|
<th className="text-right font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b] px-4 py-3">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{templates.map(t => (
|
|
<tr
|
|
key={t.id}
|
|
className="border-b border-[#1e2130] last:border-b-0 hover:bg-[#14161d] transition-colors"
|
|
>
|
|
<td className="px-4 py-3">
|
|
<span className="text-[#e2e5eb] font-medium">{t.name}</span>
|
|
{t.description && (
|
|
<p className="text-xs text-[#848b9b] line-clamp-1 mt-0.5">{t.description}</p>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3 text-[#848b9b]">{getCategoryName(t.category_id)}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={cn('font-sans text-xs text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded', COMPLEXITY_CLASSES[t.complexity])}>
|
|
{t.complexity}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className={cn(
|
|
'inline-flex items-center gap-1 font-sans text-xs text-[0.625rem] uppercase tracking-wide px-1.5 py-0.5 rounded border',
|
|
t.team_id
|
|
? 'text-[#22d3ee] bg-[rgba(34,211,238,0.10)] border-primary/20'
|
|
: 'text-[#848b9b] bg-white/5 border-[#1e2130]'
|
|
)}>
|
|
{t.team_id ? <><Users size={10} /> Team</> : <><UserIcon size={10} /> Personal</>}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-right text-[#848b9b]">{t.usage_count}</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<div className="flex items-center justify-end gap-1">
|
|
{canManageScriptTemplate(t) && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => onEdit(t.id)}
|
|
className="p-1.5 rounded-md text-[#848b9b] hover:text-[#e2e5eb] hover:bg-white/5 transition-colors"
|
|
title="Edit template"
|
|
>
|
|
<Pencil size={14} />
|
|
</button>
|
|
{deleteConfirm === t.id ? (
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDelete(t.id)}
|
|
className="text-[0.625rem] font-sans text-xs text-rose-500 hover:text-rose-400 px-1.5 py-0.5"
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDeleteConfirm(null)}
|
|
className="text-[0.625rem] font-sans text-xs text-[#848b9b] hover:text-[#e2e5eb] px-1.5 py-0.5"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => setDeleteConfirm(t.id)}
|
|
className="p-1.5 rounded-md text-[#848b9b] hover:text-rose-500 hover:bg-white/5 transition-colors"
|
|
title="Delete template"
|
|
>
|
|
<Trash2 size={14} />
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|