@skyhook-io/k8s-ui 1.6.1 → 1.6.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/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 +338 -57
- 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/ui/YamlEditor.tsx +28 -15
- package/src/components/workload/ResourceDetailDrawer.tsx +1 -1
- package/src/index.ts +3 -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
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
Copy,
|
|
23
23
|
Check,
|
|
24
24
|
Plus,
|
|
25
|
+
GitCompare,
|
|
25
26
|
} from 'lucide-react'
|
|
26
27
|
import { clsx } from 'clsx'
|
|
27
28
|
import { ResourceBar } from '../ui/ResourceBar'
|
|
@@ -137,6 +138,8 @@ import { PolicyReportCell, ClusterPolicyReportCell, KyvernoPolicyCell, ClusterPo
|
|
|
137
138
|
import { ExternalSecretCell, ClusterExternalSecretCell, SecretStoreCell, ClusterSecretStoreCell } from './renderers/eso-cells'
|
|
138
139
|
import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell } from './renderers/velero-cells'
|
|
139
140
|
import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell } from './renderers/cnpg-cells'
|
|
141
|
+
import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
142
|
+
import { isManagedResource, isComposite } from './resource-utils-crossplane'
|
|
140
143
|
import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
|
|
141
144
|
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
145
|
import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
|
|
@@ -148,6 +151,7 @@ import { AzureManagedControlPlaneCell, AzureManagedMachinePoolCell, AzureMachine
|
|
|
148
151
|
import { useRegisterShortcut, useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
149
152
|
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
150
153
|
import type { SelectedKindInfo } from './ResourcesSidebar'
|
|
154
|
+
import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
|
|
151
155
|
|
|
152
156
|
// Pod problem filter options (special multi-select, not a single column value)
|
|
153
157
|
const POD_PROBLEMS = ['CrashLoopBackOff', 'ImagePullBackOff', 'OOMKilled', 'Unschedulable', 'Not Ready', 'High Restarts', 'Init Failed', 'Exit Code Error', 'Failed', 'Other'] as const
|
|
@@ -155,7 +159,7 @@ const WORKLOAD_PROBLEMS = ['Unavailable', 'Rollout Stuck', 'Rollout In Progress'
|
|
|
155
159
|
const WORKLOAD_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets'])
|
|
156
160
|
|
|
157
161
|
// Columns to skip for auto-detected filters (high cardinality, text-like, or non-filterable)
|
|
158
|
-
const SKIP_FILTER_COLUMNS = new Set([
|
|
162
|
+
export const SKIP_FILTER_COLUMNS = new Set([
|
|
159
163
|
'name', 'age', 'keys', 'size', 'images', 'domains', 'hosts', 'rules',
|
|
160
164
|
'ports', 'message', 'url', 'ref', 'revision', 'path', 'selector', 'ready', 'restarts',
|
|
161
165
|
'completions', 'duration', 'schedule', 'lastRun', 'target', 'replicas', 'metrics',
|
|
@@ -163,12 +167,22 @@ const SKIP_FILTER_COLUMNS = new Set([
|
|
|
163
167
|
'issuer', 'domain', 'presented', 'listeners', 'routes', 'addresses', 'hostnames',
|
|
164
168
|
'parents', 'backends', 'controller', 'description', 'externalIP', 'address',
|
|
165
169
|
'conditions', 'taints', 'desired', 'upToDate', 'available', 'owner',
|
|
166
|
-
'tls', 'endpoints', 'object', 'count', 'lastSeen', '
|
|
170
|
+
'tls', 'endpoints', 'object', 'count', 'lastSeen', 'source', 'inventory',
|
|
167
171
|
'lastUpdated', 'chart', 'events', 'repo',
|
|
168
172
|
'generators', 'applications', 'destinations', 'sources', 'budget', 'healthy', 'allowed',
|
|
169
173
|
'secrets', 'subjects', 'role', 'entrypoint', 'templates',
|
|
170
174
|
])
|
|
171
175
|
|
|
176
|
+
// Namespace/node cardinality scales with cluster size, not kind enum size;
|
|
177
|
+
// node keeps a generous cap as a sanity bound, namespace is uncapped.
|
|
178
|
+
export function isColumnFilterableByDistinctCount(colKey: string, distinctCount: number): boolean {
|
|
179
|
+
if (SKIP_FILTER_COLUMNS.has(colKey)) return false
|
|
180
|
+
const maxDistinct = colKey === 'namespace'
|
|
181
|
+
? Number.POSITIVE_INFINITY
|
|
182
|
+
: colKey === 'node' ? 200 : 30
|
|
183
|
+
return distinctCount >= 2 && distinctCount <= maxDistinct
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
// Column definitions per resource kind
|
|
173
187
|
interface Column {
|
|
174
188
|
key: string
|
|
@@ -233,11 +247,11 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
233
247
|
pods: [
|
|
234
248
|
{ key: 'name', label: 'Name' },
|
|
235
249
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
236
|
-
{ key: 'containers', label: 'Containers', width: 'w-
|
|
250
|
+
{ key: 'containers', label: 'Containers', width: 'w-32' },
|
|
237
251
|
{ key: 'status', label: 'Status', width: 'w-40' },
|
|
238
252
|
{ key: 'cpu', label: 'CPU', width: 'w-40', tooltip: 'CPU usage / limit (marker = request)' },
|
|
239
253
|
{ key: 'memory', label: 'Memory', width: 'w-40', tooltip: 'Memory usage / limit (marker = request)' },
|
|
240
|
-
{ key: 'restarts', label: 'Restarts', width: 'w-
|
|
254
|
+
{ key: 'restarts', label: 'Restarts', width: 'w-28' },
|
|
241
255
|
{ key: 'podIP', label: 'Pod IP', width: 'w-32', defaultVisible: false },
|
|
242
256
|
{ key: 'node', label: 'Node', width: 'w-44', hideOnMobile: true },
|
|
243
257
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -246,8 +260,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
246
260
|
{ key: 'name', label: 'Name' },
|
|
247
261
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
248
262
|
{ 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-
|
|
263
|
+
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
|
|
264
|
+
{ key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds)' },
|
|
251
265
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
252
266
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
253
267
|
],
|
|
@@ -256,8 +270,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
256
270
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
257
271
|
{ key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Number of nodes that should run the daemon pod (based on node selector)' },
|
|
258
272
|
{ 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-
|
|
273
|
+
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template spec' },
|
|
274
|
+
{ key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds duration)' },
|
|
261
275
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
262
276
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
263
277
|
],
|
|
@@ -265,7 +279,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
265
279
|
{ key: 'name', label: 'Name' },
|
|
266
280
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
267
281
|
{ key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
|
|
268
|
-
{ key: 'upToDate', label: 'Up-to-date', width: 'w-
|
|
282
|
+
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
|
|
269
283
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
270
284
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
271
285
|
],
|
|
@@ -328,7 +342,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
328
342
|
{ key: 'name', label: 'Name' },
|
|
329
343
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
330
344
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
331
|
-
{ key: 'completions', label: 'Completions', width: 'w-
|
|
345
|
+
{ key: 'completions', label: 'Completions', width: 'w-32' },
|
|
332
346
|
{ key: 'duration', label: 'Duration', width: 'w-24', hideOnMobile: true },
|
|
333
347
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
334
348
|
],
|
|
@@ -363,7 +377,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
363
377
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
364
378
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
365
379
|
{ key: 'capacity', label: 'Capacity', width: 'w-24' },
|
|
366
|
-
{ key: 'storageClass', label: 'Storage Class', width: 'w-
|
|
380
|
+
{ key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
|
|
367
381
|
{ key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'Access modes: RWO=ReadWriteOnce, RWX=ReadWriteMany, ROX=ReadOnlyMany' },
|
|
368
382
|
{ key: 'volume', label: 'Volume', width: 'w-48', hideOnMobile: true },
|
|
369
383
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -402,7 +416,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
402
416
|
{ key: 'capacity', label: 'Capacity', width: 'w-24' },
|
|
403
417
|
{ key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'RWO=ReadWriteOnce, ROX=ReadOnlyMany, RWX=ReadWriteMany' },
|
|
404
418
|
{ key: 'reclaimPolicy', label: 'Reclaim', width: 'w-20' },
|
|
405
|
-
{ key: 'storageClass', label: 'Storage Class', width: 'w-
|
|
419
|
+
{ key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
|
|
406
420
|
{ key: 'claim', label: 'Claim', width: 'w-48', hideOnMobile: true },
|
|
407
421
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
408
422
|
],
|
|
@@ -667,8 +681,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
667
681
|
serviceaccounts: [
|
|
668
682
|
{ key: 'name', label: 'Name' },
|
|
669
683
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
670
|
-
{ key: 'automount', label: 'Automount', width: 'w-
|
|
671
|
-
{ key: 'secrets', label: 'Secrets', width: 'w-
|
|
684
|
+
{ key: 'automount', label: 'Automount', width: 'w-32', tooltip: 'Whether token is automatically mounted in pods' },
|
|
685
|
+
{ key: 'secrets', label: 'Secrets', width: 'w-24' },
|
|
672
686
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
673
687
|
],
|
|
674
688
|
roles: [
|
|
@@ -705,13 +719,13 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
705
719
|
{ key: 'name', label: 'Name' },
|
|
706
720
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
707
721
|
{ key: 'holder', label: 'Holder', width: 'w-48' },
|
|
708
|
-
{ key: 'renewTime', label: 'Last Renewed', width: 'w-
|
|
722
|
+
{ key: 'renewTime', label: 'Last Renewed', width: 'w-32' },
|
|
709
723
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
710
724
|
],
|
|
711
725
|
priorityclasses: [
|
|
712
726
|
{ key: 'name', label: 'Name' },
|
|
713
727
|
{ key: 'value', label: 'Value', width: 'w-24' },
|
|
714
|
-
{ key: 'globalDefault', label: 'Global Default', width: 'w-
|
|
728
|
+
{ key: 'globalDefault', label: 'Global Default', width: 'w-36' },
|
|
715
729
|
{ key: 'preemptionPolicy', label: 'Preemption', width: 'w-32' },
|
|
716
730
|
{ key: 'description', label: 'Description', width: 'w-64', hideOnMobile: true },
|
|
717
731
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -723,27 +737,27 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
723
737
|
],
|
|
724
738
|
mutatingwebhookconfigurations: [
|
|
725
739
|
{ key: 'name', label: 'Name' },
|
|
726
|
-
{ key: 'webhooks', label: 'Webhooks', width: 'w-
|
|
727
|
-
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-
|
|
740
|
+
{ key: 'webhooks', label: 'Webhooks', width: 'w-28' },
|
|
741
|
+
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
|
|
728
742
|
{ key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
|
|
729
743
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
730
744
|
],
|
|
731
745
|
validatingwebhookconfigurations: [
|
|
732
746
|
{ key: 'name', label: 'Name' },
|
|
733
|
-
{ key: 'webhooks', label: 'Webhooks', width: 'w-
|
|
734
|
-
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-
|
|
747
|
+
{ key: 'webhooks', label: 'Webhooks', width: 'w-28' },
|
|
748
|
+
{ key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
|
|
735
749
|
{ key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
|
|
736
750
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
737
751
|
],
|
|
738
752
|
events: [
|
|
739
753
|
{ key: 'name', label: 'Name' },
|
|
740
754
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
741
|
-
{ key: 'type', label: 'Type', width: 'w-
|
|
742
|
-
{ key: 'reason', label: 'Reason', width: 'w-
|
|
755
|
+
{ key: 'type', label: 'Type', width: 'w-24' },
|
|
756
|
+
{ key: 'reason', label: 'Reason', width: 'w-32' },
|
|
743
757
|
{ key: 'message', label: 'Message', width: 'w-64' },
|
|
744
758
|
{ key: 'object', label: 'Object', width: 'w-48', hideOnMobile: true },
|
|
745
|
-
{ key: 'count', label: 'Count', width: 'w-
|
|
746
|
-
{ key: 'lastSeen', label: 'Last Seen', width: 'w-
|
|
759
|
+
{ key: 'count', label: 'Count', width: 'w-20' },
|
|
760
|
+
{ key: 'lastSeen', label: 'Last Seen', width: 'w-28' },
|
|
747
761
|
],
|
|
748
762
|
// ============================================================================
|
|
749
763
|
// FLUXCD GITOPS RESOURCES
|
|
@@ -791,6 +805,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
791
805
|
{ key: 'chart', label: 'Chart', width: 'w-40' },
|
|
792
806
|
{ key: 'version', label: 'Version', width: 'w-24' },
|
|
793
807
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
808
|
+
{ key: 'message', label: 'Message', width: 'w-64', hideOnMobile: true, tooltip: 'Last diagnostic message — distinguishes dependency-wait, install/upgrade failure, and test failure' },
|
|
794
809
|
{ key: 'revision', label: 'Rev', width: 'w-16', hideOnMobile: true, tooltip: 'Helm release revision number' },
|
|
795
810
|
{ key: 'lastUpdated', label: 'Last Updated', width: 'w-28', tooltip: 'Time since last successful reconciliation' },
|
|
796
811
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
@@ -932,7 +947,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
932
947
|
{ key: 'store', label: 'Store', width: 'w-36' },
|
|
933
948
|
{ key: 'provider', label: 'Provider', width: 'w-28' },
|
|
934
949
|
{ key: 'refreshInterval', label: 'Refresh', width: 'w-24' },
|
|
935
|
-
{ key: 'lastSync', label: 'Last Sync', width: 'w-
|
|
950
|
+
{ key: 'lastSync', label: 'Last Sync', width: 'w-28' },
|
|
936
951
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
937
952
|
],
|
|
938
953
|
clusterexternalsecrets: [
|
|
@@ -966,7 +981,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
966
981
|
{ key: 'namespaces', label: 'Scope', width: 'w-24', tooltip: 'Included namespaces (* = all)' },
|
|
967
982
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
968
983
|
{ key: 'expiry', label: 'Expires', width: 'w-24' },
|
|
969
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
984
|
+
{ key: 'errors', label: 'Errors', width: 'w-20' },
|
|
970
985
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
971
986
|
],
|
|
972
987
|
restores: [
|
|
@@ -975,7 +990,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
975
990
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
976
991
|
{ key: 'backupName', label: 'Backup', width: 'w-40' },
|
|
977
992
|
{ key: 'duration', label: 'Duration', width: 'w-24' },
|
|
978
|
-
{ key: 'errors', label: 'Errors', width: 'w-
|
|
993
|
+
{ key: 'errors', label: 'Errors', width: 'w-20' },
|
|
979
994
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
980
995
|
],
|
|
981
996
|
schedules: [
|
|
@@ -1004,10 +1019,10 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1004
1019
|
{ key: 'name', label: 'Name' },
|
|
1005
1020
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
1006
1021
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
1007
|
-
{ key: 'instances', label: 'Instances', width: 'w-
|
|
1022
|
+
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Ready/Total' },
|
|
1008
1023
|
{ key: 'primary', label: 'Primary', width: 'w-36' },
|
|
1009
1024
|
{ key: 'image', label: 'Image', width: 'w-28' },
|
|
1010
|
-
{ key: 'storage', label: 'Storage', width: 'w-
|
|
1025
|
+
{ key: 'storage', label: 'Storage', width: 'w-28' },
|
|
1011
1026
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1012
1027
|
],
|
|
1013
1028
|
scheduledbackups: [
|
|
@@ -1016,8 +1031,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1016
1031
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
1017
1032
|
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1018
1033
|
{ key: 'schedule', label: 'Schedule', width: 'w-36' },
|
|
1019
|
-
{ key: 'lastSchedule', label: 'Last Run', width: 'w-
|
|
1020
|
-
{ key: 'suspended', label: 'Suspended', width: 'w-
|
|
1034
|
+
{ key: 'lastSchedule', label: 'Last Run', width: 'w-28' },
|
|
1035
|
+
{ key: 'suspended', label: 'Suspended', width: 'w-28' },
|
|
1021
1036
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1022
1037
|
],
|
|
1023
1038
|
poolers: [
|
|
@@ -1026,8 +1041,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1026
1041
|
{ key: 'status', label: 'Status', width: 'w-24' },
|
|
1027
1042
|
{ key: 'cluster', label: 'Cluster', width: 'w-36' },
|
|
1028
1043
|
{ key: 'type', label: 'Type', width: 'w-16' },
|
|
1029
|
-
{ key: 'poolMode', label: 'Pool Mode', width: 'w-
|
|
1030
|
-
{ key: 'instances', label: 'Instances', width: 'w-
|
|
1044
|
+
{ key: 'poolMode', label: 'Pool Mode', width: 'w-32' },
|
|
1045
|
+
{ key: 'instances', label: 'Instances', width: 'w-28', tooltip: 'Ready/Total' },
|
|
1031
1046
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1032
1047
|
],
|
|
1033
1048
|
// ============================================================================
|
|
@@ -1101,7 +1116,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1101
1116
|
{ key: 'status', label: 'Status', width: 'w-28' },
|
|
1102
1117
|
{ key: 'routing', label: 'Traffic', width: 'w-20', tooltip: 'Whether this revision is receiving traffic' },
|
|
1103
1118
|
{ key: 'image', label: 'Image', width: 'w-48' },
|
|
1104
|
-
{ key: 'concurrency', label: 'Concurrency', width: 'w-
|
|
1119
|
+
{ key: 'concurrency', label: 'Concurrency', width: 'w-32' },
|
|
1105
1120
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
1106
1121
|
],
|
|
1107
1122
|
knativeroutes: [
|
|
@@ -1363,7 +1378,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1363
1378
|
{ key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
|
|
1364
1379
|
{ key: 'cluster', label: 'Cluster', width: 'w-32 shrink-0' },
|
|
1365
1380
|
{ key: 'ready', label: 'Ready', width: 'w-20 shrink-0' },
|
|
1366
|
-
{ key: 'initialized', label: 'Initialized', width: 'w-
|
|
1381
|
+
{ key: 'initialized', label: 'Initialized', width: 'w-28 shrink-0' },
|
|
1367
1382
|
{ key: 'version', label: 'Version', width: 'w-24 shrink-0' },
|
|
1368
1383
|
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1369
1384
|
],
|
|
@@ -1513,6 +1528,43 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
1513
1528
|
{ key: 'status', label: 'Status', width: 'w-28 shrink-0' },
|
|
1514
1529
|
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1515
1530
|
],
|
|
1531
|
+
// Crossplane — Managed Resources are unbounded (one kind per provider CRD);
|
|
1532
|
+
// routed via isLikelyCrossplaneMRGroup, not by exact kind plural.
|
|
1533
|
+
crossplanemanagedresources: [
|
|
1534
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1535
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-32 shrink-0' },
|
|
1536
|
+
{ key: 'kind', label: 'Kind', width: 'w-32 shrink-0' },
|
|
1537
|
+
{ key: 'external', label: 'External Name', width: 'min-w-48', hideOnMobile: true },
|
|
1538
|
+
{ key: 'provider', label: 'Provider Config', width: 'w-40 shrink-0', hideOnMobile: true },
|
|
1539
|
+
{ key: 'status', label: 'Status', width: 'w-32 shrink-0' },
|
|
1540
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1541
|
+
],
|
|
1542
|
+
providers: [
|
|
1543
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1544
|
+
{ key: 'package', label: 'Package', width: 'min-w-48' },
|
|
1545
|
+
{ key: 'revision', label: 'Revision', width: 'w-36 shrink-0', hideOnMobile: true },
|
|
1546
|
+
{ key: 'status', label: 'Status', width: 'w-32 shrink-0' },
|
|
1547
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1548
|
+
],
|
|
1549
|
+
providerconfigs: [
|
|
1550
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1551
|
+
{ key: 'credentials', label: 'Credentials', width: 'w-36 shrink-0' },
|
|
1552
|
+
{ key: 'status', label: 'Status', width: 'w-32 shrink-0' },
|
|
1553
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1554
|
+
],
|
|
1555
|
+
compositions: [
|
|
1556
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1557
|
+
{ key: 'composite', label: 'Composite Kind', width: 'w-44 shrink-0' },
|
|
1558
|
+
{ key: 'mode', label: 'Mode', width: 'w-24 shrink-0' },
|
|
1559
|
+
{ key: 'functions', label: 'Functions', width: 'w-28 shrink-0', hideOnMobile: true },
|
|
1560
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1561
|
+
],
|
|
1562
|
+
compositeresourcedefinitions: [
|
|
1563
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
1564
|
+
{ key: 'kind', label: 'Kind', width: 'w-40 shrink-0' },
|
|
1565
|
+
{ key: 'claim', label: 'Claim Kind', width: 'w-40 shrink-0', hideOnMobile: true },
|
|
1566
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
1567
|
+
],
|
|
1516
1568
|
}
|
|
1517
1569
|
|
|
1518
1570
|
// Map (plural, group) → KNOWN_COLUMNS key for kinds that collide with core K8s
|
|
@@ -1545,8 +1597,32 @@ function normalizeKindToPlural(kind: string, group?: string): string {
|
|
|
1545
1597
|
return lower
|
|
1546
1598
|
}
|
|
1547
1599
|
|
|
1600
|
+
// Crossplane Managed Resources are unbounded (each provider ships its own
|
|
1601
|
+
// CRDs under per-service groups like s3.aws.upbound.io, compute.gcp.upbound.io).
|
|
1602
|
+
// Route any non-core-Crossplane resource in these groups to the generic MR
|
|
1603
|
+
// column set. ProviderConfig is excluded — those have their own renderer.
|
|
1604
|
+
function isLikelyCrossplaneMRGroup(kind: string, group: string): boolean {
|
|
1605
|
+
if (!group) return false
|
|
1606
|
+
const k = kind.toLowerCase()
|
|
1607
|
+
if (k === 'providerconfig' || k === 'providerconfigs') return false
|
|
1608
|
+
if (group.endsWith('.upbound.io')) return true
|
|
1609
|
+
if (group === 'kubernetes.crossplane.io' || group === 'helm.crossplane.io') return true
|
|
1610
|
+
// Reserved Crossplane core groups — never MRs.
|
|
1611
|
+
if (group === 'crossplane.io' || group === 'pkg.crossplane.io' || group === 'apiextensions.crossplane.io') {
|
|
1612
|
+
return false
|
|
1613
|
+
}
|
|
1614
|
+
// Any other *.crossplane.io subgroup is presumed to be a provider's MR group.
|
|
1615
|
+
if (group.endsWith('.crossplane.io')) return true
|
|
1616
|
+
return false
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1548
1619
|
function getColumnsForKind(kind: string, group?: string): Column[] {
|
|
1549
|
-
|
|
1620
|
+
const key = normalizeKindToPlural(kind, group)
|
|
1621
|
+
if (KNOWN_COLUMNS[key]) return KNOWN_COLUMNS[key]
|
|
1622
|
+
if (group && isLikelyCrossplaneMRGroup(kind, group)) {
|
|
1623
|
+
return KNOWN_COLUMNS.crossplanemanagedresources
|
|
1624
|
+
}
|
|
1625
|
+
return DEFAULT_COLUMNS
|
|
1550
1626
|
}
|
|
1551
1627
|
|
|
1552
1628
|
// Get the default visible columns for a kind
|
|
@@ -1679,6 +1755,26 @@ interface ResourcesViewProps {
|
|
|
1679
1755
|
* should also wire onResourceClick to handle the deep-link-on-load
|
|
1680
1756
|
* case. */
|
|
1681
1757
|
onRowSelect?: (resource: any) => void
|
|
1758
|
+
/**
|
|
1759
|
+
* Overrides the default compare-mode submit (which navigates to
|
|
1760
|
+
* `/compare?kind=...&a=...&b=...`). Hosts use this to route to a
|
|
1761
|
+
* different URL — e.g. Radar Hub's `/fleet/compare` with cluster IDs.
|
|
1762
|
+
* Picks arrive in click order, each carrying `clusterId`/`clusterName`
|
|
1763
|
+
* when `resolveRowCluster` is set (which is what keeps cross-cluster
|
|
1764
|
+
* picks distinct in the equality check).
|
|
1765
|
+
*/
|
|
1766
|
+
onCompareSubmit?: (
|
|
1767
|
+
picks: NamespacedRef[],
|
|
1768
|
+
kind: { name: string; group?: string },
|
|
1769
|
+
) => void
|
|
1770
|
+
/**
|
|
1771
|
+
* Resolves the cluster scope for a row when compare-mode is enabled.
|
|
1772
|
+
* Stamps `clusterId`/`clusterName` onto each pick so the same `ns/name`
|
|
1773
|
+
* in two different clusters is treated as two distinct picks. Hub-web
|
|
1774
|
+
* returns `row._cluster` here; OSS leaves it unset and picks key on
|
|
1775
|
+
* namespace+name only.
|
|
1776
|
+
*/
|
|
1777
|
+
resolveRowCluster?: (resource: any) => { id: string; name: string } | undefined
|
|
1682
1778
|
}
|
|
1683
1779
|
|
|
1684
1780
|
// Default selected kind
|
|
@@ -1823,6 +1919,8 @@ export function ResourcesView({
|
|
|
1823
1919
|
defaultKind = DEFAULT_KIND_INFO,
|
|
1824
1920
|
extraLeadingColumns,
|
|
1825
1921
|
onRowSelect,
|
|
1922
|
+
onCompareSubmit,
|
|
1923
|
+
resolveRowCluster,
|
|
1826
1924
|
}: ResourcesViewProps) {
|
|
1827
1925
|
const initialFilters = getInitialFiltersFromURL()
|
|
1828
1926
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
|
|
@@ -1966,6 +2064,13 @@ export function ResourcesView({
|
|
|
1966
2064
|
|
|
1967
2065
|
useEffect(() => {
|
|
1968
2066
|
const saved = loadColumnSettings(selectedKind.name, selectedKind.group)
|
|
2067
|
+
// Host-injected extra columns (e.g. fleet Cluster) default to visible
|
|
2068
|
+
// even when the saved column-visibility blob predates them — a naive
|
|
2069
|
+
// Set(saved.visible) would silently hide host columns the user has
|
|
2070
|
+
// never been shown. Trade-off: a user can't permanently hide a host
|
|
2071
|
+
// extra column via the column picker — next mount re-adds it. Track
|
|
2072
|
+
// a sibling "hidden-extras" set if this becomes a real complaint.
|
|
2073
|
+
const extraKeys = extraLeadingColumns?.map(c => c.key) ?? []
|
|
1969
2074
|
if (saved) {
|
|
1970
2075
|
// If saved columns are just the defaults but this kind has specialized columns,
|
|
1971
2076
|
// discard the stale save and use the specialized columns instead
|
|
@@ -1978,14 +2083,16 @@ export function ResourcesView({
|
|
|
1978
2083
|
setVisibleColumns(getDefaultVisibleColumns(allColumns))
|
|
1979
2084
|
setColumnWidths({})
|
|
1980
2085
|
} else {
|
|
1981
|
-
|
|
2086
|
+
const merged = new Set(saved.visible)
|
|
2087
|
+
for (const k of extraKeys) merged.add(k)
|
|
2088
|
+
setVisibleColumns(merged)
|
|
1982
2089
|
setColumnWidths(saved.widths || {})
|
|
1983
2090
|
}
|
|
1984
2091
|
} else {
|
|
1985
2092
|
setVisibleColumns(getDefaultVisibleColumns(allColumns))
|
|
1986
2093
|
setColumnWidths({})
|
|
1987
2094
|
}
|
|
1988
|
-
}, [selectedKind.name, selectedKind.group, allColumns])
|
|
2095
|
+
}, [selectedKind.name, selectedKind.group, allColumns, extraLeadingColumns])
|
|
1989
2096
|
|
|
1990
2097
|
// Save column settings when they change (skip initial load)
|
|
1991
2098
|
const isColumnSettingsLoaded = useRef(false)
|
|
@@ -2121,6 +2228,56 @@ export function ResourcesView({
|
|
|
2121
2228
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
2122
2229
|
const virtuosoRef = useRef<TableVirtuosoHandle>(null)
|
|
2123
2230
|
|
|
2231
|
+
// Compare mode is only meaningful when the host can route picks. Either
|
|
2232
|
+
// path qualifies: onNavigate for the default same-cluster `/compare?...`
|
|
2233
|
+
// URL, or onCompareSubmit for hosts that build their own URL (e.g.
|
|
2234
|
+
// Radar Hub's `/fleet/compare` with cluster IDs).
|
|
2235
|
+
const compareEnabled = !!onNavigate || !!onCompareSubmit
|
|
2236
|
+
const [compareMode, setCompareMode] = useState(false)
|
|
2237
|
+
const [comparePicks, setComparePicks] = useState<CompareTrayPick[]>([])
|
|
2238
|
+
|
|
2239
|
+
// Kind change invalidates picks AND mode — leaving the tray on with empty
|
|
2240
|
+
// pills after the user moves to a different kind is more confusing than helpful.
|
|
2241
|
+
useEffect(() => {
|
|
2242
|
+
setComparePicks([])
|
|
2243
|
+
setCompareMode(false)
|
|
2244
|
+
}, [selectedKind.name, selectedKind.group])
|
|
2245
|
+
|
|
2246
|
+
const exitCompareMode = useCallback(() => {
|
|
2247
|
+
setCompareMode(false)
|
|
2248
|
+
setComparePicks([])
|
|
2249
|
+
}, [])
|
|
2250
|
+
|
|
2251
|
+
const toggleComparePick = useCallback((resource: any) => {
|
|
2252
|
+
if (!resource?.metadata?.name) return
|
|
2253
|
+
const ns = resource.metadata.namespace || ''
|
|
2254
|
+
// Stamp clusterId on the pick when the host provides a resolver
|
|
2255
|
+
// (cross-cluster compare). Without it, two rows with the same ns+name
|
|
2256
|
+
// from different clusters would collapse into one pick.
|
|
2257
|
+
const cluster = resolveRowCluster?.(resource)
|
|
2258
|
+
setComparePicks(prev => togglePick(prev, {
|
|
2259
|
+
namespace: ns,
|
|
2260
|
+
name: resource.metadata.name,
|
|
2261
|
+
clusterId: cluster?.id,
|
|
2262
|
+
clusterName: cluster?.name,
|
|
2263
|
+
}))
|
|
2264
|
+
}, [resolveRowCluster])
|
|
2265
|
+
|
|
2266
|
+
const handleCompareNavigate = useCallback(() => {
|
|
2267
|
+
if (comparePicks.length !== 2) return
|
|
2268
|
+
if (onCompareSubmit) {
|
|
2269
|
+
onCompareSubmit(comparePicks, { name: selectedKind.name, group: selectedKind.group })
|
|
2270
|
+
return
|
|
2271
|
+
}
|
|
2272
|
+
if (!onNavigate) return
|
|
2273
|
+
const params = new URLSearchParams()
|
|
2274
|
+
params.set('kind', selectedKind.name)
|
|
2275
|
+
if (selectedKind.group) params.set('apiGroup', selectedKind.group)
|
|
2276
|
+
params.set('a', refToParam(comparePicks[0]))
|
|
2277
|
+
params.set('b', refToParam(comparePicks[1]))
|
|
2278
|
+
onNavigate(`/compare?${params.toString()}`)
|
|
2279
|
+
}, [comparePicks, selectedKind.name, selectedKind.group, onNavigate, onCompareSubmit])
|
|
2280
|
+
|
|
2124
2281
|
// Reset highlight when kind, search, sort, or namespace changes
|
|
2125
2282
|
const namespacesKey = namespaces.join(',')
|
|
2126
2283
|
useEffect(() => { setHighlightedIndex(-1) }, [selectedKind.name, searchTerm, sortColumn, sortDirection, namespacesKey])
|
|
@@ -2226,7 +2383,11 @@ export function ResourcesView({
|
|
|
2226
2383
|
description: 'Open resource detail',
|
|
2227
2384
|
category: 'Resource Actions',
|
|
2228
2385
|
scope: 'resources',
|
|
2229
|
-
handler: () =>
|
|
2386
|
+
handler: () => {
|
|
2387
|
+
const res = getHighlightedResource()
|
|
2388
|
+
if (compareMode) toggleComparePick(res)
|
|
2389
|
+
else selectResource(res)
|
|
2390
|
+
},
|
|
2230
2391
|
enabled: highlightedIndex >= 0,
|
|
2231
2392
|
},
|
|
2232
2393
|
{
|
|
@@ -2235,7 +2396,11 @@ export function ResourcesView({
|
|
|
2235
2396
|
description: 'Open resource detail',
|
|
2236
2397
|
category: 'Resource Actions',
|
|
2237
2398
|
scope: 'resources',
|
|
2238
|
-
handler: () =>
|
|
2399
|
+
handler: () => {
|
|
2400
|
+
const res = getHighlightedResource()
|
|
2401
|
+
if (compareMode) toggleComparePick(res)
|
|
2402
|
+
else selectResource(res)
|
|
2403
|
+
},
|
|
2239
2404
|
enabled: highlightedIndex >= 0,
|
|
2240
2405
|
},
|
|
2241
2406
|
{
|
|
@@ -2250,7 +2415,7 @@ export function ResourcesView({
|
|
|
2250
2415
|
const cb = onResourceClickYaml || onResourceClick
|
|
2251
2416
|
cb?.({ kind: selectedKind.name, namespace: res.metadata.namespace || '', name: res.metadata.name, group: selectedKind.group })
|
|
2252
2417
|
},
|
|
2253
|
-
enabled: highlightedIndex >= 0,
|
|
2418
|
+
enabled: highlightedIndex >= 0 && !compareMode,
|
|
2254
2419
|
},
|
|
2255
2420
|
{
|
|
2256
2421
|
id: 'resources-logs',
|
|
@@ -2274,7 +2439,7 @@ export function ResourcesView({
|
|
|
2274
2439
|
openWorkloadLogs?.({ namespace: ns, workloadKind: selectedKind.kind, workloadName: name })
|
|
2275
2440
|
}
|
|
2276
2441
|
},
|
|
2277
|
-
enabled: highlightedIndex >= 0 && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs'].includes(selectedKind.name.toLowerCase()),
|
|
2442
|
+
enabled: highlightedIndex >= 0 && !compareMode && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs'].includes(selectedKind.name.toLowerCase()),
|
|
2278
2443
|
},
|
|
2279
2444
|
{
|
|
2280
2445
|
id: 'resources-sort-name',
|
|
@@ -2307,11 +2472,11 @@ export function ResourcesView({
|
|
|
2307
2472
|
category: 'Table',
|
|
2308
2473
|
scope: 'resources',
|
|
2309
2474
|
handler: () => {
|
|
2310
|
-
// Close dropdowns first, then clear highlight, then blur search
|
|
2311
2475
|
if (showColumnPicker) { setShowColumnPicker(false); return }
|
|
2312
2476
|
if (openColumnFilter) { setOpenColumnFilter(null); return }
|
|
2313
2477
|
if (showProblemsDropdown) { setShowProblemsDropdown(false); return }
|
|
2314
2478
|
if (showLabelsDropdown) { setShowLabelsDropdown(false); return }
|
|
2479
|
+
if (compareMode) { exitCompareMode(); return }
|
|
2315
2480
|
if (highlightedIndex >= 0) setHighlightedIndex(-1)
|
|
2316
2481
|
else searchInputRef.current?.blur()
|
|
2317
2482
|
},
|
|
@@ -3156,6 +3321,14 @@ export function ResourcesView({
|
|
|
3156
3321
|
style={{ ...props.style, tableLayout: 'fixed' }}
|
|
3157
3322
|
>
|
|
3158
3323
|
<colgroup>
|
|
3324
|
+
{/*
|
|
3325
|
+
Compare-mode adds a leading <th>/<td> per row but isn't part
|
|
3326
|
+
of `columns`. Under table-layout:fixed the <colgroup> must
|
|
3327
|
+
match the actual column count, otherwise the browser pads
|
|
3328
|
+
the missing entry by stealing width from a sized neighbour
|
|
3329
|
+
— typically blowing this narrow column out to ~200px.
|
|
3330
|
+
*/}
|
|
3331
|
+
{compareMode && <col style={{ width: 36 }} />}
|
|
3159
3332
|
{columns.map(col => (
|
|
3160
3333
|
<col
|
|
3161
3334
|
key={col.key}
|
|
@@ -3173,7 +3346,7 @@ export function ResourcesView({
|
|
|
3173
3346
|
)
|
|
3174
3347
|
}),
|
|
3175
3348
|
TableRow: VirtuosoTableRow,
|
|
3176
|
-
}), [columns, columnWidths, hasResizedColumns])
|
|
3349
|
+
}), [columns, columnWidths, hasResizedColumns, compareMode])
|
|
3177
3350
|
|
|
3178
3351
|
// Calculate filter options with counts based on current resources (before filtering)
|
|
3179
3352
|
const filterOptions = useMemo(() => {
|
|
@@ -3213,10 +3386,7 @@ export function ResourcesView({
|
|
|
3213
3386
|
}
|
|
3214
3387
|
|
|
3215
3388
|
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) {
|
|
3389
|
+
if (isColumnFilterableByDistinctCount(col.key, distinctCount)) {
|
|
3220
3390
|
filterableColumns.push({
|
|
3221
3391
|
key: col.key,
|
|
3222
3392
|
label: col.label,
|
|
@@ -3648,6 +3818,22 @@ export function ResourcesView({
|
|
|
3648
3818
|
</button>
|
|
3649
3819
|
</Tooltip>
|
|
3650
3820
|
)}
|
|
3821
|
+
{compareEnabled && (
|
|
3822
|
+
<Tooltip content={compareMode ? 'Exit compare mode' : 'Compare two resources side-by-side'}>
|
|
3823
|
+
<button
|
|
3824
|
+
onClick={() => (compareMode ? exitCompareMode() : setCompareMode(true))}
|
|
3825
|
+
aria-pressed={compareMode}
|
|
3826
|
+
className={clsx(
|
|
3827
|
+
'p-2 rounded-lg transition-colors',
|
|
3828
|
+
compareMode
|
|
3829
|
+
? 'bg-skyhook-500/15 text-skyhook-300 border border-skyhook-400/50'
|
|
3830
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated',
|
|
3831
|
+
)}
|
|
3832
|
+
>
|
|
3833
|
+
<GitCompare className="w-4 h-4" />
|
|
3834
|
+
</button>
|
|
3835
|
+
</Tooltip>
|
|
3836
|
+
)}
|
|
3651
3837
|
</div>
|
|
3652
3838
|
|
|
3653
3839
|
{/* Table */}
|
|
@@ -3750,8 +3936,20 @@ export function ResourcesView({
|
|
|
3750
3936
|
components={virtuosoComponents}
|
|
3751
3937
|
fixedHeaderContent={() => (
|
|
3752
3938
|
<tr>
|
|
3939
|
+
{compareMode && (
|
|
3940
|
+
<th
|
|
3941
|
+
// Inline px width — under `table-layout:fixed`,
|
|
3942
|
+
// `w-9` is a hint the browser absorbs into leftover
|
|
3943
|
+
// row width on an icon-only column.
|
|
3944
|
+
style={{ width: 36, minWidth: 36, maxWidth: 36 }}
|
|
3945
|
+
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"
|
|
3946
|
+
title="Compare mode"
|
|
3947
|
+
>
|
|
3948
|
+
<GitCompare className="w-3.5 h-3.5 inline-block opacity-70" />
|
|
3949
|
+
</th>
|
|
3950
|
+
)}
|
|
3753
3951
|
{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)
|
|
3952
|
+
const isSortable = ['name', 'namespace', 'age', 'status', 'ready', 'restarts', 'type', 'version', 'desired', 'available', 'upToDate', 'lastSeen', 'count', 'reason', 'object', 'cpu', 'memory', 'containers'].includes(col.key)
|
|
3755
3953
|
const isSorted = sortColumn === col.key
|
|
3756
3954
|
const isLastCol = colIdx === columns.length - 1
|
|
3757
3955
|
const filterCol = filterableColumnMap.get(col.key)
|
|
@@ -3809,7 +4007,7 @@ export function ResourcesView({
|
|
|
3809
4007
|
? 'px-1.5 py-0.5 -my-0.5 selection-strong selection-text hover:bg-skyhook-500/30'
|
|
3810
4008
|
: isFilterOpen
|
|
3811
4009
|
? 'p-0.5 text-theme-text-primary'
|
|
3812
|
-
: 'p-0.5 text-theme-text-disabled opacity-
|
|
4010
|
+
: 'p-0.5 text-theme-text-disabled opacity-40 group-hover/th:opacity-100 hover:text-theme-text-primary'
|
|
3813
4011
|
)}
|
|
3814
4012
|
>
|
|
3815
4013
|
<ListFilter className="w-3 h-3" />
|
|
@@ -3921,6 +4119,13 @@ export function ResourcesView({
|
|
|
3921
4119
|
selectedResource?.namespace === resource.metadata?.namespace &&
|
|
3922
4120
|
selectedResource?.name === resource.metadata?.name
|
|
3923
4121
|
const isHighlighted = index === highlightedIndex
|
|
4122
|
+
const pickIdx = compareMode
|
|
4123
|
+
? pickIndex(comparePicks, {
|
|
4124
|
+
namespace: resource.metadata?.namespace || '',
|
|
4125
|
+
name: resource.metadata?.name || '',
|
|
4126
|
+
clusterId: resolveRowCluster?.(resource)?.id,
|
|
4127
|
+
})
|
|
4128
|
+
: -1
|
|
3924
4129
|
return (
|
|
3925
4130
|
<ResourceRowCells
|
|
3926
4131
|
resource={resource}
|
|
@@ -3932,8 +4137,10 @@ export function ResourcesView({
|
|
|
3932
4137
|
isSelected={isSelected}
|
|
3933
4138
|
isHighlighted={isHighlighted}
|
|
3934
4139
|
majorityNodeMinorVersion={majorityNodeMinorVersion}
|
|
3935
|
-
onClick={() => selectResource(resource, isSelected)}
|
|
4140
|
+
onClick={() => compareMode ? toggleComparePick(resource) : selectResource(resource, isSelected)}
|
|
3936
4141
|
onMouseEnter={() => setHighlightedIndex(-1)}
|
|
4142
|
+
compareMode={compareMode}
|
|
4143
|
+
comparePickIndex={pickIdx}
|
|
3937
4144
|
/>
|
|
3938
4145
|
)
|
|
3939
4146
|
}}
|
|
@@ -3949,6 +4156,15 @@ export function ResourcesView({
|
|
|
3949
4156
|
</MetricsContext.Provider>
|
|
3950
4157
|
)}
|
|
3951
4158
|
</div>
|
|
4159
|
+
{compareMode && compareEnabled && (
|
|
4160
|
+
<CompareTray
|
|
4161
|
+
kind={selectedKind.name}
|
|
4162
|
+
picks={comparePicks}
|
|
4163
|
+
onRemove={(idx) => setComparePicks(prev => prev.filter((_, i) => i !== idx))}
|
|
4164
|
+
onCompare={handleCompareNavigate}
|
|
4165
|
+
onExit={exitCompareMode}
|
|
4166
|
+
/>
|
|
4167
|
+
)}
|
|
3952
4168
|
</div>
|
|
3953
4169
|
</div>
|
|
3954
4170
|
</ResourcesViewDataContext.Provider>
|
|
@@ -3967,11 +4183,58 @@ interface ResourceRowCellsProps {
|
|
|
3967
4183
|
majorityNodeMinorVersion?: string
|
|
3968
4184
|
onClick?: () => void
|
|
3969
4185
|
onMouseEnter?: () => void
|
|
4186
|
+
compareMode?: boolean
|
|
4187
|
+
/** -1 when not picked; 0 = pick A; 1 = pick B. */
|
|
4188
|
+
comparePickIndex?: number
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
function rowHighlightClass(
|
|
4192
|
+
compareMode: boolean | undefined,
|
|
4193
|
+
comparePickIndex: number,
|
|
4194
|
+
isSelected: boolean | undefined,
|
|
4195
|
+
isHighlighted: boolean | undefined,
|
|
4196
|
+
): string {
|
|
4197
|
+
if (compareMode) {
|
|
4198
|
+
if (comparePickIndex === 0) return `${SIDE_TONES.a.rowBg} ${SIDE_TONES.a.rowBgHover}`
|
|
4199
|
+
if (comparePickIndex === 1) return `${SIDE_TONES.b.rowBg} ${SIDE_TONES.b.rowBgHover}`
|
|
4200
|
+
if (isHighlighted) return 'selection selection-ring'
|
|
4201
|
+
return 'group-hover/row:bg-theme-surface/50'
|
|
4202
|
+
}
|
|
4203
|
+
if (isSelected) return 'selection-strong group-hover/row:bg-skyhook-500/30'
|
|
4204
|
+
if (isHighlighted) return 'selection selection-ring'
|
|
4205
|
+
return 'group-hover/row:bg-theme-surface/50'
|
|
3970
4206
|
}
|
|
3971
4207
|
|
|
3972
|
-
function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, majorityNodeMinorVersion, onClick, onMouseEnter }: ResourceRowCellsProps) {
|
|
4208
|
+
function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, majorityNodeMinorVersion, onClick, onMouseEnter, compareMode, comparePickIndex = -1 }: ResourceRowCellsProps) {
|
|
4209
|
+
const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted)
|
|
4210
|
+
const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
|
|
3973
4211
|
return (
|
|
3974
4212
|
<>
|
|
4213
|
+
{compareMode && (
|
|
4214
|
+
<td
|
|
4215
|
+
onClick={onClick}
|
|
4216
|
+
onMouseEnter={onMouseEnter}
|
|
4217
|
+
style={{ width: 36, minWidth: 36, maxWidth: 36 }}
|
|
4218
|
+
className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight)}
|
|
4219
|
+
>
|
|
4220
|
+
{pickedSide ? (
|
|
4221
|
+
<span
|
|
4222
|
+
className={clsx(
|
|
4223
|
+
'inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold leading-none',
|
|
4224
|
+
SIDE_TONES[pickedSide].chipBg,
|
|
4225
|
+
)}
|
|
4226
|
+
aria-label={pickedSide === 'a' ? 'Pick A' : 'Pick B'}
|
|
4227
|
+
>
|
|
4228
|
+
{pickedSide === 'a' ? 'A' : 'B'}
|
|
4229
|
+
</span>
|
|
4230
|
+
) : (
|
|
4231
|
+
<span
|
|
4232
|
+
className="inline-block w-4 h-4 rounded border border-theme-border-light group-hover/row:border-skyhook-400/60 transition-colors"
|
|
4233
|
+
aria-hidden
|
|
4234
|
+
/>
|
|
4235
|
+
)}
|
|
4236
|
+
</td>
|
|
4237
|
+
)}
|
|
3975
4238
|
{columns.map((col) => (
|
|
3976
4239
|
<td
|
|
3977
4240
|
key={col.key}
|
|
@@ -3980,11 +4243,7 @@ function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, h
|
|
|
3980
4243
|
className={clsx(
|
|
3981
4244
|
'px-4 py-3 border-b-subtle cursor-pointer transition-colors',
|
|
3982
4245
|
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'
|
|
4246
|
+
rowHighlight,
|
|
3988
4247
|
)}
|
|
3989
4248
|
>
|
|
3990
4249
|
<CellContent resource={resource} kind={kind} group={group} column={col.key} majorityNodeMinorVersion={majorityNodeMinorVersion} extraColumn={extraColumnsByKey?.get(col.key)} />
|
|
@@ -4405,7 +4664,29 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
4405
4664
|
return <AzureMachineTemplateCell resource={resource} column={column} />
|
|
4406
4665
|
case 'azuremanagedclusters':
|
|
4407
4666
|
return <AzureManagedClusterCell resource={resource} column={column} />
|
|
4667
|
+
// Crossplane
|
|
4668
|
+
case 'providers':
|
|
4669
|
+
// Disambiguate from any future kind named "providers" by checking group
|
|
4670
|
+
if (resource.apiVersion?.startsWith('pkg.crossplane.io/')) {
|
|
4671
|
+
return <CrossplaneProviderCell resource={resource} column={column} />
|
|
4672
|
+
}
|
|
4673
|
+
return <GenericCell resource={resource} column={column} />
|
|
4674
|
+
case 'providerconfigs':
|
|
4675
|
+
return <CrossplaneProviderConfigCell resource={resource} column={column} />
|
|
4676
|
+
case 'compositions':
|
|
4677
|
+
return <CompositionCell resource={resource} column={column} />
|
|
4678
|
+
case 'compositeresourcedefinitions':
|
|
4679
|
+
return <XRDCell resource={resource} column={column} />
|
|
4408
4680
|
default:
|
|
4681
|
+
// Crossplane Managed Resources: unbounded plurals (one CRD per provider
|
|
4682
|
+
// service), detected via group prefix + spec.providerConfigRef heuristic.
|
|
4683
|
+
if (isLikelyCrossplaneMRGroup(kind, group ?? '') && isManagedResource(resource)) {
|
|
4684
|
+
return <ManagedResourceCell resource={resource} column={column} />
|
|
4685
|
+
}
|
|
4686
|
+
// Composite Resources (XRs) — user-defined kinds with resourceRefs.
|
|
4687
|
+
if (isComposite(resource)) {
|
|
4688
|
+
return <CompositeResourceCell resource={resource} column={column} />
|
|
4689
|
+
}
|
|
4409
4690
|
// Generic cell for CRDs and unknown resources
|
|
4410
4691
|
return <GenericCell resource={resource} column={column} />
|
|
4411
4692
|
}
|