mirror of
https://github.com/esphome/esphome.git
synced 2025-04-08 11:50:34 +01:00
1022 lines
28 KiB
JavaScript
1022 lines
28 KiB
JavaScript
'use strict';
|
|
|
|
// Document Ready
|
|
$(document).ready(function () {
|
|
M.AutoInit(document.body);
|
|
nodeGrid();
|
|
startAceWebsocket();
|
|
fixNavbarHeight();
|
|
});
|
|
|
|
// WebSocket URL Helper
|
|
const loc = window.location;
|
|
const wsLoc = new URL("./", `${loc.protocol}//${loc.host}${loc.pathname}`);
|
|
wsLoc.protocol = 'ws:';
|
|
if (loc.protocol === "https:") {
|
|
wsLoc.protocol = 'wss:';
|
|
}
|
|
const wsUrl = wsLoc.href;
|
|
|
|
/**
|
|
* Fix NavBar height
|
|
*/
|
|
const fixNavbarHeight = () => {
|
|
const fixFunc = () => {
|
|
const sel = $(".select-wrapper");
|
|
$(".navbar-fixed").css("height", (sel.position().top + sel.outerHeight()) + "px");
|
|
}
|
|
$(window).resize(fixFunc);
|
|
fixFunc();
|
|
}
|
|
|
|
/**
|
|
* Dashboard Dynamic Grid
|
|
*/
|
|
const nodeGrid = () => {
|
|
const nodeCount = document.querySelectorAll("#nodes .card").length;
|
|
const nodeGrid = document.querySelector("#nodes #grid");
|
|
|
|
if (nodeCount <= 3) {
|
|
nodeGrid.classList.add("grid-1-col");
|
|
} else if (nodeCount <= 6) {
|
|
nodeGrid.classList.add("grid-2-col");
|
|
} else {
|
|
nodeGrid.classList.add("grid-3-col");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Online/ Offline Status Indication
|
|
*/
|
|
|
|
let isFetchingPing = false;
|
|
|
|
const fetchPing = () => {
|
|
if (isFetchingPing) {
|
|
return;
|
|
}
|
|
|
|
isFetchingPing = true;
|
|
|
|
fetch(`./ping`, { credentials: "same-origin" }).then(res => res.json())
|
|
.then(response => {
|
|
for (let filename in response) {
|
|
let node = document.querySelector(`#nodes .card[data-filename="${filename}"]`);
|
|
|
|
if (node === null) {
|
|
continue;
|
|
}
|
|
|
|
let status = response[filename];
|
|
let className;
|
|
|
|
if (status === null) {
|
|
className = 'status-unknown';
|
|
} else if (status === true) {
|
|
className = 'status-online';
|
|
node.setAttribute('data-last-connected', Date.now().toString());
|
|
} else if (node.hasAttribute('data-last-connected')) {
|
|
const attr = parseInt(node.getAttribute('data-last-connected'));
|
|
if (Date.now() - attr <= 5000) {
|
|
className = 'status-not-responding';
|
|
} else {
|
|
className = 'status-offline';
|
|
}
|
|
} else {
|
|
className = 'status-offline';
|
|
}
|
|
|
|
if (node.classList.contains(className)) {
|
|
continue;
|
|
}
|
|
|
|
node.classList.remove('status-unknown', 'status-online', 'status-offline', 'status-not-responding');
|
|
node.classList.add(className);
|
|
}
|
|
|
|
isFetchingPing = false;
|
|
});
|
|
};
|
|
setInterval(fetchPing, 2000);
|
|
fetchPing();
|
|
|
|
/**
|
|
* Log Color Parsing
|
|
*/
|
|
|
|
const initializeColorState = () => {
|
|
return {
|
|
bold: false,
|
|
italic: false,
|
|
underline: false,
|
|
strikethrough: false,
|
|
foregroundColor: false,
|
|
backgroundColor: false,
|
|
carriageReturn: false,
|
|
secret: false,
|
|
};
|
|
};
|
|
|
|
const colorReplace = (pre, state, text) => {
|
|
const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
|
let i = 0;
|
|
|
|
if (state.carriageReturn) {
|
|
if (text !== "\n") {
|
|
// don't remove if \r\n
|
|
pre.removeChild(pre.lastChild);
|
|
}
|
|
state.carriageReturn = false;
|
|
}
|
|
|
|
if (text.includes("\r")) {
|
|
state.carriageReturn = true;
|
|
}
|
|
|
|
const lineSpan = document.createElement("span");
|
|
lineSpan.classList.add("line");
|
|
pre.appendChild(lineSpan);
|
|
|
|
const addSpan = (content) => {
|
|
if (content === "")
|
|
return;
|
|
|
|
const span = document.createElement("span");
|
|
if (state.bold) span.classList.add("log-bold");
|
|
if (state.italic) span.classList.add("log-italic");
|
|
if (state.underline) span.classList.add("log-underline");
|
|
if (state.strikethrough) span.classList.add("log-strikethrough");
|
|
if (state.secret) span.classList.add("log-secret");
|
|
if (state.foregroundColor !== null) span.classList.add(`log-fg-${state.foregroundColor}`);
|
|
if (state.backgroundColor !== null) span.classList.add(`log-bg-${state.backgroundColor}`);
|
|
span.appendChild(document.createTextNode(content));
|
|
lineSpan.appendChild(span);
|
|
|
|
if (state.secret) {
|
|
const redacted = document.createElement("span");
|
|
redacted.classList.add("log-secret-redacted");
|
|
redacted.appendChild(document.createTextNode("[redacted]"));
|
|
lineSpan.appendChild(redacted);
|
|
}
|
|
};
|
|
|
|
|
|
while (true) {
|
|
const match = re.exec(text);
|
|
if (match === null)
|
|
break;
|
|
|
|
const j = match.index;
|
|
addSpan(text.substring(i, j));
|
|
i = j + match[0].length;
|
|
|
|
if (match[1] === undefined) continue;
|
|
|
|
for (const colorCode of match[1].split(";")) {
|
|
switch (parseInt(colorCode)) {
|
|
case 0:
|
|
// reset
|
|
state.bold = false;
|
|
state.italic = false;
|
|
state.underline = false;
|
|
state.strikethrough = false;
|
|
state.foregroundColor = null;
|
|
state.backgroundColor = null;
|
|
state.secret = false;
|
|
break;
|
|
case 1:
|
|
state.bold = true;
|
|
break;
|
|
case 3:
|
|
state.italic = true;
|
|
break;
|
|
case 4:
|
|
state.underline = true;
|
|
break;
|
|
case 5:
|
|
state.secret = true;
|
|
break;
|
|
case 6:
|
|
state.secret = false;
|
|
break;
|
|
case 9:
|
|
state.strikethrough = true;
|
|
break;
|
|
case 22:
|
|
state.bold = false;
|
|
break;
|
|
case 23:
|
|
state.italic = false;
|
|
break;
|
|
case 24:
|
|
state.underline = false;
|
|
break;
|
|
case 29:
|
|
state.strikethrough = false;
|
|
break;
|
|
case 30:
|
|
state.foregroundColor = "black";
|
|
break;
|
|
case 31:
|
|
state.foregroundColor = "red";
|
|
break;
|
|
case 32:
|
|
state.foregroundColor = "green";
|
|
break;
|
|
case 33:
|
|
state.foregroundColor = "yellow";
|
|
break;
|
|
case 34:
|
|
state.foregroundColor = "blue";
|
|
break;
|
|
case 35:
|
|
state.foregroundColor = "magenta";
|
|
break;
|
|
case 36:
|
|
state.foregroundColor = "cyan";
|
|
break;
|
|
case 37:
|
|
state.foregroundColor = "white";
|
|
break;
|
|
case 39:
|
|
state.foregroundColor = null;
|
|
break;
|
|
case 41:
|
|
state.backgroundColor = "red";
|
|
break;
|
|
case 42:
|
|
state.backgroundColor = "green";
|
|
break;
|
|
case 43:
|
|
state.backgroundColor = "yellow";
|
|
break;
|
|
case 44:
|
|
state.backgroundColor = "blue";
|
|
break;
|
|
case 45:
|
|
state.backgroundColor = "magenta";
|
|
break;
|
|
case 46:
|
|
state.backgroundColor = "cyan";
|
|
break;
|
|
case 47:
|
|
state.backgroundColor = "white";
|
|
break;
|
|
case 40:
|
|
case 49:
|
|
state.backgroundColor = null;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
addSpan(text.substring(i));
|
|
if (pre.scrollTop + 56 >= (pre.scrollHeight - pre.offsetHeight)) {
|
|
// at bottom
|
|
pre.scrollTop = pre.scrollHeight;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Serial Port Selection
|
|
*/
|
|
|
|
const portSelect = document.querySelector('.nav-wrapper select');
|
|
let ports = [];
|
|
|
|
const fetchSerialPorts = (begin = false) => {
|
|
fetch(`./serial-ports`, { credentials: "same-origin" }).then(res => res.json())
|
|
.then(response => {
|
|
if (ports.length === response.length) {
|
|
let allEqual = true;
|
|
for (let i = 0; i < response.length; i++) {
|
|
if (ports[i].port !== response[i].port) {
|
|
allEqual = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allEqual)
|
|
return;
|
|
}
|
|
const hasNewPort = response.length >= ports.length;
|
|
|
|
ports = response;
|
|
|
|
const inst = M.FormSelect.getInstance(portSelect);
|
|
if (inst !== undefined) {
|
|
inst.destroy();
|
|
}
|
|
|
|
portSelect.innerHTML = "";
|
|
const prevSelected = getUploadPort();
|
|
for (let i = 0; i < response.length; i++) {
|
|
const val = response[i];
|
|
if (val.port === prevSelected) {
|
|
portSelect.innerHTML += `<option value="${val.port}" selected>${val.port} (${val.desc})</option>`;
|
|
} else {
|
|
portSelect.innerHTML += `<option value="${val.port}">${val.port} (${val.desc})</option>`;
|
|
}
|
|
}
|
|
|
|
M.FormSelect.init(portSelect, {});
|
|
if (!begin && hasNewPort)
|
|
M.toast({ html: "Discovered new serial port." });
|
|
});
|
|
};
|
|
|
|
const getUploadPort = () => {
|
|
const inst = M.FormSelect.getInstance(portSelect);
|
|
if (inst === undefined) {
|
|
return "OTA";
|
|
}
|
|
|
|
inst._setSelectedStates();
|
|
return inst.getSelectedValues()[0];
|
|
};
|
|
setInterval(fetchSerialPorts, 5000);
|
|
fetchSerialPorts(true);
|
|
|
|
/**
|
|
* Log Elements
|
|
*/
|
|
|
|
// Log Modal Class
|
|
class LogModal {
|
|
constructor({
|
|
name,
|
|
onPrepare = (modalElement, config) => { },
|
|
onProcessExit = (modalElement, code) => { },
|
|
onSocketClose = (modalElement) => { },
|
|
dismissible = true
|
|
}) {
|
|
this.modalId = `js-${name}-modal`;
|
|
this.dataAction = `${name}`;
|
|
this.wsUrl = `${wsUrl}${name}`;
|
|
this.dismissible = dismissible;
|
|
this.activeFilename = null;
|
|
|
|
this.modalElement = document.getElementById(this.modalId);
|
|
this.nodeFilenameElement = document.querySelector(`#${this.modalId} #js-node-filename`);
|
|
this.logElement = document.querySelector(`#${this.modalId} #js-log-area`);
|
|
this.onPrepare = onPrepare;
|
|
this.onProcessExit = onProcessExit;
|
|
this.onSocketClose = onSocketClose;
|
|
}
|
|
|
|
setup() {
|
|
const boundOnPress = this._onPress.bind(this);
|
|
document.querySelectorAll(`[data-action="${this.dataAction}"]`).forEach((button) => {
|
|
button.addEventListener('click', boundOnPress);
|
|
});
|
|
}
|
|
|
|
_setupModalInstance() {
|
|
this.modalInstance = M.Modal.init(this.modalElement, {
|
|
onOpenStart: this._onOpenStart.bind(this),
|
|
onCloseStart: this._onCloseStart.bind(this),
|
|
dismissible: this.dismissible
|
|
})
|
|
}
|
|
|
|
_onOpenStart() {
|
|
document.addEventListener('keydown', this._boundKeydown);
|
|
}
|
|
|
|
_onCloseStart() {
|
|
document.removeEventListener('keydown', this._boundKeydown);
|
|
this.activeSocket.close();
|
|
}
|
|
|
|
open(event) {
|
|
this._onPress(event);
|
|
}
|
|
|
|
_onPress(event) {
|
|
this.activeFilename = event.target.getAttribute('data-filename');
|
|
|
|
this._setupModalInstance();
|
|
this.nodeFilenameElement.innerHTML = this.activeFilename;
|
|
|
|
this.logElement.innerHTML = "";
|
|
const colorLogState = initializeColorState();
|
|
|
|
this.onPrepare(this.modalElement, this.activeFilename);
|
|
|
|
let stopped = false;
|
|
|
|
this.modalInstance.open();
|
|
|
|
const socket = new WebSocket(this.wsUrl);
|
|
this.activeSocket = socket;
|
|
socket.addEventListener('message', (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.event === "line") {
|
|
colorReplace(this.logElement, colorLogState, data.data);
|
|
} else if (data.event === "exit") {
|
|
this.onProcessExit(this.modalElement, data.code);
|
|
stopped = true;
|
|
}
|
|
});
|
|
|
|
socket.addEventListener('open', () => {
|
|
const msg = JSON.stringify(this._encodeSpawnMessage(this.activeFilename));
|
|
socket.send(msg);
|
|
});
|
|
|
|
socket.addEventListener('close', () => {
|
|
if (!stopped) {
|
|
this.onSocketClose(this.modalElement);
|
|
}
|
|
});
|
|
}
|
|
|
|
_onKeyDown(event) {
|
|
// Close on escape key
|
|
if (event.keyCode === 27) {
|
|
this.modalInstance.close();
|
|
}
|
|
}
|
|
|
|
_encodeSpawnMessage(filename) {
|
|
return {
|
|
type: 'spawn',
|
|
configuration: filename,
|
|
port: getUploadPort(),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Logs Modal
|
|
const logsModal = new LogModal({
|
|
name: "logs",
|
|
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("[data-action='stop-logs']").innerHTML = "Stop";
|
|
},
|
|
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: "Program exited successfully",
|
|
displayLength: 10000
|
|
});
|
|
} else {
|
|
M.toast({
|
|
html: `Program failed with code ${code}`,
|
|
displayLength: 10000
|
|
});
|
|
}
|
|
modalElem.querySelector("data-action='stop-logs'").innerHTML = "Close";
|
|
},
|
|
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000
|
|
});
|
|
}
|
|
})
|
|
|
|
logsModal.setup();
|
|
|
|
// Upload Modal
|
|
const uploadModal = new LogModal({
|
|
name: "upload",
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("#js-upload-modal [data-action='download-binary']").classList.add('disabled');
|
|
modalElement.querySelector("#js-upload-modal [data-action='upload']").setAttribute('data-filename', activeFilename);
|
|
modalElement.querySelector("#js-upload-modal [data-action='upload']").classList.add('disabled');
|
|
modalElement.querySelector("#js-upload-modal [data-action='edit']").setAttribute('data-filename', activeFilename);
|
|
modalElement.querySelector("#js-upload-modal [data-action='stop-logs']").innerHTML = "Stop";
|
|
},
|
|
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: "Program exited successfully",
|
|
displayLength: 10000
|
|
});
|
|
|
|
modalElement.querySelector("#js-upload-modal [data-action='download-binary']").classList.remove('disabled');
|
|
} else {
|
|
M.toast({
|
|
html: `Program failed with code ${code}`,
|
|
displayLength: 10000
|
|
});
|
|
|
|
modalElement.querySelector("#js-upload-modal [data-action='upload']").classList.add('disabled');
|
|
modalElement.querySelector("#js-upload-modal [data-action='upload']").classList.remove('disabled');
|
|
}
|
|
modalElement.querySelector("#js-upload-modal [data-action='stop-logs']").innerHTML = "Close";
|
|
},
|
|
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000
|
|
});
|
|
},
|
|
|
|
dismissible: false
|
|
})
|
|
|
|
uploadModal.setup();
|
|
|
|
const downloadAfterUploadButton = document.querySelector("#js-upload-modal [data-action='download-binary']");
|
|
downloadAfterUploadButton.addEventListener('click', () => {
|
|
const link = document.createElement("a");
|
|
link.download = name;
|
|
link.href = `./download.bin?configuration=${encodeURIComponent(uploadModal.activeFilename)}`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
});
|
|
|
|
// Validate Modal
|
|
const validateModal = new LogModal({
|
|
name: 'validate',
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("#js-validate-modal [data-action='stop-logs']").innerHTML = "Stop";
|
|
modalElement.querySelector("#js-validate-modal [data-action='edit']").setAttribute('data-filename', activeFilename);
|
|
modalElement.querySelector("#js-validate-modal [data-action='upload']").setAttribute('data-filename', activeFilename);
|
|
modalElement.querySelector("#js-validate-modal [data-action='upload']").classList.add('disabled');
|
|
},
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: `<code class="inlinecode">${validateModal.activeFilename}</code> is valid 👍`,
|
|
displayLength: 10000,
|
|
});
|
|
modalElement.querySelector("#js-validate-modal [data-action='upload']").classList.remove('disabled');
|
|
} else {
|
|
M.toast({
|
|
html: `<code class="inlinecode">${validateModal.activeFilename}</code> is invalid 😕`,
|
|
displayLength: 10000,
|
|
});
|
|
}
|
|
modalElement.querySelector("#js-validate-modal [data-action='stop-logs']").innerHTML = "Close";
|
|
},
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000,
|
|
});
|
|
},
|
|
});
|
|
|
|
validateModal.setup();
|
|
|
|
// Compile Modal
|
|
const compileModal = new LogModal({
|
|
name: 'compile',
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("#js-compile-modal [data-action='stop-logs']").innerHTML = "Stop";
|
|
modalElement.querySelector("#js-compile-modal [data-action='download-binary']").classList.add('disabled');
|
|
},
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: "Program exited successfully",
|
|
displayLength: 10000,
|
|
});
|
|
modalElement.querySelector("#js-compile-modal [data-action='download-binary']").classList.remove('disabled');
|
|
} else {
|
|
M.toast({
|
|
html: `Program failed with code ${data.code}`,
|
|
displayLength: 10000,
|
|
});
|
|
}
|
|
modalElement.querySelector("#js-compile-modal [data-action='stop-logs']").innerHTML = "Close";
|
|
},
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000,
|
|
});
|
|
},
|
|
dismissible: false,
|
|
});
|
|
|
|
compileModal.setup();
|
|
|
|
const downloadAfterCompileButton = document.querySelector("#js-compile-modal [data-action='download-binary']");
|
|
downloadAfterCompileButton.addEventListener('click', () => {
|
|
const link = document.createElement("a");
|
|
link.download = name;
|
|
link.href = `./download.bin?configuration=${encodeURIComponent(compileModal.activeFilename)}`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
});
|
|
|
|
// Clean MQTT Modal
|
|
const cleanMqttModal = new LogModal({
|
|
name: 'clean-mqtt',
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("#js-clean-mqtt-modal [data-action='stop-logs']").innerHTML = "Stop";
|
|
},
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: "Program exited successfully",
|
|
displayLength: 10000,
|
|
});
|
|
} else {
|
|
M.toast({
|
|
html: `Program failed with code ${code}`,
|
|
displayLength: 10000,
|
|
});
|
|
}
|
|
modalElement.querySelector("#js-clean-mqtt-modal [data-action='stop-logs']").innerHTML = "Close";
|
|
},
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000,
|
|
});
|
|
},
|
|
});
|
|
|
|
cleanMqttModal.setup();
|
|
|
|
// Clean Build Files Modal
|
|
const cleanModal = new LogModal({
|
|
name: 'clean',
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("#js-clean-modal [data-action='stop-logs']").innerHTML = "Stop";
|
|
},
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: "Program exited successfully",
|
|
displayLength: 10000,
|
|
});
|
|
} else {
|
|
M.toast({
|
|
html: `Program failed with code ${code}`,
|
|
displayLength: 10000,
|
|
});
|
|
}
|
|
modalElement.querySelector("#js-clean-modal [data-action='stop-logs']").innerHTML = "Close";
|
|
},
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000,
|
|
});
|
|
},
|
|
});
|
|
|
|
cleanModal.setup();
|
|
|
|
// Update All Modal
|
|
const updateAllModal = new LogModal({
|
|
name: 'update-all',
|
|
onPrepare: (modalElement, activeFilename) => {
|
|
modalElement.querySelector("#js-update-all-modal [data-action='stop-logs']").innerHTML = "Stop";
|
|
modalElement.querySelector("#js-update-all-modal #js-node-filename").style.visibility = "hidden";
|
|
},
|
|
onProcessExit: (modalElement, code) => {
|
|
if (code === 0) {
|
|
M.toast({
|
|
html: "Program exited successfully",
|
|
displayLength: 10000,
|
|
});
|
|
downloadButton.classList.remove('disabled');
|
|
} else {
|
|
M.toast({
|
|
html: `Program failed with code ${data.code}`,
|
|
displayLength: 10000,
|
|
});
|
|
}
|
|
modalElement.querySelector("#js-update-all-modal [data-action='stop-logs']").innerHTML = "Close";
|
|
},
|
|
onSocketClose: (modalElement) => {
|
|
M.toast({
|
|
html: 'Terminated process',
|
|
displayLength: 10000,
|
|
});
|
|
},
|
|
dismissible: false,
|
|
});
|
|
|
|
updateAllModal.setup();
|
|
|
|
/**
|
|
* Node Editing
|
|
*/
|
|
|
|
let editorActiveFilename = null;
|
|
let editorActiveSecrets = false;
|
|
let editorActiveWebSocket = null;
|
|
let editorValidationScheduled = false;
|
|
let editorValidationRunning = false;
|
|
|
|
// Setup Editor
|
|
const editorElement = document.querySelector("#js-editor-modal #js-editor-area");
|
|
const editor = ace.edit(editorElement);
|
|
|
|
editor.setOptions({
|
|
highlightActiveLine: true,
|
|
showPrintMargin: true,
|
|
useSoftTabs: true,
|
|
tabSize: 2,
|
|
useWorker: false,
|
|
theme: 'ace/theme/dreamweaver',
|
|
mode: 'ace/mode/yaml'
|
|
});
|
|
|
|
editor.commands.addCommand({
|
|
name: 'saveCommand',
|
|
bindKey: { win: 'Ctrl-S', mac: 'Command-S' },
|
|
exec: function () {
|
|
saveFile(editorActiveFilename);
|
|
},
|
|
readOnly: false
|
|
});
|
|
|
|
// Edit Button Listener
|
|
document.querySelectorAll("[data-action='edit']").forEach((button) => {
|
|
button.addEventListener('click', (event) => {
|
|
|
|
editorActiveFilename = event.target.getAttribute("data-filename");
|
|
const filenameField = document.querySelector("#js-editor-modal #js-node-filename");
|
|
filenameField.innerHTML = editorActiveFilename;
|
|
|
|
const saveButton = document.querySelector("#js-editor-modal [data-action='save']");
|
|
const uploadButton = document.querySelector("#js-editor-modal [data-action='upload']");
|
|
const closeButton = document.querySelector("#js-editor-modal [data-action='close']");
|
|
saveButton.setAttribute('data-filename', editorActiveFilename);
|
|
uploadButton.setAttribute('data-filename', editorActiveFilename);
|
|
uploadButton.setAttribute('onClick', `saveFile("${editorActiveFilename}")`);
|
|
if (editorActiveFilename === "secrets.yaml") {
|
|
uploadButton.classList.add("disabled");
|
|
editorActiveSecrets = true;
|
|
} else {
|
|
uploadButton.classList.remove("disabled");
|
|
editorActiveSecrets = false;
|
|
}
|
|
closeButton.setAttribute('data-filename', editorActiveFilename);
|
|
|
|
const loadingIndicator = document.querySelector("#js-editor-modal #js-loading-indicator");
|
|
const editorArea = document.querySelector("#js-editor-modal #js-editor-area");
|
|
|
|
loadingIndicator.style.display = "block";
|
|
editorArea.style.display = "none";
|
|
|
|
editor.setOption('readOnly', true);
|
|
fetch(`./edit?configuration=${editorActiveFilename}`, { credentials: "same-origin" })
|
|
.then(res => res.text()).then(response => {
|
|
editor.setValue(response, -1);
|
|
editor.setOption('readOnly', false);
|
|
loadingIndicator.style.display = "none";
|
|
editorArea.style.display = "block";
|
|
});
|
|
editor.focus();
|
|
|
|
const editModalElement = document.getElementById("js-editor-modal");
|
|
const editorModal = M.Modal.init(editModalElement, {
|
|
onOpenStart: function () {
|
|
editorModalOnOpen()
|
|
},
|
|
onCloseStart: function () {
|
|
editorModalOnClose()
|
|
},
|
|
dismissible: false
|
|
})
|
|
|
|
editorModal.open();
|
|
|
|
});
|
|
});
|
|
|
|
// Editor On Open
|
|
const editorModalOnOpen = () => {
|
|
return
|
|
}
|
|
|
|
// Editor On Close
|
|
const editorModalOnClose = () => {
|
|
editorActiveFilename = null;
|
|
}
|
|
|
|
// Editor WebSocket Validation
|
|
const startAceWebsocket = () => {
|
|
editorActiveWebSocket = new WebSocket(`${wsUrl}ace`);
|
|
|
|
editorActiveWebSocket.addEventListener('message', (event) => {
|
|
const raw = JSON.parse(event.data);
|
|
if (raw.event === "line") {
|
|
const msg = JSON.parse(raw.data);
|
|
if (msg.type === "result") {
|
|
const arr = [];
|
|
|
|
for (const v of msg.validation_errors) {
|
|
let o = {
|
|
text: v.message,
|
|
type: 'error',
|
|
row: 0,
|
|
column: 0
|
|
};
|
|
if (v.range != null) {
|
|
o.row = v.range.start_line;
|
|
o.column = v.range.start_col;
|
|
}
|
|
arr.push(o);
|
|
}
|
|
for (const v of msg.yaml_errors) {
|
|
arr.push({
|
|
text: v.message,
|
|
type: 'error',
|
|
row: 0,
|
|
column: 0
|
|
});
|
|
}
|
|
|
|
editor.session.setAnnotations(arr);
|
|
|
|
if (arr.length) {
|
|
document.querySelector("#js-editor-modal [data-action='upload']").classList.add('disabled');
|
|
} else {
|
|
document.querySelector("#js-editor-modal [data-action='upload']").classList.remove('disabled');
|
|
}
|
|
|
|
editorValidationRunning = false;
|
|
} else if (msg.type === "read_file") {
|
|
sendAceStdin({
|
|
type: 'file_response',
|
|
content: editor.getValue()
|
|
});
|
|
}
|
|
}
|
|
})
|
|
|
|
editorActiveWebSocket.addEventListener('open', () => {
|
|
const msg = JSON.stringify({ type: 'spawn' });
|
|
editorActiveWebSocket.send(msg);
|
|
});
|
|
|
|
editorActiveWebSocket.addEventListener('close', () => {
|
|
editorActiveWebSocket = null;
|
|
setTimeout(startAceWebsocket, 5000);
|
|
});
|
|
};
|
|
|
|
const sendAceStdin = (data) => {
|
|
let send = JSON.stringify({
|
|
type: 'stdin',
|
|
data: JSON.stringify(data) + '\n',
|
|
});
|
|
editorActiveWebSocket.send(send);
|
|
};
|
|
|
|
const debounce = (func, wait) => {
|
|
let timeout;
|
|
return function () {
|
|
let context = this, args = arguments;
|
|
let later = function () {
|
|
timeout = null;
|
|
func.apply(context, args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
};
|
|
|
|
editor.session.on('change', debounce(() => {
|
|
editorValidationScheduled = !editorActiveSecrets;
|
|
}, 250));
|
|
|
|
setInterval(() => {
|
|
if (!editorValidationScheduled || editorValidationRunning)
|
|
return;
|
|
if (editorActiveWebSocket == null)
|
|
return;
|
|
|
|
sendAceStdin({
|
|
type: 'validate',
|
|
file: editorActiveFilename
|
|
});
|
|
editorValidationRunning = true;
|
|
editorValidationScheduled = false;
|
|
}, 100);
|
|
|
|
// Save File
|
|
const saveFile = (filename) => {
|
|
const extensionRegex = new RegExp("(?:\.([^.]+))?$");
|
|
|
|
if (filename.match(extensionRegex)[0] !== ".yaml") {
|
|
M.toast({
|
|
html: `❌ File <code class="inlinecode">${filename}</code> cannot be saved as it is not a YAML file!`,
|
|
displayLength: 10000
|
|
});
|
|
return;
|
|
}
|
|
|
|
fetch(`./edit?configuration=${filename}`, {
|
|
credentials: "same-origin",
|
|
method: "POST",
|
|
body: editor.getValue()
|
|
})
|
|
.then((response) => {
|
|
response.text();
|
|
})
|
|
.then(() => {
|
|
M.toast({
|
|
html: `✅ Saved <code class="inlinecode">${filename}</code>`,
|
|
displayLength: 10000
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
M.toast({
|
|
html: `❌ An error occured saving <code class="inlinecode">${filename}</code>`,
|
|
displayLength: 10000
|
|
});
|
|
})
|
|
}
|
|
|
|
document.querySelectorAll("[data-action='save']").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
saveFile(editorActiveFilename);
|
|
});
|
|
});
|
|
|
|
// Delete Node
|
|
document.querySelectorAll("[data-action='delete']").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
const filename = e.target.getAttribute("data-filename");
|
|
|
|
fetch(`./delete?configuration=${filename}`, {
|
|
credentials: "same-origin",
|
|
method: "POST",
|
|
})
|
|
.then((res) => {
|
|
res.text()
|
|
})
|
|
.then(() => {
|
|
const toastHtml = `<span>🗑️ Deleted <code class="inlinecode">${filename}</code>
|
|
<button class="btn-flat toast-action">Undo</button>`;
|
|
const toast = M.toast({
|
|
html: toastHtml,
|
|
displayLength: 10000
|
|
});
|
|
const undoButton = toast.el.querySelector('.toast-action');
|
|
|
|
document.querySelector(`.card[data-filename="${filename}"]`).remove();
|
|
|
|
undoButton.addEventListener('click', () => {
|
|
fetch(`./undo-delete?configuration=${filename}`, {
|
|
credentials: "same-origin",
|
|
method: "POST",
|
|
})
|
|
.then((res) => {
|
|
res.text()
|
|
})
|
|
.then(() => {
|
|
window.location.reload(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Wizard
|
|
*/
|
|
|
|
const wizardTriggerElement = document.querySelector("[data-action='wizard']");
|
|
const wizardModal = document.getElementById("js-wizard-modal");
|
|
const wizardCloseButton = document.querySelector("[data-action='wizard-close']");
|
|
|
|
const wizardStepper = document.querySelector('#js-wizard-modal .stepper');
|
|
const wizardStepperInstace = new MStepper(wizardStepper, {
|
|
firstActive: 0,
|
|
stepTitleNavigation: false,
|
|
autoFocusInput: true,
|
|
showFeedbackLoader: true,
|
|
})
|
|
|
|
const startWizard = () => {
|
|
M.Modal.init(wizardModal, {
|
|
dismissible: false
|
|
}).open();
|
|
}
|
|
|
|
document.querySelectorAll("[data-action='wizard']").forEach((btn) => {
|
|
btn.addEventListener("click", (event) => {
|
|
startWizard();
|
|
})
|
|
});
|
|
|
|
jQuery.validator.addMethod("nospaces", (value, element) => {
|
|
return value.indexOf(' ') < 0;
|
|
}, "Name cannot contain any spaces!");
|
|
|
|
jQuery.validator.addMethod("nounderscores", (value, element) => {
|
|
return value.indexOf('_') < 0;
|
|
}, "Name cannot contain underscores!");
|
|
|
|
jQuery.validator.addMethod("lowercase", (value, element) => {
|
|
return value === value.toLowerCase();
|
|
}, "Name must be all lower case!");
|