11 KiB
Script Generator Phase 2 — Frontend Design
Date: 2026-03-13
Status: Approved
Phase: 2 of Script Generator feature
Builds on: Phase 1 backend (feat/script-generator PR #105)
Goal
Build the Script Library frontend: a three-panel page where MSP engineers can browse PowerShell script templates by category, fill in parameters, get a live preview, and generate + copy/download the final script.
Architecture
Layout: Top filter bar + two-column layout (C). Category tabs and search across the top, template list on the left, generator panel on the right. Follows the existing glassmorphism design system.
State: Zustand store (scriptGeneratorStore) chosen over a local hook because script generation will soon be embeddable in session execution (Script Output Node). A store allows any component anywhere in the tree to read/write generation state without prop drilling. Survives navigation.
Preview: Client-side lightweight substitution for live preview as the user types ({{variable_name}} replacement only). The real POST /scripts/generate endpoint is called on Generate — it applies filters (as_secure_string, as_array, as_bool), conditionals, and PowerShell-safe sanitization.
Zustand Store — scriptGeneratorStore
File: frontend/src/store/scriptGeneratorStore.ts
State shape
interface ScriptGeneratorState {
// Template browsing
categories: ScriptCategoryResponse[];
templates: ScriptTemplateListItem[];
selectedTemplate: ScriptTemplateDetail | null;
searchQuery: string;
activeCategoryId: string | null;
isLoadingTemplates: boolean;
// Form
paramValues: Record<string, string>; // keyed by variable_name
formErrors: Record<string, string>; // keyed by variable_name
// Output
generatedScript: string | null;
generationId: string | null;
isGenerating: boolean;
generateError: string | null;
// Actions
loadCategories: () => Promise<void>;
loadTemplates: () => Promise<void>;
selectTemplate: (id: string) => Promise<void>;
setCategory: (id: string | null) => void;
setSearch: (query: string) => void;
setParamValue: (variableName: string, value: string) => void;
generate: (sessionId?: string) => Promise<void>;
clearOutput: () => void;
reset: () => void;
}
Behaviour notes
setCategoryandsetSearchboth callloadTemplates()after updating state — they compose (category + search both applied to the API call)selectTemplatefetches the fullScriptTemplateDetail(includingparameters_schemaandscript_template), then callsreset()to clear previous form/output statereset()clearsparamValues,formErrors,generatedScript,generationId,generateError— does not clear template selection or browsing stategenerate()validates required params client-side first (populatesformErrors), then callsPOST /scripts/generate
Types
Added to frontend/src/types/index.ts:
export interface ScriptCategoryResponse {
id: string;
name: string;
slug: string;
description: string | null;
template_count: number;
}
export interface ScriptTemplateListItem {
id: string;
category_id: string;
name: string;
description: string | null;
script_complexity: 'simple' | 'moderate' | 'complex';
usage_count: number;
tags: string[];
}
export interface ScriptParameter {
variable_name: string;
label: string;
field_type: 'text' | 'password' | 'select' | 'multiselect' | 'checkbox' | 'number';
required: boolean;
default_value?: string;
options?: string[];
is_sensitive: boolean;
display_order: number;
help_text?: string;
}
export interface ScriptParametersSchema {
parameters: ScriptParameter[];
}
export interface ScriptTemplateDetail extends ScriptTemplateListItem {
parameters_schema: ScriptParametersSchema;
script_template: string;
}
export interface ScriptGenerateRequest {
template_id: string;
parameters: Record<string, string>;
session_id?: string;
}
export interface ScriptGenerateResponse {
generation_id: string;
generated_script: string;
template_name: string;
}
API Client
File: frontend/src/api/scripts.ts
getCategories(): Promise<ScriptCategoryResponse[]>
getTemplates(params?: { category_id?: string; search?: string }): Promise<ScriptTemplateListItem[]>
getTemplateDetail(id: string): Promise<ScriptTemplateDetail>
generate(req: ScriptGenerateRequest): Promise<ScriptGenerateResponse>
getGenerations(): Promise<ScriptGenerationRecord[]>
All methods use the existing apiClient (base URL /api/v1, auth interceptor handles token refresh).
Exported from frontend/src/api/index.ts as scriptsApi.
Component Tree
ScriptLibraryPage pages/ScriptLibraryPage.tsx
├── ScriptFilterBar components/scripts/ScriptFilterBar.tsx
├── ScriptTemplateList components/scripts/ScriptTemplateList.tsx
│ └── TemplateCard components/scripts/TemplateCard.tsx
└── ScriptGeneratorPanel components/scripts/ScriptGeneratorPanel.tsx
├── ScriptParameterForm components/scripts/ScriptParameterForm.tsx
│ └── ScriptParameterField components/scripts/ScriptParameterField.tsx
└── ScriptPreview components/scripts/ScriptPreview.tsx
└── PowerShellHighlighter components/scripts/PowerShellHighlighter.tsx
Component responsibilities
ScriptLibraryPage
- Bootstraps store on mount: calls
loadCategories()thenloadTemplates() - Renders two-column layout (template list left, generator panel right)
- Renders
ScriptFilterBarabove the two columns - No business logic — pure layout + bootstrap
ScriptFilterBar
- Category tabs (pill style,
bg-primary/10+ cyan accent on active) - Search input (debounced 300ms) — searches names, descriptions, and tags (backend handles it)
- Reads
categories,activeCategoryId,searchQueryfrom store - Calls
setCategoryandsetSearch
ScriptTemplateList
- Scrollable list of
TemplateCardcomponents - Reads
templates,isLoadingTemplates,selectedTemplatefrom store - Shows skeleton loaders while loading
- Shows empty state when no templates match
TemplateCard
- Displays: name, description (truncated to 2 lines), complexity badge, usage count, tags
- Active state:
bg-primary/10background + 3px left cyan accent bar (matches existing nav active pattern) - Calls
selectTemplate(template.id)on click - Complexity badge colors: simple → emerald-400, moderate → amber-400, complex → rose-500
ScriptGeneratorPanel
- Shows placeholder ("Select a template to get started") when
selectedTemplateis null - When template selected: renders template name/description header,
ScriptParameterForm,ScriptPreview, and action bar - Action bar: Generate button (
bg-gradient-brand) + Download.ps1button - Download triggers
new Blob([generatedScript], { type: 'text/plain' })→ anchor click - Shows
generateErrorinline below Generate button - Viewers see Generate button disabled with tooltip "Engineer access required" (checked via
usePermissions())
ScriptParameterForm
- Iterates
selectedTemplate.parameters_schema.parameterssorted bydisplay_order - Renders a
ScriptParameterFieldper parameter - Client-side required validation on Generate (marks
formErrorsin store) - Disabled entirely for viewers
ScriptParameterField
- Renders input by
field_type: text →<Input>, password →<Input type="password">, select →<select>, checkbox →<input type="checkbox">, number →<Input type="number"> - Shows
help_textas a small muted line below the field - Shows
formErrors[variable_name]as an inline error - Password fields show a show/hide toggle
- Calls
setParamValue(variable_name, value)on change
ScriptPreview
- Two modes:
- Draft mode (before Generate): client-side substitution of
{{variable_name}}inselectedTemplate.script_templateusing currentparamValues. Unfilled params render as{{variable_name}}placeholders (visually dimmed). - Generated mode (after Generate): shows
generatedScriptfrom store
- Draft mode (before Generate): client-side substitution of
- Copy icon (Lucide
Copy) in top-right corner of code block — callsnavigator.clipboard.writeText(); shows a brief "Copied!" tooltip on success - Passes script string to
PowerShellHighlighter
PowerShellHighlighter
- Pure component:
({ script: string }) => JSX.Element - Regex-based syntax highlighting (no external library):
- Comments (
#...) →text-[#8b949e] - Cmdlets (
Verb-Nounpattern) →text-[#22d3ee](cyan) - String literals (
"...",'...') →text-[#a5d6ff] - Variables (
$VarName) →text-[#79c0ff] - Parameters (
-ParamName) →text-[#d2a8ff](purple) - Keywords (
if,foreach,function, etc.) →text-[#ff7b72] - Unfilled placeholders (
{{variable_name}}) →text-amber-400with dashed underline
- Comments (
- Renders as
<pre><code>withfont-label(JetBrains Mono),bg-card, rounded corners
Routing & Navigation
- Route:
/scriptsadded tofrontend/src/router.tsxinside theProtectedRoute/AppLayoutchildren - Sidebar nav entry: "Scripts" with a
Terminalicon (Lucide), grouped under the main nav - No sub-routes needed for Phase 2
Permissions
| Action | Minimum role |
|---|---|
| View Script Library page | Any authenticated user |
| Browse templates, see preview | Any authenticated user |
| Fill form, generate, copy, download | Engineer or above |
Implemented via usePermissions() hook. Viewer-blocking applied to: ScriptParameterForm (disabled), Generate button (disabled + tooltip), Download button (disabled + tooltip).
Search Behaviour
- Search query sent as
?search=param toGET /scripts/templates - Backend searches name, description, and tags (already implemented in Phase 1)
- Debounced 300ms in
ScriptFilterBarbefore callingsetSearch - Category filter and search compose: both params sent simultaneously
Empty & Loading States
| Scenario | Treatment |
|---|---|
| Templates loading | Skeleton cards (3 placeholder TemplateCards) |
| No templates in category | Illustration + "No templates found" message |
| No search results | "No templates match your search" with clear button |
| No template selected | Right panel: centered placeholder with Terminal icon + "Select a template to get started" |
| Generating | Generate button shows spinner, disabled |
| Generate error | Inline error text below Generate button in rose-500 |
Out of Scope (Phase 2)
- Session-embedded script generation (Script Output Node) — Phase 3
- Template creation/editing UI — admin-only, deferred
- Generation history page — deferred
- Admin template management — deferred
- Script execution / RMM integration — long-term roadmap