Next (#772)
This commit is contained in:
commit
d4b8743a57
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:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { createConnectTransport } from '@connectrpc/connect-web'
|
||||||
|
|
||||||
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
|
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp, h } from 'vue'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import router from './resources/vue/router.js'
|
import router from './resources/vue/router.js'
|
||||||
|
|
@ -91,12 +91,33 @@ function setupVue (i18nSettings) {
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupErrorDisplay (errorMessage) {
|
||||||
|
const ErrorApp = {
|
||||||
|
render() {
|
||||||
|
return h('section', { class: 'bad', style: 'padding: 2em; text-align: center; margin: 2em auto;' }, [
|
||||||
|
h('h2', 'OliveTin Init Failed'),
|
||||||
|
h('p', errorMessage),
|
||||||
|
h('p', 'Please check your browser console for more details.')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = createApp(ErrorApp)
|
||||||
|
app.mount('#app')
|
||||||
|
}
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
|
try {
|
||||||
const i18nSettings = await initClient()
|
const i18nSettings = await initClient()
|
||||||
|
|
||||||
initWebsocket()
|
initWebsocket()
|
||||||
|
|
||||||
setupVue(i18nSettings)
|
setupVue(i18nSettings)
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err.message || 'Failed to initialize. Please check your configuration and try again.'
|
||||||
|
console.error('Init failed:', err)
|
||||||
|
setupErrorDisplay(errorMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,16 @@
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.8.9",
|
"picocrank": "^1.9.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.4",
|
||||||
"vue-i18n": "^11.2.1",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^16.26.0",
|
"stylelint": "^16.26.1",
|
||||||
"stylelint-config-standard": "^39.0.1"
|
"stylelint-config-standard": "^39.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -281,6 +281,26 @@
|
||||||
"@csstools/css-tokenizer": "^3.0.4"
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||||
|
"version": "1.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.20.tgz",
|
||||||
|
"integrity": "sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@csstools/css-tokenizer": {
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
|
@ -490,13 +510,13 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "11.2.1",
|
"version": "11.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||||
"integrity": "sha512-2V1A4yaN9ElAnQ6ih3HHEc+jZ+sHV6BlQHjCsnIVlOotL5NCUgJElIxgUFiJs6zV4puoAq3hHuQIfWNp+J+8yQ==",
|
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/message-compiler": "11.2.1",
|
"@intlify/message-compiler": "11.2.2",
|
||||||
"@intlify/shared": "11.2.1"
|
"@intlify/shared": "11.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
|
|
@ -506,12 +526,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "11.2.1",
|
"version": "11.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||||
"integrity": "sha512-J2454D3Agg3Kvgaj14gxTleJU8/H06Sisz7C2BwiHF0/i5Soyfb5ySpwn8GCL6yscDbOGj6xM+lUe6gO6BFQyg==",
|
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "11.2.1",
|
"@intlify/shared": "11.2.2",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -522,9 +542,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "11.2.1",
|
"version": "11.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz",
|
||||||
"integrity": "sha512-O67LZM4dbfr70WCsZLW+g+pIXdgQ66laLVd/FicW7iYgP/RuH0X1FDGSh+Hr9Gou/8TeldUE6KmTGdLwX2ufIA==",
|
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
|
|
@ -4031,9 +4051,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picocrank": {
|
"node_modules/picocrank": {
|
||||||
"version": "1.8.9",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.8.9.tgz",
|
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.9.0.tgz",
|
||||||
"integrity": "sha512-5NcLEYy4BSPhZm0tY8l/DLxaQoaOuHaf43S0MUgsKyLEiUIn9WnZGsDXHTlGnqNk6VK4+HnHs2rZnt3i5gj7FQ==",
|
"integrity": "sha512-51Ej4F0tgUp3JxywmiMW/IR6orTjs0aXsWMAbMHqYQNmFxmsR3SgQY7J9dhTCq27BMP43KGtGdx9dcItz23JkQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hugeicons/core-free-icons": "^2.0.0",
|
"@hugeicons/core-free-icons": "^2.0.0",
|
||||||
|
|
@ -4990,9 +5010,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint": {
|
"node_modules/stylelint": {
|
||||||
"version": "16.26.0",
|
"version": "16.26.1",
|
||||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz",
|
||||||
"integrity": "sha512-Y/3AVBefrkqqapVYH3LBF5TSDZ1kw+0XpdKN2KchfuhMK6lQ85S4XOG4lIZLcrcS4PWBmvcY6eS2kCQFz0jukQ==",
|
"integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -5007,6 +5027,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-syntax-patches-for-csstree": "^1.0.19",
|
||||||
"@csstools/css-tokenizer": "^3.0.4",
|
"@csstools/css-tokenizer": "^3.0.4",
|
||||||
"@csstools/media-query-list-parser": "^4.0.3",
|
"@csstools/media-query-list-parser": "^4.0.3",
|
||||||
"@csstools/selector-specificity": "^5.0.0",
|
"@csstools/selector-specificity": "^5.0.0",
|
||||||
|
|
@ -5019,7 +5040,7 @@
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"file-entry-cache": "^11.1.0",
|
"file-entry-cache": "^11.1.1",
|
||||||
"global-modules": "^2.0.0",
|
"global-modules": "^2.0.0",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"globjoin": "^0.1.4",
|
"globjoin": "^0.1.4",
|
||||||
|
|
@ -5669,13 +5690,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "11.2.1",
|
"version": "11.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||||
"integrity": "sha512-cc3Wx4eJZac9WMS8mxhfYiCipm9PBQ2Dz15piWYm7DwNcCehaKRgpolEdiqrjjT27T3Wijz3xJ7NeIc8ofIWAA==",
|
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.2.1",
|
"@intlify/core-base": "11.2.2",
|
||||||
"@intlify/shared": "11.2.1",
|
"@intlify/shared": "11.2.2",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"source": "index.html",
|
"source": "index.html",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^16.26.0",
|
"stylelint": "^16.26.1",
|
||||||
"stylelint-config-standard": "^39.0.1"
|
"stylelint-config-standard": "^39.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -30,11 +30,11 @@
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.8.9",
|
"picocrank": "^1.9.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.4",
|
||||||
"vue-i18n": "^11.2.1",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Header title="OliveTin" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="showNavigation">
|
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="showNavigation">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
||||||
<p>{{ bannerMessage }}</p>
|
<p>{{ bannerMessage }}</p>
|
||||||
|
|
@ -95,6 +95,7 @@ const username = ref('notset');
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false);
|
||||||
const serverConnection = ref(true);
|
const serverConnection = ref(true);
|
||||||
const currentVersion = ref('?');
|
const currentVersion = ref('?');
|
||||||
|
const pageTitle = ref('OliveTin');
|
||||||
const bannerMessage = ref('');
|
const bannerMessage = ref('');
|
||||||
const bannerCss = ref('');
|
const bannerCss = ref('');
|
||||||
const hasLoaded = ref(false);
|
const hasLoaded = ref(false);
|
||||||
|
|
@ -168,6 +169,7 @@ function updateHeaderFromInit() {
|
||||||
username.value = window.initResponse.authenticatedUser
|
username.value = window.initResponse.authenticatedUser
|
||||||
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
|
||||||
currentVersion.value = window.initResponse.currentVersion
|
currentVersion.value = window.initResponse.currentVersion
|
||||||
|
pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
|
||||||
bannerMessage.value = window.initResponse.bannerMessage || ''
|
bannerMessage.value = window.initResponse.bannerMessage || ''
|
||||||
bannerCss.value = window.initResponse.bannerCss || ''
|
bannerCss.value = window.initResponse.bannerCss || ''
|
||||||
showFooter.value = window.initResponse.showFooter
|
showFooter.value = window.initResponse.showFooter
|
||||||
|
|
|
||||||
|
|
@ -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,16 +115,24 @@ 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')
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard.value = ret.dashboard
|
dashboard.value = ret.dashboard
|
||||||
document.title = ret.dashboard.title + ' - OliveTin'
|
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||||
|
document.title = ret.dashboard.title + ' - ' + pageTitle
|
||||||
|
|
||||||
// Clear any previous init error since we successfully loaded
|
// Clear any previous init error since we successfully loaded
|
||||||
initError.value = null
|
initError.value = null
|
||||||
|
|
@ -84,7 +149,8 @@ async function getDashboard() {
|
||||||
// On error, provide a safe fallback state
|
// On error, provide a safe fallback state
|
||||||
console.error('Failed to load dashboard', e)
|
console.error('Failed to load dashboard', e)
|
||||||
dashboard.value = { title: title || 'Default', contents: [] }
|
dashboard.value = { title: title || 'Default', contents: [] }
|
||||||
document.title = 'Error - OliveTin'
|
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||||
|
document.title = 'Error - ' + pageTitle
|
||||||
|
|
||||||
// Stop the loading timer on error
|
// Stop the loading timer on error
|
||||||
if (loadingTimer) {
|
if (loadingTimer) {
|
||||||
|
|
@ -164,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);
|
||||||
|
|
@ -181,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,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<div :class = "statusClass + ' annotation'">
|
||||||
<span :class="['action-status', statusClass]">{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
<span>{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -35,11 +35,14 @@ const statusText = computed(() => {
|
||||||
const exitCodeText = computed(() => {
|
const exitCodeText = computed(() => {
|
||||||
const logEntry = props.logEntry
|
const logEntry = props.logEntry
|
||||||
if (!logEntry) return ''
|
if (!logEntry) return ''
|
||||||
|
if (logEntry.exitCode === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
if (logEntry.executionFinished) {
|
if (logEntry.executionFinished) {
|
||||||
if (logEntry.blocked || logEntry.timedOut) {
|
if (logEntry.blocked || logEntry.timedOut) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return ' Exit code: ' + logEntry.exitCode
|
return ' (Exit code: ' + logEntry.exitCode + ')'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
@ -49,15 +52,35 @@ const statusClass = computed(() => {
|
||||||
if (!logEntry) return ''
|
if (!logEntry) return ''
|
||||||
if (logEntry.executionFinished) {
|
if (logEntry.executionFinished) {
|
||||||
if (logEntry.blocked) {
|
if (logEntry.blocked) {
|
||||||
return 'action-blocked'
|
return 'status-blocked'
|
||||||
} else if (logEntry.timedOut) {
|
} else if (logEntry.timedOut) {
|
||||||
return 'action-timeout'
|
return 'status-timeout'
|
||||||
} else if (logEntry.exitCode === 0) {
|
} else if (logEntry.exitCode === 0) {
|
||||||
return 'action-success'
|
return 'status-success'
|
||||||
} else {
|
} else {
|
||||||
return 'action-nonzero-exit'
|
return 'status-nonzero-exit'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-success {
|
||||||
|
color: var(--karma-good-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-nonzero-exit {
|
||||||
|
color: var(--karma-bad-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-timeout {
|
||||||
|
color: var(--karma-warning-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-blocked {
|
||||||
|
color: #ca79ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :key="component.title" />
|
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :key="component.title" />
|
||||||
|
|
||||||
<div v-else-if="component.type == 'directory'">
|
<DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
|
||||||
<router-link :to="{ name: 'Dashboard', params: { title: component.title } }" class="dashboard-link">
|
|
||||||
<button>
|
|
||||||
{{ component.title }}
|
|
||||||
</button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="component.type == 'display'" class="display">
|
<DashboardComponentDisplay v-else-if="component.type == 'display'" :component="component" />
|
||||||
<div v-html="component.title" />
|
|
||||||
</div>
|
<DashboardComponentMostRecentExecution v-else-if="component.type == 'stdout-most-recent-execution'" :component="component" />
|
||||||
|
|
||||||
<template v-else-if="component.type == 'fieldset'">
|
<template v-else-if="component.type == 'fieldset'">
|
||||||
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
||||||
|
|
@ -28,6 +22,9 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ActionButton from '../ActionButton.vue'
|
import ActionButton from '../ActionButton.vue'
|
||||||
|
import DashboardComponentMostRecentExecution from './DashboardComponentMostRecentExecution.vue'
|
||||||
|
import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
|
||||||
|
import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<button @click="navigateToDirectory">
|
||||||
|
{{ component.title }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
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>
|
||||||
|
.folder-container {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
box-shadow: 0 0 .6em #aaa;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: .7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
button {
|
||||||
|
box-shadow: 0 0 .6em #000;
|
||||||
|
background-color: #111;
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #222;
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div class="display">
|
||||||
|
<div v-html="component.title" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.display {
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: .7em;
|
||||||
|
box-shadow: 0 0 .6em #aaa;
|
||||||
|
text-align: center;
|
||||||
|
font-size: small;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.display {
|
||||||
|
border-color: #000;
|
||||||
|
box-shadow: 0 0 .6em #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<div class="mre-container">
|
||||||
|
<router-link
|
||||||
|
v-if="executionTrackingId"
|
||||||
|
:to="`/logs/${executionTrackingId}`"
|
||||||
|
class="mre-link"
|
||||||
|
>
|
||||||
|
<pre class="mre-output">{{ output }}</pre>
|
||||||
|
</router-link>
|
||||||
|
<pre v-else class="mre-output fg-important">{{ output }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const output = ref('Waiting...')
|
||||||
|
const executionTrackingId = ref(null)
|
||||||
|
let eventListener = null
|
||||||
|
|
||||||
|
async function fetchMostRecentExecution() {
|
||||||
|
if (!props.component.title) {
|
||||||
|
output.value = 'Error: No action ID specified'
|
||||||
|
executionTrackingId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.client) {
|
||||||
|
output.value = 'Error: Client not initialized'
|
||||||
|
executionTrackingId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const executionStatusArgs = {
|
||||||
|
actionId: props.component.title
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.client.executionStatus(executionStatusArgs)
|
||||||
|
|
||||||
|
if (result.logEntry) {
|
||||||
|
if (result.logEntry.output !== undefined) {
|
||||||
|
output.value = result.logEntry.output
|
||||||
|
} else {
|
||||||
|
output.value = 'No output available'
|
||||||
|
}
|
||||||
|
if (result.logEntry.executionTrackingId) {
|
||||||
|
executionTrackingId.value = result.logEntry.executionTrackingId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.value = 'No output available'
|
||||||
|
executionTrackingId.value = null
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'NotFound' || err.status === 404) {
|
||||||
|
output.value = 'No execution found'
|
||||||
|
executionTrackingId.value = null
|
||||||
|
} else {
|
||||||
|
output.value = 'Error: ' + (err.message || 'Failed to fetch execution')
|
||||||
|
console.error('Failed to fetch most recent execution:', err)
|
||||||
|
executionTrackingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExecutionFinished(event) {
|
||||||
|
// The dashboard component "title" field is used for lots of things
|
||||||
|
// and in this context for MreOutput it's just to refer to an actionId.
|
||||||
|
//
|
||||||
|
// So this is not a typo.
|
||||||
|
const logEntry = event.payload.logEntry
|
||||||
|
if (logEntry && logEntry.actionId === props.component.title) {
|
||||||
|
if (logEntry.output !== undefined) {
|
||||||
|
output.value = logEntry.output
|
||||||
|
}
|
||||||
|
if (logEntry.executionTrackingId) {
|
||||||
|
executionTrackingId.value = logEntry.executionTrackingId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchMostRecentExecution()
|
||||||
|
|
||||||
|
eventListener = (event) => handleExecutionFinished(event)
|
||||||
|
window.addEventListener('EventExecutionFinished', eventListener)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (eventListener) {
|
||||||
|
window.removeEventListener('EventExecutionFinished', eventListener)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mre-container {
|
||||||
|
display: grid;
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mre-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: grid;
|
||||||
|
cursor: pointer;
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mre-link:hover .mre-output {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mre-output {
|
||||||
|
box-shadow: 0 0 .6em #aaa;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: .7em;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.mre-output {
|
||||||
|
border: 1px dashed #444;
|
||||||
|
box-shadow: 0 0 .6em #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mre-link:hover .mre-output {
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -128,7 +128,8 @@ const router = createRouter({
|
||||||
// Navigation guard to update page title
|
// Navigation guard to update page title
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (to.meta && to.meta.title) {
|
if (to.meta && to.meta.title) {
|
||||||
document.title = to.meta.title + " - OliveTin"
|
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
|
||||||
|
document.title = to.meta.title + " - " + pageTitle
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,62 @@
|
||||||
<template>
|
<template>
|
||||||
<Section title = "Get support">
|
<Section :title="t('diagnostics.get-support')">
|
||||||
<p>If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page.
|
<p>{{ t('diagnostics.get-support-description') }}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://docs.olivetin.app/sosreport.html" target="_blank">sosreport Documentation</a>
|
<a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">{{ t('diagnostics.where-to-find-help') }}</a>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">Where to find help</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title = "SSH">
|
<Section :title="t('diagnostics.ssh')">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Found Key</dt>
|
<dt>{{ t('diagnostics.found-key') }}</dt>
|
||||||
<dd>{{ diagnostics.sshFoundKey || '?' }}</dd>
|
<dd>{{ diagnostics.sshFoundKey || '?' }}</dd>
|
||||||
<dt>Found Config</dt>
|
<dt>{{ t('diagnostics.found-config') }}</dt>
|
||||||
<dd>{{ diagnostics.sshFoundConfig || '?' }}</dd>
|
<dd>{{ diagnostics.sshFoundConfig || '?' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title = "SOS Report">
|
<Section :title="t('diagnostics.sos-report')">
|
||||||
<p>This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.</p>
|
<p>{{ t('diagnostics.sos-report-description') }}</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://docs.olivetin.app/troubleshooting/sosreport.html" target="_blank">{{ t('diagnostics.sos-report-docs') }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div role="toolbar">
|
<div role="toolbar">
|
||||||
<button @click="generateSosReport" :disabled="loading" class = "good">Generate SOS Report</button>
|
<button @click="generateSosReport" :disabled="loading" class = "good">{{ t('diagnostics.generate-sos-report') }}</button>
|
||||||
|
<button @click="copySosReport" :disabled="!sosReport || loading" :class="sosReportCopied ? 'good' : ''">{{ sosReportCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea v-model="sosReport" readonly style="flex: 1; min-height: 200px; resize: vertical;"></textarea>
|
<textarea v-model="sosReport" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section :title="t('diagnostics.browser-info')">
|
||||||
|
<p>{{ t('diagnostics.browser-info-description') }}</p>
|
||||||
|
|
||||||
|
<div role="toolbar">
|
||||||
|
<button @click="generateBrowserInfo" :disabled="loading" class = "good">{{ t('diagnostics.generate-browser-info') }}</button>
|
||||||
|
<button @click="copyBrowserInfo" :disabled="!browserInfo || loading" :class="browserInfoCopied ? 'good' : ''">{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea v-model="browserInfo" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
|
||||||
</Section>
|
</Section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
const diagnostics = ref({})
|
const diagnostics = ref({})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const sosReport = ref('Waiting to start...')
|
const sosReport = ref('')
|
||||||
|
const browserInfo = ref('')
|
||||||
|
const sosReportCopied = ref(false)
|
||||||
|
const browserInfoCopied = ref(false)
|
||||||
|
|
||||||
async function fetchDiagnostics() {
|
async function fetchDiagnostics() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
@ -52,8 +70,8 @@ async function fetchDiagnostics() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch diagnostics:', err);
|
console.error('Failed to fetch diagnostics:', err);
|
||||||
diagnostics.value = {
|
diagnostics.value = {
|
||||||
sshFoundKey: 'Unknown',
|
sshFoundKey: t('diagnostics.unknown'),
|
||||||
sshFoundConfig: 'Unknown'
|
sshFoundConfig: t('diagnostics.unknown')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
@ -69,7 +87,134 @@ function formatKey(key) {
|
||||||
async function generateSosReport() {
|
async function generateSosReport() {
|
||||||
const response = await window.client.sosReport()
|
const response = await window.client.sosReport()
|
||||||
console.log("response", response)
|
console.log("response", response)
|
||||||
sosReport.value = response.alert
|
sosReport.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copySosReport() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(sosReport.value)
|
||||||
|
sosReportCopied.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
sosReportCopied.value = false
|
||||||
|
}, 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy SOS report to clipboard:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateBrowserInfo() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
let userAgentData = 'N/A'
|
||||||
|
if (navigator.userAgentData) {
|
||||||
|
try {
|
||||||
|
const uaData = await navigator.userAgentData.getHighEntropyValues([
|
||||||
|
'platform',
|
||||||
|
'platformVersion',
|
||||||
|
'architecture',
|
||||||
|
'model',
|
||||||
|
'uaFullVersion',
|
||||||
|
'bitness',
|
||||||
|
'fullVersionList'
|
||||||
|
])
|
||||||
|
userAgentData = JSON.stringify(uaData, null, 2)
|
||||||
|
} catch (err) {
|
||||||
|
userAgentData = `${t('diagnostics.useragent-data-error')}: ${err.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
platform: navigator.platform,
|
||||||
|
language: navigator.language,
|
||||||
|
languages: navigator.languages?.join(', ') || 'N/A',
|
||||||
|
cookieEnabled: navigator.cookieEnabled,
|
||||||
|
onLine: navigator.onLine,
|
||||||
|
screenWidth: screen.width,
|
||||||
|
screenHeight: screen.height,
|
||||||
|
screenColorDepth: screen.colorDepth,
|
||||||
|
screenPixelDepth: screen.pixelDepth,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
viewportHeight: window.innerHeight,
|
||||||
|
devicePixelRatio: window.devicePixelRatio || 'N/A',
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
timezoneOffset: new Date().getTimezoneOffset(),
|
||||||
|
localStorageEnabled: (() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('test', 'test')
|
||||||
|
localStorage.removeItem('test')
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
sessionStorageEnabled: (() => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('test', 'test')
|
||||||
|
sessionStorage.removeItem('test')
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
|
||||||
|
maxTouchPoints: navigator.maxTouchPoints || 'N/A',
|
||||||
|
userAgentData: userAgentData
|
||||||
|
}
|
||||||
|
|
||||||
|
const olivetinVersion = window.initResponse?.currentVersion || t('diagnostics.unknown')
|
||||||
|
const currentLanguage = locale.value || t('diagnostics.unknown')
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
output += `\`\`\`\n`
|
||||||
|
output += '### BROWSER INFO START (copy all text to BROWSER INFO END)\n'
|
||||||
|
output += `# OliveTin Information\n`
|
||||||
|
output += `olivetinVersion: ${olivetinVersion}\n`
|
||||||
|
output += `currentLanguage: ${currentLanguage}\n`
|
||||||
|
output += `\n# Browser Information\n`
|
||||||
|
output += `userAgent: ${info.userAgent}\n`
|
||||||
|
output += `platform: ${info.platform}\n`
|
||||||
|
output += `language: ${info.language}\n`
|
||||||
|
output += `languages: ${info.languages}\n`
|
||||||
|
output += `\n# User Agent Data\n`
|
||||||
|
output += `userAgentData:\n${info.userAgentData}\n`
|
||||||
|
output += `\n# Display Information\n`
|
||||||
|
output += `screenWidth: ${info.screenWidth}\n`
|
||||||
|
output += `screenHeight: ${info.screenHeight}\n`
|
||||||
|
output += `screenColorDepth: ${info.screenColorDepth}\n`
|
||||||
|
output += `screenPixelDepth: ${info.screenPixelDepth}\n`
|
||||||
|
output += `viewportWidth: ${info.viewportWidth}\n`
|
||||||
|
output += `viewportHeight: ${info.viewportHeight}\n`
|
||||||
|
output += `devicePixelRatio: ${info.devicePixelRatio}\n`
|
||||||
|
output += `\n# Feature Support\n`
|
||||||
|
output += `cookieEnabled: ${info.cookieEnabled}\n`
|
||||||
|
output += `localStorageEnabled: ${info.localStorageEnabled}\n`
|
||||||
|
output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
|
||||||
|
output += `onLine: ${info.onLine}\n`
|
||||||
|
output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
|
||||||
|
output += `maxTouchPoints: ${info.maxTouchPoints}\n`
|
||||||
|
output += `\n# Location & Time\n`
|
||||||
|
output += `timezone: ${info.timezone}\n`
|
||||||
|
output += `timezoneOffset: ${info.timezoneOffset}\n`
|
||||||
|
output += `\n### BROWSER INFO END (copy all text from BROWSER INFO START)`
|
||||||
|
output += `\n\`\`\`\n`
|
||||||
|
|
||||||
|
browserInfo.value = output
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyBrowserInfo() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(browserInfo.value)
|
||||||
|
browserInfoCopied.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
browserInfoCopied.value = false
|
||||||
|
}, 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy browser info to clipboard:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="exit-code">
|
<td class="exit-code">
|
||||||
<span :class="getStatusClass(log) + ' annotation'">
|
<ActionStatusDisplay :logEntry="log" />
|
||||||
{{ getStatusText(log) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -70,6 +68,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||||
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
import Pagination from 'picocrank/vue/components/Pagination.vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||||
|
|
||||||
const logs = ref([])
|
const logs = ref([])
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
|
|
@ -134,20 +133,6 @@ function formatTimestamp(timestamp) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(log) {
|
|
||||||
if (log.timedOut) return 'status-timeout'
|
|
||||||
if (log.blocked) return 'status-blocked'
|
|
||||||
if (log.exitCode !== 0) return 'status-error'
|
|
||||||
return 'status-success'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusText(log) {
|
|
||||||
if (log.timedOut) return t('logs.timed-out')
|
|
||||||
if (log.blocked) return t('logs.blocked')
|
|
||||||
if (log.exitCode !== 0) return `${t('logs.exit-code')} ${log.exitCode}`
|
|
||||||
return t('logs.completed')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageChange(page) {
|
function handlePageChange(page) {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
fetchLogs()
|
fetchLogs()
|
||||||
|
|
@ -227,22 +212,6 @@ onMounted(() => {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-success {
|
|
||||||
color: var(--karma-good-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-error {
|
|
||||||
color: var(--karma-bad-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-timeout {
|
|
||||||
color: var(--karma-warning-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-blocked {
|
|
||||||
color: var(--karma-neutral-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,6 @@ section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display {
|
|
||||||
border: 1px solid #666;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: .7em;
|
|
||||||
box-shadow: 0 0 .6em #aaa;
|
|
||||||
text-align: center;
|
|
||||||
font-size: small;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside .flex-row {
|
aside .flex-row {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
padding-right: .5em;
|
padding-right: .5em;
|
||||||
|
|
|
||||||
|
|
@ -1158,9 +1158,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#
|
||||||
|
# Integration Test Config: pageTitle
|
||||||
|
#
|
||||||
|
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
pageTitle: "Custom Test Title"
|
||||||
|
|
||||||
|
actions:
|
||||||
|
- title: Ping example.com
|
||||||
|
shell: ping example.com -c 1
|
||||||
|
icon: ping
|
||||||
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { describe, it, before, after } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: pageTitle', function () {
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('pageTitle')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Init API returns custom pageTitle from config', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
// Check that the Init API response (available via window.initResponse) contains pageTitle
|
||||||
|
// This is how the frontend accesses it, so it's the most reliable way to test
|
||||||
|
const initResponse = await webdriver.executeScript('return window.initResponse')
|
||||||
|
|
||||||
|
expect(initResponse).to.not.be.null
|
||||||
|
expect(initResponse).to.have.own.property('pageTitle')
|
||||||
|
expect(initResponse.pageTitle).to.equal('Custom Test Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Header displays custom pageTitle from init response', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
// Check that the pageTitle from init response is used in the header
|
||||||
|
// First verify the init response has the correct pageTitle
|
||||||
|
const pageTitle = await webdriver.executeScript('return window.initResponse?.pageTitle')
|
||||||
|
expect(pageTitle).to.equal('Custom Test Title')
|
||||||
|
|
||||||
|
// The Header component from picocrank should render the title prop
|
||||||
|
// Check for the title in the header element
|
||||||
|
const header = await webdriver.findElement(By.tagName('header'))
|
||||||
|
const headerText = await header.getText()
|
||||||
|
|
||||||
|
// The header should contain the custom page title
|
||||||
|
expect(headerText).to.include('Custom Test Title')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -44,6 +44,6 @@ describe('config: sleep', function () {
|
||||||
|
|
||||||
await killButton.click()
|
await killButton.click()
|
||||||
|
|
||||||
await requireExecutionDialogStatus(webdriver, "Completed Exit code: -1")
|
await requireExecutionDialogStatus(webdriver, "Completed (Exit code: -1)")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
logLevel: debug
|
||||||
|
|
||||||
|
actions:
|
||||||
|
- title: Check status
|
||||||
|
id: status_command
|
||||||
|
shell: |
|
||||||
|
date
|
||||||
|
|
||||||
|
icon: poop
|
||||||
|
|
||||||
|
dashboards:
|
||||||
|
- title: Test Dashboard
|
||||||
|
contents:
|
||||||
|
- title: Status Section
|
||||||
|
type: fieldset
|
||||||
|
contents:
|
||||||
|
- type: stdout-most-recent-execution
|
||||||
|
title: status_command
|
||||||
|
|
||||||
|
- title: Check status
|
||||||
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { describe, it, before, after } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By, Condition } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
getActionButtons,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: stdout-most-recent-execution', function () {
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('stdoutMostRecentExecution')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stdout-most-recent-execution component is rendered', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
const title = await webdriver.getTitle()
|
||||||
|
expect(title).to.be.equal('Test Dashboard - OliveTin')
|
||||||
|
|
||||||
|
// Wait for the mre-output element to appear
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for mre-output element', async () => {
|
||||||
|
const elements = await webdriver.findElements(By.css('.mre-output'))
|
||||||
|
return elements.length > 0
|
||||||
|
}),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
const mreElements = await webdriver.findElements(By.css('.mre-output'))
|
||||||
|
expect(mreElements).to.have.length(1, 'Expected one stdout-most-recent-execution component')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stdout-most-recent-execution displays initial state', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for mre-output element', async () => {
|
||||||
|
const elements = await webdriver.findElements(By.css('.mre-output'))
|
||||||
|
return elements.length > 0
|
||||||
|
}),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
const mreElement = await webdriver.findElement(By.css('.mre-output'))
|
||||||
|
const text = await mreElement.getText()
|
||||||
|
|
||||||
|
// Should show either "Waiting...", "No execution found", or actual output
|
||||||
|
expect(text).to.be.a('string')
|
||||||
|
expect(text.length).to.be.greaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stdout-most-recent-execution updates after action execution', async function () {
|
||||||
|
this.timeout(30000) // Increase timeout for this test
|
||||||
|
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
// Wait for the mre-output element
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for mre-output element', async () => {
|
||||||
|
const elements = await webdriver.findElements(By.css('.mre-output'))
|
||||||
|
return elements.length > 0
|
||||||
|
}),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
const mreElement = await webdriver.findElement(By.css('.mre-output'))
|
||||||
|
const initialText = await mreElement.getText()
|
||||||
|
|
||||||
|
// Find the "Check status" action button (button text is the action title, not ID)
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for Check status button', async () => {
|
||||||
|
const buttons = await webdriver.findElements(By.css('.action-button button'))
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await btn.getText()
|
||||||
|
if (text.includes('Check status')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
const buttons = await webdriver.findElements(By.css('.action-button button'))
|
||||||
|
let statusButton = null
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await btn.getText()
|
||||||
|
if (text.includes('Check status')) {
|
||||||
|
statusButton = btn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(statusButton).to.not.be.null
|
||||||
|
|
||||||
|
// Click the button to execute the action
|
||||||
|
await statusButton.click()
|
||||||
|
|
||||||
|
// Wait a moment for the action to start
|
||||||
|
await webdriver.sleep(500)
|
||||||
|
|
||||||
|
// Wait for the output to update (the component listens to EventExecutionFinished events)
|
||||||
|
// We'll wait for the output to change from the initial state
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for output to update after execution', async () => {
|
||||||
|
try {
|
||||||
|
const mreElement = await webdriver.findElement(By.css('.mre-output'))
|
||||||
|
const newText = await mreElement.getText()
|
||||||
|
// Output should change from initial state and contain actual output
|
||||||
|
// (not "Waiting...", "No execution found", or the same as initialText)
|
||||||
|
const hasChanged = newText !== initialText
|
||||||
|
const hasValidOutput = newText &&
|
||||||
|
!newText.includes('Waiting...') &&
|
||||||
|
!newText.includes('No execution found') &&
|
||||||
|
!newText.includes('Error:') &&
|
||||||
|
newText.trim().length > 0
|
||||||
|
return hasChanged && hasValidOutput
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
20000
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedMreElement = await webdriver.findElement(By.css('.mre-output'))
|
||||||
|
const updatedText = await updatedMreElement.getText()
|
||||||
|
|
||||||
|
// The date command should produce output, so verify it's not empty and not an error state
|
||||||
|
expect(updatedText).to.not.include('Waiting...')
|
||||||
|
expect(updatedText).to.not.include('No execution found')
|
||||||
|
expect(updatedText.trim().length).to.be.greaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -3,6 +3,23 @@
|
||||||
"messages": {
|
"messages": {
|
||||||
"de-DE": {
|
"de-DE": {
|
||||||
"connected": "Verbunden",
|
"connected": "Verbunden",
|
||||||
|
"diagnostics.browser-info": "Browser-Informationen",
|
||||||
|
"diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.",
|
||||||
|
"diagnostics.copied": "Kopiert!",
|
||||||
|
"diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
|
||||||
|
"diagnostics.found-config": "Konfiguration gefunden",
|
||||||
|
"diagnostics.found-key": "Schlüssel gefunden",
|
||||||
|
"diagnostics.generate-browser-info": "Browser-Informationen erstellen",
|
||||||
|
"diagnostics.generate-sos-report": "SOS-Bericht erstellen",
|
||||||
|
"diagnostics.get-support": "Unterstützung erhalten",
|
||||||
|
"diagnostics.get-support-description": "Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, einen sosreport von dieser Seite einzufügen.",
|
||||||
|
"diagnostics.sos-report": "SOS-Bericht",
|
||||||
|
"diagnostics.sos-report-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Konfiguration und Umgebung zu erstellen. Es ist eine gute Idee, dies bei einer Support-Anfrage einzufügen.",
|
||||||
|
"diagnostics.sos-report-docs": "sosreport Dokumentation",
|
||||||
|
"diagnostics.ssh": "SSH",
|
||||||
|
"diagnostics.unknown": "Unbekannt",
|
||||||
|
"diagnostics.useragent-data-error": "Fehler beim Abrufen von userAgentData",
|
||||||
|
"diagnostics.where-to-find-help": "Wo Sie Hilfe finden",
|
||||||
"docs": "Dokumentation",
|
"docs": "Dokumentation",
|
||||||
"language-dialog.browser-languages": "Browser-Sprachen",
|
"language-dialog.browser-languages": "Browser-Sprachen",
|
||||||
"language-dialog.close": "Schließen",
|
"language-dialog.close": "Schließen",
|
||||||
|
|
@ -32,6 +49,23 @@
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
"diagnostics.browser-info": "Browser Info",
|
||||||
|
"diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.",
|
||||||
|
"diagnostics.copied": "Copied!",
|
||||||
|
"diagnostics.copy-to-clipboard": "Copy to Clipboard",
|
||||||
|
"diagnostics.found-config": "Found Config",
|
||||||
|
"diagnostics.found-key": "Found Key",
|
||||||
|
"diagnostics.generate-browser-info": "Generate Browser Info",
|
||||||
|
"diagnostics.generate-sos-report": "Generate SOS Report",
|
||||||
|
"diagnostics.get-support": "Get support",
|
||||||
|
"diagnostics.get-support-description": "If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page.",
|
||||||
|
"diagnostics.sos-report": "SOS Report",
|
||||||
|
"diagnostics.sos-report-description": "This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.",
|
||||||
|
"diagnostics.sos-report-docs": "sosreport Documentation",
|
||||||
|
"diagnostics.ssh": "SSH",
|
||||||
|
"diagnostics.unknown": "Unknown",
|
||||||
|
"diagnostics.useragent-data-error": "Error retrieving userAgentData",
|
||||||
|
"diagnostics.where-to-find-help": "Where to find help",
|
||||||
"docs": "Documentation",
|
"docs": "Documentation",
|
||||||
"language-dialog.browser-languages": "Browser languages",
|
"language-dialog.browser-languages": "Browser languages",
|
||||||
"language-dialog.close": "Close",
|
"language-dialog.close": "Close",
|
||||||
|
|
@ -61,6 +95,23 @@
|
||||||
},
|
},
|
||||||
"es-ES": {
|
"es-ES": {
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"diagnostics.browser-info": "Información del navegador",
|
||||||
|
"diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.",
|
||||||
|
"diagnostics.copied": "¡Copiado!",
|
||||||
|
"diagnostics.copy-to-clipboard": "Copiar al portapapeles",
|
||||||
|
"diagnostics.found-config": "Configuración encontrada",
|
||||||
|
"diagnostics.found-key": "Clave encontrada",
|
||||||
|
"diagnostics.generate-browser-info": "Generar información del navegador",
|
||||||
|
"diagnostics.generate-sos-report": "Generar informe SOS",
|
||||||
|
"diagnostics.get-support": "Obtener soporte",
|
||||||
|
"diagnostics.get-support-description": "Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir un sosreport de esta página.",
|
||||||
|
"diagnostics.sos-report": "Informe SOS",
|
||||||
|
"diagnostics.sos-report-description": "Esta sección le permite generar un informe detallado de su configuración y entorno. Es una buena idea incluir esto al presentar una solicitud de soporte.",
|
||||||
|
"diagnostics.sos-report-docs": "Documentación de sosreport",
|
||||||
|
"diagnostics.ssh": "SSH",
|
||||||
|
"diagnostics.unknown": "Desconocido",
|
||||||
|
"diagnostics.useragent-data-error": "Error al recuperar userAgentData",
|
||||||
|
"diagnostics.where-to-find-help": "Dónde encontrar ayuda",
|
||||||
"docs": "Documentación",
|
"docs": "Documentación",
|
||||||
"language-dialog.browser-languages": "Idiomas del navegador",
|
"language-dialog.browser-languages": "Idiomas del navegador",
|
||||||
"language-dialog.close": "Cerrar",
|
"language-dialog.close": "Cerrar",
|
||||||
|
|
@ -90,6 +141,23 @@
|
||||||
},
|
},
|
||||||
"it-IT": {
|
"it-IT": {
|
||||||
"connected": "Connesso",
|
"connected": "Connesso",
|
||||||
|
"diagnostics.browser-info": "Informazioni del browser",
|
||||||
|
"diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.",
|
||||||
|
"diagnostics.copied": "Copiato!",
|
||||||
|
"diagnostics.copy-to-clipboard": "Copia negli appunti",
|
||||||
|
"diagnostics.found-config": "Configurazione trovata",
|
||||||
|
"diagnostics.found-key": "Chiave trovata",
|
||||||
|
"diagnostics.generate-browser-info": "Genera informazioni del browser",
|
||||||
|
"diagnostics.generate-sos-report": "Genera rapporto SOS",
|
||||||
|
"diagnostics.get-support": "Ottenere supporto",
|
||||||
|
"diagnostics.get-support-description": "Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere un sosreport da questa pagina.",
|
||||||
|
"diagnostics.sos-report": "Rapporto SOS",
|
||||||
|
"diagnostics.sos-report-description": "Questa sezione ti consente di generare un rapporto dettagliato della tua configurazione e ambiente. È una buona idea includere questo quando si presenta una richiesta di supporto.",
|
||||||
|
"diagnostics.sos-report-docs": "Documentazione sosreport",
|
||||||
|
"diagnostics.ssh": "SSH",
|
||||||
|
"diagnostics.unknown": "Sconosciuto",
|
||||||
|
"diagnostics.useragent-data-error": "Errore nel recupero di userAgentData",
|
||||||
|
"diagnostics.where-to-find-help": "Dove trovare aiuto",
|
||||||
"docs": "Documentazione",
|
"docs": "Documentazione",
|
||||||
"language-dialog.browser-languages": "Lingue del browser",
|
"language-dialog.browser-languages": "Lingue del browser",
|
||||||
"language-dialog.close": "Chiudi",
|
"language-dialog.close": "Chiudi",
|
||||||
|
|
@ -119,6 +187,23 @@
|
||||||
},
|
},
|
||||||
"zh-Hans-CN": {
|
"zh-Hans-CN": {
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
|
"diagnostics.browser-info": "浏览器信息",
|
||||||
|
"diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。",
|
||||||
|
"diagnostics.copied": "已复制!",
|
||||||
|
"diagnostics.copy-to-clipboard": "复制到剪贴板",
|
||||||
|
"diagnostics.found-config": "找到配置",
|
||||||
|
"diagnostics.found-key": "找到密钥",
|
||||||
|
"diagnostics.generate-browser-info": "生成浏览器信息",
|
||||||
|
"diagnostics.generate-sos-report": "生成 SOS 报告",
|
||||||
|
"diagnostics.get-support": "获取支持",
|
||||||
|
"diagnostics.get-support-description": "如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含 sosreport 将非常有帮助。",
|
||||||
|
"diagnostics.sos-report": "SOS 报告",
|
||||||
|
"diagnostics.sos-report-description": "此部分允许您生成配置和环境的详细报告。在提交支持请求时包含此信息是个好主意。",
|
||||||
|
"diagnostics.sos-report-docs": "sosreport 文档",
|
||||||
|
"diagnostics.ssh": "SSH",
|
||||||
|
"diagnostics.unknown": "未知",
|
||||||
|
"diagnostics.useragent-data-error": "检索 userAgentData 时出错",
|
||||||
|
"diagnostics.where-to-find-help": "在哪里找到帮助",
|
||||||
"docs": "文档",
|
"docs": "文档",
|
||||||
"language-dialog.browser-languages": "浏览器语言",
|
"language-dialog.browser-languages": "浏览器语言",
|
||||||
"language-dialog.close": "关闭",
|
"language-dialog.close": "关闭",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,23 @@ translations:
|
||||||
logs.exit-code: Ausführungscode
|
logs.exit-code: Ausführungscode
|
||||||
logs.completed: Abgeschlossen
|
logs.completed: Abgeschlossen
|
||||||
logs.clear-filter: Suchfilter löschen
|
logs.clear-filter: Suchfilter löschen
|
||||||
|
diagnostics.get-support: Unterstützung erhalten
|
||||||
|
diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, einen sosreport von dieser Seite einzufügen.
|
||||||
|
diagnostics.where-to-find-help: Wo Sie Hilfe finden
|
||||||
|
diagnostics.ssh: SSH
|
||||||
|
diagnostics.found-key: Schlüssel gefunden
|
||||||
|
diagnostics.found-config: Konfiguration gefunden
|
||||||
|
diagnostics.sos-report: SOS-Bericht
|
||||||
|
diagnostics.sos-report-description: Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Konfiguration und Umgebung zu erstellen. Es ist eine gute Idee, dies bei einer Support-Anfrage einzufügen.
|
||||||
|
diagnostics.sos-report-docs: sosreport Dokumentation
|
||||||
|
diagnostics.generate-sos-report: SOS-Bericht erstellen
|
||||||
|
diagnostics.browser-info: Browser-Informationen
|
||||||
|
diagnostics.browser-info-description: Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.
|
||||||
|
diagnostics.generate-browser-info: Browser-Informationen erstellen
|
||||||
|
diagnostics.copy-to-clipboard: In Zwischenablage kopieren
|
||||||
|
diagnostics.copied: Kopiert!
|
||||||
|
diagnostics.unknown: Unbekannt
|
||||||
|
diagnostics.useragent-data-error: Fehler beim Abrufen von userAgentData
|
||||||
return-to-index: Zurück zur Startseite
|
return-to-index: Zurück zur Startseite
|
||||||
search-filter: Filter aktuelle Seite
|
search-filter: Filter aktuelle Seite
|
||||||
language-dialog.title: Sprache auswählen
|
language-dialog.title: Sprache auswählen
|
||||||
|
|
|
||||||
17
lang/en.yaml
17
lang/en.yaml
|
|
@ -21,6 +21,23 @@ translations:
|
||||||
logs.exit-code: Exit code
|
logs.exit-code: Exit code
|
||||||
logs.completed: Completed
|
logs.completed: Completed
|
||||||
logs.clear-filter: Clear search filter
|
logs.clear-filter: Clear search filter
|
||||||
|
diagnostics.get-support: Get support
|
||||||
|
diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page.
|
||||||
|
diagnostics.where-to-find-help: Where to find help
|
||||||
|
diagnostics.ssh: SSH
|
||||||
|
diagnostics.found-key: Found Key
|
||||||
|
diagnostics.found-config: Found Config
|
||||||
|
diagnostics.sos-report: SOS Report
|
||||||
|
diagnostics.sos-report-description: This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.
|
||||||
|
diagnostics.sos-report-docs: sosreport Documentation
|
||||||
|
diagnostics.generate-sos-report: Generate SOS Report
|
||||||
|
diagnostics.browser-info: Browser Info
|
||||||
|
diagnostics.browser-info-description: This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.
|
||||||
|
diagnostics.generate-browser-info: Generate Browser Info
|
||||||
|
diagnostics.copy-to-clipboard: Copy to Clipboard
|
||||||
|
diagnostics.copied: Copied!
|
||||||
|
diagnostics.unknown: Unknown
|
||||||
|
diagnostics.useragent-data-error: Error retrieving userAgentData
|
||||||
return-to-index: Return to index
|
return-to-index: Return to index
|
||||||
search-filter: Filter current page
|
search-filter: Filter current page
|
||||||
language-dialog.title: Select Language
|
language-dialog.title: Select Language
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,23 @@ translations:
|
||||||
logs.exit-code: Código de salida
|
logs.exit-code: Código de salida
|
||||||
logs.completed: Completado
|
logs.completed: Completado
|
||||||
logs.clear-filter: Limpiar filtro de búsqueda
|
logs.clear-filter: Limpiar filtro de búsqueda
|
||||||
|
diagnostics.get-support: Obtener soporte
|
||||||
|
diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir un sosreport de esta página.
|
||||||
|
diagnostics.where-to-find-help: Dónde encontrar ayuda
|
||||||
|
diagnostics.ssh: SSH
|
||||||
|
diagnostics.found-key: Clave encontrada
|
||||||
|
diagnostics.found-config: Configuración encontrada
|
||||||
|
diagnostics.sos-report: Informe SOS
|
||||||
|
diagnostics.sos-report-description: Esta sección le permite generar un informe detallado de su configuración y entorno. Es una buena idea incluir esto al presentar una solicitud de soporte.
|
||||||
|
diagnostics.sos-report-docs: Documentación de sosreport
|
||||||
|
diagnostics.generate-sos-report: Generar informe SOS
|
||||||
|
diagnostics.browser-info: Información del navegador
|
||||||
|
diagnostics.browser-info-description: Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.
|
||||||
|
diagnostics.generate-browser-info: Generar información del navegador
|
||||||
|
diagnostics.copy-to-clipboard: Copiar al portapapeles
|
||||||
|
diagnostics.copied: ¡Copiado!
|
||||||
|
diagnostics.unknown: Desconocido
|
||||||
|
diagnostics.useragent-data-error: Error al recuperar userAgentData
|
||||||
return-to-index: Volver a la página principal
|
return-to-index: Volver a la página principal
|
||||||
search-filter: Filtrar página actual
|
search-filter: Filtrar página actual
|
||||||
language-dialog.title: Seleccionar idioma
|
language-dialog.title: Seleccionar idioma
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,23 @@ translations:
|
||||||
logs.exit-code: Codice di uscita
|
logs.exit-code: Codice di uscita
|
||||||
logs.completed: Completato
|
logs.completed: Completato
|
||||||
logs.clear-filter: Cancella filtro di ricerca
|
logs.clear-filter: Cancella filtro di ricerca
|
||||||
|
diagnostics.get-support: Ottenere supporto
|
||||||
|
diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere un sosreport da questa pagina.
|
||||||
|
diagnostics.where-to-find-help: Dove trovare aiuto
|
||||||
|
diagnostics.ssh: SSH
|
||||||
|
diagnostics.found-key: Chiave trovata
|
||||||
|
diagnostics.found-config: Configurazione trovata
|
||||||
|
diagnostics.sos-report: Rapporto SOS
|
||||||
|
diagnostics.sos-report-description: Questa sezione ti consente di generare un rapporto dettagliato della tua configurazione e ambiente. È una buona idea includere questo quando si presenta una richiesta di supporto.
|
||||||
|
diagnostics.sos-report-docs: Documentazione sosreport
|
||||||
|
diagnostics.generate-sos-report: Genera rapporto SOS
|
||||||
|
diagnostics.browser-info: Informazioni del browser
|
||||||
|
diagnostics.browser-info-description: Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.
|
||||||
|
diagnostics.generate-browser-info: Genera informazioni del browser
|
||||||
|
diagnostics.copy-to-clipboard: Copia negli appunti
|
||||||
|
diagnostics.copied: Copiato!
|
||||||
|
diagnostics.unknown: Sconosciuto
|
||||||
|
diagnostics.useragent-data-error: Errore nel recupero di userAgentData
|
||||||
return-to-index: Torna alla pagina principale
|
return-to-index: Torna alla pagina principale
|
||||||
search-filter: Filtra la pagina corrente
|
search-filter: Filtra la pagina corrente
|
||||||
language-dialog.title: Seleziona lingua
|
language-dialog.title: Seleziona lingua
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,20 @@ translations:
|
||||||
logs.exit-code: 退出代码
|
logs.exit-code: 退出代码
|
||||||
logs.completed: 完成
|
logs.completed: 完成
|
||||||
logs.clear-filter: 清除搜索筛选器
|
logs.clear-filter: 清除搜索筛选器
|
||||||
|
diagnostics.get-support: 获取支持
|
||||||
|
diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含 sosreport 将非常有帮助。
|
||||||
|
diagnostics.where-to-find-help: 在哪里找到帮助
|
||||||
|
diagnostics.ssh: SSH
|
||||||
|
diagnostics.found-key: 找到密钥
|
||||||
|
diagnostics.found-config: 找到配置
|
||||||
|
diagnostics.sos-report: SOS 报告
|
||||||
|
diagnostics.sos-report-description: 此部分允许您生成配置和环境的详细报告。在提交支持请求时包含此信息是个好主意。
|
||||||
|
diagnostics.sos-report-docs: sosreport 文档
|
||||||
|
diagnostics.generate-sos-report: 生成 SOS 报告
|
||||||
|
diagnostics.browser-info: 浏览器信息
|
||||||
|
diagnostics.browser-info-description: 此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。
|
||||||
|
diagnostics.generate-browser-info: 生成浏览器信息
|
||||||
|
diagnostics.copy-to-clipboard: 复制到剪贴板
|
||||||
|
diagnostics.copied: 已复制!
|
||||||
|
diagnostics.unknown: 未知
|
||||||
|
diagnostics.useragent-data-error: 检索 userAgentData 时出错
|
||||||
|
|
@ -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" +
|
||||||
|
|
|
||||||
|
|
@ -363,15 +363,25 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou
|
||||||
|
|
||||||
response := connect.NewResponse(&apiv1.LogoutResponse{})
|
response := connect.NewResponse(&apiv1.LogoutResponse{})
|
||||||
|
|
||||||
// Clear the authentication cookie by setting it to expire
|
// Clear the local authentication cookie by setting it to expire
|
||||||
cookie := &http.Cookie{
|
localCookie := &http.Cookie{
|
||||||
Name: "olivetin-sid-local",
|
Name: "olivetin-sid-local",
|
||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1, // This tells the browser to delete the cookie
|
MaxAge: -1, // This tells the browser to delete the cookie
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
}
|
}
|
||||||
response.Header().Set("Set-Cookie", cookie.String())
|
response.Header().Set("Set-Cookie", localCookie.String())
|
||||||
|
|
||||||
|
// Clear the OAuth2 authentication cookie by setting it to expire
|
||||||
|
oauth2Cookie := &http.Cookie{
|
||||||
|
Name: "olivetin-sid-oauth",
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1, // This tells the browser to delete the cookie
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
response.Header().Add("Set-Cookie", oauth2Cookie.String())
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
@ -405,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)
|
||||||
|
|
@ -421,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -825,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)
|
||||||
|
|
@ -977,6 +995,40 @@ 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 {
|
||||||
|
collectDirectoriesFromComponent(component, directories)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(component.Contents) > 0 {
|
||||||
|
searchSubcomponentsForDirectories(entityType, component.Contents, directories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectDirectoriesFromComponent(component *config.DashboardComponent, directories *[]string) {
|
||||||
|
for _, subitem := range component.Contents {
|
||||||
|
if subitem.Type == "directory" {
|
||||||
|
*directories = append(*directories, subitem.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchSubcomponentsForDirectories(entityType string, contents []*config.DashboardComponent, directories *[]string) {
|
||||||
|
for _, subitem := range 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)
|
||||||
|
|
||||||
|
|
@ -998,6 +1050,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,14 +13,24 @@ 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 matchesEntity(binding, entity) {
|
||||||
return buildAction(binding, rr)
|
return buildAction(binding, rr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +38,14 @@ func (rr *DashboardRenderRequest) findAction(title string) *apiv1.Action {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matchesEntity(binding *executor.ActionBinding, entity *entities.Entity) bool {
|
||||||
|
if entity == nil {
|
||||||
|
return binding.Entity == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.Entity != nil && binding.Entity.UniqueKey == entity.UniqueKey
|
||||||
|
}
|
||||||
|
|
||||||
func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
|
func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
|
||||||
ret := &apiv1.EffectivePolicy{
|
ret := &apiv1.EffectivePolicy{
|
||||||
ShowDiagnostics: policy.ShowDiagnostics,
|
ShowDiagnostics: policy.ShowDiagnostics,
|
||||||
|
|
|
||||||
|
|
@ -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,18 +66,44 @@ 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 isLinkType(subitem.Type) {
|
||||||
|
return cloneLinkItem(subitem, ent, clone, rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloneNonLinkItem(subitem, ent, entityType, clone, rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLinkType(itemType string) bool {
|
||||||
|
return itemType == "" || itemType == "link"
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||||
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 {
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
||||||
clone.Type = subitem.Type
|
clone.Type = subitem.Type
|
||||||
|
|
||||||
|
if isDirectoryWithEntity(clone.Type, ent, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isDirectoryWithEntity(itemType string, ent *entities.Entity, entityType string) bool {
|
||||||
|
return itemType == "directory" && ent != nil && entityType != ""
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,20 +18,82 @@ func renderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.D
|
||||||
return findAndRenderDashboard(rr, dashboardTitle)
|
return findAndRenderDashboard(rr, dashboardTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
|
func getEntityFromRequest(rr *DashboardRenderRequest) *entities.Entity {
|
||||||
for _, dashboard := range rr.cfg.Dashboards {
|
if rr.EntityType == "" || rr.EntityKey == "" {
|
||||||
if dashboard.Title != dashboardTitle {
|
return nil
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entityInstances := entities.GetEntityInstances(rr.EntityType)
|
||||||
|
if entity, ok := entityInstances[rr.EntityKey]; ok {
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
|
||||||
|
if dashboard := findDashboardByTitle(rr, dashboardTitle); dashboard != nil {
|
||||||
|
return renderDashboardIfValid(dashboard, rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderDirectoryDashboard(rr, dashboardTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findDashboardByTitle(rr *DashboardRenderRequest, dashboardTitle string) *config.DashboardComponent {
|
||||||
|
for _, dashboard := range rr.cfg.Dashboards {
|
||||||
|
if dashboard.Title == dashboardTitle {
|
||||||
|
return dashboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDashboardIfValid(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard {
|
||||||
if len(dashboard.Contents) == 0 {
|
if len(dashboard.Contents) == 0 {
|
||||||
logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
|
logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildDashboardFromConfig(dashboard, rr)
|
return buildDashboardFromConfig(dashboard, rr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderDirectoryDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard {
|
||||||
|
directoryComponent := findDirectoryComponent(rr, dashboardTitle)
|
||||||
|
if directoryComponent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entity := getEntityFromRequest(rr)
|
||||||
|
return buildDashboardFromConfigWithEntity(directoryComponent, rr, entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isMatchingDirectory(component, title) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchDirectoryInSubcomponents(component.Contents, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMatchingDirectory(component *config.DashboardComponent, title string) bool {
|
||||||
|
return component.Title == title && len(component.Contents) > 0 && component.Type != "fieldset"
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchDirectoryInSubcomponents(contents []*config.DashboardComponent, title string) *config.DashboardComponent {
|
||||||
|
for _, subitem := range contents {
|
||||||
|
if found := searchDirectoryInComponent(subitem, title); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,9 +105,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 +186,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 +209,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 +233,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,21 +271,40 @@ 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 {
|
||||||
|
if hasContents(item) {
|
||||||
|
return getTypeForComponentWithContents(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAllowedType(item.Type) {
|
||||||
|
return item.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDefaultType(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasContents(item *config.DashboardComponent) bool {
|
||||||
|
return len(item.Contents) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTypeForComponentWithContents(item *config.DashboardComponent) string {
|
||||||
|
if item.Type != "fieldset" {
|
||||||
|
return "directory"
|
||||||
|
}
|
||||||
|
return "fieldset"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowedType(itemType string) bool {
|
||||||
allowedTypes := []string{
|
allowedTypes := []string{
|
||||||
"stdout-most-recent-execution",
|
"stdout-most-recent-execution",
|
||||||
"display",
|
"display",
|
||||||
}
|
}
|
||||||
|
return slices.Contains(allowedTypes, itemType)
|
||||||
if len(item.Contents) > 0 {
|
|
||||||
if item.Type != "fieldset" {
|
|
||||||
return "directory"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "fieldset"
|
func getDefaultType(action *apiv1.Action) string {
|
||||||
} else if slices.Contains(allowedTypes, item.Type) {
|
if action == nil {
|
||||||
return item.Type
|
return "display"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "link"
|
return "link"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue