@skyhook-io/k8s-ui 1.14.5 → 1.14.7

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.
Files changed (104) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.tsx +36 -4
  3. package/src/components/applications/ApplicationsView.tsx +2 -1
  4. package/src/components/audit/AuditAlerts.tsx +35 -37
  5. package/src/components/audit/AuditCard.tsx +18 -7
  6. package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
  7. package/src/components/audit/AuditFindingsTable.tsx +23 -27
  8. package/src/components/charts/AreaChart.test.tsx +13 -0
  9. package/src/components/charts/AreaChart.tsx +28 -25
  10. package/src/components/charts/PrometheusChartsView.tsx +41 -10
  11. package/src/components/charts/axis.test.ts +16 -0
  12. package/src/components/charts/axis.ts +3 -1
  13. package/src/components/charts/format.ts +4 -0
  14. package/src/components/charts/nearestSample.test.ts +31 -0
  15. package/src/components/charts/nearestSample.ts +15 -0
  16. package/src/components/checks/ChecksView.tsx +76 -28
  17. package/src/components/gitops/GitOpsDetailLayout.tsx +25 -9
  18. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
  19. package/src/components/gitops/health-provenance.test.ts +54 -0
  20. package/src/components/gitops/health-provenance.ts +24 -0
  21. package/src/components/gitops/index.ts +2 -0
  22. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
  23. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
  24. package/src/components/gitops/tree/merge.test.ts +34 -0
  25. package/src/components/gitops/tree/merge.ts +7 -0
  26. package/src/components/issues/IssuesView.tsx +35 -54
  27. package/src/components/logs/LogCore.tsx +40 -9
  28. package/src/components/resources/ResourcesSidebar.tsx +66 -78
  29. package/src/components/resources/ResourcesView.tsx +67 -2
  30. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  31. package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
  32. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
  33. package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
  36. package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
  37. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
  38. package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
  39. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
  40. package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
  41. package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
  42. package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
  43. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
  44. package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
  46. package/src/components/resources/renderers/PodRenderer.tsx +11 -0
  47. package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
  48. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
  49. package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
  50. package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
  51. package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
  52. package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
  53. package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
  54. package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
  55. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
  56. package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
  57. package/src/components/resources/renderers/index.ts +3 -0
  58. package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
  59. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +123 -0
  60. package/src/components/resources/resource-utils.ts +18 -9
  61. package/src/components/shared/CreateResourceDialog.tsx +237 -14
  62. package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
  63. package/src/components/shared/DrainPlanDialog.tsx +297 -0
  64. package/src/components/shared/ResourceActionsBar.tsx +53 -26
  65. package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
  66. package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
  67. package/src/components/shared/index.ts +4 -0
  68. package/src/components/timeline/TimelineList.tsx +14 -11
  69. package/src/components/timeline/TimelineStrip.tsx +26 -4
  70. package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
  71. package/src/components/timeline/TimelineToolbar.tsx +43 -10
  72. package/src/components/topology/TopologyControls.tsx +27 -8
  73. package/src/components/topology/TopologyGraph.tsx +14 -5
  74. package/src/components/trace/ReachabilityView.tsx +17 -16
  75. package/src/components/ui/CardSection.tsx +79 -35
  76. package/src/components/ui/Collapse.test.tsx +74 -0
  77. package/src/components/ui/Collapse.tsx +115 -29
  78. package/src/components/ui/DialogPortal.tsx +7 -2
  79. package/src/components/ui/FetchResult.test.tsx +20 -2
  80. package/src/components/ui/FetchResult.tsx +33 -3
  81. package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
  82. package/src/components/ui/FreshnessControl.tsx +8 -2
  83. package/src/components/ui/RestrictedState.tsx +7 -4
  84. package/src/components/ui/RowActionMenu.tsx +20 -8
  85. package/src/components/ui/SelectMenu.tsx +18 -6
  86. package/src/components/ui/YamlEditor.tsx +7 -9
  87. package/src/components/ui/drawer-components.tsx +14 -21
  88. package/src/components/ui/index.ts +30 -22
  89. package/src/components/workload/WorkloadView.tsx +8 -12
  90. package/src/hooks/useAnimatedUnmount.ts +21 -3
  91. package/src/theme/components.css +20 -1
  92. package/src/types/core.ts +15 -1
  93. package/src/types/gitops-insights.ts +23 -0
  94. package/src/types/gitops-tree.ts +29 -0
  95. package/src/utils/animation.ts +93 -13
  96. package/src/utils/index.ts +1 -0
  97. package/src/utils/navigation.test.ts +20 -1
  98. package/src/utils/navigation.ts +25 -0
  99. package/src/utils/network-policy.test.ts +42 -0
  100. package/src/utils/network-policy.ts +59 -0
  101. package/src/utils/reflector.ts +139 -0
  102. package/src/utils/yaml-file-import.test.ts +189 -0
  103. package/src/utils/yaml-file-import.ts +95 -0
  104. package/src/components/logs/JsonLogLine.tsx +0 -93
