mirror of
https://github.com/OliveTin/OliveTin
synced 2026-09-25 19:25:42 +00:00
@@ -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,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)
|
||||
}
|
||||
Reference in New Issue
Block a user