@skyhook-io/radar-app 1.8.6 → 1.8.8
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 +7 -1
- package/package.json +12 -10
- package/src/App.tsx +278 -230
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1462 -246
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +775 -80
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +103 -72
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +494 -229
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +116 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +81 -14
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +320 -127
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +66 -116
- package/src/components/settings/SettingsDialog.tsx +1290 -283
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +81 -13
- package/src/components/timeline/TimelineView.tsx +842 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +912 -245
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +162 -1
- package/src/index.ts +12 -0
- package/src/main.tsx +1 -1
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
formatGitOpsSourceUrl,
|
|
20
20
|
getGitOpsResourceStatus,
|
|
21
21
|
getGitOpsTool,
|
|
22
|
+
isArgoOperationInProgress,
|
|
22
23
|
isArgoSuspendedByRadar,
|
|
23
24
|
gitOpsInsightChangeKey,
|
|
24
25
|
initNavigationMap,
|
|
@@ -46,8 +47,10 @@ import { useToast } from '../ui/Toast'
|
|
|
46
47
|
|
|
47
48
|
import {
|
|
48
49
|
fetchJSON,
|
|
50
|
+
buildArgoResourceSyncVars,
|
|
49
51
|
useApplyResource,
|
|
50
52
|
useArgoRefresh,
|
|
53
|
+
useArgoResourceValidation,
|
|
51
54
|
useArgoResume,
|
|
52
55
|
useArgoRollback,
|
|
53
56
|
useArgoSuspend,
|
|
@@ -66,6 +69,8 @@ import { useConnection } from '../../context/ConnectionContext'
|
|
|
66
69
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
67
70
|
import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
|
|
68
71
|
import { CodeViewer } from '../ui/CodeViewer'
|
|
72
|
+
import { ArgoResourceDiffLoader } from './ArgoResourceDiffLoader'
|
|
73
|
+
import { RevisionMetaChip } from './RevisionMetaChip'
|
|
69
74
|
import type { GitOpsHistoryItem } from '@skyhook-io/k8s-ui'
|
|
70
75
|
|
|
71
76
|
const GITOPS_KINDS: APIResource[] = [
|
|
@@ -80,6 +85,10 @@ const GITOPS_KINDS: APIResource[] = [
|
|
|
80
85
|
{ name: 'alerts', kind: 'Alert', group: 'notification.toolkit.fluxcd.io', version: 'v1beta3', namespaced: true, verbs: ['list', 'get'], isCrd: true },
|
|
81
86
|
]
|
|
82
87
|
|
|
88
|
+
type ArgoSyncDialogTarget =
|
|
89
|
+
| { scope: 'application' }
|
|
90
|
+
| { scope: 'resource'; resource: GitOpsInsightRef }
|
|
91
|
+
|
|
83
92
|
const KIND_BY_NAME = new Map(GITOPS_KINDS.map((k) => [k.name, k]))
|
|
84
93
|
|
|
85
94
|
// Rows are the table's primary content; their poll cadence is what the toolbar
|
|
@@ -97,12 +106,15 @@ interface GitOpsViewProps {
|
|
|
97
106
|
namespaces: string[]
|
|
98
107
|
onOpenResource: (resource: SelectedResource) => void
|
|
99
108
|
onClearNamespaces?: () => void
|
|
109
|
+
// Opens the global Settings dialog — backs the "Connect Argo CD" hint on the
|
|
110
|
+
// Changes tab of an Argo Application detail page.
|
|
111
|
+
onOpenSettings?: () => void
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
export function GitOpsView({ namespaces, onOpenResource, onClearNamespaces }: GitOpsViewProps) {
|
|
114
|
+
export function GitOpsView({ namespaces, onOpenResource, onClearNamespaces, onOpenSettings }: GitOpsViewProps) {
|
|
103
115
|
const location = useLocation()
|
|
104
116
|
if (location.pathname.startsWith('/gitops/detail/')) {
|
|
105
|
-
return <GitOpsDetailView namespaces={namespaces} onOpenResource={onOpenResource} />
|
|
117
|
+
return <GitOpsDetailView namespaces={namespaces} onOpenResource={onOpenResource} onOpenSettings={onOpenSettings} />
|
|
106
118
|
}
|
|
107
119
|
return <GitOpsTableView namespaces={namespaces} onClearNamespaces={onClearNamespaces} />
|
|
108
120
|
}
|
|
@@ -329,7 +341,7 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
|
|
|
329
341
|
)
|
|
330
342
|
}
|
|
331
343
|
|
|
332
|
-
function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
344
|
+
function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOpsViewProps) {
|
|
333
345
|
const location = useLocation()
|
|
334
346
|
const navigate = useNavigate()
|
|
335
347
|
const { showError, showSuccess } = useToast()
|
|
@@ -403,6 +415,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
403
415
|
const [helmValuesOpen, setHelmValuesOpen] = useState(false)
|
|
404
416
|
|
|
405
417
|
const argoSync = useArgoSync()
|
|
418
|
+
const argoResourceValidation = useArgoResourceValidation()
|
|
406
419
|
const argoRefresh = useArgoRefresh()
|
|
407
420
|
const argoTerminate = useArgoTerminate()
|
|
408
421
|
const argoSuspend = useArgoSuspend()
|
|
@@ -414,13 +427,23 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
414
427
|
const fluxSuspend = useFluxSuspend()
|
|
415
428
|
const fluxResume = useFluxResume()
|
|
416
429
|
|
|
417
|
-
const [
|
|
430
|
+
const [syncDialogTarget, setSyncDialogTarget] = useState<ArgoSyncDialogTarget | null>(null)
|
|
418
431
|
// Doubles as the "open" flag (truthy = dialog open) and the data carrier
|
|
419
432
|
// for which history entry to roll back to.
|
|
420
433
|
const [rollbackTarget, setRollbackTarget] = useState<GitOpsHistoryItem | null>(null)
|
|
421
434
|
// Disambiguates which refresh button is in flight (both share argoRefresh).
|
|
422
435
|
const [refreshKind, setRefreshKind] = useState<'normal' | 'hard'>('normal')
|
|
423
436
|
|
|
437
|
+
function openArgoSyncDialog(target: ArgoSyncDialogTarget) {
|
|
438
|
+
argoResourceValidation.reset()
|
|
439
|
+
setSyncDialogTarget(target)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function closeArgoSyncDialog() {
|
|
443
|
+
argoResourceValidation.reset()
|
|
444
|
+
setSyncDialogTarget(null)
|
|
445
|
+
}
|
|
446
|
+
|
|
424
447
|
const detailRow = resourceQ.data ? normalizeDetailResource(kind, group, resourceQ.data) : null
|
|
425
448
|
const tree = treeQ.data ?? null
|
|
426
449
|
const helmValues = useMemo(() => extractHelmValues(kind, resourceQ.data), [kind, resourceQ.data])
|
|
@@ -436,6 +459,13 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
436
459
|
function openResourceFromTree(ref: GitOpsTreeRef | GitOpsInsightRef) {
|
|
437
460
|
if (isGitOpsDetailRef(ref) && isValidKubernetesName(ref.name)) {
|
|
438
461
|
const detailKind = kindToPlural(ref.kind)
|
|
462
|
+
// The tree's root node is this page's own subject — clicking it must not
|
|
463
|
+
// open a nested copy of the same detail page (which stacks an identical
|
|
464
|
+
// "GitOps / X / X" breadcrumb, and again, ad infinitum). A self-reference
|
|
465
|
+
// is a no-op; the header already represents this resource.
|
|
466
|
+
if (detailKind === kind && (ref.namespace || '') === (namespace || '') && ref.name === name) {
|
|
467
|
+
return
|
|
468
|
+
}
|
|
439
469
|
const params = new URLSearchParams()
|
|
440
470
|
if (ref.group) params.set('apiGroup', ref.group)
|
|
441
471
|
// Lineage breadcrumb support: when the user opens a child GitOps CR
|
|
@@ -457,13 +487,14 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
457
487
|
}
|
|
458
488
|
|
|
459
489
|
const isRunning = resourceQ.data?.status?.operationState?.phase === 'Running'
|
|
490
|
+
const operationInProgress = isArgoOperationInProgress(resourceQ.data)
|
|
460
491
|
const isFluxWorkload = kind === 'kustomizations' || kind === 'helmreleases'
|
|
461
492
|
const isFlux = tool === 'flux'
|
|
462
493
|
const isArgoApp = kind === 'applications'
|
|
463
494
|
|
|
464
495
|
// Detail-page shortcuts. Skip when a modal is already open so a stray "s"
|
|
465
496
|
// in an input field doesn't pop another sync dialog.
|
|
466
|
-
const shortcutsEnabled = !
|
|
497
|
+
const shortcutsEnabled = !syncDialogTarget && !rollbackTarget
|
|
467
498
|
useRegisterShortcut({
|
|
468
499
|
id: 'gitops-detail-sync',
|
|
469
500
|
keys: 's',
|
|
@@ -471,11 +502,11 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
471
502
|
category: 'GitOps',
|
|
472
503
|
scope: 'gitops',
|
|
473
504
|
handler: () => {
|
|
474
|
-
if (effectiveSuspended || terminating) return
|
|
475
|
-
if (isArgoApp)
|
|
505
|
+
if (effectiveSuspended || terminating || operationInProgress) return
|
|
506
|
+
if (isArgoApp) openArgoSyncDialog({ scope: 'application' })
|
|
476
507
|
else if (isFlux) fluxReconcile.mutate({ kind, namespace, name })
|
|
477
508
|
},
|
|
478
|
-
enabled: shortcutsEnabled && (isArgoApp || isFlux) && !effectiveSuspended && !terminating,
|
|
509
|
+
enabled: shortcutsEnabled && (isArgoApp || isFlux) && !effectiveSuspended && !terminating && !(isArgoApp && operationInProgress),
|
|
479
510
|
})
|
|
480
511
|
useRegisterShortcut({
|
|
481
512
|
id: 'gitops-detail-refresh',
|
|
@@ -529,7 +560,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
529
560
|
}
|
|
530
561
|
|
|
531
562
|
const argoHandlers: ArgoActionHandlers | undefined = isArgoApp ? {
|
|
532
|
-
onSyncRequested: () =>
|
|
563
|
+
onSyncRequested: () => openArgoSyncDialog({ scope: 'application' }),
|
|
533
564
|
onRefresh: (refreshType) => {
|
|
534
565
|
setRefreshKind(refreshType)
|
|
535
566
|
argoRefresh.mutate({ namespace, name, hard: refreshType === 'hard' })
|
|
@@ -545,6 +576,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
545
576
|
resuming: argoResume.isPending,
|
|
546
577
|
autoSyncEnabled: argoAutoSyncEnabled,
|
|
547
578
|
isRunning,
|
|
579
|
+
operationInProgress,
|
|
548
580
|
} : undefined
|
|
549
581
|
|
|
550
582
|
const fluxHandlers: FluxActionHandlers | undefined = isFlux ? {
|
|
@@ -576,6 +608,13 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
576
608
|
detail={detail}
|
|
577
609
|
insight={insightsQ.data ?? null}
|
|
578
610
|
insightLoading={insightsQ.isLoading}
|
|
611
|
+
renderRevisionMeta={
|
|
612
|
+
isArgoApp && insightsQ.data?.capabilities?.revisionMetadataAvailable
|
|
613
|
+
? (revision) => (
|
|
614
|
+
<RevisionMetaChip appNamespace={namespace} appName={name} revision={revision} />
|
|
615
|
+
)
|
|
616
|
+
: undefined
|
|
617
|
+
}
|
|
579
618
|
onSelectIssue={(issue) => {
|
|
580
619
|
const ref = issue.refs?.[0]
|
|
581
620
|
if (!ref) return
|
|
@@ -679,7 +718,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
679
718
|
<GitOpsActivityInsightView
|
|
680
719
|
insight={insightsQ.data}
|
|
681
720
|
error={insightsQ.error as Error | null}
|
|
682
|
-
onRollback={isArgoApp ? (item) => {
|
|
721
|
+
onRollback={isArgoApp && !operationInProgress ? (item) => {
|
|
683
722
|
if (parseArgoRollbackID(item.id) == null) return
|
|
684
723
|
setRollbackTarget(item)
|
|
685
724
|
} : undefined}
|
|
@@ -692,8 +731,22 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
692
731
|
insight={insightsQ.data}
|
|
693
732
|
error={insightsQ.error as Error | null}
|
|
694
733
|
onOpenResource={openResourceFromTree}
|
|
734
|
+
onSyncResource={isArgoApp ? (resource) => openArgoSyncDialog({ scope: 'resource', resource }) : undefined}
|
|
735
|
+
syncResourceDisabledReason={isArgoApp ? (
|
|
736
|
+
terminating
|
|
737
|
+
? terminatingActionTooltip
|
|
738
|
+
: effectiveSuspended
|
|
739
|
+
? 'Resume the Application before syncing a resource.'
|
|
740
|
+
: operationInProgress || argoSync.isPending
|
|
741
|
+
? 'Wait for the current sync operation to finish.'
|
|
742
|
+
: undefined
|
|
743
|
+
) : undefined}
|
|
695
744
|
focusKey={changesFocusKey}
|
|
696
745
|
tree={tree}
|
|
746
|
+
renderResourceDiff={isArgoApp ? (ref) => (
|
|
747
|
+
<ArgoResourceDiffLoader appNamespace={namespace} appName={name} resourceRef={ref} />
|
|
748
|
+
) : undefined}
|
|
749
|
+
onOpenSettings={onOpenSettings}
|
|
697
750
|
/>
|
|
698
751
|
)
|
|
699
752
|
}
|
|
@@ -739,13 +792,27 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
739
792
|
{isArgoApp && (
|
|
740
793
|
<>
|
|
741
794
|
<SyncOptionsDialog
|
|
742
|
-
open={
|
|
795
|
+
open={!!syncDialogTarget}
|
|
743
796
|
appLabel={`${namespace}/${name}`}
|
|
797
|
+
resource={syncDialogTarget?.scope === 'resource' ? syncDialogTarget.resource : undefined}
|
|
744
798
|
pending={argoSync.isPending}
|
|
745
|
-
|
|
799
|
+
autoSyncEnabled={argoAutoSyncEnabled}
|
|
800
|
+
validationPending={argoResourceValidation.isPending}
|
|
801
|
+
operationInProgress={operationInProgress}
|
|
802
|
+
validationResult={argoResourceValidation.data}
|
|
803
|
+
validationError={argoResourceValidation.error?.message}
|
|
804
|
+
onCancel={closeArgoSyncDialog}
|
|
805
|
+
onValidationReset={() => argoResourceValidation.reset()}
|
|
806
|
+
onValidate={syncDialogTarget?.scope === 'resource' ? (opts) => {
|
|
807
|
+
argoResourceValidation.mutate(buildArgoResourceSyncVars(namespace, name, syncDialogTarget.resource, opts))
|
|
808
|
+
} : undefined}
|
|
746
809
|
onConfirm={(opts) => {
|
|
747
|
-
|
|
748
|
-
|
|
810
|
+
if (!syncDialogTarget) return
|
|
811
|
+
const variables = syncDialogTarget.scope === 'resource'
|
|
812
|
+
? buildArgoResourceSyncVars(namespace, name, syncDialogTarget.resource, opts)
|
|
813
|
+
: { namespace, name, ...opts }
|
|
814
|
+
argoSync.mutate(variables, {
|
|
815
|
+
onSettled: closeArgoSyncDialog,
|
|
749
816
|
})
|
|
750
817
|
}}
|
|
751
818
|
/>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ShieldCheck, ShieldAlert } from 'lucide-react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useArgoRevisionMetadata } from '../../api/client'
|
|
4
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
5
|
+
|
|
6
|
+
// Argo returns the author as "Name <email>"; show just the name.
|
|
7
|
+
function authorName(author?: string): string {
|
|
8
|
+
if (!author) return ''
|
|
9
|
+
const lt = author.indexOf('<')
|
|
10
|
+
return (lt >= 0 ? author.slice(0, lt) : author).trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// signatureInfo is a raw GPG verification line. "Good signature" → verified;
|
|
14
|
+
// any other non-empty value → a check ran but didn't verify; empty → unsigned.
|
|
15
|
+
function signatureState(sig?: string): 'good' | 'bad' | null {
|
|
16
|
+
if (!sig) return null
|
|
17
|
+
return /good signature/i.test(sig) ? 'good' : 'bad'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// RevisionMetaChip hydrates the Argo status-strip revision with Git commit
|
|
21
|
+
// detail (author, subject, signature) fetched from the Argo CD API. Renders
|
|
22
|
+
// nothing until data arrives, so the bare SHA is never blocked; the whole thing
|
|
23
|
+
// is gated by the host on capabilities.revisionMetadataAvailable.
|
|
24
|
+
export function RevisionMetaChip({
|
|
25
|
+
appNamespace,
|
|
26
|
+
appName,
|
|
27
|
+
revision,
|
|
28
|
+
}: {
|
|
29
|
+
appNamespace: string
|
|
30
|
+
appName: string
|
|
31
|
+
revision: string
|
|
32
|
+
}): ReactNode {
|
|
33
|
+
// The host only renders this chip when revision metadata is available, so no
|
|
34
|
+
// extra enabled gate is needed — the hook's own appName/revision guard suffices.
|
|
35
|
+
const { data } = useArgoRevisionMetadata(appNamespace, appName, revision)
|
|
36
|
+
if (!data) return null
|
|
37
|
+
|
|
38
|
+
const author = authorName(data.author)
|
|
39
|
+
const sig = signatureState(data.signatureInfo)
|
|
40
|
+
const subject = data.message?.split('\n')[0]?.trim()
|
|
41
|
+
if (!author && !sig && !subject) return null
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{author && <span className="shrink-0 text-theme-text-secondary">· {author}</span>}
|
|
46
|
+
{subject && (
|
|
47
|
+
<Tooltip content={data.message ?? subject} delay={300} wrapperClassName="min-w-0">
|
|
48
|
+
<span className="max-w-[32ch] truncate text-theme-text-tertiary">“{subject}”</span>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
)}
|
|
51
|
+
{sig === 'good' && (
|
|
52
|
+
<Tooltip content={data.signatureInfo ?? 'Signed commit'} delay={300} wrapperClassName="inline-flex shrink-0">
|
|
53
|
+
<ShieldCheck className="h-3 w-3 text-green-600 dark:text-green-400/80" />
|
|
54
|
+
</Tooltip>
|
|
55
|
+
)}
|
|
56
|
+
{sig === 'bad' && (
|
|
57
|
+
<Tooltip content={data.signatureInfo ?? 'Signature not verified'} delay={300} wrapperClassName="inline-flex shrink-0">
|
|
58
|
+
<ShieldAlert className="h-3 w-3 text-amber-600 dark:text-amber-400/80" />
|
|
59
|
+
</Tooltip>
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
2
|
import { Search, RefreshCw, Package, Database, AlertCircle, ExternalLink, ChevronDown, Star, Shield, BadgeCheck, Building2, Globe, ArrowUpDown, FileJson, PenTool } from 'lucide-react'
|
|
3
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { useHelmRepositories, useSearchCharts, useUpdateRepository, useUpdateRepositorySilent, useArtifactHubSearch, type ArtifactHubSortOption } from '../../api/client'
|
|
6
6
|
import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
|
|
@@ -254,8 +254,7 @@ export function ChartBrowser({ onChartSelect }: ChartBrowserProps) {
|
|
|
254
254
|
{/* Search */}
|
|
255
255
|
<div className="flex-1 relative">
|
|
256
256
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
257
|
-
<
|
|
258
|
-
type="text"
|
|
257
|
+
<Input
|
|
259
258
|
placeholder={chartSource === 'local' ? "Search charts..." : "Search ArtifactHub..."}
|
|
260
259
|
value={searchTerm}
|
|
261
260
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
Package,
|
|
18
18
|
Settings,
|
|
19
19
|
} from 'lucide-react'
|
|
20
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
20
|
+
import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
|
|
21
21
|
import {
|
|
22
22
|
useCloudRole,
|
|
23
23
|
useHelmHooksDiff,
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
import type { HelmHook, HelmRevision, HooksDiff, ResourceDiff } from '../../types'
|
|
31
31
|
import { getHelmStatusColor, getKindBadgeColor, SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
32
32
|
import { formatDate } from './helm-utils'
|
|
33
|
-
import { DiffLine, hasDiffBodyChange } from './ManifestDiffViewer'
|
|
34
33
|
import { RoleGatedPanel } from './RoleGatedPanel'
|
|
35
34
|
import { Tooltip } from '../ui/Tooltip'
|
|
36
35
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { isUpgradeSourceIssueActionable } from './HelmReleaseDrawer'
|
|
3
|
+
|
|
4
|
+
describe('isUpgradeSourceIssueActionable', () => {
|
|
5
|
+
it('keeps classic repository ambiguity informational', () => {
|
|
6
|
+
expect(isUpgradeSourceIssueActionable('ambiguous_repository')).toBe(false)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('lets OCI registration help untracked and repo-index states', () => {
|
|
10
|
+
expect(isUpgradeSourceIssueActionable('untracked')).toBe(true)
|
|
11
|
+
expect(isUpgradeSourceIssueActionable('repo_index_error')).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('does not treat a missing reason code as actionable', () => {
|
|
15
|
+
expect(isUpgradeSourceIssueActionable(undefined)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
})
|