@skyhook-io/k8s-ui 1.16.2 → 1.17.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.
Files changed (152) hide show
  1. package/package.json +3 -3
  2. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  3. package/src/components/applications/ApplicationDetail.tsx +17 -15
  4. package/src/components/checks/types.ts +2 -13
  5. package/src/components/cnpg/CNPGBackupSummary.tsx +223 -0
  6. package/src/components/cnpg/CNPGClusterSummary.tsx +222 -0
  7. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +264 -0
  8. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +80 -0
  9. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +176 -0
  10. package/src/components/cnpg/CNPGObjectSummary.test.tsx +236 -0
  11. package/src/components/cnpg/CNPGPoolerSummary.tsx +70 -0
  12. package/src/components/cnpg/CNPGSharedSummary.tsx +88 -0
  13. package/src/components/cnpg/index.ts +14 -0
  14. package/src/components/cnpg/primitives.tsx +134 -0
  15. package/src/components/cnpg/relations.test.ts +304 -0
  16. package/src/components/cnpg/relations.ts +417 -0
  17. package/src/components/cnpg/workspace.test.ts +285 -0
  18. package/src/components/cnpg/workspace.ts +700 -0
  19. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  20. package/src/components/gitops/detail-helpers.ts +1 -1
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
  22. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
  23. package/src/components/issues/IssuesView.tsx +10 -1
  24. package/src/components/issues/issue-timing.ts +2 -2
  25. package/src/components/issues/issues.test.ts +1 -1
  26. package/src/components/issues/types.ts +24 -0
  27. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  28. package/src/components/logs/WorkloadLogsViewer.tsx +17 -3
  29. package/src/components/logs/useLogBuffer.test.ts +17 -0
  30. package/src/components/logs/useLogStream.ts +11 -0
  31. package/src/components/resources/ResourcesSidebar.test.tsx +76 -0
  32. package/src/components/resources/ResourcesSidebar.tsx +183 -11
  33. package/src/components/resources/ResourcesView.tsx +12 -9
  34. package/src/components/resources/index.ts +1 -1
  35. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  36. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  37. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
  38. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  39. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  42. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  43. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
  44. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +25 -0
  45. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +16 -7
  46. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
  47. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  48. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  49. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  50. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  51. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
  53. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
  54. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  55. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  56. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  57. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  58. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  59. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  60. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  61. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  62. package/src/components/resources/renderers/HPARenderer.tsx +2 -2
  63. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  64. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  65. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  66. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  67. package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
  68. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  69. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  70. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  71. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  73. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  75. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  76. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  77. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  78. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  79. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  80. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  81. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  82. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +21 -7
  84. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  85. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  86. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  87. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  88. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  89. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  90. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  91. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
  92. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  93. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  94. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  95. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  96. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  97. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  98. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  99. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  100. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  101. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +1 -1
  102. package/src/components/resources/resource-utils-cnpg.ts +3 -7
  103. package/src/components/resources/resource-utils-keda.ts +1 -7
  104. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  105. package/src/components/resources/resource-utils-velero.ts +0 -12
  106. package/src/components/resources/resource-utils.ts +17 -33
  107. package/src/components/resources/status-overclaim.test.ts +1 -1
  108. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  109. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  110. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  111. package/src/components/shared/ResourceActionsBar.tsx +9 -20
  112. package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
  113. package/src/components/shared/SetImageDialog.test.ts +75 -6
  114. package/src/components/shared/SetImageDialog.tsx +92 -84
  115. package/src/components/shared/index.ts +2 -1
  116. package/src/components/timeline/TimelineList.tsx +187 -38
  117. package/src/components/timeline/TimelineListFold.test.tsx +56 -0
  118. package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
  119. package/src/components/timeline/TimelineToolbarState.test.tsx +98 -0
  120. package/src/components/timeline/index.ts +1 -0
  121. package/src/components/timeline/timeline-filters.ts +7 -0
  122. package/src/components/topology/TopologyGraph.tsx +5 -3
  123. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  124. package/src/components/ui/ScheduleValue.tsx +3 -2
  125. package/src/components/ui/drawer-components.test.tsx +66 -1
  126. package/src/components/ui/drawer-components.tsx +43 -29
  127. package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
  128. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  129. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  130. package/src/components/workload/WorkloadView.tsx +216 -48
  131. package/src/components/workload/index.ts +1 -0
  132. package/src/components/workload/workload-logs-availability.test.ts +6 -0
  133. package/src/index.ts +3 -0
  134. package/src/types/capacity.ts +10 -0
  135. package/src/types/core.ts +32 -1
  136. package/src/utils/format.test.ts +40 -2
  137. package/src/utils/format.ts +5 -1
  138. package/src/utils/gateway-references.test.ts +17 -0
  139. package/src/utils/gateway-references.ts +29 -0
  140. package/src/utils/gitops-write-guard.test.ts +401 -0
  141. package/src/utils/gitops-write-guard.ts +509 -0
  142. package/src/utils/index.ts +2 -0
  143. package/src/utils/ingress-class-references.ts +22 -0
  144. package/src/utils/log-level.ts +14 -0
  145. package/src/utils/navigation.test.ts +13 -0
  146. package/src/utils/navigation.ts +5 -0
  147. package/src/utils/node-lifecycle.test.ts +36 -0
  148. package/src/utils/node-lifecycle.ts +77 -0
  149. package/src/utils/resource-hierarchy.test.ts +32 -0
  150. package/src/utils/resource-hierarchy.ts +28 -2
  151. package/src/utils/resource-refs.test.ts +23 -0
  152. package/src/utils/resource-refs.ts +30 -0
