tenantpilot/specs/003-policy-explorer-ux/plan.md
Ahmed Darrazi 414eb709b9 Add implementation plan for Policy Explorer UX Upgrade
Phase 0 (Research): 6 key decisions to make
- Server/Client composition pattern for initial data + search
- Null filtering strategy (backend vs frontend)
- JSON formatting approach (native vs syntax highlighter)
- Badge color mapping for policy types
- Route strategy (/search redirect vs replacement)
- Sheet component state management

Phase 1 (Design): API contracts and component design
- getRecentPolicySettings() Server Action (new)
- searchPolicySettings() extension with limit param
- PolicyDetailSheet component (Shadcn Sheet)
- PolicyTable component with click handlers
- PolicySearchContainer client wrapper

Phase 2 (Implementation): 44 tasks across 7 epics
- Epic 1: Backend Server Actions (P1, T001-T005)
- Epic 2: Detail Sheet component (P1, T006-T011)
- Epic 3: Table & Search components (P1, T012-T018)
- Epic 4: Page refactor (P1, T019-T024)
- Epic 5: Navigation update (P2, T025-T029)
- Epic 6: Visual improvements (P3, T030-T034)
- Epic 7: Testing & validation (T035-T044)

Constitution compliance:  All checks pass
Performance targets: <2s initial load, <300ms detail sheet, <2s search
No database changes required - extends existing schema
2025-12-07 02:09:53 +01:00

20 KiB

Implementation Plan: Policy Explorer UX Upgrade

Branch: 003-policy-explorer-ux | Date: 2025-12-07 | Spec: spec.md
Status: Planning Phase

Summary

Transform the existing global policy search (/search) into a comprehensive Policy Explorer with improved UX: show 50 newest policies by default (no empty state), filter out null values automatically, add clickable rows with detail sheet for JSON formatting, improve visual hierarchy with policy type badges, and consolidate navigation.

Technical Approach: Extend existing searchPolicySettings Server Action with optional limit parameter and null-value filtering. Create new PolicyDetailSheet component using Shadcn Sheet. Refactor search page to be a Server Component that loads initial data, with client components for interactive features (search, detail sheet). Replace /search route with /policy-explorer and update navigation.

Technical Context

Language/Version: TypeScript 5.x strict mode
Primary Dependencies: Next.js 16.0.3 App Router, Drizzle ORM 0.44.7, Shadcn UI, NextAuth.js 4.24.13, date-fns 4.x
Storage: PostgreSQL (no schema changes required)
Testing: Manual testing + E2E tests with Playwright (if applicable)
Target Platform: Docker containers (standalone build), web browsers
Project Type: Web application (Next.js App Router)
Performance Goals: Initial load < 2s, detail sheet open < 300ms, search response < 2s
Constraints: Server-first architecture (Server Actions + Server Components), Azure AD multi-tenant auth, no client-side fetches
Scale/Scope: Multi-tenant SaaS, 1000+ policy settings per tenant, 100+ concurrent users

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • Uses Next.js App Router with Server Actions (no client-side fetches)
  • TypeScript strict mode enabled (strict: true in tsconfig.json)
  • Drizzle ORM for all database operations (extends existing queries)
  • Shadcn UI for all new components (Sheet, Badge components)
  • Azure AD multi-tenant authentication (uses existing session)
  • Docker deployment with standalone build (no special config needed)

Constitution Compliance: All requirements met - feature extends existing patterns

Project Structure

Documentation (this feature)

specs/003-policy-explorer-ux/
├── spec.md                   # Feature specification (✅ completed)
├── plan.md                   # This file (implementation plan)
├── research.md               # Phase 0 output (research findings)
├── data-model.md             # Phase 1 output (no DB changes, documents filtering logic)
├── quickstart.md             # Phase 1 output (developer setup guide)
├── contracts/                # Phase 1 output (API contracts)
│   └── server-actions.md     # Server Action signatures
├── checklists/
│   └── requirements.md       # Specification quality checklist (✅ completed)
└── tasks.md                  # Phase 2 output (NOT created by this plan)

Source Code (repository root)

# Existing files to modify
lib/
├── actions/
│   └── policySettings.ts              # [MODIFY] Add getRecentPolicySettings(), extend searchPolicySettings()
└── db/
    └── schema/
        └── policySettings.ts          # [NO CHANGE] Uses existing schema

components/
├── search/
│   ├── SearchInput.tsx                # [MODIFY] Add null-value filtering, refactor for new layout
│   ├── ResultsTable.tsx               # [MODIFY] Add row click handler, hover styles, badge rendering
│   ├── EmptyState.tsx                 # [MODIFY] Update message for Policy Explorer context
│   └── SyncButton.tsx                 # [NO CHANGE] Keep existing sync functionality
└── ui/
    ├── sheet.tsx                      # [CHECK] Verify Shadcn Sheet installed, add if missing
    └── badge.tsx                      # [CHECK] Verify Shadcn Badge installed, add if missing

