Skip to content

Commit 94cd52d

Browse files
authored
ACM-16425 Add-labels-column-and-filtering-to-Discovered-policies-table (stolostron#4166)
* ACM-16425 Add-labels-column-and-filtering-to-Discovered-policies-table Signed-off-by: John Swanke <jswanke@redhat.com> * tweaks Signed-off-by: John Swanke <jswanke@redhat.com> * fix test Signed-off-by: John Swanke <jswanke@redhat.com> * bug Signed-off-by: John Swanke <jswanke@redhat.com> * fix tests Signed-off-by: John Swanke <jswanke@redhat.com> * coverage Signed-off-by: John Swanke <jswanke@redhat.com> * issue Signed-off-by: John Swanke <jswanke@redhat.com> * tweak Signed-off-by: John Swanke <jswanke@redhat.com> * respond to comments Signed-off-by: John Swanke <jswanke@redhat.com> --------- Signed-off-by: John Swanke <jswanke@redhat.com>
1 parent 8f08620 commit 94cd52d

File tree

13 files changed

+395
-74
lines changed

13 files changed

+395
-74
lines changed

frontend/public/locales/en/translation.json

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"{{count}} clusters with violations_plural": "{{count}} clusters with violations",
2121
"{{count}} clusters without violations": "{{count}} cluster without violations",
2222
"{{count}} clusters without violations_plural": "{{count}} clusters without violations",
23+
"{{count}} labels": "{{count}} label",
24+
"{{count}} labels_plural": "{{count}} labels",
2325
"{{count}} more": "{{count}} more",
2426
"{{count}} more_plural": "{{count}} more",
2527
"{{count}} selected": "{{count}} selected",

frontend/src/components/HighlightSearchText.tsx

+42-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,51 @@
11
/* Copyright Contributors to the Open Cluster Management project */
2+
const MAX_LABEL_WIDTH = 28
23

3-
export function HighlightSearchText(props: Readonly<{ text?: string; searchText?: string }>) {
4-
const { text, searchText } = props
5-
return (
6-
<>
7-
{getSlicedText(text, searchText).map((idSplit) => (
8-
<span key={idSplit.text} className={idSplit.isBold ? 'pf-v5-u-font-weight-bold' : ''}>
9-
{idSplit.text}
10-
</span>
11-
))}
12-
</>
13-
)
4+
export function HighlightSearchText(props: Readonly<{ text?: string; searchText?: string; isTruncate?: boolean }>) {
5+
const { text, searchText, isTruncate } = props
6+
const segments = getSlicedText(text, searchText)
7+
if (segments.length > 1) {
8+
const isTruncateLabel = isTruncate && text && text.length > MAX_LABEL_WIDTH
9+
return (
10+
<>
11+
{segments.map((seg, inx) => {
12+
return (
13+
<span
14+
key={Number(inx)}
15+
style={
16+
seg.isBold
17+
? {
18+
color: 'var(--pf-v5-global--link--Color)',
19+
textDecoration: 'underline',
20+
background: 'none',
21+
fontWeight: 600,
22+
}
23+
: {}
24+
}
25+
>
26+
{isTruncateLabel && !seg.isBold ? '...' : seg.text}
27+
</span>
28+
)
29+
})}
30+
</>
31+
)
32+
} else if (isTruncate) {
33+
return truncate(text)
34+
}
35+
return text
1436
}
1537

1638
interface SlicedText {
1739
text: string
1840
isBold: boolean
1941
}
2042

43+
export const truncate = (label?: string) => {
44+
return label && label?.length > MAX_LABEL_WIDTH
45+
? label.slice(0, MAX_LABEL_WIDTH / 3) + '..' + label.slice((-MAX_LABEL_WIDTH * 2) / 3)
46+
: label
47+
}
48+
2149
const getSlicedText = (itemId: string = '', filterText: string = ''): SlicedText[] => {
2250
const slicedText = []
2351
if (filterText) {
@@ -84,7 +112,7 @@ const lcs = (str1: string, str2: string) => {
84112
sequence += str1[i]
85113
} else {
86114
lastSubsBegin = thisSubsBegin
87-
sequence = str1.substring(lastSubsBegin, i + 1) // - lastSubsBegin);
115+
sequence = str1.substring(lastSubsBegin, i + 1)
88116
}
89117
}
90118
}
@@ -109,7 +137,7 @@ const lcss = (str1: string, str2: string) => {
109137
let match
110138
matches = []
111139
let res = lcs(item, find)
112-
if (res.length > 1) {
140+
if (res.length > 0) {
113141
// escape search pattern (ex: if there's a period, escape to \\.)
114142
const { length: len } = res
115143
res = res.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
@@ -129,7 +157,7 @@ const lcss = (str1: string, str2: string) => {
129157
]
130158
// so that we don't constantly find the same matches over and over again
131159
// we replace the matching characters with spaces
132-
// iow the above strings become '987 87' and '873 ' so that 456 isn't found again
160+
// iow the above strings (873456 and 98745687) become '987 87' and '873 ' so that 456 isn't found again
133161
item = item.replace(regex, () => ' '.repeat(len))
134162
find = find.replace(regex, () => ' '.repeat(len))
135163
}

frontend/src/routes/Governance/common/util.tsx

+30-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { IAlertContext } from '../../../ui-components'
2727
import { useTranslation } from '../../../lib/acm-i18next'
2828
import { PolicyTableItem } from '../policies/Policies'
2929
import { LostChangesContext } from '../../../components/LostChanges'
30-
import { DiscoveredPolicyItem } from '../discovered/useFetchPolicies'
30+
import { DiscoverdPolicyTableItem, DiscoveredPolicyItem } from '../discovered/useFetchPolicies'
3131
import GatekeeperSvg from '../../../logos/gatekeeper.svg'
3232
import OcmSvg from '../../../logos/ocm.svg'
3333
import Kubernetes from '../../../logos/kubernetes.svg'
@@ -763,6 +763,35 @@ export function getEngineWithSvg(apiGroup: string): JSX.Element {
763763
)
764764
}
765765

766+
export function parseDiscoveredPolicyLabels(data: DiscoverdPolicyTableItem[]) {
767+
const allLabels = new Set<string>()
768+
const labelMap: Record<string, { pairs: Record<string, string>; labels: string[] }> = {}
769+
data?.forEach((item) => {
770+
item.policies.forEach(({ label }) => {
771+
const labels: string[] = []
772+
const pairs: Record<string, string> = {}
773+
label?.split(';').forEach((lbl) => {
774+
labels.push(lbl.trim())
775+
const [key, value] = lbl.split('=').map((seg) => seg.trim())
776+
if (
777+
!['cluster-name', 'cluster-namespace'].includes(key) &&
778+
!key.startsWith('policy.open-cluster-management.io/')
779+
) {
780+
pairs[key] = value
781+
allLabels.add(lbl.trim())
782+
}
783+
})
784+
labelMap[item.id] = { pairs, labels }
785+
})
786+
})
787+
return {
788+
labelMap,
789+
labelOptions: Array.from(allLabels).map((lbl) => {
790+
return { label: lbl, value: lbl }
791+
}),
792+
}
793+
}
794+
766795
/* istanbul ignore next */
767796
export const parseDiscoveredPolicies = (data: any): any => {
768797
return JSON.parse(JSON.stringify(data), (k, v: string) => {

frontend/src/routes/Governance/discovered/ByCluster/DiscoveredByClusterPage.test.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('DiscoveredByClusterPage', () => {
6767
},
6868
],
6969
err: undefined,
70+
labelData: undefined,
7071
})
7172
render(
7273
<RecoilRoot
@@ -151,6 +152,7 @@ describe('DiscoveredByClusterPage', () => {
151152
},
152153
],
153154
err: undefined,
155+
labelData: undefined,
154156
})
155157
render(
156158
<RecoilRoot
@@ -256,6 +258,7 @@ describe('DiscoveredByClusterPage', () => {
256258
},
257259
],
258260
err: undefined,
261+
labelData: undefined,
259262
})
260263
const { container } = render(
261264
<RecoilRoot
@@ -439,6 +442,7 @@ describe('DiscoveredByClusterPage', () => {
439442
},
440443
],
441444
err: undefined,
445+
labelData: undefined,
442446
})
443447
render(
444448
<RecoilRoot
@@ -495,6 +499,7 @@ describe('DiscoveredByClusterPage', () => {
495499
isFetching: false,
496500
data: undefined,
497501
err: undefined,
502+
labelData: undefined,
498503
})
499504
render(
500505
<RecoilRoot
@@ -531,6 +536,7 @@ describe('DiscoveredByClusterPage', () => {
531536
isFetching: false,
532537
data: undefined,
533538
err: undefined,
539+
labelData: undefined,
534540
})
535541
render(
536542
<RecoilRoot
@@ -567,6 +573,7 @@ describe('DiscoveredByClusterPage', () => {
567573
isFetching: false,
568574
data: undefined,
569575
err: undefined,
576+
labelData: undefined,
570577
})
571578
render(
572579
<RecoilRoot
@@ -603,6 +610,7 @@ describe('DiscoveredByClusterPage', () => {
603610
isFetching: false,
604611
data: undefined,
605612
err: { message: 'Error getting fetching data' } as ApolloError,
613+
labelData: undefined,
606614
})
607615
render(
608616
<RecoilRoot
@@ -639,6 +647,7 @@ describe('DiscoveredByClusterPage', () => {
639647
isFetching: true,
640648
data: undefined,
641649
err: { message: 'Error getting fetching data' } as ApolloError,
650+
labelData: undefined,
642651
})
643652
render(
644653
<RecoilRoot
@@ -735,6 +744,7 @@ describe('DiscoveredByClusterPage', () => {
735744
},
736745
],
737746
err: undefined,
747+
labelData: undefined,
738748
})
739749
render(
740750
<RecoilRoot

frontend/src/routes/Governance/discovered/DiscoveredPolicies.test.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ describe('useFetchPolicies custom hook', () => {
106106
},
107107
],
108108
err: undefined,
109+
labelData: undefined,
109110
})
110111

111112
render(
@@ -160,6 +161,7 @@ describe('useFetchPolicies custom hook', () => {
160161
isFetching: false,
161162
data: undefined,
162163
err: { message: 'Error getting fetching data' } as ApolloError,
164+
labelData: undefined,
163165
})
164166

165167
render(
@@ -176,6 +178,7 @@ describe('useFetchPolicies custom hook', () => {
176178
isFetching: true,
177179
data: undefined,
178180
err: { message: 'Error getting fetching data' } as ApolloError,
181+
labelData: undefined,
179182
})
180183

181184
render(
@@ -250,6 +253,7 @@ describe('useFetchPolicies custom hook', () => {
250253
},
251254
],
252255
err: undefined,
256+
labelData: undefined,
253257
})
254258

255259
const { baseElement } = render(
@@ -393,6 +397,7 @@ describe('useFetchPolicies custom hook', () => {
393397
},
394398
],
395399
err: undefined,
400+
labelData: undefined,
396401
})
397402

398403
const { container } = render(
@@ -557,6 +562,7 @@ describe('useFetchPolicies custom hook', () => {
557562
},
558563
],
559564
err: undefined,
565+
labelData: undefined,
560566
})
561567

562568
render(
@@ -578,7 +584,7 @@ describe('useFetchPolicies custom hook', () => {
578584

579585
expect(
580586
screen.getByRole('row', {
581-
name: /require-owner-labels Kyverno ClusterPolicy Audit Medium 1 Local/,
587+
name: /require-owner-labels Kyverno ClusterPolicy - Audit Medium 1 Local/,
582588
})
583589
).toBeInTheDocument()
584590

frontend/src/routes/Governance/discovered/DiscoveredPolicies.tsx

+30-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
IAcmTableColumn,
88
AcmAlert,
99
AcmTable,
10+
AcmLabels,
1011
} from '../../../ui-components'
1112
import { DiscoverdPolicyTableItem, useFetchPolicies } from './useFetchPolicies'
1213
import { useTranslation } from '../../../lib/acm-i18next'
@@ -24,6 +25,7 @@ import {
2425
severityCell,
2526
} from './ByCluster/common'
2627
import { ClusterPolicyViolationIcons2 } from '../components/ClusterPolicyViolations'
28+
import { exportObjectString } from '../../../resources/utils'
2729

2830
function nameCell(item: DiscoverdPolicyTableItem): ReactNode {
2931
return (
@@ -67,8 +69,17 @@ function clusterCell(item: DiscoverdPolicyTableItem): ReactNode | string {
6769
return '-'
6870
}
6971

72+
function labelsCell(
73+
item: DiscoverdPolicyTableItem,
74+
labelMap: Record<string, { pairs?: Record<string, string>; labels?: string[] }> | undefined
75+
): ReactNode | string {
76+
const labels = labelMap?.[item.id]?.pairs
77+
return <AcmLabels labels={labels} isCompact={true} />
78+
}
79+
7080
export default function DiscoveredPolicies() {
71-
const { isFetching, data, err } = useFetchPolicies()
81+
const { isFetching, data, labelData, err } = useFetchPolicies()
82+
const { labelOptions, labelMap } = labelData || {}
7283
const { t } = useTranslation()
7384

7485
const discoveredPoliciesCols = useMemo<IAcmTableColumn<DiscoverdPolicyTableItem>[]>(
@@ -99,6 +110,13 @@ export default function DiscoveredPolicies() {
99110
id: 'kind',
100111
exportContent: (item: DiscoverdPolicyTableItem) => item.kind,
101112
},
113+
{
114+
header: t('table.labels'),
115+
cell: (item: DiscoverdPolicyTableItem) => labelsCell(item, labelMap),
116+
exportContent: (item: DiscoverdPolicyTableItem) => {
117+
return exportObjectString(labelMap ? labelMap[item.id]?.pairs : {})
118+
},
119+
},
102120
{
103121
header: t('Response action'),
104122
cell: 'responseAction',
@@ -139,7 +157,7 @@ export default function DiscoveredPolicies() {
139157
exportContent: getSourceExportCSV,
140158
},
141159
],
142-
[t]
160+
[labelMap, t]
143161
)
144162

145163
const filters = useMemo<ITableFilter<DiscoverdPolicyTableItem>[]>(
@@ -207,6 +225,14 @@ export default function DiscoveredPolicies() {
207225
},
208226
getResponseActionFilter(t),
209227
getSeverityFilter(t),
228+
{
229+
id: 'label',
230+
label: t('Label'),
231+
options: labelOptions || [],
232+
tableFilterFn: (selectedValues, item) => {
233+
return selectedValues.some((val) => labelMap?.[item.id].labels.includes(val))
234+
},
235+
},
210236
{
211237
id: 'source',
212238
label: t('Source'),
@@ -216,7 +242,7 @@ export default function DiscoveredPolicies() {
216242
},
217243
},
218244
],
219-
[data, t]
245+
[data, labelMap, labelOptions, t]
220246
)
221247

222248
if (isFetching) {
@@ -238,6 +264,7 @@ export default function DiscoveredPolicies() {
238264
items={data}
239265
emptyState={<AcmEmptyState title={t(`You don't have any policies.`)} message={t('There are no policies.')} />}
240266
filters={filters}
267+
secondaryFilterIds={['label']}
241268
showExportButton
242269
exportFilePrefix="discoveredPolicies"
243270
/>

0 commit comments

Comments
 (0)