@skyhook-io/radar-app 1.17.0 → 1.17.1
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 +1 -1
- package/package.json +3 -3
- package/src/App.tsx +125 -87
- package/src/api/actions.errorCode.test.ts +40 -0
- package/src/api/actions.test.ts +48 -0
- package/src/api/actions.ts +79 -0
- package/src/api/apiResources.navigation.test.tsx +232 -0
- package/src/api/apiResources.ts +46 -3
- package/src/api/client.helmSource.test.ts +39 -0
- package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
- package/src/api/client.namespaceSwitch.test.ts +107 -0
- package/src/api/client.ts +266 -39
- package/src/api/cnpg-ha.live.test.ts +140 -0
- package/src/api/cnpg-ha.test.ts +78 -0
- package/src/api/cnpg-ha.ts +19 -0
- package/src/api/cnpg-history.ts +123 -0
- package/src/api/cnpg-inspect.ts +102 -0
- package/src/api/cnpg-protection.ts +112 -0
- package/src/api/cnpg-recovery.test.tsx +60 -0
- package/src/api/cnpg-recovery.ts +262 -0
- package/src/api/cnpg-sessions.ts +175 -0
- package/src/api/cnpg-storage.ts +138 -0
- package/src/api/cnpg.legacy-reads.test.ts +43 -0
- package/src/api/cnpg.pooler-runtime.test.ts +15 -0
- package/src/api/cnpg.ts +428 -10
- package/src/api/radarFeatures.render.test.tsx +47 -3
- package/src/api/radarFeatures.ts +4 -1
- package/src/api/reviewedActionMutation.test.tsx +164 -0
- package/src/api/reviewedActionMutation.ts +66 -0
- package/src/api/timelineSource.test.ts +9 -0
- package/src/api/timelineSource.ts +8 -0
- package/src/components/audit/AuditView.tsx +9 -0
- package/src/components/capacity/CapacityActivity.tsx +3 -31
- package/src/components/capacity/CapacityDemand.tsx +4 -36
- package/src/components/capacity/CapacityOverview.tsx +217 -56
- package/src/components/capacity/CapacityPoolDetail.tsx +6 -55
- package/src/components/capacity/CapacityView.test.tsx +323 -0
- package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
- package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
- package/src/components/capacity/routes.ts +42 -0
- package/src/components/capacity/shared.tsx +11 -129
- package/src/components/capacity/workspaceRoute.ts +18 -0
- package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
- package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
- package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
- package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
- package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
- package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
- package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
- package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
- package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
- package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
- package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
- package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
- package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
- package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
- package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
- package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
- package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
- package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
- package/src/components/cnpg/CNPGOperator.tsx +78 -30
- package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
- package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
- package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
- package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
- package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
- package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
- package/src/components/cnpg/CNPGOverview.tsx +238 -63
- package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
- package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
- package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
- package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
- package/src/components/cnpg/CNPGPerformance.tsx +477 -0
- package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
- package/src/components/cnpg/CNPGPooling.tsx +97 -40
- package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
- package/src/components/cnpg/CNPGProtection.tsx +228 -122
- package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
- package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
- package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
- package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
- package/src/components/cnpg/CNPGStorage.tsx +619 -0
- package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
- package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
- package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
- package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
- package/src/components/cnpg/CNPGTrends.test.ts +38 -0
- package/src/components/cnpg/CNPGTrends.tsx +702 -0
- package/src/components/cnpg/CNPGView.tsx +34 -21
- package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
- package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
- package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
- package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
- package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
- package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
- package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
- package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
- package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
- package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
- package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
- package/src/components/cnpg/actions/actionModel.test.ts +111 -0
- package/src/components/cnpg/actions/actionModel.ts +127 -0
- package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
- package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
- package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
- package/src/components/cnpg/archivingRepair.test.ts +104 -0
- package/src/components/cnpg/archivingRepair.ts +145 -0
- package/src/components/cnpg/baseBackup.test.ts +36 -0
- package/src/components/cnpg/baseBackup.ts +36 -0
- package/src/components/cnpg/blocking.test.ts +59 -0
- package/src/components/cnpg/blocking.ts +131 -0
- package/src/components/cnpg/coverageText.test.ts +27 -0
- package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
- package/src/components/cnpg/creation/targetModel.test.ts +137 -0
- package/src/components/cnpg/creation/targetModel.ts +149 -0
- package/src/components/cnpg/fleetStatus.test.ts +46 -0
- package/src/components/cnpg/fleetStatus.ts +43 -0
- package/src/components/cnpg/formFields.ts +2 -0
- package/src/components/cnpg/grantText.test.tsx +11 -0
- package/src/components/cnpg/host.test.tsx +24 -0
- package/src/components/cnpg/host.tsx +101 -0
- package/src/components/cnpg/inspectModel.test.ts +120 -0
- package/src/components/cnpg/inspectModel.ts +140 -0
- package/src/components/cnpg/logicalSlots.test.ts +30 -0
- package/src/components/cnpg/logicalSlots.ts +29 -0
- package/src/components/cnpg/lsn.test.ts +40 -0
- package/src/components/cnpg/lsn.ts +43 -0
- package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
- package/src/components/cnpg/operations/model.test.ts +510 -0
- package/src/components/cnpg/operations/model.ts +499 -0
- package/src/components/cnpg/operations/presentation.test.ts +80 -0
- package/src/components/cnpg/operations/presentation.ts +84 -0
- package/src/components/cnpg/operations/store.ts +98 -0
- package/src/components/cnpg/operatorStatus.test.ts +134 -0
- package/src/components/cnpg/operatorStatus.ts +194 -0
- package/src/components/cnpg/paths.test.ts +54 -0
- package/src/components/cnpg/paths.ts +47 -4
- package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
- package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
- package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
- package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
- package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
- package/src/components/cnpg/protection/scheduleModel.ts +16 -0
- package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
- package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
- package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
- package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
- package/src/components/cnpg/recovery/restoreModel.ts +685 -0
- package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
- package/src/components/cnpg/refresh.test.ts +38 -0
- package/src/components/cnpg/refresh.ts +17 -0
- package/src/components/cnpg/refreshNotice.test.tsx +29 -0
- package/src/components/cnpg/routes.test.ts +34 -3
- package/src/components/cnpg/routes.ts +60 -48
- package/src/components/cnpg/runtimeAssessment.ts +198 -0
- package/src/components/cnpg/runtimeModel.test.ts +175 -0
- package/src/components/cnpg/runtimeModel.ts +196 -0
- package/src/components/cnpg/runtimeParts.test.tsx +17 -0
- package/src/components/cnpg/runtimeParts.tsx +79 -0
- package/src/components/cnpg/shared.test.tsx +59 -0
- package/src/components/cnpg/shared.tsx +100 -182
- package/src/components/cnpg/storageModel.test.ts +107 -0
- package/src/components/cnpg/storageModel.ts +101 -0
- package/src/components/cnpg/trendSamples.test.ts +148 -0
- package/src/components/cnpg/trendSamples.ts +222 -0
- package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
- package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
- package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
- package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
- package/src/components/cnpg/useCNPGNavigate.ts +21 -0
- package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
- package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
- package/src/components/cnpg/workspaceRoute.ts +23 -0
- package/src/components/cost/ApplicationCostTab.test.ts +7 -0
- package/src/components/cost/ApplicationCostTab.tsx +7 -7
- package/src/components/cost/CostView.test.ts +2 -1
- package/src/components/cost/CostView.tsx +0 -2
- package/src/components/cost/WorkloadCostTab.tsx +2 -4
- package/src/components/cost/errors.ts +3 -1
- package/src/components/cost/source.test.ts +10 -0
- package/src/components/cost/source.ts +5 -1
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
- package/src/components/dock/BottomDock.tsx +2 -0
- package/src/components/dock/TerminalTab.test.tsx +17 -0
- package/src/components/dock/TerminalTab.tsx +7 -3
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
- package/src/components/helm/HelmCompareRoute.tsx +57 -26
- package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
- package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
- package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
- package/src/components/helm/ValuesViewer.test.tsx +301 -0
- package/src/components/helm/ValuesViewer.tsx +169 -78
- package/src/components/home/ActivitySummary.tsx +2 -1
- package/src/components/home/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- package/src/components/issues/IssuesPane.render.test.tsx +122 -0
- package/src/components/issues/IssuesPane.test.ts +65 -1
- package/src/components/issues/IssuesPane.tsx +188 -25
- package/src/components/issues/uncheckedSources.ts +21 -0
- package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
- package/src/components/resource/PVCUsageBar.test.tsx +24 -0
- package/src/components/resource/PVCUsageBar.tsx +15 -6
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resources/CompositeRenderer.tsx +4 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
- package/src/components/resources/ResourcesView.tsx +26 -12
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +28 -15
- package/src/components/timeline/TimelineList.tsx +22 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineView.tsx +18 -45
- package/src/components/timeline/ViewModeToggle.tsx +20 -0
- package/src/components/timeline/useStickyError.test.tsx +47 -0
- package/src/components/timeline/useStickyError.ts +15 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
- package/src/components/ui/Markdown.test.tsx +65 -0
- package/src/components/ui/Markdown.tsx +144 -127
- package/src/components/whats-new/releaseNotes.ts +63 -1
- package/src/components/workload/WorkloadView.test.ts +10 -1
- package/src/components/workload/WorkloadView.tsx +27 -239
- package/src/components/workspace/layout.test.tsx +29 -0
- package/src/components/workspace/layout.tsx +259 -0
- package/src/components/workspace/table.ts +7 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/integrations/resourceHost.test.tsx +55 -0
- package/src/integrations/resourceHost.ts +99 -0
- package/src/integrations/resourceHosts.test.tsx +141 -0
- package/src/integrations/resourceHosts.tsx +119 -0
- package/src/integrations/workspaceRoute.ts +20 -0
- package/src/integrations/workspaceRoutes.test.ts +40 -0
- package/src/integrations/workspaceRoutes.ts +22 -0
- package/src/integrations/workspaceScreens.tsx +14 -0
- package/src/utils/drawer-trail.ts +35 -0
- package/src/utils/kindFilterStore.test.ts +147 -0
- package/src/utils/kindFilterStore.ts +91 -0
- package/src/utils/navigation.test.ts +13 -1
- package/src/utils/navigation.ts +39 -2
- package/src/utils/page-links.ts +21 -0
- package/src/utils/tabStorage.ts +40 -0
- package/src/utils/viewMemory.test.ts +78 -0
- package/src/utils/viewMemory.ts +54 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
|
|
2
2
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
-
import { Network, AlertTriangle,
|
|
4
|
+
import { Network, AlertTriangle, Boxes, X } from 'lucide-react'
|
|
5
5
|
import {
|
|
6
6
|
clampLensToSelection,
|
|
7
7
|
deriveLiveSelection,
|
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
useDebouncedValue,
|
|
22
22
|
} from '@skyhook-io/k8s-ui'
|
|
23
23
|
import { TimelineList } from './TimelineList'
|
|
24
|
+
import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
|
|
25
|
+
import { ViewModeToggle } from './ViewModeToggle'
|
|
26
|
+
import { useStickyError } from './useStickyError'
|
|
24
27
|
import type { ActivityFilterKey } from './TimelineList'
|
|
25
28
|
import { TimelineSwimlanes } from './TimelineSwimlanes'
|
|
26
29
|
import { RetainedTimelineScrubber, extendSelection, type ScrubberDomainInfo } from './RetainedTimelineScrubber'
|
|
@@ -275,6 +278,7 @@ interface TimelineViewProps {
|
|
|
275
278
|
requiresNamespaceFilter?: boolean
|
|
276
279
|
availableNamespaces?: { name: string }[]
|
|
277
280
|
onNamespaceSelect?: (ns: string) => void
|
|
281
|
+
onOpenDiagnostics?: () => void
|
|
278
282
|
}
|
|
279
283
|
|
|
280
284
|
export function resolveApplicationTimelineScope(searchParams: URLSearchParams, namespaces: string[]) {
|
|
@@ -293,7 +297,7 @@ export function resolveApplicationTimelineScope(searchParams: URLSearchParams, n
|
|
|
293
297
|
}
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
export function TimelineView({ namespaces, onResourceClick, initialViewMode, initialFilter, initialTimeRange, requiresNamespaceFilter, availableNamespaces, onNamespaceSelect }: TimelineViewProps) {
|
|
300
|
+
export function TimelineView({ namespaces, onResourceClick, initialViewMode, initialFilter, initialTimeRange, requiresNamespaceFilter, availableNamespaces, onNamespaceSelect, onOpenDiagnostics }: TimelineViewProps) {
|
|
297
301
|
// URL is the source of truth for every control below (deep-linkable +
|
|
298
302
|
// back/forward-restorable). Read on mount, written on user change.
|
|
299
303
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
@@ -683,7 +687,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
683
687
|
// Fetch all activity - zoom controls what's visible in the UI. This ring feeds
|
|
684
688
|
// the swimlanes and the local strip's histogram, so it also runs in list mode
|
|
685
689
|
// when that strip is shown; the list itself fetches its own 2000.
|
|
686
|
-
const { data: activity, isLoading, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
|
|
690
|
+
const { data: activity, isLoading, isFetching, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
|
|
687
691
|
namespaces: timelineNamespaces,
|
|
688
692
|
timeRange: 'all',
|
|
689
693
|
includeK8sEvents: true,
|
|
@@ -701,6 +705,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
701
705
|
fromMs: isRetained ? selection.fromMs : undefined,
|
|
702
706
|
toMs: isRetained ? selection.toMs : undefined,
|
|
703
707
|
})
|
|
708
|
+
const loadError = useStickyError(error, activity != null)
|
|
704
709
|
|
|
705
710
|
// Topology powers both swimlane hierarchy and application-scoped attribution.
|
|
706
711
|
const { data: rawTopology } = useTopology(timelineNamespaces, 'resources', {
|
|
@@ -869,13 +874,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
869
874
|
{/* Failing background polls with a loaded ring: keep the data, say
|
|
870
875
|
it's going stale. The full-screen error is reserved for no-data. */}
|
|
871
876
|
{isError && activity && (
|
|
872
|
-
<
|
|
873
|
-
<AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
|
|
874
|
-
Live updates are failing — the timeline may be stale.
|
|
875
|
-
<button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
|
|
876
|
-
Retry now
|
|
877
|
-
</button>
|
|
878
|
-
</div>
|
|
877
|
+
<TimelineStaleNote error={error} subject="timeline" onRetry={() => refetch()} />
|
|
879
878
|
)}
|
|
880
879
|
{isRetained ? (
|
|
881
880
|
<RetainedTimelineScrubber
|
|
@@ -967,29 +966,23 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
967
966
|
// a load failure. With a loaded ring on screen, a failing background poll
|
|
968
967
|
// must NOT blank it (gate on data before error); the stale-data banner
|
|
969
968
|
// below carries the warning instead.
|
|
970
|
-
if (
|
|
969
|
+
if (loadError && !activity) {
|
|
971
970
|
// Surface the server's own message — a generic "failed to load" would
|
|
972
971
|
// swallow whatever the hub said. "Try again" is a full resync: the
|
|
973
972
|
// retained source drops its delta cursor and reloads the whole ring.
|
|
974
|
-
const detail = error?.message?.trim()
|
|
975
973
|
return wrap(
|
|
976
974
|
<div className="flex-1 flex flex-col">
|
|
977
975
|
<div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
|
|
978
976
|
<div />
|
|
979
977
|
<ViewModeToggle viewMode={viewMode} onViewModeChange={setViewMode} />
|
|
980
978
|
</div>
|
|
981
|
-
<
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
>
|
|
989
|
-
<RefreshCw className="w-3.5 h-3.5" />
|
|
990
|
-
Try again
|
|
991
|
-
</button>
|
|
992
|
-
</div>
|
|
979
|
+
<TimelineLoadError
|
|
980
|
+
className="flex-1"
|
|
981
|
+
error={loadError}
|
|
982
|
+
retrying={isFetching}
|
|
983
|
+
onRetry={() => refetch()}
|
|
984
|
+
onOpenDiagnostics={onOpenDiagnostics}
|
|
985
|
+
/>
|
|
993
986
|
</div>
|
|
994
987
|
)
|
|
995
988
|
}
|
|
@@ -1064,27 +1057,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
1064
1057
|
appScoped={Boolean(focusedAppKey)}
|
|
1065
1058
|
topology={stableTopology}
|
|
1066
1059
|
appScopeLoading={focusedAppLoading}
|
|
1060
|
+
onOpenDiagnostics={onOpenDiagnostics}
|
|
1067
1061
|
/>
|
|
1068
1062
|
)
|
|
1069
1063
|
}
|
|
1070
|
-
|
|
1071
|
-
function ViewModeToggle({ viewMode, onViewModeChange }: { viewMode: TimelineViewMode, onViewModeChange: (mode: TimelineViewMode) => void }) {
|
|
1072
|
-
return (
|
|
1073
|
-
<div className="flex items-center gap-1 bg-theme-base rounded-lg p-0.5 border border-theme-border">
|
|
1074
|
-
<button
|
|
1075
|
-
type="button"
|
|
1076
|
-
onClick={() => onViewModeChange('list')}
|
|
1077
|
-
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'list' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
1078
|
-
>
|
|
1079
|
-
List
|
|
1080
|
-
</button>
|
|
1081
|
-
<button
|
|
1082
|
-
type="button"
|
|
1083
|
-
onClick={() => onViewModeChange('swimlane')}
|
|
1084
|
-
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'swimlane' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
1085
|
-
>
|
|
1086
|
-
Swimlane
|
|
1087
|
-
</button>
|
|
1088
|
-
</div>
|
|
1089
|
-
)
|
|
1090
|
-
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function ViewModeToggle({ viewMode, onViewModeChange }: { viewMode: 'list' | 'swimlane', onViewModeChange: (mode: 'list' | 'swimlane') => void }) {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center gap-1 bg-theme-base rounded-lg p-0.5 border border-theme-border">
|
|
4
|
+
<button
|
|
5
|
+
type="button"
|
|
6
|
+
onClick={() => onViewModeChange('list')}
|
|
7
|
+
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'list' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
8
|
+
>
|
|
9
|
+
List
|
|
10
|
+
</button>
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
onClick={() => onViewModeChange('swimlane')}
|
|
14
|
+
className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'swimlane' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
|
|
15
|
+
>
|
|
16
|
+
Swimlane
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { useStickyError } from './useStickyError'
|
|
6
|
+
|
|
7
|
+
const seen: (Error | null)[] = []
|
|
8
|
+
function Harness({ error, hasData }: { error: Error | null; hasData: boolean }) {
|
|
9
|
+
seen.push(useStickyError(error, hasData))
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let root: Root
|
|
14
|
+
let container: HTMLDivElement
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
seen.length = 0
|
|
17
|
+
container = document.createElement('div')
|
|
18
|
+
root = createRoot(container)
|
|
19
|
+
})
|
|
20
|
+
afterEach(() => act(() => root.unmount()))
|
|
21
|
+
const render = (error: Error | null, hasData: boolean) => act(() => root.render(<Harness error={error} hasData={hasData} />))
|
|
22
|
+
const last = () => seen[seen.length - 1]
|
|
23
|
+
|
|
24
|
+
describe('useStickyError', () => {
|
|
25
|
+
it('keeps the error while a refetch with no data clears it', () => {
|
|
26
|
+
const down = new Error('database down')
|
|
27
|
+
render(down, false)
|
|
28
|
+
expect(last()).toBe(down)
|
|
29
|
+
render(null, false)
|
|
30
|
+
expect(last()).toBe(down)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('drops the error once data arrives', () => {
|
|
34
|
+
render(new Error('database down'), false)
|
|
35
|
+
render(null, true)
|
|
36
|
+
expect(last()).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// A page with data on screen whose poll starts failing: the hook must settle,
|
|
40
|
+
// not bounce between the error and null until React gives up.
|
|
41
|
+
it('settles when an error arrives while data is on screen', () => {
|
|
42
|
+
render(null, true)
|
|
43
|
+
render(new Error('database down'), true)
|
|
44
|
+
expect(last()).toBeNull()
|
|
45
|
+
expect(seen.length).toBeLessThan(10)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
// React Query clears the error of a query that has never loaded each time it
|
|
4
|
+
// refetches, so a failure that persists across polls would flicker to a loader
|
|
5
|
+
// on every interval. This keeps the last error on screen until data arrives.
|
|
6
|
+
// Once data is loaded it returns null: a failing poll then belongs to the
|
|
7
|
+
// stale-data banner, not the full-screen error.
|
|
8
|
+
export function useStickyError(error: Error | null | undefined, hasData: boolean): Error | null {
|
|
9
|
+
const [shown, setShown] = useState<Error | null>(null)
|
|
10
|
+
const next = hasData ? null : (error ?? shown)
|
|
11
|
+
// Adjusting state during render: converges in one extra pass because `next`
|
|
12
|
+
// is a fixed point once stored.
|
|
13
|
+
if (next !== shown) setShown(next)
|
|
14
|
+
return next
|
|
15
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback } from 'react'
|
|
1
|
+
import { useEffect, useState, useCallback, useRef } from 'react'
|
|
2
2
|
import { X, Copy, Check, ExternalLink } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayTransitionStyle } from '../../utils/animation'
|
|
@@ -7,15 +7,29 @@ import { useDiagnostics } from '../../api/client'
|
|
|
7
7
|
import type { DiagnosticsSnapshot, DiagEnvVar, DiagMetricsSourceHealth, DiagDropRecord, DiagErrorEntry, DiagCacheSyncStatus, DiagInformerSyncStatus, DiagSyncPhase, DiagSampleWindow } from '../../api/client'
|
|
8
8
|
import { getK8sUIPerfSnapshot, type K8sUIPerfSnapshot } from '@skyhook-io/k8s-ui'
|
|
9
9
|
|
|
10
|
+
export type DiagnosticsFocus = 'timeline'
|
|
11
|
+
|
|
10
12
|
interface DiagnosticsOverlayProps {
|
|
11
13
|
onClose: () => void
|
|
12
14
|
isOpen?: boolean
|
|
15
|
+
/** Section to scroll to on open, for callers linking to one specific problem. */
|
|
16
|
+
focus?: DiagnosticsFocus
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverlayProps) {
|
|
19
|
+
export function DiagnosticsOverlay({ onClose, isOpen = true, focus }: DiagnosticsOverlayProps) {
|
|
16
20
|
const { data, isLoading, error } = useDiagnostics(true)
|
|
17
21
|
const [copied, setCopied] = useState<'json' | 'formatted' | null>(null)
|
|
18
22
|
const [reportOpened, setReportOpened] = useState(false)
|
|
23
|
+
const timelineRef = useRef<HTMLDivElement>(null)
|
|
24
|
+
const hasData = data != null
|
|
25
|
+
|
|
26
|
+
// Once, when the snapshot first renders: the overlay polls, and re-scrolling
|
|
27
|
+
// on every refresh would yank the reader back while they read other sections.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (isOpen && hasData && focus === 'timeline') {
|
|
30
|
+
timelineRef.current?.scrollIntoView({ block: 'start' })
|
|
31
|
+
}
|
|
32
|
+
}, [isOpen, hasData, focus])
|
|
19
33
|
|
|
20
34
|
// Close on Escape (capture phase) — only while logically open: the overlay
|
|
21
35
|
// stays mounted through its exit.
|
|
@@ -119,6 +133,7 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
119
133
|
<CacheSection data={data} />
|
|
120
134
|
<MetricsSection data={data} />
|
|
121
135
|
<EventPipelineSection data={data} />
|
|
136
|
+
<div ref={timelineRef} className="scroll-mt-4"><TimelineSection data={data} /></div>
|
|
122
137
|
<InformersSection data={data} />
|
|
123
138
|
<PrometheusSection data={data} />
|
|
124
139
|
<TrafficSection data={data} />
|
|
@@ -330,6 +345,20 @@ function EventPipelineSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
|
330
345
|
)
|
|
331
346
|
}
|
|
332
347
|
|
|
348
|
+
function TimelineSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
349
|
+
if (!data.timeline) return null
|
|
350
|
+
const t = data.timeline
|
|
351
|
+
return (
|
|
352
|
+
<Section title="Timeline" warn={t.degraded}>
|
|
353
|
+
<Row label="Storage" value={t.storageType} warn={t.degraded} />
|
|
354
|
+
{t.degraded && t.degradedReason && <Row label="Unavailable" value={t.degradedReason} warn />}
|
|
355
|
+
{/* An unreachable database has no count to report; 0 would read as empty. */}
|
|
356
|
+
{(!t.degraded || t.storageType === 'memory') && <Row label="Events" value={t.totalEvents.toLocaleString()} />}
|
|
357
|
+
<Row label="Dropped" value={t.totalDrops.toLocaleString()} warn={t.totalDrops > 0} />
|
|
358
|
+
</Section>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
333
362
|
function InformersSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
334
363
|
if (!data.informers) return null
|
|
335
364
|
const inf = data.informers
|
|
@@ -702,6 +731,9 @@ export function formatForGitHub(data: DiagnosticsSnapshot, frontendPerf?: K8sUIP
|
|
|
702
731
|
const t = data.timeline
|
|
703
732
|
lines.push(`### Timeline`)
|
|
704
733
|
lines.push(`- Storage: \`${t.storageType}\` | Events: ${t.totalEvents.toLocaleString()} | Errors: ${t.storeErrors} | Drops: ${t.totalDrops}`)
|
|
734
|
+
if (t.degraded && t.degradedReason) {
|
|
735
|
+
lines.push(`- Unavailable: ${t.degradedReason}`)
|
|
736
|
+
}
|
|
705
737
|
lines.push(``)
|
|
706
738
|
}
|
|
707
739
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { Markdown } from './Markdown'
|
|
6
|
+
|
|
7
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
8
|
+
|
|
9
|
+
const TEXT = 'First paragraph.\n\nRun `kubectl get pods` then open [the docs](https://example.com).\n\n```\nkubectl describe pod x\n```'
|
|
10
|
+
|
|
11
|
+
let container: HTMLDivElement
|
|
12
|
+
let root: Root
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
container = document.createElement('div')
|
|
16
|
+
document.body.appendChild(container)
|
|
17
|
+
root = createRoot(container)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
act(() => root.unmount())
|
|
22
|
+
container.remove()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Callers pass fresh inline callbacks on every render, as the investigation
|
|
26
|
+
// views do, so the rendered elements must survive a re-render with new ones.
|
|
27
|
+
function render(label: string) {
|
|
28
|
+
act(() => {
|
|
29
|
+
root.render(
|
|
30
|
+
<Markdown
|
|
31
|
+
codeActions={(code) => <button type="button">{`${label}:${code}`}</button>}
|
|
32
|
+
linkRenderer={(href, children) => (href?.includes('example') ? <a data-custom={label}>{children}</a> : null)}
|
|
33
|
+
>
|
|
34
|
+
{TEXT}
|
|
35
|
+
</Markdown>,
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('Markdown', () => {
|
|
41
|
+
it('keeps its rendered elements across parent re-renders', () => {
|
|
42
|
+
render('a')
|
|
43
|
+
const paragraph = container.querySelector('p')
|
|
44
|
+
const pre = container.querySelector('pre')
|
|
45
|
+
expect(paragraph).not.toBeNull()
|
|
46
|
+
expect(pre).not.toBeNull()
|
|
47
|
+
|
|
48
|
+
render('b')
|
|
49
|
+
|
|
50
|
+
expect(container.querySelector('p')).toBe(paragraph)
|
|
51
|
+
expect(container.querySelector('pre')).toBe(pre)
|
|
52
|
+
expect(paragraph?.isConnected).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('applies the latest link and code hooks', () => {
|
|
56
|
+
render('a')
|
|
57
|
+
render('b')
|
|
58
|
+
|
|
59
|
+
expect(container.querySelector('a')?.getAttribute('data-custom')).toBe('b')
|
|
60
|
+
const labels = Array.from(container.querySelectorAll('button')).map((b) => b.textContent?.trim())
|
|
61
|
+
expect(labels).toContain('b:kubectl get pods')
|
|
62
|
+
expect(labels).toContain('b:kubectl describe pod x')
|
|
63
|
+
expect(labels.some((l) => l?.startsWith('a:'))).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createContext, useContext, useMemo } from 'react'
|
|
2
|
+
import ReactMarkdown, { type Components } from 'react-markdown'
|
|
2
3
|
import remarkGfm from 'remark-gfm'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
4
5
|
|
|
5
6
|
import type { ReactNode } from 'react'
|
|
6
7
|
|
|
7
|
-
interface
|
|
8
|
-
children: string
|
|
9
|
-
className?: string
|
|
8
|
+
interface MarkdownHooks {
|
|
10
9
|
/**
|
|
11
10
|
* Intercepts links before the default anchor renders. Return null to fall
|
|
12
11
|
* through. Lets a caller turn a reserved href (an in-page evidence
|
|
@@ -16,132 +15,150 @@ interface MarkdownProps {
|
|
|
16
15
|
codeActions?: (code: string) => ReactNode
|
|
17
16
|
}
|
|
18
17
|
|
|
18
|
+
interface MarkdownProps extends MarkdownHooks {
|
|
19
|
+
children: string
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MarkdownHooksContext = createContext<MarkdownHooks>({})
|
|
24
|
+
|
|
25
|
+
const REMARK_PLUGINS = [remarkGfm]
|
|
26
|
+
|
|
27
|
+
// Module-level so every render hands react-markdown the same component types.
|
|
28
|
+
// A fresh map per render makes React unmount and remount the whole rendered
|
|
29
|
+
// tree whenever the parent re-renders; WebKit then moves the scroll position
|
|
30
|
+
// of any pane the text sits in. Per-call hooks arrive through context instead.
|
|
31
|
+
const COMPONENTS: Components = {
|
|
32
|
+
h1: ({ children }) => (
|
|
33
|
+
<h1 className="text-lg font-bold text-theme-text-primary mt-4 mb-2 first:mt-0">{children}</h1>
|
|
34
|
+
),
|
|
35
|
+
h2: ({ children }) => (
|
|
36
|
+
<h2 className="text-base font-semibold text-theme-text-primary mt-4 mb-2 first:mt-0">{children}</h2>
|
|
37
|
+
),
|
|
38
|
+
h3: ({ children }) => (
|
|
39
|
+
<h3 className="text-sm font-semibold text-theme-text-primary mt-3 mb-1.5">{children}</h3>
|
|
40
|
+
),
|
|
41
|
+
h4: ({ children }) => (
|
|
42
|
+
<h4 className="text-sm font-medium text-theme-text-primary mt-2 mb-1">{children}</h4>
|
|
43
|
+
),
|
|
44
|
+
p: ({ children }) => (
|
|
45
|
+
<p className="text-theme-text-secondary my-2 leading-relaxed">{children}</p>
|
|
46
|
+
),
|
|
47
|
+
a: function MarkdownLink({ href, children }) {
|
|
48
|
+
const { linkRenderer } = useContext(MarkdownHooksContext)
|
|
49
|
+
const custom = linkRenderer?.(href, children)
|
|
50
|
+
if (custom !== null && custom !== undefined) return <>{custom}</>
|
|
51
|
+
return (
|
|
52
|
+
<a
|
|
53
|
+
href={href}
|
|
54
|
+
target="_blank"
|
|
55
|
+
rel="noopener noreferrer"
|
|
56
|
+
className="text-blue-400 hover:text-blue-300 underline underline-offset-2"
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</a>
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
ul: ({ children }) => (
|
|
63
|
+
<ul className="list-disc list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ul>
|
|
64
|
+
),
|
|
65
|
+
ol: ({ children }) => (
|
|
66
|
+
<ol className="list-decimal list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ol>
|
|
67
|
+
),
|
|
68
|
+
li: ({ children }) => (
|
|
69
|
+
<li className="leading-relaxed">{children}</li>
|
|
70
|
+
),
|
|
71
|
+
code: function MarkdownCode({ className, children }) {
|
|
72
|
+
const { codeActions } = useContext(MarkdownHooksContext)
|
|
73
|
+
const isInline = !className
|
|
74
|
+
if (isInline) {
|
|
75
|
+
const actions = codeActions?.(String(children))
|
|
76
|
+
if (actions) {
|
|
77
|
+
return (
|
|
78
|
+
<span className="inline-flex max-w-full items-start gap-1 align-middle">
|
|
79
|
+
<code className="inline-code min-w-0">{children}</code>
|
|
80
|
+
{actions}
|
|
81
|
+
</span>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<code className="inline-code">
|
|
86
|
+
{children}
|
|
87
|
+
</code>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return (
|
|
91
|
+
<code className="block font-mono text-theme-text-secondary">{children}</code>
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
pre: function MarkdownPre({ children, node }) {
|
|
95
|
+
const { codeActions } = useContext(MarkdownHooksContext)
|
|
96
|
+
const code = node?.children[0]
|
|
97
|
+
const text = code?.type === 'element'
|
|
98
|
+
? code.children.filter(child => child.type === 'text').map(child => child.value).join('')
|
|
99
|
+
: ''
|
|
100
|
+
const actions = codeActions?.(text)
|
|
101
|
+
if (actions) {
|
|
102
|
+
// Render the code directly so an unlabelled fence does not also
|
|
103
|
+
// receive the inline-code action. Only the code scrolls, not Copy.
|
|
104
|
+
return (
|
|
105
|
+
<pre className="my-3 flex items-start gap-2 rounded-lg bg-theme-elevated p-3 text-xs font-mono text-theme-text-secondary">
|
|
106
|
+
<code className="min-w-0 flex-1 overflow-x-auto">{text}</code>
|
|
107
|
+
{actions}
|
|
108
|
+
</pre>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return (
|
|
112
|
+
<pre className="my-3 p-3 bg-theme-elevated rounded-lg overflow-x-auto text-xs font-mono text-theme-text-secondary">
|
|
113
|
+
{children}
|
|
114
|
+
</pre>
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
blockquote: ({ children }) => (
|
|
118
|
+
<blockquote className="border-l-2 border-theme-border pl-3 my-2 text-theme-text-tertiary italic">
|
|
119
|
+
{children}
|
|
120
|
+
</blockquote>
|
|
121
|
+
),
|
|
122
|
+
table: ({ children }) => (
|
|
123
|
+
<div className="my-3 overflow-x-auto">
|
|
124
|
+
<table className="min-w-full border-collapse text-sm">{children}</table>
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
thead: ({ children }) => (
|
|
128
|
+
<thead className="bg-theme-elevated">{children}</thead>
|
|
129
|
+
),
|
|
130
|
+
tbody: ({ children }) => (
|
|
131
|
+
<tbody className="divide-y divide-theme-border">{children}</tbody>
|
|
132
|
+
),
|
|
133
|
+
tr: ({ children }) => (
|
|
134
|
+
<tr className="border-b border-theme-border">{children}</tr>
|
|
135
|
+
),
|
|
136
|
+
th: ({ children }) => (
|
|
137
|
+
<th className="px-3 py-2 text-left text-theme-text-primary font-medium border border-theme-border">
|
|
138
|
+
{children}
|
|
139
|
+
</th>
|
|
140
|
+
),
|
|
141
|
+
td: ({ children }) => (
|
|
142
|
+
<td className="px-3 py-2 text-theme-text-secondary border border-theme-border">{children}</td>
|
|
143
|
+
),
|
|
144
|
+
hr: () => <hr className="my-4 border-theme-border" />,
|
|
145
|
+
strong: ({ children }) => (
|
|
146
|
+
<strong className="font-semibold text-theme-text-primary">{children}</strong>
|
|
147
|
+
),
|
|
148
|
+
em: ({ children }) => (
|
|
149
|
+
<em className="italic">{children}</em>
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
|
|
19
153
|
export function Markdown({ children, className, linkRenderer, codeActions }: MarkdownProps) {
|
|
154
|
+
const hooks = useMemo(() => ({ linkRenderer, codeActions }), [linkRenderer, codeActions])
|
|
20
155
|
return (
|
|
21
156
|
<div className={clsx('markdown-content', className)}>
|
|
22
|
-
<
|
|
23
|
-
remarkPlugins={
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
),
|
|
28
|
-
h2: ({ children }) => (
|
|
29
|
-
<h2 className="text-base font-semibold text-theme-text-primary mt-4 mb-2 first:mt-0">{children}</h2>
|
|
30
|
-
),
|
|
31
|
-
h3: ({ children }) => (
|
|
32
|
-
<h3 className="text-sm font-semibold text-theme-text-primary mt-3 mb-1.5">{children}</h3>
|
|
33
|
-
),
|
|
34
|
-
h4: ({ children }) => (
|
|
35
|
-
<h4 className="text-sm font-medium text-theme-text-primary mt-2 mb-1">{children}</h4>
|
|
36
|
-
),
|
|
37
|
-
p: ({ children }) => (
|
|
38
|
-
<p className="text-theme-text-secondary my-2 leading-relaxed">{children}</p>
|
|
39
|
-
),
|
|
40
|
-
a: ({ href, children }) => {
|
|
41
|
-
const custom = linkRenderer?.(href, children)
|
|
42
|
-
if (custom !== null && custom !== undefined) return <>{custom}</>
|
|
43
|
-
return (
|
|
44
|
-
<a
|
|
45
|
-
href={href}
|
|
46
|
-
target="_blank"
|
|
47
|
-
rel="noopener noreferrer"
|
|
48
|
-
className="text-blue-400 hover:text-blue-300 underline underline-offset-2"
|
|
49
|
-
>
|
|
50
|
-
{children}
|
|
51
|
-
</a>
|
|
52
|
-
)
|
|
53
|
-
},
|
|
54
|
-
ul: ({ children }) => (
|
|
55
|
-
<ul className="list-disc list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ul>
|
|
56
|
-
),
|
|
57
|
-
ol: ({ children }) => (
|
|
58
|
-
<ol className="list-decimal list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ol>
|
|
59
|
-
),
|
|
60
|
-
li: ({ children }) => (
|
|
61
|
-
<li className="leading-relaxed">{children}</li>
|
|
62
|
-
),
|
|
63
|
-
code: ({ className, children }) => {
|
|
64
|
-
const isInline = !className
|
|
65
|
-
if (isInline) {
|
|
66
|
-
const actions = codeActions?.(String(children))
|
|
67
|
-
if (actions) {
|
|
68
|
-
return (
|
|
69
|
-
<span className="inline-flex max-w-full items-start gap-1 align-middle">
|
|
70
|
-
<code className="inline-code min-w-0">{children}</code>
|
|
71
|
-
{actions}
|
|
72
|
-
</span>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
return (
|
|
76
|
-
<code className="inline-code">
|
|
77
|
-
{children}
|
|
78
|
-
</code>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
return (
|
|
82
|
-
<code className="block font-mono text-theme-text-secondary">{children}</code>
|
|
83
|
-
)
|
|
84
|
-
},
|
|
85
|
-
pre: ({ children, node }) => {
|
|
86
|
-
const code = node?.children[0]
|
|
87
|
-
const text = code?.type === 'element'
|
|
88
|
-
? code.children.filter(child => child.type === 'text').map(child => child.value).join('')
|
|
89
|
-
: ''
|
|
90
|
-
const actions = codeActions?.(text)
|
|
91
|
-
if (actions) {
|
|
92
|
-
// Render the code directly so an unlabelled fence does not also
|
|
93
|
-
// receive the inline-code action. Only the code scrolls, not Copy.
|
|
94
|
-
return (
|
|
95
|
-
<pre className="my-3 flex items-start gap-2 rounded-lg bg-theme-elevated p-3 text-xs font-mono text-theme-text-secondary">
|
|
96
|
-
<code className="min-w-0 flex-1 overflow-x-auto">{text}</code>
|
|
97
|
-
{actions}
|
|
98
|
-
</pre>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
return (
|
|
102
|
-
<pre className="my-3 p-3 bg-theme-elevated rounded-lg overflow-x-auto text-xs font-mono text-theme-text-secondary">
|
|
103
|
-
{children}
|
|
104
|
-
</pre>
|
|
105
|
-
)
|
|
106
|
-
},
|
|
107
|
-
blockquote: ({ children }) => (
|
|
108
|
-
<blockquote className="border-l-2 border-theme-border pl-3 my-2 text-theme-text-tertiary italic">
|
|
109
|
-
{children}
|
|
110
|
-
</blockquote>
|
|
111
|
-
),
|
|
112
|
-
table: ({ children }) => (
|
|
113
|
-
<div className="my-3 overflow-x-auto">
|
|
114
|
-
<table className="min-w-full border-collapse text-sm">{children}</table>
|
|
115
|
-
</div>
|
|
116
|
-
),
|
|
117
|
-
thead: ({ children }) => (
|
|
118
|
-
<thead className="bg-theme-elevated">{children}</thead>
|
|
119
|
-
),
|
|
120
|
-
tbody: ({ children }) => (
|
|
121
|
-
<tbody className="divide-y divide-theme-border">{children}</tbody>
|
|
122
|
-
),
|
|
123
|
-
tr: ({ children }) => (
|
|
124
|
-
<tr className="border-b border-theme-border">{children}</tr>
|
|
125
|
-
),
|
|
126
|
-
th: ({ children }) => (
|
|
127
|
-
<th className="px-3 py-2 text-left text-theme-text-primary font-medium border border-theme-border">
|
|
128
|
-
{children}
|
|
129
|
-
</th>
|
|
130
|
-
),
|
|
131
|
-
td: ({ children }) => (
|
|
132
|
-
<td className="px-3 py-2 text-theme-text-secondary border border-theme-border">{children}</td>
|
|
133
|
-
),
|
|
134
|
-
hr: () => <hr className="my-4 border-theme-border" />,
|
|
135
|
-
strong: ({ children }) => (
|
|
136
|
-
<strong className="font-semibold text-theme-text-primary">{children}</strong>
|
|
137
|
-
),
|
|
138
|
-
em: ({ children }) => (
|
|
139
|
-
<em className="italic">{children}</em>
|
|
140
|
-
),
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
{children}
|
|
144
|
-
</ReactMarkdown>
|
|
157
|
+
<MarkdownHooksContext.Provider value={hooks}>
|
|
158
|
+
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={COMPONENTS}>
|
|
159
|
+
{children}
|
|
160
|
+
</ReactMarkdown>
|
|
161
|
+
</MarkdownHooksContext.Provider>
|
|
145
162
|
</div>
|
|
146
163
|
)
|
|
147
164
|
}
|