@@ -0,0 +1,77 @@
1
+ import type { HealthLevel } from '../components/resources/resource-utils'
2
+
3
+ const REMOVAL_ACTORS: Record<string, string> = {
4
+ ToBeDeletedByClusterAutoscaler: 'cluster autoscaler',
5
+ 'karpenter.sh/disrupted': 'Karpenter',
6
+ }
7
+
8
+ const NETWORK_UNAVAILABLE_GRACE_MS = 2 * 60_000
9
+
10
+ export const NODE_REMOVAL_WARNING_MS = 10 * 60_000
11
+ export const NODE_REMOVAL_CRITICAL_MS = 30 * 60_000
12
+
13
+ export interface NodeLifecycleState {
14
+ label: string
15
+ level: HealthLevel
16
+ removing: boolean
17
+ actor?: string
18
+ startedAt?: number
19
+ readinessFailed: boolean
20
+ delayed: boolean
21
+ problems: string[]
22
+ }
23
+
24
+ export type NodeLifecycleObservation = Omit<NodeLifecycleState, 'startedAt' | 'problems'> & {
25
+ startedAt?: string
26
+ problems?: string[]
27
+ }
28
+
29
+ export function getNodeLifecycle(node: any, now = Date.now()): NodeLifecycleState {
30
+ const ready = node?.status?.conditions?.find((condition: any) => condition.type === 'Ready')
31
+ const createdAt = Date.parse(node?.metadata?.creationTimestamp)
32
+ const validTime = (timestamp: number) => Number.isFinite(timestamp) && timestamp > 0 && timestamp <= now && (!Number.isFinite(createdAt) || timestamp >= createdAt) ? timestamp : undefined
33
+ let startedAt = validTime(Date.parse(node?.metadata?.deletionTimestamp))
34
+ let removing = Boolean(node?.metadata?.deletionTimestamp)
35
+ let actor: string | undefined
36
+ let candidate = false
37
+ for (const taint of node?.spec?.taints ?? []) {
38
+ if (taint.key === 'DeletionCandidateOfClusterAutoscaler' && taint.effect === 'PreferNoSchedule') candidate = true
39
+ if (taint.effect !== 'NoSchedule') continue
40
+ const source = REMOVAL_ACTORS[taint.key]
41
+ if (!source) continue
42
+ removing = true
43
+ actor = source
44
+ if (taint.key === 'ToBeDeletedByClusterAutoscaler' && /^\d+$/.test(taint.value ?? '')) {
45
+ const markerTime = validTime(Number(taint.value) * 1000)
46
+ if (markerTime !== undefined && (startedAt === undefined || markerTime < startedAt)) startedAt = markerTime
47
+ }
48
+ }
49
+ const readyTime = Date.parse(ready?.lastTransitionTime)
50
+ const readinessFailed = Boolean(ready && ready.status !== 'True' && !(removing && startedAt !== undefined && Number.isFinite(readyTime) && readyTime >= startedAt && readyTime <= now))
51
+ const delayed = removing && startedAt !== undefined && now - startedAt >= NODE_REMOVAL_WARNING_MS
52
+ let label = 'Unknown'
53
+ let level: HealthLevel = 'unknown'
54
+ if (removing) {
55
+ label = actor ? `Removing (${actor})` : 'Removing'
56
+ level = readinessFailed || (delayed && now - startedAt! >= NODE_REMOVAL_CRITICAL_MS) ? 'unhealthy' : delayed ? 'degraded' : 'neutral'
57
+ } else if (readinessFailed) {
58
+ label = 'NotReady'
59
+ level = 'unhealthy'
60
+ } else if (ready?.status === 'True') {
61
+ label = node?.spec?.unschedulable ? 'Cordoned' : candidate ? 'Scale-down candidate' : 'Ready'
62
+ level = node?.spec?.unschedulable ? 'degraded' : candidate ? 'neutral' : 'healthy'
63
+ }
64
+ const problems: string[] = []
65
+ if (removing && readinessFailed) label += ' · NotReady'
66
+ const problemLabels: Record<string, string> = { MemoryPressure: 'Memory pressure', DiskPressure: 'Disk pressure', PIDPressure: 'PID pressure', NetworkUnavailable: 'Network unavailable' }
67
+ for (const condition of node?.status?.conditions ?? []) {
68
+ if (condition.status !== 'True' || !problemLabels[condition.type]) continue
69
+ const conditionTime = Date.parse(condition.lastTransitionTime)
70
+ if (condition.type === 'NetworkUnavailable' && Number.isFinite(conditionTime) && conditionTime <= now && now - conditionTime < NETWORK_UNAVAILABLE_GRACE_MS) continue
71
+ problems.push(condition.type)
72
+ label += ` · ${problemLabels[condition.type]}`
73
+ if (condition.type !== 'NetworkUnavailable') level = 'unhealthy'
74
+ else if (level !== 'unhealthy') level = 'degraded'
75
+ }
76
+ return { label, level, removing, actor, startedAt, readinessFailed, delayed, problems }
77
+ }
@@ -1528,3 +1528,35 @@ describe('structural app members survive the window filter', () => {
1528
1528
  expect(isChildVisibleInWindow(stray, 0, 1000, { pinned: false, userExpanded: false })).toBe(false)
1529
1529
  })
