parent
9d64b64bf9
commit
37cde0d982
|
|
@ -32,17 +32,19 @@ const unicodeIcon = computed(() => {
|
||||||
|
|
||||||
function navigateToDirectory() {
|
function navigateToDirectory() {
|
||||||
const params = { title: props.component.title }
|
const params = { title: props.component.title }
|
||||||
|
|
||||||
if (props.component.entityType && props.component.entityKey) {
|
if (props.component.entityType && props.component.entityKey) {
|
||||||
params.entityType = props.component.entityType
|
params.entityType = props.component.entityType
|
||||||
params.entityKey = props.component.entityKey
|
params.entityKey = props.component.entityKey
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push({ name: 'Dashboard', params })
|
router.push({ name: 'Dashboard', params })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
|
||||||
|
@layer components {
|
||||||
.folder-container {
|
.folder-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
@ -93,5 +95,6 @@ button .title {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
|
||||||
|
@layer components {
|
||||||
.display {
|
.display {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: .7em;
|
border-radius: .7em;
|
||||||
|
|
@ -33,5 +35,6 @@ const props = defineProps({
|
||||||
box-shadow: 0 0 .6em #000;
|
box-shadow: 0 0 .6em #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mre-container" :class="component.cssClass">
|
<div class="mre-container" :class="component.cssClass">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="executionTrackingId"
|
v-if="executionTrackingId"
|
||||||
:to="`/logs/${executionTrackingId}`"
|
:to="`/logs/${executionTrackingId}`"
|
||||||
class="mre-link"
|
class="mre-link"
|
||||||
>
|
>
|
||||||
<pre class="mre-output">{{ output }}</pre>
|
<pre class="mre-output">{{ output }}</pre>
|
||||||
|
|
@ -58,7 +58,7 @@ async function fetchMostRecentExecution() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await window.client.executionStatus(executionStatusArgs)
|
const result = await window.client.executionStatus(executionStatusArgs)
|
||||||
|
|
||||||
if (result.logEntry) {
|
if (result.logEntry) {
|
||||||
updateFromLogEntry(result.logEntry)
|
updateFromLogEntry(result.logEntry)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -79,7 +79,7 @@ async function fetchMostRecentExecution() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMostRecentExecution()
|
fetchMostRecentExecution()
|
||||||
|
|
||||||
unwatchButtonResults = watch(
|
unwatchButtonResults = watch(
|
||||||
buttonResults,
|
buttonResults,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -87,7 +87,7 @@ onMounted(() => {
|
||||||
const bindingId = props.component.title
|
const bindingId = props.component.title
|
||||||
let mostRecent = null
|
let mostRecent = null
|
||||||
let mostRecentTime = null
|
let mostRecentTime = null
|
||||||
|
|
||||||
for (const trackingId in buttonResults) {
|
for (const trackingId in buttonResults) {
|
||||||
const logEntry = buttonResults[trackingId]
|
const logEntry = buttonResults[trackingId]
|
||||||
if (logEntry && logEntry.bindingId === bindingId && logEntry.executionFinished) {
|
if (logEntry && logEntry.bindingId === bindingId && logEntry.executionFinished) {
|
||||||
|
|
@ -98,7 +98,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mostRecent) {
|
if (mostRecent) {
|
||||||
updateFromLogEntry(mostRecent)
|
updateFromLogEntry(mostRecent)
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,9 @@ onBeforeUnmount(() => {
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
|
||||||
|
@layer components {
|
||||||
.mre-container {
|
.mre-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
|
|
@ -154,10 +156,11 @@ onBeforeUnmount(() => {
|
||||||
border: 1px dashed #444;
|
border: 1px dashed #444;
|
||||||
box-shadow: 0 0 .6em #444;
|
box-shadow: 0 0 .6em #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mre-link:hover .mre-output {
|
.mre-link:hover .mre-output {
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,22 @@
|
||||||
<template v-if="entityDetails.fields">
|
<template v-if="entityDetails.fields">
|
||||||
<template v-for="(value, key) in entityDetails.fields" :key="key">
|
<template v-for="(value, key) in entityDetails.fields" :key="key">
|
||||||
<dt>{{ key }}</dt>
|
<dt>{{ key }}</dt>
|
||||||
<dd>{{ value }}</dd>
|
<dd v-html="value"></dd>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</dl>
|
</dl>
|
||||||
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
|
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h3>Dashboard Entity Directories</h3>
|
<h3>Dashboard Entity Directories</h3>
|
||||||
<div v-if="filteredDirectories.length > 0" class="directories-section">
|
<div v-if="filteredDirectories.length > 0" class="directories-section">
|
||||||
<ul class="directory-list">
|
<ul class="directory-list">
|
||||||
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
|
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
params: {
|
params: {
|
||||||
title: directory,
|
title: directory,
|
||||||
entityType: entityType,
|
entityType: entityType,
|
||||||
entityKey: entityKey
|
entityKey: entityKey
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,5 @@
|
||||||
recursive: true
|
recursive: true
|
||||||
require:
|
require:
|
||||||
- mochaSetup.mjs
|
- mochaSetup.mjs
|
||||||
|
ignore:
|
||||||
|
- 'tests/**/custom-webui/**'
|
||||||
|
|
|
||||||
|
|
@ -74,4 +74,15 @@ describe('config: cssClass', function () {
|
||||||
const classAttr = await displayElements[0].getAttribute('class')
|
const classAttr = await displayElements[0].getAttribute('class')
|
||||||
expect(classAttr).to.include('test-display-class')
|
expect(classAttr).to.include('test-display-class')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('custom theme applies background color to display component via cssClass', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
const displayElements = await webdriver.findElements(By.css('.display.test-display-class'))
|
||||||
|
expect(displayElements).to.have.length.at.least(1, 'Display with test-display-class should exist')
|
||||||
|
|
||||||
|
const bgColor = await displayElements[0].getCssValue('background-color')
|
||||||
|
expect(bgColor, 'Theme theme.css should set .display.test-display-class background to rgb(64, 128, 192)')
|
||||||
|
.to.match(/rgba?\(\s*64\s*,\s*128\s*,\s*192\s*(,\s*1)?\s*\)/)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,8 @@
|
||||||
.action-button button.test-custom-class {
|
.action-button button.test-custom-class {
|
||||||
background-color: rgb(32, 64, 128);
|
background-color: rgb(32, 64, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Display cssClass must be overridable by theme rules (#804) */
|
||||||
|
.display.test-display-class {
|
||||||
|
background-color: rgb(64, 128, 192);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#
|
||||||
|
# Integration Test Config: custom JavaScript (#803)
|
||||||
|
#
|
||||||
|
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
enableCustomJs: true
|
||||||
|
|
||||||
|
actions: []
|
||||||
|
|
||||||
|
dashboards:
|
||||||
|
- title: Custom JS Dashboard
|
||||||
|
contents: []
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
window.olivetinCustomJsLoaded = true
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { describe, it, before, after, afterEach } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: customJs', function () {
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('customJs')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads custom.js when enableCustomJs is true (#803)', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
await webdriver.wait(async () => {
|
||||||
|
return await webdriver.executeScript('return window.olivetinCustomJsLoaded === true')
|
||||||
|
}, 5000, 'custom.js should set window.olivetinCustomJsLoaded when enableCustomJs is enabled')
|
||||||
|
|
||||||
|
const loaded = await webdriver.executeScript('return window.olivetinCustomJsLoaded === true')
|
||||||
|
expect(loaded).to.equal(true)
|
||||||
|
|
||||||
|
const scripts = await webdriver.findElements(By.css('#olivetin-custom-js'))
|
||||||
|
expect(scripts).to.have.length(1, 'custom.js script tag should be injected into the page')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#
|
||||||
|
# Integration Test Config: entity HTML in display components (#804)
|
||||||
|
#
|
||||||
|
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
entities:
|
||||||
|
- file: entities/html_display.yaml
|
||||||
|
name: html_display
|
||||||
|
|
||||||
|
actions: []
|
||||||
|
|
||||||
|
dashboards:
|
||||||
|
- title: Html Display Dashboard
|
||||||
|
contents:
|
||||||
|
- title: Compare result
|
||||||
|
type: fieldset
|
||||||
|
entity: html_display
|
||||||
|
contents:
|
||||||
|
- type: display
|
||||||
|
cssClass: test-html-display
|
||||||
|
title: '{{ html_display.content }}'
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
- content: |
|
||||||
|
<div class="content">entity-html-test</div>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { describe, it, before, after, afterEach } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
waitForDashboardLoaded,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: entityHtmlDisplay', function () {
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('entityHtmlDisplay')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders entity HTML content inside display components (#804)', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
await webdriver.get(runner.baseUrl() + 'dashboards/Html%20Display%20Dashboard')
|
||||||
|
await waitForDashboardLoaded()
|
||||||
|
|
||||||
|
const contentDiv = await webdriver.findElements(By.css('.display.test-html-display .content'))
|
||||||
|
expect(contentDiv).to.have.length.at.least(1, 'Entity HTML should render inside the display component')
|
||||||
|
|
||||||
|
const text = await contentDiv[0].getText()
|
||||||
|
expect(text).to.equal('entity-html-test')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildEntityFieldsetDisplayRendersEntityHtmlTitleAndCssClass(t *testing.T) {
|
||||||
|
entities.ClearEntitiesOfType("html_display")
|
||||||
|
defer entities.ClearEntitiesOfType("html_display")
|
||||||
|
|
||||||
|
entities.AddEntity("html_display", "0", map[string]any{
|
||||||
|
"content": "<div class=\"content\">test</div>",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Dashboards = []*config.DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Stream status",
|
||||||
|
Contents: []*config.DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Compare result",
|
||||||
|
Type: "fieldset",
|
||||||
|
Entity: "html_display",
|
||||||
|
Contents: []*config.DashboardComponent{
|
||||||
|
{
|
||||||
|
Type: "display",
|
||||||
|
CssClass: "full_screen",
|
||||||
|
Title: "{{ html_display.content }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ex := executor.DefaultExecutor(cfg)
|
||||||
|
ex.RebuildActionMap()
|
||||||
|
|
||||||
|
rr := &DashboardRenderRequest{
|
||||||
|
cfg: cfg,
|
||||||
|
ex: ex,
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsets := buildEntityFieldsets("html_display", cfg.Dashboards[0].Contents[0], rr)
|
||||||
|
require.Len(t, fieldsets, 1)
|
||||||
|
|
||||||
|
display := findComponentByType(fieldsets[0].Contents, "display")
|
||||||
|
require.NotNil(t, display)
|
||||||
|
assert.Equal(t, "full_screen", display.CssClass)
|
||||||
|
assert.Equal(t, "<div class=\"content\">test</div>", display.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findComponentByType(components []*apiv1.DashboardComponent, componentType string) *apiv1.DashboardComponent {
|
||||||
|
for _, component := range components {
|
||||||
|
if component.Type == componentType {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
|
if found := findNestedComponent(component, componentType); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNestedComponent(component *apiv1.DashboardComponent, componentType string) *apiv1.DashboardComponent {
|
||||||
|
if len(component.Contents) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return findComponentByType(component.Contents, componentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDashboardEntityDisplayHtmlTitle(t *testing.T) {
|
||||||
|
entities.ClearEntitiesOfType("html_display")
|
||||||
|
defer entities.ClearEntitiesOfType("html_display")
|
||||||
|
|
||||||
|
entities.AddEntity("html_display", "0", map[string]any{
|
||||||
|
"content": "<div class=\"content\">test</div>",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Dashboards = []*config.DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Html Dashboard",
|
||||||
|
Contents: []*config.DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Compare result",
|
||||||
|
Type: "fieldset",
|
||||||
|
Entity: "html_display",
|
||||||
|
Contents: []*config.DashboardComponent{
|
||||||
|
{
|
||||||
|
Type: "display",
|
||||||
|
CssClass: "full_screen",
|
||||||
|
Title: "{{ html_display.content }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, client := getNewTestServerAndClient(cfg)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
resp, err := client.GetDashboard(context.Background(), connect.NewRequest(&apiv1.GetDashboardRequest{
|
||||||
|
Title: "Html Dashboard",
|
||||||
|
}))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
display := findComponentByType(resp.Msg.Dashboard.Contents, "display")
|
||||||
|
require.NotNil(t, display)
|
||||||
|
assert.Equal(t, "full_screen", display.CssClass)
|
||||||
|
assert.Equal(t, "<div class=\"content\">test</div>", display.Title)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue