Merge branch 'next' of github.com:OliveTin/OliveTin into next

This commit is contained in:
jamesread 2025-12-01 00:24:13 +00:00
commit fe8ecf49eb
4 changed files with 314 additions and 0 deletions

View File

@ -13,6 +13,8 @@
<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-for="subcomponent in component.contents" :key="subcomponent.title">
<DashboardComponent :component="subcomponent" />
@ -28,6 +30,7 @@
<script setup>
import ActionButton from '../ActionButton.vue'
import DashboardComponentMostRecentExecution from './DashboardComponentMostRecentExecution.vue'
const props = defineProps({
component: {

View File

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

View File

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

View File

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