12 KiB
Implementation Plan: Compliance Metric Grouping
Overview
Consolidate the AEO Compliance page's metric health cards from one-per-summary-entry to one-per-metric-family. Add variant pills inside each grouped card, a hover tooltip with metric context (300ms delay), an info panel for full metric definitions, and a static metricDefinitions.json data file. All work is frontend-only — no backend changes needed. The metricFilter state changes from string|null to string[]|null to support filtering by all metric IDs in a family.
Tasks
-
1. Create metric definitions data file and install test dependencies
-
1.1 Create
frontend/src/data/metricDefinitions.json- Create the
frontend/src/data/directory - Build the JSON array from the metric definitions table provided by the user (130+ rows, 14 fields each)
- Each entry must have all 14 keys:
metric_id,metric_title,asset_types,asset_types_in_scope,application_types_in_scope,environment_in_scope,status_in_scope,instance_types_in_scope,criticality_levels_in_scope,exclusions,special_conditions,data_sources_required,business_justification,notes - Use empty strings for optional fields with no value — never
nullor omitted keys - Verify the file imports without error via a quick
JSON.parsecheck - Requirements: 6.1, 6.2, 6.3, 6.5, 8.3, 8.4
- Create the
-
1.2 Install
fast-checkas a dev dependency- Run
npm install --save-dev fast-checkinfrontend/ - Verify it appears in
package.jsondevDependencies - Requirements: (testing infrastructure)
- Run
-
-
2. Implement pure helper functions and their tests
-
2.1 Add
computeWorstStatusandgroupByMetricFamilyhelpers to CompliancePage.js- Add
STATUS_SEVERITYmap:{ 'Below 15% of Target': 0, 'Within 15% of Target': 1, 'Meets/Exceeds Target': 2 } - Implement
computeWorstStatus(statuses)— returns the status with the lowest severity rank from a non-empty array - Implement
groupByMetricFamily(allEntries, team)— filters entries by team, groups bymetric_id, returns array of{ metricId, entries, category, target, worstStatus }objects - Export both functions for testing (named exports alongside the default CompliancePage export)
- Remove the existing
teamMetrics()helper (replaced bygroupByMetricFamily) - Requirements: 1.1, 1.2, 1.6, 3.1
- Add
-
2.2 Write property test: Grouping invariant — no entries lost or misplaced
- Property 1: Grouping invariant — no entries lost or misplaced
- Create test file
frontend/src/components/pages/__tests__/complianceGrouping.property.test.js - Generate random arrays of summary entry objects with varying
metric_idandteamvalues - Verify: (a) every entry appears in exactly one group, (b) all entries within a group share the same
metric_id, (c) total entries across groups equals team-filtered input count - Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 1.1, 1.2
-
2.3 Write property test: Worst-status computation follows severity ordering
- Property 2: Worst-status computation follows severity ordering
- Generate random non-empty arrays of status strings from
{"Below 15% of Target", "Within 15% of Target", "Meets/Exceeds Target"} - Verify the result is the status with the lowest severity rank present in the array
- If the array contains "Below 15% of Target", the result must be "Below 15% of Target"
- Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 1.6, 3.1
-
* 2.4 Write property test: Device filtering with metric family includes all matching devices
- Property 3: Device filtering with metric family includes all matching devices
- Generate random device arrays (each with a
failing_metricsarray of{ metric_id }objects) and random filter ID arrays - Verify the filtered result contains exactly those devices with at least one matching
metric_idin the filter array - Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 1.8, 7.1, 7.2
-
-
3. Checkpoint — Verify helpers and property tests
- Ensure all tests pass, ask the user if questions arise.
-
4. Redesign MetricHealthCard with variant pills and worst-status coloring
-
4.1 Redesign
MetricHealthCardto accept a family group- Change props from
{ entry, active, onClick }to{ family, active, onClick, onInfoClick, definitionLookup } familyis{ metricId, entries, category, target, worstStatus }- Display base
metricIdas card title andcategoryfrom the family - Display shared
targetpercentage - Use
worstStatuscolor for card border, status pill text, and status dot - When all variants meet/exceed target, show "OK" status indicator with success color
- Add
Infoicon (lucide-react) in the top-right corner withstopPropagationon click to callonInfoClick(family.metricId) - Requirements: 1.2, 1.3, 1.6, 1.7, 3.1, 3.2, 3.3, 5.1, 5.5
- Change props from
-
4.2 Implement
VariantPillinline sub-component- Render one pill per
entryinfamily.entries - Each pill shows the entry's
descriptionorteamlabel and compliance percentage in monospace - Background tint from the entry's individual status color at ~12% opacity
- Show a glow dot when the variant's status is not "Meets/Exceeds Target"
- Layout:
inline-flexwithflexWrap: 'wrap'on the parent container - Requirements: 1.4, 1.5, 2.1, 2.2, 2.3, 2.4
- Render one pill per
-
-
5. Update CompliancePage state and rendering to use grouped families
-
5.1 Add new imports and build the definitions lookup map
- Import
{ Info }fromlucide-react - Import
MetricInfoPanelfrom./MetricInfoPanel(created in task 6) - Import
metricDefinitionsRawfrom../../data/metricDefinitions.json - Build
METRIC_DEFINITIONSlookup object at module level keyed bymetric_id - Requirements: 6.3, 6.4
- Import
-
5.2 Update state management and filter logic
- Change
metricFilterfromstring|nulltostring[]|null - Add new state:
infoMetric(string|null) — which metric's info panel is open - Add new state:
hoveredMetric(string|null) — which metric is being hovered - Add new ref:
hoverTimeoutRef— for 300ms delay management - Update
filteredDevicesfilter fromm.metric_id === metricFiltertometricFilter.includes(m.metric_id) - Requirements: 7.1, 7.2
- Change
-
5.3 Update card rendering to use
groupByMetricFamily- Replace
const metrics = teamMetrics(summary.entries, activeTeam)withconst families = groupByMetricFamily(summary.entries, activeTeam) - Replace
metrics.map(entry => <MetricHealthCard entry={entry} ... />)withfamilies.map(family => <MetricHealthCard family={family} ... />) - On card click: set
metricFiltertofamily.entries.map(e => e.metric_id)(array of all IDs in the family), or clear if already active - Active state check: compare
metricFilterarray contents against the family's metric IDs - Pass
onInfoClickhandler that setsinfoMetricstate - Pass
definitionLookupasMETRIC_DEFINITIONS - Requirements: 1.2, 1.8, 7.1, 7.3, 7.4, 7.5
- Replace
-
-
6. Implement MetricInfoPanel component
- 6.1 Create
frontend/src/components/pages/MetricInfoPanel.js- Props:
metricId,definition(from lookup or null),summaryEntries(family entries for fallback),onClose - Render overlay/slide-out panel with dark theme matching DESIGN_SYSTEM.md
- Close button (X icon, top-right)
- Metric title in h3 monospace
- Sections with monospace uppercase labels: Asset Types, Asset Types In Scope, Application Types In Scope, Environment In Scope, Status In Scope, Instance Types In Scope, Criticality Levels In Scope, Exclusions, Special Conditions, Data Sources Required, Business Justification, Notes
- Show "—" placeholder for empty string fields
- If
definitionis null: show "No detailed definition available" with summary description fallback - Click outside or close button calls
onClose() - Requirements: 5.2, 5.3, 5.4, 5.6, 5.7
- Props:
- 6.1 Create
-
7. Implement HoverTooltip inline in CompliancePage
- 7.1 Add hover tooltip logic and rendering
- On
mouseEnteron MetricHealthCard: set 300ms timeout, then sethoveredMetrictofamily.metricId - On
mouseLeave: clear timeout, sethoveredMetricto null - Render tooltip when
hoveredMetricmatches a family — positioned near the card usinggetBoundingClientRect() - Tooltip content: metric title, business justification, data sources required (from
METRIC_DEFINITIONSlookup) - Fall back to summary entry
descriptionwhen no definition exists - Dark card background with subtle border and shadow per DESIGN_SYSTEM.md
- Tooltip must not interfere with card click behavior
- Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6
- On
- 7.1 Add hover tooltip logic and rendering
-
8. Checkpoint — Verify full UI integration
- Ensure all tests pass, ask the user if questions arise.
-
9. Property tests for definitions data and lookup
-
9.1 Write property test: Definition lookup returns correct entry or null
- Property 4: Definition lookup returns correct entry or null
- Create test file
frontend/src/components/pages/__tests__/metricDefinitions.property.test.js - Generate random arrays of metric definition objects with unique
metric_idvalues - Build lookup map, query with IDs from the array (expect hit) and IDs not in the array (expect miss)
- Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 4.2, 4.6
-
9.2 Write property test: Detail panel renders all required definition fields
- Property 5: Detail panel renders all required definition fields
- Generate random metric definition objects with all 14 fields
- Extract the set of field keys that the MetricInfoPanel renders and verify all required keys are present
- Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 5.3
-
9.3 Write property test: Definitions schema validation — all entries have required fields
- Property 6: Definitions schema validation — all entries have required fields
- Generate random arrays of metric definition objects
- Verify every entry has all 14 keys present,
metric_idis a non-empty string, and optional fields (exclusions,special_conditions,notes) are strings (not null/undefined) - Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 6.2, 8.3, 8.4
-
9.4 Write property test: Lookup map construction preserves all definitions
- Property 7: Lookup map construction preserves all definitions
- Generate random definition arrays with unique
metric_idvalues - Build lookup map and verify map size equals array length, and every definition is retrievable by its
metric_id - Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 6.4
-
9.5 Write property test: JSON round-trip preserves metric definition data
- Property 8: JSON round-trip preserves metric definition data
- Generate random metric definition objects with string values for all fields
- Round-trip through
JSON.stringifythenJSON.parseand verify deep equality - Use
fc.assert(property, { numRuns: 100 }) - Validates: Requirements 8.1, 8.2
-
-
10. Final checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
Notes
- Tasks marked with
*are optional and can be skipped for faster MVP - Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document using fast-check
- Unit tests validate specific examples and edge cases
- All styling follows the project convention of inline styles (no CSS modules or Tailwind)
- The
fast-checklibrary must be installed as a dev dependency before running property tests - The
metricDefinitions.jsonfile contains 130+ rows — the user will provide the metric definitions table data for conversion computeWorstStatusandgroupByMetricFamilyare exported as named exports from CompliancePage.js for testability