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

View File

@ -42,7 +42,9 @@ function navigateToDirectory() {
} }
</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>

View File

@ -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>

View File

@ -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;
@ -159,5 +161,6 @@ onBeforeUnmount(() => {
border-color: #666; border-color: #666;
} }
} }
</style> }
</style>

View File

@ -22,7 +22,7 @@
<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>

View File

@ -2,3 +2,5 @@
recursive: true recursive: true
require: require:
- mochaSetup.mjs - mochaSetup.mjs
ignore:
- 'tests/**/custom-webui/**'

View File

@ -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*\)/)
})
}) })

View File

@ -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);
}

View File

@ -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: []

View File

@ -0,0 +1 @@
window.olivetinCustomJsLoaded = true

View File

@ -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')
})
})

View File

@ -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 }}'

View File

@ -0,0 +1,2 @@
- content: |
<div class="content">entity-html-test</div>

View File

@ -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')
})
})

View File

@ -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)
}