@@ -0,0 +1,189 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ describeFileDrag,
4
+ MAX_YAML_FILE_BYTES,
5
+ needsReplaceConfirmation,
6
+ readYamlFile,
7
+ } from './yaml-file-import'
8
+
9
+ function drag(items: Array<{ kind: string; type: string }>, types: string[] = []): DataTransfer {
10
+ return { items, types } as unknown as DataTransfer
11
+ }
12
+
13
+ // Safari keeps the dragged items protected until the drop, so `items` is empty
14
+ // mid-drag; only `types` reports that files are coming.
15
+ function safariDrag(types: string[]): DataTransfer {
16
+ return { items: [], types } as unknown as DataTransfer
17
+ }
18
+
19
+ const MULTI_DOC = `apiVersion: v1
20
+ kind: ConfigMap
21
+ metadata:
22
+ name: first
23
+ ---
24
+ apiVersion: v1
25
+ kind: Secret
26
+ metadata:
27
+ name: second
28
+ `
29
+
30
+ function yamlFile(name: string, content: string, type = ''): File {
31
+ return new File([content], name, { type })
32
+ }
33
+
34
+ describe('readYamlFile', () => {
35
+ it('loads a multi-document file as text the editor can hold unchanged', async () => {
36
+ const result = await readYamlFile([yamlFile('bundle.yaml', MULTI_DOC)])
37
+
38
+ expect(result).toEqual({ ok: true, fileName: 'bundle.yaml', yaml: MULTI_DOC })
39
+ })
40
+
41
+ it('accepts .yml as readily as .yaml', async () => {
42
+ const result = await readYamlFile([yamlFile('bundle.yml', MULTI_DOC)])
43
+
44
+ expect(result.ok).toBe(true)
45
+ })
46
+
47
+ // Browsers disagree on the MIME type for YAML — macOS Chrome commonly reports
48
+ // "", Windows reports assorted octet-stream variants — so the extension is the
49
+ // only signal that means the same thing everywhere.
50
+ it('accepts a YAML extension even when the browser reports no usable MIME type', async () => {
51
+ const result = await readYamlFile([
52
+ yamlFile('bundle.yaml', MULTI_DOC, 'application/octet-stream'),
53
+ ])
54
+
55
+ expect(result.ok).toBe(true)
56
+ })
57
+
58
+ it('rejects a non-YAML extension even when the browser claims it is YAML', async () => {
59
+ const result = await readYamlFile([yamlFile('notes.txt', MULTI_DOC, 'text/yaml')])
60
+
61
+ expect(result).toMatchObject({ ok: false, reason: 'extension' })
62
+ })
63
+
64
+ it('rejects a file that holds only whitespace', async () => {
65
+ const result = await readYamlFile([yamlFile('empty.yaml', ' \n\n ')])
66
+
67
+ expect(result).toMatchObject({ ok: false, reason: 'empty' })
68
+ })
69
+
70
+ it('rejects a file larger than the cap without reading it', async () => {
71
+ const oversize = yamlFile('huge.yaml', 'x'.repeat(MAX_YAML_FILE_BYTES + 1))
72
+
73
+ expect(await readYamlFile([oversize])).toMatchObject({ ok: false, reason: 'too-large' })
74
+ })
75
+
76
+ it('rejects a drop carrying more than one file', async () => {
77
+ const result = await readYamlFile([
78
+ yamlFile('one.yaml', MULTI_DOC),
79
+ yamlFile('two.yaml', MULTI_DOC),
80
+ ])
81
+
82
+ expect(result).toMatchObject({ ok: false, reason: 'multiple-files' })
83
+ })
84
+
85
+ it('rejects an empty selection', async () => {
86
+ expect(await readYamlFile([])).toMatchObject({ ok: false, reason: 'no-file' })
87
+ })
88
+
89
+ it('reports a file the browser cannot read rather than throwing', async () => {
90
+ const unreadable = {
91
+ name: 'gone.yaml',
92
+ size: 128,
93
+ text: () => Promise.reject(new Error('NotReadableError')),
94
+ } as unknown as File
95
+
96
+ expect(await readYamlFile([unreadable])).toMatchObject({ ok: false, reason: 'unreadable' })
97
+ })
98
+
99
+ it('explains the cap in the message it hands the error banner', async () => {
100
+ const result = await readYamlFile([yamlFile('huge.yaml', 'x'.repeat(MAX_YAML_FILE_BYTES + 1))])
101
+
102
+ expect(result.ok).toBe(false)
103
+ if (!result.ok) expect(result.message).toContain('6 MiB')
104
+ })
105
+ })
106
+
107
+ // A drag exposes only `kind` and `type`; the file name is withheld until drop,
108
+ // and YAML has no MIME type worth trusting. So the overlay promises "a file is
109
+ // coming", never "an acceptable YAML file is coming".
110
+ describe('describeFileDrag', () => {
111
+ it('ignores a drag with nothing attached', () => {
112
+ expect(describeFileDrag(null)).toBe('none')
113
+ })
114
+
115
+ it('leaves a text drag alone so the editor keeps its own drop behaviour', () => {
116
+ expect(describeFileDrag(drag([{ kind: 'string', type: 'text/plain' }]))).toBe('none')
117
+ })
118
+
119
+ it('reports a single file even when the browser withholds its type', () => {
120
+ expect(describeFileDrag(drag([{ kind: 'file', type: '' }]))).toBe('file')
121
+ })
122
+
123
+ it('counts only files when a drag carries companion text items', () => {
124
+ const dragged = drag([
125
+ { kind: 'file', type: '' },
126
+ { kind: 'string', type: 'text/uri-list' },
127
+ ])
128
+
129
+ expect(describeFileDrag(dragged)).toBe('file')
130
+ })
131
+
132
+ it('distinguishes a multi-file drag, which is the one rejection knowable up front', () => {
133
+ const dragged = drag([
134
+ { kind: 'file', type: '' },
135
+ { kind: 'file', type: '' },
136
+ ])
137
+
138
+ expect(describeFileDrag(dragged)).toBe('multiple-files')
139
+ })
140
+ })
141
+
142
+ // The create dialog opens prefilled — ResourcesView with a kind skeleton,
143
+ // WorkloadView with a duplicate — so a non-empty editor is the normal state,
144
+ // not a sign the user has work worth protecting.
145
+ describe('needsReplaceConfirmation', () => {
146
+ const SKELETON = 'apiVersion: v1\nkind: ConfigMap\n'
147
+
148
+ it('loads straight over an untouched skeleton', () => {
149
+ expect(needsReplaceConfirmation(SKELETON, SKELETON)).toBe(false)
150
+ })
151
+
152
+ it('loads straight into an empty editor', () => {
153
+ expect(needsReplaceConfirmation('', '')).toBe(false)
154
+ })
155
+
156
+ it('asks before discarding edits the user actually made', () => {
157
+ expect(needsReplaceConfirmation(`${SKELETON} name: checkout\n`, SKELETON)).toBe(true)
158
+ })
159
+
160
+ it('asks before discarding a manifest pasted into an editor that opened empty', () => {
161
+ expect(needsReplaceConfirmation(SKELETON, '')).toBe(true)
162
+ })
163
+
164
+ it('treats a whitespace-only difference as nothing worth protecting', () => {
165
+ expect(needsReplaceConfirmation(`${SKELETON}\n \n`, SKELETON)).toBe(false)
166
+ })
167
+ })
168
+
169
+ describe('describeFileDrag across browsers', () => {
170
+ it('sees a file drag in Safari, where items stay empty until the drop', () => {
171
+ expect(describeFileDrag(safariDrag(['Files']))).toBe('file')
172
+ })
173
+
174
+ it('still ignores a Safari text drag, which advertises no Files type', () => {
175
+ expect(describeFileDrag(safariDrag(['text/plain']))).toBe('none')
176
+ })
177
+
178
+ it('counts files when the browser does expose them alongside the Files type', () => {
179
+ const dragged = drag(
180
+ [
181
+ { kind: 'file', type: '' },
182
+ { kind: 'file', type: '' },
183
+ ],
184
+ ['Files'],
185
+ )
186
+
187
+ expect(describeFileDrag(dragged)).toBe('multiple-files')
188
+ })
189
+ })
@@ -0,0 +1,95 @@
1
+ // The same limit /resources/apply enforces, so the number in this message is the
2
+ // number the server uses. Loading the file blocks the main thread while the
3
+ // editor parses the buffer — around 90ms at 128 KiB, 460ms at 1 MiB, 2.4s at
4
+ // 5.5 MiB — which the import covers with a loading state rather than by capping
5
+ // below what the server would accept.
6
+ export const MAX_YAML_FILE_BYTES = 6 * 1024 * 1024
7
+
8
+ const YAML_EXTENSIONS = ['.yaml', '.yml'] as const
9
+
10
+ export const YAML_FILE_ACCEPT = '.yaml,.yml'
11
+
12
+ export type YamlFileRejectionReason =
13
+ | 'no-file'
14
+ | 'multiple-files'
15
+ | 'extension'
16
+ | 'too-large'
17
+ | 'empty'
18
+ | 'unreadable'
19
+
20
+ export type YamlFileResult =
21
+ | { ok: true; fileName: string; yaml: string }
22
+ | { ok: false; reason: YamlFileRejectionReason; message: string }
23
+
24
+ function reject(reason: YamlFileRejectionReason, message: string): YamlFileResult {
25
+ return { ok: false, reason, message }
26
+ }
27
+
28
+ function hasYamlExtension(name: string): boolean {
29
+ const lower = name.toLowerCase()
30
+ return YAML_EXTENSIONS.some((extension) => lower.endsWith(extension))
31
+ }
32
+
33
+ export async function readYamlFile(
34
+ files: ArrayLike<File> | null | undefined,
35
+ ): Promise<YamlFileResult> {
36
+ if (!files || files.length === 0) {
37
+ return reject('no-file', 'No file selected.')
38
+ }
39
+ if (files.length > 1) {
40
+ return reject('multiple-files', 'Select a single YAML file — one file at a time.')
41
+ }
42
+
43
+ const file = files[0]
44
+ if (!hasYamlExtension(file.name)) {
45
+ return reject('extension', `${file.name} is not a YAML file. Choose a .yaml or .yml file.`)
46
+ }
47
+ if (file.size > MAX_YAML_FILE_BYTES) {
48
+ const limit = Math.round(MAX_YAML_FILE_BYTES / (1024 * 1024))
49
+ return reject('too-large', `${file.name} is larger than the ${limit} MiB limit.`)
50
+ }
51
+
52
+ let content: string
53
+ try {
54
+ content = await file.text()
55
+ } catch {
56
+ return reject('unreadable', `${file.name} could not be read. Check the file and try again.`)
57
+ }
58
+
59
+ if (!content.trim()) {
60
+ return reject('empty', `${file.name} is empty.`)
61
+ }
62
+
63
+ return { ok: true, fileName: file.name, yaml: content }
64
+ }
65
+
66
+ export type FileDragState = 'none' | 'file' | 'multiple-files'
67
+
68
+ export function describeFileDrag(transfer: DataTransfer | null | undefined): FileDragState {
69
+ if (!transfer) return 'none'
70
+
71
+ // Safari keeps the dragged items protected until the drop, so `items` is
72
+ // empty for the whole drag. Every browser advertises "Files" in `types`
73
+ // though, which makes that the signal for whether files are coming; `items`
74
+ // only adds the count, where the browser is willing to give one.
75
+ const carriesFiles = transfer.types ? Array.from(transfer.types).includes('Files') : false
76
+
77
+ let files = 0
78
+ const items = transfer.items
79
+ if (items) {
80
+ for (let index = 0; index < items.length; index++) {
81
+ if (items[index].kind === 'file') files++
82
+ }
83
+ }
84
+
85
+ if (!carriesFiles && files === 0) return 'none'
86
+ return files > 1 ? 'multiple-files' : 'file'
87
+ }
88
+
89
+ // The dialog opens prefilled in both of its entry points, so "the editor holds
90
+ // YAML" does not mean the user has work to lose. Only a divergence from what
91
+ // the dialog put there does.
92
+ export function needsReplaceConfirmation(current: string, initial: string): boolean {
93
+ const edited = current.trim()
94
+ return edited !== '' && edited !== initial.trim()
95
+ }
@@ -1,93 +0,0 @@
1
- import { useState, useMemo } from 'react'
2
- import { ChevronRight, ChevronDown } from 'lucide-react'
3
- import type { LogLevel } from './useLogBuffer'
4
- import { getLogPalette, getLogLevelColor } from './log-palette'
5
-
6
- interface JsonLogLineProps {
7
- content: string
8
- level: LogLevel
9
- wordWrap: boolean
10
- /** Dark/light palette selector. Defaults to `true` for the dark-by-default viewer. */
11
- isDark?: boolean
12
- }
13
-
14
- export function JsonLogLine({ content, level, wordWrap, isDark = true }: JsonLogLineProps) {
15
- const [expanded, setExpanded] = useState(false)
16
- const palette = useMemo(() => getLogPalette(isDark), [isDark])
17
- const levelColor = getLogLevelColor(level, isDark)
18
-
19
- const parsed = useMemo(() => {
20
- try {
21
- return JSON.parse(content.trim())
22
- } catch {
23
- return null
24
- }
25
- }, [content])
26
-
27
- if (!parsed) {
28
- return (
29
- <span className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${levelColor}`}>
30
- {content}
31
- </span>
32
- )
33
- }
34
-
35
- const summary = buildSummary(parsed)
36
- const fieldCount = Object.keys(parsed).length
37
-
38
- return (
39
- <span className={levelColor}>
40
- <button
41
- onClick={() => setExpanded(!expanded)}
42
- className={`inline-flex items-center gap-0.5 ${palette.hoverSurface} rounded px-0.5 -ml-0.5 align-middle`}
43
- >
44
- {expanded
45
- ? <ChevronDown className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
46
- : <ChevronRight className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
47
- }
48
- </button>
49
- {!expanded ? (
50
- <span className={wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}>
51
- {summary}
52
- <span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
53
- </span>
54
- ) : (
55
- <span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
56
- {formatJsonExpanded(parsed)}
57
- </span>
58
- )}
59
- </span>
60
- )
61
- }
62
-
63
- function buildSummary(obj: Record<string, unknown>): string {
64
- const parts: string[] = []
65
-
66
- // Level
67
- const lvl = obj.level || obj.severity || obj.lvl
68
- if (typeof lvl === 'string') {
69
- parts.push(lvl.toUpperCase())
70
- }
71
-
72
- // Message
73
- const msg = obj.msg || obj.message
74
- if (typeof msg === 'string') {
75
- parts.push(msg)
76
- }
77
-
78
- // Error
79
- const err = obj.error || obj.err
80
- if (typeof err === 'string') {
81
- parts.push(`error=${err}`)
82
- }
83
-
84
- return parts.join(' ')
85
- }
86
-
87
- function formatJsonExpanded(obj: Record<string, unknown>): string {
88
- try {
89
- return JSON.stringify(obj, null, 2)
90
- } catch {
91
- return String(obj)
92
- }
93
- }