@skyhook-io/k8s-ui 1.16.3 → 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.
- package/package.json +1 -1
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/primitives.tsx +3 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +5 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesView.tsx +7 -8
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg.ts +0 -4
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- package/src/components/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +120 -43
- package/src/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- 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/
|
|
781
|
-
if (edge.type === 'configures' || edge.type === '
|
|
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
|
+
}
|