feat(risk-assessment): assessment results view and query hooks#8
feat(risk-assessment): assessment results view and query hooks#8
Conversation
Add query hooks for risk assessment API (fetch assessment, fetch results, create assessment, download report). Implement overall score card with submission date and action buttons, and criteria summary table with color-coded completeness and risk level labels. Integrate results view into the Product Risk Assessment tab, switching between wizard (in progress) and results (completed) based on assessment status. Implements JIRAPLAY-1380 Assisted-by: Claude Code
Reviewer's GuideImplements a completed risk assessment results view and supporting React Query hooks, wiring it into the Product Risk Assessment tab so it can switch between the wizard for in‑progress assessments and a results dashboard for completed assessments, including PDF download and criteria summary table. Sequence diagram for loading ProductRiskAssessment and switching between wizard and resultssequenceDiagram
actor User
participant ProductRiskAssessment
participant FetchRiskAssessmentHook
participant RiskAssessmentAPI
participant AssessmentWizard
participant AssessmentResults
User->>ProductRiskAssessment: openRiskAssessmentTab(riskAssessmentId)
ProductRiskAssessment->>FetchRiskAssessmentHook: useFetchRiskAssessment(riskAssessmentId)
FetchRiskAssessmentHook->>RiskAssessmentAPI: GET /api/v2/risk-assessment/{id}
RiskAssessmentAPI-->>FetchRiskAssessmentHook: RiskAssessment
FetchRiskAssessmentHook-->>ProductRiskAssessment: riskAssessment,isFetching,fetchError
alt isFetching
ProductRiskAssessment-->>User: show Spinner
else fetchError
ProductRiskAssessment-->>User: show StateError
else status is completed
ProductRiskAssessment->>AssessmentResults: render(riskAssessmentId,onStartNewAssessment)
else status is in_progress
ProductRiskAssessment->>AssessmentWizard: render(riskAssessmentId)
end
User->>ProductRiskAssessment: click StartNewAssessment
ProductRiskAssessment->>createRiskAssessmentMutation: mutate(riskAssessmentId)
createRiskAssessmentMutation->>RiskAssessmentAPI: POST /api/v2/risk-assessment { groupId }
RiskAssessmentAPI-->>createRiskAssessmentMutation: RiskAssessment
createRiskAssessmentMutation-->>ProductRiskAssessment: onSuccess(response)
ProductRiskAssessment->>Window: location.reload()
Sequence diagram for AssessmentResults data loading and PDF downloadsequenceDiagram
actor User
participant AssessmentResults
participant FetchRiskAssessmentResultsHook
participant DownloadAssessmentHook
participant RiskAssessmentAPI
participant Browser
User->>AssessmentResults: viewCompletedAssessment(riskAssessmentId)
AssessmentResults->>FetchRiskAssessmentResultsHook: useFetchRiskAssessmentResults(riskAssessmentId)
FetchRiskAssessmentResultsHook->>RiskAssessmentAPI: GET /api/v2/risk-assessment/{id}/results
RiskAssessmentAPI-->>FetchRiskAssessmentResultsHook: RiskAssessmentResults
FetchRiskAssessmentResultsHook-->>AssessmentResults: results,isFetching,fetchError
alt isFetching
AssessmentResults-->>User: show Spinner
else fetchError
AssessmentResults-->>User: show StateError
else results loaded
AssessmentResults-->>User: show overallScore,submittedDate,CriteriaSummaryTable
end
User->>AssessmentResults: click DownloadAssessment
AssessmentResults->>DownloadAssessmentHook: download()
DownloadAssessmentHook->>RiskAssessmentAPI: GET /api/v2/risk-assessment/{id}/report Accept application/pdf
RiskAssessmentAPI-->>DownloadAssessmentHook: Blob(pdf)
DownloadAssessmentHook->>Browser: createObjectURL(blob)
DownloadAssessmentHook->>Browser: clickHiddenLink(download risk-assessment-{id}.pdf)
Browser-->>User: saveOrOpenPDF
Class diagram for risk assessment types, hooks, and UI componentsclassDiagram
class RiskAssessment {
+string id
+string status
+string submittedAt
+number overallScore
}
class CriterionResult {
+string criterion
+string completeness
+string riskLevel
+number score
}
class RiskAssessmentResults {
+number overallScore
+string submittedAt
+CriterionResult[] criteria
}
class useFetchRiskAssessment {
+RiskAssessment riskAssessment
+boolean isFetching
+AxiosError fetchError
+useFetchRiskAssessment(id string) useFetchRiskAssessment
}
class useFetchRiskAssessmentResults {
+RiskAssessmentResults results
+boolean isFetching
+AxiosError fetchError
+useFetchRiskAssessmentResults(id string) useFetchRiskAssessmentResults
}
class useCreateRiskAssessmentMutation {
+mutate(groupId string) void
+useCreateRiskAssessmentMutation(onSuccess function,onError function) useCreateRiskAssessmentMutation
}
class useDownloadAssessment {
+download() void
+useDownloadAssessment(id string) useDownloadAssessment
}
class ProductRiskAssessment {
+string riskAssessmentId
+handleStartNewAssessment() void
+render() ReactNode
}
class AssessmentResults {
+string riskAssessmentId
+onStartNewAssessment() void
+render() ReactNode
}
class CriteriaSummaryTable {
+CriterionResult[] criteria
+render() ReactNode
}
class AssessmentWizard {
+string riskAssessmentId
+render() ReactNode
}
RiskAssessmentResults "1" o-- "*" CriterionResult
ProductRiskAssessment ..> useFetchRiskAssessment : uses
ProductRiskAssessment ..> useCreateRiskAssessmentMutation : uses
ProductRiskAssessment ..> AssessmentWizard : renders
ProductRiskAssessment ..> AssessmentResults : renders
AssessmentResults ..> useFetchRiskAssessmentResults : uses
AssessmentResults ..> useDownloadAssessment : uses
AssessmentResults ..> CriteriaSummaryTable : renders
CriteriaSummaryTable ..> CriterionResult : displays
useFetchRiskAssessment ..> RiskAssessment : returns
useFetchRiskAssessmentResults ..> RiskAssessmentResults : returns
useCreateRiskAssessmentMutation ..> RiskAssessment : creates
useDownloadAssessment ..> RiskAssessment : downloads report for
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 5 issues, and left some high level feedback:
- In
ProductRiskAssessment, theuseCreateRiskAssessmentMutationsuccess handler forces a fullwindow.location.reload(), which is heavy-handed for an SPA; consider driving the UI from query invalidation/returned data or local state so the view updates without a full page refresh. - The
useCreateRiskAssessmentMutationanduseDownloadAssessmenthelpers silently ignore/absorb errors (emptyonErrorcallback and no error handling indownload); consider surfacing failures viaStateErroror a notification/toast so users get feedback when actions fail. - In
riskLevelColor, theLabelcolor for the 'Low' case is set to'grey', but PatternFly uses'gray'as the valid color token; updating this avoids relying on an unsupported value and keeps styling consistent.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `ProductRiskAssessment`, the `useCreateRiskAssessmentMutation` success handler forces a full `window.location.reload()`, which is heavy-handed for an SPA; consider driving the UI from query invalidation/returned data or local state so the view updates without a full page refresh.
- The `useCreateRiskAssessmentMutation` and `useDownloadAssessment` helpers silently ignore/absorb errors (empty `onError` callback and no error handling in `download`); consider surfacing failures via `StateError` or a notification/toast so users get feedback when actions fail.
- In `riskLevelColor`, the `Label` color for the 'Low' case is set to `'grey'`, but PatternFly uses `'gray'` as the valid color token; updating this avoids relying on an unsupported value and keeps styling consistent.
## Individual Comments
### Comment 1
<location path="client/src/app/pages/sbom-group-details/product-risk-assessment.tsx" line_range="24-25" />
<code_context>
+ const { riskAssessment, isFetching, fetchError } =
+ useFetchRiskAssessment(riskAssessmentId);
+
+ const createMutation = useCreateRiskAssessmentMutation(
+ () => {
+ window.location.reload();
+ },
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid hard page reload and rely on query invalidation / local state to show the new assessment.
`window.location.reload()` on mutation success is heavy-handed and bypasses React Query’s caching. Since `useCreateRiskAssessmentMutation` already invalidates `RiskAssessmentsQueryKey`, you can instead update local state (e.g. clear the completed assessment or use the returned `RiskAssessment` to switch back to the wizard) and rely on React Query to re-fetch, avoiding a full-page refresh and improving UX.
Suggested implementation:
```typescript
const createMutation = useCreateRiskAssessmentMutation(
() => {},
() => {},
);
```
Depending on how `useFetchRiskAssessment` and `useCreateRiskAssessmentMutation` are implemented, you may want to:
1. Ensure `useCreateRiskAssessmentMutation` invalidates the relevant React Query keys (`RiskAssessmentsQueryKey` or similar) on success so that `useFetchRiskAssessment(riskAssessmentId)` refetches automatically.
2. If the UI differentiates between an "in-progress wizard" and "completed results" via local state, wire the `onSuccess` callback to update that state (e.g. reset wizard state or navigate back to the wizard view) instead of using `window.location.reload()`.
</issue_to_address>
### Comment 2
<location path="client/src/app/queries/risk-assessments.ts" line_range="57-59" />
<code_context>
+ };
+};
+
+export const useCreateRiskAssessmentMutation = (
+ onSuccess: (response: RiskAssessment) => void,
+ onError: (err: AxiosError) => void,
+) => {
+ const queryClient = useQueryClient();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Align mutation error typing with React Query’s `onError` signature.
This callback assumes all errors are `AxiosError`, but React Query’s `onError` receives `unknown` and `mutationFn` can throw non-Axios errors. That can lead callers to rely on Axios-specific fields that may not exist at runtime. Consider using `(err: unknown) => void` and narrowing inside, or explicitly casting only where you’re certain the error is Axios-based.
Suggested implementation:
```typescript
export const useCreateRiskAssessmentMutation = (
onSuccess: (response: RiskAssessment) => void,
onError: (err: unknown) => void,
) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (groupId: string) => {
const response = await axios.post<RiskAssessment>(RISK_ASSESSMENTS, {
groupId,
});
return response.data;
},
onSuccess: async (response) => {
await queryClient.invalidateQueries({
queryKey: [RiskAssessmentsQueryKey],
```
Any callers of `useCreateRiskAssessmentMutation` that currently type their error callback parameter as `AxiosError` should be updated to accept `unknown` instead, and then narrow internally using e.g. `axios.isAxiosError(err)` or type guards before accessing Axios-specific properties.
</issue_to_address>
### Comment 3
<location path="client/src/app/queries/risk-assessments.ts" line_range="79-82" />
<code_context>
+ });
+};
+
+export const useDownloadAssessment = (id?: string) => {
+ const download = async () => {
+ if (!id) return;
+ const response = await axios.get(`${RISK_ASSESSMENTS}/${id}/report`, {
+ responseType: "blob",
+ headers: { Accept: "application/pdf" },
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Add basic error handling around the download request to avoid silent failures.
If this request fails (e.g., network/server error), the user sees no indication. Consider wrapping the axios call in try/catch, showing an error state (toast/modal), and logging failures for observability.
Suggested implementation:
```typescript
export const useDownloadAssessment = (id?: string) => {
const download = async () => {
if (!id) return;
try {
const response = await axios.get(`${RISK_ASSESSMENTS}/${id}/report`, {
responseType: "blob",
headers: { Accept: "application/pdf" },
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `risk-assessment-${id}.pdf`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
// Basic error handling to avoid silent failures and aid observability
// Replace alert/console.error with app-wide toast or modal if available
// e.g., toast.error("Unable to download risk assessment report");
console.error("Failed to download risk assessment report", error);
alert("Unable to download risk assessment report. Please try again.");
}
};
```
If the application already has a centralized notification/toast system (e.g., a `useToast` hook or similar), replace the `alert` call inside the `catch` block with that mechanism to match existing UX patterns, and add/import the appropriate hook or helper at the top of this file.
</issue_to_address>
### Comment 4
<location path="client/src/app/pages/sbom-group-details/components/criteria-summary-table.tsx" line_range="12-10" />
<code_context>
+ criteria: CriterionResult[];
+}
+
+const completenessColor = (value: CriterionResult["completeness"]) => {
+ switch (value) {
+ case "Complete":
+ return "green";
+ case "Partial":
+ return "blue";
+ case "Missing":
+ return "yellow";
+ }
+};
+
</code_context>
<issue_to_address>
**suggestion:** Provide an explicit default in `completenessColor` to guard against future enum extensions.
The switch is exhaustive today, but if `completeness` gets a new value later this will start returning `undefined` and silently fall back to the label’s default color. Adding a `default` (or a `never` check) would make the behavior explicit and safer against future enum changes.
</issue_to_address>
### Comment 5
<location path="client/src/app/pages/sbom-group-details/components/criteria-summary-table.tsx" line_range="23-33" />
<code_context>
+ }
+};
+
+const riskLevelColor = (value: CriterionResult["riskLevel"]) => {
+ switch (value) {
+ case "Very high":
+ case "High":
+ return "red";
+ case "Moderate":
+ return "orange";
+ case "Low":
+ return "grey";
+ }
+};
+
</code_context>
<issue_to_address>
**suggestion:** Add a safe default branch in `riskLevelColor` for unexpected risk levels.
Right now, any future or unexpected `riskLevel` will fall through and return `undefined`, relying on downstream fallbacks. Adding a default that maps unknown values to a neutral color would make misconfigurations more visible and predictable.
```suggestion
const riskLevelColor = (value: CriterionResult["riskLevel"]) => {
switch (value) {
case "Very high":
case "High":
return "red";
case "Moderate":
return "orange";
case "Low":
return "grey";
default:
// Fallback for unexpected or future risk levels
return "grey";
}
};
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
client/src/app/pages/sbom-group-details/product-risk-assessment.tsx
Outdated
Show resolved
Hide resolved
client/src/app/pages/sbom-group-details/components/criteria-summary-table.tsx
Show resolved
Hide resolved
client/src/app/pages/sbom-group-details/components/criteria-summary-table.tsx
Outdated
Show resolved
Hide resolved
Verification Report for JIRAPLAY-1380 (commit f9bd36a)
Overall: WARNOne review feedback item upgraded to code change request: This comment was AI-generated by sdlc-workflow/verify-pr v0.5.5. |
… invalidation Remove window.location.reload() from the mutation success callback in ProductRiskAssessment. The useCreateRiskAssessmentMutation hook already invalidates RiskAssessmentsQueryKey on success, so React Query will automatically re-fetch the assessment data without a full page reload. Implements JIRAPLAY-1394 Assisted-by: Claude Code
Fix all risk assessment query hooks, TypeScript interfaces, and component
logic to match the actual backend API:
- Replace useFetchRiskAssessment with useFetchRiskAssessmentsByGroup
calling GET /risk-assessment/group/{groupId}
- Fix RiskAssessment, RiskAssessmentResults, CriterionResult types to
match backend response shapes (nested categories, scoring object)
- Add CategoryResult, CategoryScore, OverallScore, ScoringResult types
- Rename ProductRiskAssessment prop from riskAssessmentId to groupId
- Add group-to-assessment lookup flow: fetch by group, derive ID
- Fix download endpoint to use /document/{category} path
- Handle empty assessment state with StateNoData + create button
Implements JIRAPLAY-1396
Assisted-by: Claude Code
Verification Report for JIRAPLAY-1380 (commit abab1dc)
Overall: PASSAll checks pass. The PR includes the original implementation (JIRAPLAY-1380), the This comment was AI-generated by sdlc-workflow/verify-pr v0.5.7. |
- Render assessment results inside the wizard layout (left nav + right panel) instead of replacing the wizard entirely - Add "Download Assessment" button alongside "Start New Assessment" - Wrap criteria table in a Card with "Criteria Summary" title - Replace Label badges with icon + text for risk level column (ExclamationCircleIcon for high, ExclamationTriangleIcon for moderate, CheckCircleIcon for low) - Reposition buttons below score percentage and submission date - Keep wizard footer (Back/Next) visible in results view Implements JIRAPLAY-1406 Assisted-by: Claude Code
…ate in results view Remove !!resultsContent guards from isActive and isDisabled props in the assessment wizard. The first nav item (SAR) is now visually highlighted when viewing results, and Back/Next buttons follow their normal disabled logic instead of being unconditionally disabled. Implements JIRAPLAY-1407 Assisted-by: Claude Code
Call queryClient.invalidateQueries on RiskAssessmentsQueryKey after a successful document upload so the parent component re-fetches the assessment list and automatically switches from wizard to results view when the backend sets status to 'completed'. Implements JIRAPLAY-1409 Assisted-by: Claude Code
Replace plain text number prefix ('1. Category') with circular badge
indicators matching the Figma mockup. Active step shows filled primary
color circle with white text, inactive steps show outlined circles with
neutral border. Completed steps retain the green CheckCircleIcon.
Implements JIRAPLAY-1411
Assisted-by: Claude Code
The backend returns assessments sorted by createdAt ascending (oldest first). Change from assessments[0] to assessments[assessments.length - 1] so the most recent assessment determines the view state (wizard vs results). Implements JIRAPLAY-1413 Assisted-by: Claude Code
Replace global resultsContent overlay with per-category rendering: - Each wizard step independently shows upload area or category results based on CategoryResult.processed from the API - Navigation (Next/Back/nav clicks) always works, switching between categories with different content per step - Derive completed steps from API data (processed boolean) merged with local upload tracking - Refactor AssessmentResults to AssessmentCategoryResults accepting per-category data via props instead of fetching globally - Add useDeleteRiskAssessmentMutation for Start New Assessment flow (delete old + create new instead of orphaning duplicates) - Show per-category score and criteria, not flattened across all Implements JIRAPLAY-1415 Assisted-by: Claude Code
The PatternFly token --pf-t--global--color--nonstatus--white--default does not resolve to white in PF6. Use explicit #fff for the active step number text color. Implements JIRAPLAY-1415 Assisted-by: Claude Code
Add formatCriterionLabel() to convert snake_case backend keys (e.g., threat_identification) into readable labels (Threat identification). Applied to criterion names, completeness values, and risk level text. Falls back gracefully for unknown keys. Implements JIRAPLAY-1418 Assisted-by: Claude Code
Verification Report for JIRAPLAY-1426 (commit 1c6172e)
Acceptance Criteria Detail
Overall: FAILJIRAPLAY-1426 has not been implemented yet. The task is in "New" status and no commits on this PR reference it. The download hook ( This comment was AI-generated by sdlc-workflow/verify-pr v0.5.10. |
Replace useDownloadAssessmentDocument (which downloads the raw uploaded
PDF via /document/{category}) with useDownloadAssessmentReport (which
calls GET /risk-assessment/{id}/report to download the generated PDF
report). Remove the category parameter since the report covers all
categories.
Implements JIRAPLAY-1426
Assisted-by: Claude Code
Summary
useFetchRiskAssessmentsByGroup,useFetchRiskAssessmentResults,useCreateRiskAssessmentMutation,useDownloadAssessmentDocument)window.location.reload()with React Query invalidation in mutation success callbackImplements JIRAPLAY-1380
Implements JIRAPLAY-1394
Implements JIRAPLAY-1396
Implements JIRAPLAY-1406
Test plan
🤖 Generated with Claude Code