@skyhook-io/k8s-ui 1.14.4 → 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/package.json +5 -5
- package/src/components/audit/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/charts/AreaChart.test.tsx +173 -0
- package/src/components/charts/AreaChart.tsx +208 -53
- package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
- package/src/components/charts/PrometheusChartsView.tsx +205 -76
- package/src/components/charts/SeriesLegend.tsx +8 -3
- package/src/components/charts/annotations.ts +59 -0
- package/src/components/charts/axis.test.ts +48 -0
- package/src/components/charts/axis.ts +62 -0
- package/src/components/charts/colors.test.ts +27 -0
- package/src/components/charts/colors.ts +23 -0
- package/src/components/charts/format.test.ts +19 -0
- package/src/components/charts/format.ts +21 -1
- package/src/components/charts/index.ts +3 -1
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/charts/types.ts +12 -0
- package/src/components/checks/ChecksView.tsx +20 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
- package/src/components/gitops/health-provenance.test.ts +54 -0
- package/src/components/gitops/health-provenance.ts +24 -0
- package/src/components/gitops/index.ts +2 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
- package/src/components/gitops/tree/merge.test.ts +34 -0
- package/src/components/gitops/tree/merge.ts +7 -0
- package/src/components/resources/HPADiagnosisSummary.test.tsx +178 -0
- package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
- package/src/components/resources/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/index.ts +7 -0
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -57
- package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
- package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
- package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
- package/src/components/resources/renderers/PodRenderer.tsx +11 -0
- package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
- package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -21
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +116 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +231 -11
- package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
- package/src/components/shared/DrainPlanDialog.tsx +297 -0
- package/src/components/shared/ResourceActionsBar.tsx +53 -26
- package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
- package/src/components/shared/index.ts +4 -0
- package/src/components/topology/K8sResourceNode.test.ts +9 -0
- package/src/components/topology/K8sResourceNode.tsx +56 -5
- package/src/components/ui/Badge.tsx +5 -1
- package/src/components/ui/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/FreshnessControl.tsx +8 -2
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/monacoRuntime.ts +14 -0
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/monaco-deep.d.ts +10 -0
- package/src/theme/components.css +16 -0
- package/src/types/core.ts +38 -11
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +20 -1
- package/src/utils/navigation.ts +25 -0
- package/src/utils/network-policy.test.ts +42 -0
- package/src/utils/network-policy.ts +59 -0
- package/src/utils/reflector.ts +139 -0
- package/src/utils/yaml-file-import.test.ts +189 -0
- package/src/utils/yaml-file-import.ts +95 -0
- package/src/components/logs/JsonLogLine.tsx +0 -93
|
@@ -1,19 +1,53 @@
|
|
|
1
|
-
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import type * as React from 'react'
|
|
3
|
-
import { seriesColor, seriesFill, computeShortLabels } from './colors'
|
|
3
|
+
import { seriesColor, seriesFill, computeShortLabels, seriesDisplayLabels } from './colors'
|
|
4
4
|
import { formatMetricValue, formatTimestamp } from './format'
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { layoutAnnotations } from './annotations'
|
|
6
|
+
import { chartLayout, integerAxisTop, isCountUnit, yAxisValues } from './axis'
|
|
7
|
+
import type { TimeSeries, ReferenceLine, ChartAnnotation } from './types'
|
|
8
|
+
import { nearestSample } from './nearestSample'
|
|
9
|
+
|
|
10
|
+
// Below this rendered width the annotation label pills would cover most of the
|
|
11
|
+
// plot once the 1000-unit viewBox is scaled down; the lines stay and the label
|
|
12
|
+
// text moves into the hover tooltip.
|
|
13
|
+
export const ANNOTATION_LABEL_MIN_WIDTH_PX = 420
|
|
14
|
+
const ANNOTATION_HOVER_TOLERANCE = 8
|
|
15
|
+
|
|
16
|
+
export function AreaChart({ series, color, fillColor, unit, referenceLines, annotations, domain, seriesLabels, stepSeconds, layout = 'full' }: {
|
|
8
17
|
series: TimeSeries[]
|
|
9
18
|
color: string
|
|
10
19
|
fillColor: string
|
|
11
20
|
unit: string
|
|
12
21
|
referenceLines?: ReferenceLine[]
|
|
22
|
+
/** Vertical markers; only those inside the X domain render. */
|
|
23
|
+
annotations?: ChartAnnotation[]
|
|
24
|
+
/** X axis window in unix seconds. Defaults to the sample extent. */
|
|
25
|
+
domain?: { start: number; end: number }
|
|
26
|
+
/** Expected positive sample interval; omitted evaluations break paths and cannot supply hover values. */
|
|
27
|
+
stepSeconds?: number
|
|
28
|
+
/**
|
|
29
|
+
* Display name per series, parallel to `series`. Pass it when the chart
|
|
30
|
+
* shows a subset of a larger result so names stay distinguishable across
|
|
31
|
+
* the whole result; defaults to names derived from the labels given.
|
|
32
|
+
*/
|
|
33
|
+
seriesLabels?: string[]
|
|
34
|
+
/**
|
|
35
|
+
* 'full' is the fixed wide layout every existing caller gets. 'auto' switches
|
|
36
|
+
* to the compact layout (two ticks per axis, larger text, taller plot) when
|
|
37
|
+
* the rendered chart is narrower than ANNOTATION_LABEL_MIN_WIDTH_PX;
|
|
38
|
+
* 'compact' forces it. 'dashboard' uses readable, medium-width axes and
|
|
39
|
+
* keeps text and plot height stable as its container resizes.
|
|
40
|
+
*/
|
|
41
|
+
layout?: 'full' | 'auto' | 'compact' | 'dashboard'
|
|
13
42
|
}) {
|
|
14
43
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
44
|
+
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
15
45
|
const [hoverX, setHoverX] = useState<number | null>(null)
|
|
46
|
+
const [labelsFit, setLabelsFit] = useState(true)
|
|
47
|
+
const [renderedWidth, setRenderedWidth] = useState(0)
|
|
16
48
|
const multiSeries = series.length > 1
|
|
49
|
+
const compact = layout === 'compact' || (layout === 'auto' && !labelsFit) || (layout === 'dashboard' && renderedWidth > 0 && renderedWidth < 300)
|
|
50
|
+
const countAxis = isCountUnit(unit)
|
|
17
51
|
|
|
18
52
|
const chartData = useMemo(() => {
|
|
19
53
|
if (!series.length) return null
|
|
@@ -21,17 +55,23 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
21
55
|
let minTs = Infinity
|
|
22
56
|
let maxTs = -Infinity
|
|
23
57
|
let maxVal = 0
|
|
58
|
+
let minVal = 0
|
|
24
59
|
|
|
25
60
|
for (const s of series) {
|
|
26
61
|
for (const dp of s.dataPoints) {
|
|
27
62
|
if (dp.timestamp < minTs) minTs = dp.timestamp
|
|
28
63
|
if (dp.timestamp > maxTs) maxTs = dp.timestamp
|
|
29
64
|
if (dp.value != null && dp.value > maxVal) maxVal = dp.value
|
|
65
|
+
if (dp.value != null && dp.value < minVal) minVal = dp.value
|
|
30
66
|
}
|
|
31
67
|
}
|
|
32
68
|
|
|
69
|
+
if (domain && Number.isFinite(domain.start) && Number.isFinite(domain.end) && domain.end > domain.start) {
|
|
70
|
+
minTs = domain.start
|
|
71
|
+
maxTs = domain.end
|
|
72
|
+
}
|
|
33
73
|
if (minTs === maxTs) maxTs = minTs + 60
|
|
34
|
-
if (maxVal === 0) {
|
|
74
|
+
if (maxVal === 0 && minVal === 0) {
|
|
35
75
|
// Unit-appropriate floor so the Y-axis isn't misleadingly large.
|
|
36
76
|
maxVal = unit === 'cores' ? 0.01 : unit === 'bytes' ? 1024 * 1024 : unit === 'bytes/s' ? 1024 : 1
|
|
37
77
|
}
|
|
@@ -44,21 +84,28 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
44
84
|
}
|
|
45
85
|
}
|
|
46
86
|
|
|
47
|
-
const padding = maxVal * 0.1
|
|
48
|
-
|
|
87
|
+
const padding = Math.max(maxVal, -minVal) * 0.1
|
|
88
|
+
// A count axis ends on a whole step so its ticks are integers; other
|
|
89
|
+
// units keep headroom above the maximum.
|
|
90
|
+
const yMax = countAxis && minVal >= 0 ? integerAxisTop(maxVal, compact ? 1 : 4) : maxVal + padding
|
|
91
|
+
const yMin = minVal < 0 ? minVal - padding : 0
|
|
49
92
|
|
|
50
|
-
return { minTs, maxTs, yMax, series }
|
|
51
|
-
}, [series, unit, referenceLines])
|
|
93
|
+
return { minTs, maxTs, yMax, yMin, series }
|
|
94
|
+
}, [series, unit, referenceLines, domain, countAxis, compact])
|
|
52
95
|
|
|
53
96
|
// Layout constants. marginLeft sized for the widest expected Y-tick label
|
|
54
97
|
// ("422.4 MiB" etc.) — narrow grid panels squeeze the X axis so labels
|
|
55
98
|
// need extra viewBox-space to survive the down-scale.
|
|
56
|
-
const
|
|
57
|
-
const height =
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
const base = chartLayout(compact, layout === 'dashboard', renderedWidth)
|
|
100
|
+
const { width, height, marginRight, marginTop, marginBottom, fontSize, yIntervals, xIntervals } = base
|
|
101
|
+
const yValues = chartData ? yAxisValues(chartData.yMin, chartData.yMax, yIntervals, countAxis) : []
|
|
102
|
+
const yLabels = yValues.map(val => formatMetricValue(val, unit))
|
|
103
|
+
// The compact layout's larger text makes a label like "161.5 MiB" wider
|
|
104
|
+
// than the fixed margin, and SVG clips it; grow the margin to the widest
|
|
105
|
+
// label there. The full layout keeps its fixed margin for existing callers.
|
|
106
|
+
const marginLeft = compact || layout === 'dashboard'
|
|
107
|
+
? Math.max(base.marginLeft, Math.ceil(Math.max(0, ...yLabels.map(label => label.length)) * fontSize * 0.62 + 12))
|
|
108
|
+
: base.marginLeft
|
|
62
109
|
const plotWidth = width - marginLeft - marginRight
|
|
63
110
|
const plotHeight = height - marginTop - marginBottom
|
|
64
111
|
|
|
@@ -71,31 +118,31 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
71
118
|
}
|
|
72
119
|
const toY = (val: number) => {
|
|
73
120
|
if (!chartData) return marginTop + plotHeight
|
|
74
|
-
return marginTop + plotHeight - (val / chartData.yMax) * plotHeight
|
|
121
|
+
return marginTop + plotHeight - ((val - chartData.yMin) / (chartData.yMax - chartData.yMin)) * plotHeight
|
|
75
122
|
}
|
|
76
123
|
|
|
77
124
|
const yTicks = useMemo(() => {
|
|
78
125
|
if (!chartData) return []
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return Array.from({ length: count + 1 }, (_, i) => {
|
|
82
|
-
const val = (yMax / count) * i
|
|
83
|
-
return { val, y: toY(val), label: formatMetricValue(val, unit) }
|
|
84
|
-
})
|
|
85
|
-
}, [chartData, unit])
|
|
126
|
+
return yValues.map((val, i) => ({ val, y: toY(val), label: yLabels[i] }))
|
|
127
|
+
}, [chartData, yValues, yLabels])
|
|
86
128
|
|
|
87
129
|
const xTicks = useMemo(() => {
|
|
88
130
|
if (!chartData) return []
|
|
89
131
|
const { minTs, maxTs } = chartData
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
132
|
+
return Array.from({ length: xIntervals + 1 }, (_, i) => {
|
|
133
|
+
const ts = minTs + ((maxTs - minTs) / xIntervals) * i
|
|
134
|
+
// In the compact layout the two labels sit at the plot edges and would
|
|
135
|
+
// otherwise spill past the viewBox.
|
|
136
|
+
const anchor: 'start' | 'middle' | 'end' = !(compact || layout === 'dashboard') ? 'middle' : i === 0 ? 'start' : i === xIntervals ? 'end' : 'middle'
|
|
137
|
+
return { ts, x: toX(ts), label: formatTimestamp(ts), anchor }
|
|
94
138
|
})
|
|
95
|
-
}, [chartData])
|
|
139
|
+
}, [chartData, xIntervals, compact, layout, marginLeft, plotWidth])
|
|
96
140
|
|
|
97
141
|
const paths = useMemo(() => {
|
|
98
142
|
if (!chartData) return []
|
|
143
|
+
// The area fills toward zero, not the plot floor, so a series that dips
|
|
144
|
+
// below zero is shaded on the correct side of the axis.
|
|
145
|
+
const baseline = toY(0)
|
|
99
146
|
const segments: {
|
|
100
147
|
linePath: string
|
|
101
148
|
areaPath: string
|
|
@@ -113,13 +160,14 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
113
160
|
// rather than bridging across missing data or dropping to y=0. Each run
|
|
114
161
|
// becomes its own path segment; a run needs >=2 points to render a line.
|
|
115
162
|
let run: { x: number; y: number }[] = []
|
|
163
|
+
let previousTimestamp: number | undefined
|
|
116
164
|
let runIdx = 0
|
|
117
165
|
const flush = () => {
|
|
118
166
|
if (run.length >= 2) {
|
|
119
167
|
const linePath = run.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
|
|
120
168
|
const areaPath = linePath +
|
|
121
|
-
` L${run[run.length - 1].x},${
|
|
122
|
-
` L${run[0].x},${
|
|
169
|
+
` L${run[run.length - 1].x},${baseline}` +
|
|
170
|
+
` L${run[0].x},${baseline} Z`
|
|
123
171
|
segments.push({
|
|
124
172
|
linePath,
|
|
125
173
|
areaPath,
|
|
@@ -135,15 +183,42 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
135
183
|
for (const dp of s.dataPoints) {
|
|
136
184
|
if (dp.value == null) {
|
|
137
185
|
flush()
|
|
186
|
+
previousTimestamp = undefined
|
|
138
187
|
continue
|
|
139
188
|
}
|
|
189
|
+
if (stepSeconds !== undefined && previousTimestamp !== undefined && dp.timestamp - previousTimestamp > stepSeconds * 1.5) flush()
|
|
140
190
|
run.push({ x: toX(dp.timestamp), y: toY(dp.value) })
|
|
191
|
+
previousTimestamp = dp.timestamp
|
|
141
192
|
}
|
|
142
193
|
flush()
|
|
143
194
|
})
|
|
144
195
|
|
|
145
196
|
return segments
|
|
146
|
-
}, [chartData])
|
|
197
|
+
}, [chartData, marginLeft, plotWidth, marginTop, plotHeight, multiSeries, color, fillColor, stepSeconds])
|
|
198
|
+
|
|
199
|
+
const placedAnnotations = useMemo(() => {
|
|
200
|
+
if (!chartData || !annotations?.length) return []
|
|
201
|
+
return layoutAnnotations(
|
|
202
|
+
annotations,
|
|
203
|
+
toX,
|
|
204
|
+
{ minTs: chartData.minTs, maxTs: chartData.maxTs },
|
|
205
|
+
{ left: marginLeft, right: width - marginRight },
|
|
206
|
+
)
|
|
207
|
+
}, [chartData, annotations, width, marginLeft, marginRight])
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const el = wrapperRef.current
|
|
211
|
+
if (!el || (placedAnnotations.length === 0 && layout !== 'auto' && layout !== 'dashboard') || typeof ResizeObserver === 'undefined') return
|
|
212
|
+
const update = () => {
|
|
213
|
+
const measuredWidth = el.getBoundingClientRect().width
|
|
214
|
+
setLabelsFit(measuredWidth >= ANNOTATION_LABEL_MIN_WIDTH_PX)
|
|
215
|
+
if (layout === 'dashboard') setRenderedWidth(measuredWidth)
|
|
216
|
+
}
|
|
217
|
+
update()
|
|
218
|
+
const observer = new ResizeObserver(update)
|
|
219
|
+
observer.observe(el)
|
|
220
|
+
return () => observer.disconnect()
|
|
221
|
+
}, [placedAnnotations.length, layout])
|
|
147
222
|
|
|
148
223
|
// Hover: only emit a tooltip row when the hovered timestamp lies within
|
|
149
224
|
// the series' actual sample range (with 2× median-step tolerance). Without
|
|
@@ -159,9 +234,8 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
159
234
|
.map((s, i) => ({ s, i }))
|
|
160
235
|
.filter(({ s }) => s.dataPoints.filter(dp => dp.value != null).length >= 2)
|
|
161
236
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
)
|
|
237
|
+
const allLabels = seriesLabels ?? seriesDisplayLabels(chartData.series)
|
|
238
|
+
const fullLabels = validSeries.map(({ i }) => allLabels[i] ?? `series-${i}`)
|
|
165
239
|
const shortLabels = computeShortLabels(fullLabels)
|
|
166
240
|
|
|
167
241
|
const points = validSeries.map(({ s, i }, vi) => {
|
|
@@ -175,18 +249,7 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
175
249
|
if (ts < seriesMin - tolerance || ts > seriesMax + tolerance) {
|
|
176
250
|
return null
|
|
177
251
|
}
|
|
178
|
-
|
|
179
|
-
// show and would render NaN in the tooltip / a dot at y=NaN.
|
|
180
|
-
let closest: { timestamp: number; value: number } | null = null
|
|
181
|
-
let closestDist = Infinity
|
|
182
|
-
for (const dp of dps) {
|
|
183
|
-
if (dp.value == null) continue
|
|
184
|
-
const dist = Math.abs(dp.timestamp - ts)
|
|
185
|
-
if (dist < closestDist) {
|
|
186
|
-
closestDist = dist
|
|
187
|
-
closest = { timestamp: dp.timestamp, value: dp.value }
|
|
188
|
-
}
|
|
189
|
-
}
|
|
252
|
+
const closest = nearestSample(dps, ts, stepSeconds)
|
|
190
253
|
if (!closest) return null
|
|
191
254
|
return {
|
|
192
255
|
label: shortLabels[vi],
|
|
@@ -197,8 +260,12 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
197
260
|
}
|
|
198
261
|
}).filter((p): p is NonNullable<typeof p> => p !== null)
|
|
199
262
|
|
|
200
|
-
|
|
201
|
-
|
|
263
|
+
const nearbyAnnotations = placedAnnotations.filter(
|
|
264
|
+
p => Math.abs(p.x - clampedX) <= ANNOTATION_HOVER_TOLERANCE,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return { ts, x: clampedX, points, nearbyAnnotations }
|
|
268
|
+
}, [hoverX, chartData, placedAnnotations, seriesLabels, marginLeft, plotWidth, marginTop, plotHeight, multiSeries, color, stepSeconds])
|
|
202
269
|
|
|
203
270
|
const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
|
|
204
271
|
const svg = svgRef.current
|
|
@@ -212,13 +279,18 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
212
279
|
// every hook has been invoked (Rules of Hooks).
|
|
213
280
|
if (!chartData) return null
|
|
214
281
|
|
|
282
|
+
const annotationLabelHeight = 14
|
|
283
|
+
const zeroLineY = chartData.yMin < 0 ? toY(0) : null
|
|
284
|
+
|
|
215
285
|
return (
|
|
216
|
-
<div className="relative">
|
|
286
|
+
<div className="relative" ref={wrapperRef}>
|
|
217
287
|
<svg
|
|
218
288
|
ref={svgRef}
|
|
219
289
|
viewBox={`0 0 ${width} ${height}`}
|
|
220
290
|
className="w-full h-full"
|
|
291
|
+
style={layout === 'dashboard' ? { height: 240 } : undefined}
|
|
221
292
|
preserveAspectRatio="xMidYMid meet"
|
|
293
|
+
data-chart-layout={compact ? 'compact' : 'full'}
|
|
222
294
|
>
|
|
223
295
|
{/* Grid lines */}
|
|
224
296
|
{yTicks.map((tick, i) => (
|
|
@@ -235,6 +307,19 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
235
307
|
/>
|
|
236
308
|
))}
|
|
237
309
|
|
|
310
|
+
{zeroLineY !== null && (
|
|
311
|
+
<line
|
|
312
|
+
data-chart-zero-line
|
|
313
|
+
x1={marginLeft}
|
|
314
|
+
y1={zeroLineY}
|
|
315
|
+
x2={width - marginRight}
|
|
316
|
+
y2={zeroLineY}
|
|
317
|
+
stroke="currentColor"
|
|
318
|
+
className="text-theme-border/60"
|
|
319
|
+
strokeWidth="1"
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
|
|
238
323
|
{/* Y axis labels */}
|
|
239
324
|
{yTicks.map((tick, i) => (
|
|
240
325
|
<text
|
|
@@ -243,7 +328,7 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
243
328
|
y={tick.y + 4}
|
|
244
329
|
textAnchor="end"
|
|
245
330
|
className="fill-theme-text-secondary"
|
|
246
|
-
fontSize=
|
|
331
|
+
fontSize={fontSize}
|
|
247
332
|
fontFamily="ui-monospace, monospace"
|
|
248
333
|
>
|
|
249
334
|
{tick.label}
|
|
@@ -256,9 +341,9 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
256
341
|
key={`xlabel-${i}`}
|
|
257
342
|
x={tick.x}
|
|
258
343
|
y={height - 4}
|
|
259
|
-
textAnchor=
|
|
344
|
+
textAnchor={tick.anchor}
|
|
260
345
|
className="fill-theme-text-secondary"
|
|
261
|
-
fontSize=
|
|
346
|
+
fontSize={fontSize}
|
|
262
347
|
fontFamily="ui-monospace, monospace"
|
|
263
348
|
>
|
|
264
349
|
{tick.label}
|
|
@@ -335,6 +420,67 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
335
420
|
)
|
|
336
421
|
})}
|
|
337
422
|
|
|
423
|
+
{/* Annotation markers: a vertical line per recorded instant, labelled
|
|
424
|
+
in stacked rows at the top of the plot. */}
|
|
425
|
+
{placedAnnotations.map((placed, i) => {
|
|
426
|
+
const labelY = marginTop + placed.row * (annotationLabelHeight + 2)
|
|
427
|
+
const showLabel = labelsFit && !compact && !placed.labelHidden
|
|
428
|
+
return (
|
|
429
|
+
<g
|
|
430
|
+
key={`annotation-${i}`}
|
|
431
|
+
data-chart-annotation={placed.annotation.kind}
|
|
432
|
+
data-chart-annotation-label={showLabel ? 'visible' : 'hidden'}
|
|
433
|
+
>
|
|
434
|
+
<line
|
|
435
|
+
x1={placed.x}
|
|
436
|
+
y1={marginTop}
|
|
437
|
+
x2={placed.x}
|
|
438
|
+
y2={marginTop + plotHeight}
|
|
439
|
+
stroke="currentColor"
|
|
440
|
+
className="text-accent"
|
|
441
|
+
strokeWidth="1.5"
|
|
442
|
+
strokeDasharray="3 3"
|
|
443
|
+
opacity="0.9"
|
|
444
|
+
/>
|
|
445
|
+
{showLabel && (
|
|
446
|
+
<>
|
|
447
|
+
<rect
|
|
448
|
+
x={placed.labelX}
|
|
449
|
+
y={labelY}
|
|
450
|
+
width={placed.labelWidth}
|
|
451
|
+
height={annotationLabelHeight}
|
|
452
|
+
rx="3"
|
|
453
|
+
fill="currentColor"
|
|
454
|
+
className="text-theme-surface"
|
|
455
|
+
opacity="0.9"
|
|
456
|
+
/>
|
|
457
|
+
<rect
|
|
458
|
+
x={placed.labelX}
|
|
459
|
+
y={labelY}
|
|
460
|
+
width={placed.labelWidth}
|
|
461
|
+
height={annotationLabelHeight}
|
|
462
|
+
rx="3"
|
|
463
|
+
fill="none"
|
|
464
|
+
stroke="currentColor"
|
|
465
|
+
className="text-accent/50"
|
|
466
|
+
strokeWidth="1"
|
|
467
|
+
/>
|
|
468
|
+
<text
|
|
469
|
+
x={placed.labelX + 5}
|
|
470
|
+
y={labelY + annotationLabelHeight - 3.5}
|
|
471
|
+
fontSize="11"
|
|
472
|
+
fontFamily="ui-monospace, monospace"
|
|
473
|
+
fontWeight="500"
|
|
474
|
+
className="fill-accent-text"
|
|
475
|
+
>
|
|
476
|
+
{placed.labelText}
|
|
477
|
+
</text>
|
|
478
|
+
</>
|
|
479
|
+
)}
|
|
480
|
+
</g>
|
|
481
|
+
)
|
|
482
|
+
})}
|
|
483
|
+
|
|
338
484
|
{/* Hover crosshair + dots */}
|
|
339
485
|
{hoverData && (
|
|
340
486
|
<>
|
|
@@ -383,6 +529,15 @@ export function AreaChart({ series, color, fillColor, unit, referenceLines }: {
|
|
|
383
529
|
<div className="text-theme-text-tertiary mb-1.5 font-mono">
|
|
384
530
|
{new Date(hoverData.ts * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
385
531
|
</div>
|
|
532
|
+
{hoverData.nearbyAnnotations.map((placed, i) => (
|
|
533
|
+
<div key={`annotation-${i}`} className="flex items-center gap-2 py-0.5">
|
|
534
|
+
<span className="w-2 h-2 rounded-full shrink-0 bg-accent" />
|
|
535
|
+
<span className="text-accent-text">Change recorded</span>
|
|
536
|
+
<span className="text-theme-text-primary font-mono ml-auto pl-3">
|
|
537
|
+
{placed.annotation.label}
|
|
538
|
+
</span>
|
|
539
|
+
</div>
|
|
540
|
+
))}
|
|
386
541
|
{hoverData.points.map((p, i) => (
|
|
387
542
|
<div key={i} className="flex items-center gap-2 py-0.5">
|
|
388
543
|
<div
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { describePodCoverage } from "./PrometheusChartsView";
|
|
4
|
+
import type { PrometheusResourceMetricsResult } from "./PrometheusChartsView";
|
|
5
|
+
|
|
6
|
+
function result(
|
|
7
|
+
patch: Partial<PrometheusResourceMetricsResult>,
|
|
8
|
+
): PrometheusResourceMetricsResult {
|
|
9
|
+
return { unit: "cores", ...patch } as PrometheusResourceMetricsResult;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("describePodCoverage", () => {
|
|
13
|
+
it("names the pods a chart covers and what it therefore misses", () => {
|
|
14
|
+
expect(describePodCoverage(result({ pods: 3, podsTotal: 3 }))).toBe(
|
|
15
|
+
"3 current pods; pods replaced during the window are not included",
|
|
16
|
+
);
|
|
17
|
+
expect(describePodCoverage(result({ pods: 1, podsTotal: 1 }))).toBe(
|
|
18
|
+
"1 current pod; pods replaced during the window are not included",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("says when the cap cut the list, so a short count is not read as the workload", () => {
|
|
23
|
+
expect(describePodCoverage(result({ pods: 50, podsTotal: 120 }))).toBe(
|
|
24
|
+
"first 50 of 120 current pods; pods replaced during the window are not included",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("distinguishes a workload with no pods from a kind that has none to report", () => {
|
|
29
|
+
expect(describePodCoverage(result({ pods: 0 }))).toBe(
|
|
30
|
+
"This workload has no pods running, so there is nothing to chart",
|
|
31
|
+
);
|
|
32
|
+
// A Node chart carries no pod count at all; it gets no caption rather
|
|
33
|
+
// than a claim about pods.
|
|
34
|
+
expect(describePodCoverage(result({}))).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|