Complete Script Generator feature including: Backend: - ScriptCategory, ScriptTemplate, ScriptGeneration models - ScriptTemplateEngine with substitution, filters, sanitization - CRUD + share API endpoints with permission checks - Integration tests for permissions and sharing - Migration 057 with AD User Management seed templates Frontend — Script Library: - Browse templates with category tabs and search - Configure pane with parameter form and script generation - Script preview with live substitution and copy/download - scriptGeneratorStore Zustand store Frontend — Template Editor: - Full CRUD form with metadata, script body (Monaco Editor), parameters - ParameterSchemaBuilder with visual builder + JSON toggle - ScriptManagePage with routing and nav link Frontend — Parameter Detector: - Client-side PowerShell parameter detection engine - Detects script-level param() blocks and variable assignments - Type inference from PS type annotations and value patterns - ParameterDetectorStepper one-by-one review UI with accept/skip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
240 lines
15 KiB
Markdown
240 lines
15 KiB
Markdown
# Script Library — Left Pane Takeover Design Spec
|
||
|
||
> **Created:** 2026-03-13
|
||
> **Feature:** Redesign Script Library left pane to have two distinct modes: Browse and Configure
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
The Script Library left pane gains two distinct modes. In **Browse mode** the user sees the full template list with filter bar and a "Configure →" button on each card. Clicking "Configure →" transitions the entire left pane (including the filter bar) to **Configure mode**, which replaces the list with a full-height view of the selected template's header, parameter form, and action buttons (Generate, Download, Copy). The right pane becomes a **read-only preview** (`ScriptPreview` only — no form or buttons). "Back to library" returns to Browse mode with filter/search state preserved.
|
||
|
||
---
|
||
|
||
## Structural Change Summary
|
||
|
||
This is a cross-column relocation of the Generate/Download/Copy controls:
|
||
|
||
| Before | After |
|
||
|--------|-------|
|
||
| Left pane: template list + filter bar | Left pane: Browse mode (list + filter) OR Configure mode (form + actions) |
|
||
| Right pane: param form + action buttons + preview | Right pane: `ScriptPreview` only (read-only display) |
|
||
|
||
`ScriptGeneratorPanel` (which currently owns the right column's form, actions, and preview) is **deleted**. The right pane becomes `ScriptPreview` in isolation. The new `ScriptConfigurePane` component owns the left pane in Configure mode and contains the form + all action buttons.
|
||
|
||
---
|
||
|
||
## Goals
|
||
|
||
- Make template selection intentional (no accidental click-to-configure)
|
||
- Give the parameter form more vertical space by using the full left pane height
|
||
- Keep the output preview always visible on the right
|
||
- Preserve filter/search state across Browse ↔ Configure transitions
|
||
|
||
---
|
||
|
||
## Non-Goals
|
||
|
||
- No changes to `ScriptPreview` internals — it moves to the right pane as-is, including its existing copy overlay button
|
||
- No changes to the Zustand store shape or actions
|
||
- No changes to the filter/search debounce logic
|
||
- No changes to routing
|
||
|
||
---
|
||
|
||
## New Work (not pre-existing)
|
||
|
||
The **Copy button in the action bar** is new — it does not exist in the current `ScriptGeneratorPanel`. It copies `generatedScript` from the store. It is **disabled** when `generatedScript === null` (i.e., before the user has clicked Generate). The draft preview's copy needs are handled by `ScriptPreview`'s existing overlay copy button. No draft-substitution logic needs to be duplicated in `ScriptConfigurePane`.
|
||
|
||
---
|
||
|
||
## Left Pane — Two Modes
|
||
|
||
### Browse Mode
|
||
|
||
Rendered when `paneMode === 'browse'`.
|
||
|
||
The page header (`h1` "Script Library" + subtitle) stays at the top of `ScriptLibraryPage` above the two-column grid, visible in both pane modes — it is not affected by this change.
|
||
|
||
The current `ScriptLibraryPage` renders `ScriptFilterBar` at the page level above the two-column grid. In this redesign, the filter bar moves **inside the left pane column** so it can be hidden in Configure mode. `inputValue`/`setInputValue` remain owned by `ScriptLibraryPage` (not inside the left pane sub-tree) so the search text survives the unmount when the pane switches to Configure mode.
|
||
|
||
Layout (top to bottom, fills left pane height):
|
||
|
||
1. **Filter bar** (`ScriptFilterBar`) — category pills + search input
|
||
2. **Template list** (`ScriptTemplateList`) — scrollable, fills remaining height
|
||
|
||
**TemplateCard changes:**
|
||
- Root element changes from `<button>` to `<div>` — the card itself is no longer clickable
|
||
- Remove active/selected visual state (`bg-primary/10 border-l-[3px]` etc.) — no card is "selected" in browse mode
|
||
- Add **"Configure →"** button, right-aligned in the bottom row
|
||
- Style: `bg-primary/10 border border-primary/20 text-primary text-xs px-2.5 py-1 rounded-md hover:bg-primary/20 transition-colors`
|
||
- Uses unicode `→` (not a Lucide icon)
|
||
- On click: calls `onConfigure(template.id)` prop
|
||
- The rest of the card (name, description, tags, complexity badge) is non-interactive
|
||
|
||
**Updated `TemplateCard` props:**
|
||
```tsx
|
||
interface Props {
|
||
template: ScriptTemplateListItem
|
||
onConfigure: (id: string) => void
|
||
}
|
||
```
|
||
|
||
`TemplateCard` also removes its `useScriptGeneratorStore` import entirely — it no longer reads `selectedTemplate` or `selectTemplate` from the store.
|
||
|
||
### Configure Mode
|
||
|
||
Rendered when `paneMode === 'configure'`.
|
||
|
||
The entire left pane (including filter bar) is replaced by the Configure view.
|
||
|
||
Layout (top to bottom, full pane height, `overflow-y-auto`):
|
||
|
||
1. **Back button**
|
||
- Label: `← Back to library`
|
||
- Style: `flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4`
|
||
- On click: calls `onBack` prop → `ScriptLibraryPage` calls `store.clearOutput()` then `setPaneMode('browse')`
|
||
- Does NOT clear `selectedTemplate` in the store (no such action exists — `selectTemplate` only accepts a string ID). `selectedTemplate` remains set in the store after returning to Browse mode. The right pane (`ScriptPreview`) continues showing the last preview while browsing — this is intentional.
|
||
|
||
2. **Template header** (visible when `isLoadingDetail === false`)
|
||
- Name: `text-base font-semibold font-heading text-foreground`
|
||
- Description (if present): `text-sm text-muted-foreground mt-0.5`
|
||
- Tag row (left-to-right): `ShieldAlert` icon if `requires_elevation`, complexity badge, category name (resolved from `store.categories.find(c => c.id === selectedTemplate.category_id)?.name`), template tags (first 3, overflow as `+N`) — same badge/tag styles as current `TemplateCard`. If no matching category is found, omit the category name chip.
|
||
- Separator: `border-t border-border mt-3 pt-3`
|
||
|
||
3. **`ScriptParameterForm`** — existing component, `canGenerate` prop unchanged
|
||
|
||
4. **Warnings callout** — shown above Generate when `generationWarnings.length > 0` (same amber box as current `ScriptGeneratorPanel`)
|
||
|
||
5. **Action bar**
|
||
- **Generate** button: full-width, `bg-gradient-brand`, disabled/loading behavior same as current
|
||
- **Download .ps1** + **Copy** buttons: side by side below Generate, each `flex-1`
|
||
- The Copy button copies `store.generatedScript`. It is disabled when `generatedScript === null`. Draft copy is handled by `ScriptPreview`'s existing overlay — two copy entry-points is acceptable.
|
||
- Error text below if `generateError` is set
|
||
|
||
**Loading state:** When `isLoadingDetail === true`, shows a centered `<Loader2 size={28} className="text-primary animate-spin" />` filling the pane instead of the template content.
|
||
|
||
---
|
||
|
||
## Right Pane
|
||
|
||
After the redesign, the right pane contains **only `ScriptPreview`**, wrapped in a `glass-card-static h-full` container.
|
||
|
||
Two sub-states:
|
||
- **No template ever selected** (`selectedTemplate === null`): show empty state — Terminal icon + "Select a template to get started" text. **Important:** `ScriptPreview` returns `null` when `selectedTemplate` is null, so the empty state must be rendered by the right-pane wrapper in `ScriptLibraryPage`, not by `ScriptPreview` itself. Pattern:
|
||
```tsx
|
||
{selectedTemplate === null ? (
|
||
<div className="glass-card-static h-full flex flex-col items-center justify-center gap-3 text-center p-8">
|
||
<Terminal size={40} className="text-muted-foreground/40" />
|
||
<p className="text-sm text-muted-foreground">Select a template to get started</p>
|
||
</div>
|
||
) : (
|
||
<div className="glass-card-static h-full overflow-hidden p-4">
|
||
<ScriptPreview />
|
||
</div>
|
||
)}
|
||
```
|
||
- **Template selected** (in either pane mode): show `ScriptPreview`
|
||
|
||
The right pane is always read-only — no form, no Generate/Download buttons.
|
||
|
||
**Layout note:** `ScriptPreview` renders a `<div className="relative">` at its root; the copy overlay button is `position: absolute, top-3, right-3` inside it. The right-pane wrapper must **not** use `overflow-y-auto` — if it did, the absolute copy button would scroll out of view on long scripts. Instead, the wrapper is `overflow-hidden` and `ScriptPreview`'s inner `<pre>` (inside `PowerShellHighlighter`) provides its own `overflow-x-auto` for wide scripts. The right pane itself does not need to scroll vertically — `PowerShellHighlighter` already handles horizontal overflow. No changes to `ScriptPreview` are needed.
|
||
|
||
The correct right-pane wrapper pattern:
|
||
```tsx
|
||
<div className="glass-card-static h-full overflow-hidden p-4">
|
||
<ScriptPreview />
|
||
</div>
|
||
```
|
||
|
||
**Right pane during initial detail load:** When the user clicks "Configure →" for the first time (`selectedTemplate === null`, `isLoadingDetail === true`), the right pane still shows the empty state (Terminal icon). The left pane's configure view shows the loading spinner. This is acceptable — the right pane updating when `isLoadingDetail` resolves is sufficient. No right-pane loading state is needed.
|
||
|
||
---
|
||
|
||
## State — Pane Mode
|
||
|
||
Pane mode is **local React state** in `ScriptLibraryPage`, not the Zustand store.
|
||
|
||
```tsx
|
||
const [paneMode, setPaneMode] = useState<'browse' | 'configure'>('browse')
|
||
```
|
||
|
||
Transitions:
|
||
- `'browse' → 'configure'`: `onConfigure(id)` — calls `store.selectTemplate(id)` then `setPaneMode('configure')`
|
||
- `'configure' → 'browse'`: `onBack()` — calls `store.clearOutput()` then `setPaneMode('browse')`
|
||
|
||
**`isLoadingDetail` drives configure pane content, not pane mode.** When the user clicks "Configure →":
|
||
1. `selectTemplate(id)` is called (sets `isLoadingDetail: true` in store)
|
||
2. `setPaneMode('configure')` is called immediately — user sees the loading spinner in configure mode
|
||
|
||
**`selectTemplate` failure case:** If `selectTemplate` throws (network error), the store resets `isLoadingDetail: false` but leaves `selectedTemplate` unchanged. The pane mode remains `'configure'`.
|
||
|
||
- **First-selection failure** (`selectedTemplate === null` before the call): `ScriptConfigurePane` must handle `!isLoadingDetail && !selectedTemplate` — render an error state: "Failed to load template." with a "← Back to library" link.
|
||
- **Subsequent-selection failure** (`selectedTemplate` is still set from the previous template): the configure pane silently shows the previous template's form with stale data. This is an accepted edge case — network errors mid-session are rare and the user can press Back to recover. No special handling required.
|
||
|
||
**`paramValues` and `formErrors` on Back:** `clearOutput()` does not reset `paramValues` or `formErrors`. This is intentional — if the user returns to browse mode and then clicks "Configure →" on the same template again, `selectTemplate` will repopulate `paramValues` from defaults, discarding any edits. If they configure a different template, `selectTemplate` again repopulates from that template's defaults. There is no scenario where stale param values from a prior template persist into a new template's form. No additional cleanup is needed on Back.
|
||
|
||
**Filter/search preservation:** `inputValue` remains owned by `ScriptLibraryPage` at the page level. The filter bar unmounts in configure mode and remounts in browse mode with the same `inputValue`. Store's `activeCategoryId` and `searchQuery` are never cleared by pane transitions.
|
||
|
||
---
|
||
|
||
## Component Changes
|
||
|
||
| File | Change |
|
||
|------|--------|
|
||
| `frontend/src/pages/ScriptLibraryPage.tsx` | Add `paneMode` state; add `usePermissions` import for `canGenerate`; move `ScriptFilterBar` into left pane column; add `onConfigure`/`onBack` callbacks; render `ScriptConfigurePane` or browse content in left pane conditionally; render right pane as `ScriptPreview`-only with empty state |
|
||
| `frontend/src/components/scripts/TemplateCard.tsx` | Root `<button>` → `<div>`; remove `onClick`/active-state; add `onConfigure: (id: string) => void` prop; add "Configure →" button |
|
||
| `frontend/src/components/scripts/ScriptTemplateList.tsx` | Accept `onConfigure: (id: string) => void` prop; pass to each `TemplateCard`. `inputValue: string` and `onClearSearch: () => void` props are unchanged. |
|
||
| `frontend/src/components/scripts/ScriptConfigurePane.tsx` | **New** — configure mode layout (back button, template header, `ScriptParameterForm`, warnings, action bar with Generate + Download + Copy) |
|
||
| `frontend/src/components/scripts/ScriptGeneratorPanel.tsx` | **Delete** — superseded by `ScriptConfigurePane` and right-pane simplification |
|
||
|
||
No store changes. No API changes. No routing changes.
|
||
|
||
---
|
||
|
||
## Visual Spec
|
||
|
||
### Page layout (Configure mode active)
|
||
|
||
```
|
||
┌─ left pane (320px) ────────────┬─ right pane (1fr) ──────────────────┐
|
||
│ ← Back to library │ │
|
||
│ │ [ScriptPreview — always visible] │
|
||
│ Restart Windows Service │ │
|
||
│ Stops and restarts a service │ # Restart Windows Service │
|
||
│ 🛡 [Beginner] [Services] [win] │ param( │
|
||
│ ───────────────────────────── │ $ServiceName = "{{service_name}}" │
|
||
│ Service Name * │ ) [copy overlay] │
|
||
│ [________________] │ │
|
||
│ Target Computer │ │
|
||
│ [localhost______] │ │
|
||
│ Verify after restart [✓] │ │
|
||
│ │ │
|
||
│ [ Generate Script ] │ │
|
||
│ [ Download .ps1 ] [ Copy ] │ │
|
||
└────────────────────────────────┴──────────────────────────────────────┘
|
||
```
|
||
|
||
### TemplateCard — Browse mode
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ Restart Windows Service 🛡 [Beginner] │
|
||
│ Stops and restarts a named service │
|
||
│ 4× used [services] [windows] +1 [Configure →] │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### `ScriptConfigurePane` props
|
||
|
||
```tsx
|
||
interface Props {
|
||
canGenerate: boolean
|
||
onBack: () => void
|
||
}
|
||
```
|
||
|
||
All other data read from Zustand store directly. `ScriptConfigurePane` derives `canGenerate` from props only — it does NOT call `usePermissions` internally. `ScriptLibraryPage` is the sole caller of `usePermissions` for this feature.
|
||
|
||
**Download filename:** `${selectedTemplate.slug}.ps1` — consistent with the current `ScriptGeneratorPanel` behavior.
|