Script execution list
This commit is contained in:
parent
13a5b9a973
commit
55724b36b5
8 changed files with 517 additions and 14 deletions
|
|
@ -226,7 +226,7 @@ func ScheduleHook(hookID int) (err error) { // {{{
|
|||
return
|
||||
}
|
||||
|
||||
scriptLogID, err := ScriptPreservedID(hook.Script.Source)
|
||||
scriptLogID, err := ScriptPreservedID(hook.Script.Name, hook.Script.Source)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
|
|
@ -260,7 +260,7 @@ func ScheduleHook(hookID int) (err error) { // {{{
|
|||
return
|
||||
} // }}}
|
||||
|
||||
func ScriptPreservedID(source string) (id int, err error) { // {{{
|
||||
func ScriptPreservedID(name, source string) (id int, err error) { // {{{
|
||||
sum := md5.Sum([]byte(source))
|
||||
md5sum := hex.EncodeToString(sum[:])
|
||||
|
||||
|
|
@ -274,7 +274,7 @@ func ScriptPreservedID(source string) (id int, err error) { // {{{
|
|||
return
|
||||
}
|
||||
|
||||
row = db.QueryRow(`INSERT INTO script_log(md5sum, source) VALUES($1, $2) RETURNING id`, md5sum, source)
|
||||
row = db.QueryRow(`INSERT INTO script_log(md5sum, name, source) VALUES($1, $2, $3) RETURNING id`, md5sum, name, source)
|
||||
err = row.Scan(&id)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
// External
|
||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Standard
|
||||
"bytes"
|
||||
|
|
@ -25,15 +26,30 @@ type ScriptExecution struct {
|
|||
ID int
|
||||
TimeStart sql.NullTime `db:"time_start"`
|
||||
TimeEnd sql.NullTime `db:"time_end"`
|
||||
Source []byte
|
||||
Data []byte
|
||||
ScriptName string `db:"script_name"`
|
||||
Source string
|
||||
Data string
|
||||
SSH string
|
||||
Env []byte
|
||||
Env string
|
||||
OutputStdout sql.NullString `db:"output_stdout"`
|
||||
OutputStderr sql.NullString `db:"output_stderr"`
|
||||
ExitCode sql.NullInt16
|
||||
}
|
||||
|
||||
type ScriptExecutionBrief struct {
|
||||
ID int
|
||||
TimeStart sql.NullTime `db:"time_start"`
|
||||
TimeEnd sql.NullTime `db:"time_end"`
|
||||
ScriptName string `db:"script_name"`
|
||||
SSH string
|
||||
ExitCode sql.NullInt16
|
||||
HasSource bool `db:"has_source"`
|
||||
HasData bool `db:"has_data"`
|
||||
HasEnv bool `db:"has_env"`
|
||||
HasOutputStdout bool `db:"has_output_stdout"`
|
||||
HasOutputStderr bool `db:"has_output_stderr"`
|
||||
}
|
||||
|
||||
func NewScriptScheduler() (sched ScriptScheduler) {
|
||||
sched.EventQueue = make(chan string, 64)
|
||||
return
|
||||
|
|
@ -200,7 +216,7 @@ func (se *ScriptExecution) SSHCommand(stdin []byte, log bool, args ...string) (s
|
|||
func (se *ScriptExecution) UploadScript() (fnames []string, err error) { // {{{
|
||||
var filenames string
|
||||
filenames, err = se.SSHCommand(
|
||||
se.Source,
|
||||
[]byte(se.Source),
|
||||
true,
|
||||
`sh -c 'RUNENV=$(mktemp -t datagraph.XXXXXX) && SCRIPT=$(mktemp -t datagraph.XXXXXX) && touch $RUNENV $SCRIPT && chmod 700 $RUNENV $SCRIPT && cat >$SCRIPT && echo $RUNENV $SCRIPT'`,
|
||||
)
|
||||
|
|
@ -219,7 +235,7 @@ func (se *ScriptExecution) UploadScript() (fnames []string, err error) { // {{{
|
|||
} // }}}
|
||||
func (se *ScriptExecution) UploadEnv(envFname, scriptFname string) (err error) { // {{{
|
||||
env := make(map[string]string)
|
||||
err = json.Unmarshal(se.Env, &env)
|
||||
err = json.Unmarshal([]byte(se.Env), &env)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
|
|
@ -243,9 +259,79 @@ func (se *ScriptExecution) UploadEnv(envFname, scriptFname string) (err error) {
|
|||
return
|
||||
} // }}}
|
||||
func (se *ScriptExecution) RunScript(fname string) (err error) { // {{{
|
||||
_, err = se.SSHCommand(se.Data, true, fname)
|
||||
_, err = se.SSHCommand([]byte(se.Data), true, fname)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
}
|
||||
return
|
||||
} // }}}
|
||||
|
||||
func GetScriptExecutions() (executions []ScriptExecutionBrief, err error) { // {{{
|
||||
executions = []ScriptExecutionBrief{}
|
||||
|
||||
var rows *sqlx.Rows
|
||||
rows, err = db.Queryx(`
|
||||
SELECT
|
||||
e.id,
|
||||
time_start,
|
||||
time_end,
|
||||
ssh,
|
||||
sl.name AS script_name,
|
||||
exitcode,
|
||||
LENGTH(source) > 0 AS has_source,
|
||||
LENGTH(data::varchar) > 0 AS has_data,
|
||||
LENGTH(env::varchar) > 0 AS has_env,
|
||||
LENGTH(output_stdout) > 0 AS has_output_stdout,
|
||||
LENGTH(output_stderr) > 0 AS has_output_stderr
|
||||
FROM execution e
|
||||
INNER JOIN script_log sl ON e.script_log_id = sl.id
|
||||
ORDER BY
|
||||
id DESC
|
||||
LIMIT 100
|
||||
`)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var execution ScriptExecutionBrief
|
||||
err = rows.StructScan(&execution)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
executions = append(executions, execution)
|
||||
}
|
||||
|
||||
return
|
||||
} // }}}
|
||||
func GetScriptExecution(id int) (e ScriptExecution, err error) {
|
||||
row := db.QueryRowx(`
|
||||
SELECT
|
||||
e.id,
|
||||
time_start,
|
||||
time_end,
|
||||
ssh,
|
||||
sl.name AS script_name,
|
||||
sl.source,
|
||||
exitcode,
|
||||
data,
|
||||
env,
|
||||
output_stdout,
|
||||
output_stderr
|
||||
FROM execution e
|
||||
INNER JOIN script_log sl ON e.script_log_id = sl.id
|
||||
WHERE
|
||||
e.id = $1`,
|
||||
id,
|
||||
)
|
||||
|
||||
err = row.StructScan(&e)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
1
sql/0017.sql
Normal file
1
sql/0017.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE public.script_log ADD "name" varchar DEFAULT '' NOT NULL;
|
||||
|
|
@ -54,7 +54,7 @@ button {
|
|||
#menu {
|
||||
grid-area: menu;
|
||||
grid-template-columns: repeat(100, min-content);
|
||||
grid-gap: 16px;
|
||||
grid-gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
#menu.page {
|
||||
|
|
@ -64,6 +64,8 @@ button {
|
|||
#menu .item {
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
#menu .item.selected {
|
||||
font-weight: bold;
|
||||
|
|
@ -548,3 +550,88 @@ dialog#connection-data div.button {
|
|||
width: 50vw;
|
||||
font-family: monospace;
|
||||
}
|
||||
#script-executions > .label {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
color: var(--section-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
#script-executions .executions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(11, min-content);
|
||||
grid-gap: 4px 32px;
|
||||
margin-top: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
#script-executions .executions .header {
|
||||
font-weight: bold;
|
||||
}
|
||||
#script-executions .executions div {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#script-executions .executions img {
|
||||
display: block;
|
||||
height: 24px;
|
||||
}
|
||||
#script-executions .executions .time {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
}
|
||||
#script-executions .executions .source img,
|
||||
#script-executions .executions .data img,
|
||||
#script-executions .executions .env img,
|
||||
#script-executions .executions .stdout img,
|
||||
#script-executions .executions .stderr img {
|
||||
cursor: pointer;
|
||||
}
|
||||
#script-executions .executions .exitcode {
|
||||
text-align: right;
|
||||
}
|
||||
#script-executions .executions .exitcode div {
|
||||
padding: 2px 8px;
|
||||
width: 50px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
#script-executions .executions .exitcode div.code-ok {
|
||||
background-color: #6f9753;
|
||||
}
|
||||
#script-executions .executions .exitcode div.code-error {
|
||||
background-color: #a12f2f;
|
||||
}
|
||||
#script-execution-value-dialog {
|
||||
display: grid;
|
||||
}
|
||||
#script-execution-value-dialog .top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
}
|
||||
#script-execution-value-dialog .top .header {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: var(--section-color);
|
||||
}
|
||||
#script-execution-value-dialog .top .copy {
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#script-execution-value-dialog .top .copy.clicked {
|
||||
filter: invert(50%);
|
||||
}
|
||||
#script-execution-value-dialog .label {
|
||||
font-weight: bold;
|
||||
}
|
||||
#script-execution-value-dialog .value {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
height: 75vh;
|
||||
width: 80vw;
|
||||
white-space: pre;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export class App {
|
|||
this.typesList = null
|
||||
this.scriptsList = null
|
||||
this.scriptEditor = null
|
||||
this.scriptExecutionList = null
|
||||
|
||||
this.currentNode = null
|
||||
this.currentNodeID = null
|
||||
|
|
@ -21,6 +22,7 @@ export class App {
|
|||
|
||||
const events = [
|
||||
'EDITOR_NODE_SAVE',
|
||||
'EXECUTIONS_LIST_FETCHED',
|
||||
'MENU_ITEM_SELECTED',
|
||||
'NODE_CONNECT',
|
||||
'NODE_COPY_PATH',
|
||||
|
|
@ -102,6 +104,11 @@ export class App {
|
|||
this.nodeUpdate()
|
||||
break
|
||||
|
||||
case 'EXECUTIONS_LIST_FETCHED':
|
||||
const executions = document.getElementById('script-executions')
|
||||
executions.replaceChildren(this.scriptExecutionList.render())
|
||||
break
|
||||
|
||||
case 'TYPES_LIST_FETCHED':
|
||||
const types = document.getElementById('types')
|
||||
types.replaceChildren(this.typesList.render())
|
||||
|
|
@ -258,6 +265,18 @@ export class App {
|
|||
this.scriptsList.fetchScripts()
|
||||
.catch(err => showError(err))
|
||||
break
|
||||
|
||||
case 'script-execution':
|
||||
document.getElementById('script-executions').classList.add('show')
|
||||
|
||||
if (this.scriptExecutionList === null)
|
||||
this.scriptExecutionList = new ScriptExecutionList()
|
||||
this.scriptExecutionList.fetchExecutions()
|
||||
.then(() => {
|
||||
mbus.dispatch('EXECUTIONS_LIST_FETCHED')
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
break
|
||||
}
|
||||
}// }}}
|
||||
|
||||
|
|
@ -1532,4 +1551,164 @@ class ScriptHookDialog extends Component {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
class ScriptExecutionList extends Component {
|
||||
constructor() {// {{{
|
||||
super()
|
||||
this.executions = []
|
||||
}// }}}
|
||||
async fetchExecutions() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
window._app.query('/scriptexecutions/')
|
||||
.then(data => {
|
||||
this.executions = data.ScriptExecutions
|
||||
resolve()
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
renderComponent() {// {{{
|
||||
const tmpl = document.createElement('template')
|
||||
tmpl.innerHTML = `
|
||||
<div class="label">Script executions</div>
|
||||
<div class="executions">
|
||||
<div class="header">ID</div>
|
||||
<div class="header">SSH</div>
|
||||
<div class="header">Script</div>
|
||||
<div class="header">Start</div>
|
||||
<div class="header">End</div>
|
||||
<div class="header">Script</div>
|
||||
<div class="header">Data</div>
|
||||
<div class="header">Env</div>
|
||||
<div class="header">Out</div>
|
||||
<div class="header">Err</div>
|
||||
<div class="header">Exitcode</div>
|
||||
</div>
|
||||
`
|
||||
const executions = tmpl.content.querySelector('.executions')
|
||||
|
||||
for (const e of this.executions) {
|
||||
const se = new ScriptExecution(e)
|
||||
executions.append(se.render())
|
||||
}
|
||||
|
||||
return tmpl.content
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class ScriptExecution extends Component {
|
||||
constructor(execution) {// {{{
|
||||
super()
|
||||
this.execution = execution
|
||||
}// }}}
|
||||
formatTime(t) {// {{{
|
||||
const d = new Date(t)
|
||||
const year = d.getYear() + 1900
|
||||
const month = `0${d.getMonth() + 1}`.slice(-2)
|
||||
const date = `0${d.getDate()}`.slice(-2)
|
||||
const hour = `0${d.getHours()}`.slice(-2)
|
||||
const min = `0${d.getMinutes()}`.slice(-2)
|
||||
const sec = `0${d.getSeconds()}`.slice(-2)
|
||||
return `<span class="date">${year}-${month}-${date}</span> <span class="time">${hour}:${min}:${sec}</span>`
|
||||
}// }}}
|
||||
icon(name) {// {{{
|
||||
return `<img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/${name}.svg" />`
|
||||
}// }}}
|
||||
renderComponent() {// {{{
|
||||
const tmpl = document.createElement('template')
|
||||
|
||||
tmpl.innerHTML = `
|
||||
<div class="id">${this.execution.ID}</div>
|
||||
<div class="ssh">${this.execution.SSH}</div>
|
||||
<div class="name">${this.execution.ScriptName}</div>
|
||||
<div class="start">${this.formatTime(this.execution.TimeStart.Time)}</div>
|
||||
<div class="end">${this.formatTime(this.execution.TimeEnd.Time)}</div>
|
||||
<div class="source">${this.execution.HasSource ? this.icon('bash') : ''}</div>
|
||||
<div class="data">${this.execution.HasData ? this.icon('code-json') : ''}</div>
|
||||
<div class="env">${this.execution.HasEnv ? this.icon('application-braces-outline') : ''}</div>
|
||||
<div class="stdout">${this.execution.HasOutputStdout ? this.icon('text-box-check-outline') : ''}</div>
|
||||
<div class="stderr">${this.execution.HasOutputStderr ? this.icon('text-box-remove-outline') : ''}</div>
|
||||
<div class="exitcode"><div class="code-${this.execution.ExitCode.Int16 == 0 ? 'ok' : 'error'}">${this.execution.ExitCode.Int16}</div></div>
|
||||
`
|
||||
|
||||
const classValues = new Map()
|
||||
classValues.set('source', 'Source')
|
||||
classValues.set('data', 'Data')
|
||||
classValues.set('env', 'Env')
|
||||
classValues.set('stdout', 'OutputStdout')
|
||||
classValues.set('stderr', 'OutputStderr')
|
||||
|
||||
for (const [cls, value] of classValues) {
|
||||
tmpl.content.querySelector(`.${cls}`).addEventListener('click', () => {
|
||||
new ScriptExecutionValueDialog(this.execution, value).render()
|
||||
})
|
||||
}
|
||||
|
||||
return tmpl.content
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class ScriptExecutionValueDialog extends Component {
|
||||
constructor(execution, valueName) {// {{{
|
||||
super()
|
||||
this.execution = execution
|
||||
this.valueName = valueName
|
||||
|
||||
this.dlg = document.createElement('dialog')
|
||||
this.dlg.id = 'script-execution-value-dialog'
|
||||
this.dlg.addEventListener('close', () => this.dlg.remove())
|
||||
|
||||
this.value = null
|
||||
}// }}}
|
||||
getValue(execution) {
|
||||
switch (this.valueName) {
|
||||
case 'Source':
|
||||
return execution.Source
|
||||
|
||||
case 'Data':
|
||||
case 'Env':
|
||||
return JSON.stringify(
|
||||
JSON.parse(execution[this.valueName])
|
||||
,
|
||||
null,
|
||||
' '
|
||||
)
|
||||
|
||||
case 'OutputStdout':
|
||||
case 'OutputStderr':
|
||||
return execution[this.valueName]?.String
|
||||
}
|
||||
}
|
||||
renderComponent() {// {{{
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = `
|
||||
<div class="top">
|
||||
<div class="header">${this.valueName}</div>
|
||||
<img class="copy" src="/images/${_VERSION}/node_modules/@mdi/svg/svg/content-copy.svg" />
|
||||
</div>
|
||||
<div class="label">${this.execution.ID}</div>
|
||||
<div class="value"></div>
|
||||
`
|
||||
|
||||
this.value = div.querySelector('.value')
|
||||
div.querySelector('.copy').addEventListener('click', event=>{
|
||||
event.target.classList.add('clicked')
|
||||
setTimeout(()=>event.target.classList.remove('clicked'), 250)
|
||||
navigator.clipboard.writeText(this.value.innerText)
|
||||
})
|
||||
|
||||
window._app.query(`/scriptexecutions/${this.execution.ID}`)
|
||||
.then(data => {
|
||||
this.value.innerText = this.getValue(data.ScriptExecution)
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
|
||||
this.dlg.append(...div.children)
|
||||
document.body.append(this.dlg)
|
||||
this.dlg.showModal()
|
||||
|
||||
return []
|
||||
}// }}}
|
||||
}
|
||||
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ button {
|
|||
#menu {
|
||||
grid-area: menu;
|
||||
grid-template-columns: repeat(100, min-content);
|
||||
grid-gap: 16px;
|
||||
grid-gap: 24px;
|
||||
align-items: center;
|
||||
|
||||
&.page {
|
||||
|
|
@ -81,6 +81,8 @@ button {
|
|||
.item {
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
|
|
@ -700,3 +702,105 @@ dialog#connection-data {
|
|||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
#script-executions {
|
||||
& > .label {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
color: var(--section-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.executions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(11, min-content);
|
||||
grid-gap: 4px 32px;
|
||||
margin-top: 32px;
|
||||
align-items: center;
|
||||
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.source, .data, .env, .stdout, .stderr {
|
||||
img {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.exitcode {
|
||||
text-align: right;
|
||||
|
||||
div {
|
||||
padding: 2px 8px;
|
||||
width: 50px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
|
||||
&.code-ok {
|
||||
background-color: #6f9753;
|
||||
}
|
||||
|
||||
&.code-error {
|
||||
background-color: #a12f2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#script-execution-value-dialog {
|
||||
display: grid;
|
||||
|
||||
.top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
|
||||
.header {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: var(--section-color);
|
||||
}
|
||||
|
||||
.copy {
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&.clicked {
|
||||
filter: invert(50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
height: 75vh;
|
||||
width: 80vw;
|
||||
white-space: pre;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@
|
|||
<div id="layout">
|
||||
<div class="page section" id="menu">
|
||||
<div class="item" id="logo"><img src="/images/{{ .VERSION }}/logo.svg" /></div>
|
||||
<div class="item" data-section='node' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'node')">Nodes</div>
|
||||
<div class="item" data-section='type' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'type')">Types</div>
|
||||
<div class="item" data-section='script' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'script')">Scripts</div>
|
||||
<div class="item" data-section='node' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'node')">Nodes</div>
|
||||
<div class="item" data-section='type' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'type')">Types</div>
|
||||
<div class="item" data-section='script' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'script')">Scripts</div>
|
||||
<div class="item" data-section='script-execution' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'script-execution')">Script executions</div>
|
||||
</div>
|
||||
|
||||
<div class="page section" id="nodes"></div>
|
||||
|
|
@ -68,6 +69,8 @@
|
|||
|
||||
<div class="page section" id="scripts"></div>
|
||||
<div class="page section" id="editor-script"></div>
|
||||
|
||||
<div class="page section" id="script-executions"></div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
|
|
|
|||
43
webserver.go
43
webserver.go
|
|
@ -55,6 +55,8 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/hooks/update", actionHookUpdate)
|
||||
http.HandleFunc("/hooks/delete/{hookID}", actionHookDelete)
|
||||
http.HandleFunc("/hooks/schedule/{hookID}", actionHookSchedule)
|
||||
http.HandleFunc("/scriptexecutions/", actionScriptExecutions)
|
||||
http.HandleFunc("/scriptexecutions/{executionID}", actionScriptExecutionGet)
|
||||
|
||||
err = http.ListenAndServe(address, nil)
|
||||
return
|
||||
|
|
@ -752,4 +754,45 @@ func actionHookSchedule(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
func actionScriptExecutions(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
execs, err := GetScriptExecutions()
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
ScriptExecutions []ScriptExecutionBrief
|
||||
}{
|
||||
true,
|
||||
execs,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionScriptExecutionGet(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
executionID := 0
|
||||
executionIDStr := r.PathValue("executionID")
|
||||
executionID, _ = strconv.Atoi(executionIDStr)
|
||||
|
||||
execution, err := GetScriptExecution(executionID)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
ScriptExecution ScriptExecution
|
||||
}{
|
||||
true,
|
||||
execution,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue