282 lines
11 KiB
Markdown
282 lines
11 KiB
Markdown
# 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
|