feat: inline actions on dashboards

This commit is contained in:
jamesread 2025-12-01 12:29:41 +00:00
parent 2e82a3d1ed
commit e58677e12c
5 changed files with 178 additions and 6 deletions

View File

@ -0,0 +1,21 @@
#
# Integration Test Config: inline dashboard actions
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
# No top-level actions – actions will be defined inline on dashboard components.
actions: []
dashboards:
- title: Inline Dashboard
contents:
- title: Inline Dashboard Action
inlineAction:
shell: date
icon: clock

View File

@ -0,0 +1,38 @@
import { describe, it, before, after } from 'mocha'
import { assert } from 'chai'
import {
getRootAndWait,
getActionButtons,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: inlineActions', function () {
before(async function () {
await runner.start('inlineActions')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Inline dashboard actions are rendered as clickable buttons', async function () {
await getRootAndWait()
const buttons = await getActionButtons()
assert.isArray(buttons, 'Action buttons should be an array')
assert.isAtLeast(buttons.length, 1, 'There should be at least one action button')
const texts = await Promise.all(buttons.map(b => b.getText()))
const combinedText = texts.join(' ')
assert.include(
combinedText,
'Inline Dashboard Action',
'Inline dashboard action should be rendered as a button'
)
})
})

View File

@ -207,12 +207,13 @@ type LogDebugOptions struct {
}
type DashboardComponent struct {
Title string `koanf:"title"`
Type string `koanf:"type"`
Entity string `koanf:"entity"`
Icon string `koanf:"icon"`
CssClass string `koanf:"cssClass"`
Contents []*DashboardComponent `koanf:"contents"`
Title string `koanf:"title"`
Type string `koanf:"type"`
Entity string `koanf:"entity"`
Icon string `koanf:"icon"`
CssClass string `koanf:"cssClass"`
InlineAction *Action `koanf:"inlineAction"`
Contents []*DashboardComponent `koanf:"contents"`
}
func DefaultConfig() *Config {

View File

@ -19,6 +19,82 @@ func (cfg *Config) Sanitize() {
for idx := range cfg.Actions {
cfg.Actions[idx].sanitize(cfg)
}
cfg.sanitizeDashboardsForInlineActions()
}
func (cfg *Config) sanitizeDashboardsForInlineActions() {
for _, dashboard := range cfg.Dashboards {
cfg.sanitizeDashboardComponentForInlineActions(dashboard)
}
}
func (cfg *Config) sanitizeDashboardComponentForInlineActions(component *DashboardComponent) {
if component == nil {
return
}
cfg.sanitizeInlineAction(component)
cfg.sanitizeChildDashboardComponents(component)
}
func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
if component.InlineAction == nil {
return
}
if component.InlineAction.Title == "" {
component.InlineAction.Title = component.Title
}
component.InlineAction.sanitize(cfg)
if cfg.inlineActionExists(component.InlineAction) {
return
}
cfg.Actions = append(cfg.Actions, component.InlineAction)
}
func (cfg *Config) inlineActionExists(action *Action) bool {
if cfg.inlineActionPointerExists(action) {
return true
}
if cfg.inlineActionIDExists(action) {
return true
}
return false
}
func (cfg *Config) inlineActionPointerExists(action *Action) bool {
for _, existingAction := range cfg.Actions {
if existingAction == action {
return true
}
}
return false
}
func (cfg *Config) inlineActionIDExists(action *Action) bool {
if action.ID == "" {
return false
}
for _, existingAction := range cfg.Actions {
if existingAction.ID == action.ID {
return true
}
}
return false
}
func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent) {
for _, child := range component.Contents {
cfg.sanitizeDashboardComponentForInlineActions(child)
}
}
func (cfg *Config) sanitizeLogLevel() {

View File

@ -36,3 +36,39 @@ func TestSanitizeConfig(t *testing.T) {
assert.Equal(t, "Carrots", a2.Arguments[0].Title, "Arg title is set to name")
assert.Equal(t, "Waffle", a2.Arguments[0].Choices[0].Title, "Choice title is set to name")
}
func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
c := DefaultConfig()
inline := &Action{
Shell: "date",
}
dashboardActionTitle := "Inline Dashboard Action"
c.Dashboards = []*DashboardComponent{
{
Title: "My Dashboard",
Contents: []*DashboardComponent{
{
Title: dashboardActionTitle,
InlineAction: inline,
},
},
},
}
c.Sanitize()
// Inline action should have been appended to the global Actions slice.
assert.GreaterOrEqual(t, len(c.Actions), 1, "At least one action should exist after sanitization")
// It should be discoverable by the dashboard component title when no explicit title was set.
found := c.findAction(dashboardActionTitle)
if assert.NotNil(t, found, "Inline dashboard action should be discoverable by title") {
assert.Equal(t, dashboardActionTitle, found.Title, "Inline action title should default from dashboard component title")
assert.Equal(t, 3, found.Timeout, "Inline action should have default timeout applied")
assert.NotEmpty(t, found.Icon, "Inline action should have default icon applied")
assert.NotEmpty(t, found.ID, "Inline action should have a generated ID")
}
}