Merge branch 'next' into dependabot/go_modules/service/next/github.com/bufbuild/buf-1.61.0

This commit is contained in:
James Read 2025-12-01 22:18:25 +00:00 committed by GitHub
commit 75a696d8c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 656 additions and 145 deletions

View File

@ -151,6 +151,11 @@ export declare type Entity = Message<"olivetin.api.v1.Entity"> & {
* @generated from field: repeated string directories = 4;
*/
directories: string[];
/**
* @generated from field: map<string, string> fields = 5;
*/
fields: { [key: string]: string };
};
/**

File diff suppressed because one or more lines are too long

View File

@ -21,10 +21,14 @@
<h3>Used on Dashboards:</h3>
<ul>
<li v-for="dash in def.usedOnDashboards">
<router-link :to="{ name: 'Dashboard', params: { title: dash } }">
{{ dash }}
<li v-for="dash in filteredDashboards(def.usedOnDashboards)" :key="dash">
<template v-if="isEntityDirectory(dash)">
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
</template>
<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }">
{{ getDashboardTitle(dash) }}
</router-link>
<span v-else>{{ dash }}</span>
</li>
</ul>
</div>
@ -44,6 +48,21 @@
entityDefinitions.value = ret.entityDefinitions
}
function filteredDashboards(dashboards) {
return dashboards.filter(d => d && !d.includes('{{'))
}
function isEntityDirectory(dashboardTitle) {
return dashboardTitle.endsWith(' [Entity Directory]')
}
function getDashboardTitle(dashboardTitle) {
if (isEntityDirectory(dashboardTitle)) {
return dashboardTitle.slice(0, -' [Entity Directory]'.length)
}
return dashboardTitle
}
onMounted(() => {
fetchEntities()
})

View File

@ -19,15 +19,21 @@
</dd>
<dt v-if="entityDetails.title">Title</dt>
<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd>
<template v-if="entityDetails.fields">
<template v-for="(value, key) in entityDetails.fields" :key="key">
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
</template>
</template>
</dl>
<p v-if="!entityDetails.title">No details available for this entity.</p>
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
<hr />
<h3>Dashboard Entity Directories</h3>
<div v-if="entityDetails.directories && entityDetails.directories.length > 0" class="directories-section">
<div v-if="filteredDirectories.length > 0" class="directories-section">
<ul class="directory-list">
<li v-for="directory in entityDetails.directories" :key="directory">
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
<router-link
:to="{
name: 'Dashboard',
@ -50,7 +56,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
@ -64,6 +70,13 @@
entityKey: String
})
const filteredDirectories = computed(() => {
if (!entityDetails.value?.directories) {
return []
}
return entityDetails.value.directories.filter(d => d)
})
function goBack() {
router.push({ name: 'Entities' })
}

View File

@ -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) {
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 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'))
}
export async function getExecutionDialogOutput() {

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

@ -0,0 +1,7 @@
include: dashboards.d
actions:
- title: Base Action
shell: echo "base"
icon: ping

View File

@ -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!"

View File

@ -0,0 +1,30 @@
entities:
- file: entities/person.yaml
name: person
dashboards:
- title: Second Dashboard
contents:
- type: action
inlineAction:
title: Third Action
shell: echo "Second Dashboard, Third Action!"
- type: action
inlineAction:
title: Fourth Action
shell: echo "Second Dashboard, Fourth 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

View File

@ -0,0 +1,4 @@
- name: Alice
- name: Bob

View File

@ -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('Fourth Action')
})
it('Second Dashboard shows Third and Fourth 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('Fourth 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')
})
})

View File

@ -37,6 +37,7 @@ message Entity {
string unique_key = 2;
string type = 3;
repeated string directories = 4;
map<string, string> fields = 5;
}
message GetDashboardResponse {

View File

@ -271,6 +271,7 @@ type Entity struct {
UniqueKey string `protobuf:"bytes,2,opt,name=unique_key,json=uniqueKey,proto3" json:"unique_key,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Directories []string `protobuf:"bytes,4,rep,name=directories,proto3" json:"directories,omitempty"`
Fields map[string]string `protobuf:"bytes,5,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -333,6 +334,13 @@ func (x *Entity) GetDirectories() []string {
return nil
}
func (x *Entity) GetFields() map[string]string {
if x != nil {
return x.Fields
}
return nil
}
type GetDashboardResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@ -3839,13 +3847,17 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
"\x14ActionArgumentChoice\x12\x14\n" +
"\x05value\x18\x01 \x01(\tR\x05value\x12\x14\n" +
"\x05title\x18\x02 \x01(\tR\x05title\"s\n" +
"\x05title\x18\x02 \x01(\tR\x05title\"\xeb\x01\n" +
"\x06Entity\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12\x1d\n" +
"\n" +
"unique_key\x18\x02 \x01(\tR\tuniqueKey\x12\x12\n" +
"\x04type\x18\x03 \x01(\tR\x04type\x12 \n" +
"\vdirectories\x18\x04 \x03(\tR\vdirectories\"f\n" +
"\vdirectories\x18\x04 \x03(\tR\vdirectories\x12;\n" +
"\x06fields\x18\x05 \x03(\v2#.olivetin.api.v1.Entity.FieldsEntryR\x06fields\x1a9\n" +
"\vFieldsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"f\n" +
"\x14GetDashboardResponse\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"`\n" +
@ -4113,7 +4125,7 @@ func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte {
return file_olivetin_api_v1_olivetin_proto_rawDescData
}
var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 71)
var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 72)
var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
(*Action)(nil), // 0: olivetin.api.v1.Action
(*ActionArgument)(nil), // 1: olivetin.api.v1.ActionArgument
@ -4184,95 +4196,97 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
(*GetEntityRequest)(nil), // 66: olivetin.api.v1.GetEntityRequest
(*RestartActionRequest)(nil), // 67: olivetin.api.v1.RestartActionRequest
nil, // 68: olivetin.api.v1.ActionArgument.SuggestionsEntry
nil, // 69: olivetin.api.v1.DumpVarsResponse.ContentsEntry
nil, // 70: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
nil, // 69: olivetin.api.v1.Entity.FieldsEntry
nil, // 70: olivetin.api.v1.DumpVarsResponse.ContentsEntry
nil, // 71: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
}
var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
1, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument
2, // 1: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice
68, // 2: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry
7, // 3: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard
8, // 4: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent
8, // 5: olivetin.api.v1.DashboardComponent.contents:type_name -> olivetin.api.v1.DashboardComponent
0, // 6: olivetin.api.v1.DashboardComponent.action:type_name -> olivetin.api.v1.Action
10, // 7: olivetin.api.v1.StartActionRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
10, // 8: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
19, // 9: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
19, // 10: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
19, // 11: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
19, // 12: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
19, // 13: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
69, // 14: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
70, // 15: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
43, // 16: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
44, // 17: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
45, // 18: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
46, // 19: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted
42, // 20: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk
19, // 21: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
19, // 22: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
60, // 23: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
59, // 24: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
5, // 25: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
0, // 26: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
65, // 27: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
3, // 28: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
35, // 29: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.ActionEntityPair
6, // 30: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
9, // 31: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
12, // 32: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
14, // 33: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
16, // 34: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
67, // 35: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
47, // 36: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
27, // 37: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
18, // 38: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
21, // 39: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
23, // 40: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
29, // 41: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
31, // 42: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest
33, // 43: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
36, // 44: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
38, // 45: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
49, // 46: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
51, // 47: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
53, // 48: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
40, // 49: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
55, // 50: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
57, // 51: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
61, // 52: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
63, // 53: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
66, // 54: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
4, // 55: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
11, // 56: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
13, // 57: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
15, // 58: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
17, // 59: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
11, // 60: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
48, // 61: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
28, // 62: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
20, // 63: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
22, // 64: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
24, // 65: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
30, // 66: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
32, // 67: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse
34, // 68: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
37, // 69: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
39, // 70: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
50, // 71: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
52, // 72: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
54, // 73: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
41, // 74: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
56, // 75: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
58, // 76: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
62, // 77: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
64, // 78: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
3, // 79: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
55, // [55:80] is the sub-list for method output_type
30, // [30:55] is the sub-list for method input_type
30, // [30:30] is the sub-list for extension type_name
30, // [30:30] is the sub-list for extension extendee
0, // [0:30] is the sub-list for field type_name
69, // 3: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry
7, // 4: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard
8, // 5: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent
8, // 6: olivetin.api.v1.DashboardComponent.contents:type_name -> olivetin.api.v1.DashboardComponent
0, // 7: olivetin.api.v1.DashboardComponent.action:type_name -> olivetin.api.v1.Action
10, // 8: olivetin.api.v1.StartActionRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
10, // 9: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
19, // 10: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
19, // 11: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
19, // 12: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
19, // 13: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
19, // 14: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
70, // 15: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
71, // 16: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
43, // 17: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
44, // 18: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
45, // 19: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
46, // 20: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted
42, // 21: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk
19, // 22: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
19, // 23: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
60, // 24: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
59, // 25: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
5, // 26: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
0, // 27: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
65, // 28: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
3, // 29: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
35, // 30: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.ActionEntityPair
6, // 31: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
9, // 32: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
12, // 33: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
14, // 34: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
16, // 35: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
67, // 36: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
47, // 37: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
27, // 38: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
18, // 39: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
21, // 40: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
23, // 41: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
29, // 42: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
31, // 43: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest
33, // 44: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
36, // 45: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
38, // 46: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
49, // 47: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
51, // 48: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
53, // 49: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
40, // 50: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
55, // 51: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
57, // 52: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
61, // 53: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
63, // 54: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
66, // 55: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
4, // 56: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
11, // 57: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
13, // 58: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
15, // 59: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
17, // 60: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
11, // 61: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
48, // 62: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
28, // 63: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
20, // 64: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
22, // 65: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
24, // 66: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
30, // 67: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
32, // 68: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse
34, // 69: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
37, // 70: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
39, // 71: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
50, // 72: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
52, // 73: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
54, // 74: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
41, // 75: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
56, // 76: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
58, // 77: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
62, // 78: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
64, // 79: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
3, // 80: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
56, // [56:81] is the sub-list for method output_type
31, // [31:56] is the sub-list for method input_type
31, // [31:31] is the sub-list for extension type_name
31, // [31:31] is the sub-list for extension extendee
0, // [0:31] is the sub-list for field type_name
}
func init() { file_olivetin_api_v1_olivetin_proto_init() }
@ -4293,7 +4307,7 @@ func file_olivetin_api_v1_olivetin_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)),
NumEnums: 0,
NumMessages: 71,
NumMessages: 72,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -977,24 +977,49 @@ func buildSortedEntityInstances(entityType string, entityInstances map[string]*e
func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
var foundDashboards []string
seen := make(map[string]bool)
findEntityInComponents(entityTitle, "", dashboards, &foundDashboards)
findEntityInComponents(entityTitle, "", dashboards, &foundDashboards, seen)
return foundDashboards
}
func findEntityInComponents(entityTitle string, parentTitle string, components []*config.DashboardComponent, foundDashboards *[]string) {
func findEntityInComponents(entityTitle string, parentTitle string, components []*config.DashboardComponent, foundDashboards *[]string, seen map[string]bool) {
for _, component := range components {
if component.Entity == entityTitle {
*foundDashboards = append(*foundDashboards, parentTitle)
addEntityDashboard(component, parentTitle, foundDashboards, seen)
}
if len(component.Contents) > 0 {
findEntityInComponents(entityTitle, component.Title, component.Contents, foundDashboards)
findEntityInComponents(entityTitle, component.Title, component.Contents, foundDashboards, seen)
}
}
}
func addEntityDashboard(component *config.DashboardComponent, parentTitle string, foundDashboards *[]string, seen map[string]bool) {
if component.Type == "directory" {
addEntityDirectory(component, foundDashboards, seen)
} else {
addParentDashboard(parentTitle, foundDashboards, seen)
}
}
func addEntityDirectory(component *config.DashboardComponent, foundDashboards *[]string, seen map[string]bool) {
dashboardTitle := component.Title + " [Entity Directory]"
if !seen[dashboardTitle] {
*foundDashboards = append(*foundDashboards, dashboardTitle)
seen[dashboardTitle] = true
seen[component.Title] = true
}
}
func addParentDashboard(parentTitle string, foundDashboards *[]string, seen map[string]bool) {
if parentTitle != "" && !seen[parentTitle] {
*foundDashboards = append(*foundDashboards, parentTitle)
seen[parentTitle] = true
}
}
func findDirectoriesInEntityFieldsets(entityType string, dashboards []*config.DashboardComponent) []string {
var directories []string
@ -1036,26 +1061,46 @@ func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.Ge
return nil, err
}
res := &apiv1.Entity{}
instances := entities.GetEntityInstances(req.Msg.Type)
log.Infof("msg: %+v", req.Msg)
if len(instances) == 0 {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type))
}
if entity, ok := instances[req.Msg.UniqueKey]; !ok {
entity, ok := instances[req.Msg.UniqueKey]
if !ok {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity with unique key %s not found in type %s", req.Msg.UniqueKey, req.Msg.Type))
} else {
res.Title = entity.Title
res.UniqueKey = entity.UniqueKey
res.Type = req.Msg.Type
res.Directories = findDirectoriesInEntityFieldsets(req.Msg.Type, api.cfg.Dashboards)
return connect.NewResponse(res), nil
}
res := buildEntityResponse(entity, req.Msg.Type, api.cfg.Dashboards)
return connect.NewResponse(res), nil
}
func buildEntityResponse(entity *entities.Entity, entityType string, dashboards []*config.DashboardComponent) *apiv1.Entity {
res := &apiv1.Entity{
Title: entity.Title,
UniqueKey: entity.UniqueKey,
Type: entityType,
Directories: findDirectoriesInEntityFieldsets(entityType, dashboards),
Fields: serializeEntityFields(entity.Data),
}
return res
}
func serializeEntityFields(data any) map[string]string {
if data == nil {
return nil
}
dataMap, ok := data.(map[string]any)
if !ok {
return nil
}
fields := make(map[string]string)
for k, v := range dataMap {
fields[k] = fmt.Sprintf("%v", v)
}
return fields
}
func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv1.RestartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {

View File

@ -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
}

View File

@ -185,10 +185,6 @@ func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardCompo
return ret
}
func getDashboardComponentContents(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
return getDashboardComponentContentsWithEntity(dashboard, rr, nil)
}
func getDashboardComponentContentsWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0)
rootFieldset := createRootFieldset()
@ -208,10 +204,6 @@ func createRootFieldset() *apiv1.DashboardComponent {
}
}
func processDashboardSubitem(subitem *config.DashboardComponent, rr *DashboardRenderRequest, ret *[]*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent) {
processDashboardSubitemWithEntity(subitem, rr, ret, rootFieldset, nil)
}
func processDashboardSubitemWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, ret *[]*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent, entity *entities.Entity) {
if subitem.Type != "fieldset" {
rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimpleWithEntity(subitem, rr, entity))
@ -232,10 +224,6 @@ func appendRootFieldsetIfNeeded(ret []*apiv1.DashboardComponent, rootFieldset *a
return ret
}
func buildDashboardComponentSimple(subitem *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
return buildDashboardComponentSimpleWithEntity(subitem, rr, nil)
}
func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.DashboardComponent {
var contents []*apiv1.DashboardComponent

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

@ -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
}

View File

@ -19,6 +19,113 @@ 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) {
visited := make(map[*DashboardComponent]bool)
cfg.sanitizeDashboardComponentForInlineActionsHelper(component, visited)
}
func (cfg *Config) sanitizeDashboardComponentForInlineActionsHelper(component *DashboardComponent, visited map[*DashboardComponent]bool) {
if component == nil {
return
}
if visited[component] {
return
}
visited[component] = true
cfg.sanitizeInlineAction(component)
cfg.sanitizeChildDashboardComponents(component, visited)
}
func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
if component.InlineAction == nil {
return
}
sanitizeInlineActionTitles(component)
if component.Entity != "" && component.InlineAction.Entity == "" {
component.InlineAction.Entity = component.Entity
}
component.InlineAction.sanitize(cfg)
cfg.addInlineActionIfNotExists(component.InlineAction)
}
func (cfg *Config) addInlineActionIfNotExists(action *Action) {
if cfg.inlineActionExists(action) {
return
}
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 {
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, visited map[*DashboardComponent]bool) {
for _, child := range component.Contents {
if child.Entity == "" {
child.Entity = component.Entity
}
cfg.sanitizeDashboardComponentForInlineActionsHelper(child, visited)
}
}
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")
}
}

View File

@ -120,7 +120,7 @@ func findEntityTitle(data any) string {
keys[lookupKey] = k
}
for _, key := range []string{"title", "name", "id"} {
for _, key := range []string{"title", "name", "id", "hostname", "host", "label"} {
if lookupKey, exists := keys[strings.ToLower(key)]; exists {
if value, ok := mapData[lookupKey]; ok {
if valueStr, ok := value.(string); ok {

View File

@ -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)
}

View File

@ -81,7 +81,15 @@ 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
}
if title != "" {
req.DashboardActionTitles = append(req.DashboardActionTitles, title)
}
} else if sub.Type == "link" || sub.Type == "" {
req.DashboardActionTitles = append(req.DashboardActionTitles, sub.Title)
}