@skyhook-io/k8s-ui 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/src/components/charts/AreaChart.tsx +371 -0
- package/src/components/charts/MetricsSummary.tsx +49 -0
- package/src/components/charts/SeriesLegend.tsx +27 -0
- package/src/components/charts/colors.ts +49 -0
- package/src/components/charts/format.ts +41 -0
- package/src/components/charts/index.ts +14 -0
- package/src/components/charts/saturation.test.ts +60 -0
- package/src/components/charts/saturation.ts +32 -0
- package/src/components/charts/types.ts +36 -0
- package/src/components/compare/CompareResourcePicker.tsx +192 -0
- package/src/components/compare/CompareTray.tsx +130 -0
- package/src/components/compare/ResourceCompareView.tsx +272 -0
- package/src/components/compare/index.ts +14 -0
- package/src/components/compare/normalize.test.ts +193 -0
- package/src/components/compare/normalize.ts +69 -0
- package/src/components/compare/picks.test.ts +97 -0
- package/src/components/compare/picks.ts +35 -0
- package/src/components/compare/sort.test.ts +78 -0
- package/src/components/compare/sort.ts +26 -0
- package/src/components/compare/types.ts +43 -0
- package/src/components/compare/url.test.ts +61 -0
- package/src/components/compare/url.ts +26 -0
- package/src/components/resources/ResourcesView.tsx +431 -96
- package/src/components/resources/index.ts +1 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +233 -0
- package/src/components/resources/renderers/CompositionRenderer.tsx +218 -0
- package/src/components/resources/renderers/CrossplanePackageRenderer.tsx +145 -0
- package/src/components/resources/renderers/CrossplaneProviderConfigRenderer.tsx +71 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +131 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +223 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +6 -1
- package/src/components/resources/renderers/PodRenderer.tsx +207 -2
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +132 -20
- package/src/components/resources/renderers/RoleRenderer.tsx +148 -18
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +456 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +189 -2
- package/src/components/resources/renderers/XRDRenderer.tsx +153 -0
- package/src/components/resources/renderers/crossplane-cells.tsx +146 -0
- package/src/components/resources/renderers/flux-cells.tsx +12 -0
- package/src/components/resources/renderers/index.ts +8 -0
- package/src/components/resources/resource-utils-crossplane.test.ts +609 -0
- package/src/components/resources/resource-utils-crossplane.ts +325 -0
- package/src/components/resources/resource-utils-flux.ts +14 -0
- package/src/components/resources/resource-utils.ts +1 -0
- package/src/components/resources/resources-column-filter.test.ts +74 -0
- package/src/components/shared/ResourceActionsBar.tsx +77 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +138 -9
- package/src/components/topology/K8sResourceNode.tsx +21 -2
- package/src/components/topology/TopologyGraph.tsx +46 -10
- package/src/components/topology/layout.ts +20 -11
- package/src/components/topology/layout.worker.ts +21 -17
- package/src/components/ui/YamlEditor.tsx +28 -15
- package/src/components/workload/ResourceDetailDrawer.tsx +1 -1
- package/src/index.ts +6 -0
- package/src/perf/index.ts +1 -0
- package/src/perf/store.ts +110 -0
- package/src/types/core.ts +10 -0
- package/src/types/index.ts +1 -0
- package/src/types/rbac.ts +99 -0
- package/src/utils/api-resources.ts +9 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/rbac-badges.ts +61 -0
- package/src/utils/rbac-blast-radius.test.ts +137 -0
- package/src/utils/rbac-blast-radius.ts +98 -0
- package/src/utils/resource-hierarchy.ts +13 -3
- package/src/utils/structure-hash.test.ts +94 -0
- package/src/utils/structure-hash.ts +35 -0
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
Copy,
|
|
23
23
|
Check,
|
|
24
24
|
Plus,
|
|
25
|
+
GitCompare,
|
|
26
|
+
Regex,
|
|
25
27
|
} from 'lucide-react'
|
|
26
28
|
import { clsx } from 'clsx'
|
|
27
29
|
import { ResourceBar } from '../ui/ResourceBar'
|
|
@@ -137,6 +139,8 @@ import { PolicyReportCell, ClusterPolicyReportCell, KyvernoPolicyCell, ClusterPo
|
|
|
137
139
|
import { ExternalSecretCell, ClusterExternalSecretCell, SecretStoreCell, ClusterSecretStoreCell } from './renderers/eso-cells'
|
|
138
140
|
import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell } from './renderers/velero-cells'
|
|
139
141
|
import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell } from './renderers/cnpg-cells'
|
|
142
|
+
import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
143
|
+
import { isManagedResource, isComposite } from './resource-utils-crossplane'
|
|
140
144
|
import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
|
|
141
145
|
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'
|
|
142
146
|
import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
|
|
@@ -148,6 +152,7 @@ import { AzureManagedControlPlaneCell, AzureManagedMachinePoolCell, AzureMachine
|
|
|
148
152
|
import { useRegisterShortcut, useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
149
153
|
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
150
154
|
import type { SelectedKindInfo } from './ResourcesSidebar'
|
|
155
|
+
import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
|
|
151
156
|
|
|
152
157
|
// Pod problem filter options (special multi-select, not a single column value)
|
|
153
158
|
const POD_PROBLEMS = ['CrashLoopBackOff', 'ImagePullBackOff', 'OOMKilled', 'Unschedulable', 'Not Ready', 'High Restarts', 'Init Failed', 'Exit Code Error', 'Failed', 'Other'] as const
|
|
@@ -155,7 +160,7 @@ const WORKLOAD_PROBLEMS = ['Unavailable', 'Rollout Stuck', 'Rollout In Progress'
|
|
|
155
160
|
const WORKLOAD_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets'])
|
|
156
161
|
|
|
157
162
|
// Columns to skip for auto-detected filters (high cardinality, text-like, or non-filterable)
|
|
158
|
-
const SKIP_FILTER_COLUMNS = new Set([
|
|
163
|
+
export const SKIP_FILTER_COLUMNS = new Set([
|
|
159
164
|
'name', 'age', 'keys', 'size', 'images', 'domains', 'hosts', 'rules',
|
|
160
165
|
'ports', 'message', 'url', 'ref', 'revision', 'path', 'selector', 'ready', 'restarts',
|
|
161
166
|
'completions', 'duration', 'schedule', 'lastRun', 'target', 'replicas', 'metrics',
|
|
@@ -163,12 +168,22 @@ const SKIP_FILTER_COLUMNS = new Set([
|
|
|
163
168
|
'issuer', 'domain', 'presented', 'listeners', 'routes', 'addresses', 'hostnames',
|
|
164
169
|
'parents', 'backends', 'controller', 'description', 'externalIP', 'address',
|
|
165
170
|
'conditions', 'taints', 'desired', 'upToDate', 'available', 'owner',
|
|
166
|
-
'tls', 'endpoints', 'object', 'count', 'lastSeen', '
|
|
171
|
+
'tls', 'endpoints', 'object', 'count', 'lastSeen', 'source', 'inventory',
|
|
167
172
|
'lastUpdated', 'chart', 'events', 'repo',
|
|
168
173
|
'generators', 'applications', 'destinations', 'sources', 'budget', 'healthy', 'allowed',
|
|
169
174
|
'secrets', 'subjects', 'role', 'entrypoint', 'templates',
|
|
170
175
|
])
|
|
171
176
|
|
|
177
|
+
// Namespace/node cardinality scales with cluster size, not kind enum size;
|
|
178
|
+
// node keeps a generous cap as a sanity bound, namespace is uncapped.
|
|
179
|
+
export function isColumnFilterableByDistinctCount(colKey: string, distinctCount: number): boolean {
|
|
180
|
+
if (SKIP_FILTER_COLUMNS.has(colKey)) return false
|
|
181
|
+
const maxDistinct = colKey === 'namespace'
|
|
182
|
+
? Number.POSITIVE_INFINITY
|
|
183
|
+
: colKey === 'node' ? 200 : 30
|
|
184
|
+
return distinctCount >= 2 && distinctCount <= maxDistinct
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
// Column definitions per resource kind
|
|
173
188
|
interface Column {
|
|
174
189
|
key: string
|
|
@@ -233,11 +248,11 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
233
248
|
pods: [
|
|
234
249
|
{ key: 'name', label: 'Name' },
|
|
235
250
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
236
|
-
{ key: 'containers', label: 'Containers', width: 'w-
|
|
251
|
+
{ key: 'containers', label: 'Containers', width: 'w-32' },
|
|
237
252
|
{ key: 'status', label: 'Status', width: 'w-40' },
|
|
238
253
|
{ key: 'cpu', label: 'CPU', width: 'w-40', tooltip: 'CPU usage / limit (marker = request)' },
|
|
239
254
|
{ key: 'memory', label: 'Memory', width: 'w-40', tooltip: 'Memory usage / limit (marker = request)' },
|
|
240
|
-
{ key: 'restarts', label: 'Restarts', width: 'w-
|
|
255
|
+
{ key: 'restarts', label: 'Restarts', width: 'w-28' },
|
|
241
256
|
{ key: 'podIP', label: 'Pod IP', width: 'w-32', defaultVisible: false },
|
|
242
257
|
{ key: 'node', label: 'Node', width: 'w-44', hideOnMobile: true },
|
|
243
258
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -246,8 +261,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
246
261
|
{ key: 'name', label: 'Name' },
|
|
247
262
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
248
263
|
{ key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
|
|
249
|
-
{ key: 'upToDate', label: 'Up-to-date', width: 'w-
|
|
250
|
-
{ key: 'available', label: 'Available', width: 'w-
|
|
264
|
+
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
|
|
265
|
+
{ key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds)' },
|
|
251
266
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
252
267
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
253
268
|
],
|
|
@@ -256,8 +271,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
256
271
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
257
272
|
{ key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Number of nodes that should run the daemon pod (based on node selector)' },
|
|
258
273
|
{ key: 'ready', label: 'Ready', width: 'w-20', tooltip: 'Number of pods that are ready (passing readiness probes)' },
|
|
259
|
-
{ key: 'upToDate', label: 'Up-to-date', width: 'w-
|
|
260
|
-
{ key: 'available', label: 'Available', width: 'w-
|
|
274
|
+
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template spec' },
|
|
275
|
+
{ key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds duration)' },
|
|
261
276
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
262
277
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
263
278
|
],
|
|
@@ -265,7 +280,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
265
280
|
{ key: 'name', label: 'Name' },
|
|
266
281
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
267
282
|
{ key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
|
|
268
|
-
{ key: 'upToDate', label: 'Up-to-date', width: 'w-
|
|
283
|
+
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
|
|
269
284
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
270
285
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
271
286
|
],
|
|
@@ -328,7 +343,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
328
343
|
{ key: 'name', label: 'Name' },
|
|
329
344
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
330
345
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
331
|
-
{ key: 'completions', label: 'Completions', width: 'w-
|
|
346
|
+
{ key: 'completions', label: 'Completions', width: 'w-32' },
|
|
332
347
|
{ key: 'duration', label: 'Duration', width: 'w-24', hideOnMobile: true },
|
|
333
348
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
334
349
|
],
|
|
@@ -363,7 +378,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
363
378
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
364
379
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
365
380
|
{ key: 'capacity', label: 'Capacity', width: 'w-24' },
|
|
366
|
-
{ key: 'storageClass', label: 'Storage Class', width: 'w-
|
|
381
|
+
{ key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
|
|
367
382
|
{ key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'Access modes: RWO=ReadWriteOnce, RWX=ReadWriteMany, ROX=ReadOnlyMany' },
|
|
368
383
|
{ key: 'volume', label: 'Volume', width: 'w-48', hideOnMobile: true },
|
|
369
384
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -402,7 +417,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
402
417
|
{ key: 'capacity', label: 'Capacity', width: 'w-24' },
|
|
403
418
|
{ key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'RWO=ReadWriteOnce, ROX=ReadOnlyMany, RWX=ReadWriteMany' },
|
|
404
419
|
{ key: 'reclaimPolicy', label: 'Reclaim', width: 'w-20' },
|
|
405
|
-
{ key: 'storageClass', label: 'Storage Class', width: 'w-
|
|
420
|
+
{ key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
|
|
406
421
|
{ key: 'claim', label: 'Claim', width: 'w-48', hideOnMobile: true },
|
|
407
422
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
408
423
|
],
|
|
@@ -667,8 +682,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
667
682
|
serviceaccounts: [
|
|
668
683
|
{ key: 'name', label: 'Name' },
|
|
669
684
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
670
|
-
{ key: 'automount', label: 'Automount', width: 'w-
|
|
671
|
-
{ key: 'secrets', label: 'Secrets', width: 'w-
|
|
685
|
+
{ key: 'automount', label: 'Automount', width: 'w-32', tooltip: 'Whether token is automatically mounted in pods' },
|
|
686
|
+
{ key: 'secrets', label: 'Secrets', width: 'w-24' },
|
|
672
687
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
673
688
|
],
|
|
674
689
|
roles: [
|
|
@@ -705,13 +720,13 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
705
720
|
{ key: 'name', label: 'Name' },
|
|
706
721
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
707
722
|
{ key: 'holder', label: 'Holder', width: 'w-48' },
|
|
708
|
-
{ key: 'renewTime', label: 'Last Renewed', width: 'w-
|
|
723
|
+
{ key: 'renewTime', label: 'Last Renewed', width: 'w-32' },
|
|
709
724
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
710
725
|
],
|
|
711
726
|
priorityclasses: [
|
|
712
727
|
{ key: 'name', label: 'Name' },
|
|
713
728
|
{ key: 'value', label: 'Value', width: 'w-24' },
|
|
714
|
-
{ key: 'globalDefault', label: 'Global Default', width: 'w-
|
|
729
|
+
{ key: 'globalDefault', label: 'Global Default', width: 'w-36' },
|
|
715
730
|
{ key: 'preemptionPolicy', label: 'Preemption', width: 'w-32' },
|
|
716
731
|
{ key: 'description', label: 'Description', width: 'w-64', hideOnMobile: true },
|
|
717
732
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -723,27 +738,27 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
723
738
|
],
|
|
724
739
|
mutatingwebhookconfigurations: [
|
|
725
740
|
{ key: 'name', label: 'Name' },
|
|
726
|
-
{ key: 'webhooks', label: 'Webhooks', width: 'w-
|
|
727
|
-
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-
|
|
741
|
+
{ key: 'webhooks', label: 'Webhooks', width: 'w-28' },
|
|
742
|
+
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
|
|
728
743
|
{ key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
|
|
729
744
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
730
745
|
],
|
|
731
746
|
validatingwebhookconfigurations: [
|
|
732
747
|
{ key: 'name', label: 'Name' },
|
|
733
|
-
{ key: 'webhooks', label: 'Webhooks', width: 'w-
|
|
734
|
-
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-
|
|
748
|
+
{ key: 'webhooks', label: 'Webhooks', width: 'w-28' },
|
|
749
|
+
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
|
|
735
750
|
{ key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
|
|
736
751
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
737
752
|
],
|
|
738
753
|
events: [
|
|
739
754
|
{ key: 'name', label: 'Name' },
|
|
740
755
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
741
|
-
{ key: 'type', label: 'Type', width: 'w-
|
|
742
|
-
{ key: 'reason', label: 'Reason', width: 'w-
|
|
756
|
+
{ key: 'type', label: 'Type', width: 'w-24' },
|
|
757
|
+
{ key: 'reason', label: 'Reason', width: 'w-32' },
|
|
743
758
|
{ key: 'message', label: 'Message', width: 'w-64' },
|
|
744
759
|
{ key: 'object', label: 'Object', width: 'w-48', hideOnMobile: true },
|
|
745
|
-
{ key: 'count', label: 'Count', width: 'w-
|
|
746
|
-
{ key: 'lastSeen', label: 'Last Seen', width: 'w-
|
|
760
|
+
{ key: 'count', label: 'Count', width: 'w-20' },
|
|
761
|
+
{ key: 'lastSeen', label: 'Last Seen', width: 'w-28' },
|
|
747
762
|
],
|
|
748
763
|
// ============================================================================
|
|
749
764
|
// FLUXCD GITOPS RESOURCES
|
|
@@ -791,6 +806,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
791
806
|
{ key: 'chart', label: 'Chart', width: 'w-40' },
|
|
792
807
|
{ key: 'version', label: 'Version', width: 'w-24' },
|
|
793
808
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
809
|
+
{ key: 'message', label: 'Message', width: 'w-64', hideOnMobile: true, tooltip: 'Last diagnostic message — distinguishes dependency-wait, install/upgrade failure, and test failure' },
|
|
794
810
|
{ key: 'revision', label: 'Rev', width: 'w-16', hideOnMobile: true, tooltip: 'Helm release revision number' },
|
|
795
811
|
{ key: 'lastUpdated', label: 'Last Updated', width: 'w-28', tooltip: 'Time since last successful reconciliation' },
|
|
796
812
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -932,7 +948,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
932
948
|
{ key: 'store', label: 'Store', width: 'w-36' },
|
|
933
949
|
{ key: 'provider', label: 'Provider', width: 'w-28' },
|
|
934
950
|
{ key: 'refreshInterval', label: 'Refresh', width: 'w-24' },
|
|
935
|
-
{ key: 'lastSync', label: 'Last Sync', width: 'w-
|
|
951
|
+
{ key: 'lastSync', label: 'Last Sync', width: 'w-28' },
|
|
936
952
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
937
953
|
],
|
|
938
954
|
clusterexternalsecrets: [
|
|
@@ -966,7 +982,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
966
982
|
{ key: 'namespaces', label: 'Scope', width: 'w-24', tooltip: 'Included namespaces (* = all)' },
|
|
967
983
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
968
984
|
{ key: 'expiry', label: 'Expires', width: 'w-24' },
|
|
969
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
985
|
+
{ key: 'errors', label: 'Errors', width: 'w-20' },
|
|
970
986
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
971
987
|
],
|
|
972
988
|
restores: [
|
|
@@ -975,7 +991,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
975
991
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
976
992
|
{ key: 'backupName', label: 'Backup', width: 'w-40' },
|
|
977
993
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
978
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
994
|
+
{ key: 'errors', label: 'Errors', width: 'w-20' },
|
|
979
995
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
980
996
|
],
|
|
981
997
|
schedules: [
|
|
@@ -1004,10 +1020,10 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1004
1020
|
{ key: 'name', label: 'Name' },
|
|
1005
1021
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1006
1022
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
1007
|
-
{ key: 'instances', label: 'Instances', width: 'w-
|
|
1023
|
+
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Ready/Total' },
|
|
1008
1024
|
{ key: 'primary', label: 'Primary', width: 'w-36' },
|
|
1009
1025
|
{ key: 'image', label: 'Image', width: 'w-28' },
|
|
1010
|
-
{ key: 'storage', label: 'Storage', width: 'w-
|
|
1026
|
+
{ key: 'storage', label: 'Storage', width: 'w-28' },
|
|
1011
1027
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1012
1028
|
],
|
|
1013
1029
|
scheduledbackups: [
|
|
@@ -1016,8 +1032,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1016
1032
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
1017
1033
|
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1018
1034
|
{ key: 'schedule', label: 'Schedule', width: 'w-36' },
|
|
1019
|
-
{ key: 'lastSchedule', label: 'Last Run', width: 'w-
|
|
1020
|
-
{ key: 'suspended', label: 'Suspended', width: 'w-
|
|
1035
|
+
{ key: 'lastSchedule', label: 'Last Run', width: 'w-28' },
|
|
1036
|
+
{ key: 'suspended', label: 'Suspended', width: 'w-28' },
|
|
1021
1037
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1022
1038
|
],
|
|
1023
1039
|
poolers: [
|
|
@@ -1026,8 +1042,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1026
1042
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
1027
1043
|
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1028
1044
|
{ key: 'type', label: 'Type', width: 'w-16' },
|
|
1029
|
-
{ key: 'poolMode', label: 'Pool Mode', width: 'w-
|
|
1030
|
-
{ key: 'instances', label: 'Instances', width: 'w-
|
|
1045
|
+
{ key: 'poolMode', label: 'Pool Mode', width: 'w-32' },
|
|
1046
|
+
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Ready/Total' },
|
|
1031
1047
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1032
1048
|
],
|
|
1033
1049
|
// ============================================================================
|
|
@@ -1101,7 +1117,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1101
1117
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
1102
1118
|
{ key: 'routing', label: 'Traffic', width: 'w-20', tooltip: 'Whether this revision is receiving traffic' },
|
|
1103
1119
|
{ key: 'image', label: 'Image', width: 'w-48' },
|
|
1104
|
-
{ key: 'concurrency', label: 'Concurrency', width: 'w-
|
|
1120
|
+
{ key: 'concurrency', label: 'Concurrency', width: 'w-32' },
|
|
1105
1121
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1106
1122
|
],
|
|
1107
1123
|
knativeroutes: [
|
|
@@ -1363,7 +1379,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1363
1379
|
{ key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
|
|
1364
1380
|
{ key: 'cluster', label: 'Cluster', width: 'w-32 shrink-0' },
|
|
1365
1381
|
{ key: 'ready', label: 'Ready', width: 'w-20 shrink-0' },
|
|
1366
|
-
{ key: 'initialized', label: 'Initialized', width: 'w-
|
|
1382
|
+
{ key: 'initialized', label: 'Initialized', width: 'w-28 shrink-0' },
|
|
1367
1383
|
{ key: 'version', label: 'Version', width: 'w-24 shrink-0' },
|
|
1368
1384
|
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1369
1385
|
],
|
|
@@ -1513,6 +1529,43 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1513
1529
|
{ key: 'status', label: 'Status', width: 'w-28 shrink-0' },
|
|
1514
1530
|
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1515
1531
|
],
|
|
1532
|
+
// Crossplane — Managed Resources are unbounded (one kind per provider CRD);
|
|
1533
|
+
// routed via isLikelyCrossplaneMRGroup, not by exact kind plural.
|
|
1534
|
+
crossplanemanagedresources: [
|
|
1535
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1536
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-32 shrink-0' },
|
|
1537
|
+
{ key: 'kind', label: 'Kind', width: 'w-32 shrink-0' },
|
|
1538
|
+
{ key: 'external', label: 'External Name', width: 'min-w-48', hideOnMobile: true },
|
|
1539
|
+
{ key: 'provider', label: 'Provider Config', width: 'w-40 shrink-0', hideOnMobile: true },
|
|
1540
|
+
{ key: 'status', label: 'Status', width: 'w-32 shrink-0' },
|
|
1541
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1542
|
+
],
|
|
1543
|
+
providers: [
|
|
1544
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1545
|
+
{ key: 'package', label: 'Package', width: 'min-w-48' },
|
|
1546
|
+
{ key: 'revision', label: 'Revision', width: 'w-36 shrink-0', hideOnMobile: true },
|
|
1547
|
+
{ key: 'status', label: 'Status', width: 'w-32 shrink-0' },
|
|
1548
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1549
|
+
],
|
|
1550
|
+
providerconfigs: [
|
|
1551
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1552
|
+
{ key: 'credentials', label: 'Credentials', width: 'w-36 shrink-0' },
|
|
1553
|
+
{ key: 'status', label: 'Status', width: 'w-32 shrink-0' },
|
|
1554
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1555
|
+
],
|
|
1556
|
+
compositions: [
|
|
1557
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1558
|
+
{ key: 'composite', label: 'Composite Kind', width: 'w-44 shrink-0' },
|
|
1559
|
+
{ key: 'mode', label: 'Mode', width: 'w-24 shrink-0' },
|
|
1560
|
+
{ key: 'functions', label: 'Functions', width: 'w-28 shrink-0', hideOnMobile: true },
|
|
1561
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1562
|
+
],
|
|
1563
|
+
compositeresourcedefinitions: [
|
|
1564
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1565
|
+
{ key: 'kind', label: 'Kind', width: 'w-40 shrink-0' },
|
|
1566
|
+
{ key: 'claim', label: 'Claim Kind', width: 'w-40 shrink-0', hideOnMobile: true },
|
|
1567
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1568
|
+
],
|
|
1516
1569
|
}
|
|
1517
1570
|
|
|
1518
1571
|
// Map (plural, group) → KNOWN_COLUMNS key for kinds that collide with core K8s
|
|
@@ -1545,8 +1598,32 @@ function normalizeKindToPlural(kind: string, group?: string): string {
|
|
|
1545
1598
|
return lower
|
|
1546
1599
|
}
|
|
1547
1600
|
|
|
1601
|
+
// Crossplane Managed Resources are unbounded (each provider ships its own
|
|
1602
|
+
// CRDs under per-service groups like s3.aws.upbound.io, compute.gcp.upbound.io).
|
|
1603
|
+
// Route any non-core-Crossplane resource in these groups to the generic MR
|
|
1604
|
+
// column set. ProviderConfig is excluded — those have their own renderer.
|
|
1605
|
+
function isLikelyCrossplaneMRGroup(kind: string, group: string): boolean {
|
|
1606
|
+
if (!group) return false
|
|
1607
|
+
const k = kind.toLowerCase()
|
|
1608
|
+
if (k === 'providerconfig' || k === 'providerconfigs') return false
|
|
1609
|
+
if (group.endsWith('.upbound.io')) return true
|
|
1610
|
+
if (group === 'kubernetes.crossplane.io' || group === 'helm.crossplane.io') return true
|
|
1611
|
+
// Reserved Crossplane core groups — never MRs.
|
|
1612
|
+
if (group === 'crossplane.io' || group === 'pkg.crossplane.io' || group === 'apiextensions.crossplane.io') {
|
|
1613
|
+
return false
|
|
1614
|
+
}
|
|
1615
|
+
// Any other *.crossplane.io subgroup is presumed to be a provider's MR group.
|
|
1616
|
+
if (group.endsWith('.crossplane.io')) return true
|
|
1617
|
+
return false
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1548
1620
|
function getColumnsForKind(kind: string, group?: string): Column[] {
|
|
1549
|
-
|
|
1621
|
+
const key = normalizeKindToPlural(kind, group)
|
|
1622
|
+
if (KNOWN_COLUMNS[key]) return KNOWN_COLUMNS[key]
|
|
1623
|
+
if (group && isLikelyCrossplaneMRGroup(kind, group)) {
|
|
1624
|
+
return KNOWN_COLUMNS.crossplanemanagedresources
|
|
1625
|
+
}
|
|
1626
|
+
return DEFAULT_COLUMNS
|
|
1550
1627
|
}
|
|
1551
1628
|
|
|
1552
1629
|
// Get the default visible columns for a kind
|
|
@@ -1679,6 +1756,26 @@ interface ResourcesViewProps {
|
|
|
1679
1756
|
* should also wire onResourceClick to handle the deep-link-on-load
|
|
1680
1757
|
* case. */
|
|
1681
1758
|
onRowSelect?: (resource: any) => void
|
|
1759
|
+
/**
|
|
1760
|
+
* Overrides the default compare-mode submit (which navigates to
|
|
1761
|
+
* `/compare?kind=...&a=...&b=...`). Hosts use this to route to a
|
|
1762
|
+
* different URL — e.g. Radar Hub's `/fleet/compare` with cluster IDs.
|
|
1763
|
+
* Picks arrive in click order, each carrying `clusterId`/`clusterName`
|
|
1764
|
+
* when `resolveRowCluster` is set (which is what keeps cross-cluster
|
|
1765
|
+
* picks distinct in the equality check).
|
|
1766
|
+
*/
|
|
1767
|
+
onCompareSubmit?: (
|
|
1768
|
+
picks: NamespacedRef[],
|
|
1769
|
+
kind: { name: string; group?: string },
|
|
1770
|
+
) => void
|
|
1771
|
+
/**
|
|
1772
|
+
* Resolves the cluster scope for a row when compare-mode is enabled.
|
|
1773
|
+
* Stamps `clusterId`/`clusterName` onto each pick so the same `ns/name`
|
|
1774
|
+
* in two different clusters is treated as two distinct picks. Hub-web
|
|
1775
|
+
* returns `row._cluster` here; OSS leaves it unset and picks key on
|
|
1776
|
+
* namespace+name only.
|
|
1777
|
+
*/
|
|
1778
|
+
resolveRowCluster?: (resource: any) => { id: string; name: string } | undefined
|
|
1682
1779
|
}
|
|
1683
1780
|
|
|
1684
1781
|
// Default selected kind
|
|
@@ -1823,6 +1920,8 @@ export function ResourcesView({
|
|
|
1823
1920
|
defaultKind = DEFAULT_KIND_INFO,
|
|
1824
1921
|
extraLeadingColumns,
|
|
1825
1922
|
onRowSelect,
|
|
1923
|
+
onCompareSubmit,
|
|
1924
|
+
resolveRowCluster,
|
|
1826
1925
|
}: ResourcesViewProps) {
|
|
1827
1926
|
const initialFilters = getInitialFiltersFromURL()
|
|
1828
1927
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
|
|
@@ -1842,6 +1941,7 @@ export function ResourcesView({
|
|
|
1842
1941
|
onSelectedKindChange?.(selectedKind)
|
|
1843
1942
|
}, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
1844
1943
|
const [searchTerm, setSearchTerm] = useState(initialFilters.search)
|
|
1944
|
+
const [regexMode, setRegexMode] = useState(false)
|
|
1845
1945
|
const [sortColumn, setSortColumn] = useState<string | null>(null)
|
|
1846
1946
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null)
|
|
1847
1947
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
|
@@ -1966,6 +2066,13 @@ export function ResourcesView({
|
|
|
1966
2066
|
|
|
1967
2067
|
useEffect(() => {
|
|
1968
2068
|
const saved = loadColumnSettings(selectedKind.name, selectedKind.group)
|
|
2069
|
+
// Host-injected extra columns (e.g. fleet Cluster) default to visible
|
|
2070
|
+
// even when the saved column-visibility blob predates them — a naive
|
|
2071
|
+
// Set(saved.visible) would silently hide host columns the user has
|
|
2072
|
+
// never been shown. Trade-off: a user can't permanently hide a host
|
|
2073
|
+
// extra column via the column picker — next mount re-adds it. Track
|
|
2074
|
+
// a sibling "hidden-extras" set if this becomes a real complaint.
|
|
2075
|
+
const extraKeys = extraLeadingColumns?.map(c => c.key) ?? []
|
|
1969
2076
|
if (saved) {
|
|
1970
2077
|
// If saved columns are just the defaults but this kind has specialized columns,
|
|
1971
2078
|
// discard the stale save and use the specialized columns instead
|
|
@@ -1978,14 +2085,16 @@ export function ResourcesView({
|
|
|
1978
2085
|
setVisibleColumns(getDefaultVisibleColumns(allColumns))
|
|
1979
2086
|
setColumnWidths({})
|
|
1980
2087
|
} else {
|
|
1981
|
-
|
|
2088
|
+
const merged = new Set(saved.visible)
|
|
2089
|
+
for (const k of extraKeys) merged.add(k)
|
|
2090
|
+
setVisibleColumns(merged)
|
|
1982
2091
|
setColumnWidths(saved.widths || {})
|
|
1983
2092
|
}
|
|
1984
2093
|
} else {
|
|
1985
2094
|
setVisibleColumns(getDefaultVisibleColumns(allColumns))
|
|
1986
2095
|
setColumnWidths({})
|
|
1987
2096
|
}
|
|
1988
|
-
}, [selectedKind.name, selectedKind.group, allColumns])
|
|
2097
|
+
}, [selectedKind.name, selectedKind.group, allColumns, extraLeadingColumns])
|
|
1989
2098
|
|
|
1990
2099
|
// Save column settings when they change (skip initial load)
|
|
1991
2100
|
const isColumnSettingsLoaded = useRef(false)
|
|
@@ -2121,9 +2230,59 @@ export function ResourcesView({
|
|
|
2121
2230
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
2122
2231
|
const virtuosoRef = useRef<TableVirtuosoHandle>(null)
|
|
2123
2232
|
|
|
2233
|
+
// Compare mode is only meaningful when the host can route picks. Either
|
|
2234
|
+
// path qualifies: onNavigate for the default same-cluster `/compare?...`
|
|
2235
|
+
// URL, or onCompareSubmit for hosts that build their own URL (e.g.
|
|
2236
|
+
// Radar Hub's `/fleet/compare` with cluster IDs).
|
|
2237
|
+
const compareEnabled = !!onNavigate || !!onCompareSubmit
|
|
2238
|
+
const [compareMode, setCompareMode] = useState(false)
|
|
2239
|
+
const [comparePicks, setComparePicks] = useState<CompareTrayPick[]>([])
|
|
2240
|
+
|
|
2241
|
+
// Kind change invalidates picks AND mode — leaving the tray on with empty
|
|
2242
|
+
// pills after the user moves to a different kind is more confusing than helpful.
|
|
2243
|
+
useEffect(() => {
|
|
2244
|
+
setComparePicks([])
|
|
2245
|
+
setCompareMode(false)
|
|
2246
|
+
}, [selectedKind.name, selectedKind.group])
|
|
2247
|
+
|
|
2248
|
+
const exitCompareMode = useCallback(() => {
|
|
2249
|
+
setCompareMode(false)
|
|
2250
|
+
setComparePicks([])
|
|
2251
|
+
}, [])
|
|
2252
|
+
|
|
2253
|
+
const toggleComparePick = useCallback((resource: any) => {
|
|
2254
|
+
if (!resource?.metadata?.name) return
|
|
2255
|
+
const ns = resource.metadata.namespace || ''
|
|
2256
|
+
// Stamp clusterId on the pick when the host provides a resolver
|
|
2257
|
+
// (cross-cluster compare). Without it, two rows with the same ns+name
|
|
2258
|
+
// from different clusters would collapse into one pick.
|
|
2259
|
+
const cluster = resolveRowCluster?.(resource)
|
|
2260
|
+
setComparePicks(prev => togglePick(prev, {
|
|
2261
|
+
namespace: ns,
|
|
2262
|
+
name: resource.metadata.name,
|
|
2263
|
+
clusterId: cluster?.id,
|
|
2264
|
+
clusterName: cluster?.name,
|
|
2265
|
+
}))
|
|
2266
|
+
}, [resolveRowCluster])
|
|
2267
|
+
|
|
2268
|
+
const handleCompareNavigate = useCallback(() => {
|
|
2269
|
+
if (comparePicks.length !== 2) return
|
|
2270
|
+
if (onCompareSubmit) {
|
|
2271
|
+
onCompareSubmit(comparePicks, { name: selectedKind.name, group: selectedKind.group })
|
|
2272
|
+
return
|
|
2273
|
+
}
|
|
2274
|
+
if (!onNavigate) return
|
|
2275
|
+
const params = new URLSearchParams()
|
|
2276
|
+
params.set('kind', selectedKind.name)
|
|
2277
|
+
if (selectedKind.group) params.set('apiGroup', selectedKind.group)
|
|
2278
|
+
params.set('a', refToParam(comparePicks[0]))
|
|
2279
|
+
params.set('b', refToParam(comparePicks[1]))
|
|
2280
|
+
onNavigate(`/compare?${params.toString()}`)
|
|
2281
|
+
}, [comparePicks, selectedKind.name, selectedKind.group, onNavigate, onCompareSubmit])
|
|
2282
|
+
|
|
2124
2283
|
// Reset highlight when kind, search, sort, or namespace changes
|
|
2125
2284
|
const namespacesKey = namespaces.join(',')
|
|
2126
|
-
useEffect(() => { setHighlightedIndex(-1) }, [selectedKind.name, searchTerm, sortColumn, sortDirection, namespacesKey])
|
|
2285
|
+
useEffect(() => { setHighlightedIndex(-1) }, [selectedKind.name, searchTerm, regexMode, sortColumn, sortDirection, namespacesKey])
|
|
2127
2286
|
|
|
2128
2287
|
// Scroll highlighted row into view
|
|
2129
2288
|
useEffect(() => {
|
|
@@ -2226,7 +2385,11 @@ export function ResourcesView({
|
|
|
2226
2385
|
description: 'Open resource detail',
|
|
2227
2386
|
category: 'Resource Actions',
|
|
2228
2387
|
scope: 'resources',
|
|
2229
|
-
handler: () =>
|
|
2388
|
+
handler: () => {
|
|
2389
|
+
const res = getHighlightedResource()
|
|
2390
|
+
if (compareMode) toggleComparePick(res)
|
|
2391
|
+
else selectResource(res)
|
|
2392
|
+
},
|
|
2230
2393
|
enabled: highlightedIndex >= 0,
|
|
2231
2394
|
},
|
|
2232
2395
|
{
|
|
@@ -2235,7 +2398,11 @@ export function ResourcesView({
|
|
|
2235
2398
|
description: 'Open resource detail',
|
|
2236
2399
|
category: 'Resource Actions',
|
|
2237
2400
|
scope: 'resources',
|
|
2238
|
-
handler: () =>
|
|
2401
|
+
handler: () => {
|
|
2402
|
+
const res = getHighlightedResource()
|
|
2403
|
+
if (compareMode) toggleComparePick(res)
|
|
2404
|
+
else selectResource(res)
|
|
2405
|
+
},
|
|
2239
2406
|
enabled: highlightedIndex >= 0,
|
|
2240
2407
|
},
|
|
2241
2408
|
{
|
|
@@ -2250,7 +2417,7 @@ export function ResourcesView({
|
|
|
2250
2417
|
const cb = onResourceClickYaml || onResourceClick
|
|
2251
2418
|
cb?.({ kind: selectedKind.name, namespace: res.metadata.namespace || '', name: res.metadata.name, group: selectedKind.group })
|
|
2252
2419
|
},
|
|
2253
|
-
enabled: highlightedIndex >= 0,
|
|
2420
|
+
enabled: highlightedIndex >= 0 && !compareMode,
|
|
2254
2421
|
},
|
|
2255
2422
|
{
|
|
2256
2423
|
id: 'resources-logs',
|
|
@@ -2274,7 +2441,7 @@ export function ResourcesView({
|
|
|
2274
2441
|
openWorkloadLogs?.({ namespace: ns, workloadKind: selectedKind.kind, workloadName: name })
|
|
2275
2442
|
}
|
|
2276
2443
|
},
|
|
2277
|
-
enabled: highlightedIndex >= 0 && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs'].includes(selectedKind.name.toLowerCase()),
|
|
2444
|
+
enabled: highlightedIndex >= 0 && !compareMode && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs'].includes(selectedKind.name.toLowerCase()),
|
|
2278
2445
|
},
|
|
2279
2446
|
{
|
|
2280
2447
|
id: 'resources-sort-name',
|
|
@@ -2307,11 +2474,11 @@ export function ResourcesView({
|
|
|
2307
2474
|
category: 'Table',
|
|
2308
2475
|
scope: 'resources',
|
|
2309
2476
|
handler: () => {
|
|
2310
|
-
// Close dropdowns first, then clear highlight, then blur search
|
|
2311
2477
|
if (showColumnPicker) { setShowColumnPicker(false); return }
|
|
2312
2478
|
if (openColumnFilter) { setOpenColumnFilter(null); return }
|
|
2313
2479
|
if (showProblemsDropdown) { setShowProblemsDropdown(false); return }
|
|
2314
2480
|
if (showLabelsDropdown) { setShowLabelsDropdown(false); return }
|
|
2481
|
+
if (compareMode) { exitCompareMode(); return }
|
|
2315
2482
|
if (highlightedIndex >= 0) setHighlightedIndex(-1)
|
|
2316
2483
|
else searchInputRef.current?.blur()
|
|
2317
2484
|
},
|
|
@@ -2908,6 +3075,18 @@ export function ResourcesView({
|
|
|
2908
3075
|
}, [])
|
|
2909
3076
|
|
|
2910
3077
|
|
|
3078
|
+
// On an invalid pattern, fall back to a null matcher (search un-applied, all
|
|
3079
|
+
// rows shown) rather than zero results, so the table doesn't flash empty
|
|
3080
|
+
// while the user is mid-typing a pattern.
|
|
3081
|
+
const searchRegex = useMemo<{ re: RegExp | null; error: string | null }>(() => {
|
|
3082
|
+
if (!regexMode || !searchTerm) return { re: null, error: null }
|
|
3083
|
+
try {
|
|
3084
|
+
return { re: new RegExp(searchTerm, 'i'), error: null }
|
|
3085
|
+
} catch (e) {
|
|
3086
|
+
return { re: null, error: e instanceof Error ? e.message : 'Invalid regex' }
|
|
3087
|
+
}
|
|
3088
|
+
}, [regexMode, searchTerm])
|
|
3089
|
+
|
|
2911
3090
|
// Filter resources by search term, status, problems, and sort
|
|
2912
3091
|
const filteredResources = useMemo(() => {
|
|
2913
3092
|
if (!resources) return []
|
|
@@ -2916,11 +3095,21 @@ export function ResourcesView({
|
|
|
2916
3095
|
|
|
2917
3096
|
// Apply search filter
|
|
2918
3097
|
if (searchTerm) {
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3098
|
+
if (regexMode) {
|
|
3099
|
+
const re = searchRegex.re
|
|
3100
|
+
if (re) {
|
|
3101
|
+
result = result.filter((r: any) =>
|
|
3102
|
+
re.test(r.metadata?.name ?? '') ||
|
|
3103
|
+
re.test(r.metadata?.namespace ?? '')
|
|
3104
|
+
)
|
|
3105
|
+
}
|
|
3106
|
+
} else {
|
|
3107
|
+
const term = searchTerm.toLowerCase()
|
|
3108
|
+
result = result.filter((r: any) =>
|
|
3109
|
+
r.metadata?.name?.toLowerCase().includes(term) ||
|
|
3110
|
+
r.metadata?.namespace?.toLowerCase().includes(term)
|
|
3111
|
+
)
|
|
3112
|
+
}
|
|
2924
3113
|
}
|
|
2925
3114
|
|
|
2926
3115
|
// Apply column filters (generic, multi-select per column — OR within column, AND across columns)
|
|
@@ -3076,7 +3265,7 @@ export function ResourcesView({
|
|
|
3076
3265
|
}
|
|
3077
3266
|
|
|
3078
3267
|
return result
|
|
3079
|
-
}, [resources, searchTerm, columnFilters, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, sortColumn, sortDirection, getSortValue, podMatchesProblemFilter])
|
|
3268
|
+
}, [resources, searchTerm, regexMode, searchRegex, columnFilters, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, sortColumn, sortDirection, getSortValue, podMatchesProblemFilter])
|
|
3080
3269
|
|
|
3081
3270
|
// For nodes table: compute the majority minor version so outliers can be highlighted
|
|
3082
3271
|
const majorityNodeMinorVersion = useMemo(() => {
|
|
@@ -3156,6 +3345,14 @@ export function ResourcesView({
|
|
|
3156
3345
|
style={{ ...props.style, tableLayout: 'fixed' }}
|
|
3157
3346
|
>
|
|
3158
3347
|
<colgroup>
|
|
3348
|
+
{/*
|
|
3349
|
+
Compare-mode adds a leading <th>/<td> per row but isn't part
|
|
3350
|
+
of `columns`. Under table-layout:fixed the <colgroup> must
|
|
3351
|
+
match the actual column count, otherwise the browser pads
|
|
3352
|
+
the missing entry by stealing width from a sized neighbour
|
|
3353
|
+
— typically blowing this narrow column out to ~200px.
|
|
3354
|
+
*/}
|
|
3355
|
+
{compareMode && <col style={{ width: 36 }} />}
|
|
3159
3356
|
{columns.map(col => (
|
|
3160
3357
|
<col
|
|
3161
3358
|
key={col.key}
|
|
@@ -3173,7 +3370,7 @@ export function ResourcesView({
|
|
|
3173
3370
|
)
|
|
3174
3371
|
}),
|
|
3175
3372
|
TableRow: VirtuosoTableRow,
|
|
3176
|
-
}), [columns, columnWidths, hasResizedColumns])
|
|
3373
|
+
}), [columns, columnWidths, hasResizedColumns, compareMode])
|
|
3177
3374
|
|
|
3178
3375
|
// Calculate filter options with counts based on current resources (before filtering)
|
|
3179
3376
|
const filterOptions = useMemo(() => {
|
|
@@ -3213,10 +3410,7 @@ export function ResourcesView({
|
|
|
3213
3410
|
}
|
|
3214
3411
|
|
|
3215
3412
|
const distinctCount = Object.keys(valueCounts).length
|
|
3216
|
-
|
|
3217
|
-
// Node column gets a higher cap (50) since clusters commonly have 20-50 nodes
|
|
3218
|
-
const maxDistinct = col.key === 'node' ? 50 : 20
|
|
3219
|
-
if (distinctCount >= 2 && distinctCount <= maxDistinct) {
|
|
3413
|
+
if (isColumnFilterableByDistinctCount(col.key, distinctCount)) {
|
|
3220
3414
|
filterableColumns.push({
|
|
3221
3415
|
key: col.key,
|
|
3222
3416
|
label: col.label,
|
|
@@ -3387,38 +3581,68 @@ export function ResourcesView({
|
|
|
3387
3581
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
3388
3582
|
{/* Toolbar */}
|
|
3389
3583
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0">
|
|
3390
|
-
<div className="flex-1
|
|
3391
|
-
<
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3584
|
+
<div className="flex-1 min-w-0">
|
|
3585
|
+
<div className="relative max-w-md">
|
|
3586
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
3587
|
+
<input
|
|
3588
|
+
ref={searchInputRef}
|
|
3589
|
+
type="text"
|
|
3590
|
+
placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
|
|
3591
|
+
value={searchTerm}
|
|
3592
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
3593
|
+
onKeyDown={(e) => {
|
|
3594
|
+
if (e.key === 'ArrowDown') {
|
|
3595
|
+
// Hand off to the table's keyboard navigation — blur the input
|
|
3596
|
+
// so the registered ArrowDown/j/k shortcuts take over, and
|
|
3597
|
+
// highlight the first row.
|
|
3598
|
+
e.preventDefault()
|
|
3599
|
+
searchInputRef.current?.blur()
|
|
3600
|
+
setHighlightedIndex(0)
|
|
3601
|
+
} else if (e.key === 'Enter' && filteredResourceCountRef.current > 0) {
|
|
3602
|
+
// Select the first (or currently highlighted) resource
|
|
3603
|
+
e.preventDefault()
|
|
3604
|
+
searchInputRef.current?.blur()
|
|
3605
|
+
if (highlightedIndex < 0) setHighlightedIndex(0)
|
|
3606
|
+
// Defer to next frame so the highlight renders before we open
|
|
3607
|
+
requestAnimationFrame(() => {
|
|
3608
|
+
const res = highlightedResourceRef.current ?? filteredResources[0]
|
|
3609
|
+
selectResource(res)
|
|
3610
|
+
})
|
|
3611
|
+
} else if (e.key === 'Escape') {
|
|
3612
|
+
searchInputRef.current?.blur()
|
|
3613
|
+
}
|
|
3614
|
+
}}
|
|
3615
|
+
className={clsx(
|
|
3616
|
+
'w-full pl-10 pr-10 py-2 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
|
|
3617
|
+
searchRegex.error
|
|
3618
|
+
? 'border-red-500/60 focus:ring-red-500'
|
|
3619
|
+
: 'border-theme-border-light focus:ring-skyhook-500'
|
|
3620
|
+
)}
|
|
3621
|
+
/>
|
|
3622
|
+
<button
|
|
3623
|
+
type="button"
|
|
3624
|
+
onClick={() => setRegexMode((v) => !v)}
|
|
3625
|
+
aria-pressed={regexMode}
|
|
3626
|
+
aria-label={regexMode ? 'Disable regex search' : 'Enable regex search'}
|
|
3627
|
+
title={regexMode ? 'Regex search enabled — click to disable' : 'Enable regex search'}
|
|
3628
|
+
className={clsx(
|
|
3629
|
+
'absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-6 h-6 rounded transition-colors',
|
|
3630
|
+
regexMode
|
|
3631
|
+
? 'bg-skyhook-500/20 text-skyhook-400'
|
|
3632
|
+
: 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover'
|
|
3633
|
+
)}
|
|
3634
|
+
>
|
|
3635
|
+
<Regex className="w-3.5 h-3.5" />
|
|
3636
|
+
</button>
|
|
3637
|
+
{searchRegex.error && (
|
|
3638
|
+
<div
|
|
3639
|
+
title={searchRegex.error}
|
|
3640
|
+
className="absolute left-0 top-full mt-1 z-10 px-2 py-1 rounded bg-theme-elevated border border-red-500/40 text-[11px] text-red-400 shadow-theme-sm"
|
|
3641
|
+
>
|
|
3642
|
+
Invalid regex pattern
|
|
3643
|
+
</div>
|
|
3644
|
+
)}
|
|
3645
|
+
</div>
|
|
3422
3646
|
</div>
|
|
3423
3647
|
|
|
3424
3648
|
{/* Problems dropdown (pods only) */}
|
|
@@ -3648,6 +3872,22 @@ export function ResourcesView({
|
|
|
3648
3872
|
</button>
|
|
3649
3873
|
</Tooltip>
|
|
3650
3874
|
)}
|
|
3875
|
+
{compareEnabled && (
|
|
3876
|
+
<Tooltip content={compareMode ? 'Exit compare mode' : 'Compare two resources side-by-side'}>
|
|
3877
|
+
<button
|
|
3878
|
+
onClick={() => (compareMode ? exitCompareMode() : setCompareMode(true))}
|
|
3879
|
+
aria-pressed={compareMode}
|
|
3880
|
+
className={clsx(
|
|
3881
|
+
'p-2 rounded-lg transition-colors',
|
|
3882
|
+
compareMode
|
|
3883
|
+
? 'bg-skyhook-500/15 text-skyhook-300 border border-skyhook-400/50'
|
|
3884
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated',
|
|
3885
|
+
)}
|
|
3886
|
+
>
|
|
3887
|
+
<GitCompare className="w-4 h-4" />
|
|
3888
|
+
</button>
|
|
3889
|
+
</Tooltip>
|
|
3890
|
+
)}
|
|
3651
3891
|
</div>
|
|
3652
3892
|
|
|
3653
3893
|
{/* Table */}
|
|
@@ -3750,8 +3990,20 @@ export function ResourcesView({
|
|
|
3750
3990
|
components={virtuosoComponents}
|
|
3751
3991
|
fixedHeaderContent={() => (
|
|
3752
3992
|
<tr>
|
|
3993
|
+
{compareMode && (
|
|
3994
|
+
<th
|
|
3995
|
+
// Inline px width — under `table-layout:fixed`,
|
|
3996
|
+
// `w-9` is a hint the browser absorbs into leftover
|
|
3997
|
+
// row width on an icon-only column.
|
|
3998
|
+
style={{ width: 36, minWidth: 36, maxWidth: 36 }}
|
|
3999
|
+
className="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"
|
|
4000
|
+
title="Compare mode"
|
|
4001
|
+
>
|
|
4002
|
+
<GitCompare className="w-3.5 h-3.5 inline-block opacity-70" />
|
|
4003
|
+
</th>
|
|
4004
|
+
)}
|
|
3753
4005
|
{columns.map((col, colIdx) => {
|
|
3754
|
-
const isSortable = ['name', 'namespace', 'age', 'status', 'ready', 'restarts', 'type', 'version', 'desired', 'available', 'upToDate', 'lastSeen', 'count', 'reason', 'object', 'cpu', 'memory'].includes(col.key)
|
|
4006
|
+
const isSortable = ['name', 'namespace', 'age', 'status', 'ready', 'restarts', 'type', 'version', 'desired', 'available', 'upToDate', 'lastSeen', 'count', 'reason', 'object', 'cpu', 'memory', 'containers'].includes(col.key)
|
|
3755
4007
|
const isSorted = sortColumn === col.key
|
|
3756
4008
|
const isLastCol = colIdx === columns.length - 1
|
|
3757
4009
|
const filterCol = filterableColumnMap.get(col.key)
|
|
@@ -3809,7 +4061,7 @@ export function ResourcesView({
|
|
|
3809
4061
|
? 'px-1.5 py-0.5 -my-0.5 selection-strong selection-text hover:bg-skyhook-500/30'
|
|
3810
4062
|
: isFilterOpen
|
|
3811
4063
|
? 'p-0.5 text-theme-text-primary'
|
|
3812
|
-
: 'p-0.5 text-theme-text-disabled opacity-
|
|
4064
|
+
: 'p-0.5 text-theme-text-disabled opacity-40 group-hover/th:opacity-100 hover:text-theme-text-primary'
|
|
3813
4065
|
)}
|
|
3814
4066
|
>
|
|
3815
4067
|
<ListFilter className="w-3 h-3" />
|
|
@@ -3921,6 +4173,13 @@ export function ResourcesView({
|
|
|
3921
4173
|
selectedResource?.namespace === resource.metadata?.namespace &&
|
|
3922
4174
|
selectedResource?.name === resource.metadata?.name
|
|
3923
4175
|
const isHighlighted = index === highlightedIndex
|
|
4176
|
+
const pickIdx = compareMode
|
|
4177
|
+
? pickIndex(comparePicks, {
|
|
4178
|
+
namespace: resource.metadata?.namespace || '',
|
|
4179
|
+
name: resource.metadata?.name || '',
|
|
4180
|
+
clusterId: resolveRowCluster?.(resource)?.id,
|
|
4181
|
+
})
|
|
4182
|
+
: -1
|
|
3924
4183
|
return (
|
|
3925
4184
|
<ResourceRowCells
|
|
3926
4185
|
resource={resource}
|
|
@@ -3932,8 +4191,10 @@ export function ResourcesView({
|
|
|
3932
4191
|
isSelected={isSelected}
|
|
3933
4192
|
isHighlighted={isHighlighted}
|
|
3934
4193
|
majorityNodeMinorVersion={majorityNodeMinorVersion}
|
|
3935
|
-
onClick={() => selectResource(resource, isSelected)}
|
|
4194
|
+
onClick={() => compareMode ? toggleComparePick(resource) : selectResource(resource, isSelected)}
|
|
3936
4195
|
onMouseEnter={() => setHighlightedIndex(-1)}
|
|
4196
|
+
compareMode={compareMode}
|
|
4197
|
+
comparePickIndex={pickIdx}
|
|
3937
4198
|
/>
|
|
3938
4199
|
)
|
|
3939
4200
|
}}
|
|
@@ -3949,6 +4210,15 @@ export function ResourcesView({
|
|
|
3949
4210
|
</MetricsContext.Provider>
|
|
3950
4211
|
)}
|
|
3951
4212
|
</div>
|
|
4213
|
+
{compareMode && compareEnabled && (
|
|
4214
|
+
<CompareTray
|
|
4215
|
+
kind={selectedKind.name}
|
|
4216
|
+
picks={comparePicks}
|
|
4217
|
+
onRemove={(idx) => setComparePicks(prev => prev.filter((_, i) => i !== idx))}
|
|
4218
|
+
onCompare={handleCompareNavigate}
|
|
4219
|
+
onExit={exitCompareMode}
|
|
4220
|
+
/>
|
|
4221
|
+
)}
|
|
3952
4222
|
</div>
|
|
3953
4223
|
</div>
|
|
3954
4224
|
</ResourcesViewDataContext.Provider>
|
|
@@ -3967,11 +4237,58 @@ interface ResourceRowCellsProps {
|
|
|
3967
4237
|
majorityNodeMinorVersion?: string
|
|
3968
4238
|
onClick?: () => void
|
|
3969
4239
|
onMouseEnter?: () => void
|
|
4240
|
+
compareMode?: boolean
|
|
4241
|
+
/** -1 when not picked; 0 = pick A; 1 = pick B. */
|
|
4242
|
+
comparePickIndex?: number
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
function rowHighlightClass(
|
|
4246
|
+
compareMode: boolean | undefined,
|
|
4247
|
+
comparePickIndex: number,
|
|
4248
|
+
isSelected: boolean | undefined,
|
|
4249
|
+
isHighlighted: boolean | undefined,
|
|
4250
|
+
): string {
|
|
4251
|
+
if (compareMode) {
|
|
4252
|
+
if (comparePickIndex === 0) return `${SIDE_TONES.a.rowBg} ${SIDE_TONES.a.rowBgHover}`
|
|
4253
|
+
if (comparePickIndex === 1) return `${SIDE_TONES.b.rowBg} ${SIDE_TONES.b.rowBgHover}`
|
|
4254
|
+
if (isHighlighted) return 'selection selection-ring'
|
|
4255
|
+
return 'group-hover/row:bg-theme-surface/50'
|
|
4256
|
+
}
|
|
4257
|
+
if (isSelected) return 'selection-strong group-hover/row:bg-skyhook-500/30'
|
|
4258
|
+
if (isHighlighted) return 'selection selection-ring'
|
|
4259
|
+
return 'group-hover/row:bg-theme-surface/50'
|
|
3970
4260
|
}
|
|
3971
4261
|
|
|
3972
|
-
function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, majorityNodeMinorVersion, onClick, onMouseEnter }: ResourceRowCellsProps) {
|
|
4262
|
+
function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, majorityNodeMinorVersion, onClick, onMouseEnter, compareMode, comparePickIndex = -1 }: ResourceRowCellsProps) {
|
|
4263
|
+
const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted)
|
|
4264
|
+
const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
|
|
3973
4265
|
return (
|
|
3974
4266
|
<>
|
|
4267
|
+
{compareMode && (
|
|
4268
|
+
<td
|
|
4269
|
+
onClick={onClick}
|
|
4270
|
+
onMouseEnter={onMouseEnter}
|
|
4271
|
+
style={{ width: 36, minWidth: 36, maxWidth: 36 }}
|
|
4272
|
+
className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight)}
|
|
4273
|
+
>
|
|
4274
|
+
{pickedSide ? (
|
|
4275
|
+
<span
|
|
4276
|
+
className={clsx(
|
|
4277
|
+
'inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold leading-none',
|
|
4278
|
+
SIDE_TONES[pickedSide].chipBg,
|
|
4279
|
+
)}
|
|
4280
|
+
aria-label={pickedSide === 'a' ? 'Pick A' : 'Pick B'}
|
|
4281
|
+
>
|
|
4282
|
+
{pickedSide === 'a' ? 'A' : 'B'}
|
|
4283
|
+
</span>
|
|
4284
|
+
) : (
|
|
4285
|
+
<span
|
|
4286
|
+
className="inline-block w-4 h-4 rounded border border-theme-border-light group-hover/row:border-skyhook-400/60 transition-colors"
|
|
4287
|
+
aria-hidden
|
|
4288
|
+
/>
|
|
4289
|
+
)}
|
|
4290
|
+
</td>
|
|
4291
|
+
)}
|
|
3975
4292
|
{columns.map((col) => (
|
|
3976
4293
|
<td
|
|
3977
4294
|
key={col.key}
|
|
@@ -3980,11 +4297,7 @@ function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, h
|
|
|
3980
4297
|
className={clsx(
|
|
3981
4298
|
'px-4 py-3 border-b-subtle cursor-pointer transition-colors',
|
|
3982
4299
|
col.key !== 'status' && 'overflow-hidden truncate',
|
|
3983
|
-
|
|
3984
|
-
? 'selection-strong group-hover/row:bg-skyhook-500/30'
|
|
3985
|
-
: isHighlighted
|
|
3986
|
-
? 'selection selection-ring'
|
|
3987
|
-
: 'group-hover/row:bg-theme-surface/50'
|
|
4300
|
+
rowHighlight,
|
|
3988
4301
|
)}
|
|
3989
4302
|
>
|
|
3990
4303
|
<CellContent resource={resource} kind={kind} group={group} column={col.key} majorityNodeMinorVersion={majorityNodeMinorVersion} extraColumn={extraColumnsByKey?.get(col.key)} />
|
|
@@ -4405,7 +4718,29 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
4405
4718
|
return <AzureMachineTemplateCell resource={resource} column={column} />
|
|
4406
4719
|
case 'azuremanagedclusters':
|
|
4407
4720
|
return <AzureManagedClusterCell resource={resource} column={column} />
|
|
4721
|
+
// Crossplane
|
|
4722
|
+
case 'providers':
|
|
4723
|
+
// Disambiguate from any future kind named "providers" by checking group
|
|
4724
|
+
if (resource.apiVersion?.startsWith('pkg.crossplane.io/')) {
|
|
4725
|
+
return <CrossplaneProviderCell resource={resource} column={column} />
|
|
4726
|
+
}
|
|
4727
|
+
return <GenericCell resource={resource} column={column} />
|
|
4728
|
+
case 'providerconfigs':
|
|
4729
|
+
return <CrossplaneProviderConfigCell resource={resource} column={column} />
|
|
4730
|
+
case 'compositions':
|
|
4731
|
+
return <CompositionCell resource={resource} column={column} />
|
|
4732
|
+
case 'compositeresourcedefinitions':
|
|
4733
|
+
return <XRDCell resource={resource} column={column} />
|
|
4408
4734
|
default:
|
|
4735
|
+
// Crossplane Managed Resources: unbounded plurals (one CRD per provider
|
|
4736
|
+
// service), detected via group prefix + spec.providerConfigRef heuristic.
|
|
4737
|
+
if (isLikelyCrossplaneMRGroup(kind, group ?? '') && isManagedResource(resource)) {
|
|
4738
|
+
return <ManagedResourceCell resource={resource} column={column} />
|
|
4739
|
+
}
|
|
4740
|
+
// Composite Resources (XRs) — user-defined kinds with resourceRefs.
|
|
4741
|
+
if (isComposite(resource)) {
|
|
4742
|
+
return <CompositeResourceCell resource={resource} column={column} />
|
|
4743
|
+
}
|
|
4409
4744
|
// Generic cell for CRDs and unknown resources
|
|
4410
4745
|
return <GenericCell resource={resource} column={column} />
|
|
4411
4746
|
}
|