feat: Mega entity improvements - entity directories, inehrit entities (#450), ordering (#762 / #703), better entity view
This commit is contained in:
parent
736ad1c83b
commit
56365af24b
22
config.yaml
22
config.yaml
|
|
@ -189,12 +189,24 @@ actions:
|
|||
- title: Ping hypervisor2
|
||||
shell: echo "hypervisor2 online"
|
||||
|
||||
- title: Ping hypervisor3
|
||||
shell: echo "hypervisor3 online"
|
||||
|
||||
- title: Ping hypervisor4
|
||||
shell: echo "hypervisor4 online"
|
||||
|
||||
- title: "{{ server.name }} Wake on Lan"
|
||||
shell: echo "Sending Wake on LAN to {{ server.hostname }}"
|
||||
icon: <iconify-icon icon="carbon:awake"></iconify-icon>
|
||||
entity: server
|
||||
|
||||
- title: "{{ server.name }} Power Off"
|
||||
shell: "echo 'Power Off Server: {{ server.hostname }}'"
|
||||
icon: <iconify-icon icon="carbon:flash-off"></iconify-icon>
|
||||
entity: server
|
||||
|
||||
- title: "{{ server.name }} Print server name"
|
||||
shell: 'echo "Server name: {{ server.name }}"'
|
||||
entity: server
|
||||
|
||||
- title: Ping All Servers
|
||||
|
|
@ -278,6 +290,11 @@ dashboards:
|
|||
contents:
|
||||
- title: Ping hypervisor1
|
||||
- title: Ping hypervisor2
|
||||
- title: More hypervisors
|
||||
type: directory
|
||||
contents:
|
||||
- title: Ping hypervisor3
|
||||
- title: Ping hypervisor4
|
||||
|
||||
# If you specify `type: fieldset` and some `contents`, it will show your
|
||||
# actions grouped together without a folder.
|
||||
|
|
@ -299,6 +316,11 @@ dashboards:
|
|||
- title: '{{ server.name }} Wake on Lan'
|
||||
- title: '{{ server.name }} Power Off'
|
||||
|
||||
- title: More Options
|
||||
type: directory
|
||||
contents:
|
||||
- title: '{{ server.name }} Print server name'
|
||||
|
||||
# This is the second dashboard.
|
||||
- title: My Containers
|
||||
contents:
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ export declare type Entity = Message<"olivetin.api.v1.Entity"> & {
|
|||
* @generated from field: string type = 3;
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated string directories = 4;
|
||||
*/
|
||||
directories: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -204,6 +209,16 @@ export declare type GetDashboardRequest = Message<"olivetin.api.v1.GetDashboardR
|
|||
* @generated from field: string title = 1;
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string entity_type = 2;
|
||||
*/
|
||||
entityType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string entity_key = 3;
|
||||
*/
|
||||
entityKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -266,6 +281,16 @@ export declare type DashboardComponent = Message<"olivetin.api.v1.DashboardCompo
|
|||
* @generated from field: olivetin.api.v1.Action action = 6;
|
||||
*/
|
||||
action?: Action;
|
||||
|
||||
/**
|
||||
* @generated from field: string entity_type = 7;
|
||||
*/
|
||||
entityType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string entity_key = 8;
|
||||
*/
|
||||
entityKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -11,13 +11,39 @@
|
|||
</section>
|
||||
<template v-else-if="dashboard">
|
||||
<section v-if="dashboard.contents.length == 0">
|
||||
<div class="back-button-container" v-if="isDirectory">
|
||||
<button @click="goBack" class="back-button">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2>{{ dashboard.title }}</h2>
|
||||
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
|
||||
</section>
|
||||
|
||||
<section class="transparent" v-else>
|
||||
<div class="back-button-container" v-if="isDirectory">
|
||||
<button @click="goBack" class="back-button">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title">
|
||||
<h2 v-if = "dashboard.title != 'Default'">{{ component.title }}</h2>
|
||||
<h2 v-if = "dashboard.title != 'Default'">
|
||||
<router-link
|
||||
v-if="component.entityType && component.entityKey"
|
||||
:to="{
|
||||
name: 'EntityDetails',
|
||||
params: {
|
||||
entityType: component.entityType,
|
||||
entityKey: component.entityKey
|
||||
}
|
||||
}"
|
||||
class="entity-link">
|
||||
{{ component.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ component.title }}</span>
|
||||
</h2>
|
||||
|
||||
<fieldset>
|
||||
<template v-for="subcomponent in component.contents">
|
||||
|
|
@ -31,23 +57,54 @@
|
|||
|
||||
<script setup>
|
||||
import DashboardComponent from './components/DashboardComponent.vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { Loading03Icon } from '@hugeicons/core-free-icons'
|
||||
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
entityType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
entityKey: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const dashboard = ref(null)
|
||||
const loadingTime = ref(0)
|
||||
const initError = ref(null)
|
||||
let loadingTimer = null
|
||||
let checkInitInterval = null
|
||||
|
||||
const isDirectory = computed(() => {
|
||||
if (!dashboard.value || !window.initResponse) {
|
||||
return false
|
||||
}
|
||||
const rootDashboards = window.initResponse.rootDashboards || []
|
||||
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||
if (rootDashboards.length > 0) {
|
||||
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
|
||||
} else {
|
||||
router.push({ name: 'Actions' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getDashboard() {
|
||||
let title = props.title
|
||||
|
||||
|
|
@ -58,9 +115,16 @@ async function getDashboard() {
|
|||
}
|
||||
|
||||
try {
|
||||
const ret = await window.client.getDashboard({
|
||||
const request = {
|
||||
title: title,
|
||||
})
|
||||
}
|
||||
|
||||
if (props.entityType && props.entityKey) {
|
||||
request.entityType = props.entityType
|
||||
request.entityKey = props.entityKey
|
||||
}
|
||||
|
||||
const ret = await window.client.getDashboard(request)
|
||||
|
||||
if (!ret || !ret.dashboard) {
|
||||
throw new Error('No dashboard found')
|
||||
|
|
@ -166,6 +230,17 @@ h2 {
|
|||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h2 .entity-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
h2 .entity-link:hover {
|
||||
opacity: 0.7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
|
|
@ -183,4 +258,41 @@ fieldset {
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 1em;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 1em;
|
||||
background-color: var(--bg, #fff);
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: var(--bg-hover, #f5f5f5);
|
||||
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.back-button {
|
||||
background-color: var(--bg, #111);
|
||||
border-color: var(--border-color, #333);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: var(--bg-hover, #222);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<button @click="router.push({ name: 'Dashboard', params: { title: component.title } })">
|
||||
<button @click="navigateToDirectory">
|
||||
{{ component.title }}
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -15,6 +15,17 @@ const props = defineProps({
|
|||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
function navigateToDirectory() {
|
||||
const params = { title: props.component.title }
|
||||
|
||||
if (props.component.entityType && props.component.entityKey) {
|
||||
params.entityType = props.component.entityType
|
||||
params.entityKey = props.component.entityKey
|
||||
}
|
||||
|
||||
router.push({ name: 'Dashboard', params })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const routes = [
|
|||
meta: { title: 'Actions', icon: DashboardSquare01Icon }
|
||||
},
|
||||
{
|
||||
path: '/dashboards/:title',
|
||||
path: '/dashboards/:title/:entityType?/:entityKey?',
|
||||
name: 'Dashboard',
|
||||
component: () => import('./Dashboard.vue'),
|
||||
props: true,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,62 @@
|
|||
<template>
|
||||
<Section title="Entity Details">
|
||||
<div>
|
||||
<p v-if="!entityDetails">Loading entity details...</p>
|
||||
<p v-else-if="!entityDetails.title">No details available for this entity.</p>
|
||||
<p v-else>{{ entityDetails.title }}</p>
|
||||
<template #toolbar>
|
||||
<button @click="goBack" class="back-button">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
</template>
|
||||
<div v-if="!entityDetails">
|
||||
<p>Loading entity details...</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<dl>
|
||||
<dt>Type</dt>
|
||||
<dd>
|
||||
<router-link :to="{ name: 'Entities' }" class="entity-type-link">
|
||||
{{ entityType }}
|
||||
</router-link>
|
||||
</dd>
|
||||
<dt v-if="entityDetails.title">Title</dt>
|
||||
<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd>
|
||||
</dl>
|
||||
<p v-if="!entityDetails.title">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">
|
||||
<ul class="directory-list">
|
||||
<li v-for="directory in entityDetails.directories" :key="directory">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Dashboard',
|
||||
params: {
|
||||
title: directory,
|
||||
entityType: entityType,
|
||||
entityKey: entityKey
|
||||
}
|
||||
}">
|
||||
{{ directory }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p v-else>No directories found for this entity.
|
||||
<a href = "https://docs.olivetin.app/dashboards/entity-directories.html" target = "_blank">Learn more</a>
|
||||
</p>
|
||||
</template>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const entityDetails = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -19,6 +64,10 @@
|
|||
entityKey: String
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.push({ name: 'Entities' })
|
||||
}
|
||||
|
||||
async function fetchEntityDetails() {
|
||||
try {
|
||||
const response = await window.client.getEntity({
|
||||
|
|
@ -38,3 +87,77 @@
|
|||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 1em;
|
||||
background-color: var(--bg, #fff);
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: var(--bg-hover, #f5f5f5);
|
||||
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.directories-section h3 {
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.directory-list a {
|
||||
text-decoration: none;
|
||||
padding: 0.5em;
|
||||
display: inline-block;
|
||||
border-radius: 0.3em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.directory-list a:hover {
|
||||
background-color: var(--bg-hover, #f5f5f5);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.entity-type-link {
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.entity-type-link:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-color, #ccc);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.back-button {
|
||||
background-color: var(--bg, #111);
|
||||
border-color: var(--border-color, #333);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: var(--bg-hover, #222);
|
||||
}
|
||||
|
||||
.directories-section {
|
||||
border-top-color: var(--border-color, #333);
|
||||
}
|
||||
|
||||
|
||||
.directory-list a:hover {
|
||||
background-color: var(--bg-hover, #222);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ message Entity {
|
|||
string title = 1;
|
||||
string unique_key = 2;
|
||||
string type = 3;
|
||||
repeated string directories = 4;
|
||||
}
|
||||
|
||||
message GetDashboardResponse {
|
||||
|
|
@ -51,6 +52,8 @@ message EffectivePolicy {
|
|||
|
||||
message GetDashboardRequest {
|
||||
string title = 1;
|
||||
string entity_type = 2;
|
||||
string entity_key = 3;
|
||||
}
|
||||
|
||||
message Dashboard {
|
||||
|
|
@ -65,6 +68,8 @@ message DashboardComponent {
|
|||
string icon = 4;
|
||||
string css_class = 5;
|
||||
Action action = 6;
|
||||
string entity_type = 7;
|
||||
string entity_key = 8;
|
||||
}
|
||||
|
||||
message StartActionRequest {
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ type Entity struct {
|
|||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,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"`
|
||||
Directories []string `protobuf:"bytes,4,rep,name=directories,proto3" json:"directories,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -325,6 +326,13 @@ func (x *Entity) GetType() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *Entity) GetDirectories() []string {
|
||||
if x != nil {
|
||||
return x.Directories
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetDashboardResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
|
|
@ -432,6 +440,8 @@ func (x *EffectivePolicy) GetShowLogList() bool {
|
|||
type GetDashboardRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
EntityType string `protobuf:"bytes,2,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"`
|
||||
EntityKey string `protobuf:"bytes,3,opt,name=entity_key,json=entityKey,proto3" json:"entity_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -473,6 +483,20 @@ func (x *GetDashboardRequest) GetTitle() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDashboardRequest) GetEntityType() string {
|
||||
if x != nil {
|
||||
return x.EntityType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetDashboardRequest) GetEntityKey() string {
|
||||
if x != nil {
|
||||
return x.EntityKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Dashboard struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
|
|
@ -533,6 +557,8 @@ type DashboardComponent struct {
|
|||
Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||
CssClass string `protobuf:"bytes,5,opt,name=css_class,json=cssClass,proto3" json:"css_class,omitempty"`
|
||||
Action *Action `protobuf:"bytes,6,opt,name=action,proto3" json:"action,omitempty"`
|
||||
EntityType string `protobuf:"bytes,7,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"`
|
||||
EntityKey string `protobuf:"bytes,8,opt,name=entity_key,json=entityKey,proto3" json:"entity_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -609,6 +635,20 @@ func (x *DashboardComponent) GetAction() *Action {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *DashboardComponent) GetEntityType() string {
|
||||
if x != nil {
|
||||
return x.EntityType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DashboardComponent) GetEntityKey() string {
|
||||
if x != nil {
|
||||
return x.EntityKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StartActionRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
|
||||
|
|
@ -3799,30 +3839,39 @@ 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\"Q\n" +
|
||||
"\x05title\x18\x02 \x01(\tR\x05title\"s\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\"f\n" +
|
||||
"\x04type\x18\x03 \x01(\tR\x04type\x12 \n" +
|
||||
"\vdirectories\x18\x04 \x03(\tR\vdirectories\"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" +
|
||||
"\x0fEffectivePolicy\x12)\n" +
|
||||
"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
|
||||
"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"+\n" +
|
||||
"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"k\n" +
|
||||
"\x13GetDashboardRequest\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\"b\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" +
|
||||
"\ventity_type\x18\x02 \x01(\tR\n" +
|
||||
"entityType\x12\x1d\n" +
|
||||
"\n" +
|
||||
"entity_key\x18\x03 \x01(\tR\tentityKey\"b\n" +
|
||||
"\tDashboard\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12?\n" +
|
||||
"\bcontents\x18\x02 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\"\xe1\x01\n" +
|
||||
"\bcontents\x18\x02 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\"\xa1\x02\n" +
|
||||
"\x12DashboardComponent\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
|
||||
"\x04type\x18\x02 \x01(\tR\x04type\x12?\n" +
|
||||
"\bcontents\x18\x03 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\x12\x12\n" +
|
||||
"\x04icon\x18\x04 \x01(\tR\x04icon\x12\x1b\n" +
|
||||
"\tcss_class\x18\x05 \x01(\tR\bcssClass\x12/\n" +
|
||||
"\x06action\x18\x06 \x01(\v2\x17.olivetin.api.v1.ActionR\x06action\"\xa5\x01\n" +
|
||||
"\x06action\x18\x06 \x01(\v2\x17.olivetin.api.v1.ActionR\x06action\x12\x1f\n" +
|
||||
"\ventity_type\x18\a \x01(\tR\n" +
|
||||
"entityType\x12\x1d\n" +
|
||||
"\n" +
|
||||
"entity_key\x18\b \x01(\tR\tentityKey\"\xa5\x01\n" +
|
||||
"\x12StartActionRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"binding_id\x18\x01 \x01(\tR\tbindingId\x12B\n" +
|
||||
|
|
|
|||
|
|
@ -415,7 +415,13 @@ func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1
|
|||
return nil, err
|
||||
}
|
||||
|
||||
dashboardRenderRequest := api.createDashboardRenderRequest(user)
|
||||
entityType := ""
|
||||
entityKey := ""
|
||||
if req.Msg != nil {
|
||||
entityType = req.Msg.EntityType
|
||||
entityKey = req.Msg.EntityKey
|
||||
}
|
||||
dashboardRenderRequest := api.createDashboardRenderRequest(user, entityType, entityKey)
|
||||
|
||||
if api.isDefaultDashboard(req.Msg.Title) {
|
||||
return api.buildDefaultDashboardResponse(dashboardRenderRequest)
|
||||
|
|
@ -431,11 +437,13 @@ func (api *oliveTinAPI) checkDashboardAccess(user *authpublic.AuthenticatedUser)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser) *DashboardRenderRequest {
|
||||
func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser, entityType, entityKey string) *DashboardRenderRequest {
|
||||
return &DashboardRenderRequest{
|
||||
AuthenticatedUser: user,
|
||||
cfg: api.cfg,
|
||||
ex: api.executor,
|
||||
EntityType: entityType,
|
||||
EntityKey: entityKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -835,7 +843,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
|||
|
||||
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||
var rootDashboards []string
|
||||
dashboardRenderRequest := api.createDashboardRenderRequest(user)
|
||||
dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
|
||||
|
||||
api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest)
|
||||
api.addCustomDashboards(&rootDashboards, dashboards, dashboardRenderRequest)
|
||||
|
|
@ -987,6 +995,32 @@ func findEntityInComponents(entityTitle string, parentTitle string, components [
|
|||
}
|
||||
}
|
||||
|
||||
func findDirectoriesInEntityFieldsets(entityType string, dashboards []*config.DashboardComponent) []string {
|
||||
var directories []string
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
findDirectoriesInEntityFieldsetsRecursive(entityType, dashboard, &directories)
|
||||
}
|
||||
|
||||
return directories
|
||||
}
|
||||
|
||||
func findDirectoriesInEntityFieldsetsRecursive(entityType string, component *config.DashboardComponent, directories *[]string) {
|
||||
if component.Entity == entityType {
|
||||
for _, subitem := range component.Contents {
|
||||
if subitem.Type == "directory" {
|
||||
*directories = append(*directories, subitem.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(component.Contents) > 0 {
|
||||
for _, subitem := range component.Contents {
|
||||
findDirectoriesInEntityFieldsetsRecursive(entityType, subitem, directories)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
|
|
@ -1008,6 +1042,9 @@ func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.Ge
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,15 +13,31 @@ type DashboardRenderRequest struct {
|
|||
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||
cfg *config.Config
|
||||
ex *executor.Executor
|
||||
EntityType string
|
||||
EntityKey string
|
||||
}
|
||||
|
||||
func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
|
||||
return rr.findActionForEntity(title, nil)
|
||||
}
|
||||
|
||||
func (rr *DashboardRenderRequest) findActionForEntity(title string, entity *entities.Entity) *apiv1.Action {
|
||||
rr.ex.MapActionIdToBindingLock.RLock()
|
||||
defer rr.ex.MapActionIdToBindingLock.RUnlock()
|
||||
|
||||
for _, binding := range rr.ex.MapActionIdToBinding {
|
||||
if binding.Action.Title == title {
|
||||
return buildAction(binding, rr)
|
||||
if binding.Action.Title != title {
|
||||
continue
|
||||
}
|
||||
|
||||
if entity == nil {
|
||||
if binding.Entity == nil {
|
||||
return buildAction(binding, rr)
|
||||
}
|
||||
} else {
|
||||
if binding.Entity != nil && binding.Entity.UniqueKey == entity.UniqueKey {
|
||||
return buildAction(binding, rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,11 +25,13 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr
|
|||
|
||||
func buildEntityFieldset(tpl *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||
return &apiv1.DashboardComponent{
|
||||
Title: entities.ParseTemplateWith(tpl.Title, ent),
|
||||
Type: "fieldset",
|
||||
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, rr)),
|
||||
CssClass: entities.ParseTemplateWith(tpl.CssClass, ent),
|
||||
Action: rr.findAction(tpl.Title),
|
||||
Title: entities.ParseTemplateWith(tpl.Title, ent),
|
||||
Type: "fieldset",
|
||||
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, tpl.Entity, rr)),
|
||||
CssClass: entities.ParseTemplateWith(tpl.CssClass, ent),
|
||||
Action: rr.findAction(tpl.Title),
|
||||
EntityType: tpl.Entity,
|
||||
EntityKey: ent.UniqueKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,11 +50,11 @@ func removeFieldsetIfHasNoLinks(contents []*apiv1.DashboardComponent) []*apiv1.D
|
|||
*/
|
||||
}
|
||||
|
||||
func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
|
||||
func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
|
||||
ret := make([]*apiv1.DashboardComponent, 0)
|
||||
|
||||
for _, subitem := range contents {
|
||||
c := cloneItem(subitem, ent, rr)
|
||||
c := cloneItem(subitem, ent, entityType, rr)
|
||||
|
||||
log.Infof("cloneItem: %+v", c)
|
||||
|
||||
|
|
@ -64,17 +66,26 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent
|
|||
return ret
|
||||
}
|
||||
|
||||
func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||
func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||
clone := &apiv1.DashboardComponent{}
|
||||
clone.CssClass = entities.ParseTemplateWith(subitem.CssClass, ent)
|
||||
|
||||
if subitem.Type == "" || subitem.Type == "link" {
|
||||
clone.Type = "link"
|
||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
||||
clone.Action = rr.findAction(subitem.Title)
|
||||
clone.Action = rr.findActionForEntity(subitem.Title, ent)
|
||||
} else {
|
||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
||||
clone.Type = subitem.Type
|
||||
|
||||
if clone.Type == "directory" && ent != nil && entityType != "" {
|
||||
clone.EntityType = entityType
|
||||
clone.EntityKey = ent.UniqueKey
|
||||
}
|
||||
|
||||
if len(subitem.Contents) > 0 {
|
||||
clone.Contents = buildEntityFieldsetContents(subitem.Contents, ent, entityType, rr)
|
||||
}
|
||||
}
|
||||
|
||||
return clone
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
|
@ -17,6 +18,19 @@ func renderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.D
|
|||
return findAndRenderDashboard(rr, dashboardTitle)
|
||||
}
|
||||
|
||||
func getEntityFromRequest(rr *DashboardRenderRequest) *entities.Entity {
|
||||
if rr.EntityType == "" || rr.EntityKey == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
entityInstances := entities.GetEntityInstances(rr.EntityType)
|
||||
if entity, ok := entityInstances[rr.EntityKey]; ok {
|
||||
return entity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
|
||||
for _, dashboard := range rr.cfg.Dashboards {
|
||||
if dashboard.Title != dashboardTitle {
|
||||
|
|
@ -31,6 +45,35 @@ func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *
|
|||
return buildDashboardFromConfig(dashboard, rr)
|
||||
}
|
||||
|
||||
directoryComponent := findDirectoryComponent(rr, dashboardTitle)
|
||||
if directoryComponent != nil {
|
||||
entity := getEntityFromRequest(rr)
|
||||
return buildDashboardFromConfigWithEntity(directoryComponent, rr, entity)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent {
|
||||
for _, dashboard := range rr.cfg.Dashboards {
|
||||
if component := searchDirectoryInComponent(dashboard, title); component != nil {
|
||||
return component
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchDirectoryInComponent(component *config.DashboardComponent, title string) *config.DashboardComponent {
|
||||
if component.Title == title && len(component.Contents) > 0 && component.Type != "fieldset" {
|
||||
return component
|
||||
}
|
||||
|
||||
for _, subitem := range component.Contents {
|
||||
if found := searchDirectoryInComponent(subitem, title); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -42,9 +85,13 @@ func logEmptyDashboard(dashboardTitle, username string) {
|
|||
}
|
||||
|
||||
func buildDashboardFromConfig(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
|
||||
return buildDashboardFromConfigWithEntity(dashboard, rr, nil)
|
||||
}
|
||||
|
||||
func buildDashboardFromConfigWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.Dashboard {
|
||||
return &apiv1.Dashboard{
|
||||
Title: dashboard.Title,
|
||||
Contents: sortActions(removeNulls(getDashboardComponentContents(dashboard, rr))),
|
||||
Contents: sortActions(removeNulls(getDashboardComponentContentsWithEntity(dashboard, rr, entity))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,11 +166,15 @@ func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardCompo
|
|||
}
|
||||
|
||||
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()
|
||||
|
||||
for _, subitem := range dashboard.Contents {
|
||||
processDashboardSubitem(subitem, rr, &ret, rootFieldset)
|
||||
processDashboardSubitemWithEntity(subitem, rr, &ret, rootFieldset, entity)
|
||||
}
|
||||
|
||||
return appendRootFieldsetIfNeeded(ret, rootFieldset)
|
||||
|
|
@ -138,15 +189,19 @@ 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, buildDashboardComponentSimple(subitem, rr))
|
||||
rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimpleWithEntity(subitem, rr, entity))
|
||||
return
|
||||
}
|
||||
|
||||
if subitem.Entity != "" {
|
||||
*ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...)
|
||||
} else {
|
||||
*ret = append(*ret, buildDashboardComponentSimple(subitem, rr))
|
||||
*ret = append(*ret, buildDashboardComponentSimpleWithEntity(subitem, rr, entity))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,13 +213,31 @@ func appendRootFieldsetIfNeeded(ret []*apiv1.DashboardComponent, rootFieldset *a
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
if len(subitem.Contents) > 0 {
|
||||
contents = getDashboardComponentContentsWithEntity(subitem, rr, entity)
|
||||
}
|
||||
|
||||
action := rr.findActionForEntity(subitem.Title, entity)
|
||||
componentType := getDashboardComponentType(subitem, action)
|
||||
|
||||
title := subitem.Title
|
||||
if entity != nil {
|
||||
title = entities.ParseTemplateWith(subitem.Title, entity)
|
||||
}
|
||||
|
||||
newitem := &apiv1.DashboardComponent{
|
||||
Title: subitem.Title,
|
||||
Type: getDashboardComponentType(subitem),
|
||||
Contents: getDashboardComponentContents(subitem, rr),
|
||||
Title: title,
|
||||
Type: componentType,
|
||||
Contents: contents,
|
||||
Icon: getDashboardComponentIcon(subitem, rr.cfg),
|
||||
CssClass: subitem.CssClass,
|
||||
Action: rr.findAction(subitem.Title),
|
||||
Action: action,
|
||||
}
|
||||
|
||||
return newitem
|
||||
|
|
@ -178,7 +251,7 @@ func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Conf
|
|||
return item.Icon
|
||||
}
|
||||
|
||||
func getDashboardComponentType(item *config.DashboardComponent) string {
|
||||
func getDashboardComponentType(item *config.DashboardComponent, action *apiv1.Action) string {
|
||||
allowedTypes := []string{
|
||||
"stdout-most-recent-execution",
|
||||
"display",
|
||||
|
|
@ -194,5 +267,9 @@ func getDashboardComponentType(item *config.DashboardComponent) string {
|
|||
return item.Type
|
||||
}
|
||||
|
||||
if action == nil {
|
||||
return "display"
|
||||
}
|
||||
|
||||
return "link"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue