feat: Dashboard configs can now self-contain their entities, and actions all in one neat file
This commit is contained in:
parent
e58677e12c
commit
32fea4ec30
|
|
@ -3,14 +3,10 @@ import fs from 'fs'
|
|||
import { expect } from 'chai'
|
||||
import { Condition } from 'selenium-webdriver'
|
||||
|
||||
export async function getActionButtons (dashboardTitle = null) {
|
||||
// New Vue UI renders action buttons using ActionButton.vue structure
|
||||
// Each button lives under a container with class .action-button
|
||||
if (dashboardTitle == null) {
|
||||
export async function getActionButtons () {
|
||||
// Currently, only the active dashboard's contents are rendered,
|
||||
// so we don't need to scope the selector by dashboard title.
|
||||
return await webdriver.findElements(By.css('.action-button button'))
|
||||
} else {
|
||||
return await webdriver.findElements(By.css('section[title="' + dashboardTitle + '"] .action-button button'))
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExecutionDialogOutput() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
include: dashboards.d
|
||||
|
||||
actions:
|
||||
- title: Base Action
|
||||
shell: echo "base"
|
||||
icon: ping
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
dashboards:
|
||||
- title: First Dashboard
|
||||
contents:
|
||||
- type: action
|
||||
inlineAction:
|
||||
title: First Action
|
||||
shell: echo "First Dashboard, First Action!"
|
||||
|
||||
- type: action
|
||||
inlineAction:
|
||||
title: Second Action
|
||||
shell: echo "First Dashboard, Second Action!"
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
entities:
|
||||
- file: entities/person.yaml
|
||||
name: person
|
||||
|
||||
dashboards:
|
||||
- title: People
|
||||
contents:
|
||||
- type: action
|
||||
inlineAction:
|
||||
title: Third Action
|
||||
shell: echo "First Dashboard, Third Action!"
|
||||
|
||||
- type: action
|
||||
inlineAction:
|
||||
title: Forth Action
|
||||
shell: echo "First Dashboard, Forth Action!"
|
||||
|
||||
- title: "Person: {{ person.name }}"
|
||||
type: fieldset
|
||||
entity: person
|
||||
contents:
|
||||
- type: display
|
||||
title: " {{ person.name }} is a person"
|
||||
cssStyle:
|
||||
background-color: red;
|
||||
|
||||
- title: "Greet {{ person.name }}"
|
||||
inlineAction:
|
||||
shell: echo "Hello, {{ person.name }}!"
|
||||
icon: ping
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
- name: Alice
|
||||
- name: Bob
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect, assert } from 'chai'
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
getNavigationLinks,
|
||||
openSidebar,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: multi-dashboard-includes', function () {
|
||||
this.timeout(30000)
|
||||
|
||||
before(async function () {
|
||||
await runner.start('multi-dashboard-includes')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
async function clickNavigationLinkByTitle (title) {
|
||||
await openSidebar()
|
||||
|
||||
const navigationLinks = await getNavigationLinks()
|
||||
assert.isAbove(navigationLinks.length, 0, 'Expected at least one navigation link')
|
||||
|
||||
const matching = []
|
||||
for (const li of navigationLinks) {
|
||||
const liTitle = await li.getAttribute('title')
|
||||
if (liTitle === title) {
|
||||
matching.push(li)
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(matching.length, 1, `Expected exactly one navigation link with title "${title}"`)
|
||||
|
||||
await matching[0].click()
|
||||
}
|
||||
|
||||
async function getActionTitlesOnDashboard (dashboardTitle = null) {
|
||||
const buttons = await getActionButtons(dashboardTitle)
|
||||
const titles = []
|
||||
|
||||
for (const button of buttons) {
|
||||
titles.push(await button.getAttribute('title'))
|
||||
}
|
||||
|
||||
return titles
|
||||
}
|
||||
|
||||
it('Should expose both dashboards from included files in navigation', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
await openSidebar()
|
||||
const navigationLinks = await getNavigationLinks()
|
||||
assert.isAbove(navigationLinks.length, 0, 'Expected navigation to have at least one link')
|
||||
|
||||
const titles = []
|
||||
for (const li of navigationLinks) {
|
||||
titles.push(await li.getAttribute('title'))
|
||||
}
|
||||
|
||||
expect(titles).to.include('First Dashboard')
|
||||
expect(titles).to.include('Second Dashboard')
|
||||
})
|
||||
|
||||
it('First Dashboard shows First and Second inline actions from include', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
// Navigate to "First Dashboard"
|
||||
await clickNavigationLinkByTitle('First Dashboard')
|
||||
|
||||
// Buttons on this dashboard only
|
||||
const titles = await getActionTitlesOnDashboard('First Dashboard')
|
||||
|
||||
expect(titles).to.include('First Action')
|
||||
expect(titles).to.include('Second Action')
|
||||
|
||||
// Ensure actions from the second dashboard are not rendered here
|
||||
expect(titles).to.not.include('Third Action')
|
||||
expect(titles).to.not.include('Forth Action')
|
||||
})
|
||||
|
||||
it('Second Dashboard shows Third and Forth inline actions from include', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
// Navigate to "Second Dashboard"
|
||||
await clickNavigationLinkByTitle('Second Dashboard')
|
||||
|
||||
const titles = await getActionTitlesOnDashboard('Second Dashboard')
|
||||
|
||||
expect(titles).to.include('Third Action')
|
||||
expect(titles).to.include('Forth Action')
|
||||
|
||||
// Ensure actions from the first dashboard are not rendered here
|
||||
expect(titles).to.not.include('First Action')
|
||||
expect(titles).to.not.include('Second Action')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -84,7 +84,16 @@ func isLinkType(itemType string) bool {
|
|||
func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||
clone.Type = "link"
|
||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
||||
clone.Action = rr.findActionForEntity(subitem.Title, ent)
|
||||
// Prefer an entity-specific action when available, but fall back to a
|
||||
// non-entity-scoped action with the same title. This allows inline actions
|
||||
// defined inside entity dashboards to work without requiring an explicit
|
||||
// entity binding.
|
||||
action := rr.findActionForEntity(subitem.Title, ent)
|
||||
if action == nil {
|
||||
action = rr.findAction(subitem.Title)
|
||||
}
|
||||
|
||||
clone.Action = action
|
||||
return clone
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -192,10 +192,59 @@ func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{})
|
|||
}
|
||||
}
|
||||
|
||||
// mergeDashboardsWhenBothExist merges dashboards when both src and dest have dashboards.
|
||||
func mergeDashboardsWhenBothExist(srcDashboards interface{}, destDashboards interface{}, dest map[string]interface{}) {
|
||||
srcSlice, ok1 := srcDashboards.([]interface{})
|
||||
destSlice, ok2 := destDashboards.([]interface{})
|
||||
if ok1 && ok2 {
|
||||
dest["dashboards"] = append(destSlice, srcSlice...)
|
||||
} else {
|
||||
dest["dashboards"] = srcDashboards
|
||||
}
|
||||
}
|
||||
|
||||
// mergeDashboardsFromSource merges dashboards from source into destination.
|
||||
func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interface{}) {
|
||||
if destDashboards, ok := dest["dashboards"]; ok {
|
||||
mergeDashboardsWhenBothExist(srcDashboards, destDashboards, dest)
|
||||
} else {
|
||||
dest["dashboards"] = srcDashboards
|
||||
}
|
||||
}
|
||||
|
||||
// mergeEntitiesWhenBothExist merges entities when both src and dest have entities.
|
||||
func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}) {
|
||||
srcSlice, ok1 := srcEntities.([]interface{})
|
||||
destSlice, ok2 := destEntities.([]interface{})
|
||||
if ok1 && ok2 {
|
||||
dest["entities"] = append(destSlice, srcSlice...)
|
||||
} else {
|
||||
dest["entities"] = srcEntities
|
||||
}
|
||||
}
|
||||
|
||||
// mergeEntitiesFromSource merges entities from source into destination.
|
||||
func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}) {
|
||||
if destEntities, ok := dest["entities"]; ok {
|
||||
mergeEntitiesWhenBothExist(srcEntities, destEntities, dest)
|
||||
} else {
|
||||
dest["entities"] = srcEntities
|
||||
}
|
||||
}
|
||||
|
||||
func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
|
||||
if srcActions, ok := src["actions"]; ok {
|
||||
mergeActionsFromSource(srcActions, dest)
|
||||
}
|
||||
|
||||
if srcDashboards, ok := src["dashboards"]; ok {
|
||||
mergeDashboardsFromSource(srcDashboards, dest)
|
||||
}
|
||||
|
||||
if srcEntities, ok := src["entities"]; ok {
|
||||
mergeEntitiesFromSource(srcEntities, dest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,17 +42,33 @@ func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
|
|||
return
|
||||
}
|
||||
|
||||
if component.InlineAction.Title == "" {
|
||||
component.InlineAction.Title = component.Title
|
||||
sanitizeInlineActionTitles(component)
|
||||
|
||||
if component.Entity != "" && component.InlineAction.Entity == "" {
|
||||
component.InlineAction.Entity = component.Entity
|
||||
}
|
||||
|
||||
component.InlineAction.sanitize(cfg)
|
||||
|
||||
if cfg.inlineActionExists(component.InlineAction) {
|
||||
cfg.addInlineActionIfNotExists(component.InlineAction)
|
||||
}
|
||||
|
||||
func (cfg *Config) addInlineActionIfNotExists(action *Action) {
|
||||
if cfg.inlineActionExists(action) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Actions = append(cfg.Actions, component.InlineAction)
|
||||
cfg.Actions = append(cfg.Actions, action)
|
||||
}
|
||||
|
||||
func sanitizeInlineActionTitles(component *DashboardComponent) {
|
||||
if component.InlineAction.Title == "" {
|
||||
component.InlineAction.Title = component.Title
|
||||
}
|
||||
|
||||
if component.Title == "" {
|
||||
component.Title = component.InlineAction.Title
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) inlineActionExists(action *Action) bool {
|
||||
|
|
@ -93,6 +109,10 @@ func (cfg *Config) inlineActionIDExists(action *Action) bool {
|
|||
|
||||
func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent) {
|
||||
for _, child := range component.Contents {
|
||||
if child.Entity == "" {
|
||||
child.Entity = component.Entity
|
||||
}
|
||||
|
||||
cfg.sanitizeDashboardComponentForInlineActions(child)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func migrateLegacyEntityProperties(rawShellCommand string) string {
|
|||
log.WithFields(log.Fields{
|
||||
"old": entityName,
|
||||
"new": ".CurrentEntity",
|
||||
}).Warnf("Legacy entity variable name found, changing to CurrentEntity")
|
||||
}).Debugf("Legacy entity variable name found, changing to CurrentEntity")
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ func migrateLegacyEntityProperties(rawShellCommand string) string {
|
|||
log.WithFields(log.Fields{
|
||||
"old": argName,
|
||||
"new": ".CurrentEntity." + argName,
|
||||
}).Warnf("Legacy variable name found, changing to CurrentEntity")
|
||||
}).Debugf("Legacy variable name found, changing to CurrentEntity")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ func migrateLegacyArgumentNames(rawShellCommand string) string {
|
|||
log.WithFields(log.Fields{
|
||||
"old": argName,
|
||||
"new": ".Arguments." + argName,
|
||||
}).Warnf("Legacy variable name found, changing to Argument")
|
||||
}).Debugf("Legacy variable name found, changing to Argument")
|
||||
|
||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, argName, ".Arguments."+argName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,13 @@ func findDashboardActionTitles(req *RebuildActionMapRequest) {
|
|||
//gocyclo:ignore
|
||||
func recurseDashboardForActionTitles(component *config.DashboardComponent, req *RebuildActionMapRequest) {
|
||||
for _, sub := range component.Contents {
|
||||
if sub.Type == "link" || sub.Type == "" {
|
||||
if sub.InlineAction != nil {
|
||||
title := sub.Title
|
||||
if title == "" {
|
||||
title = sub.InlineAction.Title
|
||||
}
|
||||
req.DashboardActionTitles = append(req.DashboardActionTitles, title)
|
||||
} else if sub.Type == "link" || sub.Type == "" {
|
||||
req.DashboardActionTitles = append(req.DashboardActionTitles, sub.Title)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue