User-facing desktop application for server.garden
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

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}"/>
&nbsp;
&nbsp;
<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();
}
}
};
})()