Jelajahi Sumber

feat: inline actions on dashboards

jamesread 7 bulan lalu
induk
melakukan
e58677e12c

+ 21 - 0
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
+
+

+ 38 - 0
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'
+    )
+  })
+})

+ 7 - 6
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 {

+ 76 - 0
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() {

+ 36 - 0
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")
+	}
+}