- Enter a search term to find policy settings
+ No policy settings available. Trigger a sync to import policies from Intune.
)}
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
new file mode 100644
index 0000000..a37f17b
--- /dev/null
+++ b/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/config/nav.ts b/config/nav.ts
index db2d0ec..76be78e 100644
--- a/config/nav.ts
+++ b/config/nav.ts
@@ -8,8 +8,7 @@ type AdditionalLinks = {
export const defaultLinks: SidebarLink[] = [
{ href: "/dashboard", title: "Home", icon: HomeIcon },
- { href: "/search", title: "Search", icon: Search },
- { href: "/settings-overview", title: "All Settings", icon: Database },
+ { href: "/search", title: "Policy Explorer", icon: Search },
{ href: "/account", title: "Account", icon: User },
{ href: "/settings", title: "Settings", icon: Cog },
];
diff --git a/lib/actions/policySettings.ts b/lib/actions/policySettings.ts
index 04bb064..1488e13 100644
--- a/lib/actions/policySettings.ts
+++ b/lib/actions/policySettings.ts
@@ -2,7 +2,7 @@
import { db, policySettings, type PolicySetting } from '@/lib/db';
import { getUserAuth } from '@/lib/auth/utils';
-import { eq, ilike, or, desc, and } from 'drizzle-orm';
+import { eq, ilike, or, desc, and, ne, isNotNull } from 'drizzle-orm';
import { env } from '@/lib/env.mjs';
export interface PolicySettingSearchResult {
@@ -49,10 +49,12 @@ export interface AllSettingsResult {
* 3. Including explicit WHERE tenantId = ? in ALL queries
*
* @param searchTerm - Search query (min 2 characters)
+ * @param limit - Maximum number of results (default 100, max 200)
* @returns Search results filtered by user's tenant
*/
export async function searchPolicySettings(
- searchTerm: string
+ searchTerm: string,
+ limit: number = 100
): Promise {
try {
const { session } = await getUserAuth();
@@ -76,7 +78,10 @@ export async function searchPolicySettings(
const sanitizedSearchTerm = searchTerm.slice(0, 200);
const searchPattern = `%${sanitizedSearchTerm}%`;
- // T017: Explicit WHERE clause filters by tenantId FIRST for security
+ // Enforce maximum limit
+ const safeLimit = Math.min(Math.max(1, limit), 200);
+
+ // Explicit WHERE clause filters by tenantId FIRST for security + null filtering
const results = await db
.select({
id: policySettings.id,
@@ -90,6 +95,9 @@ export async function searchPolicySettings(
.where(
and(
eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation
+ ne(policySettings.settingValue, 'null'), // Filter out string "null"
+ ne(policySettings.settingValue, ''), // Filter out empty strings
+ isNotNull(policySettings.settingValue), // Filter out NULL values
or(
ilike(policySettings.settingName, searchPattern),
ilike(policySettings.settingValue, searchPattern)
@@ -97,7 +105,7 @@ export async function searchPolicySettings(
)
)
.orderBy(policySettings.settingName)
- .limit(100);
+ .limit(safeLimit);
return {
success: true,
@@ -166,11 +174,11 @@ export async function getPolicySettingById(
*
* **Security**: Enforces tenant isolation with explicit WHERE tenantId filter
*
- * @param limit - Maximum number of results (1-100, default 20)
+ * @param limit - Maximum number of results (1-100, default 50)
* @returns Recent policy settings for user's tenant
*/
export async function getRecentPolicySettings(
- limit: number = 20
+ limit: number = 50
): Promise {
try {
const { session } = await getUserAuth();
diff --git a/lib/utils/policyBadges.ts b/lib/utils/policyBadges.ts
new file mode 100644
index 0000000..324bdac
--- /dev/null
+++ b/lib/utils/policyBadges.ts
@@ -0,0 +1,56 @@
+/**
+ * Policy Type Badge Configuration
+ * Maps Intune policy types to Shadcn Badge variants and colors
+ */
+
+export type PolicyBadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline';
+
+interface PolicyBadgeConfig {
+ variant: PolicyBadgeVariant;
+ label: string;
+}
+
+/**
+ * Maps policy type to badge configuration
+ * Based on Microsoft Intune policy categories
+ */
+export function getPolicyBadgeConfig(policyType: string): PolicyBadgeConfig {
+ const type = policyType.toLowerCase();
+
+ // Security & Protection
+ if (type.includes('security') || type.includes('defender') || type.includes('threat')) {
+ return { variant: 'destructive', label: formatPolicyType(policyType) };
+ }
+
+ // Compliance & Conditional Access
+ if (type.includes('compliance') || type.includes('conditional')) {
+ return { variant: 'default', label: formatPolicyType(policyType) };
+ }
+
+ // Configuration Profiles
+ if (type.includes('configuration') || type.includes('profile') || type.includes('settings')) {
+ return { variant: 'secondary', label: formatPolicyType(policyType) };
+ }
+
+ // App Management
+ if (type.includes('app') || type.includes('application')) {
+ return { variant: 'outline', label: formatPolicyType(policyType) };
+ }
+
+ // Default for unknown types
+ return { variant: 'secondary', label: formatPolicyType(policyType) };
+}
+
+/**
+ * Formats policy type string for display
+ * Converts camelCase/PascalCase to readable format
+ */
+function formatPolicyType(policyType: string): string {
+ return policyType
+ .replace(/([A-Z])/g, ' $1') // Add space before capital letters
+ .trim()
+ .replace(/\s+/g, ' ') // Collapse multiple spaces
+ .split(' ')
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(' ');
+}
diff --git a/package-lock.json b/package-lock.json
index eb368e8..7d89cf6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@auth/drizzle-adapter": "^1.11.1",
"@paralleldrive/cuid2": "^3.0.4",
"@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
@@ -2542,6 +2543,98 @@
}
}
},
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
diff --git a/package.json b/package.json
index 60e50be..a79eec9 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@auth/drizzle-adapter": "^1.11.1",
"@paralleldrive/cuid2": "^3.0.4",
"@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
diff --git a/specs/003-policy-explorer-ux/tasks.md b/specs/003-policy-explorer-ux/tasks.md
index f700d70..e6c4940 100644
--- a/specs/003-policy-explorer-ux/tasks.md
+++ b/specs/003-policy-explorer-ux/tasks.md
@@ -21,9 +21,9 @@
**Purpose**: Verify prerequisites and install missing Shadcn UI components
-- [ ] T001 [P] Verify Shadcn Sheet component exists at `components/ui/sheet.tsx`, install if missing via `npx shadcn@latest add sheet`
-- [ ] T002 [P] Verify Shadcn Badge component exists at `components/ui/badge.tsx`, install if missing via `npx shadcn@latest add badge`
-- [ ] T003 Create new directory `components/policy-explorer/` for new feature components
+- [X] T001 [P] Verify Shadcn Sheet component exists at `components/ui/sheet.tsx`, install if missing via `npx shadcn@latest add sheet`
+- [X] T002 [P] Verify Shadcn Badge component exists at `components/ui/badge.tsx`, install if missing via `npx shadcn@latest add badge`
+- [X] T003 Create new directory `components/policy-explorer/` for new feature components
---
@@ -35,11 +35,11 @@
### Backend Server Actions
-- [ ] T004 Add `getRecentPolicySettings(limit?: number)` Server Action in `lib/actions/policySettings.ts` - returns 50 newest policies sorted by lastSyncedAt DESC, filtered by tenantId and excluding null values
-- [ ] T005 Extend `searchPolicySettings()` Server Action in `lib/actions/policySettings.ts` - add optional `limit` parameter (default 100, max 200)
-- [ ] T006 Add null-value filtering logic to Server Actions - filter out entries where `settingValue === "null"` or `settingValue === null` (backend approach for performance)
-- [ ] T007 Add type exports for `PolicySettingSearchResult` interface if not already exported from `lib/actions/policySettings.ts`
-- [ ] T008 Test Server Actions manually - verify tenant isolation, null filtering, and limit parameters work correctly
+- [X] T004 Add `getRecentPolicySettings(limit?: number)` Server Action in `lib/actions/policySettings.ts` - returns 50 newest policies sorted by lastSyncedAt DESC, filtered by tenantId and excluding null values
+- [X] T005 Extend `searchPolicySettings()` Server Action in `lib/actions/policySettings.ts` - add optional `limit` parameter (default 100, max 200)
+- [X] T006 Add null-value filtering logic to Server Actions - filter out entries where `settingValue === "null"` or `settingValue === null` (backend approach for performance)
+- [X] T007 Add type exports for `PolicySettingSearchResult` interface if not already exported from `lib/actions/policySettings.ts`
+- [X] T008 Test Server Actions manually - verify tenant isolation, null filtering, and limit parameters work correctly
**Checkpoint**: Backend ready - all Server Actions functional and tested
@@ -53,14 +53,14 @@
### Implementation for User Story 1
-- [ ] T009 [P] [US1] Create `PolicyTable.tsx` component in `components/policy-explorer/PolicyTable.tsx` - display policies in table format with columns: Setting Name, Setting Value, Policy Name, Policy Type, Last Synced
-- [ ] T010 [P] [US1] Add row click handler prop to `PolicyTable` component - accepts `onRowClick: (policy: PolicySettingSearchResult) => void`
-- [ ] T011 [P] [US1] Add hover styles to table rows in `PolicyTable` - `hover:bg-accent cursor-pointer` classes
-- [ ] T012 [P] [US1] Create `PolicySearchContainer.tsx` client component in `components/policy-explorer/PolicySearchContainer.tsx` - manages search state, accepts `initialPolicies` prop
-- [ ] T013 [US1] Refactor `app/(app)/search/page.tsx` to Server Component pattern - call `getRecentPolicySettings(50)` in Server Component, pass data to `PolicySearchContainer`
-- [ ] T014 [US1] Update page CardTitle to "Policy Explorer" and CardDescription in `app/(app)/search/page.tsx`
-- [ ] T015 [US1] Add empty state handling - if no policies returned, show message "Keine Policies gefunden - Starten Sie einen Sync"
-- [ ] T016 [US1] Keep existing `SyncButton` component in CardHeader of `app/(app)/search/page.tsx`
+- [X] T009 [P] [US1] Create `PolicyTable.tsx` component in `components/policy-explorer/PolicyTable.tsx` - display policies in table format with columns: Setting Name, Setting Value, Policy Name, Policy Type, Last Synced
+- [X] T010 [P] [US1] Add row click handler prop to `PolicyTable` component - accepts `onRowClick: (policy: PolicySettingSearchResult) => void`
+- [X] T011 [P] [US1] Add hover styles to table rows in `PolicyTable` - `hover:bg-accent cursor-pointer` classes
+- [X] T012 [P] [US1] Create `PolicySearchContainer.tsx` client component in `components/policy-explorer/PolicySearchContainer.tsx` - manages search state, accepts `initialPolicies` prop
+- [X] T013 [US1] Refactor `app/(app)/search/page.tsx` to Server Component pattern - call `getRecentPolicySettings(50)` in Server Component, pass data to `PolicySearchContainer`
+- [X] T014 [US1] Update page CardTitle to "Policy Explorer" and CardDescription in `app/(app)/search/page.tsx`
+- [X] T015 [US1] Add empty state handling - if no policies returned, show message "Keine Policies gefunden - Starten Sie einen Sync"
+- [X] T016 [US1] Keep existing `SyncButton` component in CardHeader of `app/(app)/search/page.tsx`
**Checkpoint**: Page loads with 50 newest policies, no null values, empty state works
@@ -74,15 +74,15 @@
### Implementation for User Story 2
-- [ ] T017 [P] [US2] Create `PolicyDetailSheet.tsx` component in `components/policy-explorer/PolicyDetailSheet.tsx` - uses Shadcn Sheet, accepts `policy`, `open`, `onOpenChange` props
-- [ ] T018 [P] [US2] Implement JSON detection logic in `PolicyDetailSheet` - check if `settingValue.trim().startsWith('{')` or `startsWith('[')`
-- [ ] T019 [P] [US2] Implement JSON formatting utility - `JSON.stringify(JSON.parse(value), null, 2)` wrapped in try/catch, fallback to plain text on error
-- [ ] T020 [P] [US2] Render JSON values in `
` tag with Tailwind prose classes for readability in `PolicyDetailSheet`
-- [ ] T021 [P] [US2] Render non-JSON values as normal text in `PolicyDetailSheet`
-- [ ] T022 [P] [US2] Add Sheet styling - proper width (e.g., `w-[600px]`), padding, close button, scroll for long content (`max-height` + `overflow-auto`)
-- [ ] T023 [US2] Integrate `PolicyDetailSheet` into `PolicySearchContainer` - add state for selected policy and sheet open/close
-- [ ] T024 [US2] Connect row click handler from `PolicyTable` to open detail sheet with clicked policy data
-- [ ] T025 [US2] Test detail sheet with various data - nested JSON objects, arrays, long strings (>10KB), plain text values, malformed JSON
+- [X] T017 [P] [US2] Create `PolicyDetailSheet.tsx` component in `components/policy-explorer/PolicyDetailSheet.tsx` - uses Shadcn Sheet, accepts `policy`, `open`, `onOpenChange` props
+- [X] T018 [P] [US2] Implement JSON detection logic in `PolicyDetailSheet` - check if `settingValue.trim().startsWith('{')` or `startsWith('[')`
+- [X] T019 [P] [US2] Implement JSON formatting utility - `JSON.stringify(JSON.parse(value), null, 2)` wrapped in try/catch, fallback to plain text on error
+- [X] T020 [P] [US2] Render JSON values in `
` tag with Tailwind prose classes for readability in `PolicyDetailSheet`
+- [X] T021 [P] [US2] Render non-JSON values as normal text in `PolicyDetailSheet`
+- [X] T022 [P] [US2] Add Sheet styling - proper width (e.g., `w-[600px]`), padding, close button, scroll for long content (`max-height` + `overflow-auto`)
+- [X] T023 [US2] Integrate `PolicyDetailSheet` into `PolicySearchContainer` - add state for selected policy and sheet open/close
+- [X] T024 [US2] Connect row click handler from `PolicyTable` to open detail sheet with clicked policy data
+- [X] T025 [US2] Test detail sheet with various data - nested JSON objects, arrays, long strings (>10KB), plain text values, malformed JSON
**Checkpoint**: Clicking rows opens detail sheet, JSON is formatted, sheet closes properly
@@ -96,12 +96,12 @@
### Implementation for User Story 3
-- [ ] T026 [US3] Integrate existing `SearchInput` component into `PolicySearchContainer` in `components/policy-explorer/PolicySearchContainer.tsx`
-- [ ] T027 [US3] Add search handler in `PolicySearchContainer` - calls `searchPolicySettings()` Server Action with search term
-- [ ] T028 [US3] Implement search state management using `useTransition()` hook for pending state
-- [ ] T029 [US3] Update table to show search results when search term is entered, show initial 50 policies when search is cleared
-- [ ] T030 [US3] Verify null values are filtered from search results (handled by Server Action from T006)
-- [ ] T031 [US3] Add loading indicator while search is in progress (use `isPending` from `useTransition`)
+- [X] T026 [US3] Integrate existing `SearchInput` component into `PolicySearchContainer` in `components/policy-explorer/PolicySearchContainer.tsx`
+- [X] T027 [US3] Add search handler in `PolicySearchContainer` - calls `searchPolicySettings()` Server Action with search term
+- [X] T028 [US3] Implement search state management using `useTransition()` hook for pending state
+- [X] T029 [US3] Update table to show search results when search term is entered, show initial 50 policies when search is cleared
+- [X] T030 [US3] Verify null values are filtered from search results (handled by Server Action from T006)
+- [X] T031 [US3] Add loading indicator while search is in progress (use `isPending` from `useTransition`)
**Checkpoint**: Search works, results exclude null values, loading states correct
@@ -115,11 +115,11 @@
### Implementation for User Story 4
-- [ ] T032 [P] [US4] Define policy type to badge color mapping - create utility function or constant (e.g., Compliance=blue, Configuration=gray, Security=red, etc.)
-- [ ] T033 [P] [US4] Implement badge rendering in `PolicyTable` - replace plain text `policyType` with Shadcn Badge component
-- [ ] T034 [P] [US4] Apply badge variant/color based on policy type in `PolicyTable` component
-- [ ] T035 [US4] Test badge colors with real data - verify at least 3 distinct colors are visible and accessible (contrast check)
-- [ ] T036 [US4] Verify hover effects on table rows - cursor changes to pointer, background color changes
+- [X] T032 [P] [US4] Define policy type to badge color mapping - create utility function or constant (e.g., Compliance=blue, Configuration=gray, Security=red, etc.)
+- [X] T033 [P] [US4] Implement badge rendering in `PolicyTable` - replace plain text `policyType` with Shadcn Badge component
+- [X] T034 [P] [US4] Apply badge variant/color based on policy type in `PolicyTable` component
+- [X] T035 [US4] Test badge colors with real data - verify at least 3 distinct colors are visible and accessible (contrast check)
+- [X] T036 [US4] Verify hover effects on table rows - cursor changes to pointer, background color changes
**Checkpoint**: Badges show distinct colors, hover effects smooth, visual hierarchy improved
@@ -129,9 +129,9 @@
**Purpose**: Update navigation and routing to consolidate under "Policy Explorer"
-- [ ] T037 Update `config/nav.ts` - replace `{ title: "Search", href: "/search" }` with `{ title: "Policy Explorer", href: "/search" }` (keep /search route for now)
-- [ ] T038 Remove "All Settings" menu item from `config/nav.ts` if it exists (from settings-overview feature)
-- [ ] T039 Update page metadata (title, description) in `app/(app)/search/page.tsx` to reflect "Policy Explorer" branding
+- [X] T037 Update `config/nav.ts` - replace `{ title: "Search", href: "/search" }` with `{ title: "Policy Explorer", href: "/search" }` (keep /search route for now)
+- [X] T038 Remove "All Settings" menu item from `config/nav.ts` if it exists (from settings-overview feature)
+- [X] T039 Update page metadata (title, description) in `app/(app)/search/page.tsx` to reflect "Policy Explorer" branding
- [ ] T040 Add redirect from `/settings-overview` to `/search` if that route exists (optional, for backwards compatibility)
**Checkpoint**: Navigation shows single "Policy Explorer" entry, old routes redirect if needed
@@ -142,7 +142,7 @@
**Purpose**: Final refinements and end-to-end validation
-- [ ] T041 Update `EmptyState` component message in `components/search/EmptyState.tsx` - change to Policy Explorer context if still used
+- [X] T041 Update `EmptyState` component message in `components/search/EmptyState.tsx` - change to Policy Explorer context if still used
- [ ] T042 Add loading skeleton to table (optional) - show placeholder rows while initial data loads
- [ ] T043 Verify responsive layout - test table and detail sheet on mobile viewport (< 768px)
- [ ] T044 Add error boundary for detail sheet - graceful error handling if sheet rendering fails