import './ActionButton.js' // To define action-button import { ExecutionDialog } from './ExecutionDialog.js' import { ActionStatusDisplay } from './ActionStatusDisplay.js' function createElement (tag, attributes) { const el = document.createElement(tag) if (attributes !== null) { if (attributes.classNames !== undefined) { el.classList.add(...attributes.classNames) } if (attributes.innerText !== undefined) { el.innerText = attributes.innerText } } return el } function createTag (val) { const domTag = createElement('span', { innerText: val, classNames: ['tag'] }) return domTag } function createAnnotation (key, val) { const domAnnotation = createElement('span', { classNames: ['annotation'] }) domAnnotation.appendChild(createElement('span', { innerText: key, classNames: ['annotation-key'] })) domAnnotation.appendChild(createElement('span', { innerText: val, classNames: ['annotation-value'] })) return domAnnotation } /** * This is a weird function that just sets some globals. */ export function initMarshaller () { window.showSection = showSection window.showSectionView = showSectionView window.executionDialog = new ExecutionDialog() window.logEntries = new Map() window.registeredPaths = new Map() window.breadcrumbNavigation = [] window.currentPath = '' window.addEventListener('EventExecutionStarted', onExecutionStarted) window.addEventListener('EventExecutionFinished', onExecutionFinished) window.addEventListener('EventOutputChunk', onOutputChunk) } export function marshalDashboardComponentsJsonToHtml (json) { marshalActionsJsonToHtml(json) marshalDashboardStructureToHtml(json) document.getElementById('username').innerText = json.authenticatedUser if (window.settings.AuthLocalLogin || window.settings.AuthOAuth2Providers !== null) { if (json.authenticatedUser === 'guest') { document.getElementById('link-login').hidden = false document.getElementById('link-logout').hidden = true } else { document.getElementById('link-login').hidden = true if (json.authenticatedUserProvider === 'local' || json.authenticatedUserProvider === 'oauth2') { document.getElementById('link-logout').hidden = false } } document.getElementById('username').setAttribute('title', json.authenticatedUserProvider) } document.body.setAttribute('initial-marshal-complete', 'true') } function marshalActionsJsonToHtml (json) { const currentIterationTimestamp = Date.now() window.actionButtons = {} for (const jsonButton of json.actions) { let htmlButton = window.actionButtons[jsonButton.id] if (typeof htmlButton === 'undefined') { htmlButton = document.createElement('action-button') htmlButton.constructFromJson(jsonButton) window.actionButtons[jsonButton.title] = htmlButton } htmlButton.updateFromJson(jsonButton) htmlButton.updateIterationTimestamp = currentIterationTimestamp } // Remove existing, but stale buttons (that were not updated in this round) for (const existingButton of document.querySelectorAll('action-button')) { if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) { existingButton.remove() } } } function onOutputChunk (evt) { const chunk = evt.payload if (chunk.executionTrackingId === window.executionDialog.executionTrackingId) { window.terminal.write(chunk.output) window.executionDialog.showOutput() } } function onExecutionStarted (evt) { const logEntry = evt.payload.logEntry marshalLogsJsonToHtml({ logs: [logEntry] }) } function onExecutionFinished (evt) { const logEntry = evt.payload.logEntry window.logEntries.set(logEntry.executionTrackingId, logEntry) const actionButton = window.actionButtons[logEntry.actionTitle] if (actionButton === undefined) { return } switch (actionButton.popupOnStart) { case 'execution-button': if (document.querySelector('execution-button#execution-' + logEntry.executionTrackingId) !== null) { // If the button was created in our instance document.querySelector('execution-button#execution-' + logEntry.executionTrackingId).onExecutionFinished(logEntry) } break case 'execution-dialog-stdout-only': case 'execution-dialog': actionButton.onExecutionFinished(logEntry) // We don't need to fetch the logEntry for the dialog because we already // have it, so we open the dialog and it will get updated below. window.executionDialog.show() window.executionDialog.executionTrackingId = logEntry.uuid break default: actionButton.onExecutionFinished(logEntry) break } marshalLogsJsonToHtml({ logs: [logEntry] }) // If the current execution dialog is open, update that too if (window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) { window.executionDialog.renderExecutionResult({ logEntry: logEntry }) } } function convertPathToBreadcrumb (path) { const parts = path.split('/') const result = [] for (let i = 0; i < parts.length; i++) { if (parts[i] === '') { continue } result.push(parts.slice(0, i + 1).join('/')) } return result } function showExecutionResult (pathName) { const executionTrackingId = pathName.split('/')[2] window.executionDialog.fetchExecutionResult(executionTrackingId) window.executionDialog.show() } function showSection (pathName) { if (pathName.startsWith('/logs/')) { showExecutionResult(pathName) pushNewNavigationPath(pathName) return } const path = window.registeredPaths.get(pathName) if (path === undefined) { console.warn('Section not found by path: ' + pathName) showSection('/') return } window.convertPathToBreadcrumb = convertPathToBreadcrumb window.currentPath = pathName window.breadcrumbNavigation = convertPathToBreadcrumb(pathName) for (const section of document.querySelectorAll('section')) { if (section.title === path.section) { section.style.display = 'block' } else { section.style.display = 'none' } } pushNewNavigationPath(pathName) setSectionNavigationVisible(false) showSectionView(path.view) } function pushNewNavigationPath (pathName) { window.history.pushState({ path: pathName }, null, pathName) } function setSectionNavigationVisible (visible) { const nav = document.querySelector('nav') const btn = document.getElementById('sidebar-toggler-button') nav.removeAttribute('hidden') if (document.body.classList.contains('has-sidebar')) { if (visible) { btn.setAttribute('aria-pressed', false) btn.setAttribute('aria-label', 'Open sidebar navigation') btn.innerHTML = '«' nav.classList.add('shown') } else { btn.setAttribute('aria-pressed', true) btn.setAttribute('aria-label', 'Close sidebar navigation') btn.innerHTML = '☰' nav.classList.remove('shown') } } else { btn.disabled = true } } export function setupSectionNavigation (style) { const nav = document.querySelector('nav') const btn = document.getElementById('sidebar-toggler-button') if (style === 'sidebar') { nav.classList.add('sidebar') document.body.classList.add('has-sidebar') btn.onclick = () => { if (nav.classList.contains('shown')) { setSectionNavigationVisible(false) } else { setSectionNavigationVisible(true) } } } else { nav.classList.add('topbar') document.body.classList.add('has-topbar') } registerSection('/', 'Actions', null, document.getElementById('showActions')) registerSection('/diagnostics', 'Diagnostics', null, document.getElementById('showDiagnostics')) registerSection('/logs', 'Logs', null, document.getElementById('showLogs')) registerSection('/login', 'Login', null, null) } function registerSection (path, section, view, linkElement) { window.registeredPaths.set(path, { section: section, view: view }) if (linkElement != null) { addLinkToSection(path, linkElement) } } function addLinkToSection (pathName, element) { const path = window.registeredPaths.get(pathName) element.href = 'javascript:void(0)' element.title = path.section element.onclick = () => { showSection(pathName) } } export function refreshDiagnostics () { document.getElementById('diagnostics-sshfoundkey').innerHTML = window.settings.SshFoundKey document.getElementById('diagnostics-sshfoundconfig').innerHTML = window.settings.SshFoundConfig } function getSystemTitle (title) { return title.replaceAll(' ', '') } function marshalSingleDashboard (dashboard, nav) { const oldsection = document.querySelector('section[title="' + getSystemTitle(dashboard.title) + '"]') if (oldsection != null) { oldsection.remove() } const section = document.createElement('section') section.setAttribute('system-title', getSystemTitle(dashboard.title)) section.title = section.getAttribute('system-title') const def = createFieldset('default', section) section.appendChild(def) document.getElementsByTagName('main')[0].appendChild(section) marshalContainerContents(dashboard, section, def, dashboard.title) const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]') if (oldLi != null) { oldLi.remove() } const navigationA = document.createElement('a') navigationA.title = dashboard.title navigationA.innerText = dashboard.title registerSection('/' + getSystemTitle(section.title), section.title, null, navigationA) const navigationLi = document.createElement('li') navigationLi.appendChild(navigationA) navigationLi.title = dashboard.title document.getElementById('navigation-links').appendChild(navigationLi) } function marshalDashboardStructureToHtml (json) { const nav = document.getElementById('navigation-links') for (const dashboard of json.dashboards) { marshalSingleDashboard(dashboard, nav) } const rootGroup = document.querySelector('#root-group') for (const btn of Object.values(window.actionButtons)) { if (btn.parentElement === null) { rootGroup.appendChild(btn) } } const shouldHideActions = rootGroup.querySelectorAll('action-button').length === 0 && json.dashboards.length > 0 if (shouldHideActions) { nav.querySelector('li[title="Actions"]').style.display = 'none' } if (window.currentPath !== '') { showSection(window.currentPath) } else if (window.location.pathname !== '/' && document.body.getAttribute('initial-marshal-complete') === null) { showSection(window.location.pathname) } else { if (shouldHideActions) { showSection('/' + getSystemTitle(json.dashboards[0].title)) } else { showSection('/') } } } function marshalLink (item, fieldset) { let btn = window.actionButtons[item.title] if (typeof btn === 'undefined') { btn = document.createElement('button') btn.innerText = 'Action not found: ' + item.title btn.classList.add('error') } if (item.cssClass !== '') { btn.classList.add(item.cssClass) } fieldset.appendChild(btn) } function marshalMreOutput (dashboardComponent, fieldset) { const pre = document.createElement('pre') pre.classList.add('mre-output') pre.innerHTML = 'Waiting...' const executionStatus = { actionId: dashboardComponent.title } window.fetch(window.restBaseUrl + 'ExecutionStatus', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(executionStatus) }).then((res) => { if (res.ok) { return res.json() } else { pre.innerHTML = 'error' throw new Error(res.statusText) } }).then((json) => { updateMre(pre, json.logEntry) }) const updateMre = (pre, json) => { pre.innerHTML = json.output } window.addEventListener('ExecutionFinished', (e) => { // The dashboard component "title" field is used for lots of things // and in this context for MreOutput it's just to refer an an actionId. // // So this is not a typo. if (e.payload.actionId === dashboardComponent.title) { updateMre(pre, e.payload) } }) fieldset.appendChild(pre) } function marshalContainerContents (json, section, fieldset, parentDashboard) { for (const item of json.contents) { switch (item.type) { case 'fieldset': marshalFieldset(item, section, parentDashboard) break case 'directory': { const directoryPath = marshalDirectory(item, section) marshalDirectoryButton(item, fieldset, directoryPath) } break case 'display': marshalDisplay(item, fieldset) break case 'stdout-most-recent-execution': marshalMreOutput(item, fieldset) break case 'link': marshalLink(item, fieldset) break default: } } } function createFieldset (title, parentDashboard) { const legend = document.createElement('legend') legend.innerText = title const fs = document.createElement('fieldset') fs.title = title fs.appendChild(legend) if (typeof parentDashboard === 'undefined') { fs.setAttribute('parent-dashboard', '') } else { fs.setAttribute('parent-dashboard', parentDashboard) } return fs } function marshalFieldset (item, section, parentDashboard) { const fs = createFieldset(item.title, parentDashboard) marshalContainerContents(item, section, fs, parentDashboard) section.appendChild(fs) } function showSectionView (selected) { if (selected === '') { selected = null } for (const fieldset of document.querySelectorAll('fieldset')) { if (selected === null) { if ((fieldset.id === 'root-group' || fieldset.getAttribute('parent-dashboard') !== '') && fieldset.children.length > 1) { fieldset.style.display = 'grid' } else { fieldset.style.display = 'none' } } else { if (fieldset.title === selected) { fieldset.style.display = 'grid' } else { fieldset.style.display = 'none' } } } const current = window.registeredPaths.get(window.currentPath) for (const navLink of document.querySelector('nav').querySelectorAll('a')) { if (navLink.title === current.section) { navLink.classList.add('selected') } else { navLink.classList.remove('selected') } } rebuildH1BreadcrumbNavigation(selected) pushNewNavigationPath(window.currentPath) } function rebuildH1BreadcrumbNavigation () { const title = document.querySelector('h1') title.innerHTML = '' const rootLink = document.createElement('a') rootLink.innerText = window.pageTitle rootLink.href = 'javascript:void(0)' rootLink.onclick = () => { showSection('/') } title.appendChild(rootLink) for (const pathName of window.breadcrumbNavigation) { const sep = document.createElement('span') sep.innerHTML = ' » ' title.append(sep) const path = window.registeredPaths.get(pathName) title.appendChild(createNavigationBreadcrumbDisplay(path)) } document.title = title.innerText } function createNavigationBreadcrumbDisplay (path) { const a = document.createElement('a') a.href = 'javascript:void(0)' if (path.view === null) { a.title = path.section a.innerText = path.section } else { a.innerText = path.view a.title = path.view } a.onclick = () => { showSectionView(path.view) } return a } function marshalDisplay (item, fieldset) { const display = document.createElement('div') display.innerHTML = item.title display.classList.add('display') if (item.cssClass !== '') { display.classList.add(item.cssClass) } fieldset.appendChild(display) } function marshalDirectoryButton (item, fieldset, path) { const directoryButton = document.createElement('button') directoryButton.innerHTML = '' + item.icon + ' ' + item.title directoryButton.onclick = () => { showSection(path) } fieldset.appendChild(directoryButton) } function marshalDirectory (item, section) { const fs = createFieldset(item.title) fs.style.display = 'none' const directoryBackButton = document.createElement('button') directoryBackButton.innerHTML = window.settings.DefaultIconForBack directoryBackButton.title = 'Go back one directory' directoryBackButton.onclick = () => { showSection('/' + section.title) } fs.appendChild(directoryBackButton) marshalContainerContents(item, section, fs) section.appendChild(fs) const path = '/' + section.title + '/' + getSystemTitle(item.title) registerSection(path, section.title, item.title, null) return path } export function marshalLogsJsonToHtml (json) { for (const logEntry of json.logs) { let row = document.getElementById('log-' + logEntry.executionTrackingId) if (row == null) { const tpl = document.getElementById('tplLogRow') row = tpl.content.querySelector('tr').cloneNode(true) row.id = 'log-' + logEntry.executionTrackingId row.querySelector('.content').onclick = () => { window.executionDialog.reset() window.executionDialog.show() window.executionDialog.renderExecutionResult({ logEntry: window.logEntries.get(logEntry.executionTrackingId) }) pushNewNavigationPath('/logs/' + logEntry.executionTrackingId) } row.exitCodeDisplay = new ActionStatusDisplay(row.querySelector('.exit-code')) logEntry.dom = row window.logEntries.set(logEntry.executionTrackingId, logEntry) document.querySelector('#logTableBody').prepend(row) } row.querySelector('.timestamp').innerText = logEntry.datetimeStarted row.querySelector('.content').innerText = logEntry.actionTitle row.querySelector('.icon').innerHTML = logEntry.actionIcon row.setAttribute('title', logEntry.actionTitle) row.exitCodeDisplay.update(logEntry) row.querySelector('.tags').innerHTML = '' for (const tag of logEntry.tags) { row.querySelector('.tags').append(createTag(tag)) } row.querySelector('.tags').append(createAnnotation('user', logEntry.user)) } } window.addEventListener('popstate', (e) => { e.preventDefault() if (e.state != null && typeof e.state.path !== 'undefined') { showSection(e.state.path) } }) export function refreshServerConnectionLabel () { if (window.restAvailable) { document.querySelector('#serverConnectionRest').classList.remove('error') } else { document.querySelector('#serverConnectionRest').classList.add('error') } if (window.websocketAvailable) { document.querySelector('#serverConnectionWebSocket').classList.remove('error') document.querySelector('#serverConnectionWebSocket').innerText = 'WebSocket' } else { document.querySelector('#serverConnectionWebSocket').classList.add('error') document.querySelector('#serverConnectionWebSocket').innerText = 'WebSocket Error' } }