Преглед на файлове

feat: Entity directories, inline actions, and all entity fields are now available in the entity details view

jamesread преди 7 месеца
родител
ревизия
a2410fc9d6

+ 5 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

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

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 22 - 3
frontend/resources/vue/views/EntitiesView.vue

@@ -21,10 +21,14 @@
 
 
 				<h3>Used on Dashboards:</h3>
 				<h3>Used on Dashboards:</h3>
 				<ul>
 				<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>
 						</router-link>
+						<span v-else>{{ dash }}</span>
 					</li>
 					</li>
 				</ul>
 				</ul>
 			</div>
 			</div>
@@ -44,6 +48,21 @@
         entityDefinitions.value = ret.entityDefinitions
         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(() => {
     onMounted(() => {
         fetchEntities()
         fetchEntities()
 	})
 	})

+ 17 - 4
frontend/resources/vue/views/EntityDetailsView.vue

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

+ 3 - 3
integration-tests/tests/multi-dashboard-includes/dashboards.d/people.yaml

@@ -3,17 +3,17 @@ entities:
     name: person
     name: person
 
 
 dashboards:
 dashboards:
-  - title: People
+  - title: Second Dashboard
     contents:
     contents:
       - type: action
       - type: action
         inlineAction:
         inlineAction:
           title: Third Action
           title: Third Action
-          shell: echo "First Dashboard, Third Action!"
+          shell: echo "Second Dashboard, Third Action!"
 
 
       - type: action
       - type: action
         inlineAction:
         inlineAction:
           title: Forth Action
           title: Forth Action
-          shell: echo "First Dashboard, Forth Action!"
+          shell: echo "Second Dashboard, Fourth Action!"
 
 
       - title: "Person: {{ person.name }}"
       - title: "Person: {{ person.name }}"
         type: fieldset
         type: fieldset

+ 3 - 3
integration-tests/tests/multi-dashboard-includes/multi-dashboard-includes.mjs

@@ -84,10 +84,10 @@ describe('config: multi-dashboard-includes', function () {
 
 
     // Ensure actions from the second dashboard are not rendered here
     // Ensure actions from the second dashboard are not rendered here
     expect(titles).to.not.include('Third Action')
     expect(titles).to.not.include('Third Action')
-    expect(titles).to.not.include('Forth Action')
+    expect(titles).to.not.include('Fourth Action')
   })
   })
 
 
