marshaller.js 15 KB

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