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
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
R001: How to implement Server Component with initial data load + client search?
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-highlighterorprism-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
/searchentirely, update all internal links - Option C: Keep
/searchas-is for now, create/policy-exploreras new route (migration path) - Research needed: Check for external links to
/searchroute, 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.mddocumenting 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
limitparameter (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
policyTypecolumn - 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
searchPolicySettingsServer Action on user input - Filters null values (if frontend approach chosen in Phase 0)
- Renders SearchInput + PolicyTable + PolicyDetailSheet
Page Design
Page: /policy-explorer (or refactored /search)
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:
- Server Component fetches initial 50 policies (no search term)
- Passes to Client Component as props
- Client Component renders table with initial data
- User searches → Client Component calls Server Action → Updates table
- 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.tsxwith 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.tsxwith row click handlers - T013: Add Badge rendering for policyType column
- T014: Add hover styles and cursor pointer to rows
- T015: Create
PolicySearchContainer.tsxClient 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.tswith 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
- Phase 2.1 (P1 Backend): T001-T005 (Server Actions)
- Phase 2.2 (P1 Components): T006-T011 (Detail Sheet) + T012-T018 (Table)
- Phase 2.3 (P1 Page): T019-T024 (Page refactor)
- Phase 2.4 (P2 Navigation): T025-T029 (Routing)
- Phase 2.5 (P3 Polish): T030-T034 (Visual improvements)
- Phase 2.6 (Testing): T035-T044 (Validation)
Known Constraints
- No database migrations: Feature must work with existing schema (✅ confirmed in spec)
- Server-first architecture: All data fetching via Server Actions (✅ plan follows this)
- Backwards compatibility: May need redirect from
/searchto/policy-explorer(TBD in Phase 1) - Bundle size: Must evaluate syntax highlighter libraries if chosen (TBD in Phase 0)
- 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-uxbranch - 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
developmentbranch - Deploy to staging environment
- User acceptance testing
- Deploy to production
- Monitor performance metrics
- Gather user feedback
Next Steps
- Immediate: Complete Phase 0 research (resolve all NEEDS CLARIFICATION items)
- After Phase 0: Generate
research.mdwith documented decisions - Phase 1: Create
data-model.md,contracts/,quickstart.md - Phase 2: Run
/speckit.tasksto generate detailed task breakdown - Implementation: Execute tasks in priority order (P1 → P2 → P3)
References
- Feature Spec: spec.md
- Requirements Checklist: checklists/requirements.md
- Related Feature: specs/001-global-policy-search/ (base implementation)
- Related Feature: specs/002-manual-policy-sync/ (SyncButton component)
- Constitution: .specify/memory/constitution.md
- Shadcn UI Sheet Docs: https://ui.shadcn.com/docs/components/sheet
- Shadcn UI Badge Docs: https://ui.shadcn.com/docs/components/badge