@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.
- package/package.json +5 -5
- package/src/components/applications/ApplicationDetail.tsx +36 -4
- package/src/components/applications/ApplicationsView.tsx +2 -1
- package/src/components/audit/AuditAlerts.tsx +35 -37
- package/src/components/audit/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/audit/AuditFindingsTable.tsx +23 -27
- package/src/components/charts/AreaChart.test.tsx +13 -0
- package/src/components/charts/AreaChart.tsx +28 -25
- package/src/components/charts/PrometheusChartsView.tsx +41 -10
- package/src/components/charts/axis.test.ts +16 -0
- package/src/components/charts/axis.ts +3 -1
- package/src/components/charts/format.ts +4 -0
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/checks/ChecksView.tsx +76 -28
- package/src/components/gitops/GitOpsDetailLayout.tsx +25 -9
- 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/issues/IssuesView.tsx +35 -54
- package/src/components/logs/LogCore.tsx +40 -9
- package/src/components/resources/ResourcesSidebar.tsx +66 -78
- package/src/components/resources/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
- package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
- 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/PolicyCoverageSection.tsx +7 -4
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
- 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/SbomReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
- 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 +123 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +237 -14
- 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/timeline/TimelineList.tsx +14 -11
- package/src/components/timeline/TimelineStrip.tsx +26 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
- package/src/components/timeline/TimelineToolbar.tsx +43 -10
- package/src/components/topology/TopologyControls.tsx +27 -8
- package/src/components/topology/TopologyGraph.tsx +14 -5
- package/src/components/trace/ReachabilityView.tsx +17 -16
- package/src/components/ui/CardSection.tsx +79 -35
- package/src/components/ui/Collapse.test.tsx +74 -0
- package/src/components/ui/Collapse.tsx +115 -29
- package/src/components/ui/DialogPortal.tsx +7 -2
- package/src/components/ui/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
- package/src/components/ui/FreshnessControl.tsx +8 -2
- package/src/components/ui/RestrictedState.tsx +7 -4
- package/src/components/ui/RowActionMenu.tsx +20 -8
- package/src/components/ui/SelectMenu.tsx +18 -6
- package/src/components/ui/YamlEditor.tsx +7 -9
- package/src/components/ui/drawer-components.tsx +14 -21
- package/src/components/ui/index.ts +30 -22
- package/src/components/workload/WorkloadView.tsx +8 -12
- package/src/hooks/useAnimatedUnmount.ts +21 -3
- package/src/theme/components.css +20 -1
- package/src/types/core.ts +15 -1
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- package/src/utils/animation.ts +93 -13
- 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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Info, Loader2, RefreshCw } from 'lucide-react'
|
|
2
|
+
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
3
|
+
import { Badge, type BadgeSeverity } from '../ui/Badge'
|
|
4
|
+
import { AlertBanner } from '../ui/drawer-components'
|
|
5
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
6
|
+
import { pluralize } from '../../utils/pluralize'
|
|
7
|
+
|
|
8
|
+
// Shapes returned by POST /api/nodes/{name}/drain-plan.
|
|
9
|
+
export type DrainOutcome = 'evict' | 'skip' | 'may-block'
|
|
10
|
+
|
|
11
|
+
export interface DrainPlanPod {
|
|
12
|
+
namespace: string
|
|
13
|
+
name: string
|
|
14
|
+
outcome: DrainOutcome
|
|
15
|
+
reason: string
|
|
16
|
+
emptyDir: boolean
|
|
17
|
+
pdb?: string
|
|
18
|
+
pdbChecked: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DrainPlan {
|
|
22
|
+
node: string
|
|
23
|
+
generatedAt: string
|
|
24
|
+
estimate: boolean
|
|
25
|
+
options: { ignoreDaemonSets: boolean; deleteEmptyDirData: boolean; force: boolean; gracePeriodSeconds?: number }
|
|
26
|
+
summary: { evict: number; skip: number; mayBlock: number }
|
|
27
|
+
pods: DrainPlanPod[]
|
|
28
|
+
pdbsEvaluated: boolean
|
|
29
|
+
pdbError?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The two options the operator chooses in the dialog. Both are always sent
|
|
33
|
+
// explicitly to the drain endpoint; nothing relies on server-side defaults.
|
|
34
|
+
export interface DrainDialogOptions {
|
|
35
|
+
force: boolean
|
|
36
|
+
deleteEmptyDirData: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_DRAIN_DIALOG_OPTIONS: DrainDialogOptions = { force: false, deleteEmptyDirData: false }
|
|
40
|
+
|
|
41
|
+
/** A plan is only usable when it was computed for this node with these options. */
|
|
42
|
+
export function planMatches(plan: DrainPlan | null | undefined, nodeName: string, options: DrainDialogOptions): plan is DrainPlan {
|
|
43
|
+
return Boolean(
|
|
44
|
+
plan &&
|
|
45
|
+
plan.node === nodeName &&
|
|
46
|
+
plan.options.force === options.force &&
|
|
47
|
+
plan.options.deleteEmptyDirData === options.deleteEmptyDirData,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Pods whose emptyDir data would be discarded by this drain (evicted, not skipped). */
|
|
52
|
+
export function emptyDirPodsAtRisk(plan: DrainPlan): DrainPlanPod[] {
|
|
53
|
+
return plan.pods.filter((p) => p.emptyDir && p.outcome !== 'skip')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CanConfirmDrainInput {
|
|
57
|
+
plan: DrainPlan | null | undefined
|
|
58
|
+
nodeName: string
|
|
59
|
+
options: DrainDialogOptions
|
|
60
|
+
loading: boolean
|
|
61
|
+
/** the last plan request failed; with plan support the drain stays disabled until a plan is shown again */
|
|
62
|
+
error?: string | null
|
|
63
|
+
/** false when the host cannot compute plans (no onPlanDrain); the dialog then confirms without one */
|
|
64
|
+
planSupported: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the destructive Drain button may be enabled. With plan support the
|
|
69
|
+
* operator must see a current plan first. Deleting emptyDir data is its own
|
|
70
|
+
* deliberate choice, off by default and warned about by name, so it does not
|
|
71
|
+
* gate the button a second time.
|
|
72
|
+
*/
|
|
73
|
+
export function canConfirmDrain({ plan, nodeName, options, loading, error, planSupported }: CanConfirmDrainInput): boolean {
|
|
74
|
+
if (loading) return false
|
|
75
|
+
if (planSupported && error) return false
|
|
76
|
+
const current = planMatches(plan, nodeName, options) ? plan : null
|
|
77
|
+
if (planSupported && !current) return false
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const OUTCOME_SEVERITY: Record<DrainOutcome, BadgeSeverity> = {
|
|
82
|
+
evict: 'warning',
|
|
83
|
+
'may-block': 'error',
|
|
84
|
+
skip: 'neutral',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const OUTCOME_LABEL: Record<DrainOutcome, string> = {
|
|
88
|
+
evict: 'evict',
|
|
89
|
+
'may-block': 'may block',
|
|
90
|
+
skip: 'skip',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ESTIMATE_CAVEAT =
|
|
94
|
+
'An estimate, not a guarantee: the drain re-lists live state when it runs, and pods, budgets and permissions can change until then. ' +
|
|
95
|
+
'Evictions a budget refuses are retried until the drain deadline (60 seconds, shared by all evictions); a pod covered by more than one budget is refused outright.'
|
|
96
|
+
|
|
97
|
+
interface DrainPlanContentProps {
|
|
98
|
+
nodeName: string
|
|
99
|
+
plan?: DrainPlan | null
|
|
100
|
+
loading: boolean
|
|
101
|
+
error?: string | null
|
|
102
|
+
options: DrainDialogOptions
|
|
103
|
+
onOptionsChange: (options: DrainDialogOptions) => void
|
|
104
|
+
planSupported: boolean
|
|
105
|
+
/** Recompute the plan with the current options; also the retry after a failed plan. */
|
|
106
|
+
onRefreshPlan?: () => void
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The body of the drain dialog: options, the plan table and the emptyDir
|
|
111
|
+
* warning. Presentational, so it renders without a DOM (tests use
|
|
112
|
+
* react-dom/server); the enclosing dialog owns the confirm gating.
|
|
113
|
+
*/
|
|
114
|
+
export function DrainPlanContent({
|
|
115
|
+
nodeName, plan, loading, error, options, onOptionsChange, planSupported, onRefreshPlan,
|
|
116
|
+
}: DrainPlanContentProps) {
|
|
117
|
+
const current = planMatches(plan, nodeName, options) ? plan : null
|
|
118
|
+
const atRisk = current ? emptyDirPodsAtRisk(current) : []
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="flex flex-col gap-3 text-sm text-theme-text-secondary">
|
|
122
|
+
<div className="flex flex-wrap gap-x-6 gap-y-2">
|
|
123
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
124
|
+
<input
|
|
125
|
+
type="checkbox"
|
|
126
|
+
checked={options.force}
|
|
127
|
+
onChange={(e) => onOptionsChange({ ...options, force: e.target.checked })}
|
|
128
|
+
className="rounded border-theme-border"
|
|
129
|
+
/>
|
|
130
|
+
Force: evict pods not managed by a controller (they are not recreated)
|
|
131
|
+
</label>
|
|
132
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
133
|
+
<input
|
|
134
|
+
type="checkbox"
|
|
135
|
+
checked={options.deleteEmptyDirData}
|
|
136
|
+
onChange={(e) => onOptionsChange({ ...options, deleteEmptyDirData: e.target.checked })}
|
|
137
|
+
className="rounded border-theme-border"
|
|
138
|
+
/>
|
|
139
|
+
Delete emptyDir data: evict pods that use emptyDir volumes (their data is lost)
|
|
140
|
+
</label>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{planSupported && loading && (
|
|
144
|
+
<div className="flex items-center gap-2 text-theme-text-secondary">
|
|
145
|
+
<Loader2 className="w-4 h-4 animate-spin" /> Computing the plan…
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{planSupported && !loading && error && (
|
|
150
|
+
<AlertBanner variant="error" title="Could not compute the drain plan" message={error}>
|
|
151
|
+
{onRefreshPlan && (
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={onRefreshPlan}
|
|
155
|
+
className="mt-2 rounded border border-theme-border bg-theme-surface px-2 py-1 text-xs text-theme-text-primary transition-colors hover:bg-theme-hover"
|
|
156
|
+
>
|
|
157
|
+
Try again
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
160
|
+
</AlertBanner>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{planSupported && !loading && !error && current && (
|
|
164
|
+
<>
|
|
165
|
+
<div className="flex items-start justify-between gap-2">
|
|
166
|
+
<div className="text-theme-text-primary">
|
|
167
|
+
{pluralize(current.summary.evict, 'pod')} to evict,{' '}
|
|
168
|
+
{current.pdbsEvaluated
|
|
169
|
+
? `${current.summary.mayBlock} may block on a PodDisruptionBudget`
|
|
170
|
+
: 'PodDisruptionBudgets not evaluated'}
|
|
171
|
+
, {current.summary.skip} skipped.
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex items-center gap-1 shrink-0 text-xs text-theme-text-tertiary">
|
|
174
|
+
<span>Estimated at {new Date(current.generatedAt).toLocaleTimeString()}</span>
|
|
175
|
+
<Tooltip content={ESTIMATE_CAVEAT} className="max-w-xs">
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
aria-label="About this estimate"
|
|
179
|
+
className="p-0.5 rounded text-theme-text-tertiary transition-colors hover:text-theme-text-secondary"
|
|
180
|
+
>
|
|
181
|
+
<Info className="w-3.5 h-3.5" />
|
|
182
|
+
</button>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
{onRefreshPlan && (
|
|
185
|
+
<Tooltip content="Recompute the plan">
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
aria-label="Recompute the plan"
|
|
189
|
+
onClick={onRefreshPlan}
|
|
190
|
+
className="p-1 rounded text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary"
|
|
191
|
+
>
|
|
192
|
+
<RefreshCw className="w-3.5 h-3.5" />
|
|
193
|
+
</button>
|
|
194
|
+
</Tooltip>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{!current.pdbsEvaluated && (
|
|
200
|
+
<AlertBanner
|
|
201
|
+
variant="warning"
|
|
202
|
+
title="PodDisruptionBudgets were not evaluated"
|
|
203
|
+
message={current.pdbError ?? 'Budgets could not be listed; evictions may still be refused.'}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<div className="max-h-72 overflow-auto rounded border border-theme-border">
|
|
208
|
+
<table className="w-full text-xs">
|
|
209
|
+
<tbody>
|
|
210
|
+
{current.pods.map((p) => (
|
|
211
|
+
<tr key={`${p.namespace}/${p.name}`} className="border-b border-theme-border last:border-b-0 align-top">
|
|
212
|
+
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
213
|
+
<Badge severity={OUTCOME_SEVERITY[p.outcome]} size="sm">{OUTCOME_LABEL[p.outcome]}</Badge>
|
|
214
|
+
</td>
|
|
215
|
+
<td className="px-2 py-1.5 font-mono whitespace-nowrap text-theme-text-primary">
|
|
216
|
+
{p.namespace}/{p.name}
|
|
217
|
+
</td>
|
|
218
|
+
<td className="px-2 py-1.5 text-theme-text-secondary">{p.reason}</td>
|
|
219
|
+
</tr>
|
|
220
|
+
))}
|
|
221
|
+
{current.pods.length === 0 && (
|
|
222
|
+
<tr>
|
|
223
|
+
<td className="px-2 py-3 text-theme-text-secondary" colSpan={3}>No pods are scheduled on this node.</td>
|
|
224
|
+
</tr>
|
|
225
|
+
)}
|
|
226
|
+
</tbody>
|
|
227
|
+
</table>
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{options.deleteEmptyDirData && (
|
|
233
|
+
<AlertBanner
|
|
234
|
+
variant="warning"
|
|
235
|
+
title="emptyDir data will be discarded"
|
|
236
|
+
message={
|
|
237
|
+
current && atRisk.length > 0
|
|
238
|
+
? `${pluralize(atRisk.length, 'pod')} that would be evicted ${atRisk.length === 1 ? 'uses' : 'use'} emptyDir: ${atRisk.map((p) => `${p.namespace}/${p.name}`).join(', ')}. That data cannot be recovered.`
|
|
239
|
+
: current
|
|
240
|
+
? 'No pod that would be evicted uses emptyDir right now. Any that does when the drain runs loses that data, which cannot be recovered.'
|
|
241
|
+
: 'Every evicted pod that uses emptyDir volumes loses that data, which cannot be recovered.'
|
|
242
|
+
}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface DrainPlanDialogProps {
|
|
250
|
+
open: boolean
|
|
251
|
+
nodeName: string
|
|
252
|
+
plan?: DrainPlan | null
|
|
253
|
+
loading: boolean
|
|
254
|
+
error?: string | null
|
|
255
|
+
options: DrainDialogOptions
|
|
256
|
+
onOptionsChange: (options: DrainDialogOptions) => void
|
|
257
|
+
onConfirm: (options: DrainDialogOptions) => void
|
|
258
|
+
onClose: () => void
|
|
259
|
+
isDraining: boolean
|
|
260
|
+
/** Whether the host can compute plans. Without it the dialog confirms without one. */
|
|
261
|
+
planSupported: boolean
|
|
262
|
+
/** Recompute the plan with the current options; also the retry after a failed plan. */
|
|
263
|
+
onRefreshPlan?: () => void
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function DrainPlanDialog({
|
|
267
|
+
open, nodeName, plan, loading, error, options, onOptionsChange, onConfirm, onClose, isDraining, planSupported, onRefreshPlan,
|
|
268
|
+
}: DrainPlanDialogProps) {
|
|
269
|
+
const confirmEnabled = canConfirmDrain({ plan, nodeName, options, loading, error, planSupported })
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<ConfirmDialog
|
|
273
|
+
open={open}
|
|
274
|
+
onClose={onClose}
|
|
275
|
+
onConfirm={() => onConfirm(options)}
|
|
276
|
+
title="Drain Node"
|
|
277
|
+
message={`Cordon "${nodeName}" and evict the pods listed below. The list is an estimate: the drain re-lists live state when it runs.`}
|
|
278
|
+
confirmLabel={isDraining ? 'Draining...' : 'Drain'}
|
|
279
|
+
variant="danger"
|
|
280
|
+
isLoading={isDraining}
|
|
281
|
+
isClosable
|
|
282
|
+
confirmDisabled={!confirmEnabled}
|
|
283
|
+
className="max-w-3xl"
|
|
284
|
+
>
|
|
285
|
+
<DrainPlanContent
|
|
286
|
+
nodeName={nodeName}
|
|
287
|
+
plan={plan}
|
|
288
|
+
loading={loading}
|
|
289
|
+
error={error}
|
|
290
|
+
options={options}
|
|
291
|
+
onOptionsChange={onOptionsChange}
|
|
292
|
+
planSupported={planSupported}
|
|
293
|
+
onRefreshPlan={onRefreshPlan}
|
|
294
|
+
/>
|
|
295
|
+
</ConfirmDialog>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
@@ -28,6 +28,7 @@ import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
|
28
28
|
import { displayKindName } from '../ui/drawer-components'
|
|
29
29
|
import { getDefaultContainerName } from '../resources/resource-utils'
|
|
30
30
|
import { SetImageDialog, type ManagedImageSource } from './SetImageDialog'
|
|
31
|
+
import { DrainPlanDialog, DEFAULT_DRAIN_DIALOG_OPTIONS, type DrainDialogOptions, type DrainPlan } from './DrainPlanDialog'
|
|
31
32
|
import type { WorkloadImageInventory, WorkloadImageUpdate } from '../../types/core'
|
|
32
33
|
import { isArgoRolloutResource } from '../../utils/workload-rollout'
|
|
33
34
|
import { isCoreBatchJob } from '../../utils/api-resources'
|
|
@@ -135,6 +136,17 @@ interface ResourceActionsBarProps {
|
|
|
135
136
|
isUncordoningNode?: boolean
|
|
136
137
|
onDrainNode?: (params: { name: string; options?: { deleteEmptyDirData?: boolean; force?: boolean } }) => void
|
|
137
138
|
isDrainingNode?: boolean
|
|
139
|
+
// Read-only drain plan (POST /nodes/{name}/drain-plan). When the host provides
|
|
140
|
+
// onPlanDrain, the drain dialog shows the plan before enabling the destructive action.
|
|
141
|
+
onPlanDrain?: (params: { name: string; options: DrainDialogOptions }) => void
|
|
142
|
+
onPlanDrainReset?: () => void // forget the last plan when the dialog closes, so a re-open never shows a stale one
|
|
143
|
+
drainPlan?: DrainPlan | null
|
|
144
|
+
isPlanningDrain?: boolean
|
|
145
|
+
drainPlanError?: string | null
|
|
146
|
+
// The connected backend has no drain-plan endpoint (version skew, e.g. a newer
|
|
147
|
+
// frontend against an older radar). The dialog falls back to the plan-less
|
|
148
|
+
// acknowledgement-only mode instead of keeping Drain disabled on a dead request.
|
|
149
|
+
drainPlanUnsupported?: boolean
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
export function ResourceActionsBar({
|
|
@@ -165,6 +177,7 @@ export function ResourceActionsBar({
|
|
|
165
177
|
onCordonNode, isCordoningNode,
|
|
166
178
|
onUncordonNode, isUncordoningNode,
|
|
167
179
|
onDrainNode, isDrainingNode,
|
|
180
|
+
onPlanDrain, onPlanDrainReset, drainPlan, isPlanningDrain, drainPlanError, drainPlanUnsupported,
|
|
168
181
|
}: ResourceActionsBarProps) {
|
|
169
182
|
const kind = resource.kind.toLowerCase()
|
|
170
183
|
const coreBatchJob = isCoreBatchJob(kind, resource.group)
|
|
@@ -183,7 +196,28 @@ export function ResourceActionsBar({
|
|
|
183
196
|
// Node operation confirmation state
|
|
184
197
|
const [showCordonConfirm, setShowCordonConfirm] = useState(false)
|
|
185
198
|
const [showDrainConfirm, setShowDrainConfirm] = useState(false)
|
|
186
|
-
const [
|
|
199
|
+
const [drainOptions, setDrainOptions] = useState<DrainDialogOptions>(DEFAULT_DRAIN_DIALOG_OPTIONS)
|
|
200
|
+
|
|
201
|
+
// A backend without the plan endpoint stays unsupported for as long as the dialog is
|
|
202
|
+
// open: the host reports it through a mutation error, which the next request clears,
|
|
203
|
+
// so without latching every option change would refire a request known to 404 and
|
|
204
|
+
// bounce the dialog out of its fallback mode.
|
|
205
|
+
const [planUnsupported, setPlanUnsupported] = useState(false)
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (drainPlanUnsupported) setPlanUnsupported(true)
|
|
208
|
+
}, [drainPlanUnsupported])
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!showDrainConfirm) setPlanUnsupported(false)
|
|
211
|
+
}, [showDrainConfirm])
|
|
212
|
+
const planSupported = Boolean(onPlanDrain) && !planUnsupported
|
|
213
|
+
|
|
214
|
+
// Fetch (and refetch on option changes) the read-only plan while the drain dialog is open.
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (showDrainConfirm && onPlanDrain && !planUnsupported) {
|
|
217
|
+
onPlanDrain({ name: resource.name, options: drainOptions })
|
|
218
|
+
}
|
|
219
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
220
|
+
}, [showDrainConfirm, drainOptions.force, drainOptions.deleteEmptyDirData, resource.name, planUnsupported])
|
|
187
221
|
|
|
188
222
|
// Rollback dialog state
|
|
189
223
|
const [showRevisions, setShowRevisions] = useState(false)
|
|
@@ -675,40 +709,33 @@ export function ResourceActionsBar({
|
|
|
675
709
|
isLoading={isCordoningNode}
|
|
676
710
|
/>
|
|
677
711
|
|
|
678
|
-
{/* Node drain
|
|
679
|
-
<
|
|
712
|
+
{/* Node drain: read-only plan first, then the destructive action with explicit options */}
|
|
713
|
+
<DrainPlanDialog
|
|
680
714
|
open={showDrainConfirm}
|
|
715
|
+
nodeName={resource.name}
|
|
716
|
+
plan={drainPlan}
|
|
717
|
+
loading={Boolean(isPlanningDrain)}
|
|
718
|
+
error={planSupported ? drainPlanError : null}
|
|
719
|
+
options={drainOptions}
|
|
720
|
+
onOptionsChange={setDrainOptions}
|
|
721
|
+
planSupported={planSupported}
|
|
722
|
+
onRefreshPlan={planSupported ? () => onPlanDrain?.({ name: resource.name, options: drainOptions }) : undefined}
|
|
723
|
+
isDraining={Boolean(isDrainingNode)}
|
|
681
724
|
onClose={() => {
|
|
682
725
|
setShowDrainConfirm(false)
|
|
683
|
-
|
|
726
|
+
setDrainOptions(DEFAULT_DRAIN_DIALOG_OPTIONS)
|
|
727
|
+
onPlanDrainReset?.()
|
|
684
728
|
}}
|
|
685
|
-
onConfirm={() => {
|
|
729
|
+
onConfirm={(opts) => {
|
|
686
730
|
onDrainNode?.({
|
|
687
731
|
name: resource.name,
|
|
688
|
-
options: { deleteEmptyDirData:
|
|
732
|
+
options: { deleteEmptyDirData: opts.deleteEmptyDirData, force: opts.force },
|
|
689
733
|
})
|
|
690
734
|
setShowDrainConfirm(false)
|
|
691
|
-
|
|
735
|
+
setDrainOptions(DEFAULT_DRAIN_DIALOG_OPTIONS)
|
|
736
|
+
onPlanDrainReset?.()
|
|
692
737
|
}}
|
|
693
|
-
|
|
694
|
-
message={`Cordon and evict all pods from node "${resource.name}"? DaemonSet pods will be skipped.`}
|
|
695
|
-
confirmLabel={isDrainingNode ? 'Draining...' : 'Drain'}
|
|
696
|
-
variant="danger"
|
|
697
|
-
isLoading={isDrainingNode}
|
|
698
|
-
isClosable
|
|
699
|
-
>
|
|
700
|
-
<div className="flex flex-col gap-2 text-sm text-theme-text-secondary">
|
|
701
|
-
<label className="flex items-center gap-2 cursor-pointer">
|
|
702
|
-
<input
|
|
703
|
-
type="checkbox"
|
|
704
|
-
checked={drainForce}
|
|
705
|
-
onChange={(e) => setDrainForce(e.target.checked)}
|
|
706
|
-
className="rounded border-theme-border"
|
|
707
|
-
/>
|
|
708
|
-
Force (evict pods not managed by a controller)
|
|
709
|
-
</label>
|
|
710
|
-
</div>
|
|
711
|
-
</ConfirmDialog>
|
|
738
|
+
/>
|
|
712
739
|
|
|
713
740
|
{showRevisions && isRollbackKind && (
|
|
714
741
|
<RevisionHistoryDialog
|
|
@@ -180,6 +180,27 @@ function renderCollidingKind(kind: string, apiVersion: string): string {
|
|
|
180
180
|
)
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
describe('LimitRange dispatch', () => {
|
|
184
|
+
const limitRange = {
|
|
185
|
+
apiVersion: 'v1',
|
|
186
|
+
kind: 'LimitRange',
|
|
187
|
+
metadata: { name: 'team-limits', namespace: 'dev' },
|
|
188
|
+
spec: { limits: [{ type: 'Container', defaultRequest: { cpu: '100m' }, default: { cpu: '500m' } }] },
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it('renders the declared rules instead of the generic fallback', () => {
|
|
192
|
+
const html = renderKind('limitranges', limitRange, 'dev')
|
|
193
|
+
expect(html).toContain('Defaults & Constraints')
|
|
194
|
+
expect(html).toContain('100m')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// A LimitRange has no status. Reporting one would be an invention, and the
|
|
198
|
+
// drawer header would disagree with the object.
|
|
199
|
+
it('reports no health for a kind that has none', () => {
|
|
200
|
+
expect(getResourceStatus('limitranges', limitRange)).toBeNull()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
183
204
|
describe('getResourceStatus — workload rollout activity', () => {
|
|
184
205
|
const steadyDegraded = {
|
|
185
206
|
metadata: { generation: 4 },
|
|
@@ -277,6 +298,30 @@ describe('getResourceStatus — colliding plurals', () => {
|
|
|
277
298
|
})).not.toBeNull()
|
|
278
299
|
})
|
|
279
300
|
|
|
301
|
+
it('reads an Argo Rollouts Experiment with the AnalysisPhase vocabulary', () => {
|
|
302
|
+
expect(getResourceStatus('experiments', {
|
|
303
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
304
|
+
status: { phase: 'Successful' },
|
|
305
|
+
})).toMatchObject({ text: 'Successful', level: 'healthy' })
|
|
306
|
+
|
|
307
|
+
expect(getResourceStatus('experiments', {
|
|
308
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
309
|
+
status: { phase: 'Running' },
|
|
310
|
+
})).toMatchObject({ level: 'degraded' })
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Katib ships its own Experiment at kubeflow.org sharing this plural, and it
|
|
314
|
+
// does not report AnalysisPhase. Falling through to the generic reader is
|
|
315
|
+
// fine; being scored with Argo's vocabulary is not, because that attaches a
|
|
316
|
+
// HealthLevel derived from a phase Katib never reports.
|
|
317
|
+
it('does not score a Katib Experiment with the Argo vocabulary', () => {
|
|
318
|
+
const katib = getResourceStatus('experiments', {
|
|
319
|
+
apiVersion: 'kubeflow.org/v1beta1',
|
|
320
|
+
status: { conditions: [{ type: 'Running', status: 'True' }] },
|
|
321
|
+
})
|
|
322
|
+
expect(katib?.level).toBeUndefined()
|
|
323
|
+
})
|
|
324
|
+
|
|
280
325
|
it('fabricates no engine status for a third-party backups CRD', () => {
|
|
281
326
|
expect(getResourceStatus('backups', { apiVersion: 'kubevirt.io/v1', status: {} })).toBeNull()
|
|
282
327
|
expect(getResourceStatus('backups', { apiVersion: 'kubevirt.io/v1', status: { phase: 'Running' } }))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasReflectorDetails } from '../resources/renderers/ReflectorSection'
|
|
1
2
|
import { clsx } from 'clsx'
|
|
2
3
|
import { SEVERITY_BADGE, HEALTH_BADGE_COLORS } from '../../utils/badge-colors'
|
|
3
4
|
import { isArgoRolloutResource } from '../../utils/workload-rollout'
|
|
@@ -120,6 +121,8 @@ import {
|
|
|
120
121
|
PVCRenderer,
|
|
121
122
|
RolloutRenderer,
|
|
122
123
|
AnalysisRunRenderer,
|
|
124
|
+
AnalysisTemplateRenderer,
|
|
125
|
+
ExperimentRenderer,
|
|
123
126
|
CertificateRenderer,
|
|
124
127
|
WorkflowRenderer,
|
|
125
128
|
PersistentVolumeRenderer,
|
|
@@ -231,6 +234,7 @@ import {
|
|
|
231
234
|
PriorityClassRenderer,
|
|
232
235
|
RuntimeClassRenderer,
|
|
233
236
|
LeaseRenderer,
|
|
237
|
+
LimitRangeRenderer,
|
|
234
238
|
TraefikIngressRouteRenderer,
|
|
235
239
|
TraefikMiddlewareRenderer,
|
|
236
240
|
TraefikServiceRenderer,
|
|
@@ -426,7 +430,7 @@ const KNOWN_KINDS = new Set([
|
|
|
426
430
|
'pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets',
|
|
427
431
|
'services', 'endpointslices', 'ingresses', 'configmaps', 'secrets', 'jobs', 'cronjobs', 'cronworkflows',
|
|
428
432
|
'hpas', 'horizontalpodautoscalers', 'nodes', 'persistentvolumeclaims',
|
|
429
|
-
'rollouts', 'analysisruns', 'certificates', 'workflows', 'persistentvolumes',
|
|
433
|
+
'rollouts', 'analysisruns', 'analysistemplates', 'clusteranalysistemplates', 'experiments', 'certificates', 'workflows', 'persistentvolumes',
|
|
430
434
|
'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
|
|
431
435
|
'orders', 'challenges',
|
|
432
436
|
'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates', 'clusterworkflowtemplates',
|
|
@@ -473,7 +477,7 @@ const KNOWN_KINDS = new Set([
|
|
|
473
477
|
'virtualservices', 'destinationrules', 'serviceentries',
|
|
474
478
|
'peerauthentications', 'authorizationpolicies',
|
|
475
479
|
'mutatingwebhookconfigurations', 'validatingwebhookconfigurations',
|
|
476
|
-
'ingressclasses', 'priorityclasses', 'runtimeclasses', 'leases',
|
|
480
|
+
'ingressclasses', 'priorityclasses', 'runtimeclasses', 'leases', 'limitranges',
|
|
477
481
|
'knativeservices', 'knativeconfigurations', 'knativerevisions', 'knativeroutes',
|
|
478
482
|
'brokers', 'triggers', 'eventtypes', 'pingsources', 'apiserversources', 'containersources', 'sinkbindings',
|
|
479
483
|
'channels', 'inmemorychannels', 'subscriptions', 'sequences', 'parallels',
|
|
@@ -678,7 +682,8 @@ export function ResourceRendererDispatch({
|
|
|
678
682
|
kind === 'clusters' || kind === 'backups' || kind === 'scheduledbackups' || kind === 'poolers'
|
|
679
683
|
|| kind === 'objectstores' || kind === 'databases' || kind === 'publications'
|
|
680
684
|
|| kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs'
|
|
681
|
-
|| kind === 'policies' || kind === 'rollouts' || kind === '
|
|
685
|
+
|| kind === 'policies' || kind === 'rollouts' || kind === 'experiments'
|
|
686
|
+
|| kind === 'machines' || kind === 'machinesets'
|
|
682
687
|
const isCNPGApiVersion = isApiGroup(data?.apiVersion, CNPG_GROUP)
|
|
683
688
|
const groupGatedMatched =
|
|
684
689
|
(kind === 'clusters' && (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'cluster.x-k8s.io')))
|
|
@@ -691,6 +696,11 @@ export function ResourceRendererDispatch({
|
|
|
691
696
|
&& (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'messaging.knative.dev')))
|
|
692
697
|
|| (kind === 'policies' && isApiGroup(data?.apiVersion, 'kyverno.io'))
|
|
693
698
|
|| (kind === 'rollouts' && isArgoRolloutResource(data))
|
|
699
|
+
// Katib (kubeflow.org) ships its own, unrelated Experiment CRD sharing
|
|
700
|
+
// this plural — without this gate it got the Argo Rollouts Experiment
|
|
701
|
+
// renderer's mostly-empty status view instead of its actual resource
|
|
702
|
+
// details, the same collision shape as every other check in this block.
|
|
703
|
+
|| (kind === 'experiments' && isApiGroup(data?.apiVersion, 'argoproj.io'))
|
|
694
704
|
|| ((kind === 'machines' || kind === 'machinesets')
|
|
695
705
|
&& isApiGroup(data?.apiVersion, 'cluster.x-k8s.io'))
|
|
696
706
|
const groupGatedFallthrough = isGroupGatedKind && !groupGatedMatched
|
|
@@ -741,11 +751,15 @@ export function ResourceRendererDispatch({
|
|
|
741
751
|
const HPAComp = rendererOverrides?.HPARenderer ?? HPARenderer
|
|
742
752
|
const PVCComp = rendererOverrides?.PVCRenderer ?? PVCRenderer
|
|
743
753
|
const RolloutComp = rendererOverrides?.RolloutRenderer ?? RolloutRenderer
|
|
754
|
+
const showsReflection = (kind === 'configmaps' || kind === 'secrets') && hasReflectorDetails(data, relationships?.reflection)
|
|
755
|
+
const reflectionRefs = showsReflection ? [relationships?.reflection?.source, ...(relationships?.reflection?.mirrors ?? [])].filter((ref): ref is ResourceRef => !!ref) : []
|
|
756
|
+
const withoutReflection = (refs: ResourceRef[] | undefined) => refs?.filter(ref => !reflectionRefs.some(mirror => mirror.kind === ref.kind && mirror.namespace === ref.namespace && mirror.name === ref.name && (mirror.group ?? '') === (ref.group ?? '')))
|
|
757
|
+
const sidebarRelationships = showsReflection && relationships ? { ...relationships, configRefs: withoutReflection(relationships.configRefs), consumers: withoutReflection(relationships.consumers) } : relationships
|
|
744
758
|
const scaleBlockedBy = replicaScalers(relationships?.scalers)
|
|
745
759
|
|
|
746
760
|
const sidebarContent = showCommonSections && (
|
|
747
761
|
<>
|
|
748
|
-
<RelatedResourcesSection relationships={
|
|
762
|
+
<RelatedResourcesSection relationships={sidebarRelationships} onNavigate={onNavigate} />
|
|
749
763
|
{kind !== 'events' && <EventsSection events={events || []} updates={updates || []} isLoading={eventsLoading ?? false} eventsError={eventsError ?? null} updatesError={updatesError ?? null} hint={eventsHint} />}
|
|
750
764
|
<LabelsSection data={data} />
|
|
751
765
|
<AnnotationsSection data={data} />
|
|
@@ -773,8 +787,8 @@ export function ResourceRendererDispatch({
|
|
|
773
787
|
{kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && <ServiceComp data={data} onCopy={onCopy} copied={copied} onNavigate={onNavigate} />}
|
|
774
788
|
{kind === 'endpointslices' && <EndpointSliceRenderer data={data} onNavigate={onNavigate} />}
|
|
775
789
|
{kind === 'ingresses' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <IngressRenderer data={data} onNavigate={onNavigate} />}
|
|
776
|
-
{kind === 'configmaps' && <ConfigMapRenderer data={data} />}
|
|
777
|
-
{kind === 'secrets' && <SecretRenderer data={data} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
|
|
790
|
+
{kind === 'configmaps' && <ConfigMapRenderer data={data} relationships={relationships} onNavigate={onNavigate} />}
|
|
791
|
+
{kind === 'secrets' && <SecretRenderer data={data} relationships={relationships} onNavigate={onNavigate} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
|
|
778
792
|
{kind === 'jobs' && !nonCoreJobFallthrough && <JobRenderer data={data} />}
|
|
779
793
|
{kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
|
|
780
794
|
{kind === 'cronworkflows' && <CronWorkflowRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -783,6 +797,8 @@ export function ResourceRendererDispatch({
|
|
|
783
797
|
{kind === 'persistentvolumeclaims' && <PVCComp data={data} onNavigate={onNavigate} />}
|
|
784
798
|
{kind === 'rollouts' && isArgoRolloutResource(data) && <RolloutComp data={data} onNavigate={onNavigate} />}
|
|
785
799
|
{kind === 'analysisruns' && <AnalysisRunRenderer data={data} onNavigate={onNavigate} />}
|
|
800
|
+
{(kind === 'analysistemplates' || kind === 'clusteranalysistemplates') && <AnalysisTemplateRenderer data={data} />}
|
|
801
|
+
{kind === 'experiments' && isApiGroup(data?.apiVersion, 'argoproj.io') && <ExperimentRenderer data={data} onNavigate={onNavigate} />}
|
|
786
802
|
{kind === 'certificates' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <CertificateRenderer data={data} />}
|
|
787
803
|
{kind === 'workflows' && <WorkflowRenderer data={data} onNavigate={onNavigate} />}
|
|
788
804
|
{kind === 'persistentvolumes' && <PersistentVolumeRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -920,6 +936,7 @@ export function ResourceRendererDispatch({
|
|
|
920
936
|
{kind === 'priorityclasses' && <PriorityClassRenderer data={data} />}
|
|
921
937
|
{kind === 'runtimeclasses' && <RuntimeClassRenderer data={data} />}
|
|
922
938
|
{kind === 'leases' && <LeaseRenderer data={data} />}
|
|
939
|
+
{kind === 'limitranges' && <LimitRangeRenderer data={data} />}
|
|
923
940
|
{/* Knative Serving */}
|
|
924
941
|
{(kind === 'services' && data?.apiVersion?.includes('serving.knative.dev')) && <KnativeServiceRenderer data={data} onNavigate={onNavigate} />}
|
|
925
942
|
{kind === 'knativeservices' && <KnativeServiceRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -1103,6 +1120,10 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
1103
1120
|
if (k === 'nodes') return getNodeStatus(data)
|
|
1104
1121
|
if (k === 'persistentvolumeclaims') return getPVCStatus(data)
|
|
1105
1122
|
if (k === 'analysisruns') return getAnalysisRunStatus(data)
|
|
1123
|
+
// Same collision guard as the render branch above — Katib's unrelated
|
|
1124
|
+
// Experiment CRD (kubeflow.org) shares this plural and doesn't report the
|
|
1125
|
+
// AnalysisPhase vocabulary getAnalysisRunStatus expects.
|
|
1126
|
+
if (k === 'experiments' && isApiGroup(data?.apiVersion, 'argoproj.io')) return getAnalysisRunStatus(data)
|
|
1106
1127
|
if (k === 'workflows') return getWorkflowStatus(data)
|
|
1107
1128
|
if (k === 'cronworkflows') return getCronWorkflowStatus(data)
|
|
1108
1129
|
if (k === 'certificates') {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from './ResourceRendererDispatch'
|
|
2
2
|
export { EditableYamlView, SaveSuccessAnimation } from './EditableYamlView'
|
|
3
3
|
export { ResourceActionsBar, RevisionHistoryDialog, isRolloutKind } from './ResourceActionsBar'
|
|
4
|
+
export {
|
|
5
|
+
DrainPlanDialog, DrainPlanContent, canConfirmDrain, planMatches, emptyDirPodsAtRisk, DEFAULT_DRAIN_DIALOG_OPTIONS,
|
|
6
|
+
type DrainPlan, type DrainPlanPod, type DrainOutcome, type DrainDialogOptions,
|
|
7
|
+
} from './DrainPlanDialog'
|
|
4
8
|
export { SetImageDialog, type ManagedImageSource, type SetImageDialogProps } from './SetImageDialog'
|
|
5
9
|
export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult } from './CreateResourceDialog'
|
|
6
10
|
export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
|