1530
1530
  })
1531
+
1532
+ describe('uses edges in the timeline hierarchy', () => {
1533
+ const node = (kind: string, apiVersion: string, name: string) => ({
1534
+ id: `${kind.toLowerCase()}/team/${name}`, kind, name, status: 'healthy', data: { namespace: 'team', apiVersion },
1535
+ })
1536
+ const topology = {
1537
+ nodes: [
1538
+ node('HorizontalPodAutoscaler', 'autoscaling/v2', 'web'),
1539
+ node('Deployment', 'apps/v1', 'web'),
1540
+ node('Certificate', 'cert-manager.io/v1', 'tls'),
1541
+ node('Issuer', 'cert-manager.io/v1', 'ca'),
1542
+ ],
1543
+ edges: [
1544
+ { id: 'hpa', source: 'horizontalpodautoscaler/team/web', target: 'deployment/team/web', type: 'uses' },
1545
+ { id: 'issuer', source: 'certificate/team/tls', target: 'issuer/team/ca', type: 'uses' },
1546
+ ],
1547
+ } as unknown as Topology
1548
+ const events = [
1549
+ changeEvent('HorizontalPodAutoscaler', 'team', 'web', { apiVersion: 'autoscaling/v2' }),
1550
+ changeEvent('Deployment', 'team', 'web', { apiVersion: 'apps/v1' }),
1551
+ changeEvent('Certificate', 'team', 'tls', { apiVersion: 'cert-manager.io/v1' }),
1552
+ changeEvent('Issuer', 'team', 'ca', { apiVersion: 'cert-manager.io/v1' }),
1553
+ ]
1554
+
1555
+ it('files an autoscaler under its workload but not a dependent under its dependency', () => {
1556
+ const lanes = buildResourceHierarchy({ events, topology, grouping: 'owner' })
1557
+ const top = lanes.map(lane => `${lane.kind}/${lane.name}`).sort()
1558
+ expect(top).toEqual(['Certificate/tls', 'Deployment/web', 'Issuer/ca'])
1559
+ const deployment = lanes.find(lane => lane.kind === 'Deployment')!
1560
+ expect(deployment.children?.map(child => child.kind)).toEqual(['HorizontalPodAutoscaler'])
1561
+ })
1562
+ })
@@ -699,6 +699,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
699
699
 
700
700
  // Source 2: Topology edges (for Service→Deployment, Ingress→Service, ConfigMap→Deployment).
701
701
  if (topology?.edges) {
702
+ const topologyNodeById = new Map(topology.nodes?.map(node => [node.id, node]) ?? [])
702
703
  for (const edge of topology.edges) {
703
704
  const sourceId = topologyLaneIdByNodeId.get(edge.source)
704
705
  const targetId = topologyLaneIdByNodeId.get(edge.target)
@@ -777,8 +778,15 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
777
778
  }
778
779
  }
779
780
 
