Add Show Widget for library actions with Overview charts from last run output.
This commit is contained in:
+119
-19
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user