app/
└── (app)/
    ├── search/
    │   └── page.tsx                   # [REFACTOR] Convert to Server Component with initial data load
    └── policy-explorer/
        └── page.tsx                   # [NEW] New route (may redirect from /search or replace it)

config/
└── nav.ts                             # [MODIFY] Replace "Search" with "Policy Explorer", remove "All Settings"

# New files to create
components/
└── policy-explorer/
    ├── PolicyDetailSheet.tsx          # [NEW] Slide-over detail view for policy settings
    ├── PolicyTable.tsx                # [NEW] Refactored table with click handlers and badges
    └── PolicySearchContainer.tsx      # [NEW] Client wrapper for search functionality

Structure Decision: Single Next.js project following existing App Router patterns. New components under components/policy-explorer/ to distinguish from legacy /search components. Route can either replace /search or create new /policy-explorer route (decision in Phase 1).

Phase 0: Research & Unknowns Resolution

Status: 🔄 In Progress

Research Questions

Decision: NEEDS CLARIFICATION

  • Option A: Server Component page fetches initial 50 entries, passes to Client Component wrapper
  • Option B: Separate Server Component for table + Client Component for search (parallel composition)
  • Research needed: Next.js 16 patterns for mixing Server/Client with initial data + user interactions

R002: How to filter null values - backend or frontend?

Decision: NEEDS CLARIFICATION

  • Option A: Backend filter in Drizzle query (ne(policySettings.settingValue, "null"))
  • Option B: Frontend filter after Server Action returns data
  • Option C: Hybrid - backend for initial load, frontend for search results
  • Research needed: Performance implications of filtering 1000+ records, indexing considerations

R003: How to format JSON in detail sheet?

Decision: NEEDS CLARIFICATION

  • Option A: Use JSON.parse() + JSON.stringify(parsed, null, 2) with <pre> tag
  • Option B: Install syntax highlighter like react-syntax-highlighter or prism-react-renderer
  • Option C: Use browser-native <code> with Tailwind prose styles
  • Research needed: Bundle size impact, accessibility of code blocks, whether syntax highlighting is needed for MVP

R004: Badge color mapping for policy types?

Decision: NEEDS CLARIFICATION

  • Option A: Hardcode mapping (Compliance=blue, Configuration=gray, Security=red)
  • Option B: Dynamic hashing (hash policyType string to generate consistent color)
  • Option C: Store colors in database (requires schema change - violates spec)
  • Research needed: Actual policy type values in production data, Shadcn Badge variant options

R005: Should /search route redirect to /policy-explorer or be replaced?

Decision: NEEDS CLARIFICATION

  • Option A: Keep both routes, redirect /search/policy-explorer (backwards compatibility)
  • Option B: Replace /search entirely, update all internal links
  • Option C: Keep /search as-is for now, create /policy-explorer as new route (migration path)
  • Research needed: Check for external links to /search route, impact on bookmarks

R006: How to handle Sheet component state with Server Actions?

Decision: NEEDS CLARIFICATION

  • Option A: Client Component manages open/close state, fetches detail via Server Action on open
  • Option B: Pass full policy data to Sheet component on click (no additional fetch)
  • Option C: Use URL params for selected policy ID (deep linking support)
  • Research needed: Shadcn Sheet best practices, data freshness requirements

Research Deliverables

  • research.md documenting all decisions with rationale
  • Code examples for Server/Client composition pattern
  • Performance benchmarks for null-value filtering approaches
  • Badge color mapping specification

Phase 1: Design & Contracts

Status: Pending Phase 0 completion

Data Model

No database schema changes required. Feature uses existing policySettings table:

