139 lines
12 KiB
Markdown
139 lines
12 KiB
Markdown
|
|
# Implementation Plan: Compliance Metric Estimated Resolution Date
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
This plan implements a read-only, per-metric estimated resolution date line at the top of each noncompliant metric's section in the asset sidebar (`ComplianceDetailPanel.js`). The work is frontend-only (React 19, plain JavaScript) and is built test-first: the pure date helper and its property-based tests come first, followed by the `MetricRow` rendering change and its render tests, then build and test verification.
|
|||
|
|
|
|||
|
|
All date logic is isolated in a pure helper (`frontend/src/utils/resolutionDate.js`) so it can be property- and example-tested independently of React. The component change adds a single read-only block to `MetricRow` and changes no prop signatures. The editable Resolution Date metadata `Section`, `computeSharedValues`, `handleSaveMetadata`, and the metadata PATCH flow are left unchanged.
|
|||
|
|
|
|||
|
|
## Tasks
|
|||
|
|
|
|||
|
|
- [x] 1. Create the pure resolution-date helper module
|
|||
|
|
- [x] 1.1 Implement `formatResolutionDate` and display constants in `frontend/src/utils/resolutionDate.js`
|
|||
|
|
- Create `frontend/src/utils/resolutionDate.js` following the existing `frontend/src/utils/` pure-module pattern (for example `queueGrouping.js`)
|
|||
|
|
- Export `formatResolutionDate(raw)` returning the discriminated union `{ state: 'set', value } | { state: 'none' } | { state: 'invalid' }`
|
|||
|
|
- Return `{ state: 'none' }` for `null`, `undefined`, empty string, or whitespace-only (after `trim()`)
|
|||
|
|
- Return `{ state: 'set', value }` only when the trimmed value matches `^\d{4}-\d{2}-\d{2}$` AND is a real calendar date (month `1–12`, day within the month's true length, leap-year aware); `value` is the normalized `YYYY-MM-DD` string
|
|||
|
|
- Return `{ state: 'invalid' }` for any other non-empty value (wrong shape, `2026-13-01`, `2026-02-30`, arbitrary text)
|
|||
|
|
- Keep the function pure and deterministic: no React, no I/O, no system clock/timezone/locale dependency, and do NOT use `new Date(string)` for the validity decision
|
|||
|
|
- Export `RESOLUTION_DATE_LABEL = 'Est. Resolution'`, `NO_DATE_PLACEHOLDER = 'not set'`, and `INVALID_DATE_PLACEHOLDER = 'invalid date'` as the single source of truth for component and tests
|
|||
|
|
- _Requirements: 1.1, 1.4, 1.6, 2.1_
|
|||
|
|
|
|||
|
|
- [x]* 1.2 Write property test for valid calendar date classification and formatting
|
|||
|
|
- File: `frontend/src/utils/__tests__/resolutionDate.property.test.js`
|
|||
|
|
- Use `fast-check` (v4) with Jest (`react-scripts test`); do not hand-roll generators
|
|||
|
|
- **Property 1: Valid calendar dates classify as "set" and format as YYYY-MM-DD**
|
|||
|
|
- Generator: valid calendar dates spanning years, all months, month-length boundaries (28/29/30/31), and leap days; assert `state === 'set'`, `value` matches `^\d{4}-\d{2}-\d{2}$` and equals the canonical normalized form
|
|||
|
|
- Run a minimum of 100 iterations (`{ numRuns: 100 }` or higher)
|
|||
|
|
- Tag the test: `// Feature: compliance-metric-estimated-resolution-date, Property 1: Valid calendar dates classify as "set" and format as YYYY-MM-DD`
|
|||
|
|
- **Validates: Requirements 1.1, 1.4**
|
|||
|
|
|
|||
|
|
- [x]* 1.3 Write property test for absent values classifying as "none"
|
|||
|
|
- File: `frontend/src/utils/__tests__/resolutionDate.property.test.js`
|
|||
|
|
- **Property 2: Absent values classify as "none"**
|
|||
|
|
- Generator: `fc.constantFrom(null, undefined, '')` combined with whitespace-only strings built from spaces, tabs, and newlines of varying length; assert `state === 'none'`
|
|||
|
|
- Minimum 100 iterations
|
|||
|
|
- Tag the test: `// Feature: compliance-metric-estimated-resolution-date, Property 2: Absent values classify as "none"`
|
|||
|
|
- **Validates: Requirements 2.1, 4.5**
|
|||
|
|
|
|||
|
|
- [x]* 1.4 Write property test for non-calendar-date values classifying as "invalid"
|
|||
|
|
- File: `frontend/src/utils/__tests__/resolutionDate.property.test.js`
|
|||
|
|
- **Property 3: Non-empty non-calendar-date values classify as "invalid"**
|
|||
|
|
- Generator: non-empty, non-whitespace-only strings that are not valid `YYYY-MM-DD` dates (wrong shapes, month `00`/`13`+, day `00`/`32`+, `2026-02-30`, arbitrary text), filtered to exclude any accidentally-valid date; assert `state === 'invalid'`
|
|||
|
|
- Minimum 100 iterations
|
|||
|
|
- Tag the test: `// Feature: compliance-metric-estimated-resolution-date, Property 3: Non-empty non-calendar-date values classify as "invalid"`
|
|||
|
|
- **Validates: Requirements 1.6**
|
|||
|
|
|
|||
|
|
- [x]* 1.5 Write property test for total classification over any metric list
|
|||
|
|
- File: `frontend/src/utils/__tests__/resolutionDate.property.test.js`
|
|||
|
|
- **Property 4: Classification is total over any metric list**
|
|||
|
|
- Generator: arrays mixing all input categories (valid dates, `null`, empty, whitespace-only, malformed); assert `formatResolutionDate` never throws, each result's `state` is in `{ 'set', 'none', 'invalid' }`, and the result count equals the input length
|
|||
|
|
- Minimum 100 iterations
|
|||
|
|
- Tag the test: `// Feature: compliance-metric-estimated-resolution-date, Property 4: Classification is total over any metric list`
|
|||
|
|
- **Validates: Requirements 2.2**
|
|||
|
|
|
|||
|
|
- [x]* 1.6 Write property test for per-metric independence (no collapsing)
|
|||
|
|
- File: `frontend/src/utils/__tests__/resolutionDate.property.test.js`
|
|||
|
|
- **Property 5: Each metric's display derives only from its own field (no collapsing)**
|
|||
|
|
- Generator: arrays of metric-like objects with independently chosen `resolution_date` values (including arrays forced to contain differing dates); assert each metric's derived result deep-equals `formatResolutionDate` applied to that same metric's own `resolution_date` computed in isolation, and that no result is replaced by a shared/"Multiple values" sentinel
|
|||
|
|
- Minimum 100 iterations
|
|||
|
|
- Tag the test: `// Feature: compliance-metric-estimated-resolution-date, Property 5: Each metric's display derives only from its own field (no collapsing)`
|
|||
|
|
- **Validates: Requirements 1.3, 3.2, 3.3**
|
|||
|
|
|
|||
|
|
- [x]* 1.7 Write example and edge-case unit tests for the helper
|
|||
|
|
- File: `frontend/src/utils/__tests__/resolutionDate.test.js`
|
|||
|
|
- Concrete fixtures: `'2026-07-01'` → `{ state: 'set', value: '2026-07-01' }`; `'2026-7-1'` → `invalid` (not zero-padded); `'07/01/2026'` → `invalid`; `'2024-02-29'` → `set` (leap year); `'2023-02-29'` → `invalid`; `' '` → `none`; `null` → `none`
|
|||
|
|
- _Requirements: 1.1, 1.4, 1.6, 2.1_
|
|||
|
|
|
|||
|
|
- [x] 2. Checkpoint - Ensure helper tests pass
|
|||
|
|
- Run `cd frontend && CI=true npm test -- resolutionDate` to confirm the helper and its property tests pass
|
|||
|
|
- Ensure all tests pass, ask the user if questions arise.
|
|||
|
|
|
|||
|
|
- [x] 3. Render the estimated-resolution-date line in `MetricRow`
|
|||
|
|
- [x] 3.1 Add the read-only date block to `MetricRow` in `frontend/src/components/pages/ComplianceDetailPanel.js`
|
|||
|
|
- Import `formatResolutionDate`, `RESOLUTION_DATE_LABEL`, `NO_DATE_PLACEHOLDER`, and `INVALID_DATE_PLACEHOLDER` from `../../utils/resolutionDate`
|
|||
|
|
- Render the new block as the first child of the row content, above the existing top row (`MetricChip`), the metric description, the Ivanti ID row, and the highlights list (Requirement 1.2)
|
|||
|
|
- Render the block only when `resolved` is falsy; for `resolved === true`, `MetricRow` must behave exactly as today (Requirements 3.1, 3.4)
|
|||
|
|
- For active metrics, call `formatResolutionDate(metric.resolution_date)` and render by state: `set` → label + `YYYY-MM-DD` value; `none` → label + `NO_DATE_PLACEHOLDER`; `invalid` → label + `INVALID_DATE_PLACEHOLDER`, with the rest of the row still rendering
|
|||
|
|
- Render `RESOLUTION_DATE_LABEL` as a visible text label adjacent to the value/placeholder (Requirement 1.5)
|
|||
|
|
- Use plain text only: no `input`, `button`, `a`, or change handler in the new subtree (Requirements 5.3, 5.4)
|
|||
|
|
- Do NOT change the `MetricRow` prop signature; read `resolution_date` from the existing `metric` object
|
|||
|
|
- Leave `computeSharedValues`, `handleSaveMetadata`, the editable Resolution Date metadata `Section`, and the metadata PATCH flow unchanged (Requirement 4.1)
|
|||
|
|
- Prefix any intentionally-unused variables with `_` per the project lint rules
|
|||
|
|
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.1, 2.2, 3.1, 3.2, 3.3, 3.4, 4.1, 5.3, 5.4_
|
|||
|
|
|
|||
|
|
- [x]* 3.2 Write render tests for placement, labels, and placeholders
|
|||
|
|
- File: `frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.js` using `@testing-library/react`
|
|||
|
|
- Placement (1.2): an active row with a valid date renders the estimated-resolution element before the description in document order
|
|||
|
|
- Label presence (1.5): `RESOLUTION_DATE_LABEL` text appears adjacent to the value for an active row with a valid date
|
|||
|
|
- Set value (1.1, 1.4): an active row with `'2026-07-01'` renders `2026-07-01`
|
|||
|
|
- No-date placeholder (2.1, 4.5): active rows with `null`/empty/whitespace render `NO_DATE_PLACEHOLDER` and still render the metric description
|
|||
|
|
- Invalid placeholder (1.6): an active row with a malformed date renders `INVALID_DATE_PLACEHOLDER` and still renders the metric description
|
|||
|
|
- _Requirements: 1.1, 1.2, 1.4, 1.5, 1.6, 2.1, 4.5_
|
|||
|
|
|
|||
|
|
- [x]* 3.3 Write render tests for resolved suppression, read-only structure, and role-independence
|
|||
|
|
- File: `frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.js`
|
|||
|
|
- Resolved suppression (3.1, 3.4): a resolved row with a populated date renders no estimated-resolution line; a mixed list shows the line only in active rows
|
|||
|
|
- Read-only structure (5.3): the date-line subtree contains no `input`, `button`, `a`, or change handler
|
|||
|
|
- Role-independence (5.1, 5.2, 5.4): rendering under viewer, editor, and admin auth contexts produces identical date-line output and introduces no editing control
|
|||
|
|
- Existing editor preserved (4.1): the panel still renders the editable Resolution Date `input[type=date]`
|
|||
|
|
- _Requirements: 3.1, 3.4, 4.1, 5.1, 5.2, 5.3, 5.4_
|
|||
|
|
|
|||
|
|
- [x]* 3.4 Write interaction tests for the existing save round-trip
|
|||
|
|
- File: `frontend/src/components/pages/__tests__/ComplianceDetailPanel.metricRow.test.js`
|
|||
|
|
- Successful save (4.2, 4.3, 4.5): mock a successful `PATCH` followed by `fetchDetail` returning updated metrics; assert the displayed value updates to the new date, and that clearing the field renders the no-date placeholder
|
|||
|
|
- Failed save (4.4): mock a failing `PATCH`; assert `detail` is unmodified (previously displayed date retained) and an error indication is shown
|
|||
|
|
- _Requirements: 4.2, 4.3, 4.4, 4.5_
|
|||
|
|
|
|||
|
|
- [ ] 4. Final checkpoint - Build and test verification
|
|||
|
|
- [x] 4.1 Verify production build and ESLint budget
|
|||
|
|
- Run `cd frontend && npm run build` and confirm the build compiles
|
|||
|
|
- Confirm ESLint warnings stay within the 25-warning budget; prefix any intentionally-unused variables with `_`
|
|||
|
|
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.1, 2.2, 3.1, 3.2, 3.3, 3.4, 4.1_
|
|||
|
|
|
|||
|
|
- [x] 4.2 Run the full test suite
|
|||
|
|
- Run `cd frontend && CI=true npm test` (non-watch) to execute the new property and render tests alongside the existing suite
|
|||
|
|
- Ensure all tests pass, ask the user if questions arise.
|
|||
|
|
|
|||
|
|
## Notes
|
|||
|
|
|
|||
|
|
- Tasks marked with `*` are optional test sub-tasks and can be skipped for a faster MVP, but they validate the five correctness properties and the fixed-DOM acceptance criteria and are recommended.
|
|||
|
|
- Each task references specific requirements (and, for property tests, the design property number) for traceability.
|
|||
|
|
- Test-driven ordering: the pure helper and its property tests (task 1) come before the component change (task 3) so the only branching logic is validated first.
|
|||
|
|
- Property tests use `fast-check` with a minimum of 100 iterations and are tagged with their feature and property number per the design's Testing Strategy.
|
|||
|
|
- Checkpoints (tasks 2 and 4) ensure incremental validation and a clean production build within the lint budget.
|
|||
|
|
|
|||
|
|
## Task Dependency Graph
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"waves": [
|
|||
|
|
{ "id": 0, "tasks": ["1.1"] },
|
|||
|
|
{ "id": 1, "tasks": ["1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "3.1"] },
|
|||
|
|
{ "id": 2, "tasks": ["3.2", "3.3", "3.4"] },
|
|||
|
|
{ "id": 3, "tasks": ["4.1"] },
|
|||
|
|
{ "id": 4, "tasks": ["4.2"] }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|