From b7047a71e29c0f5eb71239db8fb5799c3e59d4c5 Mon Sep 17 00:00:00 2001 From: Hoda Noori Date: Mon, 17 Aug 2026 11:54:47 +0200 Subject: [PATCH 1/8] feat(heureka): add Change Severity action to vulnerability rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new 'Change Severity' mitigation option in the popup menu of each vulnerability row on the image details page, alongside the existing FalsePositive, RiskAcceptance, and MitigateManually actions. - Add ChangeSeverityModal with a severity dropdown (SeverityValues enum), user ID, expiration date, and description fields - Uses RemediationTypeValues.Rescore for the API call with the selected severity as the override value - Wire onChangeSeveritySuccess callback through IssuesDataRow → IssuesDataRows → VulnerabilitiesTabContent → ImageIssuesList Signed-off-by: Hoda Noori --- .../ChangeSeverityModal/index.tsx | 254 ++++++++++++++++++ .../IssuesDataRows/IssuesDataRow/index.tsx | 14 + .../IssuesDataRows/IssuesDataRows.test.tsx | 3 + .../ImageIssuesList/IssuesDataRows/index.tsx | 3 + .../ImageDetails/ImageIssuesList/index.tsx | 13 + 5 files changed, 287 insertions(+) create mode 100644 apps/heureka/src/components/Service/ImageDetails/ChangeSeverityModal/index.tsx diff --git a/apps/heureka/src/components/Service/ImageDetails/ChangeSeverityModal/index.tsx b/apps/heureka/src/components/Service/ImageDetails/ChangeSeverityModal/index.tsx new file mode 100644 index 0000000000..c26d28d854 --- /dev/null +++ b/apps/heureka/src/components/Service/ImageDetails/ChangeSeverityModal/index.tsx @@ -0,0 +1,254 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useRef, useEffect } from "react" +import { + Modal, + ModalFooter, + Button, + Stack, + Textarea, + TextInput, + Select, + SelectOption, + DateTimePicker, + Message, +} from "@cloudoperators/juno-ui-components" +import { RemediationInput, RemediationTypeValues, SeverityValues } from "../../../../generated/graphql" +import { useAuth } from "@cloudoperators/greenhouse-auth-provider" + +type ChangeSeverityModalProps = { + open: boolean + onClose: () => void + onConfirm: (input: RemediationInput) => Promise<{ error: string } | void> + vulnerability: string + severity?: string + service: string + image: string +} + +const EMPTY_FORM = { + description: "", + manualUserId: "", + newSeverity: "" as SeverityValues | "", + expirationDate: null as Date | null, +} +const EMPTY_ERRORS = { description: "", userId: "", newSeverity: "", expirationDate: "" } + +const SEVERITY_OPTIONS: { label: string; value: SeverityValues }[] = [ + { label: "Critical", value: SeverityValues.Critical }, + { label: "High", value: SeverityValues.High }, + { label: "Medium", value: SeverityValues.Medium }, + { label: "Low", value: SeverityValues.Low }, + { label: "None", value: SeverityValues.None }, +] + +export const ChangeSeverityModal: React.FC = ({ + open, + onClose, + onConfirm, + vulnerability, + severity, + service, + image, +}) => { + // useAuth() from @cloudoperators/greenhouse-auth-provider returns a discriminated union without exported types + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call + const auth = useAuth() as any + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + const authUserId = + auth.status === "authenticated" + ? (auth.userId as string | undefined) || (auth.userName as string | undefined) + : null + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + const [form, setForm] = useState(EMPTY_FORM) + const [errors, setErrors] = useState(EMPTY_ERRORS) + const [isSubmitting, setIsSubmitting] = useState(false) + const [apiError, setApiError] = useState(null) + const isMountedRef = useRef(true) + + const manualUserIdTrimmed = form.manualUserId.trim() + const remediatedBy = authUserId ?? (manualUserIdTrimmed || undefined) + const isUserIdValid = !!remediatedBy + + useEffect(() => { + isMountedRef.current = true + return () => { + isMountedRef.current = false + } + }, []) + + useEffect(() => { + if (!open) { + setForm(EMPTY_FORM) + setErrors(EMPTY_ERRORS) + setApiError(null) + } + }, [open]) + + const descriptionTrimmed = form.description.trim() + + const handleConfirm = async () => { + let hasError = false + if (!form.newSeverity) { + setErrors((prev) => ({ ...prev, newSeverity: "New severity is required" })) + hasError = true + } + if (!remediatedBy) { + setErrors((prev) => ({ ...prev, userId: "User ID is required" })) + hasError = true + } + if (!descriptionTrimmed) { + setErrors((prev) => ({ ...prev, description: "Description is required" })) + hasError = true + } + if (!form.expirationDate) { + setErrors((prev) => ({ ...prev, expirationDate: "Expiration date is required" })) + hasError = true + } + if (hasError) return + + setErrors(EMPTY_ERRORS) + setIsSubmitting(true) + try { + const input: RemediationInput = { + type: RemediationTypeValues.Rescore, + vulnerability, + service, + image, + description: descriptionTrimmed, + ...(remediatedBy && { remediatedBy }), + severity: form.newSeverity as SeverityValues, + expirationDate: form.expirationDate!.toISOString(), + } + const result = await onConfirm(input) + if (result?.error) { + setApiError(result.error) + } else if (isMountedRef.current) { + setForm(EMPTY_FORM) + onClose() + } + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to change severity" + setApiError(message) + } finally { + setIsSubmitting(false) + } + } + + const handleClose = () => { + setForm(EMPTY_FORM) + setErrors(EMPTY_ERRORS) + setApiError(null) + onClose() + } + + const isConfirmDisabled = + isSubmitting || !form.newSeverity || !isUserIdValid || !descriptionTrimmed || !form.expirationDate + + return ( + + +