// Existing schema (no modifications)
export const policySettings = pgTable('policy_settings', {
  id: uuid('id').primaryKey().defaultRandom(),
  tenantId: uuid('tenant_id').notNull(), // Multi-tenant isolation
  policyName: text('policy_name').notNull(),
  policyType: text('policy_type').notNull(),
  settingName: text('setting_name').notNull(),
  settingValue: text('setting_value').notNull(),
  graphPolicyId: text('graph_policy_id'),
  lastSyncedAt: timestamp('last_synced_at').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

Filtering Logic (to be documented in data-model.md):

  • Null value detection: settingValue === "null" || settingValue === null
  • Applied at: [TBD in Phase 0 - backend vs frontend]
  • Performance impact: [TBD in Phase 0 - requires benchmarking]

API Contracts

Server Action: getRecentPolicySettings(limit?: number)

File: lib/actions/policySettings.ts (new function)

export async function getRecentPolicySettings(
  limit: number = 50
): Promise<AllSettingsResult> {
  // Returns: Recent policy settings, sorted by lastSyncedAt DESC
  // Filters: tenantId (from session), exclude null values
  // Security: Session validation, tenant isolation
}

Contract:

  • Input: limit (optional, default 50, max 100)
  • Output: { success: boolean, data?: PolicySettingSearchResult[], error?: string, totalCount?: number }
  • Security: Validates session, extracts tenantId, enforces tenant isolation
  • Performance: Database query with LIMIT clause, DESC index on lastSyncedAt

Server Action: searchPolicySettings(searchTerm: string, limit?: number) [MODIFIED]

File: lib/actions/policySettings.ts (extend existing function)

Changes:

  • Add optional limit parameter (default 100, max 200)
  • Add null-value filtering (if backend approach chosen in Phase 0)
  • Keep existing security and tenant isolation logic

Server Action: getPolicySettingById(id: string) [EXISTING]

File: lib/actions/policySettings.ts (already exists, line ~164)

Usage: Fetch full policy details for detail sheet (if on-click fetch approach chosen in Phase 0)

Component Contracts

Component: PolicyDetailSheet

File: components/policy-explorer/PolicyDetailSheet.tsx (new)

Props:

interface PolicyDetailSheetProps {
  policy: PolicySettingSearchResult | null;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

Features:

  • Shadcn Sheet component (slide-over from right)
  • JSON detection: settingValue.trim().startsWith('{') || settingValue.trim().startsWith('[')
  • JSON formatting: JSON.stringify(JSON.parse(value), null, 2) with error handling
  • Fallback: Display as plain text if JSON.parse fails

Component: PolicyTable

File: components/policy-explorer/PolicyTable.tsx (new, refactored from ResultsTable.tsx)

Props:

interface PolicyTableProps {
  policies: PolicySettingSearchResult[];
  onRowClick: (policy: PolicySettingSearchResult) => void;
}

Features:

  • Badge rendering for policyType column
  • Row hover effect: hover:bg-accent cursor-pointer
  • Click handler on TableRow
  • Date formatting with date-fns (existing dependency)

Component: PolicySearchContainer

File: components/policy-explorer/PolicySearchContainer.tsx (new)

Props:

interface PolicySearchContainerProps {
  initialPolicies: PolicySettingSearchResult[];
}

Features:

  • Client Component wrapper ('use client')
  • Manages search state and detail sheet state
  • Calls searchPolicySettings Server Action on user input
  • Filters null values (if frontend approach chosen in Phase 0)
  • Renders SearchInput + PolicyTable + PolicyDetailSheet

Page Design

File: app/(app)/policy-explorer/page.tsx (new) or app/(app)/search/page.tsx (refactor)

Composition:

export default async function PolicyExplorerPage() {
  // Server Component - fetches initial data
  const initialData = await getRecentPolicySettings(50);
  
  return (
    <main>
      <Card>
        <CardHeader>
          <CardTitle>Policy Explorer</CardTitle>
          <CardDescription>Browse and search Intune policy settings</CardDescription>
          <SyncButton /> {/* Keep existing sync button */}
        </CardHeader>
        <CardContent>
          <PolicySearchContainer initialPolicies={initialData.data ?? []} />
        </CardContent>
      </Card>
    </main>
  );
}

Flow:

  1. Server Component fetches initial 50 policies (no search term)
  2. Passes to Client Component as props
  3. Client Component renders table with initial data
  4. User searches → Client Component calls Server Action → Updates table
  5. User clicks row → Client Component opens detail sheet

Navigation Update

File: config/nav.ts

Changes:

  • Replace { title: "Search", href: "/search" } with { title: "Policy Explorer", href: "/policy-explorer" }
  • Remove { title: "All Settings", href: "/settings-overview" } (if exists)
  • Keep icon (or change to relevant icon like FileSearch or Database)

Phase 2: Implementation Tasks

Status: Pending Phase 1 completion

Task Breakdown (High-Level)

Epic 1: Backend - Server Actions (P1)

  • T001: Add getRecentPolicySettings() Server Action with null filtering
  • T002: Extend searchPolicySettings() with optional limit parameter
  • T003: Add null-value filtering to search results (if backend approach chosen)
  • T004: Add unit tests for Server Actions
  • T005: Verify tenant isolation in new query

Epic 2: Components - Detail Sheet (P1)

  • T006: Install Shadcn Sheet component (if not present)
  • T007: Install Shadcn Badge component (if not present)
  • T008: Create PolicyDetailSheet.tsx with JSON detection logic
  • T009: Create JSON formatting utility with error handling
  • T010: Style Sheet component (width, padding, close button)
  • T011: Test Sheet with various JSON structures (nested objects, arrays)

Epic 3: Components - Table & Search (P1)

  • T012: Create PolicyTable.tsx with row click handlers
  • T013: Add Badge rendering for policyType column
  • T014: Add hover styles and cursor pointer to rows
  • T015: Create PolicySearchContainer.tsx Client Component wrapper
  • T016: Implement search state management (useTransition)
  • T017: Implement detail sheet state management (open/close)
  • T018: Add null-value filtering (if frontend approach chosen)

Epic 4: Page Refactor (P1)

  • T019: Create or refactor page to Server Component pattern
  • T020: Fetch initial 50 policies in Server Component
  • T021: Pass initial data to Client Component
  • T022: Update CardHeader title to "Policy Explorer"
  • T023: Test page with no policies (empty state)
  • T024: Test page with 50+ policies (initial load performance)

Epic 5: Navigation & Routing (P2)

  • T025: Update config/nav.ts with new route
  • T026: Remove "All Settings" menu item (if exists)
  • T027: Decide on /search redirect vs replacement
  • T028: Implement redirect or route replacement
  • T029: Update internal links to use new route

Epic 6: Visual Improvements (P3)

  • T030: Define policy type to badge color mapping
  • T031: Implement badge color logic in PolicyTable
  • T032: Test badge colors with production data
  • T033: Add loading skeletons for table (optional)
  • T034: Add empty state component for no results

Epic 7: Testing & Validation (All Priorities)

  • T035: Manual test - Initial load shows 50 policies
  • T036: Manual test - Null values filtered correctly
  • T037: Manual test - Row click opens detail sheet
  • T038: Manual test - JSON formatted in detail sheet
  • T039: Manual test - Search filters null values
  • T040: Manual test - Badge colors correct
  • T041: E2E test - Page load and search flow
  • T042: E2E test - Detail sheet open/close
  • T043: Performance test - Initial load < 2s
  • T044: Performance test - Detail sheet < 300ms

Implementation Order

  1. Phase 2.1 (P1 Backend): T001-T005 (Server Actions)
  2. Phase 2.2 (P1 Components): T006-T011 (Detail Sheet) + T012-T018 (Table)
  3. Phase 2.3 (P1 Page): T019-T024 (Page refactor)
  4. Phase 2.4 (P2 Navigation): T025-T029 (Routing)
  5. Phase 2.5 (P3 Polish): T030-T034 (Visual improvements)
  6. Phase 2.6 (Testing): T035-T044 (Validation)

Known Constraints

  1. No database migrations: Feature must work with existing schema ( confirmed in spec)
  2. Server-first architecture: All data fetching via Server Actions ( plan follows this)
  3. Backwards compatibility: May need redirect from /search to /policy-explorer (TBD in Phase 1)
  4. Bundle size: Must evaluate syntax highlighter libraries if chosen (TBD in Phase 0)
  5. Tenant isolation: All queries must include tenantId filter ( existing pattern)

Risk Assessment

Risk Impact Mitigation
Null filtering on 1000+ records slow High Phase 0 benchmarking, backend index if needed
JSON.parse() fails on malformed data Medium Try/catch with fallback to plain text
Sheet component state management complex Medium Research Phase 0, follow Shadcn examples
Breaking change to /search route Low Implement redirect, communicate to users
Badge colors insufficient contrast Low Use Shadcn variants, test accessibility

Success Criteria Mapping

Success Criterion (from spec) Implementation
SC-001: Initial load < 2s Server Component with getRecentPolicySettings(50) + database indexes
SC-002: 100% valid values (no nulls) Backend or frontend filtering (Phase 0 decision)
SC-003: Detail sheet < 300ms Client-side state management, no additional data fetch
SC-004: JSON formatted correctly JSON.parse() + JSON.stringify() with <pre> tag
SC-005: Badge colors distinguishable Shadcn Badge variants, color mapping (Phase 1)
SC-006: Single "Policy Explorer" menu Update config/nav.ts, remove "All Settings"

Deployment Checklist

  • All Phase 0 research documented in research.md
  • All Phase 1 contracts documented in contracts/
  • Code committed to 003-policy-explorer-ux branch
  • Feature flag enabled (if applicable)
  • Navigation updated in config/nav.ts
  • Manual testing checklist completed
  • E2E tests passing (if implemented)
  • Performance benchmarks validated
  • Merge to development branch
  • Deploy to staging environment
  • User acceptance testing
  • Deploy to production
  • Monitor performance metrics
  • Gather user feedback

Next Steps

  1. Immediate: Complete Phase 0 research (resolve all NEEDS CLARIFICATION items)
  2. After Phase 0: Generate research.md with documented decisions
  3. Phase 1: Create data-model.md, contracts/, quickstart.md
  4. Phase 2: Run /speckit.tasks to generate detailed task breakdown
  5. Implementation: Execute tasks in priority order (P1 → P2 → P3)

References