You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
17 KiB
428 lines
17 KiB
|
|
|
|
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, |
|
` |
|
<div class="item-header horizontal align-center"> |
|
<div class="item tab-ish not-clickable"> |
|
<div class="horizontal align-center"> |
|
<img class="inline icon" src="images/${section.icon}"/> |
|
|
|
|
|
<div class="horizontal align-center"> |
|
<div class="indicator align-center"> |
|
<span class="indicator-light"></span> |
|
<span class="indicator-text">${strings.description_IndicatorNotStarted}</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="flex-grow"></div> |
|
|
|
<div class="item tab" data-value="diagram"> |
|
<span class="bold">${strings.label_Diagram}</span> |
|
</div> |
|
<div class="item tab" data-value="log"> |
|
<span class="bold">${strings.label_Log}</span> |
|
</div> |
|
</div> |
|
<div class="item-detail"> |
|
<div id="infrastructure-diagram" class="embedded-content"></div> |
|
<div id="infrastructure-log" class="embedded-content log-content display-none"></div> |
|
</div> |
|
`, |
|
(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(); |
|
} |
|
|
|
|
|
} |
|
}; |
|
})() |