Add Show Widget for library actions with Overview charts from last run output.

This commit is contained in:
2026-07-20 20:48:06 +02:00
parent e047499fed
commit d188ca41b8
20 changed files with 1918 additions and 128 deletions
+119 -19
View File
@@ -34,6 +34,7 @@ import {
type Action,
type ActionEnvVar,
type ActionKind,
type ActionWidgetType,
type AuthAuditEvent,
type Group,
type GroupScopeKind,
@@ -1121,13 +1122,18 @@ function ActionsConfigTab() {
const [draftBody, setDraftBody] = useState('')
const [draftEnv, setDraftEnv] = useState<ActionEnvVar[]>([emptyEnvRow()])
const [draftRequiresSudo, setDraftRequiresSudo] = useState(false)
const [draftShowWidget, setDraftShowWidget] = useState(false)
const [draftWidgetType, setDraftWidgetType] =
useState<ActionWidgetType>('raw')
const [isSaving, setIsSaving] = useState(false)
const [editingIsBuiltin, setEditingIsBuiltin] = useState(false)
const isEditing = editingActionId !== null
const showEditor = editorKind !== null
const canCreateActions = Boolean(me?.permissions?.includes('actions.create'))
const canUpdateActions = Boolean(me?.permissions?.includes('actions.update'))
const canDeleteActions = Boolean(me?.permissions?.includes('actions.delete'))
const fieldsLocked = editingIsBuiltin
useEffect(() => {
let isCancelled = false
@@ -1173,6 +1179,9 @@ function ActionsConfigTab() {
setDraftBody('')
setDraftEnv([emptyEnvRow()])
setDraftRequiresSudo(false)
setDraftShowWidget(false)
setDraftWidgetType('raw')
setEditingIsBuiltin(false)
setSubmitError(null)
}
@@ -1185,21 +1194,28 @@ function ActionsConfigTab() {
setShowTypePicker(false)
setEditorKind(kind)
setEditingActionId(null)
setEditingIsBuiltin(false)
setDraftName('')
setDraftDescription('')
setDraftBody('')
setDraftEnv([emptyEnvRow()])
setDraftRequiresSudo(false)
setDraftShowWidget(false)
setDraftWidgetType('raw')
setSubmitError(null)
}
function startEdit(action: Action) {
if (action.builtin || !canUpdateActions) {
if (!canUpdateActions) {
return
}
if (action.builtin && !canUpdateActions) {
return
}
setShowTypePicker(false)
setEditorKind(action.kind)
setEditingActionId(action.id)
setEditingIsBuiltin(Boolean(action.builtin))
setDraftName(action.name)
setDraftDescription(action.description)
setDraftBody(action.body)
@@ -1209,6 +1225,14 @@ function ActionsConfigTab() {
: [emptyEnvRow()],
)
setDraftRequiresSudo(Boolean(action.requires_sudo))
setDraftShowWidget(Boolean(action.show_widget))
setDraftWidgetType(
action.widget_type === 'memory' ||
action.widget_type === 'disk' ||
action.widget_type === 'raw'
? action.widget_type
: 'raw',
)
setSubmitError(null)
}
@@ -1250,14 +1274,25 @@ function ActionsConfigTab() {
setIsSaving(true)
try {
const env = normalizedEnv()
const widgetType = draftShowWidget ? draftWidgetType : ('' as const)
const response = isEditing
? await patchAction(editingActionId!, {
name: draftName,
description: draftDescription,
body: draftBody,
env,
requires_sudo: draftRequiresSudo,
})
? await patchAction(
editingActionId!,
editingIsBuiltin
? {
show_widget: draftShowWidget,
widget_type: widgetType,
}
: {
name: draftName,
description: draftDescription,
body: draftBody,
env,
requires_sudo: draftRequiresSudo,
show_widget: draftShowWidget,
widget_type: widgetType,
},
)
: await createAction({
name: draftName,
description: draftDescription,
@@ -1265,6 +1300,8 @@ function ActionsConfigTab() {
body: draftBody,
env,
requires_sudo: draftRequiresSudo,
show_widget: draftShowWidget,
widget_type: widgetType,
})
setActions([...response.actions])
clearEditor()
@@ -1355,6 +1392,14 @@ function ActionsConfigTab() {
sudo
</span>
) : null}
{action.show_widget ? (
<span className="user-badge">
widget
{action.widget_type
? `: ${action.widget_type}`
: ''}
</span>
) : null}
</div>
<div className="groups-li-meta">
{action.description || 'No description'}
@@ -1371,13 +1416,13 @@ function ActionsConfigTab() {
<button
type="button"
className="groups-edit"
disabled={action.builtin || !canUpdateActions}
disabled={!canUpdateActions}
title={
action.builtin
? 'Built-in actions cannot be edited'
: canUpdateActions
? undefined
: 'Requires actions.update'
canUpdateActions
? action.builtin
? 'Edit widget settings'
: undefined
: 'Requires actions.update'
}
onClick={() => startEdit(action)}
>
@@ -1482,7 +1527,9 @@ function ActionsConfigTab() {
>
<h2 id="action-editor-title">
{isEditing
? `Edit ${editorKind === 'shell' ? 'shell command' : 'script'}`
? editingIsBuiltin
? `Widget settings · ${draftName}`
: `Edit ${editorKind === 'shell' ? 'shell command' : 'script'}`
: `New ${editorKind === 'shell' ? 'shell command' : 'script'}`}
</h2>
@@ -1492,12 +1539,20 @@ function ActionsConfigTab() {
</p>
) : null}
{editingIsBuiltin ? (
<p className="config-hint">
Built-in actions keep a fixed command body. You can still enable
or change the Overview widget.
</p>
) : null}
<div className="config-field">
<label htmlFor="action-name">Name</label>
<input
id="action-name"
className="config-input"
value={draftName}
disabled={fieldsLocked}
onChange={(event) => setDraftName(event.target.value)}
/>
</div>
@@ -1508,6 +1563,7 @@ function ActionsConfigTab() {
id="action-description"
className="config-input"
value={draftDescription}
disabled={fieldsLocked}
onChange={(event) => setDraftDescription(event.target.value)}
/>
</div>
@@ -1521,6 +1577,7 @@ function ActionsConfigTab() {
id="action-body"
className="config-input"
value={draftBody}
disabled={fieldsLocked}
placeholder="echo {{node.ip}}"
onChange={(event) => setDraftBody(event.target.value)}
/>
@@ -1529,21 +1586,25 @@ function ActionsConfigTab() {
id="action-body"
className="config-input actions-script-input"
value={draftBody}
disabled={fieldsLocked}
rows={8}
placeholder={"#!/bin/sh\necho {{node.host}}"}
onChange={(event) => setDraftBody(event.target.value)}
/>
)}
<p className="config-hint">
Do not include a leading <code>sudo</code>; check Requires Sudo
instead so ClusterCanvas runs with <code>sudo -n</code>.
</p>
{!fieldsLocked ? (
<p className="config-hint">
Do not include a leading <code>sudo</code>; check Requires Sudo
instead so ClusterCanvas runs with <code>sudo -n</code>.
</p>
) : null}
</div>
<label className="checkbox-row">
<input
type="checkbox"
checked={draftRequiresSudo}
disabled={fieldsLocked}
onChange={(event) =>
setDraftRequiresSudo(event.target.checked)
}
@@ -1551,6 +1612,41 @@ function ActionsConfigTab() {
Requires Sudo
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={draftShowWidget}
onChange={(event) => {
const checked = event.target.checked
setDraftShowWidget(checked)
if (checked && !draftWidgetType) {
setDraftWidgetType('raw')
}
}}
/>
Show Widget
</label>
{draftShowWidget ? (
<div className="config-field">
<label htmlFor="action-widget-type">Widget type</label>
<select
id="action-widget-type"
className="config-input"
value={draftWidgetType}
onChange={(event) =>
setDraftWidgetType(event.target.value as ActionWidgetType)
}
>
<option value="memory">Memory (free -m ring)</option>
<option value="disk">Disk (df -h ring)</option>
<option value="raw">Raw text</option>
</select>
<p className="config-hint">
Parsed from the last run of this action on each node Overview.
</p>
</div>
) : null}
<div className="config-field">
<label>Environment variables</label>
<p className="config-hint">
@@ -1565,6 +1661,7 @@ function ActionsConfigTab() {
aria-label={`ENV name ${index + 1}`}
placeholder="NAME"
value={row.name}
disabled={fieldsLocked}
onChange={(event) =>
updateEnvRow(index, 'name', event.target.value)
}
@@ -1574,6 +1671,7 @@ function ActionsConfigTab() {
aria-label={`ENV value ${index + 1}`}
placeholder="value or {{node.ip}}"
value={row.value}
disabled={fieldsLocked}
onChange={(event) =>
updateEnvRow(index, 'value', event.target.value)
}
@@ -1582,6 +1680,7 @@ function ActionsConfigTab() {
type="button"
className="groups-delete"
aria-label={`Remove ENV row ${index + 1}`}
disabled={fieldsLocked}
onClick={() => removeEnvRow(index)}
>
Remove
@@ -1592,6 +1691,7 @@ function ActionsConfigTab() {
<button
type="button"
className="groups-new"
disabled={fieldsLocked}
onClick={() => addEnvRow()}
>
Add ENV variable