@skyhook-io/radar-app 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.
- package/package.json +5 -5
- package/src/App.tsx +60 -12
- package/src/RadarApp.tsx +27 -17
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.ts +168 -55
- package/src/api/cnpg.ts +95 -0
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +19 -1
- package/src/api/radarFeatures.ts +3 -0
- package/src/api/timelineSource.ts +1 -1
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/NoClusterAccessBanner.tsx +68 -0
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +18 -7
- package/src/components/capacity/CapacityView.test.tsx +31 -1
- package/src/components/capacity/shared.tsx +12 -21
- package/src/components/cnpg/CNPGClusterActivity.tsx +49 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +62 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +311 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +228 -0
- package/src/components/cnpg/CNPGDrawerTrail.tsx +37 -0
- package/src/components/cnpg/CNPGOperator.tsx +234 -0
- package/src/components/cnpg/CNPGOverview.tsx +307 -0
- package/src/components/cnpg/CNPGPooling.tsx +116 -0
- package/src/components/cnpg/CNPGProtection.tsx +320 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +143 -0
- package/src/components/cnpg/CNPGView.tsx +156 -0
- package/src/components/cnpg/paths.ts +13 -0
- package/src/components/cnpg/routes.test.ts +60 -0
- package/src/components/cnpg/routes.ts +112 -0
- package/src/components/cnpg/shared.tsx +290 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +88 -0
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/helm/ChartBrowser.tsx +9 -10
- package/src/components/helm/HelmCompareRoute.tsx +108 -115
- package/src/components/helm/HelmReleaseDrawer.tsx +26 -44
- package/src/components/helm/InstallWizard.tsx +5 -2
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/helm/TrackChartSourceDialog.tsx +10 -2
- package/src/components/helm/ValuesViewer.tsx +1 -1
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/resources/ResourceDetailDrawer.tsx +6 -0
- package/src/components/resources/ResourcesView.tsx +6 -38
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/SettingsDialog.tsx +122 -19
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.test.ts +47 -29
- package/src/components/timeline/TimelineList.tsx +60 -11
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +16 -2
- package/src/components/timeline/TimelineView.urlparams.test.ts +6 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/whats-new/WhatsNew.test.ts +107 -28
- package/src/components/whats-new/WhatsNew.tsx +146 -86
- package/src/components/whats-new/WhatsNewDialog.test.tsx +132 -10
- package/src/components/whats-new/releaseNotes.ts +155 -24
- package/src/components/workload/WorkloadView.test.ts +2 -1
- package/src/components/workload/WorkloadView.tsx +165 -294
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +9 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- package/src/hooks/useResourceCounts.ts +49 -0
- package/src/components/helm/RoleGatedPanel.tsx +0 -47
|
@@ -3,40 +3,48 @@ import { JobRenderer, JobSetRenderer } from '../resources/renderers/JobAdmission
|
|
|
3
3
|
import { RayClusterRenderer } from '../resources/renderers/RayClusterRenderer'
|
|
4
4
|
import { RayServiceRenderer } from '../resources/renderers/RayServiceRenderer'
|
|
5
5
|
import { KueueWorkloadRenderer } from '../resources/renderers/KueueWorkloadRenderer'
|
|
6
|
-
import { useMemo, useEffect, useCallback, useRef, useState } from 'react'
|
|
6
|
+
import { useMemo, useEffect, useCallback, useRef, useState, type ReactNode } from 'react'
|
|
7
7
|
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
8
|
-
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
8
|
+
import { Navigate, useNavigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
9
9
|
import { workloadPodAwaitsScheduling } from '../capacity/podDemandGate'
|
|
10
10
|
import { clsx } from 'clsx'
|
|
11
11
|
import { Terminal, Stethoscope } from 'lucide-react'
|
|
12
12
|
import {
|
|
13
13
|
WorkloadView as BaseWorkloadView,
|
|
14
|
+
isApiGroup,
|
|
14
15
|
EditableYamlView,
|
|
15
16
|
FetchResult,
|
|
16
17
|
Section,
|
|
17
18
|
type WorkloadTabType,
|
|
19
|
+
type WorkloadExtraTab,
|
|
18
20
|
type RendererOverrides,
|
|
19
21
|
type GitOpsOwnerRef,
|
|
20
|
-
type GitOpsStatus,
|
|
21
22
|
type HelmOwnerRef,
|
|
22
23
|
type AppRow,
|
|
23
24
|
type ResourceOwnershipContext,
|
|
24
25
|
type ServingResourceDetail,
|
|
25
26
|
type AuditFinding,
|
|
27
|
+
dedupeResourceRefs,
|
|
26
28
|
gitOpsRouteForOwner,
|
|
27
29
|
gitOpsOwnerFromRelationships,
|
|
28
|
-
getGitOpsResourceStatus,
|
|
29
30
|
isDiagnoseKind,
|
|
30
31
|
isRolloutKind,
|
|
31
32
|
canSetWorkloadImages,
|
|
32
33
|
isCoreBatchJob,
|
|
33
|
-
type
|
|
34
|
+
type SetImageOwnership,
|
|
35
|
+
SET_IMAGE_WRITES,
|
|
36
|
+
setImageWrites,
|
|
37
|
+
setImageSelectionKey,
|
|
38
|
+
gitOpsWriteEvidencePaths,
|
|
39
|
+
type SetImageSelection,
|
|
34
40
|
type WorkloadImageTarget,
|
|
35
41
|
} from '@skyhook-io/k8s-ui'
|
|
36
42
|
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
37
43
|
import { isJobSetV1Alpha2 } from '@skyhook-io/k8s-ui/components/resources/resource-utils-jobset-lws'
|
|
38
44
|
import type { SelectedResource, ResourceRef, Relationships, ResourceWithRelationships } from '../../types'
|
|
39
45
|
import { useHistoryPaging } from './historyPaging'
|
|
46
|
+
import { findInheritedGitOpsLookupRef, useResolvedGitOpsOwner } from '../../hooks/useResolvedGitOpsOwner'
|
|
47
|
+
import { useGitOpsWriteGuard } from '../../hooks/useGitOpsWriteGuard'
|
|
40
48
|
import {
|
|
41
49
|
kindToPlural,
|
|
42
50
|
kindToPluralWithGroup,
|
|
@@ -95,6 +103,7 @@ import { RightsizingPanel } from '../resource/RightsizingStrip'
|
|
|
95
103
|
import { WorkloadCostTab } from '../cost/WorkloadCostTab'
|
|
96
104
|
import { isOpenCostWorkloadKind } from '../cost/kinds'
|
|
97
105
|
import { isRadarFeatureUnsupported } from '../../api/radarFeatures'
|
|
106
|
+
import { useTrafficAvailable } from '../../api/traffic'
|
|
98
107
|
import { useResourceAudit, useResourceIssues, useResources, useTrace, fetchTraceWithProbes, fetchInClusterCapability, runInClusterMerged } from '../../api/client'
|
|
99
108
|
import { AuditAlerts, getRadarUpgradeRequirement, ResourceIssuesSection, ReachabilityView, TraceSummary, InClusterConsentDialog, traceFingerprint, staticPollUnreliable, summarizeInClusterTests, type Trace as NetworkTrace, type InClusterCapability, inClusterConsentGiven, consentRequestRows } from '@skyhook-io/k8s-ui'
|
|
100
109
|
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
@@ -148,6 +157,9 @@ import {
|
|
|
148
157
|
CNPGSubscriptionRenderer,
|
|
149
158
|
} from '../resources/renderers/CNPGDeclarativeRenderer'
|
|
150
159
|
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
160
|
+
import { renderCNPGSummary } from '../cnpg/CNPGSummaryHost'
|
|
161
|
+
import { CNPGClusterLogs } from '../cnpg/CNPGClusterLogs'
|
|
162
|
+
import { cnpgDetailKindFor, cnpgDetailPath } from '../cnpg/routes'
|
|
151
163
|
import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
|
|
152
164
|
import { useDesktopDownload } from '../../hooks/useDesktopDownload'
|
|
153
165
|
import { useCompareLauncher } from '../compare/useCompareLauncher'
|
|
@@ -277,6 +289,17 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
277
289
|
)
|
|
278
290
|
}
|
|
279
291
|
|
|
292
|
+
const cnpgPlural = cnpgDetailKindFor(kind, group)
|
|
293
|
+
if (cnpgPlural) {
|
|
294
|
+
const params = new URLSearchParams(searchParams)
|
|
295
|
+
params.delete('apiGroup')
|
|
296
|
+
const tab = params.get('tab')
|
|
297
|
+
if (cnpgPlural === 'clusters' && (tab === 'timeline' || tab === 'events')) params.set('tab', 'activity')
|
|
298
|
+
const base = cnpgDetailPath({ plural: cnpgPlural, namespace, name })
|
|
299
|
+
const qs = params.toString()
|
|
300
|
+
return <Navigate replace to={qs ? `${base}?${qs}` : base} state={location.state} />
|
|
301
|
+
}
|
|
302
|
+
|
|
280
303
|
return (
|
|
281
304
|
<WorkloadView
|
|
282
305
|
kind={kind}
|
|
@@ -289,6 +312,10 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
289
312
|
)
|
|
290
313
|
}
|
|
291
314
|
|
|
315
|
+
// The kinds the traffic map draws as a node of their own: those that own
|
|
316
|
+
// pods, which it groups under them.
|
|
317
|
+
const TRAFFIC_WORKLOAD_PLURALS = new Set(['deployments', 'statefulsets', 'daemonsets', 'rollouts', 'cronjobs', 'jobs'])
|
|
318
|
+
|
|
292
319
|
// ============================================================================
|
|
293
320
|
// WORKLOAD VIEW WRAPPER — injects data fetching hooks
|
|
294
321
|
// ============================================================================
|
|
@@ -312,6 +339,8 @@ interface WorkloadViewProps {
|
|
|
312
339
|
initialTab?: 'detail' | 'yaml'
|
|
313
340
|
group?: string
|
|
314
341
|
pushTabHistory?: boolean
|
|
342
|
+
breadcrumb?: ReactNode
|
|
343
|
+
extraTabs?: WorkloadExtraTab[]
|
|
315
344
|
}
|
|
316
345
|
|
|
317
346
|
interface ImageTargetOwnershipContext {
|
|
@@ -322,7 +351,6 @@ interface ImageTargetOwnershipContext {
|
|
|
322
351
|
}
|
|
323
352
|
target: WorkloadImageTarget
|
|
324
353
|
response: ResourceWithRelationships<Record<string, unknown>>
|
|
325
|
-
inheritedResponse?: ResourceWithRelationships<Record<string, unknown>>
|
|
326
354
|
}
|
|
327
355
|
|
|
328
356
|
function useActionsBarProps(
|
|
@@ -536,11 +564,14 @@ export function WorkloadView({
|
|
|
536
564
|
...rest
|
|
537
565
|
}: WorkloadViewProps) {
|
|
538
566
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
567
|
+
const location = useLocation()
|
|
539
568
|
const apiKind = kindToPluralWithGroup(kindProp, rest.group ?? '')
|
|
540
569
|
const queryClient = useQueryClient()
|
|
541
570
|
const [imageTargetOwnership, setImageTargetOwnership] =
|
|
542
571
|
useState<ImageTargetOwnershipContext | null>(null)
|
|
543
572
|
const imageOwnershipRequestRef = useRef(0)
|
|
573
|
+
// The image guard's evidence is read only once the user opens the dialog.
|
|
574
|
+
const [imageGuardRequested, setImageGuardRequested] = useState(false)
|
|
544
575
|
|
|
545
576
|
// Tab state from URL query param — migrate legacy tab names
|
|
546
577
|
const rawTab = searchParams.get('tab')
|
|
@@ -561,9 +592,9 @@ export function WorkloadView({
|
|
|
561
592
|
} else {
|
|
562
593
|
params.set('tab', tab)
|
|
563
594
|
}
|
|
564
|
-
setSearchParams(params, { replace: opts?.replace ?? !pushTabHistory })
|
|
595
|
+
setSearchParams(params, { replace: opts?.replace ?? !pushTabHistory, state: location.state })
|
|
565
596
|
},
|
|
566
|
-
[pushTabHistory, searchParams, setSearchParams],
|
|
597
|
+
[pushTabHistory, searchParams, setSearchParams, location.state],
|
|
567
598
|
)
|
|
568
599
|
|
|
569
600
|
const selectedRunKey = searchParams.get('run') ?? ''
|
|
@@ -655,125 +686,33 @@ export function WorkloadView({
|
|
|
655
686
|
: null,
|
|
656
687
|
[apiKind, imageTargetOwnership, name, namespace],
|
|
657
688
|
)
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
()
|
|
684
|
-
nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
|
|
685
|
-
[relationships, resource?.metadata?.namespace, namespace],
|
|
686
|
-
)
|
|
687
|
-
const inheritedHelmOwner = useMemo(
|
|
688
|
-
() =>
|
|
689
|
-
nativeHelmOwnerFromRelationships(
|
|
690
|
-
inheritedGitOpsResponse.data?.relationships,
|
|
691
|
-
inheritedGitOpsResponse.data?.resource?.metadata?.namespace ?? namespace,
|
|
692
|
-
),
|
|
693
|
-
[
|
|
694
|
-
inheritedGitOpsResponse.data?.relationships,
|
|
695
|
-
inheritedGitOpsResponse.data?.resource?.metadata?.namespace,
|
|
696
|
-
namespace,
|
|
697
|
-
],
|
|
698
|
-
)
|
|
699
|
-
const rawGitopsOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
|
|
700
|
-
const gitOpsSourceResource = relationshipGitopsOwner
|
|
701
|
-
? resource
|
|
702
|
-
: inheritedGitOpsResponse.data?.resource
|
|
703
|
-
const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
|
|
704
|
-
const helmSourceResource = relationshipHelmOwner
|
|
705
|
-
? resource
|
|
706
|
-
: inheritedGitOpsResponse.data?.resource
|
|
707
|
-
const targetRelationshipGitopsOwner = useMemo(
|
|
708
|
-
() => gitOpsOwnerFromRelationships(activeImageTargetOwnership?.response.relationships),
|
|
709
|
-
[activeImageTargetOwnership?.response.relationships],
|
|
710
|
-
)
|
|
711
|
-
const targetInheritedGitopsOwner = useMemo(
|
|
712
|
-
() =>
|
|
713
|
-
gitOpsOwnerFromRelationships(
|
|
714
|
-
activeImageTargetOwnership?.inheritedResponse?.relationships,
|
|
715
|
-
),
|
|
716
|
-
[activeImageTargetOwnership?.inheritedResponse?.relationships],
|
|
717
|
-
)
|
|
718
|
-
const targetRawGitopsOwner = targetRelationshipGitopsOwner ?? targetInheritedGitopsOwner
|
|
719
|
-
const targetRelationshipHelmOwner = nativeHelmOwnerFromRelationships(
|
|
720
|
-
activeImageTargetOwnership?.response.relationships,
|
|
721
|
-
activeImageTargetOwnership?.target.namespace ?? namespace,
|
|
722
|
-
)
|
|
723
|
-
const targetInheritedHelmOwner = nativeHelmOwnerFromRelationships(
|
|
724
|
-
activeImageTargetOwnership?.inheritedResponse?.relationships,
|
|
725
|
-
activeImageTargetOwnership?.target.namespace ?? namespace,
|
|
726
|
-
)
|
|
727
|
-
const targetHelmOwner = targetRelationshipHelmOwner ?? targetInheritedHelmOwner
|
|
728
|
-
const shouldResolveArgoOwner =
|
|
729
|
-
(rawGitopsOwner?.tool === 'argocd' && !rawGitopsOwner.namespace) ||
|
|
730
|
-
(targetRawGitopsOwner?.tool === 'argocd' && !targetRawGitopsOwner.namespace)
|
|
731
|
-
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
732
|
-
enabled: shouldResolveArgoOwner,
|
|
689
|
+
const workloadOwnership = useResolvedGitOpsOwner({
|
|
690
|
+
kind: apiKind,
|
|
691
|
+
group: rest.group,
|
|
692
|
+
namespace,
|
|
693
|
+
name,
|
|
694
|
+
relationships,
|
|
695
|
+
resource,
|
|
696
|
+
})
|
|
697
|
+
const {
|
|
698
|
+
owner: gitopsOwner,
|
|
699
|
+
ownerObject: gitopsOwnerObject,
|
|
700
|
+
ownerVerified: gitOpsOwnerVerified,
|
|
701
|
+
ownerPending: gitOpsOwnerPending,
|
|
702
|
+
ownerSource: gitOpsOwnerSource,
|
|
703
|
+
ownerStatus: gitOpsOwnerStatus,
|
|
704
|
+
helmOwner,
|
|
705
|
+
helmSource: helmOwnerSource,
|
|
706
|
+
} = workloadOwnership
|
|
707
|
+
const imageTargetOwnershipResolution = useResolvedGitOpsOwner({
|
|
708
|
+
kind: activeImageTargetOwnership?.target.resource ?? '',
|
|
709
|
+
group: activeImageTargetOwnership?.target.group,
|
|
710
|
+
namespace: activeImageTargetOwnership?.target.namespace ?? '',
|
|
711
|
+
name: activeImageTargetOwnership?.target.name ?? '',
|
|
712
|
+
relationships: activeImageTargetOwnership?.response.relationships,
|
|
713
|
+
resource: activeImageTargetOwnership?.response.resource,
|
|
714
|
+
enabled: Boolean(activeImageTargetOwnership),
|
|
733
715
|
})
|
|
734
|
-
const gitopsOwner = useMemo(
|
|
735
|
-
() => resolveGitOpsOwner(rawGitopsOwner, argoApplications),
|
|
736
|
-
[rawGitopsOwner, argoApplications],
|
|
737
|
-
)
|
|
738
|
-
const targetGitopsOwner = useMemo(
|
|
739
|
-
() => resolveGitOpsOwner(targetRawGitopsOwner, argoApplications),
|
|
740
|
-
[argoApplications, targetRawGitopsOwner],
|
|
741
|
-
)
|
|
742
|
-
const gitopsOwnerGroup = gitopsOwner ? gitOpsOwnerGroup(gitopsOwner) : ''
|
|
743
|
-
const shouldFetchGitOpsOwner = Boolean(gitopsOwner?.namespace)
|
|
744
|
-
const gitopsOwnerQuery = useResource<any>(
|
|
745
|
-
shouldFetchGitOpsOwner ? gitopsOwner!.kind : '',
|
|
746
|
-
gitopsOwner?.namespace ?? '',
|
|
747
|
-
gitopsOwner?.name ?? '',
|
|
748
|
-
gitopsOwnerGroup,
|
|
749
|
-
)
|
|
750
|
-
const targetGitopsOwnerGroup = targetGitopsOwner ? gitOpsOwnerGroup(targetGitopsOwner) : ''
|
|
751
|
-
const shouldFetchTargetGitOpsOwner = Boolean(
|
|
752
|
-
activeImageTargetOwnership && targetGitopsOwner?.namespace,
|
|
753
|
-
)
|
|
754
|
-
const targetGitopsOwnerQuery = useResource<Record<string, unknown>>(
|
|
755
|
-
shouldFetchTargetGitOpsOwner ? targetGitopsOwner!.kind : '',
|
|
756
|
-
targetGitopsOwner?.namespace ?? '',
|
|
757
|
-
targetGitopsOwner?.name ?? '',
|
|
758
|
-
targetGitopsOwnerGroup,
|
|
759
|
-
{ enabled: shouldFetchTargetGitOpsOwner },
|
|
760
|
-
)
|
|
761
|
-
const gitOpsOwnerStatus = useMemo(
|
|
762
|
-
() => deriveGitOpsOwnerStatus(gitopsOwner, gitopsOwnerQuery.data),
|
|
763
|
-
[gitopsOwner, gitopsOwnerQuery.data],
|
|
764
|
-
)
|
|
765
|
-
const gitOpsOwnerVerified = Boolean(gitopsOwner?.namespace && gitopsOwnerQuery.data)
|
|
766
|
-
const gitOpsOwnerPending = Boolean(
|
|
767
|
-
gitopsOwner?.namespace && gitopsOwnerQuery.isLoading && !gitopsOwnerQuery.data,
|
|
768
|
-
)
|
|
769
|
-
const gitOpsOwnerSource = useMemo(
|
|
770
|
-
() => describeGitOpsOwnerSource(rawGitopsOwner, gitOpsSourceResource),
|
|
771
|
-
[rawGitopsOwner, gitOpsSourceResource],
|
|
772
|
-
)
|
|
773
|
-
const helmOwnerSource = useMemo(
|
|
774
|
-
() => describeHelmOwnerSource(helmOwner, helmSourceResource),
|
|
775
|
-
[helmOwner, helmSourceResource],
|
|
776
|
-
)
|
|
777
716
|
|
|
778
717
|
// Fetch topology for hierarchy building (only when expanded). Polled like
|
|
779
718
|
// useTrace's "drawer feeling live" pattern — without this, a resource
|
|
@@ -970,6 +909,23 @@ export function WorkloadView({
|
|
|
970
909
|
(path: string) => navigateRouter(path),
|
|
971
910
|
[navigateRouter],
|
|
972
911
|
)
|
|
912
|
+
// Offered for the kinds the traffic map draws as one node, and only once a
|
|
913
|
+
// traffic source is known to be available on this cluster. A Job something
|
|
914
|
+
// controls (a CronJob, a JobSet) is not one: the map groups its pods under
|
|
915
|
+
// that owner, so it is offered only once the Job is known to have none.
|
|
916
|
+
const isJob = apiKind.toLowerCase() === 'jobs'
|
|
917
|
+
const controlledJob = isJob && (!resource ||
|
|
918
|
+
(resource.metadata?.ownerReferences ?? []).some((ref: { controller?: boolean }) => ref.controller))
|
|
919
|
+
const trafficKind = !!namespace && TRAFFIC_WORKLOAD_PLURALS.has(apiKind.toLowerCase()) && !controlledJob
|
|
920
|
+
const trafficAvailable = useTrafficAvailable(trafficKind)
|
|
921
|
+
const openLiveTraffic = useCallback(() => {
|
|
922
|
+
const params = new URLSearchParams()
|
|
923
|
+
const namespaces = searchParams.get('namespaces')
|
|
924
|
+
if (namespaces) params.set('namespaces', namespaces)
|
|
925
|
+
params.set('focus', `${namespace}/${name}`)
|
|
926
|
+
navigateRouter({ pathname: '/traffic', search: params.toString() })
|
|
927
|
+
}, [navigateRouter, searchParams, namespace, name])
|
|
928
|
+
const offerLiveTraffic = trafficKind && trafficAvailable
|
|
973
929
|
// Drawer TraceSummary CTA → open the full resource view ON the Reachability tab.
|
|
974
930
|
// The generic onExpand navigates to the workload path but drops the query, so we
|
|
975
931
|
// navigate directly to that path WITH ?tab=reachability - the deeplink the
|
|
@@ -994,6 +950,10 @@ export function WorkloadView({
|
|
|
994
950
|
const loadImagesWithTargetOwnership = useCallback(
|
|
995
951
|
async (params: { kind: string; namespace: string; name: string }) => {
|
|
996
952
|
const request = ++imageOwnershipRequestRef.current
|
|
953
|
+
setImageGuardRequested(true)
|
|
954
|
+
// Each open and refresh re-reads the evidence: the owner's sync policy
|
|
955
|
+
// or ignore rules may have changed since the dialog last opened.
|
|
956
|
+
void queryClient.invalidateQueries({ queryKey: ['gitops-write-evidence'] })
|
|
997
957
|
const inventory = await baseActionsBarProps.onLoadImages!(params)
|
|
998
958
|
const targetDiffers =
|
|
999
959
|
inventory.target.resource.toLowerCase() !== params.kind.toLowerCase() ||
|
|
@@ -1039,14 +999,17 @@ export function WorkloadView({
|
|
|
1039
999
|
group: inventory.target.group,
|
|
1040
1000
|
},
|
|
1041
1001
|
)
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1002
|
+
// Warm the cache the target's ownership resolution reads, so the
|
|
1003
|
+
// dialog doesn't open on a still-pending inherited lookup. A failed
|
|
1004
|
+
// read surfaces there as unknown ownership, not as a failed load.
|
|
1005
|
+
if (inheritedRef) {
|
|
1006
|
+
await fetchRelationships(
|
|
1007
|
+
kindToPluralWithGroup(inheritedRef.kind, inheritedRef.group ?? ''),
|
|
1008
|
+
inheritedRef.namespace,
|
|
1009
|
+
inheritedRef.name,
|
|
1010
|
+
inheritedRef.group,
|
|
1011
|
+
).catch(() => undefined)
|
|
1012
|
+
}
|
|
1050
1013
|
if (request === imageOwnershipRequestRef.current) {
|
|
1051
1014
|
setImageTargetOwnership({
|
|
1052
1015
|
root: {
|
|
@@ -1056,52 +1019,74 @@ export function WorkloadView({
|
|
|
1056
1019
|
},
|
|
1057
1020
|
target: inventory.target,
|
|
1058
1021
|
response,
|
|
1059
|
-
inheritedResponse,
|
|
1060
1022
|
})
|
|
1061
1023
|
}
|
|
1062
1024
|
return inventory
|
|
1063
1025
|
},
|
|
1064
1026
|
[baseActionsBarProps.onLoadImages, queryClient, setImageTargetOwnership],
|
|
1065
1027
|
)
|
|
1066
|
-
const
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1028
|
+
const imageOwnershipSource = activeImageTargetOwnership ? imageTargetOwnershipResolution : workloadOwnership
|
|
1029
|
+
const imageGuardTarget = activeImageTargetOwnership
|
|
1030
|
+
? {
|
|
1031
|
+
kind: activeImageTargetOwnership.target.kind,
|
|
1032
|
+
group: activeImageTargetOwnership.target.group,
|
|
1033
|
+
namespace: activeImageTargetOwnership.target.namespace,
|
|
1034
|
+
name: activeImageTargetOwnership.target.name,
|
|
1035
|
+
}
|
|
1036
|
+
: { kind: resource?.kind ?? pluralToKind(apiKind), group: effectiveGroup ?? '', namespace, name }
|
|
1037
|
+
// Once the dialog lists the containers, ask about each by name (so the
|
|
1038
|
+
// evidence can't predate the list) and judge the ones being changed.
|
|
1039
|
+
const [imageSelection, setImageSelection] = useState<SetImageSelection | null>(null)
|
|
1040
|
+
const imageWrites = useMemo(
|
|
1041
|
+
() =>
|
|
1042
|
+
imageSelection
|
|
1043
|
+
? setImageWrites(imageSelection.changed.length ? imageSelection.changed : imageSelection.containers)
|
|
1044
|
+
: SET_IMAGE_WRITES,
|
|
1045
|
+
[imageSelection],
|
|
1046
|
+
)
|
|
1047
|
+
const imageEvidencePaths = useMemo(
|
|
1048
|
+
() => (imageSelection ? gitOpsWriteEvidencePaths(setImageWrites(imageSelection.containers)) : undefined),
|
|
1049
|
+
[imageSelection],
|
|
1050
|
+
)
|
|
1051
|
+
const { guard: imageGuard, ownership: imageGuardOwnership } = useGitOpsWriteGuard({
|
|
1052
|
+
target: imageGuardTarget,
|
|
1053
|
+
writes: imageWrites,
|
|
1054
|
+
evidencePaths: imageEvidencePaths,
|
|
1055
|
+
ownership: imageOwnershipSource,
|
|
1056
|
+
relationshipsUnavailable: activeImageTargetOwnership
|
|
1057
|
+
? !activeImageTargetOwnership.response.relationships
|
|
1058
|
+
: Boolean(resource) && !relationships,
|
|
1059
|
+
enabled: imageGuardRequested,
|
|
1060
|
+
})
|
|
1061
|
+
const {
|
|
1062
|
+
owner: imageOwner,
|
|
1063
|
+
helmOwner: imageHelmOwner,
|
|
1064
|
+
ownerVerified: imageOwnerVerified,
|
|
1065
|
+
} = imageGuardOwnership
|
|
1066
|
+
const imageOwnership = useMemo<SetImageOwnership | undefined>(() => {
|
|
1067
|
+
if (!imageGuard) return undefined
|
|
1068
|
+
const owner = imageOwner
|
|
1069
|
+
const helmOwner = imageHelmOwner
|
|
1070
|
+
return {
|
|
1071
|
+
guard: imageGuard,
|
|
1072
|
+
onSelectionChange: setImageSelection,
|
|
1073
|
+
selectionKey: setImageSelectionKey(imageSelection),
|
|
1074
|
+
onOpenOwner: owner
|
|
1075
|
+
? imageOwnerVerified
|
|
1076
|
+
? () => handleOpenGitOpsResource(owner)
|
|
1077
|
+
: undefined
|
|
1078
|
+
: helmOwner
|
|
1079
|
+
? () => handleOpenHelmRelease(helmOwner)
|
|
1084
1080
|
: undefined,
|
|
1085
|
-
})
|
|
1086
|
-
}
|
|
1087
|
-
if (imageHelmOwner) {
|
|
1088
|
-
sources.push({
|
|
1089
|
-
type: 'Helm',
|
|
1090
|
-
label: `${imageHelmOwner.namespace}/${imageHelmOwner.name}`,
|
|
1091
|
-
onOpen: () => handleOpenHelmRelease(imageHelmOwner),
|
|
1092
|
-
})
|
|
1093
1081
|
}
|
|
1094
|
-
return sources
|
|
1095
1082
|
}, [
|
|
1096
1083
|
handleOpenGitOpsResource,
|
|
1097
1084
|
handleOpenHelmRelease,
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
imageGitopsOwnerData,
|
|
1085
|
+
imageGuard,
|
|
1086
|
+
imageOwner,
|
|
1101
1087
|
imageHelmOwner,
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
inheritedGitOpsResponse.isPending,
|
|
1088
|
+
imageOwnerVerified,
|
|
1089
|
+
imageSelection,
|
|
1105
1090
|
])
|
|
1106
1091
|
const actionsBarProps = useMemo(
|
|
1107
1092
|
() => ({
|
|
@@ -1111,12 +1096,12 @@ export function WorkloadView({
|
|
|
1111
1096
|
: undefined,
|
|
1112
1097
|
onCompareTo,
|
|
1113
1098
|
onCompareAcrossClusters,
|
|
1114
|
-
|
|
1099
|
+
imageOwnership,
|
|
1115
1100
|
}),
|
|
1116
1101
|
[
|
|
1117
1102
|
baseActionsBarProps,
|
|
1118
1103
|
loadImagesWithTargetOwnership,
|
|
1119
|
-
|
|
1104
|
+
imageOwnership,
|
|
1120
1105
|
onCompareTo,
|
|
1121
1106
|
onCompareAcrossClusters,
|
|
1122
1107
|
],
|
|
@@ -1264,6 +1249,9 @@ export function WorkloadView({
|
|
|
1264
1249
|
onSelectRun={handleSelectedRunChange}
|
|
1265
1250
|
/>
|
|
1266
1251
|
)}
|
|
1252
|
+
renderSummary={({ apiKind: ak, namespace: ns, name: n, resource: res, context, onNavigate }) =>
|
|
1253
|
+
renderCNPGSummary({ apiKind: ak, namespace: ns, name: n, group: effectiveGroup, resource: res, context, onNavigate })
|
|
1254
|
+
}
|
|
1267
1255
|
renderExpandedOverview={({ kind: k, apiKind, namespace: ns, name: n, resource: res }) =>
|
|
1268
1256
|
supportsBatchExecution(k, apiKind, effectiveGroup, res?.apiVersion) &&
|
|
1269
1257
|
res ? (
|
|
@@ -1373,7 +1361,7 @@ export function WorkloadView({
|
|
|
1373
1361
|
)}
|
|
1374
1362
|
hasOperationalIssues={hasOperationalIssues}
|
|
1375
1363
|
operationalIssuesPending={issuesPending}
|
|
1376
|
-
onOpenGitOpsResource={
|
|
1364
|
+
onOpenGitOpsResource={gitopsOwnerObject ? handleOpenGitOpsResource : undefined}
|
|
1377
1365
|
resolvedGitOpsOwner={gitopsOwner}
|
|
1378
1366
|
gitOpsOwnerVerified={gitOpsOwnerVerified}
|
|
1379
1367
|
gitOpsOwnerPending={gitOpsOwnerPending}
|
|
@@ -1383,6 +1371,7 @@ export function WorkloadView({
|
|
|
1383
1371
|
helmOwnerSource={helmOwnerSource}
|
|
1384
1372
|
onOpenHelmRelease={handleOpenHelmRelease}
|
|
1385
1373
|
onNavigateGitOpsPath={handleNavigateGitOpsPath}
|
|
1374
|
+
onOpenLiveTraffic={offerLiveTraffic ? openLiveTraffic : undefined}
|
|
1386
1375
|
/>
|
|
1387
1376
|
<CreateResourceDialog
|
|
1388
1377
|
open={duplicateDialogOpen}
|
|
@@ -1406,7 +1395,7 @@ export function WorkloadView({
|
|
|
1406
1395
|
|
|
1407
1396
|
function collectServingRefs(relationships: Relationships | undefined): ResourceRef[] {
|
|
1408
1397
|
if (!relationships) return []
|
|
1409
|
-
return
|
|
1398
|
+
return dedupeResourceRefs([
|
|
1410
1399
|
...(relationships.services ?? []),
|
|
1411
1400
|
...(relationships.ingresses ?? []),
|
|
1412
1401
|
...(relationships.gateways ?? []),
|
|
@@ -1414,45 +1403,6 @@ function collectServingRefs(relationships: Relationships | undefined): ResourceR
|
|
|
1414
1403
|
])
|
|
1415
1404
|
}
|
|
1416
1405
|
|
|
1417
|
-
function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
1418
|
-
const seen = new Set<string>()
|
|
1419
|
-
return refs.filter((ref) => {
|
|
1420
|
-
const key = `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
1421
|
-
if (seen.has(key)) return false
|
|
1422
|
-
seen.add(key)
|
|
1423
|
-
return true
|
|
1424
|
-
})
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
function resolveGitOpsOwner(
|
|
1428
|
-
owner: GitOpsOwnerRef | null,
|
|
1429
|
-
argoApplications: any[] | undefined,
|
|
1430
|
-
): GitOpsOwnerRef | null {
|
|
1431
|
-
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
1432
|
-
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
1433
|
-
if (matches.length !== 1) return owner
|
|
1434
|
-
const namespace = matches[0]?.metadata?.namespace
|
|
1435
|
-
return namespace ? { ...owner, namespace } : owner
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
export function findInheritedGitOpsLookupRef(
|
|
1439
|
-
relationships: Relationships | undefined,
|
|
1440
|
-
directOwner: GitOpsOwnerRef | null,
|
|
1441
|
-
current: ResourceRef,
|
|
1442
|
-
): ResourceRef | null {
|
|
1443
|
-
if (directOwner) return null
|
|
1444
|
-
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
1445
|
-
(ref) => !gitOpsOwnerFromRelationships({ managedBy: [ref] }) && !isNativeHelmManager(ref),
|
|
1446
|
-
)
|
|
1447
|
-
const candidates = [
|
|
1448
|
-
relationships?.deployment,
|
|
1449
|
-
...inheritedManagerRefs,
|
|
1450
|
-
relationships?.owner,
|
|
1451
|
-
].filter(Boolean) as ResourceRef[]
|
|
1452
|
-
|
|
1453
|
-
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
1406
|
const POD_OWNERSHIP_WORKLOAD_KINDS = new Set([
|
|
1457
1407
|
'deployments',
|
|
1458
1408
|
'statefulsets',
|
|
@@ -1532,89 +1482,6 @@ function sameWorkload(
|
|
|
1532
1482
|
)
|
|
1533
1483
|
}
|
|
1534
1484
|
|
|
1535
|
-
function nativeHelmOwnerFromRelationships(
|
|
1536
|
-
relationships: Relationships | undefined,
|
|
1537
|
-
fallbackNamespace: string,
|
|
1538
|
-
): HelmOwnerRef | null {
|
|
1539
|
-
const ref = relationships?.managedBy?.[0]
|
|
1540
|
-
if (!ref || !isNativeHelmManager(ref)) return null
|
|
1541
|
-
return {
|
|
1542
|
-
namespace: ref.namespace || fallbackNamespace,
|
|
1543
|
-
name: ref.name,
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
1548
|
-
return (
|
|
1549
|
-
kindToPluralWithGroup(ref.kind, ref.group ?? '') ===
|
|
1550
|
-
kindToPluralWithGroup(current.kind, current.group ?? '') &&
|
|
1551
|
-
ref.namespace === current.namespace &&
|
|
1552
|
-
ref.name === current.name &&
|
|
1553
|
-
(ref.group ?? '') === (current.group ?? '')
|
|
1554
|
-
)
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function isNativeHelmManager(ref: ResourceRef): boolean {
|
|
1558
|
-
return ref.kind === 'HelmRelease' && ref.group !== 'helm.toolkit.fluxcd.io'
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any): string | null {
|
|
1562
|
-
if (!owner || !resource) return null
|
|
1563
|
-
const labels = resource.metadata?.labels ?? {}
|
|
1564
|
-
const annotations = resource.metadata?.annotations ?? {}
|
|
1565
|
-
|
|
1566
|
-
if (owner.tool === 'fluxcd') {
|
|
1567
|
-
const nameKey =
|
|
1568
|
-
owner.kind === 'helmreleases'
|
|
1569
|
-
? 'helm.toolkit.fluxcd.io/name'
|
|
1570
|
-
: 'kustomize.toolkit.fluxcd.io/name'
|
|
1571
|
-
const nsKey =
|
|
1572
|
-
owner.kind === 'helmreleases'
|
|
1573
|
-
? 'helm.toolkit.fluxcd.io/namespace'
|
|
1574
|
-
: 'kustomize.toolkit.fluxcd.io/namespace'
|
|
1575
|
-
if (labels[nameKey] || labels[nsKey]) {
|
|
1576
|
-
return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
const trackingID = annotations['argocd.argoproj.io/tracking-id']
|
|
1581
|
-
if (trackingID) return `argocd.argoproj.io/tracking-id=${trackingID}`
|
|
1582
|
-
const argoInstance = labels['argocd.argoproj.io/instance']
|
|
1583
|
-
if (argoInstance) return `argocd.argoproj.io/instance=${argoInstance}`
|
|
1584
|
-
return null
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function describeHelmOwnerSource(owner: HelmOwnerRef | null, resource: any): string | null {
|
|
1588
|
-
if (!owner || !resource) return null
|
|
1589
|
-
const annotations = resource.metadata?.annotations ?? {}
|
|
1590
|
-
const releaseName = annotations['meta.helm.sh/release-name']
|
|
1591
|
-
const releaseNamespace = annotations['meta.helm.sh/release-namespace']
|
|
1592
|
-
if (releaseName || releaseNamespace) {
|
|
1593
|
-
return `meta.helm.sh/release-name=${releaseName ?? ''}, meta.helm.sh/release-namespace=${releaseNamespace ?? ''}`
|
|
1594
|
-
}
|
|
1595
|
-
return null
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
function gitOpsOwnerGroup(owner: GitOpsOwnerRef): string {
|
|
1599
|
-
if (owner.tool === 'argocd') return 'argoproj.io'
|
|
1600
|
-
if (owner.kind === 'kustomizations') return 'kustomize.toolkit.fluxcd.io'
|
|
1601
|
-
return 'helm.toolkit.fluxcd.io'
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function deriveGitOpsOwnerStatus(owner: GitOpsOwnerRef | null, resource: any): GitOpsStatus | null {
|
|
1605
|
-
if (!owner || !resource || !hasGitOpsStatusPayload(owner, resource)) return null
|
|
1606
|
-
return getGitOpsResourceStatus(owner.kind, resource)
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
1610
|
-
if (owner.kind === 'applications') {
|
|
1611
|
-
const status = resource.status ?? {}
|
|
1612
|
-
return Boolean(status.sync?.status || status.health?.status || status.operationState?.phase)
|
|
1613
|
-
}
|
|
1614
|
-
if (resource.spec?.suspend === true) return true
|
|
1615
|
-
return Array.isArray(resource.status?.conditions) && resource.status.conditions.length > 0
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
1485
|
// ============================================================================
|
|
1619
1486
|
// LOGS TAB — platform-specific (uses data-fetching hooks)
|
|
1620
1487
|
// ============================================================================
|
|
@@ -1672,6 +1539,10 @@ function LogsTabContent({
|
|
|
1672
1539
|
)
|
|
1673
1540
|
}
|
|
1674
1541
|
|
|
1542
|
+
if (kind === 'Cluster' && isApiGroup(resource?.apiVersion, 'postgresql.cnpg.io')) {
|
|
1543
|
+
return <CNPGClusterLogs namespace={namespace} name={name} />
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1675
1546
|
// Workload kinds with stable pod selectors use the aggregated workload logs viewer
|
|
1676
1547
|
if (WORKLOAD_LOG_KINDS.has(kind) && (kind !== 'Job' || isCoreBatchJob(apiKind, group))) {
|
|
1677
1548
|
return (
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A note from the host about how far back the timeline goes. The host owns
|
|
6
|
+
* the wording and decides when there is one. Standalone Radar serves
|
|
7
|
+
* everything it holds and never sets it.
|
|
8
|
+
*/
|
|
9
|
+
const TimelineRetentionNoteContext = createContext<ReactNode>(undefined)
|
|
10
|
+
|
|
11
|
+
export const TimelineRetentionNoteProvider = TimelineRetentionNoteContext.Provider
|
|
12
|
+
|
|
13
|
+
export function useTimelineRetentionNote(): ReactNode {
|
|
14
|
+
return useContext(TimelineRetentionNoteContext)
|
|
15
|
+
}
|