fix: custom CSS and custom JS (#804 and #803) hopefully

This commit is contained in:
jamesread
2026-06-16 11:56:14 +01:00
parent 9d64b64bf9
commit 37cde0d982
14 changed files with 289 additions and 23 deletions
@@ -32,17 +32,19 @@ const unicodeIcon = computed(() => {
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>
<style>
@layer components {
.folder-container {
display: grid;
}
@@ -93,5 +95,6 @@ button .title {
color: #fff;
}
}
}
</style>
</style>
@@ -13,7 +13,9 @@ const props = defineProps({
})
</script>
<style scoped>
<style>
@layer components {
.display {
padding: 1em;
border-radius: .7em;
@@ -33,5 +35,6 @@ const props = defineProps({
box-shadow: 0 0 .6em #000;
}
}
}
</style>
</style>
@@ -1,8 +1,8 @@
<template>
<div class="mre-container" :class="component.cssClass">
<router-link
v-if="executionTrackingId"
:to="`/logs/${executionTrackingId}`"
<div class="mre-container" :class="component.cssClass">
<router-link
v-if="executionTrackingId"
:to="`/logs/${executionTrackingId}`"
class="mre-link"
>
<pre class="mre-output">{{ output }}</pre>
@@ -58,7 +58,7 @@ async function fetchMostRecentExecution() {
}
const result = await window.client.executionStatus(executionStatusArgs)
if (result.logEntry) {
updateFromLogEntry(result.logEntry)
} else {
@@ -79,7 +79,7 @@ async function fetchMostRecentExecution() {
onMounted(() => {
fetchMostRecentExecution()
unwatchButtonResults = watch(
buttonResults,
() => {
@@ -87,7 +87,7 @@ onMounted(() => {
const bindingId = props.component.title
let mostRecent = null
let mostRecentTime = null
for (const trackingId in buttonResults) {
const logEntry = buttonResults[trackingId]
if (logEntry && logEntry.bindingId === bindingId && logEntry.executionFinished) {
@@ -98,7 +98,7 @@ onMounted(() => {
}
}
}
if (mostRecent) {
updateFromLogEntry(mostRecent)
}
@@ -114,7 +114,9 @@ onBeforeUnmount(() => {
})
</script>
<style scoped>
<style>
@layer components {
.mre-container {
display: grid;
grid-column: span 2;
@@ -154,10 +156,11 @@ onBeforeUnmount(() => {
border: 1px dashed #444;
box-shadow: 0 0 .6em #444;
}
.mre-link:hover .mre-output {
border-color: #666;
}
}
</style>
}
</style>
@@ -22,22 +22,22 @@
<template v-if="entityDetails.fields">
<template v-for="(value, key) in entityDetails.fields" :key="key">
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
<dd v-html="value"></dd>
</template>
</template>
</dl>
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
<hr />
<h3>Dashboard Entity Directories</h3>
<div v-if="filteredDirectories.length > 0" class="directories-section">
<ul class="directory-list">
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
<router-link
:to="{
name: 'Dashboard',
params: {
<router-link
:to="{
name: 'Dashboard',
params: {
title: directory,
entityType: entityType,
entityKey: entityKey
+2
View File
@@ -2,3 +2,5 @@
recursive: true
require:
- mochaSetup.mjs
ignore:
- 'tests/**/custom-webui/**'
@@ -74,4 +74,15 @@ describe('config: cssClass', function () {
const classAttr = await displayElements[0].getAttribute('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 {
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)
}