const wizardInfrastructureProvisioning = (function(undefined) { let pollingInterval; let pollingMode; const originalIndicatorClass = 'indicator align-center'; const tabClass = "item tab-ish horizontal space-between"; let onSectionUpdated = (section) => {}; let svg = null; return { id: "infrastructure-provisioning", title: strings.title_InfrastructureProvisioning, description: "description_InfrastructureProvisioning", controller: (file, path, element, state, setState) => { if(!state) { setState({build: 'latest'}); return; } const sections = [{ url: "terraform-global", icon: "cloud-instance.png", name: strings.label_GlobalTerraformProject }]; file.servers.filter(x => file['server-status'][x.id]) .forEach(x => { sections.push({ url: `terraform-local-${x.id}`, icon: x.icon, name: strings.label_NodeTerraformProject(x.id) }); sections.push({ url: `docker-compose-${x.id}`, icon: x.icon, name: strings.label_NodeDockerCompose(x.id) }); }); const waiting = document.getElementById('infrastructure-display-waiting'); const display = document.getElementById('infrastructure-display'); const previousBuilds = document.getElementById('infrastructure-provisioning-previous-builds'); const previousBuildsContainer = document.getElementById('infrastructure-provisioning-previous-builds-container'); const sectionsContainer = element.querySelector(".infrastructure-provisioning-list"); while(sectionsContainer.hasChildNodes()) { sectionsContainer.removeChild(sectionsContainer.lastChild); } sections.forEach(section => { const sectionDiv = document.createElement("div"); sectionDiv.className = `${tabClass} not-clickable`; const sectionGroup = document.createElement("div"); sectionGroup.className = "horizontal"; const sectionIcon = document.createElement("img"); sectionIcon.className = "inline icon"; sectionIcon.src = `images/${section.icon}`; const sectionTitle = document.createElement("span"); sectionTitle.className = "bold"; sectionTitle.textContent = section.name; sectionGroup.appendChild(sectionIcon); sectionGroup.appendChild(sectionTitle); const indicatorContainer = document.createElement("div"); indicatorContainer.className = "horizontal align-center"; const indicator = document.createElement("div"); indicator.className = originalIndicatorClass; const indicatorLight = document.createElement("span"); const indicatorText = document.createElement("span"); indicatorText.textContent = strings.description_IndicatorUnknown; indicatorLight.className = "indicator-light"; indicatorText.className = "indicator-text"; indicator.appendChild(indicatorLight); indicator.appendChild(indicatorText); indicatorContainer.appendChild(indicator); sectionDiv.appendChild(sectionGroup); sectionDiv.appendChild(indicator); sectionsContainer.appendChild(sectionDiv); section.div = sectionDiv; section.indicator = indicator; }); const setIndicator = (status, indicator, indicatorText) => { const isError = status.Error || (status.Complete && !status.Success); const colorClass = isError ? 'error' : 'ok'; const className = `${originalIndicatorClass} ${status.Complete ? '' : 'in-progress'} ${colorClass}`; indicator.className = className; const completeContent = isError ? strings.description_IndicatorError : strings.description_IndicatorOk; const statusWord = status.Complete ? completeContent : strings.description_IndicatorInProgress; const resources = Object.values(status.Status.Modules).flatMap(x => x.Resources); const totalPlanned = resources.filter(x => x.Plan && x.Plan != "none").length; const totalCompleted = resources.filter( x => (x.Plan && x.Plan != "none") && (x.State == "ok" || x.State == "destroyed") ).length; indicatorText.textContent = `${statusWord} (${totalCompleted}/${totalPlanned})`; }; const openModal = (section) => { let selectedTab = 'diagram'; modalService.open( section.name, `
   
${strings.description_IndicatorNotStarted}
${strings.label_Diagram}
${strings.label_Log}
`, (resolve, reject) => { let status = section.statusObject; const element = document.getElementById("modal-body"); const diagram = document.getElementById('infrastructure-diagram'); const log = document.getElementById('infrastructure-log'); const indicator = element.querySelector(".indicator"); const indicatorText = indicator.querySelector(".indicator-text"); document.getElementById('modal-container').className = "modal container dark"; const doTabStuff = () => { Array.from(element.querySelectorAll(".item.tab")).forEach((x, i) => { if(x.dataset.value == selectedTab) { x.classList.add("detail-selected") } else { x.classList.remove("detail-selected") } x.onclick = () => { selectedTab = x.dataset.value; doTabStuff(); } }); diagram.style.display = selectedTab == "diagram" ? "block" : "none"; log.style.display = selectedTab == "log" ? "block" : "none"; }; element.querySelector('.item-detail').style.display = 'block'; doTabStuff(); // when the modal loads, set up & correctly size the diagram svg diagram.innerHTML = section.sanitizedSvgText; svg = document.querySelector('#infrastructure-diagram svg'); const width = Number(svg.getAttribute('width').replace(/[a-z]+/, '')); const height = Number(svg.getAttribute('height').replace(/[a-z]+/, '')); const aspect = width/height; const infraComputedStyle = window.getComputedStyle(diagram); const cssMaxWidthPx = Number(infraComputedStyle.maxWidth.replace('px', '')); const cssHeightPx = Number(infraComputedStyle.height.replace('px', '')); if (cssMaxWidthPx == NaN) { throw new Error("unable to calculate cssMaxWidthPx: got NaN") } if (cssMaxWidthPx == NaN) { throw new Error("unable to calculate cssHeightPx: got NaN") } if(width > cssMaxWidthPx) { svg.setAttribute('width', `${cssMaxWidthPx}px`); svg.setAttribute('height', `${cssMaxWidthPx/aspect}px`); } else { svg.setAttribute('width', `${width}px`); svg.setAttribute('height', `${height}px`); svg.style.margin = `${(cssHeightPx-height)/2}px ${(cssMaxWidthPx-width)/2}px `; } onSectionUpdated = (otherSection) => { // only update the modal when the current (open modal) section is updated if(otherSection.url != section.url) { return; } status = otherSection.statusObject; setIndicator(status, indicator, indicatorText); // STEP 1: html-ize the ansi-colored log file and insert it into the page const htmlWithInlineStyles = new AnsiToHtml().toHtml(status.Log); // This is a silly hack to get around the Content Security Policy not allowing inline CSS // first we replace all the inline styles with ids and record the value of the inline style for that id const stylesToApply = []; const htmlWithIds = htmlWithInlineStyles.replace( /style="[^"]+"/g, (style) => { toReturn = `id="ansicolor-${stylesToApply.length}"`; stylesToApply.push(style); return toReturn; } ); // now we can load it into the page because all the inline styles have been replaced. log.innerHTML = DOMPurify.sanitize(htmlWithIds); // finally, we go back through and re-apply the colors that we recorded by id. stylesToApply.forEach((style, i) => { const colorMatch = style.match(/color\s?:\s*(#[A-Fa-f0-9]+)/); if(colorMatch) { document.getElementById(`ansicolor-${i}`).style.color = colorMatch[1]; } }); // STEP 2: update the css classes on the svg based on the terraform apply status to animate it. const moduleAllResourcesOk = {}; Object.entries(status.Status.Modules).forEach(([moduleName, module]) => { const moduleElement = svg.querySelector(`[data-dot=${moduleName.replace(/[.-]/g, "_")}]`); let allResourcesOk = true; module.Resources.forEach(resource => { if(resource.State != "ok" && resource.ResourceType != "") { allResourcesOk = false; } const dotAddress = `${moduleName}_${resource.DisplayName}`.replace(/[.-]/g, "_"); const element = svg.querySelector(`[data-dot=${dotAddress}]`); if(element) { element.setAttribute("class", `resource ${resource.State}`); //console.log(dotAddress, resource.Plan, resource.State); } }); if (moduleElement) { moduleElement.setAttribute("class", `module ${allResourcesOk ? "" : "waiting"}`); } moduleAllResourcesOk[moduleName] = allResourcesOk; }); status.Status.Connections.forEach(connection => { const paramDot = `param_${connection.DisplayName.replace(/[.-]/g, "_")}`; const fromDot = connection.From.replace(/[.-]/g, "_"); const toDot = connection.To.replace(/[.-]/g, "_"); const param = svg.querySelector(`[data-dot="${paramDot}"]`); const from = svg.querySelector(`[data-dot="${fromDot}->${paramDot}"]`); const to = svg.querySelector(`[data-dot="${paramDot}->${toDot}"]`); const elements = [param, from, to]; elements.forEach(el => { if(el) { if(moduleAllResourcesOk[connection.From] || connection.From.startsWith("var") || connection.From.startsWith("data")) { el.classList.remove("waiting"); } else { el.classList.add("waiting"); } } }); }); }; onSectionUpdated(section); }, [{ innerHTML: strings.button_Back, escapeKey: true, onclick: (resolve, reject) => resolve() }] ) }; const poll = () => { const objList = pollingMode ? invokeObjectStorageListPolling : invokeObjectStorageList; const objGet = pollingMode ? invokeObjectStorageGetPolling : invokeObjectStorageGet; objList(`rootsystem/automation/`).then(results => { const buildDirectories = results.filter(x => x.isDirectory && Number(x.name) != NaN); if(buildDirectories.length == 0) { return; } previousBuildsContainer.style.display = buildDirectories.length > 1 ? 'block' : 'none'; let currentBuildDirectoryIndex = buildDirectories.length - 1; if(state.build != 'latest') { buildDirectories.forEach((x, i) => { if(Number(x.name) == state.build) { currentBuildDirectoryIndex = i; } }); } sections.forEach(section => { const indicatorText = section.indicator.querySelector('.indicator-text'); const buildNumber = buildDirectories[currentBuildDirectoryIndex].name; const url = `rootsystem/automation/${buildNumber}/${section.url}`; if(!section.diagramSvgText) { objGet(`${url}/diagram.svg`).then( (svgResult) => { if(!svgResult.notFound) { section.div.className = tabClass; section.div.onclick = () => openModal(section); section.sanitizedSvgText = DOMPurify.sanitize(svgResult.file.content); } } ); } objGet(`${url}/status.json`).then( (statusResult) => { if(statusResult.notFound) { section.indicator.className = originalIndicatorClass; indicatorText.textContent = strings.description_IndicatorNotStarted; return } const status = JSON.parse(statusResult.file.content); section.statusObject = status; //if(section.url.includes('docker')) { console.log(status); } console.log("asd"); onSectionUpdated(section); setIndicator(status, section.indicator, indicatorText); } ); }); const listBuildDirectories = buildDirectories.map(x => { return objList(`rootsystem/automation/${x.name}/terraform-global/`); }); Promise.all(listBuildDirectories).then(buildDirsContents => { while(previousBuilds.hasChildNodes()) { previousBuilds.removeChild(previousBuilds.lastChild); } const latestOption = document.createElement('option'); latestOption.value = "latest"; const latestBuildDirContents = buildDirsContents[buildDirsContents.length-1]; const latestBuildDir = buildDirectories[buildDirectories.length-1]; const lastModified = latestBuildDirContents.filter(y => !y.isDirectory)[0].lastModified; latestOption.textContent = `latest (#${latestBuildDir.name} - ${new Date(lastModified).toLocaleString()})`; latestOption.selected = state.build == 'latest'; buildDirectories.forEach((directory, i) => { const option = document.createElement('option'); option.value = Number(directory.name); const lastModified = buildDirsContents[i].filter(y => !y.isDirectory)[0].lastModified; option.textContent = `#${directory.name} - ${new Date(lastModified).toLocaleString()}`; option.selected = state.build == option.value; previousBuilds.prepend(option); }); previousBuilds.prepend(latestOption); pollingMode = true; display.style.display = 'block'; waiting.style.display = 'none'; }); }); }; const resetDisplayAndClearInterval = () => { waiting.style.display = 'block'; display.style.display = 'none'; sections.forEach(section => { if(section.indicator) { section.indicator.className = originalIndicatorClass; section.indicator.querySelector('.indicator-text').textContent = strings.description_IndicatorUnknown; } }); clearInterval(pollingInterval); pollingInterval = null; }; previousBuilds.onchange = () => { resetDisplayAndClearInterval(); setState({ ...state, build: previousBuilds.value }); }; if(!pollingInterval) { pollingMode = false; pollingInterval = setInterval(() => { // clear the interval and exit early if the user navigated away from this screen. if(element.style.display != "block") { resetDisplayAndClearInterval(); return; } poll(); }, 5000); poll(); } } }; })()