@skyhook-io/radar-app 1.14.5 → 1.14.6
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 +2 -9
- package/package.json +6 -6
- package/src/App.tsx +91 -328
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +233 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +14 -8
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +18 -0
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +42 -0
- package/src/components/execution/JobSetAdmission.tsx +17 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.tsx +2 -5
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/model.test.ts +2 -1
- package/src/components/rightsizing/notices.test.tsx +4 -3
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/SettingsDialog.tsx +157 -42
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +18 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/command-items.ts +0 -3
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +36 -18
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +18 -1
- package/src/utils/navigation.ts +12 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/components/ui/CommandPalette.tsx +0 -261
package/src/App.tsx
CHANGED
|
@@ -9,8 +9,9 @@ import { DebugOverlay } from './components/DebugOverlay'
|
|
|
9
9
|
import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
|
|
10
10
|
import { investigationWorkspaceSearch, isInvestigationWorkspacePath, useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
|
|
11
11
|
import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
|
|
12
|
-
import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader
|
|
12
|
+
import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
|
|
13
13
|
import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
14
|
+
import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
|
|
14
15
|
import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
|
|
15
16
|
import { TimelineView } from './components/timeline/TimelineView'
|
|
16
17
|
import { ResourcesView } from './components/resources/ResourcesView'
|
|
@@ -49,7 +50,6 @@ import { UserMenu } from './components/UserMenu'
|
|
|
49
50
|
import { ErrorBoundary } from './components/ui/ErrorBoundary'
|
|
50
51
|
import { UpdateNotification } from './components/ui/UpdateNotification'
|
|
51
52
|
import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
|
|
52
|
-
import { CommandPalette } from './components/ui/CommandPalette'
|
|
53
53
|
import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
|
|
54
54
|
import { useEventSource } from './hooks/useEventSource'
|
|
55
55
|
import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
|
|
@@ -61,7 +61,7 @@ import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
|
61
61
|
import { useDocumentTitle } from './hooks/useDocumentTitle'
|
|
62
62
|
import type { ClusterLoadState } from './types/clusterLoadState'
|
|
63
63
|
import { useClusterLoadState } from './hooks/useClusterLoadState'
|
|
64
|
-
import { Network,
|
|
64
|
+
import { Network, Sun, Moon, Star, Bug, SquareTerminal, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
|
|
65
65
|
import { useTheme } from './context/ThemeContext'
|
|
66
66
|
import { Tooltip } from './components/ui/Tooltip'
|
|
67
67
|
import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
|
|
@@ -73,8 +73,6 @@ import { type OmnibarHandle } from './components/ui/Omnibar'
|
|
|
73
73
|
import { RadarOmnibar } from './components/ui/RadarOmnibar'
|
|
74
74
|
import type { ContextSwitcherHandle } from './components/ContextSwitcher'
|
|
75
75
|
|
|
76
|
-
const radarLogoUrl = assetUrl('/images/radar/radar-icon.svg')
|
|
77
|
-
|
|
78
76
|
// All possible node kinds (core + GitOps)
|
|
79
77
|
const ALL_NODE_KINDS: NodeKind[] = [
|
|
80
78
|
'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
|
|
@@ -343,30 +341,23 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
343
341
|
[navCustomization],
|
|
344
342
|
)
|
|
345
343
|
// Resolve every host-takeover URL ONCE (memoized on navCustomization) so the
|
|
346
|
-
// setMainView intercept, redirect effect,
|
|
347
|
-
//
|
|
344
|
+
// setMainView intercept, redirect effect, inline-view gating, and the cert
|
|
345
|
+
// click handler all consume the SAME value — host
|
|
348
346
|
// callbacks aren't guaranteed idempotent (scope / flags / signed URLs can
|
|
349
347
|
// shift between calls). undefined = not taken over → Radar renders the view
|
|
350
|
-
// itself.
|
|
351
|
-
// 'checks' target for back-compat.
|
|
348
|
+
// itself.
|
|
352
349
|
const takeover: Record<FleetTakeoverTarget, string | undefined> = useMemo(
|
|
353
350
|
() => ({
|
|
354
351
|
issues: navCustomization.fleetTakeoverHref?.('issues'),
|
|
355
352
|
gitops: navCustomization.fleetTakeoverHref?.('gitops'),
|
|
356
|
-
checks: navCustomization.fleetTakeoverHref?.('checks')
|
|
353
|
+
checks: navCustomization.fleetTakeoverHref?.('checks'),
|
|
357
354
|
certs: navCustomization.fleetTakeoverHref?.('certs'),
|
|
358
355
|
}),
|
|
359
356
|
[navCustomization],
|
|
360
357
|
)
|
|
361
358
|
const { pinned: navRailPinned, togglePinned: toggleNavRailPinned } = useNavRailPinned()
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const showNavRail = !navCustomization.embedded
|
|
365
|
-
// Chromeless embed: the host (Radar Hub) owns ALL chrome and drives view
|
|
366
|
-
// navigation + scope from its own UI, so Radar renders just the active view's
|
|
367
|
-
// content — no top bar, no view-switcher. Used for per-cluster views surfaced
|
|
368
|
-
// as native cloud destinations behind a cluster picker.
|
|
369
|
-
const chromeless = navCustomization.embedded === true && navCustomization.chrome === 'none'
|
|
359
|
+
const embedded = navCustomization.embedded === true
|
|
360
|
+
const showNavRail = !embedded
|
|
370
361
|
// Force the slim rail on narrow windows: a pinned 176px rail needs viewport
|
|
371
362
|
// ≥976 to keep content above its ~800px floor (collapsed needs only ≥856).
|
|
372
363
|
// Below 976 we render collapsed regardless of the pin preference — a
|
|
@@ -513,15 +504,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
513
504
|
}, [diagnoseOpen, diagnoseMaximized, dismissDiagnoseForNavigation, setMainView])
|
|
514
505
|
|
|
515
506
|
// Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
|
|
516
|
-
// own fleet pages scoped to this cluster
|
|
517
|
-
// Radar drops the matching pills (see the nav below). In-app nav hands off in
|
|
507
|
+
// own fleet pages scoped to this cluster. In-app navigation hands off in
|
|
518
508
|
// setMainView (above); direct /<view> URL entry funnels through the redirect
|
|
519
509
|
// effect below. Both consume the memoized `takeover` resolved above. Standalone
|
|
520
510
|
// OSS (no fleetTakeoverHref) is unaffected and renders the in-app view.
|
|
521
511
|
//
|
|
522
512
|
// Has the host claimed this view? View-shaped targets only ('certs' has no
|
|
523
|
-
// Radar view — only its Home card consults `takeover`). Used to
|
|
524
|
-
//
|
|
513
|
+
// Radar view — only its Home card consults `takeover`). Used to gate the
|
|
514
|
+
// inline view render in favor of the "Opening…" splash.
|
|
525
515
|
const isViewTakenOver = (view: ExtendedMainView): boolean =>
|
|
526
516
|
(view === 'issues' || view === 'gitops' || view === 'checks') && !!takeover[view]
|
|
527
517
|
// The host's URL for the CURRENT view, if taken over. Drives the redirect
|
|
@@ -565,9 +555,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
565
555
|
// Help overlay state
|
|
566
556
|
const [showHelp, setShowHelp] = useState(false)
|
|
567
557
|
|
|
568
|
-
// Command palette state
|
|
569
|
-
const [showCommandPalette, setShowCommandPalette] = useState(false)
|
|
570
|
-
|
|
571
558
|
// Settings dialog state
|
|
572
559
|
const [showSettings, setShowSettings] = useState(false)
|
|
573
560
|
const [settingsSection, setSettingsSection] = useState<SettingsSectionId>('overview')
|
|
@@ -683,7 +670,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
683
670
|
// Dialog-kind overlays wait their exit duration (shorter than the entrance);
|
|
684
671
|
// drawers keep the symmetric slide.
|
|
685
672
|
const helpOverlay = useAnimatedUnmount(showHelp, overlayExitMs('dialog'))
|
|
686
|
-
const commandPaletteAnim = useAnimatedUnmount(showCommandPalette, overlayExitMs('dialog'))
|
|
687
673
|
const diagnosticsOverlay = useAnimatedUnmount(showDiagnostics, overlayExitMs('dialog'))
|
|
688
674
|
|
|
689
675
|
// Hold last valid values so drawers can animate out before data disappears
|
|
@@ -830,7 +816,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
830
816
|
// Theme toggle for keyboard shortcut
|
|
831
817
|
const { toggleTheme } = useTheme()
|
|
832
818
|
|
|
833
|
-
// Context switching for command palette
|
|
834
819
|
const switchContext = useSwitchContext()
|
|
835
820
|
|
|
836
821
|
// Refs for dropdown components to trigger them via shortcuts
|
|
@@ -909,10 +894,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
909
894
|
category: 'General' as const,
|
|
910
895
|
scope: 'global' as const,
|
|
911
896
|
allowInInputs: true,
|
|
912
|
-
//
|
|
913
|
-
|
|
914
|
-
// otherwise both the host omnibar and Radar's palette fire on one ⌘K.
|
|
915
|
-
handler: () => { if (showNavRail) omnibarRef.current?.focus(); else if (!chromeless) setShowCommandPalette(true) },
|
|
897
|
+
// Radar Hub owns its own omnibar.
|
|
898
|
+
handler: () => { if (!embedded) omnibarRef.current?.focus() },
|
|
916
899
|
},
|
|
917
900
|
{
|
|
918
901
|
id: 'diagnostics',
|
|
@@ -981,7 +964,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
981
964
|
const { data: namespaceScope } = useNamespaceScope()
|
|
982
965
|
|
|
983
966
|
// Context switch state
|
|
984
|
-
const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch } = useContextSwitch()
|
|
967
|
+
const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch, takeOpenAfterSwitch } = useContextSwitch()
|
|
985
968
|
|
|
986
969
|
// Connection state (for graceful startup)
|
|
987
970
|
const { connection, retry: retryConnection, isRetrying, updateFromSSE: updateConnectionFromSSE } = useConnection()
|
|
@@ -1008,7 +991,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1008
991
|
const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
|
|
1009
992
|
namespaces,
|
|
1010
993
|
mainView,
|
|
1011
|
-
chromeless,
|
|
994
|
+
chromeless: embedded,
|
|
1012
995
|
contentReady,
|
|
1013
996
|
onClusterLoadStateChange,
|
|
1014
997
|
})
|
|
@@ -1158,7 +1141,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1158
1141
|
const { topology, connected: eventStreamConnected, connecting: eventStreamConnecting, reconnect: reconnectEventStream } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
|
|
1159
1142
|
onContextSwitchComplete: endSwitch,
|
|
1160
1143
|
onContextSwitchProgress: updateProgress,
|
|
1161
|
-
onContextChanged: () => {
|
|
1144
|
+
onContextChanged: (context) => {
|
|
1162
1145
|
// Clear all React Query caches when cluster context changes
|
|
1163
1146
|
// This ensures helm releases, resources, etc. are refetched from the new cluster
|
|
1164
1147
|
// removeQueries clears cached data, invalidateQueries triggers refetch
|
|
@@ -1181,7 +1164,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1181
1164
|
// Reset resource-specific params while retaining the durable investigation
|
|
1182
1165
|
// focus. Diagnose resolves runs by id and owns whether the focused run is
|
|
1183
1166
|
// still readable after the context switch.
|
|
1184
|
-
|
|
1167
|
+
const openAfter = takeOpenAfterSwitch(context)
|
|
1168
|
+
if (openAfter) {
|
|
1169
|
+
// The switch was made to open this resource on the new cluster; the
|
|
1170
|
+
// page it was requested from belongs to the old one.
|
|
1171
|
+
setSelectedResource(openAfter)
|
|
1172
|
+
const params = new URLSearchParams()
|
|
1173
|
+
params.set('resource', openAfter.namespace ? `${openAfter.namespace}/${openAfter.name}` : openAfter.name)
|
|
1174
|
+
if (openAfter.group) params.set('apiGroup', openAfter.group)
|
|
1175
|
+
navigate({ pathname: `/resources/${openAfter.kind}`, search: params.toString() }, { replace: true })
|
|
1176
|
+
} else if (isInvestigationWorkspacePath(location.pathname)) {
|
|
1185
1177
|
navigate(
|
|
1186
1178
|
{
|
|
1187
1179
|
pathname: location.pathname,
|
|
@@ -1321,6 +1313,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1321
1313
|
if (node.kind === 'Internet') return
|
|
1322
1314
|
|
|
1323
1315
|
const nodeGroup = apiVersionToGroup(node.data.apiVersion as string | undefined)
|
|
1316
|
+
const resourceKind = topologyNodeResourceKind(node)
|
|
1324
1317
|
// Radar's topology PodGroup is a virtual pod aggregate. A Kubernetes
|
|
1325
1318
|
// scheduling.k8s.io PodGroup carries apiVersion and is a real resource.
|
|
1326
1319
|
if (node.kind === 'PodGroup' && !nodeGroup) return
|
|
@@ -1330,14 +1323,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1330
1323
|
// detail page with tree + insights + ops that the drawer can't reproduce.
|
|
1331
1324
|
// Route there from the main topology when the node is one of those kinds;
|
|
1332
1325
|
// everything else falls back to the drawer.
|
|
1333
|
-
const gitOpsPath = gitOpsRouteForKind(
|
|
1326
|
+
const gitOpsPath = gitOpsRouteForKind(resourceKind, namespace, node.name, nodeGroup, true)
|
|
1334
1327
|
if (gitOpsPath) {
|
|
1335
1328
|
navigate(gitOpsPath)
|
|
1336
1329
|
return
|
|
1337
1330
|
}
|
|
1338
1331
|
|
|
1339
1332
|
navigateToResource({
|
|
1340
|
-
kind: kindToPluralWithGroup(
|
|
1333
|
+
kind: kindToPluralWithGroup(resourceKind, nodeGroup),
|
|
1341
1334
|
namespace,
|
|
1342
1335
|
name: node.name,
|
|
1343
1336
|
group: nodeGroup,
|
|
@@ -1752,46 +1745,20 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1752
1745
|
− nav rail), not the viewport, so it collapses gracefully on narrow windows.
|
|
1753
1746
|
The AI panel docks BELOW the navbar and pushes only the content region, so the
|
|
1754
1747
|
navbar is never squeezed by it — these thresholds react to real window width. */}
|
|
1755
|
-
{!
|
|
1748
|
+
{!embedded && (
|
|
1756
1749
|
<header className="@container relative z-50 flex items-center justify-between px-4 py-2 bg-theme-base/90 backdrop-blur-sm border-b border-theme-border/50">
|
|
1757
|
-
{/*
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
{navCustomization.contextSlot ? (
|
|
1770
|
-
// Embedded host supplies its own cluster switcher — keep the two
|
|
1771
|
-
// controls separate (the host owns the cluster chip's styling).
|
|
1772
|
-
<>
|
|
1773
|
-
{navCustomization.contextSlot}
|
|
1774
|
-
<NamespaceSwitcher
|
|
1775
|
-
ref={namespaceSwitcherRef}
|
|
1776
|
-
disabled={namespaceFilter.disabled}
|
|
1777
|
-
disabledTooltip={namespaceFilter.tooltip}
|
|
1778
|
-
/>
|
|
1779
|
-
</>
|
|
1780
|
-
) : (
|
|
1781
|
-
// Standalone: cluster + namespace as one "scope" pill — two
|
|
1782
|
-
// borderless segments split by a divider, reading as a single
|
|
1783
|
-
// "what am I looking at" unit. The shared ScopePill shell is the
|
|
1784
|
-
// same one Radar Hub's cluster top bar uses, so the two match.
|
|
1785
|
-
<ScopePill>
|
|
1786
|
-
<ContextSwitcher ref={contextSwitcherRef} variant="segment" />
|
|
1787
|
-
<NamespaceSwitcher
|
|
1788
|
-
ref={namespaceSwitcherRef}
|
|
1789
|
-
variant="segment"
|
|
1790
|
-
disabled={namespaceFilter.disabled}
|
|
1791
|
-
disabledTooltip={namespaceFilter.tooltip}
|
|
1792
|
-
/>
|
|
1793
|
-
</ScopePill>
|
|
1794
|
-
)}
|
|
1750
|
+
{/* Fixed width keeps the omnibar steady as cluster and namespace names change. */}
|
|
1751
|
+
<div className="flex items-center gap-4 shrink-0 w-[492px]">
|
|
1752
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
1753
|
+
<ScopePill>
|
|
1754
|
+
<ContextSwitcher ref={contextSwitcherRef} variant="segment" />
|
|
1755
|
+
<NamespaceSwitcher
|
|
1756
|
+
ref={namespaceSwitcherRef}
|
|
1757
|
+
variant="segment"
|
|
1758
|
+
disabled={namespaceFilter.disabled}
|
|
1759
|
+
disabledTooltip={namespaceFilter.tooltip}
|
|
1760
|
+
/>
|
|
1761
|
+
</ScopePill>
|
|
1795
1762
|
{/* Connection status — a fixed-size dot (state in the tooltip), an
|
|
1796
1763
|
optional reconnect button, and when the header is wide enough
|
|
1797
1764
|
(xl+), a label. The label is nowrap and unbounded: it overflows
|
|
@@ -1817,7 +1784,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1817
1784
|
}`}
|
|
1818
1785
|
/>
|
|
1819
1786
|
</Tooltip>
|
|
1820
|
-
{
|
|
1787
|
+
{headerConnectionDisplayLabel && (
|
|
1821
1788
|
<span className="hidden xl:flex items-center gap-1.5 whitespace-nowrap text-[11px] text-theme-text-tertiary">
|
|
1822
1789
|
{showClusterWarmupLabel && <Loader2 className="w-3 h-3 animate-spin" />}
|
|
1823
1790
|
{headerConnectionDisplayLabel}
|
|
@@ -1842,65 +1809,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1842
1809
|
</div>
|
|
1843
1810
|
</div>
|
|
1844
1811
|
|
|
1845
|
-
{/* Center: View tabs — embedded/pill layout only. Standalone Radar
|
|
1846
|
-
navigates via the left rail (showNavRail), so the pill bar is
|
|
1847
|
-
suppressed there to avoid a duplicate primary nav. */}
|
|
1848
|
-
{!showNavRail && (
|
|
1849
|
-
<div className="@min-[920px]:absolute @min-[920px]:left-1/2 @min-[920px]:-translate-x-1/2 flex items-center gap-0.5 bg-theme-elevated/50 rounded-full p-1 ml-2 @min-[920px]:ml-0">
|
|
1850
|
-
{([
|
|
1851
|
-
{ view: 'home' as const, icon: Home, label: 'Home' },
|
|
1852
|
-
{ view: 'topology' as const, icon: Network, label: 'Topology' },
|
|
1853
|
-
{ view: 'resources' as const, icon: List, label: 'Resources' },
|
|
1854
|
-
{ view: 'capacity' as const, icon: Gauge, label: 'Capacity' },
|
|
1855
|
-
{ view: 'timeline' as const, icon: Clock, label: 'Timeline' },
|
|
1856
|
-
{ view: 'helm' as const, icon: Package, label: 'Helm' },
|
|
1857
|
-
{ view: 'gitops' as const, icon: GitBranch, label: 'GitOps' },
|
|
1858
|
-
// Applications is intentionally hidden from the pill bar for now —
|
|
1859
|
-
// the bar is full, and the view's primary home is Cloud's fleet
|
|
1860
|
-
// rail. The view still exists and is reachable via /applications
|
|
1861
|
-
// and the view-switching shortcuts. Same treatment as Cost below.
|
|
1862
|
-
{ view: 'traffic' as const, icon: Activity, label: 'Live Traffic' },
|
|
1863
|
-
// Cost is intentionally hidden from the pill bar for now — the view still
|
|
1864
|
-
// exists and is reachable via /cost, the Home dashboard card, and the
|
|
1865
|
-
// command palette (⌘K). Remove this comment to restore it.
|
|
1866
|
-
{ view: 'checks' as const, icon: ShieldCheck, label: 'Checks' },
|
|
1867
|
-
] as const)
|
|
1868
|
-
// In Cloud, fleet-shaped views (Checks, Issues, GitOps) are owned by
|
|
1869
|
-
// the host's left rail; the per-cluster view is just that fleet page
|
|
1870
|
-
// filtered to this cluster, so duplicating it as a peer pill here
|
|
1871
|
-
// would be a second copy that teleports out of the cluster shell.
|
|
1872
|
-
// Drop any pill the host took over — cluster-scoped access stays
|
|
1873
|
-
// available via the Home cards (redirected by the takeover effect
|
|
1874
|
-
// above), ⌘K, and bookmarks. Standalone OSS keeps every pill.
|
|
1875
|
-
.filter(({ view }) => !isViewTakenOver(view))
|
|
1876
|
-
.map(({ view, icon: Icon, label }) => (
|
|
1877
|
-
<Tooltip key={view} content={label} delay={100} position="bottom">
|
|
1878
|
-
<button
|
|
1879
|
-
onClick={() => setMainView(view)}
|
|
1880
|
-
className={`flex items-center gap-1 px-2 py-1 text-[13px] rounded-full transition-colors ${
|
|
1881
|
-
mainView === view || (mainView === 'helmCompare' && view === 'helm')
|
|
1882
|
-
? 'bg-skyhook-600 dark:bg-skyhook-500 text-white shadow-glow-brand-sm'
|
|
1883
|
-
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover'
|
|
1884
|
-
}`}
|
|
1885
|
-
>
|
|
1886
|
-
<Icon className="w-4 h-4" />
|
|
1887
|
-
{/* Labels appear only when the absolute-centered nav has
|
|
1888
|
-
enough horizontal room past the left section. Right-side
|
|
1889
|
-
chrome that adds further pressure (Connected text, star
|
|
1890
|
-
count) is intentionally pushed to the next tier (xl) so
|
|
1891
|
-
label rendering and right-side expansion stay decoupled.
|
|
1892
|
-
Per-button Tooltip discloses labels on hover when the
|
|
1893
|
-
icon-only viewport is in effect. The 1440 anchor is an
|
|
1894
|
-
off-system breakpoint chosen by measurement at the time
|
|
1895
|
-
of this PR — recompute if the cluster switcher cap or
|
|
1896
|
-
other left-section chrome changes appreciably. */}
|
|
1897
|
-
<span className="hidden @min-[1264px]:inline">{label}</span>
|
|
1898
|
-
</button>
|
|
1899
|
-
</Tooltip>
|
|
1900
|
-
))}
|
|
1901
|
-
</div>
|
|
1902
|
-
)}
|
|
1903
|
-
|
|
1904
1812
|
{/* Center: omnibar — standalone search + command surface (the ⌘K entry).
|
|
1905
1813
|
Its container is one of three EQUAL flex-1 columns (see the left/right
|
|
1906
1814
|
groups): equal side columns keep this middle column — and the search
|
|
@@ -1908,60 +1816,37 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1908
1816
|
chrome gets (cluster name, namespace label, "Discovering…" /
|
|
1909
1817
|
"Disconnected" text). Overflowing side content truncates instead of
|
|
1910
1818
|
dragging the box. Same pattern as Radar Hub's ClusterTopBar. */}
|
|
1911
|
-
|
|
1912
|
-
<
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
</div>
|
|
1932
|
-
)}
|
|
1819
|
+
<div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
|
|
1820
|
+
<RadarOmnibar
|
|
1821
|
+
ref={omnibarRef}
|
|
1822
|
+
onNavigateView={(view) => setMainView(view)}
|
|
1823
|
+
onNavigateKind={(kind, group) => {
|
|
1824
|
+
const params = new URLSearchParams(searchParams)
|
|
1825
|
+
params.delete('kind')
|
|
1826
|
+
if (group) params.set('apiGroup', group); else params.delete('apiGroup')
|
|
1827
|
+
params.delete('resource')
|
|
1828
|
+
params.delete('full')
|
|
1829
|
+
params.delete('tab')
|
|
1830
|
+
navigate({ pathname: `/resources/${kind}`, search: params.toString() })
|
|
1831
|
+
}}
|
|
1832
|
+
onSwitchContext={(name) => switchContext.mutate({ name }, { onSettled: () => setNamespaces([]) })}
|
|
1833
|
+
onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }}
|
|
1834
|
+
onToggleTheme={toggleTheme}
|
|
1835
|
+
onShowDiagnostics={() => setShowDiagnostics(true)}
|
|
1836
|
+
onOpenResource={(hit) => navigateToResourceList(searchHitToSelectedResource(hit))}
|
|
1837
|
+
/>
|
|
1838
|
+
</div>
|
|
1933
1839
|
|
|
1934
1840
|
{/* Right: Controls */}
|
|
1935
1841
|
<div className="flex items-center gap-3 shrink-0">
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
<button
|
|
1940
|
-
onClick={() => setShowCommandPalette(true)}
|
|
1941
|
-
aria-label="Open command palette"
|
|
1942
|
-
className="hidden lg:flex items-center gap-2 h-7 px-2.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1943
|
-
>
|
|
1944
|
-
<Search className="w-3.5 h-3.5" />
|
|
1945
|
-
<kbd className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded border border-theme-border-light">
|
|
1946
|
-
{typeof navigator !== 'undefined' && navigator.platform.includes('Mac') ? '⌘' : 'Ctrl+'}K
|
|
1947
|
-
</kbd>
|
|
1948
|
-
</button>
|
|
1949
|
-
)}
|
|
1950
|
-
|
|
1951
|
-
{/* GitHub star — hidden in embedded mode (not OSS-distribution chrome). */}
|
|
1952
|
-
{!navCustomization.embedded && (
|
|
1953
|
-
<div className="hidden @min-[1100px]:block">
|
|
1954
|
-
<GitHubStarButton />
|
|
1955
|
-
</div>
|
|
1956
|
-
)}
|
|
1842
|
+
<div className="hidden @min-[1100px]:block">
|
|
1843
|
+
<GitHubStarButton />
|
|
1844
|
+
</div>
|
|
1957
1845
|
|
|
1958
1846
|
{/* AI investigations (self-hides when no agent CLI is present) */}
|
|
1959
1847
|
<GlobalDiagnoseButton />
|
|
1960
1848
|
|
|
1961
|
-
|
|
1962
|
-
Cloud embeds render chromeless anyway; the explicit gate is
|
|
1963
|
-
belt-and-braces for future chrome-bearing embedders. */}
|
|
1964
|
-
{!navCustomization.embedded && <CloudFunnelButton />}
|
|
1849
|
+
<CloudFunnelButton />
|
|
1965
1850
|
|
|
1966
1851
|
{/* Local terminal */}
|
|
1967
1852
|
{capabilities.localTerminal && (
|
|
@@ -1976,50 +1861,28 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1976
1861
|
</Tooltip>
|
|
1977
1862
|
)}
|
|
1978
1863
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
fight them (one writes to Radar's localStorage key, the other
|
|
1983
|
-
to the host's cookie/backend) and the user would see the theme
|
|
1984
|
-
bounce on every navigation between host routes and /c/:id. */}
|
|
1985
|
-
{!navCustomization.embedded && (
|
|
1986
|
-
<div className="hidden @min-[920px]:flex items-center">
|
|
1987
|
-
<ThemeToggle />
|
|
1988
|
-
</div>
|
|
1989
|
-
)}
|
|
1990
|
-
|
|
1991
|
-
{/* Help + Report-a-bug — standalone only (the left rail owns chrome;
|
|
1992
|
-
embedded hosts provide their own help/support). These replace the
|
|
1993
|
-
old floating bottom-right pair. Settings moved to the rail bottom. */}
|
|
1994
|
-
{showNavRail && (
|
|
1995
|
-
<>
|
|
1996
|
-
<Tooltip content="Keyboard shortcuts (?)">
|
|
1997
|
-
<button
|
|
1998
|
-
onClick={() => setShowHelp(true)}
|
|
1999
|
-
aria-label="Show keyboard shortcuts"
|
|
2000
|
-
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
2001
|
-
>
|
|
2002
|
-
<HelpCircle className="w-4 h-4" />
|
|
2003
|
-
</button>
|
|
2004
|
-
</Tooltip>
|
|
2005
|
-
<Tooltip content="Report a bug / Diagnostics">
|
|
2006
|
-
<button
|
|
2007
|
-
onClick={() => setShowDiagnostics(true)}
|
|
2008
|
-
aria-label="Open diagnostics"
|
|
2009
|
-
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
2010
|
-
>
|
|
2011
|
-
<Bug className="w-4 h-4" />
|
|
2012
|
-
</button>
|
|
2013
|
-
</Tooltip>
|
|
2014
|
-
</>
|
|
2015
|
-
)}
|
|
2016
|
-
|
|
2017
|
-
{/* Account moved to the rail bottom (standalone). Embedded never showed
|
|
2018
|
-
Radar's UserMenu — the host provides its own via rightExtras. */}
|
|
1864
|
+
<div className="hidden @min-[920px]:flex items-center">
|
|
1865
|
+
<ThemeToggle />
|
|
1866
|
+
</div>
|
|
2019
1867
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
1868
|
+
<Tooltip content="Keyboard shortcuts (?)">
|
|
1869
|
+
<button
|
|
1870
|
+
onClick={() => setShowHelp(true)}
|
|
1871
|
+
aria-label="Show keyboard shortcuts"
|
|
1872
|
+
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1873
|
+
>
|
|
1874
|
+
<HelpCircle className="w-4 h-4" />
|
|
1875
|
+
</button>
|
|
1876
|
+
</Tooltip>
|
|
1877
|
+
<Tooltip content="Report a bug / Diagnostics">
|
|
1878
|
+
<button
|
|
1879
|
+
onClick={() => setShowDiagnostics(true)}
|
|
1880
|
+
aria-label="Open diagnostics"
|
|
1881
|
+
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1882
|
+
>
|
|
1883
|
+
<Bug className="w-4 h-4" />
|
|
1884
|
+
</button>
|
|
1885
|
+
</Tooltip>
|
|
2023
1886
|
</div>
|
|
2024
1887
|
</header>
|
|
2025
1888
|
)}
|
|
@@ -2454,7 +2317,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2454
2317
|
initialTab={drawerInitialTab}
|
|
2455
2318
|
// No Radar header in chromeless embeds (Radar Hub) — anchor the drawer
|
|
2456
2319
|
// to the top of the content area instead of leaving a 49px gap.
|
|
2457
|
-
headerHeight={
|
|
2320
|
+
headerHeight={embedded ? 0 : undefined}
|
|
2458
2321
|
rightInset={contentGutter}
|
|
2459
2322
|
isOpen={resourceDrawer.isOpen}
|
|
2460
2323
|
expanded={drawerExpandedProp}
|
|
@@ -2523,7 +2386,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2523
2386
|
maximized = fills the frame. */}
|
|
2524
2387
|
{diagnoseOpen && (
|
|
2525
2388
|
<DiagnoseSurface
|
|
2526
|
-
topInset={
|
|
2389
|
+
topInset={embedded ? 0 : APP_HEADER_HEIGHT}
|
|
2527
2390
|
onBrowseIssues={() => setMainView('issues')}
|
|
2528
2391
|
onOpenResource={(ref, investigationRunID) => {
|
|
2529
2392
|
const resource: SelectedResource = {
|
|
@@ -2579,55 +2442,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2579
2442
|
{/* Spacer for dock */}
|
|
2580
2443
|
<DockSpacer />
|
|
2581
2444
|
|
|
2582
|
-
{/* Floating action buttons — embedded only, and not in chromeless (the
|
|
2583
|
-
host owns help/diagnostics chrome). Standalone moved help + bug to
|
|
2584
|
-
visible top-bar icons (the rail owns chrome). */}
|
|
2585
|
-
{!showNavRail && !chromeless && (
|
|
2586
|
-
<FloatingButtons showHelp={showHelp} showCommandPalette={showCommandPalette} showDiagnostics={showDiagnostics} onHelp={() => setShowHelp(true)} onBugReport={() => setShowDiagnostics(true)} />
|
|
2587
|
-
)}
|
|
2588
|
-
|
|
2589
2445
|
{/* Keyboard shortcut help overlay */}
|
|
2590
2446
|
{helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
|
|
2591
2447
|
|
|
2592
|
-
{/* Command palette */}
|
|
2593
|
-
{commandPaletteAnim.shouldRender && (
|
|
2594
|
-
<CommandPalette
|
|
2595
|
-
isOpen={commandPaletteAnim.isOpen}
|
|
2596
|
-
onClose={() => setShowCommandPalette(false)}
|
|
2597
|
-
onNavigateView={(view) => setMainView(view)}
|
|
2598
|
-
onNavigateKind={(kind, group) => {
|
|
2599
|
-
const params = new URLSearchParams(searchParams)
|
|
2600
|
-
params.delete('kind')
|
|
2601
|
-
if (group) params.set('apiGroup', group)
|
|
2602
|
-
else params.delete('apiGroup')
|
|
2603
|
-
params.delete('resource')
|
|
2604
|
-
params.delete('full')
|
|
2605
|
-
params.delete('tab')
|
|
2606
|
-
navigate({ pathname: `/resources/${kind}`, search: params.toString() })
|
|
2607
|
-
// Focus the table search after navigation — the user came from ⌘K
|
|
2608
|
-
// (keyboard flow) and expects to type a resource name immediately.
|
|
2609
|
-
setTimeout(() => {
|
|
2610
|
-
(document.querySelector('input[placeholder="Search... (press /)"]') as HTMLInputElement)?.focus()
|
|
2611
|
-
}, 100)
|
|
2612
|
-
}}
|
|
2613
|
-
onSwitchContext={(name) => switchContext.mutate(
|
|
2614
|
-
{ name },
|
|
2615
|
-
// Namespace filter from the previous context may not exist in the
|
|
2616
|
-
// new one — clear it so resource lists don't silently go empty.
|
|
2617
|
-
// The server clears all per-user picks on context switch already;
|
|
2618
|
-
// local state mirrors that via the namespace-scope effect.
|
|
2619
|
-
{ onSettled: () => setNamespaces([]) },
|
|
2620
|
-
)}
|
|
2621
|
-
onSetNamespaces={(ns) => {
|
|
2622
|
-
if (namespaceScope?.cacheScoped && ns.length !== 1) return
|
|
2623
|
-
setNamespaces(ns)
|
|
2624
|
-
setActiveNamespace.mutate({ namespaces: ns })
|
|
2625
|
-
}}
|
|
2626
|
-
onToggleTheme={toggleTheme}
|
|
2627
|
-
onShowDiagnostics={() => setShowDiagnostics(true)}
|
|
2628
|
-
/>
|
|
2629
|
-
)}
|
|
2630
|
-
|
|
2631
2448
|
{/* Diagnostics overlay */}
|
|
2632
2449
|
{diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
|
|
2633
2450
|
|
|
@@ -2666,29 +2483,6 @@ function DockSpacer() {
|
|
|
2666
2483
|
)
|
|
2667
2484
|
}
|
|
2668
2485
|
|
|
2669
|
-
// Floating action buttons that position themselves above the dock
|
|
2670
|
-
function FloatingButtons({ showHelp, showCommandPalette, showDiagnostics, onHelp, onBugReport }: { showHelp: boolean; showCommandPalette: boolean; showDiagnostics: boolean; onHelp: () => void; onBugReport: () => void }) {
|
|
2671
|
-
const { tabs } = useDock()
|
|
2672
|
-
if (showHelp || showCommandPalette || showDiagnostics) return null
|
|
2673
|
-
// When dock tab bar is visible (36px), shift the buttons up above it
|
|
2674
|
-
const bottom = tabs.length > 0 ? 'bottom-10' : 'bottom-2'
|
|
2675
|
-
const btnClass = 'w-7 h-7 flex items-center justify-center rounded-full bg-theme-elevated/80 hover:bg-theme-hover border border-theme-border-light text-theme-text-tertiary hover:text-theme-text-secondary text-xs font-medium shadow-sm backdrop-blur-sm transition-all'
|
|
2676
|
-
return (
|
|
2677
|
-
<div className={`fixed ${bottom} right-4 z-40 flex items-center gap-1.5`}>
|
|
2678
|
-
<Tooltip content="Report bug / Diagnostics" position="top">
|
|
2679
|
-
<button onClick={onBugReport} aria-label="Open diagnostics" className={btnClass}>
|
|
2680
|
-
<Bug className="w-3.5 h-3.5" />
|
|
2681
|
-
</button>
|
|
2682
|
-
</Tooltip>
|
|
2683
|
-
<Tooltip content="Keyboard shortcuts (?)" position="top">
|
|
2684
|
-
<button onClick={onHelp} aria-label="Show keyboard shortcuts" className={btnClass}>
|
|
2685
|
-
?
|
|
2686
|
-
</button>
|
|
2687
|
-
</Tooltip>
|
|
2688
|
-
</div>
|
|
2689
|
-
)
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
2486
|
// Main App component wrapped with providers
|
|
2693
2487
|
function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
|
|
2694
2488
|
return (
|
|
@@ -2710,37 +2504,6 @@ function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadSt
|
|
|
2710
2504
|
)
|
|
2711
2505
|
}
|
|
2712
2506
|
|
|
2713
|
-
// Header brand: emerald-square radar icon + stacked "Radar" / "by Skyhook"
|
|
2714
|
-
// wordmark. Shares its visual shape with the radar-hub-web shell so the
|
|
2715
|
-
// standalone OSS app and the embedded Cloud experience read as the same
|
|
2716
|
-
// product, and is narrow enough to leave room for the cluster switcher
|
|
2717
|
-
// and nav block on standard laptop viewports.
|
|
2718
|
-
function Logo() {
|
|
2719
|
-
return (
|
|
2720
|
-
<div className="flex items-center gap-2.5">
|
|
2721
|
-
<div className="relative w-7 h-7 rounded-lg overflow-hidden flex-shrink-0 bg-emerald-500/10 border border-emerald-500/20">
|
|
2722
|
-
<img
|
|
2723
|
-
src={radarLogoUrl}
|
|
2724
|
-
alt=""
|
|
2725
|
-
aria-hidden
|
|
2726
|
-
className="w-full h-full p-0.5"
|
|
2727
|
-
// Fail loud on a missing/blocked asset rather than rendering an
|
|
2728
|
-
// empty emerald square next to the wordmark — the latter reads
|
|
2729
|
-
// as broken chrome with no diagnostics. Most likely cause is a
|
|
2730
|
-
// build/deploy path mismatch.
|
|
2731
|
-
onError={(e) =>
|
|
2732
|
-
console.error('Radar logo asset failed to load:', (e.currentTarget as HTMLImageElement).src)
|
|
2733
|
-
}
|
|
2734
|
-
/>
|
|
2735
|
-
</div>
|
|
2736
|
-
<div className="flex flex-col leading-none">
|
|
2737
|
-
<span className="font-semibold text-[15px] tracking-tight text-theme-text-primary">Radar</span>
|
|
2738
|
-
<span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">by Skyhook</span>
|
|
2739
|
-
</div>
|
|
2740
|
-
</div>
|
|
2741
|
-
)
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
2507
|
// GitHub star button with live star count + programmatic starring via gh CLI
|
|
2745
2508
|
// Shows a callout popover when the backend says shouldPrompt is true (synced with CLI state)
|
|
2746
2509
|
function GitHubStarButton() {
|