Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/heureka-eslint-vite-react-ts-migration.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion apps/heureka/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import { CodegenConfig } from "@graphql-codegen/cli"
//@ts-ignore
import * as dotenv from "dotenv"

// Load environment variables from .env file
Expand Down
50 changes: 22 additions & 28 deletions apps/heureka/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
Comment thread
hodanoori marked this conversation as resolved.
{
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"],
},
]
7 changes: 7 additions & 0 deletions apps/heureka/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
Expand Down
6 changes: 6 additions & 0 deletions apps/heureka/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
},
})
Expand All @@ -102,7 +106,9 @@ const App = (props: AppProps) => {
<AuthProvider
embedded={props.embedded && !!props.auth}
auth={
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
props.auth &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
("getSnapshot" in props.auth ? props.auth : { getSnapshot: () => props.auth as AuthState })
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -83,18 +84,18 @@ 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
if (filter?.vulnerability && !filter.vulnerability.includes(cveNumber)) return false
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: {
Expand Down Expand Up @@ -158,7 +159,7 @@ export const IssuesDataRow = ({
</Stack>
</DataGridCell>
{showFalsePositiveAction && (
<DataGridCell className="cursor-default interactive" onClick={(e) => e.stopPropagation()}>
<DataGridCell className="cursor-default interactive" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
{isSubmitting ? (
<Spinner variant="primary" size="small" className="ml-auto" />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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(
<Suspense fallback={<div>Loading...</div>}>
Expand All @@ -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)
})

Expand All @@ -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(
<Suspense fallback={<div>Loading...</div>}>
Expand All @@ -226,7 +228,7 @@ describe("IssuesDataRows — risk acceptance", () => {
const issuesPromise = makeImagesPromise(["CVE-2024-9999"])
const remediationsPromise = makeRemediationsPromise([])

await act(async () => {
act(() => {
renderWithRouter(issuesPromise, remediationsPromise)
})

Expand All @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -74,20 +75,18 @@ 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
if (filter?.vulnerability && !filter.vulnerability.includes(cveNumber)) return false
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: {
Expand Down Expand Up @@ -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()}
>
<Stack gap="1.5" alignment="center">
<Icon icon="openInNew" size="16" />
Expand All @@ -159,7 +158,7 @@ export const RemediatedIssueDataRow = ({
{issue.description && needsExpansion && (
<a
href="#"
onClick={(e) => {
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
toggleDescription(e)
}}
Expand All @@ -173,7 +172,7 @@ export const RemediatedIssueDataRow = ({
)}
</Stack>
</DataGridCell>
<DataGridCell className="cursor-default interactive" onClick={(e) => e.stopPropagation()}>
<DataGridCell className="cursor-default interactive" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
{isSubmitting ? (
<Spinner variant="primary" size="small" className="ml-auto" />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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)
})

Expand All @@ -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)
})

Expand Down
Loading
Loading