marshaller.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import './ActionButton.js' // To define action-button
  2. import { ExecutionDialog } from './ExecutionDialog.js'
  3. /**
  4. * This is a weird function that just sets some globals.
  5. */
  6. export function initMarshaller () {
  7. window.changeDirectory = changeDirectory
  8. window.showSection = showSection
  9. window.executionDialog = new ExecutionDialog()
  10. window.logEntries = {}
  11. window.addEventListener('EventExecutionFinished', onExecutionFinished)
  12. }
  13. export function marshalDashboardComponentsJsonToHtml (json) {
  14. marshalActionsJsonToHtml(json)
  15. marshalDashboardStructureToHtml(json)
  16. document.getElementById('username').innerText = json.authenticatedUser
  17. changeDirectory(null)
  18. }
  19. function marshalActionsJsonToHtml (json) {
  20. const currentIterationTimestamp = Date.now()
  21. window.actionButtons = {}
  22. for (const jsonButton of json.actions) {
  23. let htmlButton = window.actionButtons[jsonButton.id]
  24. if (typeof htmlButton === 'undefined') {
  25. htmlButton = document.createElement('action-button')
  26. htmlButton.constructFromJson(jsonButton)
  27. window.actionButtons[jsonButton.title] = htmlButton
  28. }
  29. htmlButton.updateFromJson(jsonButton)
  30. htmlButton.updateIterationTimestamp = currentIterationTimestamp
  31. }
  32. // Remove existing, but stale buttons (that were not updated in this round)
  33. for (const existingButton of document.querySelectorAll('action-button')) {
  34. if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
  35. existingButton.remove()
  36. }
  37. }
  38. }
  39. function onExecutionFinished (evt) {
  40. const logEntry = evt.payload.logEntry
  41. const actionButton = window.actionButtons[logEntry.actionTitle]
  42. if (actionButton === undefined) {
  43. return
  44. }
  45. switch (actionButton.popupOnStart) {
  46. case 'execution-button':
  47. document.querySelector('execution-button#execution-' + logEntry.executionTrackingId).onExecutionFinished(logEntry)
  48. break
  49. case 'execution-dialog-stdout-only':
  50. case 'execution-dialog':
  51. actionButton.onExecutionFinished(logEntry)
  52. // We don't need to fetch the logEntry for the dialog because we already
  53. // have it, so we open the dialog and it will get updated below.
  54. window.executionDialog.show()
  55. window.executionDialog.executionUuid = logEntry.uuid
  56. break
  57. default:
  58. actionButton.onExecutionFinished(logEntry)
  59. break
  60. }
  61. marshalLogsJsonToHtml({
  62. logs: [logEntry]
  63. })
  64. // If the current execution dialog is open, update that too
  65. if (window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
  66. window.executionDialog.renderExecutionResult({
  67. logEntry: logEntry
  68. })
  69. }
  70. }
  71. function showSection (title) {
  72. for (const section of document.querySelectorAll('section')) {
  73. if (section.title === title) {
  74. section.style.display = 'block'
  75. } else {
  76. section.style.display = 'none'
  77. }
  78. }
  79. setSectionNavigationVisible(false)
  80. changeDirectory(null)
  81. }
  82. function setSectionNavigationVisible (visible) {
  83. const nav = document.querySelector('nav')
  84. const btn = document.getElementById('sidebar-toggler-button')
  85. if (document.body.classList.contains('has-sidebar')) {
  86. if (visible) {
  87. btn.setAttribute('aria-pressed', false)
  88. btn.setAttribute('aria-label', 'Open sidebar navigation')
  89. btn.innerHTML = '«'
  90. nav.classList.add('shown')
  91. nav.style.display = 'flex'
  92. } else {
  93. btn.setAttribute('aria-pressed', true)
  94. btn.setAttribute('aria-label', 'Close sidebar navigation')
  95. btn.innerHTML = '☰'
  96. nav.classList.remove('shown')
  97. setTimeout(() => {
  98. nav.style.display = 'none'
  99. }, 600)
  100. }
  101. } else {
  102. btn.disabled = true
  103. }
  104. }
  105. export function setupSectionNavigation (style) {
  106. const nav = document.querySelector('nav')
  107. const btn = document.getElementById('sidebar-toggler-button')
  108. if (style === 'sidebar') {
  109. nav.classList.add('sidebar')
  110. document.body.classList.add('has-sidebar')
  111. btn.onclick = () => {
  112. if (nav.classList.contains('shown')) {
  113. setSectionNavigationVisible(false)
  114. } else {
  115. setSectionNavigationVisible(true)
  116. }
  117. }
  118. } else {
  119. nav.classList.add('topbar')
  120. document.body.classList.add('has-topbar')
  121. }
  122. document.getElementById('showActions').onclick = () => { showSection('Actions') }
  123. document.getElementById('showLogs').onclick = () => { showSection('Logs') }
  124. }
  125. function marshalDashboardStructureToHtml (json) {
  126. const nav = document.getElementById('navigation-links')
  127. for (const dashboard of json.dashboards) {
  128. const oldsection = document.querySelector('section[title="' + dashboard.title + '"]')
  129. if (oldsection != null) {
  130. oldsection.remove()
  131. }
  132. const section = document.createElement('section')
  133. section.title = dashboard.title
  134. const def = createFieldset('default', section)
  135. section.appendChild(def)
  136. document.getElementsByTagName('main')[0].appendChild(section)
  137. marshalContainerContents(dashboard, section, def, dashboard.title)
  138. const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]')
  139. if (oldLi != null) {
  140. oldLi.remove()
  141. }
  142. const navigationA = document.createElement('a')
  143. navigationA.title = dashboard.title
  144. navigationA.innerText = dashboard.title
  145. navigationA.setAttribute('href', '#' + dashboard.title)
  146. navigationA.onclick = () => {
  147. showSection(dashboard.title)
  148. }
  149. const navigationLi = document.createElement('li')
  150. navigationLi.appendChild(navigationA)
  151. navigationLi.title = dashboard.title
  152. document.getElementById('navigation-links').appendChild(navigationLi)
  153. }
  154. const rootGroup = document.querySelector('#root-group')
  155. for (const btn of Object.values(window.actionButtons)) {
  156. if (btn.parentElement === null) {
  157. rootGroup.appendChild(btn)
  158. }
  159. }
  160. if (rootGroup.querySelectorAll('action-button').length === 0 && json.dashboards.length > 0) {
  161. nav.querySelector('li[title="Actions"]').style.display = 'none'
  162. showSection(json.dashboards[0].title)
  163. } else {
  164. showSection('Actions')
  165. }
  166. }
  167. function marshalLink (item, fieldset) {
  168. let btn = window.actionButtons[item.title]
  169. if (typeof btn === 'undefined') {
  170. btn = document.createElement('button')
  171. btn.innerText = 'Action not found: ' + item.title
  172. btn.classList.add('error')
  173. }
  174. fieldset.appendChild(btn)
  175. }
  176. function marshalMreOutput (dashboardComponent, fieldset) {
  177. const pre = document.createElement('pre')
  178. pre.classList.add('mre-output')
  179. pre.innerHTML = 'Waiting...'
  180. const executionStatus = {
  181. actionId: dashboardComponent.title
  182. }
  183. window.fetch(window.restBaseUrl + 'ExecutionStatus', {
  184. method: 'POST',
  185. headers: {
  186. 'Content-Type': 'application/json'
  187. },
  188. body: JSON.stringify(executionStatus)
  189. }).then((res) => {
  190. if (res.ok) {
  191. return res.json()
  192. } else {
  193. pre.innerHTML = 'error'
  194. throw new Error(res.statusText)
  195. }
  196. }).then((json) => {
  197. updateMre(pre, json.logEntry)
  198. })
  199. const updateMre = (pre, json) => {
  200. pre.innerHTML = json.stdout
  201. }
  202. window.addEventListener('ExecutionFinished', (e) => {
  203. // The dashboard component "title" field is used for lots of things
  204. // and in this context for MreOutput it's just to refer an an actionId.
  205. //
  206. // So this is not a typo.
  207. if (e.payload.actionId === dashboardComponent.title) {
  208. updateMre(pre, e.payload)
  209. }
  210. })
  211. fieldset.appendChild(pre)
  212. }
  213. function marshalContainerContents (json, section, fieldset, parentDashboard) {
  214. for (const item of json.contents) {
  215. switch (item.type) {
  216. case 'fieldset':
  217. marshalFieldset(item, section, parentDashboard)
  218. break
  219. case 'directory':
  220. marshalDirectoryButton(item, fieldset)
  221. marshalDirectory(item, section)
  222. break
  223. case 'display':
  224. marshalDisplay(item, fieldset)
  225. break
  226. case 'stdout-most-recent-execution':
  227. marshalMreOutput(item, fieldset)
  228. break
  229. case 'link':
  230. marshalLink(item, fieldset)
  231. break
  232. default:
  233. }
  234. }
  235. }
  236. function createFieldset (title, parentDashboard) {
  237. const legend = document.createElement('legend')
  238. legend.innerText = title
  239. const fs = document.createElement('fieldset')
  240. fs.title = title
  241. fs.appendChild(legend)
  242. if (typeof parentDashboard === 'undefined') {
  243. fs.setAttribute('parent-dashboard', '')
  244. } else {
  245. fs.setAttribute('parent-dashboard', parentDashboard)
  246. }
  247. return fs
  248. }
  249. function marshalFieldset (item, section, parentDashboard) {
  250. const fs = createFieldset(item.title, parentDashboard)
  251. marshalContainerContents(item, section, fs)
  252. section.appendChild(fs)
  253. }
  254. function changeDirectory (selected) {
  255. if (selected === '') {
  256. selected = null
  257. }
  258. if (selected === null) {
  259. window.directoryNavigation = []
  260. } else if (selected === '..') {
  261. window.directoryNavigation.pop()
  262. if (window.directoryNavigation.length > 0) {
  263. selected = window.directoryNavigation[window.directoryNavigation.length - 1]
  264. } else {
  265. selected = null
  266. }
  267. } else {
  268. // If the selected item is already in the nav list, pop elements until we get
  269. // "back" to the existing nav item
  270. while (window.directoryNavigation.includes(selected)) {
  271. window.directoryNavigation.pop()
  272. }
  273. window.directoryNavigation.push(selected)
  274. }
  275. for (const fieldset of document.querySelectorAll('fieldset')) {
  276. if (selected === null) {
  277. if ((fieldset.id === 'root-group' || fieldset.getAttribute('parent-dashboard') !== '') && fieldset.children.length > 1) {
  278. fieldset.style.display = 'grid'
  279. } else {
  280. fieldset.style.display = 'none'
  281. }
  282. } else {
  283. if (fieldset.title === selected) {
  284. fieldset.style.display = 'grid'
  285. } else {
  286. fieldset.style.display = 'none'
  287. }
  288. }
  289. }
  290. const title = document.querySelector('h1')
  291. title.innerHTML = ''
  292. const rootLink = createDirectoryBreadcrumb(window.pageTitle, null)
  293. title.appendChild(rootLink)
  294. for (const dir of window.directoryNavigation) {
  295. const sep = document.createElement('span')
  296. sep.innerHTML = ' » '
  297. title.append(sep)
  298. if (dir === selected) {
  299. title.append(selected)
  300. } else {
  301. title.appendChild(createDirectoryBreadcrumb(dir))
  302. }
  303. }
  304. document.title = title.innerText
  305. if (selected === null) {
  306. window.location.hash = null
  307. window.history.pushState({ dir: null }, null, '#')
  308. } else {
  309. window.location.hash = selected
  310. window.history.pushState({ dir: selected }, null, '#' + selected)
  311. }
  312. }
  313. function createDirectoryBreadcrumb (title, link) {
  314. const a = document.createElement('a')
  315. a.innerText = title
  316. a.title = title
  317. if (typeof link === 'undefined') {
  318. link = title
  319. }
  320. if (link === null) {
  321. a.href = '#'
  322. } else {
  323. a.href = '#' + link
  324. }
  325. a.onclick = () => {
  326. changeDirectory(link)
  327. }
  328. return a
  329. }
  330. function marshalDisplay (item, fieldset) {
  331. const display = document.createElement('div')
  332. display.innerHTML = item.title
  333. fieldset.appendChild(display)
  334. }
  335. function marshalDirectoryButton (item, fieldset) {
  336. const directoryButton = document.createElement('button')
  337. directoryButton.innerHTML = '<span class = "icon">&#128193;</span> ' + item.title
  338. directoryButton.onclick = () => {
  339. changeDirectory(item.title)
  340. }
  341. fieldset.appendChild(directoryButton)
  342. }
  343. function marshalDirectory (item, section) {
  344. const fs = createFieldset(item.title)
  345. fs.style.display = 'none'
  346. const directoryBackButton = document.createElement('button')
  347. directoryBackButton.innerHTML = '&laquo;'
  348. directoryBackButton.title = 'Go back one directory'
  349. directoryBackButton.onclick = () => {
  350. changeDirectory('..')
  351. }
  352. fs.appendChild(directoryBackButton)
  353. marshalContainerContents(item, section, fs)
  354. section.appendChild(fs)
  355. }
  356. export function marshalLogsJsonToHtml (json) {
  357. for (const logEntry of json.logs) {
  358. const existing = window.logEntries[logEntry.executionTrackingId]
  359. if (existing !== undefined) {
  360. continue
  361. }
  362. window.logEntries[logEntry.executionTrackingId] = logEntry
  363. const tpl = document.getElementById('tplLogRow')
  364. const row = tpl.content.querySelector('tr').cloneNode(true)
  365. if (logEntry.stdout.length === 0) {
  366. logEntry.stdout = '(empty)'
  367. }
  368. if (logEntry.stderr.length === 0) {
  369. logEntry.stderr = '(empty)'
  370. }
  371. let logTableExitCode = logEntry.exitCode
  372. if (logEntry.exitCode === 0) {
  373. logTableExitCode = 'OK'
  374. }
  375. if (logEntry.timedOut) {
  376. logTableExitCode += ' (timed out)'
  377. }
  378. row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
  379. row.querySelector('.content').innerText = logEntry.actionTitle
  380. row.querySelector('.icon').innerHTML = logEntry.actionIcon
  381. row.querySelector('pre.stdout').innerText = logEntry.stdout
  382. row.querySelector('pre.stderr').innerText = logEntry.stderr
  383. row.querySelector('.exit-code').innerText = logTableExitCode
  384. row.setAttribute('title', logEntry.actionTitle)
  385. row.querySelector('.content').onclick = () => {
  386. window.executionDialog.reset()
  387. window.executionDialog.show()
  388. window.executionDialog.renderExecutionResult({
  389. logEntry: window.logEntries[logEntry.executionTrackingId]
  390. })
  391. }
  392. for (const tag of logEntry.tags) {
  393. const domTag = document.createElement('span')
  394. domTag.classList.add('tag')
  395. domTag.innerText = tag
  396. row.querySelector('.tags').append(domTag)
  397. }
  398. document.querySelector('#logTableBody').prepend(row)
  399. }
  400. }
  401. window.addEventListener('popstate', (e) => {
  402. e.preventDefault()
  403. if (e.state != null && typeof e.state.dir !== 'undefined') {
  404. changeDirectory(e.state.dir)
  405. }
  406. })