Merge branch 'next' of github.com:OliveTin/OliveTin into next
This commit is contained in:
commit
fe8ecf49eb
|
|
@ -13,6 +13,8 @@
|
||||||
<div v-html="component.title" />
|
<div v-html="component.title" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DashboardComponentMostRecentExecution v-else-if="component.type == 'stdout-most-recent-execution'" :component="component" />
|
||||||
|
|
||||||
<template v-else-if="component.type == 'fieldset'">
|
<template v-else-if="component.type == 'fieldset'">
|
||||||
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
||||||
<DashboardComponent :component="subcomponent" />
|
<DashboardComponent :component="subcomponent" />
|
||||||
|
|
@ -28,6 +30,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ActionButton from '../ActionButton.vue'
|
import ActionButton from '../ActionButton.vue'
|
||||||
|
import DashboardComponentMostRecentExecution from './DashboardComponentMostRecentExecution.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue