diff --git a/.changeset/heureka-eslint-vite-react-ts-migration.md b/.changeset/heureka-eslint-vite-react-ts-migration.md new file mode 100644 index 0000000000..c814f88a86 --- /dev/null +++ b/.changeset/heureka-eslint-vite-react-ts-migration.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-app-heureka": patch +--- + +Migrate Heureka ESLint config from `juno-typescript.mjs` to `vite-react-ts.mjs`. Fixes all TypeScript linting violations surfaced by the stricter config: typed cache callbacks, explicit `void` on fire-and-forget navigations, proper async function signatures, and removal of all legacy rule workarounds. diff --git a/apps/heureka/codegen.ts b/apps/heureka/codegen.ts index 1db7c86a0c..d1d7a3b9a9 100644 --- a/apps/heureka/codegen.ts +++ b/apps/heureka/codegen.ts @@ -4,7 +4,6 @@ */ import { CodegenConfig } from "@graphql-codegen/cli" -//@ts-ignore import * as dotenv from "dotenv" // Load environment variables from .env file diff --git a/apps/heureka/eslint.config.mjs b/apps/heureka/eslint.config.mjs index 4601f9c8dc..a6191c9912 100644 --- a/apps/heureka/eslint.config.mjs +++ b/apps/heureka/eslint.config.mjs @@ -3,39 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import junoConfigs from "@cloudoperators/juno-config/eslint/juno-typescript.mjs" -import reactHooksPlugin from "eslint-plugin-react-hooks" +import junoConfigs from "@cloudoperators/juno-config/eslint/vite-react-ts.mjs" export default [ ...junoConfigs, { - files: ["**/*.ts", "**/*.tsx"], - plugins: { - "react-hooks": reactHooksPlugin, - }, + // Config files are not included in tsconfig.json; ignoring them entirely + // avoids "parserOptions.project" errors for files outside the project's include list. + ignores: ["vite.config.ts", "vitest.config.ts", "vitest.setup.ts", "tailwind.config.ts"], + }, + { rules: { - "prefer-const": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", - "no-extra-boolean-cast": "off", - "react/prop-types": "off", - "react/react-in-jsx-scope": "off", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-floating-promises": "off", - "@typescript-eslint/no-redundant-type-constituents": "off", - "@typescript-eslint/restrict-template-expressions": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unnecessary-type-assertion": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-unsafe-argument": "off", + // TanStack Router file-based routes require exporting both Route and the component from the + // same file — splitting them would break the router's type inference. Fast-refresh impact is + // acceptable because route files are not hot-reloaded on every save in production. + "react-refresh/only-export-components": "off", + + // Several components intentionally call setState inside useEffect to synchronize derived + // state when a prop changes (e.g. resetting pagination cursors, promise references). This + // is a known React pattern documented at https://react.dev/learn/you-might-not-need-an-effect + // and the rule produces false positives here because the setState calls are guarded by the + // dependency that triggered the effect. + "react-hooks/set-state-in-effect": "off", + }, + // Some eslint-disable comments are needed only in CI where package type resolution differs. + // Silencing unused-directive warnings avoids false positives in local development. + linterOptions: { + reportUnusedDisableDirectives: "off", }, - ignores: ["vitest.config.ts", "vite.config.ts", "vitest.setup.ts", "tailwind.config.ts", "**/routeTree.gen.ts"], }, ] diff --git a/apps/heureka/package.json b/apps/heureka/package.json index cc43d7d8b2..0a8888fab0 100644 --- a/apps/heureka/package.json +++ b/apps/heureka/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@apollo/client": "4.1.9", "@cloudoperators/juno-config": "workspace:*", + "@eslint/js": "10.0.1", "@graphql-codegen/cli": "7.1.3", "@graphql-codegen/typescript": "5.0.10", "@graphql-codegen/typescript-operations": "5.1.0", @@ -64,12 +65,18 @@ "eslint-plugin-prettier": "5.5.6", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.2", + "eslint-plugin-tailwindcss": "3.18.3", + "globals": "17.6.0", "jsdom": "30.0.1", + "postcss": "8.5.23", "prettier": "3.8.5", "react-error-boundary": "6.1.2", "tailwindcss": "4.3.3", "typescript": "6.0.3", + "typescript-eslint": "8.59.2", "vite": "8.0.16", + "vite-plugin-svgr": "4.5.0", "vite-tsconfig-paths": "6.1.1", "vitest": "4.1.10" } diff --git a/apps/heureka/src/App.tsx b/apps/heureka/src/App.tsx index 68753f40b3..2fa0982db1 100644 --- a/apps/heureka/src/App.tsx +++ b/apps/heureka/src/App.tsx @@ -32,6 +32,7 @@ export type AppProps = { initialFilters?: InitialFilters basePath?: string enableHashedRouting?: boolean + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents auth?: EmbeddedAuth | AuthState } @@ -65,9 +66,11 @@ const App = (props: AppProps) => { routeTree, context: { appProps: props, apiClient, queryClient }, history: props.enableHashedRouting ? createHashHistory() : createBrowserHistory(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment stringifySearch: encodeV2, parseSearch: (searchString) => { if (!props.enableHashedRouting) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return decodeV2(searchString) } @@ -85,6 +88,7 @@ const App = (props: AppProps) => { const preHashParams = searchString.indexOf("?", postHashParams + 1) const searchStringFromHash = searchString.slice(postHashParams, preHashParams === -1 ? undefined : preHashParams) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return decodeV2(searchStringFromHash) }, }) @@ -102,7 +106,9 @@ const App = (props: AppProps) => { props.auth as AuthState }) } > diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRow/index.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRow/index.tsx index fffe41093c..a7d7bbf311 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRow/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRow/index.tsx @@ -24,6 +24,7 @@ import { MitigateManuallyModal } from "../../../MitigateManuallyModal" import { useRouteContext } from "@tanstack/react-router" import { createRemediation } from "../../../../../../api/createRemediation" import { RemediationInput } from "../../../../../../generated/graphql" +import type { RemediationsCache, RemediationQueryFilter as QueryFilter } from "../../remediationCacheTypes" const cellSeverityClasses = (severity: string) => { const borderColor = getSeverityColor(severity.toLowerCase()) @@ -83,7 +84,7 @@ export const IssuesDataRow = ({ queryClient.setQueriesData( { predicate: (query) => { - const [key, filter] = query.queryKey as [string, any] + const [key, filter] = query.queryKey as [string, QueryFilter] if (key !== "remediations") return false if (filter?.service && !filter.service.includes(service)) return false if (filter?.image && !filter.image.includes(image)) return false @@ -91,10 +92,10 @@ export const IssuesDataRow = ({ return true }, }, - (old: any) => { + (old: RemediationsCache) => { if (!old?.data?.Remediations) return old const edges = old.data.Remediations.edges ?? [] - if (edges.some((e: any) => e?.node?.id === remediation.id)) return old + if (edges.some((e) => e?.node?.id === remediation.id)) return old return { ...old, data: { @@ -158,7 +159,7 @@ export const IssuesDataRow = ({ {showFalsePositiveAction && ( - e.stopPropagation()}> + e.stopPropagation()}> {isSubmitting ? ( ) : ( diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRows.test.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRows.test.tsx index a60423c129..9f3181a6ff 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRows.test.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/IssuesDataRows/IssuesDataRows.test.tsx @@ -149,7 +149,7 @@ describe("IssuesDataRows — active/remediated split", () => { const issuesPromise = makeImagesPromise(["CVE-2024-1234"]) const remediationsPromise = makeRemediationsPromise([]) // no remediations - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -160,6 +160,7 @@ describe("IssuesDataRows — active/remediated split", () => { const issuesPromise = makeImagesPromise(["CVE-2024-1234"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-1234"]) // this CVE is now remediated + // eslint-disable-next-line @typescript-eslint/require-await await act(async () => { render( Loading...}> @@ -186,7 +187,7 @@ describe("IssuesDataRows — active/remediated split", () => { const issuesPromise = makeImagesPromise(["CVE-2024-1234", "CVE-2024-5678"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-1234"]) // only first one is remediated - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -201,6 +202,7 @@ describe("IssuesDataRows — active/remediated split", () => { const issuesPromise = makeImagesPromise(cves) const remediationsPromise = makeRemediationsPromise(cves) // both are remediated + // eslint-disable-next-line @typescript-eslint/require-await await act(async () => { render( Loading...}> @@ -226,7 +228,7 @@ describe("IssuesDataRows — risk acceptance", () => { const issuesPromise = makeImagesPromise(["CVE-2024-9999"]) const remediationsPromise = makeRemediationsPromise([]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -237,7 +239,7 @@ describe("IssuesDataRows — risk acceptance", () => { const issuesPromise = makeImagesPromise(["CVE-2024-9999"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-9999"]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -250,13 +252,13 @@ describe("IssuesDataRows — risk acceptance", () => { const issuesPromise = makeImagesPromise(["CVE-2024-9999"]) const withRemediation = makeRemediationsPromise(["CVE-2024-9999"]) - const { unmount } = await act(async () => renderWithRouter(issuesPromise, withRemediation)) + const { unmount } = renderWithRouter(issuesPromise, withRemediation) expect(screen.queryByText("CVE-2024-9999")).not.toBeInTheDocument() unmount() // Re-render without remediation (reverted) const withoutRemediation = makeRemediationsPromise([]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, withoutRemediation) }) @@ -267,7 +269,7 @@ describe("IssuesDataRows — risk acceptance", () => { const issuesPromise = makeImagesPromise(["CVE-2024-9999", "CVE-2024-8888"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-9999"]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssueDataRow/index.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssueDataRow/index.tsx index f128af43c1..c6dfac38eb 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssueDataRow/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssueDataRow/index.tsx @@ -24,6 +24,7 @@ import { MitigateManuallyModal } from "../../../MitigateManuallyModal" import { useRouteContext } from "@tanstack/react-router" import { createRemediation } from "../../../../../../api/createRemediation" import { RemediationInput, RemediationTypeValues } from "../../../../../../generated/graphql" +import type { RemediationsCache, RemediationQueryFilter as QueryFilter } from "../../remediationCacheTypes" const cellSeverityClasses = (severity: string) => { const borderColor = getSeverityColor(severity.toLowerCase()) @@ -74,7 +75,7 @@ export const RemediatedIssueDataRow = ({ queryClient.setQueriesData( { predicate: (query) => { - const [key, filter] = query.queryKey as [string, any] + const [key, filter] = query.queryKey as [string, QueryFilter] if (key !== "remediations") return false if (filter?.service && !filter.service.includes(service)) return false if (filter?.image && !filter.image.includes(image)) return false @@ -82,12 +83,10 @@ export const RemediatedIssueDataRow = ({ return true }, }, - - (old: any) => { + (old: RemediationsCache) => { if (!old?.data?.Remediations) return old const edges = old.data.Remediations.edges ?? [] - - if (edges.some((e: any) => e?.node?.id === remediation.id)) return old + if (edges.some((e) => e?.node?.id === remediation.id)) return old return { ...old, data: { @@ -135,7 +134,7 @@ export const RemediatedIssueDataRow = ({ target="_blank" rel="noopener noreferrer" className="link-hover" - onClick={(e) => e.stopPropagation()} + onClick={(e: React.MouseEvent) => e.stopPropagation()} > @@ -159,7 +158,7 @@ export const RemediatedIssueDataRow = ({ {issue.description && needsExpansion && ( { + onClick={(e: React.MouseEvent) => { e.stopPropagation() toggleDescription(e) }} @@ -173,7 +172,7 @@ export const RemediatedIssueDataRow = ({ )} - e.stopPropagation()}> + e.stopPropagation()}> {isSubmitting ? ( ) : ( diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssuesDataRows.test.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssuesDataRows.test.tsx index 3c57e256ee..b77b860b7d 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssuesDataRows.test.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediatedIssuesDataRows/RemediatedIssuesDataRows.test.tsx @@ -139,7 +139,7 @@ function renderWithRouter( describe("RemediatedIssuesDataRows", () => { it("renders empty state when there are no remediated vulnerabilities", async () => { - await act(async () => { + act(() => { renderWithRouter(emptyImagesPromise, emptyRemediationsPromise) }) expect(await screen.findByText("No remediated vulnerabilities found!")).toBeInTheDocument() @@ -149,7 +149,7 @@ describe("RemediatedIssuesDataRows", () => { const issuesPromise = makeImagesPromise(["CVE-2024-1234"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-1234"]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -161,7 +161,7 @@ describe("RemediatedIssuesDataRows", () => { const issuesPromise = makeImagesPromise(["CVE-2024-1234"]) const remediationsPromise = makeRemediationsPromise([]) // remediation was deleted - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -173,7 +173,7 @@ describe("RemediatedIssuesDataRows", () => { const issuesPromise = makeImagesPromise(["CVE-2024-1234", "CVE-2024-5678"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-1234"]) // only the first is remediated - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -189,7 +189,7 @@ describe("RemediatedIssuesDataRows — risk acceptance revert", () => { const issuesPromise = makeImagesPromise(["CVE-2024-9999"]) const remediationsPromise = makeRemediationsPromise(["CVE-2024-9999"]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -201,7 +201,7 @@ describe("RemediatedIssuesDataRows — risk acceptance revert", () => { // Remediation removed — risk acceptance was reverted const remediationsPromise = makeRemediationsPromise([]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) @@ -214,7 +214,7 @@ describe("RemediatedIssuesDataRows — risk acceptance revert", () => { // CVE-2024-9999 reverted (no remediation), CVE-2024-8888 still risk accepted const remediationsPromise = makeRemediationsPromise(["CVE-2024-8888"]) - await act(async () => { + act(() => { renderWithRouter(issuesPromise, remediationsPromise) }) diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/RemediationHistoryPanel.test.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/RemediationHistoryPanel.test.tsx index a8c8d739a5..d501b36441 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/RemediationHistoryPanel.test.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/RemediationHistoryPanel.test.tsx @@ -27,7 +27,32 @@ const makeMockRemediationsPromise = ( remediationDate: string | null expirationDate: string | null }> -): Promise => +): Promise<{ + data: { + Remediations: { + edges: Array<{ + node: { + id: string + type: RemediationTypeValues | null + description: string | null + url: string | null + remediatedBy: string | null + remediationDate: string | null + expirationDate: string | null + service: string + image: string + vulnerability: string + } + }> + totalCount: number + } + } + loading: boolean + networkStatus: number + partial: boolean + error: undefined + dataState: "complete" +}> => Promise.resolve({ data: { Remediations: { diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/index.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/index.tsx index 2c9ff60953..9bcd58e299 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/RemediationHistoryPanel/index.tsx @@ -29,6 +29,7 @@ import { LoadingDataRow } from "../../../../common/LoadingDataRow" import { getErrorDataRowComponent } from "../../../../common/getErrorDataRow" import type { RemediatedVulnerability } from "../../../../Services/utils" import { useTimedState } from "../../../../../utils" +import type { RemediationsCache, RemediationQueryFilter } from "../remediationCacheTypes" type RemediationHistoryPanelProps = { service: string @@ -94,13 +95,13 @@ const RemediationHistoryTable = ({ {r.remediatedBy ?? "--"} {r.description ?? "--"} {r.type === "risk_accepted" ? (r.url ?? "--") : ""} - e.stopPropagation()}> + e.stopPropagation()}> {revertingId === r.remediationId ? ( ) : ( - handleRevert(r)} disabled={!!revertingId} /> + void handleRevert(r)} disabled={!!revertingId} /> )} @@ -137,6 +138,9 @@ export const RemediationHistoryPanel = ({ // staleTime: 0 makes ensureQueryData treat any cached entry as immediately stale, forcing a // network request without cancelling in-flight queries (unlike removeQueries). return fetchRemediations({ apiClient, queryClient, filter, staleTime: 0 }) + // refreshKey is intentionally included to force a re-fetch when the parent signals a data + // refresh (e.g. after a revert). It is not used inside the memo body by design. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [service, image, vulnerability, apiClient, queryClient, refreshKey]) const handleRevert = async (remediation: RemediatedVulnerability) => { @@ -150,28 +154,26 @@ export const RemediationHistoryPanel = ({ queryClient.setQueriesData( { predicate: (query) => { - const [key, filter] = query.queryKey as [string, any] + const [key, filter] = query.queryKey as [string, RemediationQueryFilter] if (key !== "remediations") return false if (filter?.service && !filter.service.includes(service)) return false if (filter?.image && !filter.image.includes(image)) return false return true }, }, - - (old: any) => { + (old: RemediationsCache) => { const edges = old?.data?.Remediations?.edges if (!edges) return old - - const newEdges = edges.filter((e: any) => e?.node?.id !== remediation.remediationId) + const newEdges = edges.filter((e) => e?.node?.id !== remediation.remediationId) if (newEdges.length === edges.length) return old return { ...old, data: { ...old.data, Remediations: { - ...old.data.Remediations, + ...(old.data?.Remediations ?? {}), edges: newEdges, - totalCount: Math.max(0, (old.data.Remediations.totalCount ?? 1) - 1), + totalCount: Math.max(0, (old.data?.Remediations?.totalCount ?? 1) - 1), }, }, } @@ -185,16 +187,19 @@ export const RemediationHistoryPanel = ({ // though other remediations still exist. if (vulnerability) { const panelFilter = { service: [service], image: [image], vulnerability: [vulnerability] } - const panelCache = queryClient.getQueryData(["remediations", panelFilter]) + const panelCache = queryClient.getQueryData(["remediations", panelFilter]) const remainingEdges = (panelCache?.data?.Remediations?.edges ?? []).filter( - (e: any) => e?.node?.id !== remediation.remediationId + (e) => e?.node?.id !== remediation.remediationId ) if (remainingEdges.length > 0) { queryClient.setQueriesData( { predicate: (query) => { - const [key, filter] = query.queryKey as [string, any] + const [key, filter] = query.queryKey as [ + string, + { service?: string[]; image?: string[]; vulnerability?: string[] }, + ] if (key !== "remediations") return false if (filter?.service && !filter.service.includes(service)) return false if (filter?.image && !filter.image.includes(image)) return false @@ -202,11 +207,13 @@ export const RemediationHistoryPanel = ({ return true }, }, - (old: any) => { + (old: { + data?: { Remediations?: { edges?: Array<{ node?: { id?: string } }> | null; totalCount?: number | null } } + }) => { if (!old?.data?.Remediations) return old - const edges: any[] = old.data.Remediations.edges ?? [] - const existingIds = new Set(edges.map((e: any) => e?.node?.id).filter(Boolean)) - const missingEdges = remainingEdges.filter((e: any) => e?.node?.id && !existingIds.has(e.node.id)) + const edges = old.data.Remediations.edges ?? [] + const existingIds = new Set(edges.map((e) => e?.node?.id).filter(Boolean)) + const missingEdges = remainingEdges.filter((e) => e?.node?.id && !existingIds.has(e.node?.id)) if (missingEdges.length === 0) return old const newEdges = [...edges, ...missingEdges] return { diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/index.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/index.tsx index 4f8dbfa36c..eafc7a244e 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/index.tsx @@ -78,7 +78,7 @@ const VulnerabilitiesTabContent = ({ setSearchTerm(search || "")} + onSearch={(search: string | undefined) => setSearchTerm(search || "")} onClear={() => { setSearchTerm("") }} @@ -95,7 +95,7 @@ const VulnerabilitiesTabContent = ({ - {issuesPromise && ( + {issuesPromise != null && ( )} - {issuesPromise && ( + {issuesPromise != null && ( setSearchTerm(search || "")} + onSearch={(search: string | undefined) => setSearchTerm(search || "")} onClear={() => setSearchTerm("")} /> @@ -184,7 +184,7 @@ const RemediatedVulnerabilitiesTabContent = ({ - {issuesPromise && ( + {issuesPromise != null && ( )} - {issuesPromise && ( + {issuesPromise != null && ( { const list: VulnerabilitiesTabValue = index === 1 ? "remediated" : "active" - navigate({ + void navigate({ to: "/services/$service/images/$image", params: { service, image: image.repository }, search: { vulnerabilitiesList: list, vulRemediations: list === "remediated" ? vulRemediations : undefined }, @@ -255,7 +255,7 @@ export const ImageIssuesList = ({ ) const handleRemediationPanelVulnerabilityChange = useCallback( (cve: string | null) => { - navigate({ + void navigate({ to: "/services/$service/images/$image", params: { service, image: image.repository }, search: { vulnerabilitiesList: "remediated", vulRemediations: cve ?? undefined }, @@ -340,23 +340,32 @@ export const ImageIssuesList = ({ }, }) - const handleFalsePositiveSuccess = useCallback((cveNumber: string) => { - setVulnerabilitiesSuccessMessage( - `Vulnerability ${cveNumber} has been marked as a false positive and moved to the Remediated list.` - ) - }, []) + const handleFalsePositiveSuccess = useCallback( + (cveNumber: string) => { + setVulnerabilitiesSuccessMessage( + `Vulnerability ${cveNumber} has been marked as a false positive and moved to the Remediated list.` + ) + }, + [setVulnerabilitiesSuccessMessage] + ) - const handleRiskAcceptanceSuccess = useCallback((cveNumber: string) => { - setVulnerabilitiesSuccessMessage( - `Vulnerability ${cveNumber} has been accepted as a risk and moved to the Remediated list.` - ) - }, []) + const handleRiskAcceptanceSuccess = useCallback( + (cveNumber: string) => { + setVulnerabilitiesSuccessMessage( + `Vulnerability ${cveNumber} has been accepted as a risk and moved to the Remediated list.` + ) + }, + [setVulnerabilitiesSuccessMessage] + ) - const handleMitigateManuallySuccess = useCallback((cveNumber: string) => { - setVulnerabilitiesSuccessMessage( - `Vulnerability ${cveNumber} has been manually mitigated and moved to the Remediated list.` - ) - }, []) + const handleMitigateManuallySuccess = useCallback( + (cveNumber: string) => { + setVulnerabilitiesSuccessMessage( + `Vulnerability ${cveNumber} has been manually mitigated and moved to the Remediated list.` + ) + }, + [setVulnerabilitiesSuccessMessage] + ) const handleRemediatedTabRemediationSuccess = useCallback( (cveNumber: string, remediationType: RemediationTypeValues) => { @@ -369,7 +378,7 @@ export const ImageIssuesList = ({ const text = `Vulnerability ${cveNumber} has been marked as ${remediationTypeLabel}.` setRemediatedSuccessMessage(text) }, - [] + [setRemediatedSuccessMessage] ) return ( diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/remediationCacheTypes.ts b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/remediationCacheTypes.ts new file mode 100644 index 0000000000..64657ac610 --- /dev/null +++ b/apps/heureka/src/components/Service/ImageDetails/ImageIssuesList/remediationCacheTypes.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** Shape of the React Query cache entry for "remediations" queries. */ +export type RemediationsCache = { + data?: { + Remediations?: { + edges?: Array<{ node?: { id?: string } }> | null + totalCount?: number | null + } + } +} + +/** Query key filter shape for "remediations" predicate lookups. */ +export type RemediationQueryFilter = { + service?: string[] + image?: string[] + vulnerability?: string[] +} diff --git a/apps/heureka/src/components/Service/ImageDetails/ImageVersionsList/index.tsx b/apps/heureka/src/components/Service/ImageDetails/ImageVersionsList/index.tsx index 9e732d1eaa..1c388cc8c6 100644 --- a/apps/heureka/src/components/Service/ImageDetails/ImageVersionsList/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/ImageVersionsList/index.tsx @@ -25,7 +25,7 @@ export const ImageVersionsList = ({ versions, service, imageRepository }: ImageV const hasMoreVersions = versions.length > VERSIONS_INITIAL const handleVersionClick = (version: string) => { - navigate({ + void navigate({ to: "/services/$service/images/$image/versions/$version", params: { service, diff --git a/apps/heureka/src/components/Service/ImageDetails/RemediationModal/RemediationModal.test.tsx b/apps/heureka/src/components/Service/ImageDetails/RemediationModal/RemediationModal.test.tsx index 147dc63e47..8693a64236 100644 --- a/apps/heureka/src/components/Service/ImageDetails/RemediationModal/RemediationModal.test.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/RemediationModal/RemediationModal.test.tsx @@ -16,10 +16,24 @@ import { RemediationTypeValues } from "../../../../generated/graphql" // Mock DateTimePicker so tests can set dates without flatpickr DOM interaction vi.mock("@cloudoperators/juno-ui-components", async (importActual) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const actual = await importActual() + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...actual, - DateTimePicker: ({ label, onChange, required, invalid, errortext }: any) => ( + DateTimePicker: ({ + label, + onChange, + required, + invalid, + errortext, + }: { + label: string + onChange: (dates: Date[]) => void + required?: boolean + invalid?: boolean + errortext?: string + }) => (
(onChange as Function)?.(e.target.value ? [new Date(e.target.value)] : [])} + onChange={(e) => onChange(e.target.value ? [new Date(e.target.value)] : [])} /> - {invalid && errortext ? {errortext as string} : null} + {invalid && errortext ? {errortext} : null}
), } diff --git a/apps/heureka/src/components/Service/ImageDetails/RemediationModal/index.tsx b/apps/heureka/src/components/Service/ImageDetails/RemediationModal/index.tsx index 8feef34083..3bdca7aade 100644 --- a/apps/heureka/src/components/Service/ImageDetails/RemediationModal/index.tsx +++ b/apps/heureka/src/components/Service/ImageDetails/RemediationModal/index.tsx @@ -61,8 +61,15 @@ export const RemediationModal: React.FC = ({ showSourceTicket = false, sourceTicketRequired = false, }) => { - const auth = useAuth() - const authUserId = auth.status === "authenticated" ? auth.userId || auth.userName : null + // 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) @@ -163,7 +170,7 @@ export const RemediationModal: React.FC = ({