@skyhook-io/k8s-ui 1.18.0 → 1.18.2
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 +21 -0
- package/package.json +1 -1
- package/src/components/applications/ApplicationsView.tsx +8 -7
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
- package/src/components/compare/CompareResourcePicker.tsx +18 -4
- package/src/components/compare/ResourceCompareView.tsx +6 -2
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
- package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
- package/src/components/gitops/GitOpsTableView.tsx +46 -12
- package/src/components/gitops/detail-helpers.test.ts +58 -13
- package/src/components/gitops/detail-helpers.ts +37 -6
- package/src/components/gitops/index.ts +1 -0
- package/src/components/issues/IssuesView.tsx +16 -11
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/severity.ts +1 -0
- package/src/components/logs/LogCore.tsx +13 -11
- package/src/components/logs/LogsViewer.test.tsx +104 -59
- package/src/components/logs/LogsViewer.tsx +28 -8
- package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
- package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
- package/src/components/namespace-switcher/NamespacePicker.tsx +44 -8
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
- package/src/components/resources/ResourcesSidebar.tsx +62 -26
- package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
- package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
- package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
- package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
- package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
- package/src/components/resources/ResourcesView.tsx +342 -83
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +15 -0
- package/src/components/resources/printer-columns.test.ts +36 -0
- package/src/components/resources/printer-columns.ts +12 -1
- package/src/components/resources/readiness-gates.test.ts +95 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
- package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
- package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
- package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
- package/src/components/resources/renderers/PodRenderer.tsx +32 -9
- package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
- package/src/components/resources/resource-utils-crossplane.ts +44 -8
- package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
- package/src/components/resources/resource-utils-flagger.ts +76 -0
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
- package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
- package/src/components/resources/resource-utils-trivy.test.ts +87 -0
- package/src/components/resources/resource-utils-trivy.ts +122 -0
- package/src/components/resources/resource-utils.ts +80 -2
- package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
- package/src/components/shared/CreateResourceDialog.tsx +5 -0
- package/src/components/shared/DetailShell.phone.test.tsx +54 -0
- package/src/components/shared/DetailShell.tsx +21 -8
- package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
- package/src/components/shared/EditableYamlView.test.tsx +58 -0
- package/src/components/shared/EditableYamlView.tsx +18 -3
- package/src/components/shared/FilterRailPanel.test.tsx +39 -0
- package/src/components/shared/FilterRailPanel.tsx +62 -0
- package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
- package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
- package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
- package/src/components/shared/ManagedByChip.test.tsx +22 -0
- package/src/components/shared/ManagedByChip.tsx +158 -5
- package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
- package/src/components/shared/ResourceActionsBar.tsx +31 -9
- package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +69 -31
- package/src/components/shared/SetImageDialog.tsx +3 -0
- package/src/components/timeline/TimelineList.tsx +5 -1
- package/src/components/topology/TopologyControls.tsx +1 -1
- package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
- package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
- package/src/components/ui/FetchResult.test.tsx +19 -2
- package/src/components/ui/FetchResult.tsx +13 -1
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
- package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
- package/src/components/ui/MultiSelectPicker.tsx +20 -2
- package/src/components/ui/PageHeader.tsx +7 -4
- package/src/components/ui/SummaryTile.tsx +13 -3
- package/src/components/ui/Toast.tsx +33 -26
- package/src/components/ui/Tooltip.touch.test.tsx +55 -0
- package/src/components/ui/Tooltip.tsx +11 -2
- package/src/components/ui/YamlReview.test.tsx +52 -0
- package/src/components/ui/YamlReview.tsx +7 -0
- package/src/components/ui/drawer-components.test.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +302 -18
- package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +73 -0
- package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
- package/src/components/workload/WorkloadView.tsx +105 -34
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
- package/src/hooks/useKeepInViewport.test.ts +27 -0
- package/src/hooks/useKeepInViewport.ts +87 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/usePhoneLayout.ts +12 -0
- package/src/theme/tailwind-theme.css +5 -0
- package/src/types/core.ts +119 -3
- package/src/types/gitops-flux-inventory.test.ts +24 -0
- package/src/types/gitops.ts +5 -5
- package/src/types/index.ts +3 -0
- package/src/types/read-coverage.ts +65 -0
- package/src/types/resource-coverage.ts +40 -0
- package/src/utils/api-resources.ts +1 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +6 -1
- package/src/utils/gitops-owner.test.ts +42 -1
- package/src/utils/gitops-owner.ts +158 -8
- package/src/utils/gitops-write-guard.test.ts +332 -0
- package/src/utils/gitops-write-guard.ts +175 -11
- package/src/utils/index.ts +3 -0
- package/src/utils/read-actions.test.ts +22 -0
- package/src/utils/read-actions.ts +90 -0
- package/src/utils/resource-coverage.test.ts +121 -0
- package/src/utils/resource-coverage.ts +117 -0
- package/src/utils/touch-intent.test.ts +39 -0
- package/src/utils/touch-intent.ts +35 -0
- package/vitest.config.ts +26 -0
package/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Shared, source-distributed Kubernetes UI components used by Radar.
|
|
4
4
|
|
|
5
|
+
## Toast updates
|
|
6
|
+
|
|
7
|
+
`useToast().showToast(message, { id })` updates an existing toast with that ID
|
|
8
|
+
in place and restarts its dismiss timer. Omit `id` for independent toasts.
|
|
9
|
+
Set `duration: 0` to keep a progress toast visible until its result replaces it;
|
|
10
|
+
omit `duration` on the result to use the normal dismiss time.
|
|
11
|
+
|
|
5
12
|
## Local terminal metadata
|
|
6
13
|
|
|
7
14
|
`LocalTerminalTab` accepts optional `onSessionInfo` and `toolbarExtra` props.
|
|
@@ -42,6 +49,12 @@ resolve: {
|
|
|
42
49
|
|
|
43
50
|
webpack 5 hosts need the same mapping in `resolve.alias`. Without it the build fails to resolve the worker import.
|
|
44
51
|
|
|
52
|
+
## Resource read coverage
|
|
53
|
+
|
|
54
|
+
`ResourceReadCoverage` describes the scope an answer covers (and `narrowedBy`, when the caller's access, Radar's pin or what Radar collects narrowed it), what Radar read of it, and coded gaps naming what it did not read and why. `READ_ACTIONS` / `readActionFor` say what resolves each code. `ResourceCoverage` additionally permits `unknown` in UI adapters for older Radar instances. It does not describe object freshness or count truncation.
|
|
55
|
+
|
|
56
|
+
Pass list coverage through `ResourceQueryResult.coverage`, and per-kind count coverage through `ResourcesView.resourceCoverage` / `ResourcesSidebar.resourceCoverage`. Counts stay useful for sizing; a partial count displays as a lower bound and a partial zero does not hide a kind as empty; a category total is a lower bound when any of its kinds is partial or narrowed by what Radar collects. A complete list whose scope Radar's collection narrowed (`narrowedBy: ["namespace_collection"]`) gets a neutral note naming the collected namespaces. Unknown or absent coverage keeps the presentation that predates coverage. Partial list notices name the affected namespaces when the gap names at most three; larger or unnamed sets keep the compact wording. Loaded list counts retain their coverage when cached. `GitOpsTableView` accepts aggregate `coverageStatus` and `countsPartial` keys; hosts must combine coverage across every cluster/source contributing to the view, and a host that passes no `coverageStatus` gets the unqualified view. Keep source-specific metadata in the host rather than repeating it on every row.
|
|
57
|
+
|
|
45
58
|
## Resource kind views and certainty
|
|
46
59
|
|
|
47
60
|
`ResourcesView.renderKindView(kind)` optionally replaces a kind's table while
|
|
@@ -70,3 +83,11 @@ tooltip. `certaintyGlyph` and `certaintyValueLabel` expose the same vocabulary.
|
|
|
70
83
|
`ActionConfirmDialog` adds context, reviewed writes and optional typed acknowledgement to the simpler `ConfirmDialog`. Hosts own loading, permission checks, stale-fact validation and result classification. Its `result` binds the state (`error`, `unknown` or `partial`), message and optional heading; unknown and partial results lock another submit. Retain that result outside the dialog across dismissal and remount, and clear it only after explicitly reviewing current state. The component does not infer outcomes from HTTP status. Remount it when changing the reviewed target or action, so acknowledgement and disclosure state start fresh. The default error heading makes no claim about whether a write occurred; use a stronger result title only when the producer proves it.
|
|
71
84
|
|
|
72
85
|
Existing CNPG primitive exports remain available and delegate to these shared implementations.
|
|
86
|
+
|
|
87
|
+
`PrinterColumnDef.jsonPath` optionally carries source provenance from newer Radar servers. The UI does not evaluate it: values still come from the server's table converter. Without it, printer-cell tones stay generic. Crossplane pause qualification only recognizes exact Ready/Synced condition paths and corroborated annotation/condition evidence; same-named unrelated columns retain their normal tones. `ConditionTone` also supports opt-in `neutral`; `defaultConditionTone` retains its original return vocabulary. `ComposedRefStatus.pauseState` optionally supplies recorded Crossplane pause context for a composed-resource row.
|
|
88
|
+
|
|
89
|
+
`ForceDeleteConfirmDialog.resourceData` optionally supplies the current object for Crossplane pause context. Paused MR/XR/Claim shapes holding a known Crossplane finalizer receive a cleanup warning; the prop does not change confirmation or force-delete behavior. YAML operation context uses the existing baseline and predicted manifests; removing a pause annotation requests resumption rather than proving it.
|
|
90
|
+
|
|
91
|
+
`YamlReview.mode` distinguishes Apply, strict Create and Update (default: `apply`). Hosts using strict creation should pass `create` so an unpreviewable new document does not imply unknown live pause state. Crossplane operation context reports what the server preview actually keeps or clears; unsupported dry-run documents retain conditional uncertainty rather than treating submitted YAML as a live baseline.
|
|
92
|
+
|
|
93
|
+
For a single editor target, `YamlReview.resourceData` may retain its raw condition evidence when the review YAML intentionally omits status. `EditableYamlView` binds that snapshot to its preview. It is not used to classify unrelated documents in a batch. Generic Apply with neither a known Crossplane shape nor raw acknowledged pause does not infer a resource’s controller from the annotation alone.
|
package/package.json
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { ReadyBar } from './ReadyBar'
|
|
47
47
|
import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo, BatchSignalChip } from './AppChips'
|
|
48
48
|
import { AppIdentityTooltip, EnvHint, RolloutSummaryTooltip } from './AppTooltips'
|
|
49
|
+
import { FilterRailPanel } from '../shared/FilterRailPanel'
|
|
49
50
|
|
|
50
51
|
function ApplicationRuntimeCell({ apps, workloadClass, ready, desired }: { apps: AppRow[]; workloadClass: AppWorkloadClass; ready: number; desired: number }) {
|
|
51
52
|
if (workloadClass !== 'job') {
|
|
@@ -337,6 +338,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
337
338
|
// showSystem lives in the Filters rail, so Clear resets it too (and its
|
|
338
339
|
// non-default ON state counts as an active filter that surfaces the button).
|
|
339
340
|
const anyFilterActive = filters.isActive
|
|
341
|
+
const activeFilterCount = fHealth.size + fClass.size + fType.size + fEnv.size + fSource.size + (systemCount > 0 && showSystem ? 1 : 0)
|
|
340
342
|
const clearAllFilters = () => {
|
|
341
343
|
filters.clearAll()
|
|
342
344
|
}
|
|
@@ -355,7 +357,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
355
357
|
actions={
|
|
356
358
|
<>
|
|
357
359
|
{headerActions}
|
|
358
|
-
<SummaryTile label={total === 1 ? 'application' : 'applications'} value={total} loading={initialLoading} />
|
|
360
|
+
<SummaryTile label={total === 1 ? 'application' : 'applications'} phoneLabel={total === 1 ? 'app' : 'apps'} value={total} loading={initialLoading} />
|
|
359
361
|
{healthTile('unhealthy', 'error')}
|
|
360
362
|
{healthTile('degraded', 'warning')}
|
|
361
363
|
{healthTile('healthy', 'success')}
|
|
@@ -381,13 +383,12 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
381
383
|
Arbitrary 900px breakpoint (not `sm`) so the collapse is independent of
|
|
382
384
|
the consuming app's Tailwind breakpoint config (Hub uses defaults). */}
|
|
383
385
|
<aside className="flex w-52 shrink-0 flex-col overflow-hidden border-r border-theme-border bg-theme-surface/90 max-[899px]:max-h-72 max-[899px]:w-full max-[899px]:border-b max-[899px]:border-r-0">
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
{anyFilterActive && (
|
|
386
|
+
<FilterRailPanel
|
|
387
|
+
activeCount={activeFilterCount}
|
|
388
|
+
clear={anyFilterActive && (
|
|
387
389
|
<button type="button" onClick={clearAllFilters} className="text-[10px] font-medium text-blue-500 hover:text-blue-400">Clear</button>
|
|
388
390
|
)}
|
|
389
|
-
|
|
390
|
-
<div className="flex-1 overflow-y-auto">
|
|
391
|
+
>
|
|
391
392
|
{initialLoading ? (
|
|
392
393
|
<BoardRailSkeleton
|
|
393
394
|
sections={[
|
|
@@ -414,7 +415,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
414
415
|
)}
|
|
415
416
|
</>
|
|
416
417
|
)}
|
|
417
|
-
</
|
|
418
|
+
</FilterRailPanel>
|
|
418
419
|
</aside>
|
|
419
420
|
|
|
420
421
|
{/* Content: toolbar (search + sort) over the scrollable table. */}
|
|
@@ -12,6 +12,7 @@ import { ClusterName } from '../ui/ClusterName'
|
|
|
12
12
|
import { Input } from '../ui/Input'
|
|
13
13
|
import { MiddleEllipsis } from '../ui/MiddleEllipsis'
|
|
14
14
|
import { StatusDot, type StatusTone } from '../ui/status-tone'
|
|
15
|
+
import { useKeepInViewport } from '../../hooks/useKeepInViewport'
|
|
15
16
|
|
|
16
17
|
export interface ClusterSwitcherItem {
|
|
17
18
|
id: string
|
|
@@ -107,6 +108,8 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
107
108
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
108
109
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
109
110
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
111
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
112
|
+
useKeepInViewport(menuRef, isOpen)
|
|
110
113
|
|
|
111
114
|
useImperativeHandle(ref, () => ({
|
|
112
115
|
open: () => {
|
|
@@ -284,6 +287,7 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
284
287
|
|
|
285
288
|
{isOpen && (
|
|
286
289
|
<div
|
|
290
|
+
ref={menuRef}
|
|
287
291
|
className={`absolute top-full ${positionClass} mt-1 z-50 min-w-[280px] max-w-[420px] bg-theme-surface border border-theme-border-light rounded-lg shadow-xl overflow-hidden`}
|
|
288
292
|
>
|
|
289
293
|
{searchable && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, useId } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, useId, type ReactNode } from 'react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { GitCompare, Search, X } from 'lucide-react'
|
|
4
4
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
@@ -18,6 +18,11 @@ export interface CompareResourcePickerProps {
|
|
|
18
18
|
sourceSide?: CompareSide
|
|
19
19
|
/** Candidate resources — same kind as source. Source is filtered out automatically. */
|
|
20
20
|
candidates: CompareResourceRef[]
|
|
21
|
+
/** Says the candidate list may be incomplete; shown as a warning above it. */
|
|
22
|
+
coverageNote?: ReactNode
|
|
23
|
+
/** Says the list covers only the namespaces Radar collects; shown neutrally.
|
|
24
|
+
* Ignored when coverageNote is set. */
|
|
25
|
+
scopeNote?: ReactNode
|
|
21
26
|
loading?: boolean
|
|
22
27
|
error?: unknown
|
|
23
28
|
onPick: (r: CompareResourceRef) => void
|
|
@@ -30,6 +35,8 @@ export function CompareResourcePicker({
|
|
|
30
35
|
sourceSide = 'a',
|
|
31
36
|
candidates,
|
|
32
37
|
loading,
|
|
38
|
+
coverageNote,
|
|
39
|
+
scopeNote,
|
|
33
40
|
error,
|
|
34
41
|
onPick,
|
|
35
42
|
}: CompareResourcePickerProps) {
|
|
@@ -133,6 +140,9 @@ export function CompareResourcePicker({
|
|
|
133
140
|
</div>
|
|
134
141
|
</div>
|
|
135
142
|
|
|
143
|
+
{coverageNote
|
|
144
|
+
? <div role="status" className="px-4 py-2 text-xs text-warning-text border-b border-theme-border">{coverageNote}</div>
|
|
145
|
+
: scopeNote && <div role="status" className="px-4 py-2 text-xs text-theme-text-tertiary border-b border-theme-border">{scopeNote}</div>}
|
|
136
146
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
137
147
|
{loading && (
|
|
138
148
|
<div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
|
|
@@ -146,9 +156,13 @@ export function CompareResourcePicker({
|
|
|
146
156
|
)}
|
|
147
157
|
{!loading && error == null && filtered.length === 0 && (
|
|
148
158
|
<div className="flex flex-col items-center justify-center py-12 text-theme-text-tertiary text-sm gap-1">
|
|
149
|
-
{candidates.length
|
|
150
|
-
?
|
|
151
|
-
:
|
|
159
|
+
{candidates.length > 1
|
|
160
|
+
? 'No matches.'
|
|
161
|
+
: coverageNote
|
|
162
|
+
? `No other ${source.kind} found in what Radar could read.`
|
|
163
|
+
: scopeNote
|
|
164
|
+
? `No other ${source.kind} found in the collected namespaces.`
|
|
165
|
+
: `No other ${source.kind} available to compare against.`}
|
|
152
166
|
</div>
|
|
153
167
|
)}
|
|
154
168
|
{!loading && error == null && filtered.length > 0 && (
|
|
@@ -5,6 +5,7 @@ import { YamlDiffEditor } from '../ui/YamlEditor'
|
|
|
5
5
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
6
6
|
import { Tooltip } from '../ui/Tooltip'
|
|
7
7
|
import { pluralToKind } from '../../utils/navigation'
|
|
8
|
+
import { usePhoneLayout } from '../../hooks/usePhoneLayout'
|
|
8
9
|
import { toComparableYaml } from './normalize'
|
|
9
10
|
import { SIDE_TONES, type CompareSide } from './types'
|
|
10
11
|
|
|
@@ -120,7 +121,10 @@ export function ResourceCompareView({
|
|
|
120
121
|
bleed = false,
|
|
121
122
|
}: ResourceCompareViewProps) {
|
|
122
123
|
const [specOnly, setSpecOnly] = useState(false)
|
|
123
|
-
const [
|
|
124
|
+
const [unifiedPicked, setUnified] = useState(false)
|
|
125
|
+
// Two side-by-side panes don't fit a phone, so phones always get one column.
|
|
126
|
+
const isPhone = usePhoneLayout()
|
|
127
|
+
const unified = unifiedPicked || isPhone
|
|
124
128
|
const [hideUnchanged, setHideUnchanged] = useState(true)
|
|
125
129
|
const [rawMetadata, setRawMetadata] = useState(false)
|
|
126
130
|
|
|
@@ -180,7 +184,7 @@ export function ResourceCompareView({
|
|
|
180
184
|
<ToggleButton active={rawMetadata} onClick={() => setRawMetadata(v => !v)} icon={<Sparkles className="w-3.5 h-3.5" />} label="Raw metadata" tooltip="Keep server-assigned noise (uid, resourceVersion, managedFields, last-applied)" />
|
|
181
185
|
<ToggleButton active={specOnly} onClick={() => setSpecOnly(v => !v)} icon={<FileCode2 className="w-3.5 h-3.5" />} label="Spec only" tooltip="Drop status fields from both sides" />
|
|
182
186
|
<ToggleButton active={hideUnchanged} onClick={() => setHideUnchanged(v => !v)} icon={<FileText className="w-3.5 h-3.5" />} label="Diff only" tooltip="Collapse unchanged regions" />
|
|
183
|
-
<ToggleButton active={unified} onClick={() => setUnified(v => !v)} icon={<Rows className="w-3.5 h-3.5" />} label="Unified" tooltip="Switch between side-by-side and single-column" />
|
|
187
|
+
{!isPhone && <ToggleButton active={unified} onClick={() => setUnified(v => !v)} icon={<Rows className="w-3.5 h-3.5" />} label="Unified" tooltip="Switch between side-by-side and single-column" />}
|
|
184
188
|
</div>
|
|
185
189
|
|
|
186
190
|
<div className="w-px h-5 bg-theme-border shrink-0" aria-hidden />
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { GitOpsDetailLayout, type GitOpsDetailLayoutProps } from './GitOpsDetailLayout'
|
|
4
|
+
|
|
5
|
+
const noop = () => {}
|
|
6
|
+
|
|
7
|
+
const shell: Omit<GitOpsDetailLayoutProps, 'identity' | 'isArgoApp'> = {
|
|
8
|
+
status: null,
|
|
9
|
+
terminating: false,
|
|
10
|
+
terminatingChipTooltip: '',
|
|
11
|
+
terminatingActionTooltip: '',
|
|
12
|
+
insight: null,
|
|
13
|
+
insightLoading: false,
|
|
14
|
+
isFlux: false,
|
|
15
|
+
isFluxWorkload: false,
|
|
16
|
+
activeTab: 'topology',
|
|
17
|
+
onTabChange: noop,
|
|
18
|
+
fullscreen: false,
|
|
19
|
+
onToggleFullscreen: noop,
|
|
20
|
+
renderTabBody: () => null,
|
|
21
|
+
onNavigateRoot: noop,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('GitOpsDetailLayout', () => {
|
|
25
|
+
it('shows the tool chip, facts and Argo operations for an Argo CD Application', () => {
|
|
26
|
+
const html = renderToString(
|
|
27
|
+
<GitOpsDetailLayout
|
|
28
|
+
{...shell}
|
|
29
|
+
identity={{ kind: 'applications', group: 'argoproj.io', namespace: 'shop', name: 'wordpress', toolLabel: 'ArgoCD', kindLabel: 'Application' }}
|
|
30
|
+
detail={{ project: 'default', destination: 'in-cluster' }}
|
|
31
|
+
isArgoApp
|
|
32
|
+
argo={{
|
|
33
|
+
onSyncRequested: noop, onRefresh: noop, onTerminate: noop, onSuspend: noop, onResume: noop,
|
|
34
|
+
syncing: false, refreshing: false, refreshingKind: 'normal', terminating: false, suspending: false, resuming: false,
|
|
35
|
+
isRunning: false, autoSyncEnabled: false,
|
|
36
|
+
}}
|
|
37
|
+
/>,
|
|
38
|
+
)
|
|
39
|
+
expect(html).toContain('ArgoCD')
|
|
40
|
+
expect(html).toContain('Project')
|
|
41
|
+
expect(html).toContain('Sync…')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// A root that is not a GitOps resource gets one state: its name and why
|
|
45
|
+
// there is no GitOps view, with no tool label, facts or operations.
|
|
46
|
+
it('shows only the refusal for a root that is not a GitOps resource', () => {
|
|
47
|
+
const message = 'Application.app.k8s.io shop/wordpress is not a supported Argo CD or Flux resource, so it has no GitOps view'
|
|
48
|
+
const html = renderToString(
|
|
49
|
+
<GitOpsDetailLayout
|
|
50
|
+
{...shell}
|
|
51
|
+
identity={{ kind: 'applications', group: 'app.k8s.io', namespace: 'shop', name: 'wordpress', kindLabel: 'Application' }}
|
|
52
|
+
isArgoApp={false}
|
|
53
|
+
resourceError={new Error(message)}
|
|
54
|
+
/>,
|
|
55
|
+
)
|
|
56
|
+
expect(html).toContain('wordpress')
|
|
57
|
+
expect(html).toContain(message)
|
|
58
|
+
expect(html).not.toContain('ArgoCD')
|
|
59
|
+
expect(html).not.toContain('FluxCD')
|
|
60
|
+
expect(html).not.toContain('Project')
|
|
61
|
+
expect(html).not.toContain('Sync…')
|
|
62
|
+
expect(html).not.toContain('Reconcile')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -20,8 +20,9 @@ export interface GitOpsDetailIdentity {
|
|
|
20
20
|
name: string
|
|
21
21
|
// toolLabel: "ArgoCD" | "FluxCD" — passed through as a chip; kept as a
|
|
22
22
|
// free string so future tools (Komodor, Argo Workflows, etc.) don't need
|
|
23
|
-
// an enum bump here.
|
|
24
|
-
|
|
23
|
+
// an enum bump here. Omitted for a root that is not a GitOps resource,
|
|
24
|
+
// which hides the tool/kind chip.
|
|
25
|
+
toolLabel?: string
|
|
25
26
|
// kindLabel: human-facing kind (e.g. "Application"). Caller does the
|
|
26
27
|
// lookup against its API resource catalog.
|
|
27
28
|
kindLabel: string
|
|
@@ -117,7 +118,9 @@ export interface GitOpsDetailLayoutProps {
|
|
|
117
118
|
terminatingActionTooltip: string
|
|
118
119
|
|
|
119
120
|
// Spec/config row
|
|
120
|
-
|
|
121
|
+
// Omitted for a root that is not a GitOps resource, which hides the
|
|
122
|
+
// spec/config facts row.
|
|
123
|
+
detail?: GitOpsDetailMetadata
|
|
121
124
|
|
|
122
125
|
// Insights — drives StatusStrip + IssuesBand
|
|
123
126
|
insight: GitOpsInsight | null
|
|
@@ -334,9 +337,11 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
334
337
|
</span>
|
|
335
338
|
</Tooltip>
|
|
336
339
|
)}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
+
{identity.toolLabel && (
|
|
341
|
+
<span className="inline-flex shrink-0 items-center rounded border border-theme-border bg-theme-hover/50 px-1.5 py-0.5 text-[11px] font-medium text-theme-text-secondary">
|
|
342
|
+
{identity.toolLabel} · {identity.kindLabel}
|
|
343
|
+
</span>
|
|
344
|
+
)}
|
|
340
345
|
{isFleetContext && destinationCluster && (
|
|
341
346
|
// Cross-cluster lineage chip — fleet-only. Renders the
|
|
342
347
|
// destination cluster alongside the tool/kind chip so
|
|
@@ -350,14 +355,16 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
350
355
|
)}
|
|
351
356
|
</div>
|
|
352
357
|
{/* Spec/config row */}
|
|
353
|
-
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
{detail && (
|
|
359
|
+
<div className="mt-2 flex flex-wrap gap-x-5 gap-y-0.5 text-[11px] text-theme-text-tertiary">
|
|
360
|
+
<AppFact label="Project" value={detail.project || '-'} />
|
|
361
|
+
{detail.repository && <AppFact label="Source" value={detail.repository} />}
|
|
362
|
+
{detail.path && <AppFact label="Path" value={detail.path} />}
|
|
363
|
+
{detail.chart && <AppFact label="Chart" value={detail.chart} />}
|
|
364
|
+
<AppFact label="Destination" value={detail.destination || '-'} />
|
|
365
|
+
{detail.autoSyncMode && <AppFact label="Sync mode" value={detail.autoSyncMode} />}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
361
368
|
</div>
|
|
362
369
|
{/* Action buttons */}
|
|
363
370
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -518,7 +525,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
518
525
|
{resourceLoading ? (
|
|
519
526
|
<PaneLoader label="Loading GitOps resource…" className="flex-1" />
|
|
520
527
|
) : resourceError ? (
|
|
521
|
-
<div className="p-4 text-sm text-red-500">Failed to load
|
|
528
|
+
<div className="p-4 text-sm text-red-500">Failed to load GitOps view: {resourceError.message}</div>
|
|
522
529
|
) : (
|
|
523
530
|
<div className={bodyShellClass}>
|
|
524
531
|
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-theme-border bg-theme-base px-4 py-2">
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { GitOpsTableView } from './GitOpsTableView'
|
|
4
|
+
|
|
5
|
+
const render = (props: Partial<Parameters<typeof GitOpsTableView>[0]> = {}) =>
|
|
6
|
+
renderToString(<GitOpsTableView rows={[]} counts={{}} onRowClick={() => {}} {...props} />)
|
|
7
|
+
|
|
8
|
+
describe('GitOpsTableView coverage', () => {
|
|
9
|
+
it('shows the onboarding state to a host that passes no coverage, as before coverage', () => {
|
|
10
|
+
expect(render()).toContain('No GitOps resources detected')
|
|
11
|
+
expect(render({ coverageStatus: 'complete' })).toContain('No GitOps resources detected')
|
|
12
|
+
})
|
|
13
|
+
it('keeps the unqualified labels and empty text for unknown coverage', () => {
|
|
14
|
+
const html = render({ coverageStatus: 'unknown', counts: { 'argoproj.io/Application': 2 } })
|
|
15
|
+
expect(html).toContain('Total Applications')
|
|
16
|
+
expect(html).toContain('No applications found.')
|
|
17
|
+
expect(html).not.toContain('Partial results')
|
|
18
|
+
})
|
|
19
|
+
it('says what a complete empty answer covers', () => {
|
|
20
|
+
expect(render({ coverageStatus: 'complete', counts: { 'argoproj.io/Application': 2 } })).toContain('No applications found in the requested scope.')
|
|
21
|
+
})
|
|
22
|
+
it('does not call a partial answer empty', () => {
|
|
23
|
+
const html = render({ coverageStatus: 'partial' })
|
|
24
|
+
expect(html).not.toContain('No GitOps resources detected')
|
|
25
|
+
expect(html).toContain('Partial results')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -41,6 +41,7 @@ import { toggleSet } from './GitOpsGraphFilterRail'
|
|
|
41
41
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
42
42
|
import { parseContextName } from '../../utils/context-name'
|
|
43
43
|
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format'
|
|
44
|
+
import { FilterRailPanel } from '../shared/FilterRailPanel'
|
|
44
45
|
|
|
45
46
|
// =============================================================================
|
|
46
47
|
// GitOpsTableView — the canonical GitOps fleet list (Argo + Flux).
|
|
@@ -202,7 +203,13 @@ export interface GitOpsTableViewProps {
|
|
|
202
203
|
// counts keyed "group/Kind" — e.g. "argoproj.io/Application" → 17. Drives
|
|
203
204
|
// the Scope-section mode tabs and the empty-state check.
|
|
204
205
|
counts: Record<string, number>
|
|
206
|
+
// Read coverage of `rows`, combined across every source and cluster that
|
|
207
|
+
// contributes to them. `unknown` (the default) keeps the presentation that
|
|
208
|
+
// predates coverage; only `partial` qualifies the view.
|
|
209
|
+
coverageStatus?: 'complete' | 'partial' | 'unknown'
|
|
205
210
|
countsUnavailable?: string[]
|
|
211
|
+
// Count keys whose counts are lower bounds.
|
|
212
|
+
countsPartial?: string[]
|
|
206
213
|
// @deprecated Superseded by `freshnessSlot` — kept for source compatibility
|
|
207
214
|
// with existing consumers; no longer drives any affordance here.
|
|
208
215
|
onRefresh?: () => void
|
|
@@ -305,6 +312,8 @@ export function GitOpsTableView({
|
|
|
305
312
|
error,
|
|
306
313
|
counts,
|
|
307
314
|
countsUnavailable,
|
|
315
|
+
countsPartial,
|
|
316
|
+
coverageStatus = 'unknown',
|
|
308
317
|
freshnessSlot,
|
|
309
318
|
onRowClick,
|
|
310
319
|
rowHrefFor,
|
|
@@ -403,6 +412,13 @@ export function GitOpsTableView({
|
|
|
403
412
|
projects: counts['argoproj.io/AppProject'] ?? 0,
|
|
404
413
|
alerts: counts['notification.toolkit.fluxcd.io/Alert'] ?? 0,
|
|
405
414
|
}
|
|
415
|
+
const partialCountKeys = new Set(countsPartial ?? [])
|
|
416
|
+
const modeCountsPartial: Record<GitOpsMode, boolean> = {
|
|
417
|
+
applications: coverageStatus === 'partial',
|
|
418
|
+
sources: ['source.toolkit.fluxcd.io/GitRepository', 'source.toolkit.fluxcd.io/OCIRepository', 'source.toolkit.fluxcd.io/HelmRepository'].some(key => partialCountKeys.has(key)),
|
|
419
|
+
projects: partialCountKeys.has('argoproj.io/AppProject'),
|
|
420
|
+
alerts: partialCountKeys.has('notification.toolkit.fluxcd.io/Alert'),
|
|
421
|
+
}
|
|
406
422
|
const totalGitOps = Object.entries(counts).reduce(
|
|
407
423
|
(sum, [key, n]) => sum + (isGitOpsCountKey(key) ? n : 0),
|
|
408
424
|
0,
|
|
@@ -548,7 +564,7 @@ export function GitOpsTableView({
|
|
|
548
564
|
// Also require zero actual rows: the cold-cache retry can populate `rows`
|
|
549
565
|
// before the separate counts map catches up, and a populated table must not
|
|
550
566
|
// be hidden behind a "nothing here" screen.
|
|
551
|
-
if (totalGitOps === 0 && allRowsInput.length === 0 && !loading && !hasGlobalNamespaceFilter && !hasUnavailableGitOpsCounts) {
|
|
567
|
+
if (totalGitOps === 0 && allRowsInput.length === 0 && !loading && !hasGlobalNamespaceFilter && !hasUnavailableGitOpsCounts && coverageStatus !== 'partial') {
|
|
552
568
|
return (
|
|
553
569
|
<div className="flex h-full min-h-0 flex-1 items-start justify-center bg-theme-base px-4 pb-4 pt-[22vh]">
|
|
554
570
|
<div className="rounded-lg border border-theme-border bg-theme-surface p-8 text-center">
|
|
@@ -580,7 +596,7 @@ export function GitOpsTableView({
|
|
|
580
596
|
const summaryTiles: SummaryTileSpec[] = [
|
|
581
597
|
{
|
|
582
598
|
key: 'total',
|
|
583
|
-
label: 'Total Applications',
|
|
599
|
+
label: coverageStatus === 'partial' ? 'Observed Applications' : 'Total Applications',
|
|
584
600
|
value: allRows.length,
|
|
585
601
|
tone: 'neutral',
|
|
586
602
|
active: noOtherFiltersActive(),
|
|
@@ -678,6 +694,7 @@ export function GitOpsTableView({
|
|
|
678
694
|
mode={mode}
|
|
679
695
|
onModeChange={setMode}
|
|
680
696
|
modeCounts={modeCounts}
|
|
697
|
+
modeCountsPartial={modeCountsPartial}
|
|
681
698
|
syncCounts={syncCounts}
|
|
682
699
|
syncFilters={syncFilters}
|
|
683
700
|
onToggleSync={(value) => filters.toggle('sync', value)}
|
|
@@ -790,6 +807,10 @@ export function GitOpsTableView({
|
|
|
790
807
|
</div>
|
|
791
808
|
</div>
|
|
792
809
|
|
|
810
|
+
{coverageStatus === 'partial' && (
|
|
811
|
+
<p role="status" className="px-4 py-2 text-xs text-warning-text border-b border-theme-border">Partial results. Radar couldn't read every namespace for some GitOps resources or sources, so counts are lower bounds.</p>
|
|
812
|
+
)}
|
|
813
|
+
|
|
793
814
|
{/* pb-20 keeps the last row (and its three-dot menu) scrollable clear
|
|
794
815
|
of the app's fixed bottom-right overlay buttons; without the slack
|
|
795
816
|
the bottom row's action trigger sits under them and can't be clicked
|
|
@@ -806,7 +827,7 @@ export function GitOpsTableView({
|
|
|
806
827
|
<div className="p-4 text-sm text-red-500">Failed to load GitOps applications: {error.message}</div>
|
|
807
828
|
) : filteredRows.length === 0 ? (
|
|
808
829
|
<div className="flex h-full flex-col items-center justify-center gap-3 text-sm text-theme-text-secondary">
|
|
809
|
-
<p>{allRows.length
|
|
830
|
+
<p>{noApplicationsMessage(allRows.length > 0, coverageStatus, hasGlobalNamespaceFilter)}</p>
|
|
810
831
|
{hasGlobalNamespaceFilter && globalNamespaces && (
|
|
811
832
|
<p className="text-xs text-theme-text-tertiary">
|
|
812
833
|
Viewing {globalNamespaces.length === 1 ? `namespace: ${globalNamespaces[0]}` : `${globalNamespaces.length} namespaces`}
|
|
@@ -857,6 +878,7 @@ function GitOpsFilterSidebar({
|
|
|
857
878
|
mode,
|
|
858
879
|
onModeChange,
|
|
859
880
|
modeCounts,
|
|
881
|
+
modeCountsPartial,
|
|
860
882
|
syncCounts,
|
|
861
883
|
syncFilters,
|
|
862
884
|
onToggleSync,
|
|
@@ -884,6 +906,7 @@ function GitOpsFilterSidebar({
|
|
|
884
906
|
mode: GitOpsMode
|
|
885
907
|
onModeChange: (mode: GitOpsMode) => void
|
|
886
908
|
modeCounts: Record<GitOpsMode, number>
|
|
909
|
+
modeCountsPartial: Record<GitOpsMode, boolean>
|
|
887
910
|
syncCounts: Map<string, number>
|
|
888
911
|
syncFilters: Set<string>
|
|
889
912
|
onToggleSync: (value: string) => void
|
|
@@ -915,13 +938,17 @@ function GitOpsFilterSidebar({
|
|
|
915
938
|
side === 'right' ? 'border-l max-[899px]:border-l-0' : 'border-r max-[899px]:border-r-0'
|
|
916
939
|
}`}
|
|
917
940
|
>
|
|
918
|
-
<
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
941
|
+
<FilterRailPanel
|
|
942
|
+
activeCount={
|
|
943
|
+
syncFilters.size + healthFilters.size + automationFilters.size + kindFilters.size +
|
|
944
|
+
projectFilters.size + namespaceFilters.size + (lifecycleFilter === 'all' ? 0 : 1)
|
|
945
|
+
}
|
|
946
|
+
clear={
|
|
947
|
+
<button type="button" onClick={onClear} className="text-[10px] font-medium text-blue-500 hover:text-blue-400">
|
|
948
|
+
Clear
|
|
949
|
+
</button>
|
|
950
|
+
}
|
|
951
|
+
>
|
|
925
952
|
{loading ? (
|
|
926
953
|
<BoardRailSkeleton
|
|
927
954
|
sections={[
|
|
@@ -949,7 +976,7 @@ function GitOpsFilterSidebar({
|
|
|
949
976
|
}`}
|
|
950
977
|
>
|
|
951
978
|
<div className="font-medium">{modeLabel(item)}</div>
|
|
952
|
-
<div className={mode === item ? 'text-white/70' : 'text-theme-text-tertiary'}>{modeCounts[item]}</div>
|
|
979
|
+
<div className={mode === item ? 'text-white/70' : 'text-theme-text-tertiary'}>{modeCountsPartial[item] ? '≥' : ''}{modeCounts[item]}</div>
|
|
953
980
|
</button>
|
|
954
981
|
))}
|
|
955
982
|
</div>
|
|
@@ -1040,7 +1067,7 @@ function GitOpsFilterSidebar({
|
|
|
1040
1067
|
</GitOpsFilterSection>
|
|
1041
1068
|
</>
|
|
1042
1069
|
)}
|
|
1043
|
-
</
|
|
1070
|
+
</FilterRailPanel>
|
|
1044
1071
|
</aside>
|
|
1045
1072
|
)
|
|
1046
1073
|
}
|
|
@@ -1882,6 +1909,13 @@ function countMap(values: string[]) {
|
|
|
1882
1909
|
return counts
|
|
1883
1910
|
}
|
|
1884
1911
|
|
|
1912
|
+
function noApplicationsMessage(hasRows: boolean, coverageStatus: GitOpsTableViewProps['coverageStatus'], namespaceFiltered: boolean): string {
|
|
1913
|
+
if (hasRows) return 'No applications match the current filters.'
|
|
1914
|
+
if (coverageStatus === 'complete') return 'No applications found in the requested scope.'
|
|
1915
|
+
if (coverageStatus === 'partial') return 'No applications found in the namespaces Radar could read.'
|
|
1916
|
+
return namespaceFiltered ? 'No applications match the current filters.' : 'No applications found.'
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1885
1919
|
function countLabels(rows: GitOpsRow[]) {
|
|
1886
1920
|
const counts = new Map<string, number>()
|
|
1887
1921
|
for (const row of rows) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatGitOpsSourceUrl,
|
|
6
6
|
getGitOpsResourceStatus,
|
|
7
7
|
getGitOpsTool,
|
|
8
|
+
gitOpsDetailGroup,
|
|
8
9
|
isDestinationRef,
|
|
9
10
|
parseArgoRollbackID,
|
|
10
11
|
} from './detail-helpers'
|
|
@@ -75,26 +76,47 @@ describe('formatGitOpsSourceUrl', () => {
|
|
|
75
76
|
})
|
|
76
77
|
|
|
77
78
|
describe('getGitOpsTool', () => {
|
|
78
|
-
//
|
|
79
|
-
test('Argo
|
|
79
|
+
// The API group decides, the same rule as the backend (pkg/gitops ToolFor).
|
|
80
|
+
test('Argo CD kinds in argoproj.io', () => {
|
|
80
81
|
expect(getGitOpsTool('applications', 'argoproj.io')).toBe('argo')
|
|
81
82
|
expect(getGitOpsTool('applicationsets', 'argoproj.io')).toBe('argo')
|
|
82
83
|
expect(getGitOpsTool('appprojects', 'argoproj.io')).toBe('argo')
|
|
83
84
|
})
|
|
84
|
-
|
|
85
|
-
// Defends against a normalizer that drops `group` from the frontend payload.
|
|
86
|
-
test('Argo kinds without group still route to argo', () => {
|
|
87
|
-
expect(getGitOpsTool('applications', undefined)).toBe('argo')
|
|
88
|
-
expect(getGitOpsTool('applicationsets', '')).toBe('argo')
|
|
89
|
-
})
|
|
90
|
-
// Flux kinds with their canonical groups.
|
|
91
|
-
test('Flux via group', () => {
|
|
85
|
+
test('Flux groups', () => {
|
|
92
86
|
expect(getGitOpsTool('kustomizations', 'kustomize.toolkit.fluxcd.io')).toBe('flux')
|
|
93
87
|
expect(getGitOpsTool('helmreleases', 'helm.toolkit.fluxcd.io')).toBe('flux')
|
|
88
|
+
expect(getGitOpsTool('gitrepositories', 'source.toolkit.fluxcd.io')).toBe('flux')
|
|
89
|
+
})
|
|
90
|
+
// SIG Apps defines its own Application kind; offering Argo Sync on it
|
|
91
|
+
// would sync a same-named argoproj.io Application instead.
|
|
92
|
+
test('an Application outside argoproj.io is not Argo CD', () => {
|
|
93
|
+
expect(getGitOpsTool('applications', 'app.k8s.io')).toBeNull()
|
|
94
|
+
expect(getGitOpsTool('applications', 'source.toolkit.fluxcd.io')).toBeNull()
|
|
95
|
+
expect(getGitOpsTool('kustomizations', 'source.toolkit.fluxcd.io')).toBeNull()
|
|
96
|
+
expect(getGitOpsTool('helmreleases', 'kustomize.toolkit.fluxcd.io')).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
// argoproj.io is shared with Argo Rollouts and Workflows.
|
|
99
|
+
test('other argoproj.io kinds are not Argo CD', () => {
|
|
100
|
+
expect(getGitOpsTool('rollouts', 'argoproj.io')).toBeNull()
|
|
101
|
+
expect(getGitOpsTool('workflows', 'argoproj.io')).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
// The kind alone is not identity, and nothing defaults to Flux.
|
|
104
|
+
test('no group or an unknown group is not GitOps', () => {
|
|
105
|
+
expect(getGitOpsTool('applications', undefined)).toBeNull()
|
|
106
|
+
expect(getGitOpsTool('applicationsets', '')).toBeNull()
|
|
107
|
+
expect(getGitOpsTool('somecustomresource', 'unknown.io')).toBeNull()
|
|
108
|
+
expect(getGitOpsTool('applications', 'notargoproj.io')).toBeNull()
|
|
109
|
+
expect(getGitOpsTool('kustomizations', 'fake.kustomize.toolkit.fluxcd.io')).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('gitOpsDetailGroup', () => {
|
|
114
|
+
test('the loaded object\'s apiVersion wins over the route group', () => {
|
|
115
|
+
expect(gitOpsDetailGroup('argoproj.io', { apiVersion: 'app.k8s.io/v1beta1' })).toBe('app.k8s.io')
|
|
94
116
|
})
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(
|
|
117
|
+
test('the route group stands in until the object loads', () => {
|
|
118
|
+
expect(gitOpsDetailGroup('app.k8s.io', undefined)).toBe('app.k8s.io')
|
|
119
|
+
expect(gitOpsDetailGroup('argoproj.io', {})).toBe('argoproj.io')
|
|
98
120
|
})
|
|
99
121
|
})
|
|
100
122
|
|
|
@@ -181,3 +203,26 @@ describe('isDestinationRef', () => {
|
|
|
181
203
|
expect(isDestinationRef(null, true, { group: 'apps', kind: 'Deployment', namespace: 'prod', name: 'billing' })).toBe(true)
|
|
182
204
|
})
|
|
183
205
|
})
|
|
206
|
+
|
|
207
|
+
describe('GitOps status identity', () => {
|
|
208
|
+
test.each([
|
|
209
|
+
['applications', 'app.k8s.io/v1beta1'],
|
|
210
|
+
['applicationsets', 'vendor.io/v1'],
|
|
211
|
+
['kustomizations', 'source.toolkit.fluxcd.io/v1'],
|
|
212
|
+
['helmreleases', 'vendor.helm.toolkit.fluxcd.io/v1'],
|
|
213
|
+
])('does not interpret foreign %s status from %s', (kind, apiVersion) => {
|
|
214
|
+
expect(getGitOpsResourceStatus(kind, {
|
|
215
|
+
apiVersion,
|
|
216
|
+
status: { sync: { status: 'Synced' }, health: { status: 'Healthy' }, conditions: [{ type: 'Ready', status: 'True' }] },
|
|
217
|
+
})).toBeNull()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('preserves existing controller-specific status-only callers', () => {
|
|
221
|
+
expect(getGitOpsResourceStatus('applications', { status: { sync: { status: 'Synced' }, health: { status: 'Healthy' } } })?.sync).toBe('Synced')
|
|
222
|
+
expect(getGitOpsResourceStatus('kustomizations', { status: { conditions: [{ type: 'Ready', status: 'True' }] } })?.health).toBe('Healthy')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('does not interpret an AppProject as a Flux resource', () => {
|
|
226
|
+
expect(getGitOpsResourceStatus('appprojects', { apiVersion: 'argoproj.io/v1alpha1', status: { conditions: [{ type: 'Ready', status: 'True' }] } })).toBeNull()
|
|
227
|
+
})
|
|
228
|
+
})
|