780
- // configures/uses/protects: ConfigMap→Deployment, HPA→Deployment, PDB→Deployment (target is parent)
781
- if (edge.type === 'configures' || edge.type === 'uses' || edge.type === 'protects') {
781
+ // configures/protects: ConfigMap→Deployment, PDB→Deployment (target is parent)
782
+ if (edge.type === 'configures' || edge.type === 'protects') {
783
+ link(sourceId, targetId, sourceExists)
784
+ }
785
+ // uses: only an autoscaler or a mounted claim belongs under the workload it
786
+ // serves. Other uses edges point from a dependent to what it depends on
787
+ // (Certificate→Issuer, Gateway→GatewayClass); nesting there would file
788
+ // every dependent under one shared dependency.
789
+ if (edge.type === 'uses' && attachesToTarget(topologyNodeById.get(edge.source))) {
782
790
  link(sourceId, targetId, sourceExists)
783
791
  }
784
792
  }
@@ -1468,3 +1476,21 @@ export function countEventsInHierarchy(lanes: ResourceLane[]): number {
1468
1476
  lanes.forEach(walk)
1469
1477
  return count
1470
1478
  }
1479
+
1480
+ // The uses-edge sources that attach to their target workload: autoscalers and
1481
+ // PersistentVolumeClaims, matched on exact API group.
1482
+ const ATTACHED_USES_SOURCES = new Set([
1483
+ 'autoscaling/HorizontalPodAutoscaler',
1484
+ 'autoscaling.k8s.io/VerticalPodAutoscaler',
1485
+ 'keda.sh/ScaledObject',
1486
+ 'keda.sh/ScaledJob',
1487
+ '/PersistentVolumeClaim',
1488
+ ])
1489
+
1490
+ function attachesToTarget(node: Topology['nodes'][number] | undefined): boolean {
1491
+ if (!node) return false
1492
+ const kind = topologyNodeResourceKind(node)
1493
+ const apiVersion = typeof node.data?.apiVersion === 'string' ? node.data.apiVersion : ''
1494
+ const group = canonicalResourceGroup(kind, apiVersionToGroup(apiVersion)) ?? ''
1495
+ return ATTACHED_USES_SOURCES.has(`${group}/${kind}`)
1496
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ResourceRef } from '../types/core'
3
+ import { dedupeResourceRefs, omitResourceRefs, resourceRefKey } from './resource-refs'
4
+
5
+ describe('resource reference identity', () => {
6
+ const core: ResourceRef = { kind: 'Service', namespace: 'team', name: 'app' }
7
+ const knative: ResourceRef = { ...core, group: 'serving.knative.dev' }
8
+
9
+ it('deduplicates core aliases while preserving an identically named custom resource', () => {
10
+ const refs = [core, { ...core, group: '' }, knative]
11
+ expect(dedupeResourceRefs(refs)).toEqual([core, knative])
12
+ expect(refs).toHaveLength(3)
13
+ expect(resourceRefKey(core)).not.toBe(resourceRefKey(knative))
14
+ })
15
+
16
+ it('omits only the exact mirrored identity without mutating either list', () => {
17
+ const refs = [core, knative]
18
+ const excluded = [{ ...core, group: '' }]
19
+ expect(omitResourceRefs(refs, excluded)).toEqual([knative])
20
+ expect(refs).toEqual([core, knative])
21
+ expect(excluded).toHaveLength(1)
22
+ })
23
+ })
@@ -0,0 +1,30 @@
1
+ import type { ResourceRef } from '../types/core'
2
+
3
+ /**
4
+ * resourceKey mirrors Go `audit.ResourceKey(group, kind, namespace, name)`:
5
+ * `group|Kind|namespace|name`. Group first because group and namespace can each
6
+ * independently be empty; `|` is delimiter-safe (K8s API groups follow
7
+ * DNS-subdomain rules and can't contain it).
8
+ */
9
+ export function resourceKey(group: string, kind: string, namespace: string, name: string): string {
10
+ return `${group}|${kind}|${namespace}|${name}`;
11
+ }
12
+
13
+ export function resourceRefKey(ref: ResourceRef): string {
14
+ return resourceKey(ref.group ?? '', ref.kind, ref.namespace, ref.name);
15
+ }
16
+
17
+ export function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
18
+ const seen = new Set<string>()
19
+ return refs.filter(ref => {
20
+ const key = resourceRefKey(ref)
21
+ if (seen.has(key)) return false
22
+ seen.add(key)
23
+ return true
24
+ })
25
+ }
26
+
27
+ export function omitResourceRefs(refs: ResourceRef[], excluded: ResourceRef[]): ResourceRef[] {
28
+ const excludedKeys = new Set(excluded.map(resourceRefKey))
29
+ return refs.filter(ref => !excludedKeys.has(resourceRefKey(ref)))
30
+ }