@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^26.2.0",
|
|
92
|
-
"@types/react": "^19.
|
|
93
|
-
"@types/react-dom": "^19.
|
|
92
|
+
"@types/react": "^19.3.0",
|
|
93
|
+
"@types/react-dom": "^19.3.0",
|
|
94
94
|
"@xterm/addon-fit": "^0.11.0",
|
|
95
95
|
"@xterm/addon-web-links": "^0.12.0",
|
|
96
96
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"elkjs": "^0.11.1",
|
|
101
101
|
"lucide-react": "^1.37.0",
|
|
102
102
|
"monaco-editor": "0.55.1",
|
|
103
|
-
"react": "^19.
|
|
104
|
-
"react-dom": "^19.
|
|
103
|
+
"react": "^19.3.0",
|
|
104
|
+
"react-dom": "^19.3.0",
|
|
105
105
|
"typescript": "^6.0.2",
|
|
106
106
|
"vitest": "^4.1.11",
|
|
107
107
|
"yaml": "^2.9.0"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ClipboardCheck, ArrowRight, Check } from 'lucide-react'
|
|
1
|
+
import { ClipboardCheck, ArrowRight, Check, AlertTriangle } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { SEVERITY_TEXT, SEVERITY_DOT } from '../../utils/badge-colors'
|
|
4
4
|
import { SEVERITY_FILL_CLASS, SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
5
5
|
|
|
6
6
|
export interface AuditCardData {
|
|
7
|
+
missingInputs?: string[]
|
|
7
8
|
passing: number
|
|
8
9
|
/** Raw compatibility counts; warning renders as Medium and danger as High. */
|
|
9
10
|
warning: number
|
|
@@ -17,16 +18,17 @@ interface AuditCardProps {
|
|
|
17
18
|
onNavigate: () => void
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
type SeverityLevel = 'success' | 'high' | 'medium'
|
|
21
|
+
type SeverityLevel = 'success' | 'high' | 'medium' | 'neutral'
|
|
21
22
|
|
|
22
23
|
function getSeverityLevel(data: AuditCardData): SeverityLevel {
|
|
23
|
-
if (data.warning + data.danger === 0) return 'success'
|
|
24
|
+
if (data.warning + data.danger === 0) return data.missingInputs?.length || data.passing === 0 ? 'neutral' : 'success'
|
|
24
25
|
const highRatio = data.danger / (data.warning + data.danger)
|
|
25
26
|
if (highRatio > 0.2) return 'high'
|
|
26
27
|
return 'medium'
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const ACCENT_BG: Record<SeverityLevel, string> = {
|
|
31
|
+
neutral: 'bg-theme-elevated',
|
|
30
32
|
success: 'bg-green-500/10',
|
|
31
33
|
high: 'bg-orange-500/10',
|
|
32
34
|
medium: 'bg-yellow-500/10',
|
|
@@ -35,9 +37,10 @@ const ACCENT_BG: Record<SeverityLevel, string> = {
|
|
|
35
37
|
export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
36
38
|
const total = data.passing + data.warning + data.danger
|
|
37
39
|
const issueCount = data.warning + data.danger
|
|
38
|
-
const
|
|
40
|
+
const incomplete = (data.missingInputs?.length ?? 0) > 0
|
|
41
|
+
const allPassing = issueCount === 0 && total > 0 && !incomplete
|
|
39
42
|
const level = getSeverityLevel(data)
|
|
40
|
-
const accentColor = level === 'success' ? SEVERITY_TEXT.success : SEVERITY_TEXT_CLASS[level]
|
|
43
|
+
const accentColor = level === 'neutral' ? 'text-theme-text-secondary' : level === 'success' ? SEVERITY_TEXT.success : SEVERITY_TEXT_CLASS[level]
|
|
41
44
|
const accentBg = ACCENT_BG[level]
|
|
42
45
|
|
|
43
46
|
return (
|
|
@@ -56,7 +59,7 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
56
59
|
</span>
|
|
57
60
|
) : (
|
|
58
61
|
<span className={clsx('badge-sm', accentBg, accentColor)}>
|
|
59
|
-
<Check className="w-3 h-3" />
|
|
62
|
+
{allPassing ? <Check className="w-3 h-3" /> : <ClipboardCheck className="w-3 h-3" />}
|
|
60
63
|
</span>
|
|
61
64
|
)}
|
|
62
65
|
</div>
|
|
@@ -71,6 +74,12 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
71
74
|
<span className="text-xs text-theme-text-tertiary">{total} checks across {Object.keys(data.categories).length} categories</span>
|
|
72
75
|
)}
|
|
73
76
|
</div>
|
|
77
|
+
) : issueCount === 0 ? (
|
|
78
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
79
|
+
{incomplete ? <AlertTriangle className="w-8 h-8 text-warning-text" /> : <ClipboardCheck className="w-8 h-8 text-theme-text-tertiary" />}
|
|
80
|
+
<span className="text-sm font-medium text-theme-text-primary">{incomplete ? 'Some checks could not run' : 'No resources evaluated'}</span>
|
|
81
|
+
<span className="text-xs text-theme-text-secondary">{incomplete ? 'No findings in the available data.' : 'Check the selected scope and check settings.'}</span>
|
|
82
|
+
</div>
|
|
74
83
|
) : (
|
|
75
84
|
<>
|
|
76
85
|
{/* Distribution bar — only show when we have passing data for context */}
|
|
@@ -107,7 +116,7 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
107
116
|
)}
|
|
108
117
|
</div>
|
|
109
118
|
) : (
|
|
110
|
-
<span className={clsx('text-xs font-semibold', SEVERITY_TEXT.success)}>All passing</span>
|
|
119
|
+
<span className={clsx('text-xs font-semibold', incomplete ? 'text-theme-text-secondary' : SEVERITY_TEXT.success)}>{incomplete ? 'No findings' : 'All passing'}</span>
|
|
111
120
|
)}
|
|
112
121
|
</div>
|
|
113
122
|
)
|
|
@@ -117,6 +126,8 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
|
|
|
117
126
|
)}
|
|
118
127
|
</div>
|
|
119
128
|
|
|
129
|
+
{incomplete && issueCount > 0 && <p className="px-4 pb-2 text-xs text-warning-text">Some checks could not run.</p>}
|
|
130
|
+
|
|
120
131
|
<div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-end">
|
|
121
132
|
<span className={clsx(
|
|
122
133
|
'flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider transition-colors',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { AuditCard } from './AuditCard'
|
|
4
|
+
import { ChecksView } from '../checks/ChecksView'
|
|
5
|
+
import { FilterLocationProvider } from '../../filter-state'
|
|
6
|
+
|
|
7
|
+
describe('audit coverage presentation', () => {
|
|
8
|
+
for (const [passing, missingInputs, expected] of [
|
|
9
|
+
[0, [], 'No resources evaluated'],
|
|
10
|
+
[0, ['secrets'], 'Some checks could not run'],
|
|
11
|
+
[4, ['secrets'], 'Some checks could not run'],
|
|
12
|
+
[4, [], 'All checks passing'],
|
|
13
|
+
] as const) {
|
|
14
|
+
it(`shows ${expected} with ${passing} passing and ${missingInputs.length} unavailable inputs`, () => {
|
|
15
|
+
const html = renderToString(<AuditCard data={{ passing, missingInputs: [...missingInputs], warning: 0, danger: 0, categories: {} }} onNavigate={() => {}} />)
|
|
16
|
+
expect(html).toContain(expected)
|
|
17
|
+
if (missingInputs.length || passing === 0) expect(html).not.toContain('All checks passing')
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
it('keeps a coverage warning alongside real findings', () => {
|
|
21
|
+
const html = renderToString(<AuditCard data={{ passing: 1, missingInputs: ['secrets'], warning: 1, danger: 0, categories: { Security: { passing: 1, warning: 0, danger: 0 } } }} onNavigate={() => {}} />)
|
|
22
|
+
expect(html).toContain('Some checks could not run')
|
|
23
|
+
expect(html).not.toContain('All passing')
|
|
24
|
+
})
|
|
25
|
+
for (const [evaluated, missingInputs, expected] of [
|
|
26
|
+
[0, [], 'No resources evaluated'],
|
|
27
|
+
[0, ['secrets'], 'No findings in the available data'],
|
|
28
|
+
[4, ['secrets'], 'No findings in the available data'],
|
|
29
|
+
[4, [], 'Every audited resource passed'],
|
|
30
|
+
] as const) {
|
|
31
|
+
it(`Checks: ${expected}`, () => {
|
|
32
|
+
const html = renderToString(
|
|
33
|
+
<FilterLocationProvider value={{ searchParams: new URLSearchParams(), update: () => {} }}>
|
|
34
|
+
<ChecksView checks={[]} catalog={{}} anyData evaluated={evaluated} missingInputs={[...missingInputs]} />
|
|
35
|
+
</FilterLocationProvider>,
|
|
36
|
+
)
|
|
37
|
+
expect(html).toContain(expected)
|
|
38
|
+
if (missingInputs.length) {
|
|
39
|
+
expect(html).toContain('Unavailable inputs:')
|
|
40
|
+
expect(html).toContain('secrets')
|
|
41
|
+
expect(html).not.toContain('Every audited resource passed')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { AreaChart } from './AreaChart'
|
|
4
|
+
import { layoutAnnotations, ANNOTATION_LABEL_ROWS } from './annotations'
|
|
5
|
+
import { formatTimestamp } from './format'
|
|
6
|
+
import type { ChartAnnotation, TimeSeries } from './types'
|
|
7
|
+
|
|
8
|
+
const t0 = 1_788_679_000
|
|
9
|
+
|
|
10
|
+
function series(points: Array<[number, number | null]>, labels: Record<string, string> = {}): TimeSeries {
|
|
11
|
+
return { labels, dataPoints: points.map(([offset, value]) => ({ timestamp: t0 + offset, value })) }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function render(props: Partial<Parameters<typeof AreaChart>[0]> & { series: TimeSeries[] }) {
|
|
15
|
+
return renderToStaticMarkup(
|
|
16
|
+
<AreaChart color="#3b82f6" fillColor="#3b82f622" unit="" {...props} />,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function attrs(html: string, tag: string, attr: string): string[] {
|
|
21
|
+
const out: string[] = []
|
|
22
|
+
const re = new RegExp(`<${tag}\\b[^>]*\\s${attr}="([^"]*)"`, 'g')
|
|
23
|
+
for (const match of html.matchAll(re)) out.push(match[1])
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('AreaChart annotations', () => {
|
|
28
|
+
const base = [series([[0, 1], [600, 2], [1200, 3], [1800, 2]])]
|
|
29
|
+
|
|
30
|
+
it('renders a marker and label per annotation inside the domain and none outside', () => {
|
|
31
|
+
const annotations: ChartAnnotation[] = [
|
|
32
|
+
{ timestamp: t0 + 600, label: 'ConfigMap api-config', kind: 'change' },
|
|
33
|
+
{ timestamp: t0 + 9_000, label: 'outside', kind: 'change' },
|
|
34
|
+
]
|
|
35
|
+
const html = render({ series: base, annotations })
|
|
36
|
+
expect(html.match(/data-chart-annotation="change"/g)).toHaveLength(1)
|
|
37
|
+
expect(html).toContain('data-chart-annotation-label="visible"')
|
|
38
|
+
expect(html).toContain('ConfigMap api-config')
|
|
39
|
+
expect(html).not.toContain('outside')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('stacks overlapping labels into rows and hides labels past the row limit', () => {
|
|
43
|
+
const toX = (ts: number) => 84 + ((ts - t0) / 1800) * 876
|
|
44
|
+
const crowded: ChartAnnotation[] = Array.from({ length: ANNOTATION_LABEL_ROWS + 1 }, (_, i) => ({
|
|
45
|
+
timestamp: t0 + i * 5,
|
|
46
|
+
label: `Change ${i}`,
|
|
47
|
+
kind: 'change',
|
|
48
|
+
}))
|
|
49
|
+
const placed = layoutAnnotations(crowded, toX, { minTs: t0, maxTs: t0 + 1800 }, { left: 84, right: 960 })
|
|
50
|
+
expect(placed.map(p => p.row)).toEqual([0, 1, 2, -1])
|
|
51
|
+
expect(placed.at(-1)?.labelHidden).toBe(true)
|
|
52
|
+
const far: ChartAnnotation[] = [
|
|
53
|
+
{ timestamp: t0, label: 'first', kind: 'change' },
|
|
54
|
+
{ timestamp: t0 + 1200, label: 'second', kind: 'change' },
|
|
55
|
+
]
|
|
56
|
+
expect(layoutAnnotations(far, toX, { minTs: t0, maxTs: t0 + 1800 }, { left: 84, right: 960 }).map(p => p.row)).toEqual([0, 0])
|
|
57
|
+
const html = render({ series: base, annotations: crowded })
|
|
58
|
+
expect(html.match(/data-chart-annotation-label="visible"/g)).toHaveLength(ANNOTATION_LABEL_ROWS)
|
|
59
|
+
expect(html.match(/data-chart-annotation-label="hidden"/g)).toHaveLength(1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('keeps a long label readable by truncating it', () => {
|
|
63
|
+
const html = render({
|
|
64
|
+
series: base,
|
|
65
|
+
annotations: [{ timestamp: t0 + 600, label: 'ConfigMap a-very-long-configmap-name-that-goes-on', kind: 'change' }],
|
|
66
|
+
})
|
|
67
|
+
expect(html).toContain('ConfigMap a-very-long-confi…')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('AreaChart domain and axes', () => {
|
|
72
|
+
it('uses an explicit domain wider than the samples for the X axis', () => {
|
|
73
|
+
const html = render({
|
|
74
|
+
series: [series([[600, 1], [1200, 2]])],
|
|
75
|
+
domain: { start: t0, end: t0 + 3600 },
|
|
76
|
+
})
|
|
77
|
+
const labels = attrs(html, 'text', 'text-anchor')
|
|
78
|
+
expect(labels.length).toBeGreaterThan(0)
|
|
79
|
+
expect(html).toContain(`>${formatTimestamp(t0)}<`)
|
|
80
|
+
expect(html).toContain(`>${formatTimestamp(t0 + 3600)}<`)
|
|
81
|
+
const xs = attrs(html, 'text', 'x').map(Number).filter(x => x >= 84)
|
|
82
|
+
expect(Math.max(...xs)).toBeCloseTo(960, 0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('extends the Y axis below zero and draws a zero line for negative values', () => {
|
|
86
|
+
const html = render({ series: [series([[0, -4], [600, 2], [1200, -1]])] })
|
|
87
|
+
expect(html).toContain('data-chart-zero-line')
|
|
88
|
+
expect(html).toContain('>-4.40<')
|
|
89
|
+
const positive = render({ series: [series([[0, 1], [600, 2]])] })
|
|
90
|
+
expect(positive).not.toContain('data-chart-zero-line')
|
|
91
|
+
expect(positive).toContain('>0<')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('breaks the line across null gaps instead of bridging them', () => {
|
|
95
|
+
const html = render({ series: [series([[0, 1], [300, 2], [600, null], [900, 3], [1200, 4]])] })
|
|
96
|
+
const lines = attrs(html, 'path', 'fill').filter(fill => fill === 'none')
|
|
97
|
+
expect(lines).toHaveLength(2)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('renders sparse samples without a path until two finite points exist', () => {
|
|
101
|
+
expect(attrs(render({ series: [series([[0, 1]])] }), 'path', 'fill')).toHaveLength(0)
|
|
102
|
+
expect(attrs(render({ series: [series([[0, 1], [3600, 5]])] }), 'path', 'fill').filter(fill => fill === 'none')).toHaveLength(1)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('breaks omitted evaluations only when the caller supplies the interval', () => {
|
|
106
|
+
const sparse = [series([[0, 1], [15, 2], [105, 3], [120, 4]])]
|
|
107
|
+
const paths = (stepSeconds?: number) => attrs(render({ series: sparse, stepSeconds }), 'path', 'fill').filter(fill => fill === 'none')
|
|
108
|
+
expect(paths(15)).toHaveLength(2)
|
|
109
|
+
expect(paths()).toHaveLength(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('preserves explicit gaps, zeros and isolated-sample behavior with a known interval', () => {
|
|
113
|
+
const gaps = [series([[0, 0], [15, 0], [30, null], [45, 1], [60, 2]])]
|
|
114
|
+
expect(attrs(render({ series: gaps, stepSeconds: 15 }), 'path', 'fill').filter(fill => fill === 'none')).toHaveLength(2)
|
|
115
|
+
expect(attrs(render({ series: [series([[0, 1]])], stepSeconds: 15 }), 'path', 'fill')).toHaveLength(0)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('renders nothing for an empty series list', () => {
|
|
119
|
+
expect(render({ series: [] })).toBe('')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('AreaChart compact layout and count axes', () => {
|
|
124
|
+
const base = [series([[0, 1], [600, 2], [1200, 3], [1800, 2]])]
|
|
125
|
+
|
|
126
|
+
it('keeps the full layout for existing callers', () => {
|
|
127
|
+
const html = render({ series: base })
|
|
128
|
+
expect(html).toContain('viewBox="0 0 1000 300"')
|
|
129
|
+
expect(html).toContain('data-chart-layout="full"')
|
|
130
|
+
expect(attrs(html, 'text', 'text-anchor').filter(a => a === 'end')).toHaveLength(5)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('draws two ticks per axis, larger text and a taller plot when compact', () => {
|
|
134
|
+
const html = render({
|
|
135
|
+
series: base,
|
|
136
|
+
layout: 'compact',
|
|
137
|
+
annotations: [{ timestamp: t0 + 600, label: 'ConfigMap api-config', kind: 'change' }],
|
|
138
|
+
})
|
|
139
|
+
expect(html).toContain('viewBox="0 0 400 260"')
|
|
140
|
+
expect(html).toContain('data-chart-layout="compact"')
|
|
141
|
+
// Two Y labels plus the right-aligned last X label.
|
|
142
|
+
expect(attrs(html, 'text', 'text-anchor').filter(a => a === 'end')).toHaveLength(3)
|
|
143
|
+
expect(attrs(html, 'text', 'text-anchor').filter(a => a === 'start')).toHaveLength(1)
|
|
144
|
+
expect(attrs(html, 'text', 'text-anchor').filter(a => a === 'middle')).toHaveLength(0)
|
|
145
|
+
expect(html).toContain('font-size="14"')
|
|
146
|
+
expect(html).toContain('data-chart-annotation-label="hidden"')
|
|
147
|
+
expect(html).toContain(`>${formatTimestamp(t0)}<`)
|
|
148
|
+
expect(html).toContain(`>${formatTimestamp(t0 + 1800)}<`)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('widens the compact left margin so a long Y label is not clipped', () => {
|
|
152
|
+
const html = render({ series: [series([[0, 100 * 1024 * 1024], [600, 161.5 * 1024 * 1024]])], unit: 'bytes', layout: 'compact' })
|
|
153
|
+
expect(html).toContain('177.7 MiB')
|
|
154
|
+
const yLabelX = Math.min(...attrs(html, 'text', 'x').map(Number).filter(x => x > 0))
|
|
155
|
+
expect(yLabelX).toBeGreaterThan(70)
|
|
156
|
+
const compactSmall = render({ series: [series([[0, 1], [600, 2]])], layout: 'compact' })
|
|
157
|
+
expect(Math.min(...attrs(compactSmall, 'text', 'x').map(Number).filter(x => x > 0))).toBe(52)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('uses integer ticks with a nice step for count series', () => {
|
|
161
|
+
const zero = render({ series: [series([[0, 0], [600, 0], [1200, 0]])], unit: 'count' })
|
|
162
|
+
expect(zero).toContain('>0<')
|
|
163
|
+
expect(zero).toContain('>1<')
|
|
164
|
+
expect(zero).not.toContain('1.10')
|
|
165
|
+
const restarts = render({ series: [series([[0, 6], [600, 12], [1200, 18]])], unit: 'count' })
|
|
166
|
+
for (const tick of ['0', '5', '10', '15', '20']) expect(restarts).toContain(`>${tick}<`)
|
|
167
|
+
expect(restarts).not.toContain('6.05')
|
|
168
|
+
const compactCount = render({ series: [series([[0, 6], [600, 18]])], unit: 'count', layout: 'compact' })
|
|
169
|
+
expect(attrs(compactCount, 'text', 'text-anchor').filter(a => a === 'end')).toHaveLength(3)
|
|
170
|
+
expect(compactCount).toContain('>20<')
|
|
171
|
+
expect(compactCount).not.toContain('>10<')
|
|
172
|
+
})
|
|
173
|
+
})
|