@skyhook-io/k8s-ui 1.8.11 → 1.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/applications/ApplicationDetail.test.tsx +136 -8
- package/src/components/applications/ApplicationDetail.tsx +1215 -487
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +10 -3
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
- package/src/components/gitops/insights/insights-helpers.ts +5 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
- package/src/components/issues/IssuesView.tsx +55 -29
- package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
- package/src/components/issues/ResourceIssuesSection.tsx +21 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.tsx +109 -19
- package/src/components/resources/column-filter-serialization.test.ts +79 -1
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils.ts +94 -25
- package/src/components/shared/ResourceActionsBar.tsx +54 -22
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +7 -3
- package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyGraph.tsx +76 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +23 -15
- package/src/components/topology/index.ts +2 -0
- package/src/components/ui/Badge.tsx +1 -1
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/Tooltip.test.tsx +21 -0
- package/src/components/ui/Tooltip.tsx +9 -12
- package/src/components/ui/drawer-components.tsx +9 -6
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +24 -3
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +1 -0
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/api-resources.test.ts +36 -1
- package/src/utils/api-resources.ts +16 -0
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/applications.test.ts +34 -0
- package/src/utils/applications.ts +25 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/resource-hierarchy.test.ts +35 -1
- package/src/utils/resource-hierarchy.ts +20 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { ResourceIssuesSection } from './ResourceIssuesSection'
|
|
3
|
+
import { ResourceIssuesSection, sameResource } from './ResourceIssuesSection'
|
|
4
|
+
import { IssueRow } from './IssuesView'
|
|
4
5
|
import type { Issue } from './types'
|
|
5
6
|
|
|
6
7
|
const issue: Issue = {
|
|
@@ -37,4 +38,35 @@ describe('ResourceIssuesSection', () => {
|
|
|
37
38
|
expect(html).not.toContain('Context')
|
|
38
39
|
expect(html).not.toContain('Blocked pods')
|
|
39
40
|
})
|
|
41
|
+
|
|
42
|
+
it('shows the Subject deep-link by default', () => {
|
|
43
|
+
const html = renderToString(<IssueRow issue={issue} open onToggle={() => {}} onResourceClick={() => {}} />)
|
|
44
|
+
expect(html).toContain('Subject')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('suppresses the Subject deep-link when the issue subject is the embedding resource', () => {
|
|
48
|
+
// apiKind (plural) must still resolve to the same identity as the wire kind.
|
|
49
|
+
const html = renderToString(<IssueRow issue={issue} open onToggle={() => {}} onResourceClick={() => {}} hideSubject />)
|
|
50
|
+
expect(html).not.toContain('Subject')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('sameResource', () => {
|
|
55
|
+
const subject = { kind: 'PersistentVolumeClaim', namespace: 'demo', name: 'data' }
|
|
56
|
+
|
|
57
|
+
it('matches across singular kind vs plural API name', () => {
|
|
58
|
+
expect(sameResource(subject, { kind: 'persistentvolumeclaims', namespace: 'demo', name: 'data' })).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('treats a missing namespace as empty', () => {
|
|
62
|
+
expect(sameResource({ kind: 'Node', name: 'n1' }, { kind: 'nodes', namespace: '', name: 'n1' })).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('does not match a different name', () => {
|
|
66
|
+
expect(sameResource(subject, { kind: 'persistentvolumeclaims', namespace: 'demo', name: 'other' })).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('does not match a different kind', () => {
|
|
70
|
+
expect(sameResource(subject, { kind: 'configmaps', namespace: 'demo', name: 'data' })).toBe(false)
|
|
71
|
+
})
|
|
40
72
|
})
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
import { AlertTriangle } from 'lucide-react'
|
|
3
|
+
import { kindToPlural } from '../../utils/navigation'
|
|
3
4
|
import { IssueRow } from './IssuesView'
|
|
4
|
-
import { compareIssues, type Issue, type IssueResourceRef } from './types'
|
|
5
|
+
import { compareIssues, subjectRef, type Issue, type IssueResourceRef } from './types'
|
|
5
6
|
|
|
6
7
|
export function ResourceIssuesSection({
|
|
7
8
|
issues,
|
|
8
9
|
onResourceClick,
|
|
10
|
+
subjectResource,
|
|
9
11
|
}: {
|
|
10
12
|
issues: Issue[] | undefined
|
|
11
13
|
/** When provided, related resources in a causal link become clickable. */
|
|
12
14
|
onResourceClick?: (ref: IssueResourceRef) => void
|
|
15
|
+
/** The resource this section is embedded under. When an issue's subject IS
|
|
16
|
+
* this resource, its redundant "Subject" deep-link is suppressed — the drawer
|
|
17
|
+
* header already names it. Omit (e.g. the standalone fleet queue) to always
|
|
18
|
+
* show the subject. */
|
|
19
|
+
subjectResource?: IssueResourceRef
|
|
13
20
|
}) {
|
|
14
21
|
const sorted = useMemo(() => [...(issues ?? [])].sort(compareIssues), [issues])
|
|
15
22
|
const [openId, setOpenId] = useState<string | null>(null)
|
|
@@ -32,6 +39,7 @@ export function ResourceIssuesSection({
|
|
|
32
39
|
open={openId === key}
|
|
33
40
|
onToggle={() => setOpenId((cur) => (cur === key ? null : key))}
|
|
34
41
|
onResourceClick={onResourceClick}
|
|
42
|
+
hideSubject={subjectResource ? sameResource(subjectRef(issue), subjectResource) : false}
|
|
35
43
|
/>
|
|
36
44
|
)
|
|
37
45
|
})}
|
|
@@ -40,6 +48,18 @@ export function ResourceIssuesSection({
|
|
|
40
48
|
)
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
// Identity match tolerant of singular/plural + casing on kind (the subject ref
|
|
52
|
+
// carries the wire kind, the host typically passes the plural API name) — group
|
|
53
|
+
// is deliberately ignored: a per-resource issue feed is already scoped to one
|
|
54
|
+
// resource, and kind+namespace+name uniquely identifies it there.
|
|
55
|
+
export function sameResource(a: IssueResourceRef, b: IssueResourceRef): boolean {
|
|
56
|
+
return (
|
|
57
|
+
a.name === b.name &&
|
|
58
|
+
(a.namespace ?? '') === (b.namespace ?? '') &&
|
|
59
|
+
kindToPlural(a.kind).toLowerCase() === kindToPlural(b.kind).toLowerCase()
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
function issueKey(issue: Issue): string {
|
|
44
64
|
return `${issue.cluster_id ?? ''}:${issue.id}`
|
|
45
65
|
}
|
|
@@ -101,6 +101,7 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
101
101
|
gitops_operation_failed: 'GitOps operation failed',
|
|
102
102
|
gitops_out_of_sync: 'GitOps out of sync',
|
|
103
103
|
gitops_health_degraded: 'GitOps health degraded',
|
|
104
|
+
gitops_stale: 'GitOps comparisons stale',
|
|
104
105
|
helm_release_failed: 'Helm release failed',
|
|
105
106
|
webhook_backend_down: 'Webhook backend down',
|
|
106
107
|
control_plane_not_ready: 'Control plane not ready',
|
|
@@ -33,7 +33,7 @@ import { ResourceBar } from '../ui/ResourceBar'
|
|
|
33
33
|
import type { SelectedResource, APIResource } from '../../types'
|
|
34
34
|
import { isForbiddenError } from '../../types/fetch-error'
|
|
35
35
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
36
|
-
import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
|
|
36
|
+
import { categorizeResources, CORE_RESOURCES, findAPIResourceForRoute } from '../../utils/api-resources'
|
|
37
37
|
import {
|
|
38
38
|
getPodStatus,
|
|
39
39
|
getPodRestarts,
|
|
@@ -129,6 +129,7 @@ import {
|
|
|
129
129
|
getCellFilterValue,
|
|
130
130
|
parseColumnFilters,
|
|
131
131
|
serializeColumnFilters,
|
|
132
|
+
parseColumnFilterExcludes,
|
|
132
133
|
podMatchesProblemCategory,
|
|
133
134
|
SEVERITY_DOT_COLOR,
|
|
134
135
|
} from './resource-utils'
|
|
@@ -2036,13 +2037,9 @@ function getInitialKindFromURL(
|
|
|
2036
2037
|
}
|
|
2037
2038
|
const group = new URLSearchParams(search).get('apiGroup') || ''
|
|
2038
2039
|
if (kind) {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
const coreMatch = CORE_RESOURCES.find(r => r.kind === kind || r.name === kind)
|
|
2043
|
-
if (coreMatch) {
|
|
2044
|
-
return { name: coreMatch.name, kind: coreMatch.kind, group: coreMatch.group }
|
|
2045
|
-
}
|
|
2040
|
+
const coreMatch = findAPIResourceForRoute(undefined, kind, group)
|
|
2041
|
+
if (coreMatch) {
|
|
2042
|
+
return { name: coreMatch.name, kind: coreMatch.kind, group: coreMatch.group }
|
|
2046
2043
|
}
|
|
2047
2044
|
return { name: kind, kind: kind, group }
|
|
2048
2045
|
}
|
|
@@ -2100,11 +2097,14 @@ export function deriveSidebarResourceCounts(
|
|
|
2100
2097
|
function getInitialFiltersFromURL() {
|
|
2101
2098
|
const params = new URLSearchParams(window.location.search)
|
|
2102
2099
|
// Parse generic column filters
|
|
2103
|
-
const
|
|
2100
|
+
const filtersParam = params.get('filters')
|
|
2101
|
+
const columnFilters = parseColumnFilters(filtersParam)
|
|
2102
|
+
const columnFilterExcludes = parseColumnFilterExcludes(filtersParam)
|
|
2104
2103
|
const result = {
|
|
2105
2104
|
search: params.get('search') || '',
|
|
2106
2105
|
regex: params.get('regex') === 'true',
|
|
2107
2106
|
columnFilters,
|
|
2107
|
+
columnFilterExcludes,
|
|
2108
2108
|
problemFilters: params.get('problems')?.split(',').filter(Boolean) || [],
|
|
2109
2109
|
showInactive: params.get('showInactive') === 'true',
|
|
2110
2110
|
labelSelector: params.get('labels') || '', // e.g., "app=caretta,version=v1"
|
|
@@ -2188,6 +2188,7 @@ export function ResourcesView({
|
|
|
2188
2188
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null)
|
|
2189
2189
|
// Filter state
|
|
2190
2190
|
const [columnFilters, setColumnFilters] = useState<Record<string, string[]>>(initialFilters.columnFilters)
|
|
2191
|
+
const [columnFilterExcludes, setColumnFilterExcludes] = useState<Record<string, boolean>>(initialFilters.columnFilterExcludes)
|
|
2191
2192
|
const [problemFilters, setProblemFilters] = useState<string[]>(initialFilters.problemFilters)
|
|
2192
2193
|
const [openColumnFilter, setOpenColumnFilter] = useState<string | null>(null)
|
|
2193
2194
|
const [columnFilterSearch, setColumnFilterSearch] = useState('')
|
|
@@ -2237,6 +2238,25 @@ export function ResourcesView({
|
|
|
2237
2238
|
delete next[key]
|
|
2238
2239
|
return next
|
|
2239
2240
|
})
|
|
2241
|
+
setColumnFilterExcludes(prev => {
|
|
2242
|
+
if (!prev[key]) return prev
|
|
2243
|
+
const next = { ...prev }
|
|
2244
|
+
delete next[key]
|
|
2245
|
+
return next
|
|
2246
|
+
})
|
|
2247
|
+
}, [])
|
|
2248
|
+
|
|
2249
|
+
const setColumnFilterMode = useCallback((key: string, exclude: boolean) => {
|
|
2250
|
+
setColumnFilterExcludes(prev => {
|
|
2251
|
+
if (Boolean(prev[key]) === exclude) return prev
|
|
2252
|
+
const next = { ...prev }
|
|
2253
|
+
if (exclude) {
|
|
2254
|
+
next[key] = true
|
|
2255
|
+
} else {
|
|
2256
|
+
delete next[key]
|
|
2257
|
+
}
|
|
2258
|
+
return next
|
|
2259
|
+
})
|
|
2240
2260
|
}, [])
|
|
2241
2261
|
|
|
2242
2262
|
const toggleColumnFilterValue = useCallback((key: string, value: string) => {
|
|
@@ -2248,6 +2268,14 @@ export function ResourcesView({
|
|
|
2248
2268
|
const updated = current.filter(v => v !== value)
|
|
2249
2269
|
if (updated.length === 0) {
|
|
2250
2270
|
delete next[key]
|
|
2271
|
+
// The operator is meaningless with no selected values — drop it so a
|
|
2272
|
+
// stale exclude flag doesn't linger in state or re-arm on reselect.
|
|
2273
|
+
setColumnFilterExcludes(inv => {
|
|
2274
|
+
if (!inv[key]) return inv
|
|
2275
|
+
const nextInv = { ...inv }
|
|
2276
|
+
delete nextInv[key]
|
|
2277
|
+
return nextInv
|
|
2278
|
+
})
|
|
2251
2279
|
} else {
|
|
2252
2280
|
next[key] = updated
|
|
2253
2281
|
}
|
|
@@ -2550,6 +2578,12 @@ export function ResourcesView({
|
|
|
2550
2578
|
delete next[key]
|
|
2551
2579
|
return next
|
|
2552
2580
|
})
|
|
2581
|
+
setColumnFilterExcludes(prev => {
|
|
2582
|
+
if (!(key in prev)) return prev
|
|
2583
|
+
const next = { ...prev }
|
|
2584
|
+
delete next[key]
|
|
2585
|
+
return next
|
|
2586
|
+
})
|
|
2553
2587
|
if (sortColumn === key) {
|
|
2554
2588
|
setSortColumn(null)
|
|
2555
2589
|
setSortDirection(null)
|
|
@@ -2944,6 +2978,12 @@ export function ResourcesView({
|
|
|
2944
2978
|
setColumnFilters(newFilters.columnFilters)
|
|
2945
2979
|
}
|
|
2946
2980
|
|
|
2981
|
+
// Update column filter operators (include/exclude) if changed
|
|
2982
|
+
const excludeKeys = (m: Record<string, boolean>) => Object.keys(m).filter(k => m[k]).sort().join(',')
|
|
2983
|
+
if (excludeKeys(newFilters.columnFilterExcludes) !== excludeKeys(columnFilterExcludes)) {
|
|
2984
|
+
setColumnFilterExcludes(newFilters.columnFilterExcludes)
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2947
2987
|
// Reset the flag after a tick to allow normal URL updates
|
|
2948
2988
|
requestAnimationFrame(() => {
|
|
2949
2989
|
isSyncingFromURL.current = false
|
|
@@ -2969,6 +3009,7 @@ export function ResourcesView({
|
|
|
2969
3009
|
search: string,
|
|
2970
3010
|
regex: boolean,
|
|
2971
3011
|
colFilters: Record<string, string[]>,
|
|
3012
|
+
colExcludes: Record<string, boolean>,
|
|
2972
3013
|
problems: string[],
|
|
2973
3014
|
showInactive: boolean,
|
|
2974
3015
|
resourceNs?: string,
|
|
@@ -2995,8 +3036,13 @@ export function ResourcesView({
|
|
|
2995
3036
|
} else {
|
|
2996
3037
|
params.delete('regex')
|
|
2997
3038
|
}
|
|
2998
|
-
// Write column filters as `filters` param;
|
|
2999
|
-
|
|
3039
|
+
// Write column filters as `filters` param; the exclude operator is folded
|
|
3040
|
+
// into each column entry, so it can never drift from the values it negates.
|
|
3041
|
+
// Guard against a stale exclude flag on a column with no active values.
|
|
3042
|
+
const activeExcludes = Object.fromEntries(
|
|
3043
|
+
Object.entries(colExcludes).filter(([k, on]) => on && (colFilters[k]?.length ?? 0) > 0)
|
|
3044
|
+
)
|
|
3045
|
+
const filtersStr = serializeColumnFilters(colFilters, activeExcludes)
|
|
3000
3046
|
if (filtersStr) {
|
|
3001
3047
|
params.set('filters', filtersStr)
|
|
3002
3048
|
} else {
|
|
@@ -3058,6 +3104,7 @@ export function ResourcesView({
|
|
|
3058
3104
|
setSearchTerm('')
|
|
3059
3105
|
setRegexMode(false)
|
|
3060
3106
|
setColumnFilters({})
|
|
3107
|
+
setColumnFilterExcludes({})
|
|
3061
3108
|
setProblemFilters([])
|
|
3062
3109
|
setLabelSelector('')
|
|
3063
3110
|
setOwnerKind('')
|
|
@@ -3124,8 +3171,8 @@ export function ResourcesView({
|
|
|
3124
3171
|
shouldPushHistory.current = false
|
|
3125
3172
|
prevSelectedResourceRef.current = current
|
|
3126
3173
|
|
|
3127
|
-
updateURL(selectedKind, searchTerm, regexMode, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource?.namespace, selectedResource?.name, pushHistory)
|
|
3128
|
-
}, [selectedKind, searchTerm, regexMode, columnFilters, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
|
|
3174
|
+
updateURL(selectedKind, searchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, selectedResource?.namespace, selectedResource?.name, pushHistory)
|
|
3175
|
+
}, [selectedKind, searchTerm, regexMode, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, selectedResource, updateURL, basePath, locationPathname])
|
|
3129
3176
|
|
|
3130
3177
|
// Handle resource click from URL on mount
|
|
3131
3178
|
useEffect(() => {
|
|
@@ -3425,6 +3472,7 @@ export function ResourcesView({
|
|
|
3425
3472
|
setOpenColumnFilter(null)
|
|
3426
3473
|
if (!isSyncingFromURL.current) {
|
|
3427
3474
|
setColumnFilters({})
|
|
3475
|
+
setColumnFilterExcludes({})
|
|
3428
3476
|
}
|
|
3429
3477
|
setProblemFilters([])
|
|
3430
3478
|
}, [selectedKind.name])
|
|
@@ -3598,7 +3646,8 @@ export function ResourcesView({
|
|
|
3598
3646
|
activeColFilters.every(([col, vals]) => {
|
|
3599
3647
|
const extra = extraColumnsByKey.get(col)
|
|
3600
3648
|
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col, kindLower)
|
|
3601
|
-
|
|
3649
|
+
const match = vals.includes(cellVal)
|
|
3650
|
+
return columnFilterExcludes[col] ? !match : match
|
|
3602
3651
|
})
|
|
3603
3652
|
)
|
|
3604
3653
|
}
|
|
@@ -3741,7 +3790,7 @@ export function ResourcesView({
|
|
|
3741
3790
|
}
|
|
3742
3791
|
|
|
3743
3792
|
return result
|
|
3744
|
-
}, [resources, searchTerm, regexMode, searchRegex, columnFilters, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, sortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
3793
|
+
}, [resources, searchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, sortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
3745
3794
|
|
|
3746
3795
|
// For nodes table: compute the majority minor version so outliers can be highlighted
|
|
3747
3796
|
const majorityNodeMinorVersion = useMemo(() => {
|
|
@@ -4693,7 +4742,7 @@ export function ResourcesView({
|
|
|
4693
4742
|
className="flex items-center gap-1 px-2 py-1 text-xs selection selection-text rounded-md hover:selection-strong transition-colors"
|
|
4694
4743
|
>
|
|
4695
4744
|
<ListFilter className="w-3 h-3" />
|
|
4696
|
-
<span>{key}: {vals.join(', ')}</span>
|
|
4745
|
+
<span>{key}: {columnFilterExcludes[key] ? 'not ' : ''}{vals.join(', ')}</span>
|
|
4697
4746
|
<X className="w-3 h-3" />
|
|
4698
4747
|
</button>
|
|
4699
4748
|
))}
|
|
@@ -4831,7 +4880,14 @@ export function ResourcesView({
|
|
|
4831
4880
|
)}
|
|
4832
4881
|
>
|
|
4833
4882
|
<ListFilter className="w-3 h-3" />
|
|
4834
|
-
{hasActiveFilter &&
|
|
4883
|
+
{hasActiveFilter && (
|
|
4884
|
+
<span
|
|
4885
|
+
className="text-[10px] leading-none font-semibold"
|
|
4886
|
+
aria-label={`${activeFilterValues.length} ${columnFilterExcludes[col.key] ? 'excluded' : 'included'} value${activeFilterValues.length === 1 ? '' : 's'}`}
|
|
4887
|
+
>
|
|
4888
|
+
{columnFilterExcludes[col.key] ? `≠ ${activeFilterValues.length}` : activeFilterValues.length}
|
|
4889
|
+
</span>
|
|
4890
|
+
)}
|
|
4835
4891
|
</button>
|
|
4836
4892
|
{hasActiveFilter && (
|
|
4837
4893
|
<button
|
|
@@ -4865,6 +4921,34 @@ export function ResourcesView({
|
|
|
4865
4921
|
)}
|
|
4866
4922
|
onClick={(e) => e.stopPropagation()}
|
|
4867
4923
|
>
|
|
4924
|
+
<div className="flex items-center gap-1 p-1.5 border-b border-theme-border" role="group" aria-label={`${col.label} filter mode`}>
|
|
4925
|
+
<button
|
|
4926
|
+
onClick={() => setColumnFilterMode(col.key, false)}
|
|
4927
|
+
aria-pressed={!columnFilterExcludes[col.key]}
|
|
4928
|
+
className={clsx(
|
|
4929
|
+
'flex-1 px-2 py-1 text-xs rounded transition-colors',
|
|
4930
|
+
!columnFilterExcludes[col.key]
|
|
4931
|
+
? 'selection-strong selection-text'
|
|
4932
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary'
|
|
4933
|
+
)}
|
|
4934
|
+
title="Show rows matching the selected values"
|
|
4935
|
+
>
|
|
4936
|
+
Include
|
|
4937
|
+
</button>
|
|
4938
|
+
<button
|
|
4939
|
+
onClick={() => setColumnFilterMode(col.key, true)}
|
|
4940
|
+
aria-pressed={Boolean(columnFilterExcludes[col.key])}
|
|
4941
|
+
className={clsx(
|
|
4942
|
+
'flex-1 px-2 py-1 text-xs rounded transition-colors',
|
|
4943
|
+
columnFilterExcludes[col.key]
|
|
4944
|
+
? 'selection-strong selection-text'
|
|
4945
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary'
|
|
4946
|
+
)}
|
|
4947
|
+
title="Show rows that do NOT match the selected values"
|
|
4948
|
+
>
|
|
4949
|
+
Exclude
|
|
4950
|
+
</button>
|
|
4951
|
+
</div>
|
|
4868
4952
|
{values.length > 5 ? (
|
|
4869
4953
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
4870
4954
|
<div className="relative flex-1">
|
|
@@ -5922,11 +6006,17 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
5922
6006
|
<button
|
|
5923
6007
|
onClick={(e) => {
|
|
5924
6008
|
e.stopPropagation()
|
|
5925
|
-
// Merge node filter into existing column filters via URL
|
|
6009
|
+
// Merge node filter into existing column filters via URL,
|
|
6010
|
+
// preserving any exclude operators already set on other columns.
|
|
5926
6011
|
const params = new URLSearchParams(window.location.search)
|
|
5927
6012
|
const existing = parseColumnFilters(params.get('filters'))
|
|
6013
|
+
const existingExcludes = parseColumnFilterExcludes(params.get('filters'))
|
|
5928
6014
|
existing['node'] = [nodeVal]
|
|
5929
|
-
|
|
6015
|
+
// Clicking a node means "show pods on this node", so force the
|
|
6016
|
+
// node column back to include mode even if it was previously
|
|
6017
|
+
// set to exclude — otherwise the shortcut would hide those pods.
|
|
6018
|
+
delete existingExcludes['node']
|
|
6019
|
+
params.set('filters', serializeColumnFilters(existing, existingExcludes))
|
|
5930
6020
|
navigate?.(`/resources/pods?${params.toString()}`)
|
|
5931
6021
|
}}
|
|
5932
6022
|
className="text-sm text-blue-400 hover:text-blue-300 hover:underline truncate block text-left"
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
parseColumnFilters,
|
|
4
|
+
serializeColumnFilters,
|
|
5
|
+
parseColumnFilterExcludes,
|
|
6
|
+
} from './resource-utils'
|
|
3
7
|
|
|
4
8
|
describe('column filter serialization round-trip', () => {
|
|
5
9
|
it('round-trips built-in keys', () => {
|
|
@@ -23,4 +27,78 @@ describe('column filter serialization round-trip', () => {
|
|
|
23
27
|
it('parses legacy unencoded built-in keys', () => {
|
|
24
28
|
expect(parseColumnFilters('status:Running')).toEqual({ status: ['Running'] })
|
|
25
29
|
})
|
|
30
|
+
|
|
31
|
+
it('treats a two-part filter as implicit include (no excludes)', () => {
|
|
32
|
+
expect(parseColumnFilterExcludes('status:Running')).toEqual({})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('column filter include/exclude operator', () => {
|
|
37
|
+
it('serializes excluded columns with the explicit exclude operator', () => {
|
|
38
|
+
const filters = { status: ['Running', 'Completed'], namespace: ['default'] }
|
|
39
|
+
const excludes = { status: true }
|
|
40
|
+
expect(serializeColumnFilters(filters, excludes)).toBe(
|
|
41
|
+
'status:exclude:Running,Completed|namespace:default'
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('round-trips values through an excluded column', () => {
|
|
46
|
+
const filters = { status: ['Running', 'Completed'] }
|
|
47
|
+
const serialized = serializeColumnFilters(filters, { status: true })
|
|
48
|
+
expect(parseColumnFilters(serialized)).toEqual(filters)
|
|
49
|
+
expect(parseColumnFilterExcludes(serialized)).toEqual({ status: true })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('parses the explicit include operator as non-excluded', () => {
|
|
53
|
+
expect(parseColumnFilters('namespace:include:default')).toEqual({ namespace: ['default'] })
|
|
54
|
+
expect(parseColumnFilterExcludes('namespace:include:default')).toEqual({})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('keeps a value literally named "exclude" in the two-part form', () => {
|
|
58
|
+
expect(parseColumnFilters('reason:exclude')).toEqual({ reason: ['exclude'] })
|
|
59
|
+
expect(parseColumnFilterExcludes('reason:exclude')).toEqual({})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('handles a value literally named "exclude" under the exclude operator', () => {
|
|
63
|
+
const serialized = serializeColumnFilters({ reason: ['exclude'] }, { reason: true })
|
|
64
|
+
expect(serialized).toBe('reason:exclude:exclude')
|
|
65
|
+
expect(parseColumnFilters(serialized)).toEqual({ reason: ['exclude'] })
|
|
66
|
+
expect(parseColumnFilterExcludes(serialized)).toEqual({ reason: true })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('does not emit an operator for an excluded column with no values', () => {
|
|
70
|
+
expect(serializeColumnFilters({ status: [] }, { status: true })).toBe('')
|
|
71
|
+
expect(parseColumnFilterExcludes('')).toEqual({})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('preserves the exclude operator for custom-column keys', () => {
|
|
75
|
+
const filters = { 'label:tier': ['control-plane'] }
|
|
76
|
+
const serialized = serializeColumnFilters(filters, { 'label:tier': true })
|
|
77
|
+
expect(serialized).toBe('label%3Atier:exclude:control-plane')
|
|
78
|
+
expect(parseColumnFilters(serialized)).toEqual(filters)
|
|
79
|
+
expect(parseColumnFilterExcludes(serialized)).toEqual({ 'label:tier': true })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('returns empty structures for empty/absent params', () => {
|
|
83
|
+
expect(parseColumnFilters('')).toEqual({})
|
|
84
|
+
expect(parseColumnFilters(null)).toEqual({})
|
|
85
|
+
expect(parseColumnFilterExcludes(null)).toEqual({})
|
|
86
|
+
expect(serializeColumnFilters({})).toBe('')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('ignores prototype-polluting keys from a crafted param', () => {
|
|
90
|
+
const parsed = parseColumnFilters('__proto__:Running|constructor:x|status:Running')
|
|
91
|
+
expect(parsed).toEqual({ status: ['Running'] })
|
|
92
|
+
expect(Object.prototype.hasOwnProperty.call(parsed, '__proto__')).toBe(false)
|
|
93
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined()
|
|
94
|
+
expect(parseColumnFilterExcludes('__proto__:exclude:Running')).toEqual({})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('lets the last segment win the mode when a column repeats', () => {
|
|
98
|
+
expect(parseColumnFilters('status:exclude:Running|status:Completed')).toEqual({ status: ['Completed'] })
|
|
99
|
+
expect(parseColumnFilterExcludes('status:exclude:Running|status:Completed')).toEqual({})
|
|
100
|
+
|
|
101
|
+
expect(parseColumnFilters('status:Running|status:exclude:Completed')).toEqual({ status: ['Completed'] })
|
|
102
|
+
expect(parseColumnFilterExcludes('status:Running|status:exclude:Completed')).toEqual({ status: true })
|
|
103
|
+
})
|
|
26
104
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Cpu, Network, Cloud } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
4
4
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
5
5
|
import { getAWSMachineStatus, getAWSMachineInstanceType, getAWSMachineInstanceState, getAWSMachineInstanceID } from '../resource-utils-aws-capi'
|
|
6
6
|
|
|
@@ -16,6 +16,7 @@ export function AWSMachineRenderer({ data }: Props) {
|
|
|
16
16
|
|
|
17
17
|
const machineStatus = getAWSMachineStatus(data)
|
|
18
18
|
const isFailed = machineStatus.level === 'unhealthy'
|
|
19
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
19
20
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
20
21
|
|
|
21
22
|
const instanceState = getAWSMachineInstanceState(data)
|
|
@@ -23,7 +24,7 @@ export function AWSMachineRenderer({ data }: Props) {
|
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
26
|
<>
|
|
26
|
-
{isFailed && (
|
|
27
|
+
{isFailed && !operationalIssuesShown && (
|
|
27
28
|
<AlertBanner
|
|
28
29
|
variant="error"
|
|
29
30
|
title="AWS Machine Not Ready"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Globe, Server } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
4
4
|
import { getAWSManagedClusterStatus, getAWSManagedClusterEndpoint, getAWSManagedClusterFailureDomains } from '../resource-utils-aws-capi'
|
|
5
5
|
|
|
@@ -11,13 +11,14 @@ export function AWSManagedClusterRenderer({ data }: Props) {
|
|
|
11
11
|
const conditions = getCAPIConditions(data)
|
|
12
12
|
const clusterStatus = getAWSManagedClusterStatus(data)
|
|
13
13
|
const isFailed = clusterStatus.level === 'unhealthy'
|
|
14
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
14
15
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
15
16
|
const failureDomains = getAWSManagedClusterFailureDomains(data)
|
|
16
17
|
const endpoint = getAWSManagedClusterEndpoint(data)
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
20
|
<>
|
|
20
|
-
{isFailed && (
|
|
21
|
+
{isFailed && !operationalIssuesShown && (
|
|
21
22
|
<AlertBanner
|
|
22
23
|
variant="error"
|
|
23
24
|
title="Managed Cluster Not Ready"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Globe, Network, Shield, Server, Package } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
4
4
|
import {
|
|
5
5
|
getAWSMCPStatus, getAWSMCPEKSClusterName, getAWSMCPRegion, getAWSMCPVersion,
|
|
@@ -18,6 +18,7 @@ export function AWSManagedControlPlaneRenderer({ data }: Props) {
|
|
|
18
18
|
|
|
19
19
|
const mcpStatus = getAWSMCPStatus(data)
|
|
20
20
|
const isFailed = mcpStatus.level === 'unhealthy'
|
|
21
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
21
22
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
22
23
|
|
|
23
24
|
const vpc = getAWSMCPVPC(data)
|
|
@@ -29,7 +30,7 @@ export function AWSManagedControlPlaneRenderer({ data }: Props) {
|
|
|
29
30
|
|
|
30
31
|
return (
|
|
31
32
|
<>
|
|
32
|
-
{isFailed && (
|
|
33
|
+
{isFailed && !operationalIssuesShown && (
|
|
33
34
|
<AlertBanner
|
|
34
35
|
variant="error"
|
|
35
36
|
title="EKS Control Plane Not Ready"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server, Settings } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
4
4
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
5
5
|
import { getAWSMMPStatus, getAWSMMPInstanceType, getAWSMMPCapacityType, getAWSMMPAMIType, getAWSMMPNodegroupName, getAWSMMPScaling } from '../resource-utils-aws-capi'
|
|
6
6
|
import { CAPACITY_TYPE_BADGE } from '../../../utils/badge-colors'
|
|
@@ -17,6 +17,7 @@ export function AWSManagedMachinePoolRenderer({ data }: Props) {
|
|
|
17
17
|
|
|
18
18
|
const mmpStatus = getAWSMMPStatus(data)
|
|
19
19
|
const isFailed = mmpStatus.level === 'unhealthy'
|
|
20
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
20
21
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
21
22
|
|
|
22
23
|
const scaling = getAWSMMPScaling(data)
|
|
@@ -26,7 +27,7 @@ export function AWSManagedMachinePoolRenderer({ data }: Props) {
|
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<>
|
|
29
|
-
{isFailed && (
|
|
30
|
+
{isFailed && !operationalIssuesShown && (
|
|
30
31
|
<AlertBanner
|
|
31
32
|
variant="error"
|
|
32
33
|
title="Managed Machine Pool Not Ready"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Cloud } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
4
4
|
import { getAzureMachineStatus, getAzureMachineVMSize, getAzureMachineProviderID } from '../resource-utils-azure-capi'
|
|
5
5
|
|
|
@@ -13,12 +13,13 @@ export function AzureMachineRenderer({ data }: Props) {
|
|
|
13
13
|
const conditions = getCAPIConditions(data)
|
|
14
14
|
const machineStatus = getAzureMachineStatus(data)
|
|
15
15
|
const isFailed = machineStatus.level === 'unhealthy'
|
|
16
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
16
17
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
17
18
|
const providerID = getAzureMachineProviderID(data)
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
21
|
<>
|
|
21
|
-
{isFailed && (
|
|
22
|
+
{isFailed && !operationalIssuesShown && (
|
|
22
23
|
<AlertBanner variant="error" title="Azure Machine Not Ready" message={readyCond?.message || 'AzureMachine is not ready.'} />
|
|
23
24
|
)}
|
|
24
25
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Globe, Shield, Settings } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
3
3
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
4
4
|
import { getAzureMCPStatus, getAzureMCPLocation, getAzureMCPVersion, getAzureMCPResourceGroup, getAzureMCPSKUTier, getAzureMCPNetworkPlugin, getAzureMCPNetworkPolicy, getAzureMCPDNSPrefix, getAzureMCPUpgradeChannel, getAzureMCPPrivateCluster } from '../resource-utils-azure-capi'
|
|
5
5
|
|
|
@@ -13,13 +13,14 @@ export function AzureManagedControlPlaneRenderer({ data }: Props) {
|
|
|
13
13
|
const conditions = getCAPIConditions(data)
|
|
14
14
|
const mcpStatus = getAzureMCPStatus(data)
|
|
15
15
|
const isFailed = mcpStatus.level === 'unhealthy'
|
|
16
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
16
17
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
17
18
|
const apiAccess = spec.apiServerAccessProfile || {}
|
|
18
19
|
const authorizedRanges = apiAccess.authorizedIPRanges || []
|
|
19
20
|
|
|
20
21
|
return (
|
|
21
22
|
<>
|
|
22
|
-
{isFailed && (
|
|
23
|
+
{isFailed && !operationalIssuesShown && (
|
|
23
24
|
<AlertBanner variant="error" title="AKS Control Plane Not Ready" message={readyCond?.message || 'AzureManagedControlPlane is not ready.'} />
|
|
24
25
|
)}
|
|
25
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server, Settings } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, useOperationalIssuesShown} from '../../ui/drawer-components'
|
|
4
4
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
5
5
|
import { getAzureMMPStatus, getAzureMMPSKU, getAzureMMPMode, getAzureMMPOSDiskInfo, getAzureMMPScaling, getAzureMMPScaleSetPriority, getAzureMMPOSType } from '../resource-utils-azure-capi'
|
|
6
6
|
import { CAPACITY_TYPE_BADGE, NODEPOOL_MODE_BADGE } from '../../../utils/badge-colors'
|
|
@@ -16,6 +16,7 @@ export function AzureManagedMachinePoolRenderer({ data }: Props) {
|
|
|
16
16
|
const conditions = getCAPIConditions(data)
|
|
17
17
|
const mmpStatus = getAzureMMPStatus(data)
|
|
18
18
|
const isFailed = mmpStatus.level === 'unhealthy'
|
|
19
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
19
20
|
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
20
21
|
const scaling = getAzureMMPScaling(data)
|
|
21
22
|
const mode = getAzureMMPMode(data)
|
|
@@ -26,7 +27,7 @@ export function AzureManagedMachinePoolRenderer({ data }: Props) {
|
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<>
|
|
29
|
-
{isFailed && (
|
|
30
|
+
{isFailed && !operationalIssuesShown && (
|
|
30
31
|
<AlertBanner variant="error" title="AKS Node Pool Not Ready" message={readyCond?.message || 'AzureManagedMachinePool is not ready.'} />
|
|
31
32
|
)}
|
|
32
33
|
|