Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1b85084
chore(heureka): removes action column header and correct its layout
hodanoori Feb 19, 2026
6320edc
chore(heureka): use pills to show image versions
hodanoori Feb 19, 2026
84d3880
fix(heureka): preserve revert false positive message by avoiding inli…
hodanoori Mar 12, 2026
624578f
fix(heureka): only expose auth user ID when running in embedded mode
hodanoori Mar 12, 2026
c1ccafa
fix(heureka): fix revert false positive UX in remediated vulnerabilities
hodanoori Mar 12, 2026
52369c1
refactor(heureka): extract useTimedState hook to consolidate auto-exp…
hodanoori Mar 12, 2026
d4aa062
chore(heureka): improve success messages and add spacing in remediati…
hodanoori Mar 13, 2026
4981791
chore(heureka): improve false positive UX and data freshness
hodanoori Mar 13, 2026
ddff9bc
Merge branch 'main' into hoda-heureka-improve-ux
hodanoori Mar 13, 2026
b3519c6
fix(heureka): align fallback and loading row colSpan with DataGrid co…
hodanoori Mar 13, 2026
8d3b7e5
fix(heureka): store shouldExpire predicate in a ref to prevent timer …
hodanoori Mar 13, 2026
d093720
fix(heureka): fix useTimedState timer reset by storing shouldExpire p…
hodanoori Mar 13, 2026
e4af8d5
chore(heureka): adds changeset
hodanoori Mar 13, 2026
ea4e6ec
fix(heureka): hide revert spinner as soon as success message appears …
hodanoori Mar 13, 2026
751d5d9
fix(heureka): hide revert spinner as soon as success message appears …
hodanoori Mar 13, 2026
6e9351a
fix(heureka): show success message immediately after API call and run…
hodanoori Mar 13, 2026
2b1ad32
fix(heureka): address Copilot review comments on PR
hodanoori Mar 13, 2026
a534acf
test(heureka): fix ErrorBoundary className test assertion
hodanoori Mar 13, 2026
5fee78e
fix(heureka): addresses remaining code review comments
hodanoori Mar 17, 2026
e9bc877
Merge branch 'main' into hoda-heureka-improve-ux
hodanoori Mar 17, 2026
e897b11
fix(heureka): fetches always fresh data in RemediationHistoryPanel af…
hodanoori Mar 17, 2026
383cff0
Merge branch 'main' into hoda-heureka-improve-ux
hodanoori Mar 18, 2026
e860672
Merge remote-tracking branch 'origin/main' into hoda-heureka-improve-ux
hodanoori Apr 1, 2026
cf561b2
feat(heureka): use remediations as source of truth for active/remedia…
hodanoori Apr 1, 2026
022ec38
feat(heureka): adds tests for active/remediated vulnerability split l…
hodanoori Apr 1, 2026
08ffcec
Merge branch 'main' into hoda-heureka-improve-ux
hodanoori Apr 8, 2026
481b3b5
fix(heureka): revert useAuth() in FalsePositiveModal and document Rea…
hodanoori Apr 9, 2026
cda6548
fix(heureka): externalize React in greenhouse-auth-provider and use u…
hodanoori Apr 9, 2026
a2feb7b
test(heureka): wrap tests with AuthProvider after useAuth() migration
hodanoori Apr 9, 2026
0d0b90e
chore(heureka): simplify AuthProvider props inline per reviewer sugge…
hodanoori Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/ten-mammals-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@cloudoperators/juno-app-heureka": minor
"@cloudoperators/greenhouse-auth-provider": patch
---

Enables authentication support for Heureka and improves false positive remediation UX: inline spinner feedback during API operations, timed success messages, proper error display without unhandled exceptions, auth user ID scoped to embedded mode, and instant tab updates after mark FP or revert using the remediations query as an override on top of status filters.

Fix `greenhouse-auth-provider` bundling React into its output by adding `rollupOptions.external` to the Vite build config. React is a peer dependency and must not be included in the bundle to avoid multiple React instance conflicts in micro-frontend architectures.
15 changes: 12 additions & 3 deletions apps/heureka/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,25 @@ import { ErrorBoundary } from "./components/common/ErrorBoundary"
import { getClient } from "./apollo-client"
import { routeTree } from "./routeTree.gen"
import { StoreProvider } from "./store/StoreProvider"
import { AuthProvider, EmbeddedAuth } from "@cloudoperators/greenhouse-auth-provider"
import { AuthProvider, type AuthState, type EmbeddedAuth } from "@cloudoperators/greenhouse-auth-provider"

export type InitialFilters = {
support_group?: string[]
}

export type { AuthState, EmbeddedAuth } from "@cloudoperators/greenhouse-auth-provider"

const queryClient = new QueryClient()

