docs: add Script Generator Phase 2 frontend design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
- `setCategory` and `setSearch` both call `loadTemplates()` after updating state — they compose (category + search both applied to the API call)
|
||||
- `selectTemplate` fetches the full `ScriptTemplateDetail` (including `parameters_schema` and `script_template`), then calls `reset()` to clear previous form/output state
|
||||
- `reset()` clears `paramValues`, `formErrors`, `generatedScript`, `generationId`, `generateError` — does not clear template selection or browsing state
|
||||
- `generate()` validates required params client-side first (populates `formErrors`), then calls `POST /scripts/generate`
|
||||
|
||||
---
|
||||
|
||||
## Types
|
||||
|
||||
Added to `frontend/src/types/index.ts`:
|
||||
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
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()` then `loadTemplates()`
|
||||
- Renders two-column layout (template list left, generator panel right)
|
||||
- Renders `ScriptFilterBar` above 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`, `searchQuery` from store
|
||||
- Calls `setCategory` and `setSearch`
|
||||
|
||||
**`ScriptTemplateList`**
|
||||
- Scrollable list of `TemplateCard` components
|
||||
- Reads `templates`, `isLoadingTemplates`, `selectedTemplate` from 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/10` background + 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 `selectedTemplate` is null
|
||||
- When template selected: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, and action bar
|
||||
- Action bar: Generate button (`bg-gradient-brand`) + Download `.ps1` button
|
||||
- Download triggers `new Blob([generatedScript], { type: 'text/plain' })` → anchor click
|
||||
- Shows `generateError` inline below Generate button
|
||||
- Viewers see Generate button disabled with tooltip "Engineer access required" (checked via `usePermissions()`)
|
||||
|
||||
**`ScriptParameterForm`**
|
||||
- Iterates `selectedTemplate.parameters_schema.parameters` sorted by `display_order`
|
||||
- Renders a `ScriptParameterField` per parameter
|
||||
- Client-side required validation on Generate (marks `formErrors` in 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_text` as 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}}` in `selectedTemplate.script_template` using current `paramValues`. Unfilled params render as `{{variable_name}}` placeholders (visually dimmed).
|
||||
- **Generated mode** (after Generate): shows `generatedScript` from store
|
||||
- Copy icon (Lucide `Copy`) in top-right corner of code block — calls `navigator.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-Noun` pattern) → `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-400` with dashed underline
|
||||
- Renders as `<pre><code>` with `font-label` (JetBrains Mono), `bg-card`, rounded corners
|
||||
|
||||
---
|
||||
|
||||
## Routing & Navigation
|
||||
|
||||
- Route: `/scripts` added to `frontend/src/router.tsx` inside the `ProtectedRoute`/`AppLayout` children
|
||||
- Sidebar nav entry: "Scripts" with a `Terminal` icon (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 to `GET /scripts/templates`
|
||||
- Backend searches name, description, and tags (already implemented in Phase 1)
|
||||
- Debounced 300ms in `ScriptFilterBar` before calling `setSearch`
|
||||
- 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
|
||||
Reference in New Issue
Block a user