Skip to content

Commit 5261dfd

Browse files
authored
chore(doop): prevent mutations form original violationGroups when filtering improve debouncing of search filter (#1199)
* chore(doop): remove headline to allign with the other plugins * chore(doop): debounce filter correctly and clean timouts * chore(doop): No mutate original violationGroups * chore(doop): added changeset * chore(doop): added headers * chore(doop): removed log line
1 parent d3f9442 commit 5261dfd

8 files changed

Lines changed: 411 additions & 233 deletions

File tree

.changeset/lovely-hats-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudoperators/juno-app-doop": patch
3+
---
4+
5+
Prevent mutations form original violationGroups data when filtering and improved debouncing of search filter

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ vite.config.*.timestamp-*
2020
.env
2121
act_*.json
2222
.secret
23+
24+
.out-of-code-insights

apps/doop/src/App.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,6 @@ const App = (props: AppProps = {}) => {
107107
return (
108108
<MessagesProvider>
109109
<AppShell pageHeader={`Doop`} embedded={props.embedded === true}>
110-
<ContentHeading
111-
// @ts-expect-error TS(2339) FIXME: Property 'displayName' does not exist on type '{}'... Remove this comment to see the full error message
112-
heading={`Decentralized Observer of Policies ${props.displayName ? ` - ${props.displayName}` : ""}`}
113-
/>
114110
<QueryClientProvider client={queryClient}>
115111
<StrictMode>
116112
<RouterProvider basepath={props.basePath || "/"} router={router} />

apps/doop/src/components/filters/FilterSelect.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useState } from "react"
6+
import React, { useState, useRef } from "react"
77

88
import {
99
Button,
@@ -30,22 +30,23 @@ const FilterSelect = () => {
3030
const { add: addFilter, removeAll, setSearchTerm } = useFiltersActions()
3131
const searchValue = useFiltersSearchTerm()
3232
const activeFilters = useFiltersActive() || []
33+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
3334

34-
const handleSearchChange = (value: any) => {
35-
// debounce setSearchTerm to avoid unnecessary re-renders
36-
const debouncedSearchTerm = setTimeout(() => {
37-
setSearchTerm(value.target.value.trim())
35+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
36+
const term = e.target.value.trim()
37+
38+
if (debounceRef.current) clearTimeout(debounceRef.current)
39+
40+
debounceRef.current = setTimeout(() => {
41+
setSearchTerm(term)
3842
navigate({
3943
to: "/violations",
4044
search: (prev) => ({
4145
...prev,
42-
searchTerm: value.target.value.trim(),
46+
searchTerm: term,
4347
}),
4448
})
4549
}, 500)
46-
47-
// clear timeout if we have a new value
48-
return () => clearTimeout(debouncedSearchTerm)
4950
}
5051

5152
const handleFilterValueChange = (value: string) => {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { filterViolations } from "./filterViolations"
7+
8+
describe("filterViolations", () => {
9+
let originalViolationGroups: any[]
10+
let clusterIdentities: any[]
11+
12+
beforeEach(() => {
13+
originalViolationGroups = [
14+
{
15+
id: "vg1",
16+
constraints: [
17+
{
18+
metadata: { severity: "debug", name: "cpu" },
19+
violation_groups: [{ pattern: { object_identity: { service: "alpha" } }, instances: [{ cluster: "c1" }] }],
20+
},
21+
{
22+
metadata: { severity: "critical", name: "memory" },
23+
violation_groups: [{ pattern: { object_identity: { service: "beta" } }, instances: [{ cluster: "c2" }] }],
24+
},
25+
],
26+
},
27+
{
28+
id: "vg2",
29+
constraints: [
30+
{
31+
metadata: { severity: "warning", name: "disk" },
32+
violation_groups: [{ pattern: { object_identity: { service: "gamma" } }, instances: [{ cluster: "c1" }] }],
33+
},
34+
],
35+
},
36+
]
37+
38+
clusterIdentities = [
39+
{ cluster: "c1", region: "eu" },
40+
{ cluster: "c2", region: "us" },
41+
]
42+
})
43+
44+
it("filters out debug severities when showDebugSeverities is false", () => {
45+
const inputCopy = JSON.parse(JSON.stringify(originalViolationGroups))
46+
const result = filterViolations({
47+
violationGroups: inputCopy,
48+
clusterIdentities,
49+
activeFilters: [],
50+
searchTerm: "",
51+
showDebugSeverities: false,
52+
})
53+
54+
expect(result).toHaveLength(2)
55+
expect(result[0].constraints).toHaveLength(1)
56+
expect(result[0].constraints[0].metadata.severity).toBe("critical")
57+
58+
// Check original input is not modified
59+
expect(inputCopy[0].constraints).toHaveLength(2)
60+
})
61+
62+
it("filters by active filters (severity and check)", () => {
63+
const activeFilters = [
64+
{ key: "violation_group:severity", value: "warning" },
65+
{ key: "check:service", value: "gamma" },
66+
]
67+
68+
const inputCopy = JSON.parse(JSON.stringify(originalViolationGroups))
69+
const result = filterViolations({
70+
violationGroups: inputCopy,
71+
clusterIdentities,
72+
activeFilters,
73+
searchTerm: "",
74+
showDebugSeverities: true,
75+
})
76+
77+
expect(result).toHaveLength(1)
78+
expect(result[0].id).toBe("vg2")
79+
expect(result[0].constraints[0].metadata.name).toBe("disk")
80+
81+
// Original input is intact
82+
expect(inputCopy[1].constraints[0].metadata.name).toBe("disk")
83+
})
84+
85+
it("filters by search term", () => {
86+
const inputCopy = JSON.parse(JSON.stringify(originalViolationGroups))
87+
const result = filterViolations({
88+
violationGroups: inputCopy,
89+
clusterIdentities,
90+
activeFilters: [],
91+
searchTerm: "memory",
92+
showDebugSeverities: true,
93+
})
94+
95+
expect(result).toHaveLength(1)
96+
expect(result[0].constraints[0].metadata.name).toBe("memory")
97+
98+
// Original input unchanged
99+
expect(inputCopy[0].constraints).toHaveLength(2)
100+
})
101+
102+
it("applies all filters together", () => {
103+
const activeFilters = [{ key: "check:service", value: "beta" }]
104+
const inputCopy = JSON.parse(JSON.stringify(originalViolationGroups))
105+
const result = filterViolations({
106+
violationGroups: inputCopy,
107+
clusterIdentities,
108+
activeFilters,
109+
searchTerm: "memory",
110+
showDebugSeverities: false,
111+
})
112+
113+
expect(result).toHaveLength(1)
114+
expect(result[0].constraints[0].metadata.name).toBe("memory")
115+
116+
// Original input unchanged !!!
117+
expect(inputCopy[0].constraints.map((c: any) => c.metadata.name)).toEqual(["cpu", "memory"])
118+
})
119+
120+
it("returns empty array if nothing matches", () => {
121+
const result = filterViolations({
122+
violationGroups: JSON.parse(JSON.stringify(originalViolationGroups)),
123+
clusterIdentities,
124+
activeFilters: [{ key: "check:service", value: "nonexistent" }],
125+
searchTerm: "nope",
126+
showDebugSeverities: false,
127+
})
128+
129+
expect(result).toEqual([])
130+
})
131+
})

0 commit comments

Comments
 (0)