diff --git a/integration-tests/tests/inlineActions/config.yaml b/integration-tests/tests/inlineActions/config.yaml new file mode 100644 index 0000000..f0784a2 --- /dev/null +++ b/integration-tests/tests/inlineActions/config.yaml @@ -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 + + diff --git a/integration-tests/tests/inlineActions/inlineActions.mjs b/integration-tests/tests/inlineActions/inlineActions.mjs new file mode 100644 index 0000000..3b5e9ce --- /dev/null +++ b/integration-tests/tests/inlineActions/inlineActions.mjs @@ -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' + ) + }) +}) \ No newline at end of file diff --git a/service/internal/config/config.go b/service/internal/config/config.go index de0efca..c7de2db 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -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 { diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 9040ade..d8d3d89 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -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() { diff --git a/service/internal/config/sanitize_test.go b/service/internal/config/sanitize_test.go index f6e09af..85298b9 100644 --- a/service/internal/config/sanitize_test.go +++ b/service/internal/config/sanitize_test.go @@ -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") + } +}