/** Auth can be EmbeddedAuth (from shell) or a plain AuthState (e.g. from appProps.json, which cannot contain functions). */
export type AppProps = {
theme?: "theme-dark" | "theme-light"
apiEndpoint?: string
embedded?: boolean
initialFilters?: InitialFilters
basePath?: string
enableHashedRouting?: boolean
auth?: EmbeddedAuth
auth?: EmbeddedAuth | AuthState
}

const router = createRouter({
Expand Down Expand Up @@ -96,7 +99,13 @@ const App = (props: AppProps) => {
<AppShell embedded={props.embedded} pageHeader={<PageHeader applicationName="Heureka" />}>
<ErrorBoundary>
<StrictMode>
<AuthProvider embedded={props.embedded} auth={props.auth}>
<AuthProvider
embedded={props.embedded && !!props.auth}
auth={
props.auth &&
("getSnapshot" in props.auth ? props.auth : { getSnapshot: () => props.auth as AuthState })
}
>
<StoreProvider>
<RouterProvider basepath={props.basePath || "/"} router={router} />
</StoreProvider>
Expand Down
2 changes: 2 additions & 0 deletions apps/heureka/src/api/fetchImages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ export const fetchImages = ({
afterVersions,
vulFilter,
],
staleTime: 2.5 * 60 * 1000,
queryFn: () =>
apiClient.query<GetImagesQuery>({
query: GetImagesDocument,
fetchPolicy: "network-only",
variables: {
imgFilter: filter,
vulFilter,
Expand Down
4 changes: 4 additions & 0 deletions apps/heureka/src/api/fetchRemediations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@ import { RouteContext } from "../routes/-types"

type FetchRemediationsParams = Pick<RouteContext, "queryClient" | "apiClient"> & {
filter?: RemediationFilter
staleTime?: number
}

export const fetchRemediations = ({
queryClient,
apiClient,
filter,
staleTime = 2.5 * 60 * 1000,
}: FetchRemediationsParams): Promise<ObservableQuery.Result<GetRemediationsQuery>> => {
const queryKey = ["remediations", filter]

return queryClient.ensureQueryData({
queryKey,
staleTime,
queryFn: () =>
apiClient.query<GetRemediationsQuery>({
query: GetRemediationsDocument,
fetchPolicy: "network-only",
variables: {
filter,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import React from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { PortalProvider } from "@cloudoperators/juno-ui-components"
import { AuthProvider } from "@cloudoperators/greenhouse-auth-provider"
import { FalsePositiveModal } from "./index"

const mockAuth = { getSnapshot: () => ({ status: "anonymous" as const }) }

const defaultProps = {
open: true,
onClose: () => {},
Expand All @@ -20,9 +23,11 @@ const defaultProps = {

const renderModal = (props = {}) => {
return render(
<PortalProvider>
<FalsePositiveModal {...defaultProps} {...props} />
</PortalProvider>
<AuthProvider embedded auth={mockAuth}>
<PortalProvider>
<FalsePositiveModal {...defaultProps} {...props} />
</PortalProvider>
</AuthProvider>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ import {
Button,
Stack,
Textarea,
TextInput,
DateTimePicker,
Message,
} from "@cloudoperators/juno-ui-components"
import { RemediationInput, RemediationTypeValues, SeverityValues } from "../../../../generated/graphql"
import { useAuth } from "@cloudoperators/greenhouse-auth-provider"

type FalsePositiveModalProps = {
open: boolean
onClose: () => void
onConfirm: (input: RemediationInput) => Promise<void>
onConfirm: (input: RemediationInput) => Promise<{ error: string } | void>
vulnerability: string
severity?: string
service: string
Expand Down Expand Up @@ -50,12 +52,20 @@ export const FalsePositiveModal: React.FC<FalsePositiveModalProps> = ({
errorMessage,
onSetError,
}) => {
const auth = useAuth()
const authUserId = auth.status === "authenticated" ? auth.userId : null
const [description, setDescription] = useState<string>("")
const [manualUserId, setManualUserId] = useState<string>("")
const [expirationDate, setExpirationDate] = useState<Date | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const [descriptionError, setDescriptionError] = useState<string>("")
const [userIdError, setUserIdError] = useState<string>("")
const isMountedRef = useRef(true)

const manualUserIdTrimmed = manualUserId.trim()
const remediatedBy = authUserId ?? (manualUserIdTrimmed || undefined)
const isUserIdValid = !!remediatedBy

useEffect(() => {
return () => {
isMountedRef.current = false
Expand All @@ -69,8 +79,13 @@ export const FalsePositiveModal: React.FC<FalsePositiveModalProps> = ({
setDescriptionError("Description is required")
return
}
if (!remediatedBy) {
setUserIdError("User ID is required")
return
}

setDescriptionError("")
setUserIdError("")
setIsSubmitting(true)
try {
const input: RemediationInput = {
Expand All @@ -79,14 +94,20 @@ export const FalsePositiveModal: React.FC<FalsePositiveModalProps> = ({
service,
image,
description: descriptionTrimmed,
...(remediatedBy && { remediatedBy }),
...(severity && { severity: toSeverityValue(severity) }),
...(expirationDate && { expirationDate: expirationDate.toISOString() }),
}
await onConfirm(input)
const result = await onConfirm(input)
if (isMountedRef.current) {
setDescription("")
setExpirationDate(null)
onClose()
if (result?.error) {
onSetError?.(result.error)
} else {
setDescription("")
setManualUserId("")
setExpirationDate(null)
onClose()
}
}
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to create remediation"
Expand All @@ -102,8 +123,10 @@ export const FalsePositiveModal: React.FC<FalsePositiveModalProps> = ({

const handleClose = () => {
setDescription("")
setManualUserId("")
setExpirationDate(null)
setDescriptionError("")
setUserIdError("")
onSetError?.(null)
onClose()
}
Expand All @@ -129,7 +152,7 @@ export const FalsePositiveModal: React.FC<FalsePositiveModalProps> = ({
onClick={handleConfirm}
label={CONFIRM_LABEL}
variant="primary"
disabled={isSubmitting || !descriptionTrimmed}
disabled={isSubmitting || !descriptionTrimmed || !isUserIdValid}
/>
</Stack>
</ModalFooter>
Expand All @@ -146,6 +169,22 @@ export const FalsePositiveModal: React.FC<FalsePositiveModalProps> = ({
<div>
<strong>Image:</strong> {image}
</div>
<div>
<TextInput
label="User ID"
value={authUserId ?? manualUserId}
onChange={(e) => {
setManualUserId(e.target.value)
if (userIdError) setUserIdError("")
}}
disabled={!!authUserId}
required
invalid={!!userIdError}
errortext={userIdError}
placeholder={authUserId ? undefined : "Enter your user ID"}
helptext={authUserId ? "User ID from current session (read-only)." : "Enter your user ID."}
/>
</div>
<div>
<DateTimePicker
label="Expiration Date"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
PopupMenu,
PopupMenuOptions,
PopupMenuItem,
Spinner,
Icon,
} from "@cloudoperators/juno-ui-components"
import { Icon } from "@cloudoperators/juno-ui-components"
import { IssueIcon } from "../../../../../common/IssueIcon"
import { IssueTimestamp } from "../../../../../common/IssueTimestamp"
import { ImageVulnerability } from "../../../../../Services/utils"
Expand Down Expand Up @@ -50,6 +51,7 @@ export const IssuesDataRow = ({
}: IssuesDataRowProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
const { needsExpansion, textRef } = useTextOverflow(issue?.description || "")
const { apiClient } = useRouteContext({ from: "/services/$service" })
Expand All @@ -67,15 +69,20 @@ export const IssuesDataRow = ({
setIsModalOpen(true)
}

const handleModalConfirm = async (input: RemediationInput) => {
const handleModalConfirm = async (input: RemediationInput): Promise<{ error: string } | void> => {
setCreateError(null)
setIsModalOpen(false)
setIsSubmitting(true)
try {
await createRemediation({ apiClient, input })
const cveNumber = issue?.name || "unknown"
await onFalsePositiveSuccess?.(cveNumber)
setIsModalOpen(false)
// Fire refresh in the background so the spinner clears immediately after createRemediation.
Promise.resolve(onFalsePositiveSuccess?.(cveNumber)).catch(() => {})
Comment thread
hodanoori marked this conversation as resolved.
} catch (error) {
setCreateError(error instanceof Error ? error.message : "Failed to create remediation")
setIsModalOpen(true)
return { error: error instanceof Error ? error.message : "Failed to create remediation" }
} finally {
setIsSubmitting(false)
}
}

Expand Down Expand Up @@ -126,11 +133,15 @@ export const IssuesDataRow = ({
</DataGridCell>
{showFalsePositiveAction && (
<DataGridCell className="cursor-default interactive" onClick={(e) => e.stopPropagation()}>
<PopupMenu icon="moreVert" className="whitespace-nowrap">
<PopupMenuOptions>
<PopupMenuItem label="Mark False Positive" onClick={handleFalsePositiveClick} />
</PopupMenuOptions>
</PopupMenu>
{isSubmitting ? (
<Spinner variant="primary" size="small" className="ml-auto" />
) : (
<PopupMenu icon="moreVert" className="whitespace-nowrap ml-auto">
<PopupMenuOptions>
<PopupMenuItem label="Mark False Positive" onClick={handleFalsePositiveClick} />
</PopupMenuOptions>
</PopupMenu>
)}
</DataGridCell>
)}
</DataGridRow>
Expand Down
Loading
Loading