@skyhook-io/k8s-ui 1.8.15 → 1.9.1
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 +1 -1
- package/src/components/applications/ApplicationDetail.test.tsx +28 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -0
- package/src/components/audit/AuditAlerts.tsx +13 -11
- package/src/components/audit/AuditBadgeTooltip.test.tsx +6 -0
- package/src/components/audit/AuditBadgeTooltip.tsx +9 -6
- package/src/components/audit/AuditCard.tsx +15 -13
- package/src/components/audit/AuditFindingsTable.tsx +33 -31
- package/src/components/audit/AuditSeverityPresentation.test.tsx +64 -0
- package/src/components/checks/types.ts +4 -4
- package/src/components/issues/types.ts +4 -0
- package/src/components/logs/useLogSearch.ts +12 -7
- package/src/components/resources/ResourcesView.per-container.test.ts +111 -0
- package/src/components/resources/ResourcesView.tsx +310 -99
- package/src/components/resources/renderers/PodRenderer.tsx +4 -1
- package/src/components/timeline/TimelineList.tsx +7 -2
- package/src/components/timeline/TimelineSwimlanes.tsx +37 -19
- package/src/components/timeline/index.ts +0 -2
- package/src/components/timeline/timeline-live.test.ts +0 -37
- package/src/components/timeline/timeline-live.ts +2 -24
- package/src/components/topology/K8sResourceNode.tsx +11 -9
- package/src/components/ui/FilterPill.tsx +3 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useDebouncedValue.ts +29 -0
- package/src/types/core.ts +17 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
|
1
|
+
import { useState, useMemo, useCallback, useDeferredValue, useEffect, useRef } from 'react'
|
|
2
2
|
import type { VirtuosoHandle } from 'react-virtuoso'
|
|
3
3
|
import type { LogEntry } from './useLogBuffer'
|
|
4
4
|
import { stripAnsi, escapeRegExp } from '../../utils/log-format'
|
|
@@ -40,17 +40,22 @@ export function useLogSearch(
|
|
|
40
40
|
const [currentMatch, setCurrentMatch] = useState(0)
|
|
41
41
|
const [isOpen, setIsOpen] = useState(false)
|
|
42
42
|
|
|
43
|
+
// The scan strips ANSI and regex-tests every buffered line — on a multi-MB
|
|
44
|
+
// buffer that's too slow to run synchronously per keystroke. Deferring lets
|
|
45
|
+
// the input echo immediately while the match set catches up.
|
|
46
|
+
const deferredQuery = useDeferredValue(query)
|
|
47
|
+
|
|
43
48
|
const { matchIndices, regexError } = useMemo(() => {
|
|
44
|
-
if (!
|
|
49
|
+
if (!deferredQuery) {
|
|
45
50
|
return { matchIndices: [] as number[], regexError: null }
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
try {
|
|
49
54
|
let pattern: RegExp
|
|
50
55
|
if (isRegex) {
|
|
51
|
-
pattern = new RegExp(
|
|
56
|
+
pattern = new RegExp(deferredQuery, isCaseSensitive ? 'g' : 'gi')
|
|
52
57
|
} else {
|
|
53
|
-
pattern = new RegExp(escapeRegExp(
|
|
58
|
+
pattern = new RegExp(escapeRegExp(deferredQuery), isCaseSensitive ? 'g' : 'gi')
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
const indices: number[] = []
|
|
@@ -65,14 +70,14 @@ export function useLogSearch(
|
|
|
65
70
|
} catch (e) {
|
|
66
71
|
return { matchIndices: [] as number[], regexError: e instanceof Error ? e.message : 'Invalid regex' }
|
|
67
72
|
}
|
|
68
|
-
}, [entries,
|
|
73
|
+
}, [entries, deferredQuery, isRegex, isCaseSensitive])
|
|
69
74
|
|
|
70
75
|
// Filtered entries for filter mode
|
|
71
76
|
const filteredEntries = useMemo(() => {
|
|
72
|
-
if (!isFilterMode || !
|
|
77
|
+
if (!isFilterMode || !deferredQuery) return entries
|
|
73
78
|
const matchSet = new Set(matchIndices)
|
|
74
79
|
return entries.filter((_, i) => matchSet.has(i))
|
|
75
|
-
}, [entries, isFilterMode,
|
|
80
|
+
}, [entries, isFilterMode, deferredQuery, matchIndices])
|
|
76
81
|
|
|
77
82
|
// Reset current match when search criteria change (but not when new entries arrive during streaming)
|
|
78
83
|
const prevCriteria = useRef({ query, isRegex, isCaseSensitive })
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ContainerResourceMetrics } from '../../types'
|
|
3
|
+
import { podAggregate, readContainer } from './ResourcesView'
|
|
4
|
+
|
|
5
|
+
// Build a CPU-only container fixture; memory fields default to 0.
|
|
6
|
+
function cpu(name: string, usage: number, request: number, limit: number): ContainerResourceMetrics {
|
|
7
|
+
return { name, cpu: usage, cpuRequest: request, cpuLimit: limit, memory: 0, memoryRequest: 0, memoryLimit: 0 }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Build a memory-only container fixture; cpu fields default to 0.
|
|
11
|
+
function mem(name: string, usage: number, request: number, limit: number): ContainerResourceMetrics {
|
|
12
|
+
return { name, cpu: 0, cpuRequest: 0, cpuLimit: 0, memory: usage, memoryRequest: request, memoryLimit: limit }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('readContainer', () => {
|
|
16
|
+
it("uses limit as the yardstick when a limit is set", () => {
|
|
17
|
+
const r = readContainer(cpu('app', 50, 25, 100), 'cpu')
|
|
18
|
+
expect(r.yardstick).toBe('limit')
|
|
19
|
+
expect(r.denom).toBe(100)
|
|
20
|
+
expect(r.pct).toBe(50)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('falls back to request when limit is 0 but request is set', () => {
|
|
24
|
+
const r = readContainer(cpu('app', 80, 100, 0), 'cpu')
|
|
25
|
+
expect(r.yardstick).toBe('request')
|
|
26
|
+
expect(r.denom).toBe(100)
|
|
27
|
+
expect(r.pct).toBe(80)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('allows request-only pct to exceed 100%', () => {
|
|
31
|
+
const r = readContainer(cpu('app', 200, 100, 0), 'cpu')
|
|
32
|
+
expect(r.yardstick).toBe('request')
|
|
33
|
+
expect(r.pct).toBe(200)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("reports 'none' when neither request nor limit is set", () => {
|
|
37
|
+
const r = readContainer(cpu('app', 40, 0, 0), 'cpu')
|
|
38
|
+
expect(r.yardstick).toBe('none')
|
|
39
|
+
expect(r.pct).toBe(-1)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('reads the memory fields for the memory kind', () => {
|
|
43
|
+
const r = readContainer(mem('app', 64, 32, 128), 'memory')
|
|
44
|
+
expect(r.yardstick).toBe('limit')
|
|
45
|
+
expect(r.denom).toBe(128)
|
|
46
|
+
expect(r.pct).toBe(50)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('podAggregate', () => {
|
|
51
|
+
it("reports mode 'limit' with the summed limit and request marker when every container is limited", () => {
|
|
52
|
+
const containers = [
|
|
53
|
+
cpu('app', 30, 50, 100),
|
|
54
|
+
cpu('sidecar', 40, 50, 200),
|
|
55
|
+
]
|
|
56
|
+
const result = podAggregate(containers, 'cpu')
|
|
57
|
+
expect(result.mode).toBe('limit')
|
|
58
|
+
expect(result.totalUsage).toBe(70)
|
|
59
|
+
expect(result.denom).toBe(300) // summed limit
|
|
60
|
+
expect(result.markerPct).toBeCloseTo((100 / 300) * 100) // summed request / summed limit
|
|
61
|
+
expect(result.unlimitedCount).toBe(0)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("omits the marker in 'limit' mode when no container sets a request", () => {
|
|
65
|
+
const containers = [cpu('a', 30, 0, 100), cpu('b', 40, 0, 200)]
|
|
66
|
+
const result = podAggregate(containers, 'cpu')
|
|
67
|
+
expect(result.mode).toBe('limit')
|
|
68
|
+
expect(result.markerPct).toBeUndefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("reports mode 'partial' with usage and unbounded count when only some containers are limited", () => {
|
|
72
|
+
const containers = [
|
|
73
|
+
cpu('app', 80, 0, 0), // dominant, unbounded
|
|
74
|
+
cpu('sidecar', 5, 0, 100), // small, limited
|
|
75
|
+
]
|
|
76
|
+
const result = podAggregate(containers, 'cpu')
|
|
77
|
+
expect(result.mode).toBe('partial')
|
|
78
|
+
expect(result.totalUsage).toBe(85)
|
|
79
|
+
expect(result.denom).toBe(0) // a partial limit sum is not a real ceiling
|
|
80
|
+
expect(result.unlimitedCount).toBe(1)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("reports mode 'request' with the summed request when there are no limits but some requests", () => {
|
|
84
|
+
const containers = [
|
|
85
|
+
cpu('a', 50, 100, 0),
|
|
86
|
+
cpu('b', 40, 200, 0),
|
|
87
|
+
]
|
|
88
|
+
const result = podAggregate(containers, 'cpu')
|
|
89
|
+
expect(result.mode).toBe('request')
|
|
90
|
+
expect(result.totalUsage).toBe(90)
|
|
91
|
+
expect(result.denom).toBe(300) // summed request
|
|
92
|
+
expect(result.unlimitedCount).toBe(2)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("reports mode 'none' when no container sets a request or limit", () => {
|
|
96
|
+
const containers = [cpu('a', 10, 0, 0), cpu('b', 20, 0, 0)]
|
|
97
|
+
const result = podAggregate(containers, 'cpu')
|
|
98
|
+
expect(result.mode).toBe('none')
|
|
99
|
+
expect(result.totalUsage).toBe(30)
|
|
100
|
+
expect(result.denom).toBe(0)
|
|
101
|
+
expect(result.unlimitedCount).toBe(2)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('aggregates the memory fields for the memory kind', () => {
|
|
105
|
+
const containers = [mem('a', 64, 32, 128), mem('b', 96, 64, 256)]
|
|
106
|
+
const result = podAggregate(containers, 'memory')
|
|
107
|
+
expect(result.mode).toBe('limit')
|
|
108
|
+
expect(result.totalUsage).toBe(160)
|
|
109
|
+
expect(result.denom).toBe(384) // summed memory limit
|
|
110
|
+
})
|
|
111
|
+
})
|