@skyhook-io/k8s-ui 1.18.0 → 1.18.2
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 +21 -0
- package/package.json +1 -1
- package/src/components/applications/ApplicationsView.tsx +8 -7
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
- package/src/components/compare/CompareResourcePicker.tsx +18 -4
- package/src/components/compare/ResourceCompareView.tsx +6 -2
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
- package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
- package/src/components/gitops/GitOpsTableView.tsx +46 -12
- 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/issues/IssuesView.tsx +16 -11
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/severity.ts +1 -0
- package/src/components/logs/LogCore.tsx +13 -11
- package/src/components/logs/LogsViewer.test.tsx +104 -59
- package/src/components/logs/LogsViewer.tsx +28 -8
- package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
- package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
- package/src/components/namespace-switcher/NamespacePicker.tsx +44 -8
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
- package/src/components/resources/ResourcesSidebar.tsx +62 -26
- package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
- package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
- package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
- 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 +342 -83
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +15 -0
- package/src/components/resources/printer-columns.test.ts +36 -0
- package/src/components/resources/printer-columns.ts +12 -1
- package/src/components/resources/readiness-gates.test.ts +95 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
- package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
- package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
- package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
- package/src/components/resources/renderers/PodRenderer.tsx +32 -9
- package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
- package/src/components/resources/resource-utils-crossplane.ts +44 -8
- package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
- package/src/components/resources/resource-utils-flagger.ts +76 -0
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
- package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
- 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 +80 -2
- package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
- package/src/components/shared/CreateResourceDialog.tsx +5 -0
- package/src/components/shared/DetailShell.phone.test.tsx +54 -0
- package/src/components/shared/DetailShell.tsx +21 -8
- package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
- package/src/components/shared/EditableYamlView.test.tsx +58 -0
- package/src/components/shared/EditableYamlView.tsx +18 -3
- package/src/components/shared/FilterRailPanel.test.tsx +39 -0
- package/src/components/shared/FilterRailPanel.tsx +62 -0
- package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
- package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
- package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
- package/src/components/shared/ManagedByChip.test.tsx +22 -0
- package/src/components/shared/ManagedByChip.tsx +158 -5
- package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
- package/src/components/shared/ResourceActionsBar.tsx +31 -9
- package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +69 -31
- package/src/components/shared/SetImageDialog.tsx +3 -0
- package/src/components/timeline/TimelineList.tsx +5 -1
- package/src/components/topology/TopologyControls.tsx +1 -1
- package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
- package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
- package/src/components/ui/FetchResult.test.tsx +19 -2
- package/src/components/ui/FetchResult.tsx +13 -1
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
- package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
- package/src/components/ui/MultiSelectPicker.tsx +20 -2
- package/src/components/ui/PageHeader.tsx +7 -4
- package/src/components/ui/SummaryTile.tsx +13 -3
- package/src/components/ui/Toast.tsx +33 -26
- package/src/components/ui/Tooltip.touch.test.tsx +55 -0
- package/src/components/ui/Tooltip.tsx +11 -2
- package/src/components/ui/YamlReview.test.tsx +52 -0
- package/src/components/ui/YamlReview.tsx +7 -0
- package/src/components/ui/drawer-components.test.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +302 -18
- package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +73 -0
- package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
- package/src/components/workload/WorkloadView.tsx +105 -34
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
- package/src/hooks/useKeepInViewport.test.ts +27 -0
- package/src/hooks/useKeepInViewport.ts +87 -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 +119 -3
- package/src/types/gitops-flux-inventory.test.ts +24 -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/api-resources.ts +1 -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 +158 -8
- package/src/utils/gitops-write-guard.test.ts +332 -0
- package/src/utils/gitops-write-guard.ts +175 -11
- package/src/utils/index.ts +3 -0
- package/src/utils/read-actions.test.ts +22 -0
- package/src/utils/read-actions.ts +90 -0
- package/src/utils/resource-coverage.test.ts +121 -0
- package/src/utils/resource-coverage.ts +117 -0
- package/src/utils/touch-intent.test.ts +39 -0
- package/src/utils/touch-intent.ts +35 -0
- package/vitest.config.ts +26 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { getOpenTelemetryColumnValue } from './resource-utils-opentelemetry'
|
|
2
|
+
import { getFlaggerColumnValue } from './resource-utils-flagger'
|
|
3
|
+
import type { ResourceCoverage } from '../../types/resource-coverage'
|
|
4
|
+
import { resourceCoverageNotice, resourceEmptyMessage, resourceNarrowedNotice } from '../../utils/resource-coverage'
|
|
1
5
|
import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
|
|
2
6
|
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
3
7
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue'
|
|
8
|
+
import { usePhoneLayout } from '../../hooks/usePhoneLayout'
|
|
9
|
+
import { CrossplaneDeleteNotice } from '../ui/CrossplaneOperationNotice'
|
|
4
10
|
import { Badge } from '../ui/Badge'
|
|
5
11
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
12
|
+
import { DialogPortal } from '../ui/DialogPortal'
|
|
6
13
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
7
14
|
import { FetchResult } from '../ui/FetchResult'
|
|
8
15
|
import { Input } from '../ui/Input'
|
|
@@ -32,6 +39,7 @@ import {
|
|
|
32
39
|
ListChecks,
|
|
33
40
|
Minus,
|
|
34
41
|
Scale,
|
|
42
|
+
PanelLeft,
|
|
35
43
|
} from 'lucide-react'
|
|
36
44
|
import { clsx } from 'clsx'
|
|
37
45
|
import { ResourceBar } from '../ui/ResourceBar'
|
|
@@ -148,6 +156,7 @@ import {
|
|
|
148
156
|
SEVERITY_DOT_COLOR,
|
|
149
157
|
healthColors,
|
|
150
158
|
} from './resource-utils'
|
|
159
|
+
import { TRIVY_GROUP, searchTrivyFindings, type TrivyFindingMatch, type TrivyFindingsSearch } from './resource-utils-trivy'
|
|
151
160
|
import { getGenericResourceStatus } from './generic-status'
|
|
152
161
|
import { ActiveFilterBar } from './ActiveFilterBar'
|
|
153
162
|
import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
|
|
@@ -156,7 +165,7 @@ import {
|
|
|
156
165
|
printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
|
|
157
166
|
} from './printer-columns'
|
|
158
167
|
import { SEVERITY_BADGE, SEVERITY_TEXT, healthToSeverity, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
159
|
-
import { pluralize } from '../../utils/pluralize'
|
|
168
|
+
import { englishPlural, isEnglishPlural, pluralize } from '../../utils/pluralize'
|
|
160
169
|
import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
|
|
161
170
|
import { formatRelativeAgeTime, parseQuantityToNumber } from '../../utils/format'
|
|
162
171
|
import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
|
|
@@ -201,8 +210,9 @@ import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell, Volum
|
|
|
201
210
|
import { isVeleroResource } from './resource-utils-velero'
|
|
202
211
|
import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell, CNPGObjectStoreCell, CNPGDeclarativeCell, CNPGImageCatalogCell } from './renderers/cnpg-cells'
|
|
203
212
|
import { isApiGroup, CNPG_GROUP } from './resource-utils-cnpg'
|
|
204
|
-
import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
205
|
-
import { isManagedResource, isComposite } from './resource-utils-crossplane'
|
|
213
|
+
import { CrossplaneStatusCell, ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
214
|
+
import { getCrossplaneStatus, isManagedResource, isComposite, getCrossplanePauseState } from './resource-utils-crossplane'
|
|
215
|
+
import { CrossplanePauseChip } from './renderers/CrossplanePauseContext'
|
|
206
216
|
import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
|
|
207
217
|
import { KnativeServiceCell, ConfigurationCell as KnativeConfigurationCell, RevisionCell as KnativeRevisionCell, RouteCell as KnativeRouteCell, BrokerCell, TriggerCell, EventTypeCell, PingSourceCell, ApiServerSourceCell, ContainerSourceCell, SinkBindingCell, ChannelCell, InMemoryChannelCell, SubscriptionCell, SequenceCell, ParallelCell, DomainMappingCell, ServerlessServiceCell, KnativeIngressCell, KnativeCertificateCell } from './renderers/knative-cells'
|
|
208
218
|
import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
|
|
@@ -215,7 +225,7 @@ import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
|
|
|
215
225
|
import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
|
|
216
226
|
import { useRegisterShortcut, useRegisterShortcuts, type KeyboardShortcut } from '../../hooks/useKeyboardShortcuts'
|
|
217
227
|
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
218
|
-
import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
|
|
228
|
+
import type { SelectedKindInfo, SidebarCategoryWorkspace, SidebarDisplayState } from './ResourcesSidebar'
|
|
219
229
|
import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
|
|
220
230
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
221
231
|
|
|
@@ -338,6 +348,24 @@ const COMPARE_COLUMN_STYLE: React.CSSProperties = {
|
|
|
338
348
|
maxWidth: COMPARE_COLUMN_WIDTH,
|
|
339
349
|
}
|
|
340
350
|
|
|
351
|
+
// Phones: the leading columns (compare marker, selection checkbox, then Name
|
|
352
|
+
// when it is the first column) stay in place while the rest of the table
|
|
353
|
+
// scrolls sideways, so every row stays identifiable. The opaque background
|
|
354
|
+
// keeps scrolled cells from showing through; on phones it hides the selection
|
|
355
|
+
// tint on these cells, where a tap opens the full-screen detail anyway.
|
|
356
|
+
// The offset rides a CSS variable that only the phone classes read, so the
|
|
357
|
+
// desktop header (position: relative) never moves.
|
|
358
|
+
const CHECKBOX_COLUMN_WIDTH = 40
|
|
359
|
+
const PHONE_PINNED_CELL = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[1] phone:bg-theme-surface'
|
|
360
|
+
const PHONE_PINNED_HEAD = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[3]'
|
|
361
|
+
const PHONE_NAME_MAX_VW = 45
|
|
362
|
+
function phonePinnedLeft(compareMode: boolean | undefined, showCheckbox: boolean | undefined, column: 'compare' | 'checkbox' | 'name'): React.CSSProperties {
|
|
363
|
+
const compare = compareMode ? COMPARE_COLUMN_WIDTH : 0
|
|
364
|
+
const checkbox = showCheckbox ? CHECKBOX_COLUMN_WIDTH : 0
|
|
365
|
+
const left = column === 'compare' ? 0 : column === 'checkbox' ? compare : compare + checkbox
|
|
366
|
+
return { '--pin-left': `${left}px` } as React.CSSProperties
|
|
367
|
+
}
|
|
368
|
+
|
|
341
369
|
// Built-in sortable keys. Hoisted so getColumnMinWidth can reserve room for the
|
|
342
370
|
// sort icon without the header render and the width math drifting apart.
|
|
343
371
|
const BUILTIN_SORTABLE_COLUMN_KEYS = new Set([
|
|
@@ -551,6 +579,44 @@ const DEFAULT_COLUMNS: Column[] = [
|
|
|
551
579
|
]
|
|
552
580
|
|
|
553
581
|
const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
582
|
+
opentelemetrycollectors: [
|
|
583
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
584
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
585
|
+
{ key: 'status', label: 'Reconciliation', width: 'w-40 shrink-0', tooltip: 'Operator reconciliation only, not Pod health or telemetry delivery.' },
|
|
586
|
+
{ key: 'mode', label: 'Mode', width: 'w-28 shrink-0' },
|
|
587
|
+
{ key: 'reportedPods', label: 'Reported Pods', width: 'w-32 shrink-0', tooltip: 'Last reported ready/total snapshot. Total is observed Pods for Deployment/StatefulSet, desired scheduled Pods for DaemonSet. Inspect the owned workload for current readiness.' },
|
|
588
|
+
{ key: 'version', label: 'Version', width: 'w-24 shrink-0' },
|
|
589
|
+
{ key: 'image', label: 'Image', width: 'min-w-48' },
|
|
590
|
+
{ key: 'management', label: 'Management', width: 'w-28 shrink-0' },
|
|
591
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
592
|
+
],
|
|
593
|
+
instrumentations: [
|
|
594
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
595
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
596
|
+
{ key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
|
|
597
|
+
{ key: 'endpoint', label: 'OTLP Endpoint', width: 'min-w-48' },
|
|
598
|
+
{ key: 'sampler', label: 'Sampler', width: 'w-40 shrink-0' },
|
|
599
|
+
{ key: 'samplerArgument', label: 'Sampler Argument', width: 'w-32 shrink-0' },
|
|
600
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
601
|
+
],
|
|
602
|
+
canaries: [
|
|
603
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
604
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
605
|
+
{ key: 'status', label: 'Rollout State', width: 'w-40 shrink-0', tooltip: 'Last recorded rollout outcome, with requested suspension separate from a failed result. Not a latest-generation guarantee.' },
|
|
606
|
+
{ key: 'strategy', label: 'Strategy', width: 'w-36 shrink-0' },
|
|
607
|
+
{ key: 'target', label: 'Canary Target', width: 'min-w-40' },
|
|
608
|
+
{ key: 'canaryWeight', label: 'Reported Weight', width: 'w-36 shrink-0', tooltip: 'Last reported canary routing weight, not measured traffic. Blue/green and A/B strategies are not weighted.' },
|
|
609
|
+
{ key: 'suspended', label: 'Suspend Requested', width: 'w-36 shrink-0' },
|
|
610
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
611
|
+
],
|
|
612
|
+
metrictemplates: [
|
|
613
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
614
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
615
|
+
{ key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
|
|
616
|
+
{ key: 'provider', label: 'Provider', width: 'w-32 shrink-0' },
|
|
617
|
+
{ key: 'address', label: 'Provider Address', width: 'min-w-48' },
|
|
618
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
619
|
+
],
|
|
554
620
|
pods: [
|
|
555
621
|
{ key: 'name', label: 'Name' },
|
|
556
622
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -2848,6 +2914,10 @@ function isCuratedUnboundedGroup(key: string, group: string): boolean {
|
|
|
2848
2914
|
* cannot occupy one. See CORE_API_GROUPS.
|
|
2849
2915
|
*/
|
|
2850
2916
|
const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
|
|
2917
|
+
opentelemetrycollectors: ['opentelemetry.io'],
|
|
2918
|
+
instrumentations: ['opentelemetry.io'],
|
|
2919
|
+
canaries: ['flagger.app'],
|
|
2920
|
+
metrictemplates: ['flagger.app'],
|
|
2851
2921
|
challenges: ['acme.cert-manager.io'],
|
|
2852
2922
|
orders: ['acme.cert-manager.io'],
|
|
2853
2923
|
compositeresourcedefinitions: ['apiextensions.crossplane.io'],
|
|
@@ -3129,12 +3199,12 @@ function buildPrinterColumn(def: PrinterColumnDef, index: number, table: Printer
|
|
|
3129
3199
|
// A column the vendor named Ready/Healthy/Established carries the only
|
|
3130
3200
|
// scannable signal these kinds have — they have no curated Status column.
|
|
3131
3201
|
// Rendering it as grey text would leave a screen of uniform "True".
|
|
3132
|
-
const tone = printerCellTone(def, value)
|
|
3202
|
+
const tone = printerCellTone(def, value, resource)
|
|
3133
3203
|
if (tone) {
|
|
3134
3204
|
return (
|
|
3135
3205
|
// The value, not the column's description — the header tooltip
|
|
3136
3206
|
// carries that. A cell tooltip exists to reveal a truncated value.
|
|
3137
|
-
<Tooltip content={text}>
|
|
3207
|
+
<Tooltip content={tone === 'neutral' ? `${text} · ${String(value).trim().toLowerCase() === 'false' ? 'Controller recorded reconciliation paused' : 'Last reported readiness while reconciliation is paused'}` : text}>
|
|
3138
3208
|
<span className={clsx('badge', healthColors[tone])}>{text}</span>
|
|
3139
3209
|
</Tooltip>
|
|
3140
3210
|
)
|
|
@@ -3363,6 +3433,7 @@ interface ResourcesViewData {
|
|
|
3363
3433
|
export const ResourcesViewDataContext = React.createContext<ResourcesViewData>({})
|
|
3364
3434
|
|
|
3365
3435
|
export interface ResourceQueryResult {
|
|
3436
|
+
coverage?: ResourceCoverage
|
|
3366
3437
|
resourceName?: string
|
|
3367
3438
|
group?: string
|
|
3368
3439
|
data?: any[]
|
|
@@ -3398,6 +3469,7 @@ interface ResourcesViewProps {
|
|
|
3398
3469
|
resourceQueries?: ResourceQueryResult[]
|
|
3399
3470
|
// Lightweight counts for sidebar badges (from /api/resource-counts)
|
|
3400
3471
|
resourceCounts?: Record<string, number | null>
|
|
3472
|
+
resourceCoverage?: Record<string, ResourceCoverage>
|
|
3401
3473
|
resourceForbidden?: string[]
|
|
3402
3474
|
/** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
|
|
3403
3475
|
* keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
|
|
@@ -3551,6 +3623,7 @@ const DEFAULT_KIND_INFO: SelectedKindInfo = { name: 'pods', kind: 'Pod', group:
|
|
|
3551
3623
|
export const LOADED_RESOURCE_COUNT_TTL_MS = 5 * 60 * 1000
|
|
3552
3624
|
|
|
3553
3625
|
interface LoadedResourceCount {
|
|
3626
|
+
coverage?: ResourceCoverage
|
|
3554
3627
|
count: number
|
|
3555
3628
|
expiresAt: number
|
|
3556
3629
|
}
|
|
@@ -3660,6 +3733,22 @@ export function deriveSidebarResourceCounts(
|
|
|
3660
3733
|
return results
|
|
3661
3734
|
}
|
|
3662
3735
|
|
|
3736
|
+
export function deriveSidebarResourceCoverage(
|
|
3737
|
+
resourceCounts: Record<string, number | null> | undefined,
|
|
3738
|
+
resourceCoverage: Record<string, ResourceCoverage> | undefined,
|
|
3739
|
+
loadedCountCache: LoadedResourceCountCache,
|
|
3740
|
+
now = Date.now(),
|
|
3741
|
+
): Record<string, ResourceCoverage> {
|
|
3742
|
+
const result: Record<string, ResourceCoverage> = { ...resourceCoverage }
|
|
3743
|
+
for (const key of Object.keys(resourceCounts ?? {})) {
|
|
3744
|
+
if (!(key in result)) result[key] = { status: 'unknown' }
|
|
3745
|
+
}
|
|
3746
|
+
for (const [key, cached] of Object.entries(loadedCountCache)) {
|
|
3747
|
+
if (!(key in result) && cached.expiresAt > now) result[key] = cached.coverage ?? { status: 'unknown' }
|
|
3748
|
+
}
|
|
3749
|
+
return result
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3663
3752
|
const FILTER_INTENT_PARAMS = [...RESOURCE_LIST_FILTER_PARAMS, 'resource']
|
|
3664
3753
|
|
|
3665
3754
|
/**
|
|
@@ -3694,6 +3783,7 @@ export function ResourcesView({
|
|
|
3694
3783
|
apiResources: apiResourcesProp,
|
|
3695
3784
|
resourceQueries: resourceQueriesProp,
|
|
3696
3785
|
resourceCounts: resourceCountsProp,
|
|
3786
|
+
resourceCoverage: resourceCoverageProp,
|
|
3697
3787
|
resourceForbidden: resourceForbiddenProp,
|
|
3698
3788
|
resourceReasons,
|
|
3699
3789
|
resourceUnavailable: resourceUnavailableProp,
|
|
@@ -3742,6 +3832,15 @@ export function ResourcesView({
|
|
|
3742
3832
|
}: ResourcesViewProps) {
|
|
3743
3833
|
const initialFilters = getInitialFiltersFromURL()
|
|
3744
3834
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
|
|
3835
|
+
// Phones: the 224px kind list would leave the table a sliver, so it lives in
|
|
3836
|
+
// a drawer over the table, opened from the toolbar and closed by a pick.
|
|
3837
|
+
const isPhone = usePhoneLayout()
|
|
3838
|
+
const [kindsDrawerOpen, setKindsDrawerOpen] = useState(false)
|
|
3839
|
+
// The drawer unmounts its sidebar on close; keep its display settings here.
|
|
3840
|
+
const [drawerSidebarState, setDrawerSidebarState] = useState<SidebarDisplayState>(() => ({ showEmptyKinds: false, favoritesExpanded: pinned.length > 0 }))
|
|
3841
|
+
useEffect(() => {
|
|
3842
|
+
if (!isPhone) setKindsDrawerOpen(false)
|
|
3843
|
+
}, [isPhone])
|
|
3745
3844
|
// Sync selectedKind from URL when the URL changes (browser back, external sidebar navigation).
|
|
3746
3845
|
// Deps are URL-derived only — including selectedKind.name/group would race against pending
|
|
3747
3846
|
// navigation: a sidebar click flips state before navigate() lands, this effect re-reads the
|
|
@@ -4317,7 +4416,7 @@ export function ResourcesView({
|
|
|
4317
4416
|
category: 'Search',
|
|
4318
4417
|
scope: 'resources',
|
|
4319
4418
|
handler: () => searchInputRef.current?.focus(),
|
|
4320
|
-
enabled: tableActive,
|
|
4419
|
+
enabled: tableActive && !kindsDrawerOpen,
|
|
4321
4420
|
})
|
|
4322
4421
|
|
|
4323
4422
|
// Keyboard navigation: highlighted row state
|
|
@@ -4426,6 +4525,8 @@ export function ResourcesView({
|
|
|
4426
4525
|
onResourceClick?.(isSelected ? null : stripped)
|
|
4427
4526
|
}, [onRowSelect, onResourceClick, selectedKind.name, selectedKind.group])
|
|
4428
4527
|
|
|
4528
|
+
// Shortcuts here (and the search shortcut above) pause while the phone
|
|
4529
|
+
// kind drawer is open: it's modal, so the table behind it takes no keys.
|
|
4429
4530
|
useRegisterShortcuts(([
|
|
4430
4531
|
{
|
|
4431
4532
|
id: 'resources-nav-down',
|
|
@@ -4599,7 +4700,7 @@ export function ResourcesView({
|
|
|
4599
4700
|
else searchInputRef.current?.blur()
|
|
4600
4701
|
},
|
|
4601
4702
|
},
|
|
4602
|
-
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && (sc.enabled ?? true) })))
|
|
4703
|
+
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && !kindsDrawerOpen && (sc.enabled ?? true) })))
|
|
4603
4704
|
|
|
4604
4705
|
// Refs for accessing filteredResources inside shortcuts (computed later in component)
|
|
4605
4706
|
const filteredResourceCountRef = useRef(0)
|
|
@@ -4609,7 +4710,7 @@ export function ResourcesView({
|
|
|
4609
4710
|
const flatKindListRef = useRef<SelectedKindInfo[]>([])
|
|
4610
4711
|
|
|
4611
4712
|
// Sidebar kind navigation: [ = previous kind, ] = next kind
|
|
4612
|
-
useRegisterShortcuts([
|
|
4713
|
+
useRegisterShortcuts(([
|
|
4613
4714
|
{
|
|
4614
4715
|
id: 'resources-prev-kind',
|
|
4615
4716
|
keys: '[',
|
|
@@ -4642,7 +4743,7 @@ export function ResourcesView({
|
|
|
4642
4743
|
onKindChange?.()
|
|
4643
4744
|
},
|
|
4644
4745
|
},
|
|
4645
|
-
])
|
|
4746
|
+
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: !kindsDrawerOpen && (sc.enabled ?? true) })))
|
|
4646
4747
|
|
|
4647
4748
|
// Close dropdowns on outside click
|
|
4648
4749
|
useEffect(() => {
|
|
@@ -5165,16 +5266,16 @@ export function ResourcesView({
|
|
|
5165
5266
|
}
|
|
5166
5267
|
|
|
5167
5268
|
const existing = next[selectedKindCountKey]
|
|
5168
|
-
if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt) {
|
|
5269
|
+
if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt && existing.coverage === selectedQuery?.coverage) {
|
|
5169
5270
|
return changed ? next : prev
|
|
5170
5271
|
}
|
|
5171
5272
|
|
|
5172
5273
|
return {
|
|
5173
5274
|
...next,
|
|
5174
|
-
[selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt },
|
|
5275
|
+
[selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt, coverage: selectedQuery?.coverage },
|
|
5175
5276
|
}
|
|
5176
5277
|
})
|
|
5177
|
-
}, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt])
|
|
5278
|
+
}, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt, selectedQuery?.coverage])
|
|
5178
5279
|
|
|
5179
5280
|
// Derive counts — prefer lightweight resourceCounts prop over full query data
|
|
5180
5281
|
const counts = useMemo(() => {
|
|
@@ -5195,6 +5296,12 @@ export function ResourcesView({
|
|
|
5195
5296
|
return results
|
|
5196
5297
|
}, [useNewCountsMode, resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache, selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, resourceQueries])
|
|
5197
5298
|
|
|
5299
|
+
const sidebarResourceCoverage = useMemo(() => {
|
|
5300
|
+
const result = deriveSidebarResourceCoverage(resourceCountsProp, resourceCoverageProp, loadedCountCache)
|
|
5301
|
+
if (selectedQueryHasLoadedCount) result[selectedKindCountKey] = selectedQuery?.coverage ?? { status: 'unknown' }
|
|
5302
|
+
return result
|
|
5303
|
+
}, [resourceCountsProp, resourceCoverageProp, loadedCountCache, selectedQueryHasLoadedCount, selectedKindCountKey, selectedQuery?.coverage])
|
|
5304
|
+
|
|
5198
5305
|
const sidebarResourceUnavailable = useMemo(() => {
|
|
5199
5306
|
if (!resourceUnavailableProp?.length) {
|
|
5200
5307
|
return resourceUnavailableProp
|
|
@@ -5318,14 +5425,26 @@ export function ResourcesView({
|
|
|
5318
5425
|
if (issue) return issueTitle(issue)
|
|
5319
5426
|
if (operationalIssuesIncomplete) return 'Unknown'
|
|
5320
5427
|
}
|
|
5428
|
+
if (column === 'status') {
|
|
5429
|
+
const managedColumns = isLikelyCrossplaneMRGroup(selectedKind.name, selectedKind.group ?? '')
|
|
5430
|
+
if ((managedColumns && isManagedResource(resource))
|
|
5431
|
+
|| ((!hasCuratedColumns(selectedKind.name, selectedKind.group) || managedColumns)
|
|
5432
|
+
&& getCrossplanePauseState(resource) === 'acknowledged')) {
|
|
5433
|
+
return getCrossplaneStatus(resource).text
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5321
5436
|
return getCellFilterValue(resource, column, kind)
|
|
5322
|
-
}, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
|
|
5437
|
+
}, [operationalIssues, operationalIssuesIncomplete, selectedKind.name, selectedKind.group])
|
|
5323
5438
|
|
|
5324
5439
|
// Get sortable value from a resource for a given column
|
|
5325
5440
|
const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
|
|
5326
5441
|
const meta = resource.metadata || {}
|
|
5327
5442
|
const status = resource.status || {}
|
|
5328
5443
|
const kindLower = kind?.toLowerCase() || ''
|
|
5444
|
+
const otelValue = getOpenTelemetryColumnValue(resource, column)
|
|
5445
|
+
if (otelValue !== undefined) return otelValue
|
|
5446
|
+
const flaggerValue = getFlaggerColumnValue(resource, column)
|
|
5447
|
+
if (flaggerValue !== undefined) return flaggerValue
|
|
5329
5448
|
|
|
5330
5449
|
switch (column) {
|
|
5331
5450
|
case 'name':
|
|
@@ -5462,13 +5581,18 @@ export function ResourcesView({
|
|
|
5462
5581
|
}
|
|
5463
5582
|
}, [regexMode, deferredSearchTerm])
|
|
5464
5583
|
|
|
5584
|
+
const isTrivyKind = selectedKind.group === TRIVY_GROUP
|
|
5585
|
+
|
|
5465
5586
|
// Filter resources by search term, status, problems, and sort
|
|
5466
|
-
const filteredResources = useMemo(() => {
|
|
5467
|
-
if (!tableActive || !resources) return []
|
|
5587
|
+
const { list: filteredResources, findings: findingsSearch } = useMemo((): { list: any[]; findings: TrivyFindingsSearch | null } => {
|
|
5588
|
+
if (!tableActive || !resources) return { list: [], findings: null }
|
|
5468
5589
|
|
|
5469
5590
|
let result = resources
|
|
5591
|
+
let findings: TrivyFindingsSearch | null = null
|
|
5470
5592
|
|
|
5471
|
-
// Apply search filter
|
|
5593
|
+
// Apply search filter. On Trivy report kinds a plain search also matches
|
|
5594
|
+
// findings and runs below, after the other filters. Regex mode stays on
|
|
5595
|
+
// names: a pattern run over every finding could backtrack for seconds.
|
|
5472
5596
|
if (deferredSearchTerm) {
|
|
5473
5597
|
if (regexMode) {
|
|
5474
5598
|
const re = searchRegex.re
|
|
@@ -5478,7 +5602,7 @@ export function ResourcesView({
|
|
|
5478
5602
|
re.test(r.metadata?.namespace ?? '')
|
|
5479
5603
|
)
|
|
5480
5604
|
}
|
|
5481
|
-
} else {
|
|
5605
|
+
} else if (!isTrivyKind) {
|
|
5482
5606
|
const term = deferredSearchTerm.toLowerCase()
|
|
5483
5607
|
result = result.filter((r: any) =>
|
|
5484
5608
|
r.metadata?.name?.toLowerCase().includes(term) ||
|
|
@@ -5554,6 +5678,19 @@ export function ResourcesView({
|
|
|
5554
5678
|
})
|
|
5555
5679
|
}
|
|
5556
5680
|
|
|
5681
|
+
// Trivy findings are searched after the other filters, so rows they hide
|
|
5682
|
+
// don't spend the findings budget.
|
|
5683
|
+
if (deferredSearchTerm && !regexMode && isTrivyKind) {
|
|
5684
|
+
const term = deferredSearchTerm.toLowerCase()
|
|
5685
|
+
const matched = searchTrivyFindings(result, term)
|
|
5686
|
+
findings = matched
|
|
5687
|
+
result = result.filter((r: any) =>
|
|
5688
|
+
r.metadata?.name?.toLowerCase().includes(term) ||
|
|
5689
|
+
r.metadata?.namespace?.toLowerCase().includes(term) ||
|
|
5690
|
+
matched.matches.has(r)
|
|
5691
|
+
)
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5557
5694
|
// Apply custom sorting if set. Extra columns override
|
|
5558
5695
|
// the built-in getSortValue for their key.
|
|
5559
5696
|
if (effectiveSortColumn && sortDirection) {
|
|
@@ -5649,8 +5786,8 @@ export function ResourcesView({
|
|
|
5649
5786
|
}
|
|
5650
5787
|
}
|
|
5651
5788
|
|
|
5652
|
-
return result
|
|
5653
|
-
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
5789
|
+
return { list: result, findings }
|
|
5790
|
+
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, isTrivyKind, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
|
|
5654
5791
|
|
|
5655
5792
|
// For nodes table: compute the majority minor version so outliers can be highlighted
|
|
5656
5793
|
const majorityNodeMinorVersion = useMemo(() => {
|
|
@@ -5754,7 +5891,9 @@ export function ResourcesView({
|
|
|
5754
5891
|
const selectedKindName = selectedKind.name.toLowerCase()
|
|
5755
5892
|
const canBulkRestartSelectedKind = onBulkRestart != null && BULK_RESTART_WORKLOAD_KINDS.has(selectedKindName)
|
|
5756
5893
|
const canBulkScaleSelectedKind = onBulkScale != null && BULK_SCALE_WORKLOAD_KINDS.has(selectedKindName)
|
|
5757
|
-
|
|
5894
|
+
// Phones skip bulk selection and create (see the toolbar): phone triage
|
|
5895
|
+
// is one resource at a time.
|
|
5896
|
+
const canBulkSelect = !isPhone && (onBulkDelete != null || canBulkRestartSelectedKind || canBulkScaleSelectedKind)
|
|
5758
5897
|
const isBulkMutating = isBulkDeleting || isBulkRestarting || isBulkScaling
|
|
5759
5898
|
|
|
5760
5899
|
const openBulkScaleDialog = useCallback(() => {
|
|
@@ -5805,14 +5944,30 @@ export function ResourcesView({
|
|
|
5805
5944
|
// Fixed-width columns can consume the table's flexible space and collapse
|
|
5806
5945
|
// the required name column. Keep a real table minimum and let the container
|
|
5807
5946
|
// scroll horizontally when the viewport is too narrow.
|
|
5947
|
+
// Phones pin a leading Name column (see PHONE_PINNED_CELL). Cap it at
|
|
5948
|
+
// PHONE_NAME_MAX_VW percent of the screen so a width saved on desktop can't
|
|
5949
|
+
// make the pinned column cover it; the saved width itself is left alone.
|
|
5950
|
+
// Tracked through resizes and rotation, which can stay within phone width.
|
|
5951
|
+
const [phoneViewportWidth, setPhoneViewportWidth] = useState(() => (typeof window === 'undefined' ? 0 : window.innerWidth))
|
|
5952
|
+
useEffect(() => {
|
|
5953
|
+
if (!isPhone) return
|
|
5954
|
+
const onResize = () => setPhoneViewportWidth(window.innerWidth)
|
|
5955
|
+
onResize()
|
|
5956
|
+
window.addEventListener('resize', onResize)
|
|
5957
|
+
return () => window.removeEventListener('resize', onResize)
|
|
5958
|
+
}, [isPhone])
|
|
5959
|
+
const phoneNameCap = isPhone && columns[0]?.key === 'name' ? Math.round(phoneViewportWidth * PHONE_NAME_MAX_VW / 100) : Infinity
|
|
5808
5960
|
const tableMinWidth = useMemo(() => {
|
|
5809
5961
|
const compareColumnWidth = compareMode ? COMPARE_COLUMN_WIDTH : 0
|
|
5810
|
-
const baseMinWidth = columns.reduce((sum, col) =>
|
|
5962
|
+
const baseMinWidth = columns.reduce((sum, col, i) => {
|
|
5963
|
+
const width = columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)
|
|
5964
|
+
return sum + (i === 0 && col.key === 'name' ? Math.min(width, phoneNameCap) : width)
|
|
5965
|
+
}, compareColumnWidth)
|
|
5811
5966
|
const flexibleNameColumn = columns.find(col => col.key === 'name' && !columnWidths[col.key])
|
|
5812
5967
|
|
|
5813
5968
|
if (!hasResizedColumns || !flexibleNameColumn) return baseMinWidth
|
|
5814
5969
|
return baseMinWidth + getColumnMinWidth(flexibleNameColumn, extraColumnsByKey)
|
|
5815
|
-
}, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch])
|
|
5970
|
+
}, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch, phoneNameCap])
|
|
5816
5971
|
|
|
5817
5972
|
// Stable virtuoso components — memoized to avoid remounting the table on every render
|
|
5818
5973
|
const virtuosoComponents = useMemo(() => ({
|
|
@@ -5834,13 +5989,15 @@ export function ResourcesView({
|
|
|
5834
5989
|
*/}
|
|
5835
5990
|
{compareMode && <col style={{ width: COMPARE_COLUMN_WIDTH }} />}
|
|
5836
5991
|
{isCheckboxMode && <col style={{ width: '40px' }} />}
|
|
5837
|
-
{columns.map(col => (
|
|
5992
|
+
{columns.map((col, i) => (
|
|
5838
5993
|
<col
|
|
5839
5994
|
key={col.key}
|
|
5840
5995
|
style={{
|
|
5841
|
-
width:
|
|
5842
|
-
?
|
|
5843
|
-
: col.key
|
|
5996
|
+
width: i === 0 && col.key === 'name' && phoneNameCap !== Infinity
|
|
5997
|
+
? `min(${columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)}px, ${PHONE_NAME_MAX_VW}vw)`
|
|
5998
|
+
: columnWidths[col.key]
|
|
5999
|
+
? `${columnWidths[col.key]}px`
|
|
6000
|
+
: col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
|
|
5844
6001
|
}}
|
|
5845
6002
|
/>
|
|
5846
6003
|
))}
|
|
@@ -5851,7 +6008,7 @@ export function ResourcesView({
|
|
|
5851
6008
|
)
|
|
5852
6009
|
}),
|
|
5853
6010
|
TableRow: VirtuosoTableRow,
|
|
5854
|
-
}), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch])
|
|
6011
|
+
}), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch, phoneNameCap])
|
|
5855
6012
|
|
|
5856
6013
|
// Calculate filter options with counts based on current resources (before filtering)
|
|
5857
6014
|
const filterOptions = useMemo(() => {
|
|
@@ -6049,46 +6206,86 @@ export function ResourcesView({
|
|
|
6049
6206
|
onOpenWorkloadLogs,
|
|
6050
6207
|
}), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
|
|
6051
6208
|
|
|
6209
|
+
const renderSidebar = (className?: string, inDrawer = false) => (
|
|
6210
|
+
<ResourcesSidebar
|
|
6211
|
+
className={className}
|
|
6212
|
+
displayState={inDrawer ? drawerSidebarState : undefined}
|
|
6213
|
+
onDisplayStateChange={inDrawer ? setDrawerSidebarState : undefined}
|
|
6214
|
+
containEscape={inDrawer}
|
|
6215
|
+
selectedKind={selectedKind}
|
|
6216
|
+
onSelectedKindChange={(kind) => {
|
|
6217
|
+
shouldPushHistory.current = true
|
|
6218
|
+
setSelectedKind(kind)
|
|
6219
|
+
setKindsDrawerOpen(false)
|
|
6220
|
+
}}
|
|
6221
|
+
onKindChange={onKindChange}
|
|
6222
|
+
apiResources={apiResourcesProp}
|
|
6223
|
+
resourceCounts={counts}
|
|
6224
|
+
resourceCoverage={sidebarResourceCoverage}
|
|
6225
|
+
resourceForbidden={Array.from(forbiddenKinds)}
|
|
6226
|
+
resourceUnavailable={sidebarResourceUnavailable}
|
|
6227
|
+
pinned={pinned}
|
|
6228
|
+
togglePin={togglePin}
|
|
6229
|
+
isPinned={isPinned}
|
|
6230
|
+
categoryWorkspaces={sidebarCategoryWorkspaces}
|
|
6231
|
+
onKindNavigated={() => {
|
|
6232
|
+
// After selecting a kind via keyboard, move focus to the table search
|
|
6233
|
+
// so the user can immediately filter within the selected kind.
|
|
6234
|
+
setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
6235
|
+
}}
|
|
6236
|
+
/>
|
|
6237
|
+
)
|
|
6238
|
+
|
|
6239
|
+
const kindsButton = (
|
|
6240
|
+
<button
|
|
6241
|
+
type="button"
|
|
6242
|
+
onClick={() => setKindsDrawerOpen(true)}
|
|
6243
|
+
aria-expanded={kindsDrawerOpen}
|
|
6244
|
+
aria-label={`Resource kinds (showing ${selectedKind.kind})`}
|
|
6245
|
+
className="flex shrink-0 items-center gap-1.5 rounded-lg border border-theme-border bg-theme-elevated px-2.5 py-1.5 text-sm font-medium text-theme-text-primary hover:bg-theme-hover"
|
|
6246
|
+
>
|
|
6247
|
+
<PanelLeft className="h-4 w-4 text-theme-text-tertiary" />
|
|
6248
|
+
<span className="max-w-[7rem] truncate">{selectedKind.kind}</span>
|
|
6249
|
+
</button>
|
|
6250
|
+
)
|
|
6251
|
+
|
|
6052
6252
|
return (
|
|
6053
6253
|
<ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
|
|
6054
6254
|
<div className="flex h-full w-full">
|
|
6055
|
-
{/* Sidebar - Resource Types
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
}
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
pinned={pinned}
|
|
6069
|
-
togglePin={togglePin}
|
|
6070
|
-
isPinned={isPinned}
|
|
6071
|
-
categoryWorkspaces={sidebarCategoryWorkspaces}
|
|
6072
|
-
onKindNavigated={() => {
|
|
6073
|
-
// After selecting a kind via keyboard, move focus to the table search
|
|
6074
|
-
// so the user can immediately filter within the selected kind.
|
|
6075
|
-
setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
6076
|
-
}}
|
|
6077
|
-
/>
|
|
6255
|
+
{/* Sidebar - Resource Types. On phones it is a modal drawer: the
|
|
6256
|
+
dialog primitive traps focus, captures Escape before the table's
|
|
6257
|
+
own shortcuts, and returns focus to the kinds button. */}
|
|
6258
|
+
{!hideSidebar && !isPhone && renderSidebar()}
|
|
6259
|
+
{!hideSidebar && isPhone && (
|
|
6260
|
+
<DialogPortal
|
|
6261
|
+
open={kindsDrawerOpen}
|
|
6262
|
+
onClose={() => setKindsDrawerOpen(false)}
|
|
6263
|
+
ariaLabel="Resource kinds"
|
|
6264
|
+
className="!ml-0 !mr-auto h-full w-72 max-w-[85vw] self-stretch overflow-hidden rounded-none border-y-0 border-l-0"
|
|
6265
|
+
>
|
|
6266
|
+
{renderSidebar('!w-full h-full border-r-0', true)}
|
|
6267
|
+
</DialogPortal>
|
|
6078
6268
|
)}
|
|
6079
6269
|
|
|
6080
6270
|
{kindView !== null ? (
|
|
6081
|
-
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
6271
|
+
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
6272
|
+
{!hideSidebar && isPhone && (
|
|
6273
|
+
<div className="flex items-center px-3 py-2 border-b border-theme-border bg-theme-base shrink-0">{kindsButton}</div>
|
|
6274
|
+
)}
|
|
6275
|
+
{kindView}
|
|
6276
|
+
</div>
|
|
6082
6277
|
) : (
|
|
6083
6278
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
6084
6279
|
{/* Toolbar */}
|
|
6085
|
-
|
|
6086
|
-
|
|
6280
|
+
{/* Phones: the toolbar wraps and search takes its own full row. */}
|
|
6281
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0 phone:relative phone:flex-wrap phone:gap-2 phone:px-3">
|
|
6282
|
+
{!hideSidebar && isPhone && kindsButton}
|
|
6283
|
+
<div className="flex-1 min-w-0 phone:order-last phone:basis-full">
|
|
6087
6284
|
<div className="relative max-w-md">
|
|
6088
6285
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
6089
6286
|
<Input
|
|
6090
6287
|
ref={searchInputRef}
|
|
6091
|
-
placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
|
|
6288
|
+
placeholder={regexMode ? 'Search by regex... (press /)' : isTrivyKind ? 'Search names and findings... (press /)' : 'Search... (press /)'}
|
|
6092
6289
|
value={searchTerm}
|
|
6093
6290
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
6094
6291
|
onKeyDown={(e) => {
|
|
@@ -6150,6 +6347,11 @@ export function ResourcesView({
|
|
|
6150
6347
|
<X className="w-3.5 h-3.5" />
|
|
6151
6348
|
</button>
|
|
6152
6349
|
)}
|
|
6350
|
+
{!searchRegex.error && findingsSearch?.truncated && (
|
|
6351
|
+
<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">
|
|
6352
|
+
Too many findings to search them all here; some weren't searched.
|
|
6353
|
+
</div>
|
|
6354
|
+
)}
|
|
6153
6355
|
{searchRegex.error && (
|
|
6154
6356
|
<div
|
|
6155
6357
|
title={searchRegex.error}
|
|
@@ -6161,9 +6363,11 @@ export function ResourcesView({
|
|
|
6161
6363
|
</div>
|
|
6162
6364
|
</div>
|
|
6163
6365
|
|
|
6164
|
-
{/* Problems dropdown (pods only)
|
|
6366
|
+
{/* Problems dropdown (pods only). On phones the toolbar wraps, so this
|
|
6367
|
+
menu and Labels' anchor to the toolbar (full width below it)
|
|
6368
|
+
rather than to a trigger that may sit near either edge. */}
|
|
6165
6369
|
{filterOptions?.problems && filterOptions.problems.length > 0 && (
|
|
6166
|
-
<div className="relative" ref={problemsDropdownRef}>
|
|
6370
|
+
<div className="relative phone:static" ref={problemsDropdownRef}>
|
|
6167
6371
|
<button
|
|
6168
6372
|
onClick={() => { setShowProblemsDropdown(!showProblemsDropdown); setShowLabelsDropdown(false) }}
|
|
6169
6373
|
className={clsx(
|
|
@@ -6182,7 +6386,7 @@ export function ResourcesView({
|
|
|
6182
6386
|
)}
|
|
6183
6387
|
</button>
|
|
6184
6388
|
{showProblemsDropdown && (
|
|
6185
|
-
<div className="absolute right-0 top-full mt-1 min-w-48 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6389
|
+
<div className="absolute right-0 top-full mt-1 min-w-48 phone:left-3 phone:right-3 phone:min-w-0 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6186
6390
|
{problemFilters.length > 0 && (
|
|
6187
6391
|
<div className="flex items-center justify-between px-3 py-1.5 border-b border-theme-border">
|
|
6188
6392
|
<span className="text-xs font-medium text-theme-text-secondary">Problems</span>
|
|
@@ -6213,7 +6417,7 @@ export function ResourcesView({
|
|
|
6213
6417
|
|
|
6214
6418
|
{/* Labels dropdown */}
|
|
6215
6419
|
{filterOptions?.labels && filterOptions.labels.length > 0 && (
|
|
6216
|
-
<div className="relative" ref={labelsDropdownRef}>
|
|
6420
|
+
<div className="relative phone:static" ref={labelsDropdownRef}>
|
|
6217
6421
|
<button
|
|
6218
6422
|
onClick={() => { setShowLabelsDropdown(!showLabelsDropdown); setShowProblemsDropdown(false); setLabelSearch('') }}
|
|
6219
6423
|
className={clsx(
|
|
@@ -6237,7 +6441,7 @@ export function ResourcesView({
|
|
|
6237
6441
|
? labels.filter(l => l.value.toLowerCase().includes(labelSearch.toLowerCase()))
|
|
6238
6442
|
: labels
|
|
6239
6443
|
return (
|
|
6240
|
-
<div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6444
|
+
<div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 phone:left-3 phone:right-3 phone:min-w-0 phone:max-w-none bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6241
6445
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
6242
6446
|
<div className="relative flex-1">
|
|
6243
6447
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
@@ -6417,7 +6621,7 @@ export function ResourcesView({
|
|
|
6417
6621
|
</div>
|
|
6418
6622
|
)}
|
|
6419
6623
|
</div>
|
|
6420
|
-
{onCreateResource && (
|
|
6624
|
+
{onCreateResource && !isPhone && (
|
|
6421
6625
|
<Tooltip content={`Create ${selectedKind.kind || 'resource'}`}>
|
|
6422
6626
|
<button
|
|
6423
6627
|
aria-label={`Create ${selectedKind.kind || 'resource'}`}
|
|
@@ -6477,7 +6681,7 @@ export function ResourcesView({
|
|
|
6477
6681
|
|
|
6478
6682
|
{/* Bulk actions bar */}
|
|
6479
6683
|
{showBulkBar && (
|
|
6480
|
-
<div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
|
|
6684
|
+
<div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0 phone:flex-wrap phone:gap-2 phone:px-3">
|
|
6481
6685
|
<span className="text-sm font-medium text-theme-text-primary">
|
|
6482
6686
|
{checkedItems.length} selected
|
|
6483
6687
|
</span>
|
|
@@ -6524,6 +6728,18 @@ export function ResourcesView({
|
|
|
6524
6728
|
</div>
|
|
6525
6729
|
)}
|
|
6526
6730
|
|
|
6731
|
+
{!isLoading && !selectedQueryError && resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind)) && (
|
|
6732
|
+
<div role="status" className="px-4 py-2 text-xs text-warning-text border-b border-theme-border bg-theme-surface">
|
|
6733
|
+
{resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind))}
|
|
6734
|
+
</div>
|
|
6735
|
+
)}
|
|
6736
|
+
|
|
6737
|
+
{!isLoading && !selectedQueryError && resourceNarrowedNotice(selectedQuery?.coverage) && (
|
|
6738
|
+
<div role="status" className="px-4 py-2 text-xs text-theme-text-tertiary border-b border-theme-border bg-theme-surface">
|
|
6739
|
+
{resourceNarrowedNotice(selectedQuery?.coverage)}
|
|
6740
|
+
</div>
|
|
6741
|
+
)}
|
|
6742
|
+
|
|
6527
6743
|
{!isLoading && !isSelectedForbidden && !largeListGuard && filteredResources.length > 0 && (
|
|
6528
6744
|
<ActiveFilterBar
|
|
6529
6745
|
columnFilters={columnFilters}
|
|
@@ -6565,7 +6781,7 @@ export function ResourcesView({
|
|
|
6565
6781
|
/>
|
|
6566
6782
|
</div>
|
|
6567
6783
|
</div>
|
|
6568
|
-
) : selectedQueryError && !Array.isArray(resources) ? (
|
|
6784
|
+
) : selectedQueryError && !Array.isArray(resources) && !largeListGuard ? (
|
|
6569
6785
|
// A failed fetch with no rows to fall back on must not reach the
|
|
6570
6786
|
// empty state below — it would tell the user the cluster has none.
|
|
6571
6787
|
<FetchResult
|
|
@@ -6585,7 +6801,9 @@ export function ResourcesView({
|
|
|
6585
6801
|
<p className="text-sm mt-1 max-w-xl">
|
|
6586
6802
|
{largeListGuard.reason === 'count-unavailable'
|
|
6587
6803
|
? 'Radar could not verify this list is small enough to load. Choose a namespace or smaller namespace set to try again.'
|
|
6588
|
-
:
|
|
6804
|
+
: largeListGuard.count == null
|
|
6805
|
+
? 'This list exceeds the resource limit. Choose a namespace or smaller namespace set to load this view.'
|
|
6806
|
+
: `${largeListGuard.count.toLocaleString()} ${largeListGuard.kind.toLowerCase()} were read in the current scope. Choose a namespace or smaller namespace set to load this view.`}
|
|
6589
6807
|
</p>
|
|
6590
6808
|
<p className="text-xs mt-2 text-theme-text-disabled">
|
|
6591
6809
|
Radar limits full table loads to {largeListGuard.limit.toLocaleString()} resources to keep the UI responsive.
|
|
@@ -6596,8 +6814,8 @@ export function ResourcesView({
|
|
|
6596
6814
|
</div>
|
|
6597
6815
|
) : filteredResources.length === 0 ? (
|
|
6598
6816
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary">
|
|
6599
|
-
<p>
|
|
6600
|
-
{emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
|
|
6817
|
+
<p>{resourceEmptyMessage(selectedKind.kind, selectedQuery?.coverage, (resources?.length ?? 0) > 0)}</p>
|
|
6818
|
+
{selectedQuery?.coverage?.status !== 'partial' && emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
|
|
6601
6819
|
<p className="text-xs mt-2 max-w-md text-center text-theme-text-disabled">
|
|
6602
6820
|
{emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0)}
|
|
6603
6821
|
</p>
|
|
@@ -6614,6 +6832,13 @@ export function ResourcesView({
|
|
|
6614
6832
|
<X className="w-3.5 h-3.5" />
|
|
6615
6833
|
</button>
|
|
6616
6834
|
)}
|
|
6835
|
+
{searchTerm && isTrivyKind && (
|
|
6836
|
+
<p className="text-xs mt-1 text-theme-text-tertiary">
|
|
6837
|
+
{regexMode
|
|
6838
|
+
? 'Regex search matches names and namespaces only.'
|
|
6839
|
+
: 'Searched names, namespaces and report findings (CVE, package, check ID, image).'}
|
|
6840
|
+
</p>
|
|
6841
|
+
)}
|
|
6617
6842
|
{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>}
|
|
6618
6843
|
<ActiveFilterBar
|
|
6619
6844
|
columnFilters={columnFilters}
|
|
@@ -6653,15 +6878,15 @@ export function ResourcesView({
|
|
|
6653
6878
|
// Inline px width — under `table-layout:fixed`,
|
|
6654
6879
|
// `w-9` is a hint the browser absorbs into leftover
|
|
6655
6880
|
// row width on an icon-only column.
|
|
6656
|
-
style={COMPARE_COLUMN_STYLE}
|
|
6657
|
-
className=
|
|
6881
|
+
style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, isCheckboxMode, 'compare') }}
|
|
6882
|
+
className={clsx('px-2 py-3 text-xs font-medium uppercase tracking-wide bg-theme-base border-b border-r-subtle border-theme-border text-center text-skyhook-400', PHONE_PINNED_HEAD)}
|
|
6658
6883
|
title="Compare mode"
|
|
6659
6884
|
>
|
|
6660
6885
|
<GitCompare className="w-3.5 h-3.5 inline-block opacity-70" />
|
|
6661
6886
|
</th>
|
|
6662
6887
|
)}
|
|
6663
6888
|
{isCheckboxMode && (
|
|
6664
|
-
<th className=
|
|
6889
|
+
<th style={phonePinnedLeft(compareMode, isCheckboxMode, 'checkbox')} className={clsx('bg-theme-surface border-b border-theme-border w-10 text-center px-0 py-3', PHONE_PINNED_HEAD)}>
|
|
6665
6890
|
<input
|
|
6666
6891
|
type="checkbox"
|
|
6667
6892
|
checked={allVisibleChecked}
|
|
@@ -6684,9 +6909,11 @@ export function ResourcesView({
|
|
|
6684
6909
|
return (
|
|
6685
6910
|
<th
|
|
6686
6911
|
key={col.key}
|
|
6912
|
+
style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, isCheckboxMode, 'name') : undefined}
|
|
6687
6913
|
className={clsx(
|
|
6688
6914
|
'text-left px-4 py-3 text-xs font-medium uppercase tracking-wide relative group/th',
|
|
6689
6915
|
'bg-theme-base border-b border-theme-border',
|
|
6916
|
+
colIdx === 0 && col.key === 'name' && PHONE_PINNED_HEAD,
|
|
6690
6917
|
!isLastCol && 'border-r-subtle',
|
|
6691
6918
|
isSortable ? 'text-theme-text-secondary hover:text-theme-text-primary cursor-pointer select-none' : 'text-theme-text-secondary'
|
|
6692
6919
|
)}
|
|
@@ -6874,7 +7101,7 @@ export function ResourcesView({
|
|
|
6874
7101
|
})()}
|
|
6875
7102
|
{/* Resize handle with visible divider */}
|
|
6876
7103
|
<div
|
|
6877
|
-
className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center"
|
|
7104
|
+
className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center phone:hidden"
|
|
6878
7105
|
style={{ transform: 'translateX(50%)' , zIndex: 10 }}
|
|
6879
7106
|
onMouseDown={(e) => {
|
|
6880
7107
|
const th = e.currentTarget.parentElement!
|
|
@@ -6922,6 +7149,7 @@ export function ResourcesView({
|
|
|
6922
7149
|
comparePickIndex={pickIdx}
|
|
6923
7150
|
rowHref={rowHrefFor?.(resource)}
|
|
6924
7151
|
onRowCheckToggle={toggleChecked}
|
|
7152
|
+
findingMatch={findingsSearch?.matches.get(resource)}
|
|
6925
7153
|
/>
|
|
6926
7154
|
)
|
|
6927
7155
|
}}
|
|
@@ -6955,13 +7183,12 @@ export function ResourcesView({
|
|
|
6955
7183
|
open={tableActive && showBulkDeleteConfirm}
|
|
6956
7184
|
onClose={() => { setShowBulkDeleteConfirm(false); setBulkForceDelete(false) }}
|
|
6957
7185
|
onConfirm={() => {
|
|
7186
|
+
const submittedKeys = new Set(checkedItems.map(getResourceKey))
|
|
7187
|
+
setShowBulkDeleteConfirm(false)
|
|
7188
|
+
setBulkForceDelete(false)
|
|
6958
7189
|
onBulkDelete?.(checkedBulkItems, {
|
|
6959
7190
|
force: bulkForceDelete,
|
|
6960
|
-
onSuccess: () =>
|
|
6961
|
-
clearBulkSelection()
|
|
6962
|
-
setShowBulkDeleteConfirm(false)
|
|
6963
|
-
setBulkForceDelete(false)
|
|
6964
|
-
},
|
|
7191
|
+
onSuccess: () => setCheckedResources(current => new Set([...current].filter(key => !submittedKeys.has(key)))),
|
|
6965
7192
|
})
|
|
6966
7193
|
}}
|
|
6967
7194
|
title={`Delete ${checkedItems.length} ${selectedKind.kind}${checkedItems.length > 1 ? 's' : ''}?`}
|
|
@@ -6972,6 +7199,7 @@ export function ResourcesView({
|
|
|
6972
7199
|
isLoading={isBulkDeleting}
|
|
6973
7200
|
isClosable
|
|
6974
7201
|
>
|
|
7202
|
+
<CrossplaneDeleteNotice resources={checkedItems} force={bulkForceDelete} />
|
|
6975
7203
|
<label className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
6976
7204
|
<input
|
|
6977
7205
|
type="checkbox"
|
|
@@ -7089,6 +7317,9 @@ interface ResourceRowCellsProps {
|
|
|
7089
7317
|
* is unaffected (still toggles picks). */
|
|
7090
7318
|
rowHref?: string
|
|
7091
7319
|
onRowCheckToggle?: (resource: any) => void
|
|
7320
|
+
/** What a Trivy report matched the table search through, when its name
|
|
7321
|
+
* and namespace didn't. */
|
|
7322
|
+
findingMatch?: TrivyFindingMatch
|
|
7092
7323
|
}
|
|
7093
7324
|
|
|
7094
7325
|
function rowHighlightClass(
|
|
@@ -7110,7 +7341,7 @@ function rowHighlightClass(
|
|
|
7110
7341
|
return 'group-hover/row:bg-theme-surface/50'
|
|
7111
7342
|
}
|
|
7112
7343
|
|
|
7113
|
-
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) {
|
|
7344
|
+
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) {
|
|
7114
7345
|
const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted, isChecked)
|
|
7115
7346
|
const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
|
|
7116
7347
|
const onClick = onRowClick ? () => onRowClick(resource, !!isSelected) : undefined
|
|
@@ -7125,8 +7356,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7125
7356
|
<td
|
|
7126
7357
|
onClick={onClick}
|
|
7127
7358
|
onMouseEnter={onMouseEnter}
|
|
7128
|
-
style={COMPARE_COLUMN_STYLE}
|
|
7129
|
-
className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight)}
|
|
7359
|
+
style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, showCheckbox, 'compare') }}
|
|
7360
|
+
className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight, PHONE_PINNED_CELL)}
|
|
7130
7361
|
>
|
|
7131
7362
|
{pickedSide ? (
|
|
7132
7363
|
<span
|
|
@@ -7148,7 +7379,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7148
7379
|
)}
|
|
7149
7380
|
{showCheckbox && (
|
|
7150
7381
|
<td
|
|
7151
|
-
|
|
7382
|
+
style={phonePinnedLeft(compareMode, showCheckbox, 'checkbox')}
|
|
7383
|
+
className={clsx('border-b-subtle text-center px-0 py-3 w-10 cursor-pointer transition-colors', rowHighlight, PHONE_PINNED_CELL)}
|
|
7152
7384
|
onClick={(e) => { e.stopPropagation(); onRowCheckToggle?.(resource) }}
|
|
7153
7385
|
>
|
|
7154
7386
|
<input
|
|
@@ -7159,16 +7391,18 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7159
7391
|
/>
|
|
7160
7392
|
</td>
|
|
7161
7393
|
)}
|
|
7162
|
-
{columns.map((col) => (
|
|
7394
|
+
{columns.map((col, colIdx) => (
|
|
7163
7395
|
<td
|
|
7164
7396
|
key={col.key}
|
|
7165
7397
|
onClick={cellsAreClickable ? onClick : undefined}
|
|
7166
7398
|
onMouseEnter={onMouseEnter}
|
|
7399
|
+
style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, showCheckbox, 'name') : undefined}
|
|
7167
7400
|
className={clsx(
|
|
7168
7401
|
'px-4 py-3 border-b-subtle transition-colors',
|
|
7169
7402
|
cellsAreClickable && 'cursor-pointer',
|
|
7170
7403
|
col.key !== 'status' && 'overflow-hidden truncate',
|
|
7171
7404
|
rowHighlight,
|
|
7405
|
+
colIdx === 0 && col.key === 'name' && PHONE_PINNED_CELL,
|
|
7172
7406
|
)}
|
|
7173
7407
|
>
|
|
7174
7408
|
<CellContent
|
|
@@ -7180,6 +7414,7 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7180
7414
|
storeProviders={storeProviders}
|
|
7181
7415
|
extraColumn={extraColumnsByKey?.get(col.key)}
|
|
7182
7416
|
nameHref={col.key === 'name' ? rowHref : undefined}
|
|
7417
|
+
findingMatch={col.key === 'name' ? findingMatch : undefined}
|
|
7183
7418
|
/>
|
|
7184
7419
|
</td>
|
|
7185
7420
|
))}
|
|
@@ -7229,9 +7464,10 @@ interface CellContentProps {
|
|
|
7229
7464
|
/** When set on the name column, the resource name renders as `<a href>`
|
|
7230
7465
|
* so ⌘-click / copy-link / hover-URL all work. */
|
|
7231
7466
|
nameHref?: string
|
|
7467
|
+
findingMatch?: TrivyFindingMatch
|
|
7232
7468
|
}
|
|
7233
7469
|
|
|
7234
|
-
function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref }: CellContentProps) {
|
|
7470
|
+
function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref, findingMatch }: CellContentProps) {
|
|
7235
7471
|
const { auditBadges } = useContext(ResourcesViewDataContext)
|
|
7236
7472
|
// Parent-injected extra columns short-circuit the built-in switch.
|
|
7237
7473
|
// Used by hosts that inject leading columns (e.g. a multi-cluster Cluster column).
|
|
@@ -7245,11 +7481,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7245
7481
|
if (column === 'name') {
|
|
7246
7482
|
const isTerminating = !!meta.deletionTimestamp
|
|
7247
7483
|
const nameClass = clsx('text-sm font-medium truncate block', isTerminating ? 'text-theme-text-tertiary line-through' : 'text-theme-text-primary')
|
|
7484
|
+
const pauseState = isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged' ? getCrossplanePauseState(resource) : null
|
|
7248
7485
|
const audit = auditBadges?.[`${meta.namespace || ''}/${meta.name}`]
|
|
7249
7486
|
const auditTotal = audit ? audit.danger + audit.warning : 0
|
|
7250
7487
|
return (
|
|
7251
7488
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
7252
|
-
<Tooltip content={meta.name}>
|
|
7489
|
+
<Tooltip content={meta.name} wrapperClassName={findingMatch || pauseState ? 'min-w-0' : undefined}>
|
|
7253
7490
|
{nameHref ? (
|
|
7254
7491
|
<a
|
|
7255
7492
|
href={nameHref}
|
|
@@ -7264,6 +7501,14 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7264
7501
|
)}
|
|
7265
7502
|
</Tooltip>
|
|
7266
7503
|
<CopyNameButton name={meta.name} />
|
|
7504
|
+
<CrossplanePauseChip state={pauseState} compact />
|
|
7505
|
+
{findingMatch && (
|
|
7506
|
+
<Tooltip content={findingMatch.text}>
|
|
7507
|
+
<span className="shrink-0 text-xs italic text-theme-text-tertiary cursor-help">
|
|
7508
|
+
{findingMatch.site === 'image' ? 'in image' : 'in findings'}
|
|
7509
|
+
</span>
|
|
7510
|
+
</Tooltip>
|
|
7511
|
+
)}
|
|
7267
7512
|
{auditTotal > 0 && audit && (
|
|
7268
7513
|
<Tooltip content={audit.messages && audit.messages.length > 0
|
|
7269
7514
|
? <AuditBadgeTooltip messages={audit.messages} />
|
|
@@ -7305,6 +7550,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7305
7550
|
|
|
7306
7551
|
// Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
|
|
7307
7552
|
const kindLower = normalizeKindToPlural(kind, group)
|
|
7553
|
+
if (column === 'status' && isLikelyCrossplaneMRGroup(kind, group ?? '') && isManagedResource(resource)) {
|
|
7554
|
+
return <ManagedResourceCell resource={resource} column={column} />
|
|
7555
|
+
}
|
|
7308
7556
|
if (group && !hasCuratedColumns(kind, group)) {
|
|
7309
7557
|
return <GenericCell resource={resource} column={column} />
|
|
7310
7558
|
}
|
|
@@ -7331,6 +7579,14 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7331
7579
|
return <KyvernoCleanupPolicyCell resource={resource} column={column} />
|
|
7332
7580
|
}
|
|
7333
7581
|
|
|
7582
|
+
if (isApiGroup(resource.apiVersion, 'opentelemetry.io') && ['opentelemetrycollectors', 'instrumentations'].includes(kindLower)) {
|
|
7583
|
+
if (column === 'status') return <GenericCell resource={resource} column={column} />
|
|
7584
|
+
return <span className="text-sm text-theme-text-secondary break-all">{getOpenTelemetryColumnValue(resource, column) || '-'}</span>
|
|
7585
|
+
}
|
|
7586
|
+
if (isApiGroup(resource.apiVersion, 'flagger.app') && ['canaries', 'metrictemplates'].includes(kindLower)) {
|
|
7587
|
+
if (column === 'status') return <GenericCell resource={resource} column={column} />
|
|
7588
|
+
return <span className="text-sm text-theme-text-secondary break-all">{getFlaggerColumnValue(resource, column) || '-'}</span>
|
|
7589
|
+
}
|
|
7334
7590
|
switch (kindLower) {
|
|
7335
7591
|
case 'pods':
|
|
7336
7592
|
return <PodCell resource={resource} column={column} />
|
|
@@ -7400,7 +7656,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7400
7656
|
// Disambiguate Gateway API vs Istio Gateway by apiVersion. Retained
|
|
7401
7657
|
// alongside the case above because not every call site normalizes the
|
|
7402
7658
|
// kind through GROUP_QUALIFIED_COLUMN_KEYS before dispatching.
|
|
7403
|
-
if (resource.apiVersion
|
|
7659
|
+
if (isApiGroup(resource.apiVersion, 'networking.istio.io')) {
|
|
7404
7660
|
return <IstioGatewayCell resource={resource} column={column} />
|
|
7405
7661
|
}
|
|
7406
7662
|
return <GatewayCell resource={resource} column={column} />
|
|
@@ -7890,6 +8146,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7890
8146
|
function GenericCell({ resource, column }: { resource: any; column: string }) {
|
|
7891
8147
|
switch (column) {
|
|
7892
8148
|
case 'status': {
|
|
8149
|
+
if (isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged') {
|
|
8150
|
+
return <CrossplaneStatusCell resource={resource} />
|
|
8151
|
+
}
|
|
7893
8152
|
const derived = getGenericResourceStatus(resource)
|
|
7894
8153
|
if (!derived) return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
7895
8154
|
return (
|