@skyhook-io/k8s-ui 1.17.1 → 1.18.1
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/README.md +13 -0
- package/package.json +4 -4
- package/src/components/compare/CompareResourcePicker.tsx +18 -4
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -2
- package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
- package/src/components/gitops/GitOpsTableView.tsx +33 -4
- package/src/components/gitops/detail-helpers.test.ts +58 -13
- package/src/components/gitops/detail-helpers.ts +37 -6
- package/src/components/gitops/index.ts +1 -0
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +27 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +16 -10
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +73 -9
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +45 -28
- package/src/components/gitops/tree/tree-helpers.test.ts +6 -0
- package/src/components/gitops/tree/tree-helpers.ts +5 -1
- package/src/components/issues/IssuesView.tsx +18 -10
- package/src/components/issues/ResourceIssuesSection.tsx +5 -2
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +26 -0
- package/src/components/issues/severity.ts +9 -2
- package/src/components/issues/types.ts +32 -4
- package/src/components/logs/LogCore.theme.test.tsx +126 -1
- package/src/components/logs/LogCore.tsx +159 -40
- package/src/components/logs/LogsViewer.test.tsx +183 -60
- package/src/components/logs/LogsViewer.tsx +49 -12
- package/src/components/logs/StructuredLogLine.tsx +1 -1
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -1
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
- package/src/components/namespace-switcher/NamespacePicker.tsx +42 -8
- package/src/components/resources/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
- package/src/components/resources/ResourcesSidebar.tsx +36 -21
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -2
- package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
- package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
- package/src/components/resources/ResourcesView.tsx +198 -93
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
- package/src/components/resources/renderers/PodRenderer.tsx +72 -20
- package/src/components/resources/resource-utils-trivy.test.ts +87 -0
- package/src/components/resources/resource-utils-trivy.ts +122 -0
- package/src/components/resources/resource-utils.ts +15 -2
- package/src/components/resources/summarize-scheduler-message.test.ts +1 -0
- package/src/components/shared/DetailShell.tsx +9 -7
- package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/ManagedByChip.test.tsx +22 -0
- package/src/components/shared/ManagedByChip.tsx +24 -4
- package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
- package/src/components/shared/ResourceActionsBar.tsx +12 -6
- package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +48 -31
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineList.tsx +5 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- package/src/components/ui/FetchResult.test.tsx +19 -2
- package/src/components/ui/FetchResult.tsx +13 -1
- package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
- package/src/components/ui/MultiSelectPicker.tsx +20 -2
- package/src/components/ui/PageHeader.tsx +6 -4
- package/src/components/ui/Toast.tsx +48 -30
- package/src/components/ui/drawer-components.test.tsx +253 -0
- package/src/components/ui/drawer-components.tsx +315 -11
- package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
- package/src/components/ui/index.ts +1 -1
- package/src/components/workload/WorkloadView.relationships.test.tsx +33 -0
- package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
- package/src/components/workload/WorkloadView.tsx +68 -14
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/usePhoneLayout.ts +12 -0
- package/src/theme/tailwind-theme.css +5 -0
- package/src/types/core.ts +91 -3
- package/src/types/gitops-flux-inventory.test.ts +24 -0
- package/src/types/gitops-insights.ts +1 -0
- package/src/types/gitops-tree.ts +2 -0
- package/src/types/gitops.ts +5 -5
- package/src/types/index.ts +3 -0
- package/src/types/read-coverage.ts +65 -0
- package/src/types/resource-coverage.ts +40 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +6 -1
- package/src/utils/gitops-owner.test.ts +42 -1
- package/src/utils/gitops-owner.ts +22 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/read-actions.test.ts +22 -0
- package/src/utils/read-actions.ts +56 -0
- package/src/utils/resource-coverage.test.ts +121 -0
- package/src/utils/resource-coverage.ts +117 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import type { ResourceCoverage } from '../../types/resource-coverage'
|
|
2
|
+
import { resourceCoverageNotice, resourceEmptyMessage, resourceNarrowedNotice } from '../../utils/resource-coverage'
|
|
1
3
|
import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
|
|
2
4
|
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
3
5
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue'
|
|
6
|
+
import { Badge } from '../ui/Badge'
|
|
4
7
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
5
8
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
6
9
|
import { FetchResult } from '../ui/FetchResult'
|
|
7
10
|
import { Input } from '../ui/Input'
|
|
11
|
+
import { ColumnPickerList } from './ColumnPickerList'
|
|
12
|
+
import { applyColumnOrder, sanitizeColumnOrder } from './column-order'
|
|
8
13
|
import type { TopPodMetrics, TopNodeMetrics, ContainerResourceMetrics } from '../../types'
|
|
9
14
|
import {
|
|
10
15
|
Search,
|
|
@@ -145,6 +150,7 @@ import {
|
|
|
145
150
|
SEVERITY_DOT_COLOR,
|
|
146
151
|
healthColors,
|
|
147
152
|
} from './resource-utils'
|
|
153
|
+
import { TRIVY_GROUP, searchTrivyFindings, type TrivyFindingMatch, type TrivyFindingsSearch } from './resource-utils-trivy'
|
|
148
154
|
import { getGenericResourceStatus } from './generic-status'
|
|
149
155
|
import { ActiveFilterBar } from './ActiveFilterBar'
|
|
150
156
|
import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
|
|
@@ -153,7 +159,7 @@ import {
|
|
|
153
159
|
printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
|
|
154
160
|
} from './printer-columns'
|
|
155
161
|
import { SEVERITY_BADGE, SEVERITY_TEXT, healthToSeverity, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
156
|
-
import { pluralize } from '../../utils/pluralize'
|
|
162
|
+
import { englishPlural, isEnglishPlural, pluralize } from '../../utils/pluralize'
|
|
157
163
|
import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
|
|
158
164
|
import { formatRelativeAgeTime, parseQuantityToNumber } from '../../utils/format'
|
|
159
165
|
import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
|
|
@@ -161,6 +167,8 @@ import { isRolloutActivityVisible } from '../../utils/workload-rollout'
|
|
|
161
167
|
import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
|
|
162
168
|
import { Tooltip } from '../ui/Tooltip'
|
|
163
169
|
import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
|
|
170
|
+
import type { Issue } from '../issues/types'
|
|
171
|
+
import { issueTitle } from '../issues/severity'
|
|
164
172
|
import { SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
165
173
|
// CRD-specific cell components (extracted)
|
|
166
174
|
import { GitRepositoryCell, OCIRepositoryCell, HelmRepositoryCell, KustomizationCell, FluxHelmReleaseCell, FluxAlertCell } from './renderers/flux-cells'
|
|
@@ -3255,6 +3263,7 @@ function filterHostExtras(
|
|
|
3255
3263
|
|
|
3256
3264
|
// localStorage helpers for column settings
|
|
3257
3265
|
const COLUMN_SETTINGS_PREFIX = 'radar-columns-'
|
|
3266
|
+
const CHECKBOX_VISIBILITY_KEY = 'radar-resource-checkboxes'
|
|
3258
3267
|
|
|
3259
3268
|
// Storage identity for a kind's column settings. normalizeKindToPlural falls
|
|
3260
3269
|
// through to the bare plural for anything outside the curated collision
|
|
@@ -3280,6 +3289,8 @@ interface ColumnSettings {
|
|
|
3280
3289
|
* load tell a column they hid from one that did not exist yet. Absent on
|
|
3281
3290
|
* blobs written before this field. */
|
|
3282
3291
|
known?: string[]
|
|
3292
|
+
/** Optional display order for movable columns, including hidden ones. */
|
|
3293
|
+
order?: string[]
|
|
3283
3294
|
}
|
|
3284
3295
|
|
|
3285
3296
|
function loadColumnSettings(kind: string, group?: string): ColumnSettings | null {
|
|
@@ -3342,6 +3353,9 @@ interface ResourcesViewData {
|
|
|
3342
3353
|
// cluster-scoped kind. An ExternalSecret names a store; only the store
|
|
3343
3354
|
// knows which backend it reads from.
|
|
3344
3355
|
storeProviders?: Record<string, string>
|
|
3356
|
+
/** Current CronJob Issues keyed by "namespace/name", supplied by the host. */
|
|
3357
|
+
operationalIssuesIncomplete?: boolean
|
|
3358
|
+
operationalIssues?: Record<string, Issue>
|
|
3345
3359
|
/** Cluster Audit findings keyed by "namespace/name". Raw compatibility
|
|
3346
3360
|
* counts render danger as High and warning as Medium. */
|
|
3347
3361
|
auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
|
|
@@ -3352,6 +3366,7 @@ interface ResourcesViewData {
|
|
|
3352
3366
|
export const ResourcesViewDataContext = React.createContext<ResourcesViewData>({})
|
|
3353
3367
|
|
|
3354
3368
|
export interface ResourceQueryResult {
|
|
3369
|
+
coverage?: ResourceCoverage
|
|
3355
3370
|
resourceName?: string
|
|
3356
3371
|
group?: string
|
|
3357
3372
|
data?: any[]
|
|
@@ -3387,6 +3402,7 @@ interface ResourcesViewProps {
|
|
|
3387
3402
|
resourceQueries?: ResourceQueryResult[]
|
|
3388
3403
|
// Lightweight counts for sidebar badges (from /api/resource-counts)
|
|
3389
3404
|
resourceCounts?: Record<string, number | null>
|
|
3405
|
+
resourceCoverage?: Record<string, ResourceCoverage>
|
|
3390
3406
|
resourceForbidden?: string[]
|
|
3391
3407
|
/** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
|
|
3392
3408
|
* keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
|
|
@@ -3406,6 +3422,9 @@ interface ResourcesViewProps {
|
|
|
3406
3422
|
// cluster-scoped kind. An ExternalSecret names a store; only the store
|
|
3407
3423
|
// knows which backend it reads from.
|
|
3408
3424
|
storeProviders?: Record<string, string>
|
|
3425
|
+
/** Current CronJob Issues keyed by "namespace/name", supplied by the host. */
|
|
3426
|
+
operationalIssuesIncomplete?: boolean
|
|
3427
|
+
operationalIssues?: Record<string, Issue>
|
|
3409
3428
|
/** Cluster Audit findings keyed by "namespace/name". Raw compatibility
|
|
3410
3429
|
* counts render danger as High and warning as Medium. */
|
|
3411
3430
|
auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
|
|
@@ -3537,6 +3556,7 @@ const DEFAULT_KIND_INFO: SelectedKindInfo = { name: 'pods', kind: 'Pod', group:
|
|
|
3537
3556
|
export const LOADED_RESOURCE_COUNT_TTL_MS = 5 * 60 * 1000
|
|
3538
3557
|
|
|
3539
3558
|
interface LoadedResourceCount {
|
|
3559
|
+
coverage?: ResourceCoverage
|
|
3540
3560
|
count: number
|
|
3541
3561
|
expiresAt: number
|
|
3542
3562
|
}
|
|
@@ -3646,6 +3666,22 @@ export function deriveSidebarResourceCounts(
|
|
|
3646
3666
|
return results
|
|
3647
3667
|
}
|
|
3648
3668
|
|
|
3669
|
+
export function deriveSidebarResourceCoverage(
|
|
3670
|
+
resourceCounts: Record<string, number | null> | undefined,
|
|
3671
|
+
resourceCoverage: Record<string, ResourceCoverage> | undefined,
|
|
3672
|
+
loadedCountCache: LoadedResourceCountCache,
|
|
3673
|
+
now = Date.now(),
|
|
3674
|
+
): Record<string, ResourceCoverage> {
|
|
3675
|
+
const result: Record<string, ResourceCoverage> = { ...resourceCoverage }
|
|
3676
|
+
for (const key of Object.keys(resourceCounts ?? {})) {
|
|
3677
|
+
if (!(key in result)) result[key] = { status: 'unknown' }
|
|
3678
|
+
}
|
|
3679
|
+
for (const [key, cached] of Object.entries(loadedCountCache)) {
|
|
3680
|
+
if (!(key in result) && cached.expiresAt > now) result[key] = cached.coverage ?? { status: 'unknown' }
|
|
3681
|
+
}
|
|
3682
|
+
return result
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3649
3685
|
const FILTER_INTENT_PARAMS = [...RESOURCE_LIST_FILTER_PARAMS, 'resource']
|
|
3650
3686
|
|
|
3651
3687
|
/**
|
|
@@ -3680,6 +3716,7 @@ export function ResourcesView({
|
|
|
3680
3716
|
apiResources: apiResourcesProp,
|
|
3681
3717
|
resourceQueries: resourceQueriesProp,
|
|
3682
3718
|
resourceCounts: resourceCountsProp,
|
|
3719
|
+
resourceCoverage: resourceCoverageProp,
|
|
3683
3720
|
resourceForbidden: resourceForbiddenProp,
|
|
3684
3721
|
resourceReasons,
|
|
3685
3722
|
resourceUnavailable: resourceUnavailableProp,
|
|
@@ -3692,6 +3729,8 @@ export function ResourcesView({
|
|
|
3692
3729
|
certExpiryError,
|
|
3693
3730
|
storeProviders,
|
|
3694
3731
|
auditBadges,
|
|
3732
|
+
operationalIssues,
|
|
3733
|
+
operationalIssuesIncomplete,
|
|
3695
3734
|
pinned = [],
|
|
3696
3735
|
togglePin = () => {},
|
|
3697
3736
|
isPinned = () => false,
|
|
@@ -3747,7 +3786,6 @@ export function ResourcesView({
|
|
|
3747
3786
|
}, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
3748
3787
|
|
|
3749
3788
|
useEffect(() => {
|
|
3750
|
-
setBulkMode(false)
|
|
3751
3789
|
setCheckedResources(new Set())
|
|
3752
3790
|
setShowBulkDeleteConfirm(false)
|
|
3753
3791
|
setShowBulkRestartConfirm(false)
|
|
@@ -3808,6 +3846,7 @@ export function ResourcesView({
|
|
|
3808
3846
|
// Column visibility and resize state
|
|
3809
3847
|
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(new Set())
|
|
3810
3848
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
|
|
3849
|
+
const [columnOrder, setColumnOrder] = useState<string[] | null>(null)
|
|
3811
3850
|
const [showColumnPicker, setShowColumnPicker] = useState(false)
|
|
3812
3851
|
const [customColumns, setCustomColumns] = useState<CustomColumnDef[]>([])
|
|
3813
3852
|
const [customColumnDraft, setCustomColumnDraft] = useState<CustomColumnDef>({ source: 'label', path: '' })
|
|
@@ -3820,10 +3859,10 @@ export function ResourcesView({
|
|
|
3820
3859
|
const [ownerKind, setOwnerKind] = useState<string>(initialFilters.ownerKind)
|
|
3821
3860
|
const [ownerName, setOwnerName] = useState<string>(initialFilters.ownerName)
|
|
3822
3861
|
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3862
|
+
const [showCheckboxes, setShowCheckboxes] = useState(() => {
|
|
3863
|
+
try { return localStorage.getItem(CHECKBOX_VISIBILITY_KEY) === 'true' }
|
|
3864
|
+
catch { return false }
|
|
3865
|
+
})
|
|
3827
3866
|
const [checkedResources, setCheckedResources] = useState<Set<string>>(new Set())
|
|
3828
3867
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
|
3829
3868
|
const [showBulkRestartConfirm, setShowBulkRestartConfirm] = useState(false)
|
|
@@ -3831,8 +3870,7 @@ export function ResourcesView({
|
|
|
3831
3870
|
const [bulkForceDelete, setBulkForceDelete] = useState(false)
|
|
3832
3871
|
const [bulkScaleReplicas, setBulkScaleReplicas] = useState(0)
|
|
3833
3872
|
|
|
3834
|
-
const
|
|
3835
|
-
setBulkMode(false)
|
|
3873
|
+
const clearBulkSelection = useCallback(() => {
|
|
3836
3874
|
setCheckedResources(new Set())
|
|
3837
3875
|
}, [])
|
|
3838
3876
|
|
|
@@ -3949,13 +3987,16 @@ export function ResourcesView({
|
|
|
3949
3987
|
[printerTable, builtPrinterColumns],
|
|
3950
3988
|
)
|
|
3951
3989
|
|
|
3952
|
-
const
|
|
3990
|
+
const definedColumns = useMemo(() => {
|
|
3953
3991
|
const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
|
|
3954
3992
|
if (!extraLeadingColumns?.length && !builtCustomColumns.length) return kindColumns
|
|
3955
3993
|
const builtinKeys = new Set(kindColumns.map(c => c.key))
|
|
3956
3994
|
const filteredExtras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
|
|
3957
3995
|
return [...filteredExtras, ...kindColumns, ...builtCustomColumns]
|
|
3958
3996
|
}, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
|
|
3997
|
+
// Display order. Everything downstream reads this, so the picker, the header
|
|
3998
|
+
// and the resize width snapshot (which maps <th> index to key) agree.
|
|
3999
|
+
const allColumns = useMemo(() => applyColumnOrder(definedColumns, columnOrder), [definedColumns, columnOrder])
|
|
3959
4000
|
|
|
3960
4001
|
// The offered key set, and a stable signature for it. allColumns is a new
|
|
3961
4002
|
// identity on every printer-column refetch; depending on the array itself
|
|
@@ -4090,15 +4131,18 @@ export function ResourcesView({
|
|
|
4090
4131
|
hadSavedColumnSettings.current = false
|
|
4091
4132
|
setVisibleColumns(getDefaultVisibleColumns(effective))
|
|
4092
4133
|
setColumnWidths({})
|
|
4134
|
+
setColumnOrder(null)
|
|
4093
4135
|
} else {
|
|
4094
4136
|
setVisibleColumns(mergeSavedVisibleColumns(
|
|
4095
4137
|
saved.visible, extraKeys, builtPrinterColumns, effective, savedKnown,
|
|
4096
4138
|
))
|
|
4097
4139
|
setColumnWidths(saved.widths || {})
|
|
4140
|
+
setColumnOrder(sanitizeColumnOrder(saved.order))
|
|
4098
4141
|
}
|
|
4099
4142
|
} else {
|
|
4100
4143
|
setVisibleColumns(getDefaultVisibleColumns(effective))
|
|
4101
4144
|
setColumnWidths({})
|
|
4145
|
+
setColumnOrder(null)
|
|
4102
4146
|
}
|
|
4103
4147
|
// printerColumnsKey, not builtPrinterColumns: the built array is a new
|
|
4104
4148
|
// identity on every refetch, and re-running this effect would reset the
|
|
@@ -4120,12 +4164,13 @@ export function ResourcesView({
|
|
|
4120
4164
|
widths: columnWidths,
|
|
4121
4165
|
custom: customColumns,
|
|
4122
4166
|
known: Array.from(knownColumnKeys.current),
|
|
4167
|
+
order: columnOrder ?? undefined,
|
|
4123
4168
|
})
|
|
4124
4169
|
// A persisted blob blocks GPU auto-show from here on — same rule in-session
|
|
4125
4170
|
// as across sessions, so a later data refresh can't override user choices.
|
|
4126
4171
|
hadSavedColumnSettings.current = true
|
|
4127
4172
|
// allColumnKeysSig, not allColumnKeys: see the memo above.
|
|
4128
|
-
}, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group, allColumnKeysSig, tableActive])
|
|
4173
|
+
}, [visibleColumns, columnWidths, customColumns, columnOrder, selectedKind.name, selectedKind.group, allColumnKeysSig, tableActive])
|
|
4129
4174
|
|
|
4130
4175
|
// Close column picker on outside click or Escape
|
|
4131
4176
|
useEffect(() => {
|
|
@@ -4233,6 +4278,7 @@ export function ResourcesView({
|
|
|
4233
4278
|
const customKeys = new Set(builtCustomColumns.map(c => c.key))
|
|
4234
4279
|
setVisibleColumns(getDefaultVisibleColumns(allColumns.filter(c => !customKeys.has(c.key))))
|
|
4235
4280
|
setColumnWidths({})
|
|
4281
|
+
setColumnOrder(null)
|
|
4236
4282
|
setSortColumn(null)
|
|
4237
4283
|
setSortDirection(null)
|
|
4238
4284
|
saveSavedSort(selectedKind.name, selectedKind.group, null)
|
|
@@ -4552,14 +4598,11 @@ export function ResourcesView({
|
|
|
4552
4598
|
{
|
|
4553
4599
|
id: 'resources-bulk-select',
|
|
4554
4600
|
keys: 'b',
|
|
4555
|
-
description: '
|
|
4601
|
+
description: 'Show or hide selection checkboxes',
|
|
4556
4602
|
category: 'Table',
|
|
4557
4603
|
scope: 'resources',
|
|
4558
4604
|
handler: () => {
|
|
4559
|
-
if (
|
|
4560
|
-
if (!canBulkSelect) return
|
|
4561
|
-
exitCompareMode()
|
|
4562
|
-
setBulkMode(true)
|
|
4605
|
+
if (canBulkSelect) toggleCheckboxVisibility()
|
|
4563
4606
|
},
|
|
4564
4607
|
},
|
|
4565
4608
|
{
|
|
@@ -4574,7 +4617,7 @@ export function ResourcesView({
|
|
|
4574
4617
|
if (showProblemsDropdown) { setShowProblemsDropdown(false); return }
|
|
4575
4618
|
if (showLabelsDropdown) { setShowLabelsDropdown(false); return }
|
|
4576
4619
|
if (compareMode) { exitCompareMode(); return }
|
|
4577
|
-
if (
|
|
4620
|
+
if (checkedResources.size > 0) { clearBulkSelection(); return }
|
|
4578
4621
|
if (highlightedIndex >= 0) setHighlightedIndex(-1)
|
|
4579
4622
|
else searchInputRef.current?.blur()
|
|
4580
4623
|
},
|
|
@@ -5145,16 +5188,16 @@ export function ResourcesView({
|
|
|
5145
5188
|
}
|
|
5146
5189
|
|
|
5147
5190
|
const existing = next[selectedKindCountKey]
|
|
5148
|
-
if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt) {
|
|
5191
|
+
if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt && existing.coverage === selectedQuery?.coverage) {
|
|
5149
5192
|
return changed ? next : prev
|
|
5150
5193
|
}
|
|
5151
5194
|
|
|
5152
5195
|
return {
|
|
5153
5196
|
...next,
|
|
5154
|
-
[selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt },
|
|
5197
|
+
[selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt, coverage: selectedQuery?.coverage },
|
|
5155
5198
|
}
|
|
5156
5199
|
})
|
|
5157
|
-
}, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt])
|
|
5200
|
+
}, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt, selectedQuery?.coverage])
|
|
5158
5201
|
|
|
5159
5202
|
// Derive counts — prefer lightweight resourceCounts prop over full query data
|
|
5160
5203
|
const counts = useMemo(() => {
|
|
@@ -5175,6 +5218,12 @@ export function ResourcesView({
|
|
|
5175
5218
|
return results
|
|
5176
5219
|
}, [useNewCountsMode, resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache, selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, resourceQueries])
|
|
5177
5220
|
|
|
5221
|
+
const sidebarResourceCoverage = useMemo(() => {
|
|
5222
|
+
const result = deriveSidebarResourceCoverage(resourceCountsProp, resourceCoverageProp, loadedCountCache)
|
|
5223
|
+
if (selectedQueryHasLoadedCount) result[selectedKindCountKey] = selectedQuery?.coverage ?? { status: 'unknown' }
|
|
5224
|
+
return result
|
|
5225
|
+
}, [resourceCountsProp, resourceCoverageProp, loadedCountCache, selectedQueryHasLoadedCount, selectedKindCountKey, selectedQuery?.coverage])
|
|
5226
|
+
|
|
5178
5227
|
const sidebarResourceUnavailable = useMemo(() => {
|
|
5179
5228
|
if (!resourceUnavailableProp?.length) {
|
|
5180
5229
|
return resourceUnavailableProp
|
|
@@ -5292,6 +5341,15 @@ export function ResourcesView({
|
|
|
5292
5341
|
saveSavedSort(selectedKind.name, selectedKind.group, newColumn && newDirection ? { column: newColumn, direction: newDirection } : null)
|
|
5293
5342
|
}, [sortColumn, sortDirection, canSortBy, selectedKind.name, selectedKind.group])
|
|
5294
5343
|
|
|
5344
|
+
const getDisplayFilterValue = useCallback((resource: { metadata?: { namespace?: string; name?: string } }, column: string, kind: string): string => {
|
|
5345
|
+
if (column === 'status' && selectedKind.group === 'batch' && kind === 'cronjobs') {
|
|
5346
|
+
const issue = operationalIssues?.[`${resource.metadata?.namespace ?? ''}/${resource.metadata?.name ?? ''}`]
|
|
5347
|
+
if (issue) return issueTitle(issue)
|
|
5348
|
+
if (operationalIssuesIncomplete) return 'Unknown'
|
|
5349
|
+
}
|
|
5350
|
+
return getCellFilterValue(resource, column, kind)
|
|
5351
|
+
}, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
|
|
5352
|
+
|
|
5295
5353
|
// Get sortable value from a resource for a given column
|
|
5296
5354
|
const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
|
|
5297
5355
|
const meta = resource.metadata || {}
|
|
@@ -5320,7 +5378,7 @@ export function ResourcesView({
|
|
|
5320
5378
|
if (hasCuratedColumns(selectedKind.name, selectedKind.group) && status.phase) {
|
|
5321
5379
|
return status.phase
|
|
5322
5380
|
}
|
|
5323
|
-
return
|
|
5381
|
+
return getDisplayFilterValue(resource, 'status', kindLower)
|
|
5324
5382
|
case 'servers':
|
|
5325
5383
|
// Numeric so 10 does not sort before 9.
|
|
5326
5384
|
if (kindLower === 'istiogateways') return getIstioGatewayServerCount(resource)
|
|
@@ -5410,7 +5468,7 @@ export function ResourcesView({
|
|
|
5410
5468
|
default:
|
|
5411
5469
|
return ''
|
|
5412
5470
|
}
|
|
5413
|
-
}, [metricsLookup, selectedKind.name, selectedKind.group])
|
|
5471
|
+
}, [metricsLookup, selectedKind.name, selectedKind.group, getDisplayFilterValue])
|
|
5414
5472
|
|
|
5415
5473
|
// Helper to check if a pod matches problem filters
|
|
5416
5474
|
const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
|
|
@@ -5433,13 +5491,18 @@ export function ResourcesView({
|
|
|
5433
5491
|
}
|
|
5434
5492
|
}, [regexMode, deferredSearchTerm])
|
|
5435
5493
|
|
|
5494
|
+
const isTrivyKind = selectedKind.group === TRIVY_GROUP
|
|
5495
|
+
|
|
5436
5496
|
// Filter resources by search term, status, problems, and sort
|
|
5437
|
-
const filteredResources = useMemo(() => {
|
|
5438
|
-
if (!tableActive || !resources) return []
|
|
5497
|
+
const { list: filteredResources, findings: findingsSearch } = useMemo((): { list: any[]; findings: TrivyFindingsSearch | null } => {
|
|
5498
|
+
if (!tableActive || !resources) return { list: [], findings: null }
|
|
5439
5499
|
|
|
5440
5500
|
let result = resources
|
|
5501
|
+
let findings: TrivyFindingsSearch | null = null
|
|
5441
5502
|
|
|
5442
|
-
// Apply search filter
|
|
5503
|
+
// Apply search filter. On Trivy report kinds a plain search also matches
|
|
5504
|
+
// findings and runs below, after the other filters. Regex mode stays on
|
|
5505
|
+
// names: a pattern run over every finding could backtrack for seconds.
|
|
5443
5506
|
if (deferredSearchTerm) {
|
|
5444
5507
|
if (regexMode) {
|
|
5445
5508
|
const re = searchRegex.re
|
|
@@ -5449,7 +5512,7 @@ export function ResourcesView({
|
|
|
5449
5512
|
re.test(r.metadata?.namespace ?? '')
|
|
5450
5513
|
)
|
|
5451
5514
|
}
|
|
5452
|
-
} else {
|
|
5515
|
+
} else if (!isTrivyKind) {
|
|
5453
5516
|
const term = deferredSearchTerm.toLowerCase()
|
|
5454
5517
|
result = result.filter((r: any) =>
|
|
5455
5518
|
r.metadata?.name?.toLowerCase().includes(term) ||
|
|
@@ -5467,7 +5530,7 @@ export function ResourcesView({
|
|
|
5467
5530
|
result = result.filter((r: any) =>
|
|
5468
5531
|
activeColFilters.every(([col, vals]) => {
|
|
5469
5532
|
const extra = extraColumnsByKey.get(col)
|
|
5470
|
-
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) :
|
|
5533
|
+
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getDisplayFilterValue(r, col, cellFilterKind)
|
|
5471
5534
|
const match = vals.includes(cellVal)
|
|
5472
5535
|
return columnFilterExcludes[col] ? !match : match
|
|
5473
5536
|
})
|
|
@@ -5525,6 +5588,19 @@ export function ResourcesView({
|
|
|
5525
5588
|
})
|
|
5526
5589
|
}
|
|
5527
5590
|
|
|
5591
|
+
// Trivy findings are searched after the other filters, so rows they hide
|
|
5592
|
+
// don't spend the findings budget.
|
|
5593
|
+
if (deferredSearchTerm && !regexMode && isTrivyKind) {
|
|
5594
|
+
const term = deferredSearchTerm.toLowerCase()
|
|
5595
|
+
const matched = searchTrivyFindings(result, term)
|
|
5596
|
+
findings = matched
|
|
5597
|
+
result = result.filter((r: any) =>
|
|
5598
|
+
r.metadata?.name?.toLowerCase().includes(term) ||
|
|
5599
|
+
r.metadata?.namespace?.toLowerCase().includes(term) ||
|
|
5600
|
+
matched.matches.has(r)
|
|
5601
|
+
)
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5528
5604
|
// Apply custom sorting if set. Extra columns override
|
|
5529
5605
|
// the built-in getSortValue for their key.
|
|
5530
5606
|
if (effectiveSortColumn && sortDirection) {
|
|
@@ -5620,8 +5696,8 @@ export function ResourcesView({
|
|
|
5620
5696
|
}
|
|
5621
5697
|
}
|
|
5622
5698
|
|
|
5623
|
-
return result
|
|
5624
|
-
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
5699
|
+
return { list: result, findings }
|
|
5700
|
+
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, isTrivyKind, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
5625
5701
|
|
|
5626
5702
|
// For nodes table: compute the majority minor version so outliers can be highlighted
|
|
5627
5703
|
const majorityNodeMinorVersion = useMemo(() => {
|
|
@@ -5746,16 +5822,25 @@ export function ResourcesView({
|
|
|
5746
5822
|
setCheckedResources(allVisibleChecked ? new Set() : new Set(filteredResources.map(getResourceKey)))
|
|
5747
5823
|
}, [allVisibleChecked, filteredResources, getResourceKey])
|
|
5748
5824
|
|
|
5749
|
-
const
|
|
5825
|
+
const toggleCheckboxVisibility = useCallback(() => {
|
|
5826
|
+
const next = compareMode || !showCheckboxes
|
|
5827
|
+
exitCompareMode()
|
|
5828
|
+
clearBulkSelection()
|
|
5829
|
+
setShowCheckboxes(next)
|
|
5830
|
+
try { localStorage.setItem(CHECKBOX_VISIBILITY_KEY, String(next)) }
|
|
5831
|
+
catch { /* Keep the choice for this session when storage is unavailable. */ }
|
|
5832
|
+
}, [compareMode, showCheckboxes, exitCompareMode, clearBulkSelection])
|
|
5833
|
+
|
|
5834
|
+
const isCheckboxMode = canBulkSelect && !compareMode && showCheckboxes
|
|
5835
|
+
const showBulkBar = isCheckboxMode && checkedItems.length > 0
|
|
5750
5836
|
|
|
5751
5837
|
// Stable row handlers so the memoized ResourceRowCells skips re-rendering
|
|
5752
5838
|
// rows whose data didn't change on a refetch (React Query's structural
|
|
5753
5839
|
// sharing keeps unchanged resources referentially identical).
|
|
5754
5840
|
const handleRowClick = useCallback((resource: any, isSelected: boolean) => {
|
|
5755
5841
|
if (compareMode) toggleComparePick(resource)
|
|
5756
|
-
else if (isCheckboxMode) toggleChecked(resource)
|
|
5757
5842
|
else selectResource(resource, isSelected)
|
|
5758
|
-
}, [compareMode,
|
|
5843
|
+
}, [compareMode, toggleComparePick, selectResource])
|
|
5759
5844
|
const handleRowMouseEnter = useCallback(() => setHighlightedIndex(-1), [])
|
|
5760
5845
|
|
|
5761
5846
|
// Filter columns by visibility
|
|
@@ -5847,7 +5932,7 @@ export function ResourcesView({
|
|
|
5847
5932
|
// Count distinct values for this column
|
|
5848
5933
|
const valueCounts: Record<string, number> = {}
|
|
5849
5934
|
for (const r of resources) {
|
|
5850
|
-
const val = extra?.getFilterValue ? extra.getFilterValue(r) :
|
|
5935
|
+
const val = extra?.getFilterValue ? extra.getFilterValue(r) : getDisplayFilterValue(r, col.key, cellFilterKind)
|
|
5851
5936
|
if (val) {
|
|
5852
5937
|
valueCounts[val] = (valueCounts[val] || 0) + 1
|
|
5853
5938
|
}
|
|
@@ -5930,7 +6015,7 @@ export function ResourcesView({
|
|
|
5930
6015
|
|
|
5931
6016
|
if (filterableColumns.length === 0 && !problems && labelValues.length === 0) return null
|
|
5932
6017
|
return { columns: filterableColumns, problems, labels: labelValues }
|
|
5933
|
-
}, [tableActive, resources, selectedKind.name, selectedKind.group, allColumns])
|
|
6018
|
+
}, [tableActive, resources, selectedKind.name, selectedKind.group, allColumns, getDisplayFilterValue])
|
|
5934
6019
|
|
|
5935
6020
|
// Map filterable columns by key for O(1) lookup in header rendering
|
|
5936
6021
|
const filterableColumnMap = useMemo(() => {
|
|
@@ -6005,9 +6090,11 @@ export function ResourcesView({
|
|
|
6005
6090
|
certExpiry,
|
|
6006
6091
|
certExpiryError,
|
|
6007
6092
|
auditBadges,
|
|
6093
|
+
operationalIssues,
|
|
6094
|
+
operationalIssuesIncomplete,
|
|
6008
6095
|
onOpenLogs,
|
|
6009
6096
|
onOpenWorkloadLogs,
|
|
6010
|
-
}), [onNavigate, certExpiry, certExpiryError, auditBadges, onOpenLogs, onOpenWorkloadLogs])
|
|
6097
|
+
}), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
|
|
6011
6098
|
|
|
6012
6099
|
return (
|
|
6013
6100
|
<ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
|
|
@@ -6023,6 +6110,7 @@ export function ResourcesView({
|
|
|
6023
6110
|
onKindChange={onKindChange}
|
|
6024
6111
|
apiResources={apiResourcesProp}
|
|
6025
6112
|
resourceCounts={counts}
|
|
6113
|
+
resourceCoverage={sidebarResourceCoverage}
|
|
6026
6114
|
resourceForbidden={Array.from(forbiddenKinds)}
|
|
6027
6115
|
resourceUnavailable={sidebarResourceUnavailable}
|
|
6028
6116
|
pinned={pinned}
|
|
@@ -6048,7 +6136,7 @@ export function ResourcesView({
|
|
|
6048
6136
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
6049
6137
|
<Input
|
|
6050
6138
|
ref={searchInputRef}
|
|
6051
|
-
placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
|
|
6139
|
+
placeholder={regexMode ? 'Search by regex... (press /)' : isTrivyKind ? 'Search names and findings... (press /)' : 'Search... (press /)'}
|
|
6052
6140
|
value={searchTerm}
|
|
6053
6141
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
6054
6142
|
onKeyDown={(e) => {
|
|
@@ -6110,6 +6198,11 @@ export function ResourcesView({
|
|
|
6110
6198
|
<X className="w-3.5 h-3.5" />
|
|
6111
6199
|
</button>
|
|
6112
6200
|
)}
|
|
6201
|
+
{!searchRegex.error && findingsSearch?.truncated && (
|
|
6202
|
+
<div className="absolute left-0 top-full mt-1 z-10 px-2 py-1 rounded bg-theme-elevated border border-theme-border text-xs text-theme-text-secondary shadow-theme-sm">
|
|
6203
|
+
Too many findings to search them all here; some weren't searched.
|
|
6204
|
+
</div>
|
|
6205
|
+
)}
|
|
6113
6206
|
{searchRegex.error && (
|
|
6114
6207
|
<div
|
|
6115
6208
|
title={searchRegex.error}
|
|
@@ -6316,41 +6409,14 @@ export function ResourcesView({
|
|
|
6316
6409
|
{/* Column list scrolls; the add-column footer stays pinned so
|
|
6317
6410
|
its input isn't pushed below the fold on kinds with many columns. */}
|
|
6318
6411
|
<div className="overflow-y-auto flex-1 min-h-0">
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
<input
|
|
6328
|
-
type="checkbox"
|
|
6329
|
-
checked={visibleColumns.has(col.key)}
|
|
6330
|
-
onChange={() => toggleColumnVisibility(col.key)}
|
|
6331
|
-
disabled={col.key === 'name'}
|
|
6332
|
-
className="rounded border-theme-border"
|
|
6333
|
-
/>
|
|
6334
|
-
<span className={clsx(
|
|
6335
|
-
'text-sm truncate',
|
|
6336
|
-
col.key === 'name' ? 'text-theme-text-tertiary' : 'text-theme-text-primary'
|
|
6337
|
-
)} title={col.tooltip || col.label}>
|
|
6338
|
-
{col.label}
|
|
6339
|
-
</span>
|
|
6340
|
-
</label>
|
|
6341
|
-
{isCustom && (
|
|
6342
|
-
<button
|
|
6343
|
-
type="button"
|
|
6344
|
-
onClick={() => removeCustomColumn(col.key)}
|
|
6345
|
-
className="shrink-0 p-0.5 text-theme-text-tertiary hover:text-red-500 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
|
6346
|
-
title="Remove column"
|
|
6347
|
-
>
|
|
6348
|
-
<X className="w-3.5 h-3.5" />
|
|
6349
|
-
</button>
|
|
6350
|
-
)}
|
|
6351
|
-
</div>
|
|
6352
|
-
)
|
|
6353
|
-
})}
|
|
6412
|
+
<ColumnPickerList
|
|
6413
|
+
columns={allColumns}
|
|
6414
|
+
visibleColumns={visibleColumns}
|
|
6415
|
+
customColumnKeys={customColumnKeySet}
|
|
6416
|
+
onToggle={toggleColumnVisibility}
|
|
6417
|
+
onRemoveCustom={removeCustomColumn}
|
|
6418
|
+
onReorder={setColumnOrder}
|
|
6419
|
+
/>
|
|
6354
6420
|
</div>
|
|
6355
6421
|
{/* Add a label/annotation column */}
|
|
6356
6422
|
<div className="shrink-0 border-t border-theme-border pt-2 px-3 pb-1">
|
|
@@ -6420,7 +6486,7 @@ export function ResourcesView({
|
|
|
6420
6486
|
<button
|
|
6421
6487
|
onClick={() => {
|
|
6422
6488
|
if (compareMode) { exitCompareMode(); return }
|
|
6423
|
-
|
|
6489
|
+
clearBulkSelection()
|
|
6424
6490
|
setCompareMode(true)
|
|
6425
6491
|
}}
|
|
6426
6492
|
aria-pressed={compareMode}
|
|
@@ -6436,17 +6502,14 @@ export function ResourcesView({
|
|
|
6436
6502
|
</Tooltip>
|
|
6437
6503
|
)}
|
|
6438
6504
|
{canBulkSelect && (
|
|
6439
|
-
<Tooltip content={
|
|
6505
|
+
<Tooltip content={isCheckboxMode ? 'Hide selection checkboxes' : 'Show selection checkboxes'}>
|
|
6440
6506
|
<button
|
|
6441
|
-
onClick={
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
setBulkMode(true)
|
|
6445
|
-
}}
|
|
6446
|
-
aria-pressed={bulkMode}
|
|
6507
|
+
onClick={toggleCheckboxVisibility}
|
|
6508
|
+
aria-label={isCheckboxMode ? 'Hide selection checkboxes' : 'Show selection checkboxes'}
|
|
6509
|
+
aria-pressed={isCheckboxMode}
|
|
6447
6510
|
className={clsx(
|
|
6448
6511
|
'p-2 rounded-lg transition-colors',
|
|
6449
|
-
|
|
6512
|
+
isCheckboxMode
|
|
6450
6513
|
? 'bg-skyhook-500/15 text-skyhook-300 border border-skyhook-400/50'
|
|
6451
6514
|
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated',
|
|
6452
6515
|
)}
|
|
@@ -6466,7 +6529,7 @@ export function ResourcesView({
|
|
|
6466
6529
|
</div>
|
|
6467
6530
|
|
|
6468
6531
|
{/* Bulk actions bar */}
|
|
6469
|
-
{
|
|
6532
|
+
{showBulkBar && (
|
|
6470
6533
|
<div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
|
|
6471
6534
|
<span className="text-sm font-medium text-theme-text-primary">
|
|
6472
6535
|
{checkedItems.length} selected
|
|
@@ -6506,14 +6569,26 @@ export function ResourcesView({
|
|
|
6506
6569
|
)}
|
|
6507
6570
|
<button
|
|
6508
6571
|
type="button"
|
|
6509
|
-
onClick={
|
|
6572
|
+
onClick={clearBulkSelection}
|
|
6510
6573
|
className="px-3 py-1.5 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors"
|
|
6511
6574
|
>
|
|
6512
|
-
|
|
6575
|
+
Clear selection
|
|
6513
6576
|
</button>
|
|
6514
6577
|
</div>
|
|
6515
6578
|
)}
|
|
6516
6579
|
|
|
6580
|
+
{!isLoading && !selectedQueryError && resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind)) && (
|
|
6581
|
+
<div role="status" className="px-4 py-2 text-xs text-warning-text border-b border-theme-border bg-theme-surface">
|
|
6582
|
+
{resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind))}
|
|
6583
|
+
</div>
|
|
6584
|
+
)}
|
|
6585
|
+
|
|
6586
|
+
{!isLoading && !selectedQueryError && resourceNarrowedNotice(selectedQuery?.coverage) && (
|
|
6587
|
+
<div role="status" className="px-4 py-2 text-xs text-theme-text-tertiary border-b border-theme-border bg-theme-surface">
|
|
6588
|
+
{resourceNarrowedNotice(selectedQuery?.coverage)}
|
|
6589
|
+
</div>
|
|
6590
|
+
)}
|
|
6591
|
+
|
|
6517
6592
|
{!isLoading && !isSelectedForbidden && !largeListGuard && filteredResources.length > 0 && (
|
|
6518
6593
|
<ActiveFilterBar
|
|
6519
6594
|
columnFilters={columnFilters}
|
|
@@ -6555,7 +6630,7 @@ export function ResourcesView({
|
|
|
6555
6630
|
/>
|
|
6556
6631
|
</div>
|
|
6557
6632
|
</div>
|
|
6558
|
-
) : selectedQueryError && !Array.isArray(resources) ? (
|
|
6633
|
+
) : selectedQueryError && !Array.isArray(resources) && !largeListGuard ? (
|
|
6559
6634
|
// A failed fetch with no rows to fall back on must not reach the
|
|
6560
6635
|
// empty state below — it would tell the user the cluster has none.
|
|
6561
6636
|
<FetchResult
|
|
@@ -6575,7 +6650,9 @@ export function ResourcesView({
|
|
|
6575
6650
|
<p className="text-sm mt-1 max-w-xl">
|
|
6576
6651
|
{largeListGuard.reason === 'count-unavailable'
|
|
6577
6652
|
? 'Radar could not verify this list is small enough to load. Choose a namespace or smaller namespace set to try again.'
|
|
6578
|
-
:
|
|
6653
|
+
: largeListGuard.count == null
|
|
6654
|
+
? 'This list exceeds the resource limit. Choose a namespace or smaller namespace set to load this view.'
|
|
6655
|
+
: `${largeListGuard.count.toLocaleString()} ${largeListGuard.kind.toLowerCase()} were read in the current scope. Choose a namespace or smaller namespace set to load this view.`}
|
|
6579
6656
|
</p>
|
|
6580
6657
|
<p className="text-xs mt-2 text-theme-text-disabled">
|
|
6581
6658
|
Radar limits full table loads to {largeListGuard.limit.toLocaleString()} resources to keep the UI responsive.
|
|
@@ -6586,8 +6663,8 @@ export function ResourcesView({
|
|
|
6586
6663
|
</div>
|
|
6587
6664
|
) : filteredResources.length === 0 ? (
|
|
6588
6665
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary">
|
|
6589
|
-
<p>
|
|
6590
|
-
{emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
|
|
6666
|
+
<p>{resourceEmptyMessage(selectedKind.kind, selectedQuery?.coverage, (resources?.length ?? 0) > 0)}</p>
|
|
6667
|
+
{selectedQuery?.coverage?.status !== 'partial' && emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
|
|
6591
6668
|
<p className="text-xs mt-2 max-w-md text-center text-theme-text-disabled">
|
|
6592
6669
|
{emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0)}
|
|
6593
6670
|
</p>
|
|
@@ -6604,6 +6681,13 @@ export function ResourcesView({
|
|
|
6604
6681
|
<X className="w-3.5 h-3.5" />
|
|
6605
6682
|
</button>
|
|
6606
6683
|
)}
|
|
6684
|
+
{searchTerm && isTrivyKind && (
|
|
6685
|
+
<p className="text-xs mt-1 text-theme-text-tertiary">
|
|
6686
|
+
{regexMode
|
|
6687
|
+
? 'Regex search matches names and namespaces only.'
|
|
6688
|
+
: 'Searched names, namespaces and report findings (CVE, package, check ID, image).'}
|
|
6689
|
+
</p>
|
|
6690
|
+
)}
|
|
6607
6691
|
{namespaces.length > 0 && <p className="text-sm mt-1 text-theme-text-disabled">Searching in {namespaces.length === 1 ? `namespace: ${namespaces[0]}` : `${namespaces.length} namespaces`}</p>}
|
|
6608
6692
|
<ActiveFilterBar
|
|
6609
6693
|
columnFilters={columnFilters}
|
|
@@ -6912,6 +6996,7 @@ export function ResourcesView({
|
|
|
6912
6996
|
comparePickIndex={pickIdx}
|
|
6913
6997
|
rowHref={rowHrefFor?.(resource)}
|
|
6914
6998
|
onRowCheckToggle={toggleChecked}
|
|
6999
|
+
findingMatch={findingsSearch?.matches.get(resource)}
|
|
6915
7000
|
/>
|
|
6916
7001
|
)
|
|
6917
7002
|
}}
|
|
@@ -6948,7 +7033,7 @@ export function ResourcesView({
|
|
|
6948
7033
|
onBulkDelete?.(checkedBulkItems, {
|
|
6949
7034
|
force: bulkForceDelete,
|
|
6950
7035
|
onSuccess: () => {
|
|
6951
|
-
|
|
7036
|
+
clearBulkSelection()
|
|
6952
7037
|
setShowBulkDeleteConfirm(false)
|
|
6953
7038
|
setBulkForceDelete(false)
|
|
6954
7039
|
},
|
|
@@ -6978,7 +7063,7 @@ export function ResourcesView({
|
|
|
6978
7063
|
onConfirm={() => {
|
|
6979
7064
|
onBulkRestart?.(checkedBulkItems, {
|
|
6980
7065
|
onSuccess: () => {
|
|
6981
|
-
|
|
7066
|
+
clearBulkSelection()
|
|
6982
7067
|
setShowBulkRestartConfirm(false)
|
|
6983
7068
|
},
|
|
6984
7069
|
})
|
|
@@ -6997,7 +7082,7 @@ export function ResourcesView({
|
|
|
6997
7082
|
onConfirm={() => {
|
|
6998
7083
|
onBulkScale?.(checkedBulkItems, bulkScaleReplicas, {
|
|
6999
7084
|
onSuccess: () => {
|
|
7000
|
-
|
|
7085
|
+
clearBulkSelection()
|
|
7001
7086
|
setShowBulkScaleDialog(false)
|
|
7002
7087
|
},
|
|
7003
7088
|
})
|
|
@@ -7079,6 +7164,9 @@ interface ResourceRowCellsProps {
|
|
|
7079
7164
|
* is unaffected (still toggles picks). */
|
|
7080
7165
|
rowHref?: string
|
|
7081
7166
|
onRowCheckToggle?: (resource: any) => void
|
|
7167
|
+
/** What a Trivy report matched the table search through, when its name
|
|
7168
|
+
* and namespace didn't. */
|
|
7169
|
+
findingMatch?: TrivyFindingMatch
|
|
7082
7170
|
}
|
|
7083
7171
|
|
|
7084
7172
|
function rowHighlightClass(
|
|
@@ -7100,7 +7188,7 @@ function rowHighlightClass(
|
|
|
7100
7188
|
return 'group-hover/row:bg-theme-surface/50'
|
|
7101
7189
|
}
|
|
7102
7190
|
|
|
7103
|
-
const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, isChecked, showCheckbox, majorityNodeMinorVersion, storeProviders, onRowClick, onRowMouseEnter, compareMode, comparePickIndex = -1, rowHref, onRowCheckToggle }: ResourceRowCellsProps) {
|
|
7191
|
+
const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, isChecked, showCheckbox, majorityNodeMinorVersion, storeProviders, onRowClick, onRowMouseEnter, compareMode, comparePickIndex = -1, rowHref, onRowCheckToggle, findingMatch }: ResourceRowCellsProps) {
|
|
7104
7192
|
const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted, isChecked)
|
|
7105
7193
|
const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
|
|
7106
7194
|
const onClick = onRowClick ? () => onRowClick(resource, !!isSelected) : undefined
|
|
@@ -7170,6 +7258,7 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7170
7258
|
storeProviders={storeProviders}
|
|
7171
7259
|
extraColumn={extraColumnsByKey?.get(col.key)}
|
|
7172
7260
|
nameHref={col.key === 'name' ? rowHref : undefined}
|
|
7261
|
+
findingMatch={col.key === 'name' ? findingMatch : undefined}
|
|
7173
7262
|
/>
|
|
7174
7263
|
</td>
|
|
7175
7264
|
))}
|
|
@@ -7219,9 +7308,10 @@ interface CellContentProps {
|
|
|
7219
7308
|
/** When set on the name column, the resource name renders as `<a href>`
|
|
7220
7309
|
* so ⌘-click / copy-link / hover-URL all work. */
|
|
7221
7310
|
nameHref?: string
|
|
7311
|
+
findingMatch?: TrivyFindingMatch
|
|
7222
7312
|
}
|
|
7223
7313
|
|
|
7224
|
-
function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref }: CellContentProps) {
|
|
7314
|
+
function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref, findingMatch }: CellContentProps) {
|
|
7225
7315
|
const { auditBadges } = useContext(ResourcesViewDataContext)
|
|
7226
7316
|
// Parent-injected extra columns short-circuit the built-in switch.
|
|
7227
7317
|
// Used by hosts that inject leading columns (e.g. a multi-cluster Cluster column).
|
|
@@ -7239,7 +7329,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7239
7329
|
const auditTotal = audit ? audit.danger + audit.warning : 0
|
|
7240
7330
|
return (
|
|
7241
7331
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
7242
|
-
<Tooltip content={meta.name}>
|
|
7332
|
+
<Tooltip content={meta.name} wrapperClassName={findingMatch ? 'min-w-0' : undefined}>
|
|
7243
7333
|
{nameHref ? (
|
|
7244
7334
|
<a
|
|
7245
7335
|
href={nameHref}
|
|
@@ -7254,6 +7344,13 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7254
7344
|
)}
|
|
7255
7345
|
</Tooltip>
|
|
7256
7346
|
<CopyNameButton name={meta.name} />
|
|
7347
|
+
{findingMatch && (
|
|
7348
|
+
<Tooltip content={findingMatch.text}>
|
|
7349
|
+
<span className="shrink-0 text-xs italic text-theme-text-tertiary cursor-help">
|
|
7350
|
+
{findingMatch.site === 'image' ? 'in image' : 'in findings'}
|
|
7351
|
+
</span>
|
|
7352
|
+
</Tooltip>
|
|
7353
|
+
)}
|
|
7257
7354
|
{auditTotal > 0 && audit && (
|
|
7258
7355
|
<Tooltip content={audit.messages && audit.messages.length > 0
|
|
7259
7356
|
? <AuditBadgeTooltip messages={audit.messages} />
|
|
@@ -7390,7 +7487,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7390
7487
|
// Disambiguate Gateway API vs Istio Gateway by apiVersion. Retained
|
|
7391
7488
|
// alongside the case above because not every call site normalizes the
|
|
7392
7489
|
// kind through GROUP_QUALIFIED_COLUMN_KEYS before dispatching.
|
|
7393
|
-
if (resource.apiVersion
|
|
7490
|
+
if (isApiGroup(resource.apiVersion, 'networking.istio.io')) {
|
|
7394
7491
|
return <IstioGatewayCell resource={resource} column={column} />
|
|
7395
7492
|
}
|
|
7396
7493
|
return <GatewayCell resource={resource} column={column} />
|
|
@@ -8629,6 +8726,7 @@ function JobCell({ resource, column }: { resource: any; column: string }) {
|
|
|
8629
8726
|
}
|
|
8630
8727
|
|
|
8631
8728
|
function CronJobCell({ resource, column }: { resource: any; column: string }) {
|
|
8729
|
+
const { operationalIssues, operationalIssuesIncomplete } = useContext(ResourcesViewDataContext)
|
|
8632
8730
|
switch (column) {
|
|
8633
8731
|
case 'schedule': {
|
|
8634
8732
|
const { cron, readable } = getCronJobSchedule(resource)
|
|
@@ -8640,6 +8738,13 @@ function CronJobCell({ resource, column }: { resource: any; column: string }) {
|
|
|
8640
8738
|
)
|
|
8641
8739
|
}
|
|
8642
8740
|
case 'status': {
|
|
8741
|
+
const issue = operationalIssues?.[`${resource.metadata?.namespace ?? ''}/${resource.metadata?.name ?? ''}`]
|
|
8742
|
+
if (issue) {
|
|
8743
|
+
return <Tooltip content={issue.message}><Badge severity={issue.severity === 'critical' ? 'error' : 'warning'}>{issueTitle(issue)}</Badge></Tooltip>
|
|
8744
|
+
}
|
|
8745
|
+
if (operationalIssuesIncomplete) {
|
|
8746
|
+
return <Tooltip content="The issue list is incomplete. This CronJob's status could not be checked."><Badge severity="neutral">Unknown</Badge></Tooltip>
|
|
8747
|
+
}
|
|
8643
8748
|
const status = getCronJobStatus(resource)
|
|
8644
8749
|
return (
|
|
8645
8750
|
<span className={clsx('badge', status.color)}>
|