@skyhook-io/k8s-ui 1.7.13 → 1.7.15
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 +6 -7
- package/src/components/audit/index.ts +1 -1
- package/src/components/checks/ChecksView.tsx +269 -142
- package/src/components/checks/index.ts +14 -1
- package/src/components/issues/IssuesView.tsx +60 -23
- package/src/components/issues/index.ts +2 -2
- package/src/components/issues/issues.test.ts +7 -0
- package/src/components/issues/types.ts +37 -7
- package/src/components/logs/WorkloadLogsViewer.tsx +16 -7
- package/src/components/resources/ResourcesView.tsx +167 -21
- package/src/components/resources/get-pod-problems.test.ts +127 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +56 -0
- package/src/components/resources/renderers/PodRenderer.tsx +1 -1
- package/src/components/resources/resource-utils.ts +39 -18
- package/src/components/shared/ResourceActionsBar.tsx +6 -6
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -2
- package/src/components/ui/drawer-components.tsx +8 -0
- package/src/components/workload/WorkloadView.tsx +4 -4
- package/src/types/core.ts +10 -0
- package/src/utils/bulk-workload-actions.test.ts +78 -0
- package/src/utils/bulk-workload-actions.ts +50 -0
- package/src/utils/index.ts +1 -0
|
@@ -701,6 +701,14 @@ export function formatKindName(kind: string): string {
|
|
|
701
701
|
return kind
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
// Resolve the display label for a kind chip. The fetched resource's `dataKind`
|
|
705
|
+
// is the authoritative PascalCase kind and is correct for every CRD; prefer it.
|
|
706
|
+
// Before data loads, fall back to deriving from the URL plural — which is only
|
|
707
|
+
// reliable for the core kinds in formatKindName's map.
|
|
708
|
+
export function displayKindName(urlPlural: string, dataKind?: string): string {
|
|
709
|
+
return dataKind || formatKindName(urlPlural)
|
|
710
|
+
}
|
|
711
|
+
|
|
704
712
|
// Type for copy handler
|
|
705
713
|
export type CopyHandler = (text: string, key: string) => void
|
|
706
714
|
|
|
@@ -51,7 +51,7 @@ import { EditableYamlView, SaveSuccessAnimation } from '../shared/EditableYamlVi
|
|
|
51
51
|
import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from '../shared/ResourceRendererDispatch'
|
|
52
52
|
import { DetailShell, type DetailShellTab } from '../shared/DetailShell'
|
|
53
53
|
import { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
|
|
54
|
-
import { getKindColorOutline,
|
|
54
|
+
import { getKindColorOutline, displayKindName } from '../ui/drawer-components'
|
|
55
55
|
import { midTruncate } from '../../utils/format'
|
|
56
56
|
|
|
57
57
|
export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'yaml'
|
|
@@ -555,7 +555,7 @@ export function WorkloadView({
|
|
|
555
555
|
<div className="flex items-center justify-between px-4 pt-3 pb-2">
|
|
556
556
|
<div className="flex items-center gap-2 flex-wrap">
|
|
557
557
|
<span className={clsx('badge', getKindColorOutline(apiKind))}>
|
|
558
|
-
{
|
|
558
|
+
{displayKindName(apiKind, resource?.kind)}
|
|
559
559
|
</span>
|
|
560
560
|
{status && (
|
|
561
561
|
<span className={clsx('badge', status.color)}>
|
|
@@ -706,7 +706,7 @@ export function WorkloadView({
|
|
|
706
706
|
</div>
|
|
707
707
|
<div className="flex items-center gap-3 text-sm text-theme-text-secondary">
|
|
708
708
|
<span className={clsx('badge', getKindColorOutline(apiKind))}>
|
|
709
|
-
{
|
|
709
|
+
{displayKindName(apiKind, resource?.kind)}
|
|
710
710
|
</span>
|
|
711
711
|
{status && (
|
|
712
712
|
<span className={clsx('badge', status.color)}>
|
|
@@ -860,7 +860,7 @@ export function WorkloadView({
|
|
|
860
860
|
)}
|
|
861
861
|
>
|
|
862
862
|
<span className="truncate text-xs font-medium text-theme-text-primary">{midTruncate(o.name, 26)}</span>
|
|
863
|
-
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary">{
|
|
863
|
+
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary">{o.kind}</span>
|
|
864
864
|
</button>
|
|
865
865
|
)
|
|
866
866
|
})}
|
package/src/types/core.ts
CHANGED
|
@@ -47,6 +47,15 @@ export const OPTIONAL_RESOURCE_KINDS: ReadonlyArray<keyof ResourcePermissions> =
|
|
|
47
47
|
'verticalPodAutoscalers',
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
+
// Per-workload write permissions. Field names must match
|
|
51
|
+
// WorkloadWritePermissions in internal/k8s/capabilities.go.
|
|
52
|
+
export interface WorkloadWritePermissions {
|
|
53
|
+
deployments: boolean
|
|
54
|
+
daemonSets: boolean
|
|
55
|
+
statefulSets: boolean
|
|
56
|
+
rollouts: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
// Feature capabilities based on RBAC permissions
|
|
51
60
|
export interface Capabilities {
|
|
52
61
|
exec: boolean // Terminal feature (pods/exec)
|
|
@@ -57,6 +66,7 @@ export interface Capabilities {
|
|
|
57
66
|
secretsUpdate: boolean // Update secrets (inline editing)
|
|
58
67
|
helmWrite: boolean // Helm write operations (install, upgrade, rollback, uninstall, apply values)
|
|
59
68
|
nodeWrite: boolean // Node write operations (cordon, uncordon, drain)
|
|
69
|
+
workloadWrites?: WorkloadWritePermissions // Workload patch permissions (restart/scale controls)
|
|
60
70
|
mcpEnabled: boolean // MCP server is running
|
|
61
71
|
// How / where this Radar binary is running. Optional on the wire so a
|
|
62
72
|
// newer frontend (e.g. radar-hub-web bundling a fresher @skyhook-io/radar-app)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
canBulkRestartKind,
|
|
4
|
+
canBulkScaleKind,
|
|
5
|
+
intersectWorkloadWrites,
|
|
6
|
+
} from './bulk-workload-actions'
|
|
7
|
+
import type { Capabilities, WorkloadWritePermissions } from '../types/core'
|
|
8
|
+
|
|
9
|
+
const writes = (overrides: Partial<WorkloadWritePermissions> = {}): WorkloadWritePermissions => ({
|
|
10
|
+
deployments: false,
|
|
11
|
+
daemonSets: false,
|
|
12
|
+
statefulSets: false,
|
|
13
|
+
rollouts: false,
|
|
14
|
+
...overrides,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const caps = (workloadWrites: WorkloadWritePermissions): Pick<Capabilities, 'workloadWrites'> => ({
|
|
18
|
+
workloadWrites,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('bulk workload action gating', () => {
|
|
22
|
+
it('allows restart for patchable workload kinds', () => {
|
|
23
|
+
const all = writes({ deployments: true, daemonSets: true, statefulSets: true, rollouts: true })
|
|
24
|
+
|
|
25
|
+
expect(canBulkRestartKind({ name: 'deployments', group: 'apps' }, all)).toBe(true)
|
|
26
|
+
expect(canBulkRestartKind({ name: 'daemonsets', group: 'apps' }, all)).toBe(true)
|
|
27
|
+
expect(canBulkRestartKind({ name: 'statefulsets', group: 'apps' }, all)).toBe(true)
|
|
28
|
+
expect(canBulkRestartKind({ name: 'rollouts', group: 'argoproj.io' }, all)).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('requires the apps group for built-in workload restart', () => {
|
|
32
|
+
const all = writes({ deployments: true, daemonSets: true, statefulSets: true })
|
|
33
|
+
|
|
34
|
+
expect(canBulkRestartKind({ name: 'deployments', group: 'apps' }, all)).toBe(true)
|
|
35
|
+
expect(canBulkRestartKind({ name: 'deployments', group: 'example.com' }, all)).toBe(false)
|
|
36
|
+
expect(canBulkRestartKind({ name: 'deployments' }, all)).toBe(false)
|
|
37
|
+
expect(canBulkRestartKind({ name: 'daemonsets', group: 'example.com' }, all)).toBe(false)
|
|
38
|
+
expect(canBulkRestartKind({ name: 'statefulsets', group: 'example.com' }, all)).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('requires the Argo group for rollout restart', () => {
|
|
42
|
+
const all = writes({ rollouts: true })
|
|
43
|
+
|
|
44
|
+
expect(canBulkRestartKind({ name: 'rollouts', group: 'argoproj.io' }, all)).toBe(true)
|
|
45
|
+
expect(canBulkRestartKind({ name: 'rollouts', group: 'example.com' }, all)).toBe(false)
|
|
46
|
+
expect(canBulkRestartKind({ name: 'rollouts' }, all)).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('allows scale only for deployments and statefulsets', () => {
|
|
50
|
+
const all = writes({ deployments: true, daemonSets: true, statefulSets: true, rollouts: true })
|
|
51
|
+
|
|
52
|
+
expect(canBulkScaleKind({ name: 'deployments', group: 'apps' }, all)).toBe(true)
|
|
53
|
+
expect(canBulkScaleKind({ name: 'statefulsets', group: 'apps' }, all)).toBe(true)
|
|
54
|
+
expect(canBulkScaleKind({ name: 'daemonsets', group: 'apps' }, all)).toBe(false)
|
|
55
|
+
expect(canBulkScaleKind({ name: 'rollouts', group: 'argoproj.io' }, all)).toBe(false)
|
|
56
|
+
expect(canBulkScaleKind({ name: 'deployments', group: 'example.com' }, all)).toBe(false)
|
|
57
|
+
expect(canBulkScaleKind({ name: 'deployments' }, all)).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('withholds actions when permissions are absent', () => {
|
|
61
|
+
expect(canBulkRestartKind({ name: 'deployments', group: 'apps' }, undefined)).toBe(false)
|
|
62
|
+
expect(canBulkScaleKind({ name: 'deployments', group: 'apps' }, undefined)).toBe(false)
|
|
63
|
+
expect(intersectWorkloadWrites(undefined)).toBeUndefined()
|
|
64
|
+
expect(intersectWorkloadWrites([])).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('intersects multi-namespace permissions with AND semantics', () => {
|
|
68
|
+
expect(intersectWorkloadWrites([
|
|
69
|
+
caps(writes({ deployments: true, daemonSets: true, statefulSets: true, rollouts: true })),
|
|
70
|
+
caps(writes({ deployments: true, statefulSets: true })),
|
|
71
|
+
])).toEqual({
|
|
72
|
+
deployments: true,
|
|
73
|
+
daemonSets: false,
|
|
74
|
+
statefulSets: true,
|
|
75
|
+
rollouts: false,
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Capabilities, WorkloadWritePermissions } from '../types/core'
|
|
2
|
+
|
|
3
|
+
export type BulkWorkloadKindInfo = {
|
|
4
|
+
name: string
|
|
5
|
+
group?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function canBulkRestartKind(
|
|
9
|
+
kind: BulkWorkloadKindInfo | null | undefined,
|
|
10
|
+
writes: WorkloadWritePermissions | undefined,
|
|
11
|
+
): boolean {
|
|
12
|
+
switch (kind?.name.toLowerCase()) {
|
|
13
|
+
case 'deployments':
|
|
14
|
+
return kind.group === 'apps' && writes?.deployments === true
|
|
15
|
+
case 'daemonsets':
|
|
16
|
+
return kind.group === 'apps' && writes?.daemonSets === true
|
|
17
|
+
case 'statefulsets':
|
|
18
|
+
return kind.group === 'apps' && writes?.statefulSets === true
|
|
19
|
+
case 'rollouts':
|
|
20
|
+
return kind.group === 'argoproj.io' && writes?.rollouts === true
|
|
21
|
+
default:
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function canBulkScaleKind(
|
|
27
|
+
kind: BulkWorkloadKindInfo | null | undefined,
|
|
28
|
+
writes: WorkloadWritePermissions | undefined,
|
|
29
|
+
): boolean {
|
|
30
|
+
switch (kind?.name.toLowerCase()) {
|
|
31
|
+
case 'deployments':
|
|
32
|
+
return kind.group === 'apps' && writes?.deployments === true
|
|
33
|
+
case 'statefulsets':
|
|
34
|
+
return kind.group === 'apps' && writes?.statefulSets === true
|
|
35
|
+
default:
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function intersectWorkloadWrites(
|
|
41
|
+
capabilities: Array<Pick<Capabilities, 'workloadWrites'>> | undefined,
|
|
42
|
+
): WorkloadWritePermissions | undefined {
|
|
43
|
+
if (!capabilities || capabilities.length === 0) return undefined
|
|
44
|
+
return {
|
|
45
|
+
deployments: capabilities.every(c => c.workloadWrites?.deployments === true),
|
|
46
|
+
daemonSets: capabilities.every(c => c.workloadWrites?.daemonSets === true),
|
|
47
|
+
statefulSets: capabilities.every(c => c.workloadWrites?.statefulSets === true),
|
|
48
|
+
rollouts: capabilities.every(c => c.workloadWrites?.rollouts === true),
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/utils/index.ts
CHANGED