-  it('Second Dashboard shows Third and Forth inline actions from include', async function () {
+  it('Second Dashboard shows Third and Fourth inline actions from include', async function () {
     await getRootAndWait()
     await getRootAndWait()
 
 
     // Navigate to "Second Dashboard"
     // Navigate to "Second Dashboard"
@@ -96,7 +96,7 @@ describe('config: multi-dashboard-includes', function () {
     const titles = await getActionTitlesOnDashboard('Second Dashboard')
     const titles = await getActionTitlesOnDashboard('Second Dashboard')
 
 
     expect(titles).to.include('Third Action')
     expect(titles).to.include('Third Action')
-    expect(titles).to.include('Forth Action')
+    expect(titles).to.include('Fourth Action')
 
 
     // Ensure actions from the first dashboard are not rendered here
     // Ensure actions from the first dashboard are not rendered here
     expect(titles).to.not.include('First Action')
     expect(titles).to.not.include('First Action')

+ 1 - 0
proto/olivetin/api/v1/olivetin.proto

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

+ 102 - 88
service/gen/olivetin/api/v1/olivetin.pb.go

@@ -271,6 +271,7 @@ type Entity struct {
 	UniqueKey     string                 `protobuf:"bytes,2,opt,name=unique_key,json=uniqueKey,proto3" json:"unique_key,omitempty"`
 	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"`
 	Type          string                 `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
 	Directories   []string               `protobuf:"bytes,4,rep,name=directories,proto3" json:"directories,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
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 	sizeCache     protoimpl.SizeCache
 }
 }
@@ -333,6 +334,13 @@ func (x *Entity) GetDirectories() []string {
 	return nil
 	return nil
 }
 }
 
 
+func (x *Entity) GetFields() map[string]string {
+	if x != nil {
+		return x.Fields
+	}
+	return nil
+}
+
 type GetDashboardResponse struct {
 type GetDashboardResponse struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
 	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" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"B\n" +
 	"\x14ActionArgumentChoice\x12\x14\n" +
 	"\x14ActionArgumentChoice\x12\x14\n" +
 	"\x05value\x18\x01 \x01(\tR\x05value\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" +
 	"\x06Entity\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1d\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1d\n" +
 	"\n" +
 	"\n" +
 	"unique_key\x18\x02 \x01(\tR\tuniqueKey\x12\x12\n" +
 	"unique_key\x18\x02 \x01(\tR\tuniqueKey\x12\x12\n" +
 	"\x04type\x18\x03 \x01(\tR\x04type\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" +
 	"\x14GetDashboardResponse\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
 	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"`\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
 	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{
 var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
 	(*Action)(nil),                          // 0: olivetin.api.v1.Action
 	(*Action)(nil),                          // 0: olivetin.api.v1.Action
 	(*ActionArgument)(nil),                  // 1: olivetin.api.v1.ActionArgument
 	(*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
 	(*GetEntityRequest)(nil),                // 66: olivetin.api.v1.GetEntityRequest
 	(*RestartActionRequest)(nil),            // 67: olivetin.api.v1.RestartActionRequest
 	(*RestartActionRequest)(nil),            // 67: olivetin.api.v1.RestartActionRequest
 	nil,                                     // 68: olivetin.api.v1.ActionArgument.SuggestionsEntry
 	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{
 var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
 	1,  // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument
 	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
 	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
 	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() }
 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(),
 			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)),
 			RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)),
 			NumEnums:      0,
 			NumEnums:      0,
-			NumMessages:   71,
+			NumMessages:   72,
 			NumExtensions: 0,
 			NumExtensions: 0,
 			NumServices:   1,
 			NumServices:   1,
 		},
 		},

+ 61 - 16
service/internal/api/api.go

@@ -977,24 +977,49 @@ func buildSortedEntityInstances(entityType string, entityInstances map[string]*e
 
 
 func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
 func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
 	var foundDashboards []string
 	var foundDashboards []string
+	seen := make(map[string]bool)
 
 
-	findEntityInComponents(entityTitle, "", dashboards, &foundDashboards)
+	findEntityInComponents(entityTitle, "", dashboards, &foundDashboards, seen)
 
 
 	return foundDashboards
 	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 {
 	for _, component := range components {
 		if component.Entity == entityTitle {
 		if component.Entity == entityTitle {
-			*foundDashboards = append(*foundDashboards, parentTitle)
+			addEntityDashboard(component, parentTitle, foundDashboards, seen)
 		}
 		}
 
 
 		if len(component.Contents) > 0 {
 		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 {
 func findDirectoriesInEntityFieldsets(entityType string, dashboards []*config.DashboardComponent) []string {
 	var directories []string
 	var directories []string
 
 
@@ -1036,26 +1061,46 @@ func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.Ge
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	res := &apiv1.Entity{}
-
 	instances := entities.GetEntityInstances(req.Msg.Type)
 	instances := entities.GetEntityInstances(req.Msg.Type)
-
-	log.Infof("msg: %+v", req.Msg)
-
 	if len(instances) == 0 {
 	if len(instances) == 0 {
 		return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type))
 		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))
 		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) {
 func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv1.RestartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {

+ 14 - 3
service/internal/config/sanitize.go

@@ -29,12 +29,23 @@ func (cfg *Config) sanitizeDashboardsForInlineActions() {
 	}
 	}
 }
 }
 func (cfg *Config) sanitizeDashboardComponentForInlineActions(component *DashboardComponent) {
 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 {
 	if component == nil {
 		return
 		return
 	}
 	}
 
 
+	if visited[component] {
+		return
+	}
+
+	visited[component] = true
+
 	cfg.sanitizeInlineAction(component)
 	cfg.sanitizeInlineAction(component)
-	cfg.sanitizeChildDashboardComponents(component)
+	cfg.sanitizeChildDashboardComponents(component, visited)
 }
 }
 
 
 func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
 func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
@@ -107,13 +118,13 @@ func (cfg *Config) inlineActionIDExists(action *Action) bool {
 	return false
 	return false
 }
 }
 
 
-func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent) {
+func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent, visited map[*DashboardComponent]bool) {
 	for _, child := range component.Contents {
 	for _, child := range component.Contents {
 		if child.Entity == "" {
 		if child.Entity == "" {
 			child.Entity = component.Entity
 			child.Entity = component.Entity
 		}
 		}
 
 
-		cfg.sanitizeDashboardComponentForInlineActions(child)
+		cfg.sanitizeDashboardComponentForInlineActionsHelper(child, visited)
 	}
 	}
 }
 }
 
 

+ 1 - 1
service/internal/entities/storage.go

@@ -120,7 +120,7 @@ func findEntityTitle(data any) string {
 			keys[lookupKey] = k
 			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 lookupKey, exists := keys[strings.ToLower(key)]; exists {
 				if value, ok := mapData[lookupKey]; ok {
 				if value, ok := mapData[lookupKey]; ok {
 					if valueStr, ok := value.(string); ok {
 					if valueStr, ok := value.(string); ok {

+ 3 - 1
service/internal/executor/executor_actions.go

@@ -86,7 +86,9 @@ func recurseDashboardForActionTitles(component *config.DashboardComponent, req *
 			if title == "" {
 			if title == "" {
 				title = sub.InlineAction.Title
 				title = sub.InlineAction.Title
 			}
 			}
-			req.DashboardActionTitles = append(req.DashboardActionTitles, title)
+			if title != "" {
+				req.DashboardActionTitles = append(req.DashboardActionTitles, title)
+			}
 		} else if sub.Type == "link" || sub.Type == "" {
 		} else if sub.Type == "link" || sub.Type == "" {
 			req.DashboardActionTitles = append(req.DashboardActionTitles, sub.Title)
 			req.DashboardActionTitles = append(req.DashboardActionTitles, sub.Title)
 		}
 		}

Някои файлове не бяха показани, защото твърде много файлове са промени