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
|
- title: Ping hypervisor2
|
||||||
shell: echo "hypervisor2 online"
|
shell: echo "hypervisor2 online"
|
||||||
|
|
||||||
|
- title: Ping hypervisor3
|
||||||
|
shell: echo "hypervisor3 online"
|
||||||
|
|
||||||
|
- title: Ping hypervisor4
|
||||||
|
shell: echo "hypervisor4 online"
|
||||||
|
|
||||||
- title: "{{ server.name }} Wake on Lan"
|
- title: "{{ server.name }} Wake on Lan"
|
||||||
shell: echo "Sending Wake on LAN to {{ server.hostname }}"
|
shell: echo "Sending Wake on LAN to {{ server.hostname }}"
|
||||||
|
icon: <iconify-icon icon="carbon:awake"></iconify-icon>
|
||||||
entity: server
|
entity: server
|
||||||
|
|
||||||
- title: "{{ server.name }} Power Off"
|
- title: "{{ server.name }} Power Off"
|
||||||
shell: "echo 'Power Off Server: {{ server.hostname }}'"
|
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
|
entity: server
|
||||||
|
|
||||||
- title: Ping All Servers
|
- title: Ping All Servers
|
||||||
|
|
@ -278,6 +290,11 @@ dashboards:
|
||||||
contents:
|
contents:
|
||||||
- title: Ping hypervisor1
|
- title: Ping hypervisor1
|
||||||
- title: Ping hypervisor2
|
- 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
|
# If you specify `type: fieldset` and some `contents`, it will show your
|
||||||
# actions grouped together without a folder.
|
# actions grouped together without a folder.
|
||||||
|
|
@ -299,6 +316,11 @@ dashboards:
|
||||||
- title: '{{ server.name }} Wake on Lan'
|
- title: '{{ server.name }} Wake on Lan'
|
||||||
- title: '{{ server.name }} Power Off'
|
- title: '{{ server.name }} Power Off'
|
||||||
|
|
||||||
|
- title: More Options
|
||||||
|
type: directory
|
||||||
|
contents:
|
||||||
|
- title: '{{ server.name }} Print server name'
|
||||||
|
|
||||||
# This is the second dashboard.
|
# This is the second dashboard.
|
||||||
- title: My Containers
|
- title: My Containers
|
||||||
contents:
|
contents:
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,11 @@ export declare type Entity = Message<"olivetin.api.v1.Entity"> & {
|
||||||
* @generated from field: string type = 3;
|
* @generated from field: string type = 3;
|
||||||
*/
|
*/
|
||||||
type: string;
|
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;
|
* @generated from field: string title = 1;
|
||||||
*/
|
*/
|
||||||
title: string;
|
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;
|
* @generated from field: olivetin.api.v1.Action action = 6;
|
||||||
*/
|
*/
|
||||||
action?: Action;
|
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>
|
</section>
|
||||||
<template v-else-if="dashboard">
|
<template v-else-if="dashboard">
|
||||||
<section v-if="dashboard.contents.length == 0">
|
<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>
|
<h2>{{ dashboard.title }}</h2>
|
||||||
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
|
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="transparent" v-else>
|
<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">
|
<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>
|
<fieldset>
|
||||||
<template v-for="subcomponent in component.contents">
|
<template v-for="subcomponent in component.contents">
|
||||||
|
|
@ -31,23 +57,54 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import DashboardComponent from './components/DashboardComponent.vue'
|
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 { HugeiconsIcon } from '@hugeicons/vue'
|
||||||
import { Loading03Icon } from '@hugeicons/core-free-icons'
|
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
entityType: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
entityKey: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const dashboard = ref(null)
|
const dashboard = ref(null)
|
||||||
const loadingTime = ref(0)
|
const loadingTime = ref(0)
|
||||||
const initError = ref(null)
|
const initError = ref(null)
|
||||||
let loadingTimer = null
|
let loadingTimer = null
|
||||||
let checkInitInterval = 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() {
|
async function getDashboard() {
|
||||||
let title = props.title
|
let title = props.title
|
||||||
|
|
||||||
|
|
@ -58,9 +115,16 @@ async function getDashboard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ret = await window.client.getDashboard({
|
const request = {
|
||||||
title: title,
|
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) {
|
if (!ret || !ret.dashboard) {
|
||||||
throw new Error('No dashboard found')
|
throw new Error('No dashboard found')
|
||||||
|
|
@ -166,6 +230,17 @@ h2 {
|
||||||
grid-column: 1 / -1;
|
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 {
|
fieldset {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, 180px);
|
grid-template-columns: repeat(auto-fit, 180px);
|
||||||
|
|
@ -183,4 +258,41 @@ fieldset {
|
||||||
transform: rotate(360deg);
|
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>
|
</style>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="router.push({ name: 'Dashboard', params: { title: component.title } })">
|
<button @click="navigateToDirectory">
|
||||||
{{ component.title }}
|
{{ component.title }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -15,6 +15,17 @@ const props = defineProps({
|
||||||
required: true
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const routes = [
|
||||||
meta: { title: 'Actions', icon: DashboardSquare01Icon }
|
meta: { title: 'Actions', icon: DashboardSquare01Icon }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboards/:title',
|
path: '/dashboards/:title/:entityType?/:entityKey?',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
component: () => import('./Dashboard.vue'),
|
component: () => import('./Dashboard.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,62 @@
|
||||||
<template>
|
<template>
|
||||||
<Section title="Entity Details">
|
<Section title="Entity Details">
|
||||||
<div>
|
<template #toolbar>
|
||||||
<p v-if="!entityDetails">Loading entity details...</p>
|
<button @click="goBack" class="back-button">
|
||||||
<p v-else-if="!entityDetails.title">No details available for this entity.</p>
|
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
|
||||||
<p v-else>{{ entityDetails.title }}</p>
|
<span>Back</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div v-if="!entityDetails">
|
||||||
|
<p>Loading entity details...</p>
|
||||||
</div>
|
</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>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
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'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const entityDetails = ref(null)
|
const entityDetails = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -19,6 +64,10 @@
|
||||||
entityKey: String
|
entityKey: String
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: 'Entities' })
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchEntityDetails() {
|
async function fetchEntityDetails() {
|
||||||
try {
|
try {
|
||||||
const response = await window.client.getEntity({
|
const response = await window.client.getEntity({
|
||||||
|
|
@ -38,3 +87,77 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</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 title = 1;
|
||||||
string unique_key = 2;
|
string unique_key = 2;
|
||||||
string type = 3;
|
string type = 3;
|
||||||
|
repeated string directories = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetDashboardResponse {
|
message GetDashboardResponse {
|
||||||
|
|
@ -51,6 +52,8 @@ message EffectivePolicy {
|
||||||
|
|
||||||
message GetDashboardRequest {
|
message GetDashboardRequest {
|
||||||
string title = 1;
|
string title = 1;
|
||||||
|
string entity_type = 2;
|
||||||
|
string entity_key = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Dashboard {
|
message Dashboard {
|
||||||
|
|
@ -65,6 +68,8 @@ message DashboardComponent {
|
||||||
string icon = 4;
|
string icon = 4;
|
||||||
string css_class = 5;
|
string css_class = 5;
|
||||||
Action action = 6;
|
Action action = 6;
|
||||||
|
string entity_type = 7;
|
||||||
|
string entity_key = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartActionRequest {
|
message StartActionRequest {
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,7 @@ type Entity struct {
|
||||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
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"`
|
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"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -325,6 +326,13 @@ func (x *Entity) GetType() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Entity) GetDirectories() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Directories
|
||||||
|
}
|
||||||
|
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"`
|
||||||
|
|
@ -432,6 +440,8 @@ func (x *EffectivePolicy) GetShowLogList() bool {
|
||||||
type GetDashboardRequest struct {
|
type GetDashboardRequest 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"`
|
||||||
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -473,6 +483,20 @@ func (x *GetDashboardRequest) GetTitle() string {
|
||||||
return ""
|
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 {
|
type Dashboard 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"`
|
||||||
|
|
@ -533,6 +557,8 @@ type DashboardComponent struct {
|
||||||
Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
|
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"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -609,6 +635,20 @@ func (x *DashboardComponent) GetAction() *Action {
|
||||||
return nil
|
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 {
|
type StartActionRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"`
|
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" +
|
"\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\"Q\n" +
|
"\x05title\x18\x02 \x01(\tR\x05title\"s\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\"f\n" +
|
"\x04type\x18\x03 \x01(\tR\x04type\x12 \n" +
|
||||||
|
"\vdirectories\x18\x04 \x03(\tR\vdirectories\"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" +
|
||||||
"\x0fEffectivePolicy\x12)\n" +
|
"\x0fEffectivePolicy\x12)\n" +
|
||||||
"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\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" +
|
"\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" +
|
"\tDashboard\x12\x14\n" +
|
||||||
"\x05title\x18\x01 \x01(\tR\x05title\x12?\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" +
|
"\x12DashboardComponent\x12\x14\n" +
|
||||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
|
"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
|
||||||
"\x04type\x18\x02 \x01(\tR\x04type\x12?\n" +
|
"\x04type\x18\x02 \x01(\tR\x04type\x12?\n" +
|
||||||
"\bcontents\x18\x03 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\x12\x12\n" +
|
"\bcontents\x18\x03 \x03(\v2#.olivetin.api.v1.DashboardComponentR\bcontents\x12\x12\n" +
|
||||||
"\x04icon\x18\x04 \x01(\tR\x04icon\x12\x1b\n" +
|
"\x04icon\x18\x04 \x01(\tR\x04icon\x12\x1b\n" +
|
||||||
"\tcss_class\x18\x05 \x01(\tR\bcssClass\x12/\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" +
|
"\x12StartActionRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"binding_id\x18\x01 \x01(\tR\tbindingId\x12B\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
|
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) {
|
if api.isDefaultDashboard(req.Msg.Title) {
|
||||||
return api.buildDefaultDashboardResponse(dashboardRenderRequest)
|
return api.buildDefaultDashboardResponse(dashboardRenderRequest)
|
||||||
|
|
@ -431,11 +437,13 @@ func (api *oliveTinAPI) checkDashboardAccess(user *authpublic.AuthenticatedUser)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser) *DashboardRenderRequest {
|
func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser, entityType, entityKey string) *DashboardRenderRequest {
|
||||||
return &DashboardRenderRequest{
|
return &DashboardRenderRequest{
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
cfg: api.cfg,
|
cfg: api.cfg,
|
||||||
ex: api.executor,
|
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 {
|
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||||
var rootDashboards []string
|
var rootDashboards []string
|
||||||
dashboardRenderRequest := api.createDashboardRenderRequest(user)
|
dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
|
||||||
|
|
||||||
api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest)
|
api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest)
|
||||||
api.addCustomDashboards(&rootDashboards, dashboards, 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) {
|
func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
|
||||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
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))
|
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 {
|
} else {
|
||||||
res.Title = entity.Title
|
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
|
return connect.NewResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,32 @@ type DashboardRenderRequest struct {
|
||||||
AuthenticatedUser *authpublic.AuthenticatedUser
|
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
ex *executor.Executor
|
ex *executor.Executor
|
||||||
|
EntityType string
|
||||||
|
EntityKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
|
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()
|
rr.ex.MapActionIdToBindingLock.RLock()
|
||||||
defer rr.ex.MapActionIdToBindingLock.RUnlock()
|
defer rr.ex.MapActionIdToBindingLock.RUnlock()
|
||||||
|
|
||||||
for _, binding := range rr.ex.MapActionIdToBinding {
|
for _, binding := range rr.ex.MapActionIdToBinding {
|
||||||
if binding.Action.Title == title {
|
if binding.Action.Title != title {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity == nil {
|
||||||
|
if binding.Entity == nil {
|
||||||
return buildAction(binding, rr)
|
return buildAction(binding, rr)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if binding.Entity != nil && binding.Entity.UniqueKey == entity.UniqueKey {
|
||||||
|
return buildAction(binding, rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,11 @@ func buildEntityFieldset(tpl *config.DashboardComponent, ent *entities.Entity, r
|
||||||
return &apiv1.DashboardComponent{
|
return &apiv1.DashboardComponent{
|
||||||
Title: entities.ParseTemplateWith(tpl.Title, ent),
|
Title: entities.ParseTemplateWith(tpl.Title, ent),
|
||||||
Type: "fieldset",
|
Type: "fieldset",
|
||||||
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, rr)),
|
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, tpl.Entity, rr)),
|
||||||
CssClass: entities.ParseTemplateWith(tpl.CssClass, ent),
|
CssClass: entities.ParseTemplateWith(tpl.CssClass, ent),
|
||||||
Action: rr.findAction(tpl.Title),
|
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)
|
ret := make([]*apiv1.DashboardComponent, 0)
|
||||||
|
|
||||||
for _, subitem := range contents {
|
for _, subitem := range contents {
|
||||||
c := cloneItem(subitem, ent, rr)
|
c := cloneItem(subitem, ent, entityType, rr)
|
||||||
|
|
||||||
log.Infof("cloneItem: %+v", c)
|
log.Infof("cloneItem: %+v", c)
|
||||||
|
|
||||||
|
|
@ -64,17 +66,26 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent
|
||||||
return ret
|
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 := &apiv1.DashboardComponent{}
|
||||||
clone.CssClass = entities.ParseTemplateWith(subitem.CssClass, ent)
|
clone.CssClass = entities.ParseTemplateWith(subitem.CssClass, ent)
|
||||||
|
|
||||||
if subitem.Type == "" || subitem.Type == "link" {
|
if subitem.Type == "" || subitem.Type == "link" {
|
||||||
clone.Type = "link"
|
clone.Type = "link"
|
||||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
||||||
clone.Action = rr.findAction(subitem.Title)
|
clone.Action = rr.findActionForEntity(subitem.Title, ent)
|
||||||
} else {
|
} else {
|
||||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
||||||
clone.Type = subitem.Type
|
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
|
return clone
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
@ -17,6 +18,19 @@ func renderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.D
|
||||||
return findAndRenderDashboard(rr, dashboardTitle)
|
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 {
|
func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
|
||||||
for _, dashboard := range rr.cfg.Dashboards {
|
for _, dashboard := range rr.cfg.Dashboards {
|
||||||
if dashboard.Title != dashboardTitle {
|
if dashboard.Title != dashboardTitle {
|
||||||
|
|
@ -31,6 +45,35 @@ func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *
|
||||||
return buildDashboardFromConfig(dashboard, rr)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,9 +85,13 @@ func logEmptyDashboard(dashboardTitle, username string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDashboardFromConfig(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
|
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{
|
return &apiv1.Dashboard{
|
||||||
Title: dashboard.Title,
|
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 {
|
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)
|
ret := make([]*apiv1.DashboardComponent, 0)
|
||||||
rootFieldset := createRootFieldset()
|
rootFieldset := createRootFieldset()
|
||||||
|
|
||||||
for _, subitem := range dashboard.Contents {
|
for _, subitem := range dashboard.Contents {
|
||||||
processDashboardSubitem(subitem, rr, &ret, rootFieldset)
|
processDashboardSubitemWithEntity(subitem, rr, &ret, rootFieldset, entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
return appendRootFieldsetIfNeeded(ret, rootFieldset)
|
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) {
|
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" {
|
if subitem.Type != "fieldset" {
|
||||||
rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimple(subitem, rr))
|
rootFieldset.Contents = append(rootFieldset.Contents, buildDashboardComponentSimpleWithEntity(subitem, rr, entity))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if subitem.Entity != "" {
|
if subitem.Entity != "" {
|
||||||
*ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...)
|
*ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...)
|
||||||
} else {
|
} 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 {
|
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{
|
newitem := &apiv1.DashboardComponent{
|
||||||
Title: subitem.Title,
|
Title: title,
|
||||||
Type: getDashboardComponentType(subitem),
|
Type: componentType,
|
||||||
Contents: getDashboardComponentContents(subitem, rr),
|
Contents: contents,
|
||||||
Icon: getDashboardComponentIcon(subitem, rr.cfg),
|
Icon: getDashboardComponentIcon(subitem, rr.cfg),
|
||||||
CssClass: subitem.CssClass,
|
CssClass: subitem.CssClass,
|
||||||
Action: rr.findAction(subitem.Title),
|
Action: action,
|
||||||
}
|
}
|
||||||
|
|
||||||
return newitem
|
return newitem
|
||||||
|
|
@ -178,7 +251,7 @@ func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Conf
|
||||||
return item.Icon
|
return item.Icon
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDashboardComponentType(item *config.DashboardComponent) string {
|
func getDashboardComponentType(item *config.DashboardComponent, action *apiv1.Action) string {
|
||||||
allowedTypes := []string{
|
allowedTypes := []string{
|
||||||
"stdout-most-recent-execution",
|
"stdout-most-recent-execution",
|
||||||
"display",
|
"display",
|
||||||
|
|
@ -194,5 +267,9 @@ func getDashboardComponentType(item *config.DashboardComponent) string {
|
||||||
return item.Type
|
return item.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action == nil {
|
||||||
|
return "display"
|
||||||
|
}
|
||||||
|
|
||||||
return "link"
|
return "link"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue