@skyhook-io/k8s-ui 1.18.1 → 1.19.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/README.md +8 -0
- package/package.json +1 -1
- package/src/components/applications/ApplicationsView.tsx +8 -7
- package/src/components/charts/AreaChart.test.tsx +39 -1
- package/src/components/charts/AreaChart.tsx +32 -13
- package/src/components/charts/axis.test.ts +17 -1
- package/src/components/charts/axis.ts +13 -0
- package/src/components/charts/format.test.ts +159 -2
- package/src/components/charts/format.ts +118 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
- package/src/components/compare/ResourceCompareView.tsx +6 -2
- package/src/components/gitops/GitOpsTableView.tsx +13 -8
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +32 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +66 -36
- package/src/components/gitops/insights/GitOpsIssuesBand.test.tsx +108 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +28 -0
- package/src/components/gitops/insights/insights-helpers.ts +5 -3
- package/src/components/issues/IssueGuidance.test.tsx +190 -0
- package/src/components/issues/IssueGuidance.tsx +48 -0
- package/src/components/issues/IssuesView.tsx +17 -5
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/index.ts +1 -0
- package/src/components/issues/severity.ts +2 -0
- package/src/components/issues/types.ts +10 -0
- package/src/components/logs/LogCore.tsx +13 -11
- package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +2 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
- package/src/components/resources/ResourcesSidebar.tsx +26 -5
- package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
- package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
- package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
- package/src/components/resources/ResourcesView.tsx +238 -66
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +15 -0
- package/src/components/resources/printer-columns.test.ts +36 -0
- package/src/components/resources/printer-columns.ts +12 -1
- package/src/components/resources/readiness-gates.test.ts +95 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
- package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
- package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
- package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
- package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
- package/src/components/resources/resource-utils-crossplane.ts +44 -8
- package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
- package/src/components/resources/resource-utils-flagger.ts +76 -0
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
- package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
- package/src/components/resources/resource-utils.ts +80 -2
- package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
- package/src/components/shared/CreateResourceDialog.tsx +5 -0
- package/src/components/shared/DetailShell.phone.test.tsx +54 -0
- package/src/components/shared/DetailShell.tsx +14 -3
- package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
- package/src/components/shared/EditableYamlView.test.tsx +58 -0
- package/src/components/shared/EditableYamlView.tsx +18 -3
- package/src/components/shared/FilterRailPanel.test.tsx +39 -0
- package/src/components/shared/FilterRailPanel.tsx +62 -0
- package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
- package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
- package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
- package/src/components/shared/ManagedByChip.tsx +135 -2
- package/src/components/shared/ResourceActionsBar.tsx +22 -6
- package/src/components/shared/ResourceRendererDispatch.tsx +23 -0
- package/src/components/shared/SetImageDialog.tsx +3 -0
- package/src/components/topology/TopologyControls.tsx +1 -1
- package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
- package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
- package/src/components/ui/PageHeader.tsx +3 -2
- package/src/components/ui/SummaryTile.tsx +13 -3
- package/src/components/ui/Tooltip.touch.test.tsx +55 -0
- package/src/components/ui/Tooltip.tsx +11 -2
- package/src/components/ui/YamlReview.test.tsx +52 -0
- package/src/components/ui/YamlReview.tsx +7 -0
- package/src/components/ui/drawer-components.test.tsx +27 -0
- package/src/components/ui/drawer-components.tsx +22 -42
- package/src/components/workload/WorkloadView.relationships.test.tsx +40 -0
- package/src/components/workload/WorkloadView.tsx +57 -34
- package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
- package/src/hooks/useKeepInViewport.test.ts +27 -0
- package/src/hooks/useKeepInViewport.ts +87 -0
- package/src/types/core.ts +54 -1
- package/src/types/gitops-insights.ts +8 -0
- package/src/utils/api-resources.ts +1 -0
- package/src/utils/gitops-owner.ts +136 -8
- package/src/utils/gitops-write-guard.test.ts +332 -0
- package/src/utils/gitops-write-guard.ts +175 -11
- package/src/utils/index.ts +1 -0
- package/src/utils/read-actions.ts +34 -0
- package/src/utils/touch-intent.test.ts +39 -0
- package/src/utils/touch-intent.ts +35 -0
- package/vitest.config.ts +26 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { getOpenTelemetryColumnValue } from './resource-utils-opentelemetry'
|
|
2
|
+
import { getFlaggerColumnValue } from './resource-utils-flagger'
|
|
1
3
|
import type { ResourceCoverage } from '../../types/resource-coverage'
|
|
2
4
|
import { resourceCoverageNotice, resourceEmptyMessage, resourceNarrowedNotice } from '../../utils/resource-coverage'
|
|
3
5
|
import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
|
|
4
6
|
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
5
7
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue'
|
|
8
|
+
import { usePhoneLayout } from '../../hooks/usePhoneLayout'
|
|
9
|
+
import { CrossplaneDeleteNotice } from '../ui/CrossplaneOperationNotice'
|
|
6
10
|
import { Badge } from '../ui/Badge'
|
|
7
11
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
12
|
+
import { DialogPortal } from '../ui/DialogPortal'
|
|
8
13
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
9
14
|
import { FetchResult } from '../ui/FetchResult'
|
|
10
15
|
import { Input } from '../ui/Input'
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
ListChecks,
|
|
35
40
|
Minus,
|
|
36
41
|
Scale,
|
|
42
|
+
PanelLeft,
|
|
37
43
|
} from 'lucide-react'
|
|
38
44
|
import { clsx } from 'clsx'
|
|
39
45
|
import { ResourceBar } from '../ui/ResourceBar'
|
|
@@ -204,8 +210,9 @@ import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell, Volum
|
|
|
204
210
|
import { isVeleroResource } from './resource-utils-velero'
|
|
205
211
|
import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell, CNPGObjectStoreCell, CNPGDeclarativeCell, CNPGImageCatalogCell } from './renderers/cnpg-cells'
|
|
206
212
|
import { isApiGroup, CNPG_GROUP } from './resource-utils-cnpg'
|
|
207
|
-
import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
208
|
-
import { isManagedResource, isComposite } from './resource-utils-crossplane'
|
|
213
|
+
import { CrossplaneStatusCell, ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
|
|
214
|
+
import { getCrossplaneStatus, isManagedResource, isComposite, getCrossplanePauseState } from './resource-utils-crossplane'
|
|
215
|
+
import { CrossplanePauseChip } from './renderers/CrossplanePauseContext'
|
|
209
216
|
import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
|
|
210
217
|
import { KnativeServiceCell, ConfigurationCell as KnativeConfigurationCell, RevisionCell as KnativeRevisionCell, RouteCell as KnativeRouteCell, BrokerCell, TriggerCell, EventTypeCell, PingSourceCell, ApiServerSourceCell, ContainerSourceCell, SinkBindingCell, ChannelCell, InMemoryChannelCell, SubscriptionCell, SequenceCell, ParallelCell, DomainMappingCell, ServerlessServiceCell, KnativeIngressCell, KnativeCertificateCell } from './renderers/knative-cells'
|
|
211
218
|
import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
|
|
@@ -218,7 +225,7 @@ import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
|
|
|
218
225
|
import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
|
|
219
226
|
import { useRegisterShortcut, useRegisterShortcuts, type KeyboardShortcut } from '../../hooks/useKeyboardShortcuts'
|
|
220
227
|
import { ResourcesSidebar } from './ResourcesSidebar'
|
|
221
|
-
import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
|
|
228
|
+
import type { SelectedKindInfo, SidebarCategoryWorkspace, SidebarDisplayState } from './ResourcesSidebar'
|
|
222
229
|
import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
|
|
223
230
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
224
231
|
|
|
@@ -341,6 +348,24 @@ const COMPARE_COLUMN_STYLE: React.CSSProperties = {
|
|
|
341
348
|
maxWidth: COMPARE_COLUMN_WIDTH,
|
|
342
349
|
}
|
|
343
350
|
|
|
351
|
+
// Phones: the leading columns (compare marker, selection checkbox, then Name
|
|
352
|
+
// when it is the first column) stay in place while the rest of the table
|
|
353
|
+
// scrolls sideways, so every row stays identifiable. The opaque background
|
|
354
|
+
// keeps scrolled cells from showing through; on phones it hides the selection
|
|
355
|
+
// tint on these cells, where a tap opens the full-screen detail anyway.
|
|
356
|
+
// The offset rides a CSS variable that only the phone classes read, so the
|
|
357
|
+
// desktop header (position: relative) never moves.
|
|
358
|
+
const CHECKBOX_COLUMN_WIDTH = 40
|
|
359
|
+
const PHONE_PINNED_CELL = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[1] phone:bg-theme-surface'
|
|
360
|
+
const PHONE_PINNED_HEAD = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[3]'
|
|
361
|
+
const PHONE_NAME_MAX_VW = 45
|
|
362
|
+
function phonePinnedLeft(compareMode: boolean | undefined, showCheckbox: boolean | undefined, column: 'compare' | 'checkbox' | 'name'): React.CSSProperties {
|
|
363
|
+
const compare = compareMode ? COMPARE_COLUMN_WIDTH : 0
|
|
364
|
+
const checkbox = showCheckbox ? CHECKBOX_COLUMN_WIDTH : 0
|
|
365
|
+
const left = column === 'compare' ? 0 : column === 'checkbox' ? compare : compare + checkbox
|
|
366
|
+
return { '--pin-left': `${left}px` } as React.CSSProperties
|
|
367
|
+
}
|
|
368
|
+
|
|
344
369
|
// Built-in sortable keys. Hoisted so getColumnMinWidth can reserve room for the
|
|
345
370
|
// sort icon without the header render and the width math drifting apart.
|
|
346
371
|
const BUILTIN_SORTABLE_COLUMN_KEYS = new Set([
|
|
@@ -554,6 +579,44 @@ const DEFAULT_COLUMNS: Column[] = [
|
|
|
554
579
|
]
|
|
555
580
|
|
|
556
581
|
const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
582
|
+
opentelemetrycollectors: [
|
|
583
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
584
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
585
|
+
{ key: 'status', label: 'Reconciliation', width: 'w-40 shrink-0', tooltip: 'Operator reconciliation only, not Pod health or telemetry delivery.' },
|
|
586
|
+
{ key: 'mode', label: 'Mode', width: 'w-28 shrink-0' },
|
|
587
|
+
{ key: 'reportedPods', label: 'Reported Pods', width: 'w-32 shrink-0', tooltip: 'Last reported ready/total snapshot. Total is observed Pods for Deployment/StatefulSet, desired scheduled Pods for DaemonSet. Inspect the owned workload for current readiness.' },
|
|
588
|
+
{ key: 'version', label: 'Version', width: 'w-24 shrink-0' },
|
|
589
|
+
{ key: 'image', label: 'Image', width: 'min-w-48' },
|
|
590
|
+
{ key: 'management', label: 'Management', width: 'w-28 shrink-0' },
|
|
591
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
592
|
+
],
|
|
593
|
+
instrumentations: [
|
|
594
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
595
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
596
|
+
{ key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
|
|
597
|
+
{ key: 'endpoint', label: 'OTLP Endpoint', width: 'min-w-48' },
|
|
598
|
+
{ key: 'sampler', label: 'Sampler', width: 'w-40 shrink-0' },
|
|
599
|
+
{ key: 'samplerArgument', label: 'Sampler Argument', width: 'w-32 shrink-0' },
|
|
600
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
601
|
+
],
|
|
602
|
+
canaries: [
|
|
603
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
604
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
605
|
+
{ key: 'status', label: 'Rollout State', width: 'w-40 shrink-0', tooltip: 'Last recorded rollout outcome, with requested suspension separate from a failed result. Not a latest-generation guarantee.' },
|
|
606
|
+
{ key: 'strategy', label: 'Strategy', width: 'w-36 shrink-0' },
|
|
607
|
+
{ key: 'target', label: 'Canary Target', width: 'min-w-40' },
|
|
608
|
+
{ key: 'canaryWeight', label: 'Reported Weight', width: 'w-36 shrink-0', tooltip: 'Last reported canary routing weight, not measured traffic. Blue/green and A/B strategies are not weighted.' },
|
|
609
|
+
{ key: 'suspended', label: 'Suspend Requested', width: 'w-36 shrink-0' },
|
|
610
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
611
|
+
],
|
|
612
|
+
metrictemplates: [
|
|
613
|
+
{ key: 'name', label: 'Name', width: 'min-w-40' },
|
|
614
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
|
|
615
|
+
{ key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
|
|
616
|
+
{ key: 'provider', label: 'Provider', width: 'w-32 shrink-0' },
|
|
617
|
+
{ key: 'address', label: 'Provider Address', width: 'min-w-48' },
|
|
618
|
+
{ key: 'age', label: 'Age', width: 'w-24 shrink-0' },
|
|
619
|
+
],
|
|
557
620
|
pods: [
|
|
558
621
|
{ key: 'name', label: 'Name' },
|
|
559
622
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -2851,6 +2914,10 @@ function isCuratedUnboundedGroup(key: string, group: string): boolean {
|
|
|
2851
2914
|
* cannot occupy one. See CORE_API_GROUPS.
|
|
2852
2915
|
*/
|
|
2853
2916
|
const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
|
|
2917
|
+
opentelemetrycollectors: ['opentelemetry.io'],
|
|
2918
|
+
instrumentations: ['opentelemetry.io'],
|
|
2919
|
+
canaries: ['flagger.app'],
|
|
2920
|
+
metrictemplates: ['flagger.app'],
|
|
2854
2921
|
challenges: ['acme.cert-manager.io'],
|
|
2855
2922
|
orders: ['acme.cert-manager.io'],
|
|
2856
2923
|
compositeresourcedefinitions: ['apiextensions.crossplane.io'],
|
|
@@ -3132,12 +3199,12 @@ function buildPrinterColumn(def: PrinterColumnDef, index: number, table: Printer
|
|
|
3132
3199
|
// A column the vendor named Ready/Healthy/Established carries the only
|
|
3133
3200
|
// scannable signal these kinds have — they have no curated Status column.
|
|
3134
3201
|
// Rendering it as grey text would leave a screen of uniform "True".
|
|
3135
|
-
const tone = printerCellTone(def, value)
|
|
3202
|
+
const tone = printerCellTone(def, value, resource)
|
|
3136
3203
|
if (tone) {
|
|
3137
3204
|
return (
|
|
3138
3205
|
// The value, not the column's description — the header tooltip
|
|
3139
3206
|
// carries that. A cell tooltip exists to reveal a truncated value.
|
|
3140
|
-
<Tooltip content={text}>
|
|
3207
|
+
<Tooltip content={tone === 'neutral' ? `${text} · ${String(value).trim().toLowerCase() === 'false' ? 'Controller recorded reconciliation paused' : 'Last reported readiness while reconciliation is paused'}` : text}>
|
|
3141
3208
|
<span className={clsx('badge', healthColors[tone])}>{text}</span>
|
|
3142
3209
|
</Tooltip>
|
|
3143
3210
|
)
|
|
@@ -3765,6 +3832,15 @@ export function ResourcesView({
|
|
|
3765
3832
|
}: ResourcesViewProps) {
|
|
3766
3833
|
const initialFilters = getInitialFiltersFromURL()
|
|
3767
3834
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
|
|
3835
|
+
// Phones: the 224px kind list would leave the table a sliver, so it lives in
|
|
3836
|
+
// a drawer over the table, opened from the toolbar and closed by a pick.
|
|
3837
|
+
const isPhone = usePhoneLayout()
|
|
3838
|
+
const [kindsDrawerOpen, setKindsDrawerOpen] = useState(false)
|
|
3839
|
+
// The drawer unmounts its sidebar on close; keep its display settings here.
|
|
3840
|
+
const [drawerSidebarState, setDrawerSidebarState] = useState<SidebarDisplayState>(() => ({ showEmptyKinds: false, favoritesExpanded: pinned.length > 0 }))
|
|
3841
|
+
useEffect(() => {
|
|
3842
|
+
if (!isPhone) setKindsDrawerOpen(false)
|
|
3843
|
+
}, [isPhone])
|
|
3768
3844
|
// Sync selectedKind from URL when the URL changes (browser back, external sidebar navigation).
|
|
3769
3845
|
// Deps are URL-derived only — including selectedKind.name/group would race against pending
|
|
3770
3846
|
// navigation: a sidebar click flips state before navigate() lands, this effect re-reads the
|
|
@@ -4340,7 +4416,7 @@ export function ResourcesView({
|
|
|
4340
4416
|
category: 'Search',
|
|
4341
4417
|
scope: 'resources',
|
|
4342
4418
|
handler: () => searchInputRef.current?.focus(),
|
|
4343
|
-
enabled: tableActive,
|
|
4419
|
+
enabled: tableActive && !kindsDrawerOpen,
|
|
4344
4420
|
})
|
|
4345
4421
|
|
|
4346
4422
|
// Keyboard navigation: highlighted row state
|
|
@@ -4449,6 +4525,8 @@ export function ResourcesView({
|
|
|
4449
4525
|
onResourceClick?.(isSelected ? null : stripped)
|
|
4450
4526
|
}, [onRowSelect, onResourceClick, selectedKind.name, selectedKind.group])
|
|
4451
4527
|
|
|
4528
|
+
// Shortcuts here (and the search shortcut above) pause while the phone
|
|
4529
|
+
// kind drawer is open: it's modal, so the table behind it takes no keys.
|
|
4452
4530
|
useRegisterShortcuts(([
|
|
4453
4531
|
{
|
|
4454
4532
|
id: 'resources-nav-down',
|
|
@@ -4622,7 +4700,7 @@ export function ResourcesView({
|
|
|
4622
4700
|
else searchInputRef.current?.blur()
|
|
4623
4701
|
},
|
|
4624
4702
|
},
|
|
4625
|
-
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && (sc.enabled ?? true) })))
|
|
4703
|
+
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && !kindsDrawerOpen && (sc.enabled ?? true) })))
|
|
4626
4704
|
|
|
4627
4705
|
// Refs for accessing filteredResources inside shortcuts (computed later in component)
|
|
4628
4706
|
const filteredResourceCountRef = useRef(0)
|
|
@@ -4632,7 +4710,7 @@ export function ResourcesView({
|
|
|
4632
4710
|
const flatKindListRef = useRef<SelectedKindInfo[]>([])
|
|
4633
4711
|
|
|
4634
4712
|
// Sidebar kind navigation: [ = previous kind, ] = next kind
|
|
4635
|
-
useRegisterShortcuts([
|
|
4713
|
+
useRegisterShortcuts(([
|
|
4636
4714
|
{
|
|
4637
4715
|
id: 'resources-prev-kind',
|
|
4638
4716
|
keys: '[',
|
|
@@ -4665,7 +4743,7 @@ export function ResourcesView({
|
|
|
4665
4743
|
onKindChange?.()
|
|
4666
4744
|
},
|
|
4667
4745
|
},
|
|
4668
|
-
])
|
|
4746
|
+
] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: !kindsDrawerOpen && (sc.enabled ?? true) })))
|
|
4669
4747
|
|
|
4670
4748
|
// Close dropdowns on outside click
|
|
4671
4749
|
useEffect(() => {
|
|
@@ -5347,14 +5425,26 @@ export function ResourcesView({
|
|
|
5347
5425
|
if (issue) return issueTitle(issue)
|
|
5348
5426
|
if (operationalIssuesIncomplete) return 'Unknown'
|
|
5349
5427
|
}
|
|
5428
|
+
if (column === 'status') {
|
|
5429
|
+
const managedColumns = isLikelyCrossplaneMRGroup(selectedKind.name, selectedKind.group ?? '')
|
|
5430
|
+
if ((managedColumns && isManagedResource(resource))
|
|
5431
|
+
|| ((!hasCuratedColumns(selectedKind.name, selectedKind.group) || managedColumns)
|
|
5432
|
+
&& getCrossplanePauseState(resource) === 'acknowledged')) {
|
|
5433
|
+
return getCrossplaneStatus(resource).text
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5350
5436
|
return getCellFilterValue(resource, column, kind)
|
|
5351
|
-
}, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
|
|
5437
|
+
}, [operationalIssues, operationalIssuesIncomplete, selectedKind.name, selectedKind.group])
|
|
5352
5438
|
|
|
5353
5439
|
// Get sortable value from a resource for a given column
|
|
5354
5440
|
const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
|
|
5355
5441
|
const meta = resource.metadata || {}
|
|
5356
5442
|
const status = resource.status || {}
|
|
5357
5443
|
const kindLower = kind?.toLowerCase() || ''
|
|
5444
|
+
const otelValue = getOpenTelemetryColumnValue(resource, column)
|
|
5445
|
+
if (otelValue !== undefined) return otelValue
|
|
5446
|
+
const flaggerValue = getFlaggerColumnValue(resource, column)
|
|
5447
|
+
if (flaggerValue !== undefined) return flaggerValue
|
|
5358
5448
|
|
|
5359
5449
|
switch (column) {
|
|
5360
5450
|
case 'name':
|
|
@@ -5801,7 +5891,9 @@ export function ResourcesView({
|
|
|
5801
5891
|
const selectedKindName = selectedKind.name.toLowerCase()
|
|
5802
5892
|
const canBulkRestartSelectedKind = onBulkRestart != null && BULK_RESTART_WORKLOAD_KINDS.has(selectedKindName)
|
|
5803
5893
|
const canBulkScaleSelectedKind = onBulkScale != null && BULK_SCALE_WORKLOAD_KINDS.has(selectedKindName)
|
|
5804
|
-
|
|
5894
|
+
// Phones skip bulk selection and create (see the toolbar): phone triage
|
|
5895
|
+
// is one resource at a time.
|
|
5896
|
+
const canBulkSelect = !isPhone && (onBulkDelete != null || canBulkRestartSelectedKind || canBulkScaleSelectedKind)
|
|
5805
5897
|
const isBulkMutating = isBulkDeleting || isBulkRestarting || isBulkScaling
|
|
5806
5898
|
|
|
5807
5899
|
const openBulkScaleDialog = useCallback(() => {
|
|
@@ -5852,14 +5944,30 @@ export function ResourcesView({
|
|
|
5852
5944
|
// Fixed-width columns can consume the table's flexible space and collapse
|
|
5853
5945
|
// the required name column. Keep a real table minimum and let the container
|
|
5854
5946
|
// scroll horizontally when the viewport is too narrow.
|
|
5947
|
+
// Phones pin a leading Name column (see PHONE_PINNED_CELL). Cap it at
|
|
5948
|
+
// PHONE_NAME_MAX_VW percent of the screen so a width saved on desktop can't
|
|
5949
|
+
// make the pinned column cover it; the saved width itself is left alone.
|
|
5950
|
+
// Tracked through resizes and rotation, which can stay within phone width.
|
|
5951
|
+
const [phoneViewportWidth, setPhoneViewportWidth] = useState(() => (typeof window === 'undefined' ? 0 : window.innerWidth))
|
|
5952
|
+
useEffect(() => {
|
|
5953
|
+
if (!isPhone) return
|
|
5954
|
+
const onResize = () => setPhoneViewportWidth(window.innerWidth)
|
|
5955
|
+
onResize()
|
|
5956
|
+
window.addEventListener('resize', onResize)
|
|
5957
|
+
return () => window.removeEventListener('resize', onResize)
|
|
5958
|
+
}, [isPhone])
|
|
5959
|
+
const phoneNameCap = isPhone && columns[0]?.key === 'name' ? Math.round(phoneViewportWidth * PHONE_NAME_MAX_VW / 100) : Infinity
|
|
5855
5960
|
const tableMinWidth = useMemo(() => {
|
|
5856
5961
|
const compareColumnWidth = compareMode ? COMPARE_COLUMN_WIDTH : 0
|
|
5857
|
-
const baseMinWidth = columns.reduce((sum, col) =>
|
|
5962
|
+
const baseMinWidth = columns.reduce((sum, col, i) => {
|
|
5963
|
+
const width = columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)
|
|
5964
|
+
return sum + (i === 0 && col.key === 'name' ? Math.min(width, phoneNameCap) : width)
|
|
5965
|
+
}, compareColumnWidth)
|
|
5858
5966
|
const flexibleNameColumn = columns.find(col => col.key === 'name' && !columnWidths[col.key])
|
|
5859
5967
|
|
|
5860
5968
|
if (!hasResizedColumns || !flexibleNameColumn) return baseMinWidth
|
|
5861
5969
|
return baseMinWidth + getColumnMinWidth(flexibleNameColumn, extraColumnsByKey)
|
|
5862
|
-
}, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch])
|
|
5970
|
+
}, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch, phoneNameCap])
|
|
5863
5971
|
|
|
5864
5972
|
// Stable virtuoso components — memoized to avoid remounting the table on every render
|
|
5865
5973
|
const virtuosoComponents = useMemo(() => ({
|
|
@@ -5881,13 +5989,15 @@ export function ResourcesView({
|
|
|
5881
5989
|
*/}
|
|
5882
5990
|
{compareMode && <col style={{ width: COMPARE_COLUMN_WIDTH }} />}
|
|
5883
5991
|
{isCheckboxMode && <col style={{ width: '40px' }} />}
|
|
5884
|
-
{columns.map(col => (
|
|
5992
|
+
{columns.map((col, i) => (
|
|
5885
5993
|
<col
|
|
5886
5994
|
key={col.key}
|
|
5887
5995
|
style={{
|
|
5888
|
-
width:
|
|
5889
|
-
?
|
|
5890
|
-
: col.key
|
|
5996
|
+
width: i === 0 && col.key === 'name' && phoneNameCap !== Infinity
|
|
5997
|
+
? `min(${columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)}px, ${PHONE_NAME_MAX_VW}vw)`
|
|
5998
|
+
: columnWidths[col.key]
|
|
5999
|
+
? `${columnWidths[col.key]}px`
|
|
6000
|
+
: col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
|
|
5891
6001
|
}}
|
|
5892
6002
|
/>
|
|
5893
6003
|
))}
|
|
@@ -5898,7 +6008,7 @@ export function ResourcesView({
|
|
|
5898
6008
|
)
|
|
5899
6009
|
}),
|
|
5900
6010
|
TableRow: VirtuosoTableRow,
|
|
5901
|
-
}), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch])
|
|
6011
|
+
}), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch, phoneNameCap])
|
|
5902
6012
|
|
|
5903
6013
|
// Calculate filter options with counts based on current resources (before filtering)
|
|
5904
6014
|
const filterOptions = useMemo(() => {
|
|
@@ -6096,42 +6206,81 @@ export function ResourcesView({
|
|
|
6096
6206
|
onOpenWorkloadLogs,
|
|
6097
6207
|
}), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
|
|
6098
6208
|
|
|
6209
|
+
const renderSidebar = (className?: string, inDrawer = false) => (
|
|
6210
|
+
<ResourcesSidebar
|
|
6211
|
+
className={className}
|
|
6212
|
+
displayState={inDrawer ? drawerSidebarState : undefined}
|
|
6213
|
+
onDisplayStateChange={inDrawer ? setDrawerSidebarState : undefined}
|
|
6214
|
+
containEscape={inDrawer}
|
|
6215
|
+
selectedKind={selectedKind}
|
|
6216
|
+
onSelectedKindChange={(kind) => {
|
|
6217
|
+
shouldPushHistory.current = true
|
|
6218
|
+
setSelectedKind(kind)
|
|
6219
|
+
setKindsDrawerOpen(false)
|
|
6220
|
+
}}
|
|
6221
|
+
onKindChange={onKindChange}
|
|
6222
|
+
apiResources={apiResourcesProp}
|
|
6223
|
+
resourceCounts={counts}
|
|
6224
|
+
resourceCoverage={sidebarResourceCoverage}
|
|
6225
|
+
resourceForbidden={Array.from(forbiddenKinds)}
|
|
6226
|
+
resourceUnavailable={sidebarResourceUnavailable}
|
|
6227
|
+
pinned={pinned}
|
|
6228
|
+
togglePin={togglePin}
|
|
6229
|
+
isPinned={isPinned}
|
|
6230
|
+
categoryWorkspaces={sidebarCategoryWorkspaces}
|
|
6231
|
+
onKindNavigated={() => {
|
|
6232
|
+
// After selecting a kind via keyboard, move focus to the table search
|
|
6233
|
+
// so the user can immediately filter within the selected kind.
|
|
6234
|
+
setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
6235
|
+
}}
|
|
6236
|
+
/>
|
|
6237
|
+
)
|
|
6238
|
+
|
|
6239
|
+
const kindsButton = (
|
|
6240
|
+
<button
|
|
6241
|
+
type="button"
|
|
6242
|
+
onClick={() => setKindsDrawerOpen(true)}
|
|
6243
|
+
aria-expanded={kindsDrawerOpen}
|
|
6244
|
+
aria-label={`Resource kinds (showing ${selectedKind.kind})`}
|
|
6245
|
+
className="flex shrink-0 items-center gap-1.5 rounded-lg border border-theme-border bg-theme-elevated px-2.5 py-1.5 text-sm font-medium text-theme-text-primary hover:bg-theme-hover"
|
|
6246
|
+
>
|
|
6247
|
+
<PanelLeft className="h-4 w-4 text-theme-text-tertiary" />
|
|
6248
|
+
<span className="max-w-[7rem] truncate">{selectedKind.kind}</span>
|
|
6249
|
+
</button>
|
|
6250
|
+
)
|
|
6251
|
+
|
|
6099
6252
|
return (
|
|
6100
6253
|
<ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
|
|
6101
6254
|
<div className="flex h-full w-full">
|
|
6102
|
-
{/* Sidebar - Resource Types
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
}
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
resourceUnavailable={sidebarResourceUnavailable}
|
|
6116
|
-
pinned={pinned}
|
|
6117
|
-
togglePin={togglePin}
|
|
6118
|
-
isPinned={isPinned}
|
|
6119
|
-
categoryWorkspaces={sidebarCategoryWorkspaces}
|
|
6120
|
-
onKindNavigated={() => {
|
|
6121
|
-
// After selecting a kind via keyboard, move focus to the table search
|
|
6122
|
-
// so the user can immediately filter within the selected kind.
|
|
6123
|
-
setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
6124
|
-
}}
|
|
6125
|
-
/>
|
|
6255
|
+
{/* Sidebar - Resource Types. On phones it is a modal drawer: the
|
|
6256
|
+
dialog primitive traps focus, captures Escape before the table's
|
|
6257
|
+
own shortcuts, and returns focus to the kinds button. */}
|
|
6258
|
+
{!hideSidebar && !isPhone && renderSidebar()}
|
|
6259
|
+
{!hideSidebar && isPhone && (
|
|
6260
|
+
<DialogPortal
|
|
6261
|
+
open={kindsDrawerOpen}
|
|
6262
|
+
onClose={() => setKindsDrawerOpen(false)}
|
|
6263
|
+
ariaLabel="Resource kinds"
|
|
6264
|
+
className="!ml-0 !mr-auto h-full w-72 max-w-[85vw] self-stretch overflow-hidden rounded-none border-y-0 border-l-0"
|
|
6265
|
+
>
|
|
6266
|
+
{renderSidebar('!w-full h-full border-r-0', true)}
|
|
6267
|
+
</DialogPortal>
|
|
6126
6268
|
)}
|
|
6127
6269
|
|
|
6128
6270
|
{kindView !== null ? (
|
|
6129
|
-
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
6271
|
+
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
6272
|
+
{!hideSidebar && isPhone && (
|
|
6273
|
+
<div className="flex items-center px-3 py-2 border-b border-theme-border bg-theme-base shrink-0">{kindsButton}</div>
|
|
6274
|
+
)}
|
|
6275
|
+
{kindView}
|
|
6276
|
+
</div>
|
|
6130
6277
|
) : (
|
|
6131
6278
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
|
|
6132
6279
|
{/* Toolbar */}
|
|
6133
|
-
|
|
6134
|
-
|
|
6280
|
+
{/* Phones: the toolbar wraps and search takes its own full row. */}
|
|
6281
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0 phone:relative phone:flex-wrap phone:gap-2 phone:px-3">
|
|
6282
|
+
{!hideSidebar && isPhone && kindsButton}
|
|
6283
|
+
<div className="flex-1 min-w-0 phone:order-last phone:basis-full">
|
|
6135
6284
|
<div className="relative max-w-md">
|
|
6136
6285
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
6137
6286
|
<Input
|
|
@@ -6214,9 +6363,11 @@ export function ResourcesView({
|
|
|
6214
6363
|
</div>
|
|
6215
6364
|
</div>
|
|
6216
6365
|
|
|
6217
|
-
{/* Problems dropdown (pods only)
|
|
6366
|
+
{/* Problems dropdown (pods only). On phones the toolbar wraps, so this
|
|
6367
|
+
menu and Labels' anchor to the toolbar (full width below it)
|
|
6368
|
+
rather than to a trigger that may sit near either edge. */}
|
|
6218
6369
|
{filterOptions?.problems && filterOptions.problems.length > 0 && (
|
|
6219
|
-
<div className="relative" ref={problemsDropdownRef}>
|
|
6370
|
+
<div className="relative phone:static" ref={problemsDropdownRef}>
|
|
6220
6371
|
<button
|
|
6221
6372
|
onClick={() => { setShowProblemsDropdown(!showProblemsDropdown); setShowLabelsDropdown(false) }}
|
|
6222
6373
|
className={clsx(
|
|
@@ -6235,7 +6386,7 @@ export function ResourcesView({
|
|
|
6235
6386
|
)}
|
|
6236
6387
|
</button>
|
|
6237
6388
|
{showProblemsDropdown && (
|
|
6238
|
-
<div className="absolute right-0 top-full mt-1 min-w-48 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6389
|
+
<div className="absolute right-0 top-full mt-1 min-w-48 phone:left-3 phone:right-3 phone:min-w-0 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6239
6390
|
{problemFilters.length > 0 && (
|
|
6240
6391
|
<div className="flex items-center justify-between px-3 py-1.5 border-b border-theme-border">
|
|
6241
6392
|
<span className="text-xs font-medium text-theme-text-secondary">Problems</span>
|
|
@@ -6266,7 +6417,7 @@ export function ResourcesView({
|
|
|
6266
6417
|
|
|
6267
6418
|
{/* Labels dropdown */}
|
|
6268
6419
|
{filterOptions?.labels && filterOptions.labels.length > 0 && (
|
|
6269
|
-
<div className="relative" ref={labelsDropdownRef}>
|
|
6420
|
+
<div className="relative phone:static" ref={labelsDropdownRef}>
|
|
6270
6421
|
<button
|
|
6271
6422
|
onClick={() => { setShowLabelsDropdown(!showLabelsDropdown); setShowProblemsDropdown(false); setLabelSearch('') }}
|
|
6272
6423
|
className={clsx(
|
|
@@ -6290,7 +6441,7 @@ export function ResourcesView({
|
|
|
6290
6441
|
? labels.filter(l => l.value.toLowerCase().includes(labelSearch.toLowerCase()))
|
|
6291
6442
|
: labels
|
|
6292
6443
|
return (
|
|
6293
|
-
<div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6444
|
+
<div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 phone:left-3 phone:right-3 phone:min-w-0 phone:max-w-none bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
|
|
6294
6445
|
<div className="flex items-center gap-2 p-2 border-b border-theme-border">
|
|
6295
6446
|
<div className="relative flex-1">
|
|
6296
6447
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
@@ -6470,7 +6621,7 @@ export function ResourcesView({
|
|
|
6470
6621
|
</div>
|
|
6471
6622
|
)}
|
|
6472
6623
|
</div>
|
|
6473
|
-
{onCreateResource && (
|
|
6624
|
+
{onCreateResource && !isPhone && (
|
|
6474
6625
|
<Tooltip content={`Create ${selectedKind.kind || 'resource'}`}>
|
|
6475
6626
|
<button
|
|
6476
6627
|
aria-label={`Create ${selectedKind.kind || 'resource'}`}
|
|
@@ -6530,7 +6681,7 @@ export function ResourcesView({
|
|
|
6530
6681
|
|
|
6531
6682
|
{/* Bulk actions bar */}
|
|
6532
6683
|
{showBulkBar && (
|
|
6533
|
-
<div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
|
|
6684
|
+
<div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0 phone:flex-wrap phone:gap-2 phone:px-3">
|
|
6534
6685
|
<span className="text-sm font-medium text-theme-text-primary">
|
|
6535
6686
|
{checkedItems.length} selected
|
|
6536
6687
|
</span>
|
|
@@ -6727,15 +6878,15 @@ export function ResourcesView({
|
|
|
6727
6878
|
// Inline px width — under `table-layout:fixed`,
|
|
6728
6879
|
// `w-9` is a hint the browser absorbs into leftover
|
|
6729
6880
|
// row width on an icon-only column.
|
|
6730
|
-
style={COMPARE_COLUMN_STYLE}
|
|
6731
|
-
className=
|
|
6881
|
+
style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, isCheckboxMode, 'compare') }}
|
|
6882
|
+
className={clsx('px-2 py-3 text-xs font-medium uppercase tracking-wide bg-theme-base border-b border-r-subtle border-theme-border text-center text-skyhook-400', PHONE_PINNED_HEAD)}
|
|
6732
6883
|
title="Compare mode"
|
|
6733
6884
|
>
|
|
6734
6885
|
<GitCompare className="w-3.5 h-3.5 inline-block opacity-70" />
|
|
6735
6886
|
</th>
|
|
6736
6887
|
)}
|
|
6737
6888
|
{isCheckboxMode && (
|
|
6738
|
-
<th className=
|
|
6889
|
+
<th style={phonePinnedLeft(compareMode, isCheckboxMode, 'checkbox')} className={clsx('bg-theme-surface border-b border-theme-border w-10 text-center px-0 py-3', PHONE_PINNED_HEAD)}>
|
|
6739
6890
|
<input
|
|
6740
6891
|
type="checkbox"
|
|
6741
6892
|
checked={allVisibleChecked}
|
|
@@ -6758,9 +6909,11 @@ export function ResourcesView({
|
|
|
6758
6909
|
return (
|
|
6759
6910
|
<th
|
|
6760
6911
|
key={col.key}
|
|
6912
|
+
style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, isCheckboxMode, 'name') : undefined}
|
|
6761
6913
|
className={clsx(
|
|
6762
6914
|
'text-left px-4 py-3 text-xs font-medium uppercase tracking-wide relative group/th',
|
|
6763
6915
|
'bg-theme-base border-b border-theme-border',
|
|
6916
|
+
colIdx === 0 && col.key === 'name' && PHONE_PINNED_HEAD,
|
|
6764
6917
|
!isLastCol && 'border-r-subtle',
|
|
6765
6918
|
isSortable ? 'text-theme-text-secondary hover:text-theme-text-primary cursor-pointer select-none' : 'text-theme-text-secondary'
|
|
6766
6919
|
)}
|
|
@@ -6948,7 +7101,7 @@ export function ResourcesView({
|
|
|
6948
7101
|
})()}
|
|
6949
7102
|
{/* Resize handle with visible divider */}
|
|
6950
7103
|
<div
|
|
6951
|
-
className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center"
|
|
7104
|
+
className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center phone:hidden"
|
|
6952
7105
|
style={{ transform: 'translateX(50%)' , zIndex: 10 }}
|
|
6953
7106
|
onMouseDown={(e) => {
|
|
6954
7107
|
const th = e.currentTarget.parentElement!
|
|
@@ -7030,13 +7183,12 @@ export function ResourcesView({
|
|
|
7030
7183
|
open={tableActive && showBulkDeleteConfirm}
|
|
7031
7184
|
onClose={() => { setShowBulkDeleteConfirm(false); setBulkForceDelete(false) }}
|
|
7032
7185
|
onConfirm={() => {
|
|
7186
|
+
const submittedKeys = new Set(checkedItems.map(getResourceKey))
|
|
7187
|
+
setShowBulkDeleteConfirm(false)
|
|
7188
|
+
setBulkForceDelete(false)
|
|
7033
7189
|
onBulkDelete?.(checkedBulkItems, {
|
|
7034
7190
|
force: bulkForceDelete,
|
|
7035
|
-
onSuccess: () =>
|
|
7036
|
-
clearBulkSelection()
|
|
7037
|
-
setShowBulkDeleteConfirm(false)
|
|
7038
|
-
setBulkForceDelete(false)
|
|
7039
|
-
},
|
|
7191
|
+
onSuccess: () => setCheckedResources(current => new Set([...current].filter(key => !submittedKeys.has(key)))),
|
|
7040
7192
|
})
|
|
7041
7193
|
}}
|
|
7042
7194
|
title={`Delete ${checkedItems.length} ${selectedKind.kind}${checkedItems.length > 1 ? 's' : ''}?`}
|
|
@@ -7047,6 +7199,7 @@ export function ResourcesView({
|
|
|
7047
7199
|
isLoading={isBulkDeleting}
|
|
7048
7200
|
isClosable
|
|
7049
7201
|
>
|
|
7202
|
+
<CrossplaneDeleteNotice resources={checkedItems} force={bulkForceDelete} />
|
|
7050
7203
|
<label className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
7051
7204
|
<input
|
|
7052
7205
|
type="checkbox"
|
|
@@ -7203,8 +7356,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7203
7356
|
<td
|
|
7204
7357
|
onClick={onClick}
|
|
7205
7358
|
onMouseEnter={onMouseEnter}
|
|
7206
|
-
style={COMPARE_COLUMN_STYLE}
|
|
7207
|
-
className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight)}
|
|
7359
|
+
style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, showCheckbox, 'compare') }}
|
|
7360
|
+
className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight, PHONE_PINNED_CELL)}
|
|
7208
7361
|
>
|
|
7209
7362
|
{pickedSide ? (
|
|
7210
7363
|
<span
|
|
@@ -7226,7 +7379,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7226
7379
|
)}
|
|
7227
7380
|
{showCheckbox && (
|
|
7228
7381
|
<td
|
|
7229
|
-
|
|
7382
|
+
style={phonePinnedLeft(compareMode, showCheckbox, 'checkbox')}
|
|
7383
|
+
className={clsx('border-b-subtle text-center px-0 py-3 w-10 cursor-pointer transition-colors', rowHighlight, PHONE_PINNED_CELL)}
|
|
7230
7384
|
onClick={(e) => { e.stopPropagation(); onRowCheckToggle?.(resource) }}
|
|
7231
7385
|
>
|
|
7232
7386
|
<input
|
|
@@ -7237,16 +7391,18 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
|
|
|
7237
7391
|
/>
|
|
7238
7392
|
</td>
|
|
7239
7393
|
)}
|
|
7240
|
-
{columns.map((col) => (
|
|
7394
|
+
{columns.map((col, colIdx) => (
|
|
7241
7395
|
<td
|
|
7242
7396
|
key={col.key}
|
|
7243
7397
|
onClick={cellsAreClickable ? onClick : undefined}
|
|
7244
7398
|
onMouseEnter={onMouseEnter}
|
|
7399
|
+
style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, showCheckbox, 'name') : undefined}
|
|
7245
7400
|
className={clsx(
|
|
7246
7401
|
'px-4 py-3 border-b-subtle transition-colors',
|
|
7247
7402
|
cellsAreClickable && 'cursor-pointer',
|
|
7248
7403
|
col.key !== 'status' && 'overflow-hidden truncate',
|
|
7249
7404
|
rowHighlight,
|
|
7405
|
+
colIdx === 0 && col.key === 'name' && PHONE_PINNED_CELL,
|
|
7250
7406
|
)}
|
|
7251
7407
|
>
|
|
7252
7408
|
<CellContent
|
|
@@ -7325,11 +7481,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7325
7481
|
if (column === 'name') {
|
|
7326
7482
|
const isTerminating = !!meta.deletionTimestamp
|
|
7327
7483
|
const nameClass = clsx('text-sm font-medium truncate block', isTerminating ? 'text-theme-text-tertiary line-through' : 'text-theme-text-primary')
|
|
7484
|
+
const pauseState = isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged' ? getCrossplanePauseState(resource) : null
|
|
7328
7485
|
const audit = auditBadges?.[`${meta.namespace || ''}/${meta.name}`]
|
|
7329
7486
|
const auditTotal = audit ? audit.danger + audit.warning : 0
|
|
7330
7487
|
return (
|
|
7331
7488
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
7332
|
-
<Tooltip content={meta.name} wrapperClassName={findingMatch ? 'min-w-0' : undefined}>
|
|
7489
|
+
<Tooltip content={meta.name} wrapperClassName={findingMatch || pauseState ? 'min-w-0' : undefined}>
|
|
7333
7490
|
{nameHref ? (
|
|
7334
7491
|
<a
|
|
7335
7492
|
href={nameHref}
|
|
@@ -7344,6 +7501,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7344
7501
|
)}
|
|
7345
7502
|
</Tooltip>
|
|
7346
7503
|
<CopyNameButton name={meta.name} />
|
|
7504
|
+
<CrossplanePauseChip state={pauseState} compact />
|
|
7347
7505
|
{findingMatch && (
|
|
7348
7506
|
<Tooltip content={findingMatch.text}>
|
|
7349
7507
|
<span className="shrink-0 text-xs italic text-theme-text-tertiary cursor-help">
|
|
@@ -7392,6 +7550,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7392
7550
|
|
|
7393
7551
|
// Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
|
|
7394
7552
|
const kindLower = normalizeKindToPlural(kind, group)
|
|
7553
|
+
if (column === 'status' && isLikelyCrossplaneMRGroup(kind, group ?? '') && isManagedResource(resource)) {
|
|
7554
|
+
return <ManagedResourceCell resource={resource} column={column} />
|
|
7555
|
+
}
|
|
7395
7556
|
if (group && !hasCuratedColumns(kind, group)) {
|
|
7396
7557
|
return <GenericCell resource={resource} column={column} />
|
|
7397
7558
|
}
|
|
@@ -7418,6 +7579,14 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7418
7579
|
return <KyvernoCleanupPolicyCell resource={resource} column={column} />
|
|
7419
7580
|
}
|
|
7420
7581
|
|
|
7582
|
+
if (isApiGroup(resource.apiVersion, 'opentelemetry.io') && ['opentelemetrycollectors', 'instrumentations'].includes(kindLower)) {
|
|
7583
|
+
if (column === 'status') return <GenericCell resource={resource} column={column} />
|
|
7584
|
+
return <span className="text-sm text-theme-text-secondary break-all">{getOpenTelemetryColumnValue(resource, column) || '-'}</span>
|
|
7585
|
+
}
|
|
7586
|
+
if (isApiGroup(resource.apiVersion, 'flagger.app') && ['canaries', 'metrictemplates'].includes(kindLower)) {
|
|
7587
|
+
if (column === 'status') return <GenericCell resource={resource} column={column} />
|
|
7588
|
+
return <span className="text-sm text-theme-text-secondary break-all">{getFlaggerColumnValue(resource, column) || '-'}</span>
|
|
7589
|
+
}
|
|
7421
7590
|
switch (kindLower) {
|
|
7422
7591
|
case 'pods':
|
|
7423
7592
|
return <PodCell resource={resource} column={column} />
|
|
@@ -7977,6 +8146,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7977
8146
|
function GenericCell({ resource, column }: { resource: any; column: string }) {
|
|
7978
8147
|
switch (column) {
|
|
7979
8148
|
case 'status': {
|
|
8149
|
+
if (isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged') {
|
|
8150
|
+
return <CrossplaneStatusCell resource={resource} />
|
|
8151
|
+
}
|
|
7980
8152
|
const derived = getGenericResourceStatus(resource)
|
|
7981
8153
|
if (!derived) return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
7982
8154
|
return (
|
|
@@ -108,8 +108,8 @@ describe('curated column ownership', () => {
|
|
|
108
108
|
// Brace-counting, not parsing: assert the extraction actually saw the whole
|
|
109
109
|
// table, or a silently-missed key would let the guard above pass vacuously.
|
|
110
110
|
it('extracted every curated key', () => {
|
|
111
|
-
expect(CURATED.length).toBe(
|
|
112
|
-
expect(OWNED.size).toBe(
|
|
111
|
+
expect(CURATED.length).toBe(244)
|
|
112
|
+
expect(OWNED.size).toBe(206)
|
|
113
113
|
expect(CURATED).toContain('pods')
|
|
114
114
|
expect(CURATED).toContain('crossplanemanagedresources')
|
|
115
115
|
expect([...OWNED]).toContain('awsmanagedcontrolplanes')
|
|
@@ -166,7 +166,7 @@ describe('every declaration resolves', () => {
|
|
|
166
166
|
.map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
|
|
167
167
|
|
|
168
168
|
it('covers the whole ownership table', () => {
|
|
169
|
-
expect(DECLARED.length).toBe(
|
|
169
|
+
expect(DECLARED.length).toBe(206)
|
|
170
170
|
})
|
|
171
171
|
|
|
172
172
|
it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
|