diff options
Diffstat (limited to 'web/gui/main.js')
-rw-r--r-- | web/gui/main.js | 5155 |
1 files changed, 5155 insertions, 0 deletions
diff --git a/web/gui/main.js b/web/gui/main.js new file mode 100644 index 0000000..5bf11e5 --- /dev/null +++ b/web/gui/main.js @@ -0,0 +1,5155 @@ +// Main JavaScript file for the Netdata GUI. + +// Codacy declarations +/* global NETDATA */ + +// netdata snapshot data +var netdataSnapshotData = null; + +// enable alarms checking and notifications +var netdataShowAlarms = true; + +// enable registry updates +var netdataRegistry = true; + +// forward definition only - not used here +var netdataServer = undefined; +var netdataServerStatic = undefined; +var netdataCheckXSS = undefined; + +// control the welcome modal and analytics +var this_is_demo = null; + +function escapeUserInputHTML(s) { + return s.toString() + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/#/g, '#') + .replace(/'/g, ''') + .replace(/\(/g, '(') + .replace(/\)/g, ')') + .replace(/\//g, '/'); +} + +function verifyURL(s) { + if (typeof (s) === 'string' && (s.startsWith('http://') || s.startsWith('https://'))) { + return s + .replace(/'/g, '%22') + .replace(/"/g, '%27') + .replace(/\)/g, '%28') + .replace(/\(/g, '%29'); + } + + console.log('invalid URL detected:'); + console.log(s); + return 'javascript:alert("invalid url");'; +} + +// -------------------------------------------------------------------- +// urlOptions + +var urlOptions = { + hash: '#', + theme: null, + help: null, + mode: 'live', // 'live', 'print' + update_always: false, + pan_and_zoom: false, + server: null, + after: 0, + before: 0, + highlight: false, + highlight_after: 0, + highlight_before: 0, + nowelcome: false, + show_alarms: false, + chart: null, + family: null, + alarm: null, + alarm_unique_id: 0, + alarm_id: 0, + alarm_event_id: 0, + alarm_when: 0, + + hasProperty: function (property) { + // console.log('checking property ' + property + ' of type ' + typeof(this[property])); + return typeof this[property] !== 'undefined'; + }, + + genHash: function (forReload) { + var hash = urlOptions.hash; + + if (urlOptions.pan_and_zoom === true) { + hash += ';after=' + urlOptions.after.toString() + + ';before=' + urlOptions.before.toString(); + } + + if (urlOptions.highlight === true) { + hash += ';highlight_after=' + urlOptions.highlight_after.toString() + + ';highlight_before=' + urlOptions.highlight_before.toString(); + } + + if (urlOptions.theme !== null) { + hash += ';theme=' + urlOptions.theme.toString(); + } + + if (urlOptions.help !== null) { + hash += ';help=' + urlOptions.help.toString(); + } + + if (urlOptions.update_always === true) { + hash += ';update_always=true'; + } + + if (forReload === true && urlOptions.server !== null) { + hash += ';server=' + urlOptions.server.toString(); + } + + if (urlOptions.mode !== 'live') { + hash += ';mode=' + urlOptions.mode; + } + + return hash; + }, + + parseHash: function () { + var variables = document.location.hash.split(';'); + var len = variables.length; + while (len--) { + if (len !== 0) { + var p = variables[len].split('='); + if (urlOptions.hasProperty(p[0]) && typeof p[1] !== 'undefined') { + urlOptions[p[0]] = decodeURIComponent(p[1]); + } + } else { + if (variables[len].length > 0) { + urlOptions.hash = variables[len]; + } + } + } + + var booleans = ['nowelcome', 'show_alarms', 'update_always']; + len = booleans.length; + while (len--) { + if (urlOptions[booleans[len]] === 'true' || urlOptions[booleans[len]] === true || urlOptions[booleans[len]] === '1' || urlOptions[booleans[len]] === 1) { + urlOptions[booleans[len]] = true; + } else { + urlOptions[booleans[len]] = false; + } + } + + var numeric = ['after', 'before', 'highlight_after', 'highlight_before', 'alarm_when']; + len = numeric.length; + while (len--) { + if (typeof urlOptions[numeric[len]] === 'string') { + try { + urlOptions[numeric[len]] = parseInt(urlOptions[numeric[len]]); + } + catch (e) { + console.log('failed to parse URL hash parameter ' + numeric[len]); + urlOptions[numeric[len]] = 0; + } + } + } + + if (urlOptions.alarm_when) { + // if alarm_when exists, create after/before params + // -/+ 2 minutes from the alarm, and reload the page + const alarmTime = new Date(urlOptions.alarm_when * 1000).valueOf(); + const timeMarginMs = 120000; // 2 mins + + const after = alarmTime - timeMarginMs; + const before = alarmTime + timeMarginMs; + const newHash = document.location.hash.replace( + /;alarm_when=[0-9]*/i, + ";after=" + after + ";before=" + before, + ); + history.replaceState(null, '', newHash); + location.reload(); + } + + if (urlOptions.server !== null && urlOptions.server !== '') { + netdataServerStatic = document.location.origin.toString() + document.location.pathname.toString(); + netdataServer = urlOptions.server; + netdataCheckXSS = true; + } else { + urlOptions.server = null; + } + + if (urlOptions.before > 0 && urlOptions.after > 0) { + urlOptions.pan_and_zoom = true; + urlOptions.nowelcome = true; + } else { + urlOptions.pan_and_zoom = false; + } + + if (urlOptions.highlight_before > 0 && urlOptions.highlight_after > 0) { + urlOptions.highlight = true; + } else { + urlOptions.highlight = false; + } + + switch (urlOptions.mode) { + case 'print': + urlOptions.theme = 'white'; + urlOptions.welcome = false; + urlOptions.help = false; + urlOptions.show_alarms = false; + + if (urlOptions.pan_and_zoom === false) { + urlOptions.pan_and_zoom = true; + urlOptions.before = Date.now(); + urlOptions.after = urlOptions.before - 600000; + } + + netdataShowAlarms = false; + netdataRegistry = false; + this_is_demo = false; + break; + + case 'live': + default: + urlOptions.mode = 'live'; + break; + } + + // console.log(urlOptions); + }, + + hashUpdate: function () { + history.replaceState(null, '', urlOptions.genHash(true)); + }, + + netdataPanAndZoomCallback: function (status, after, before) { + //console.log(1); + //console.log(new Error().stack); + + if (netdataSnapshotData === null) { + urlOptions.pan_and_zoom = status; + urlOptions.after = after; + urlOptions.before = before; + urlOptions.hashUpdate(); + } + }, + + netdataHighlightCallback: function (status, after, before) { + //console.log(2); + //console.log(new Error().stack); + + if (status === true && (after === null || before === null || after <= 0 || before <= 0 || after >= before)) { + status = false; + after = 0; + before = 0; + } + + if (netdataSnapshotData === null) { + urlOptions.highlight = status; + } else { + urlOptions.highlight = false; + } + + urlOptions.highlight_after = Math.round(after); + urlOptions.highlight_before = Math.round(before); + urlOptions.hashUpdate(); + + var show_eye = NETDATA.globalChartUnderlay.hasViewport(); + + if (status === true && after > 0 && before > 0 && after < before) { + var d1 = NETDATA.dateTime.localeDateString(after); + var d2 = NETDATA.dateTime.localeDateString(before); + if (d1 === d2) { + d2 = ''; + } + document.getElementById('navbar-highlight-content').innerHTML = + ((show_eye === true) ? '<span class="navbar-highlight-bar highlight-tooltip" onclick="urlOptions.showHighlight();" title="restore the highlighted view" data-toggle="tooltip" data-placement="bottom">' : '<span>').toString() + + 'highlighted time-frame' + + ' <b>' + d1 + ' <code>' + NETDATA.dateTime.localeTimeString(after) + '</code></b> to ' + + ' <b>' + d2 + ' <code>' + NETDATA.dateTime.localeTimeString(before) + '</code></b>, ' + + 'duration <b>' + NETDATA.seconds4human(Math.round((before - after) / 1000)) + '</b>' + + '</span>' + + '<span class="navbar-highlight-button-right highlight-tooltip" onclick="urlOptions.clearHighlight();" title="clear the highlighted time-frame" data-toggle="tooltip" data-placement="bottom"><i class="fas fa-times"></i></span>'; + + $('.navbar-highlight').show(); + + $('.highlight-tooltip').tooltip({ + html: true, + delay: { show: 500, hide: 0 }, + container: 'body' + }); + } else { + $('.navbar-highlight').hide(); + } + }, + + clearHighlight: function () { + NETDATA.globalChartUnderlay.clear(); + + if (NETDATA.globalPanAndZoom.isActive() === true) { + NETDATA.globalPanAndZoom.clearMaster(); + } + }, + + showHighlight: function () { + NETDATA.globalChartUnderlay.focus(); + } +}; + +urlOptions.parseHash(); + +// -------------------------------------------------------------------- +// check options that should be processed before loading netdata.js + +var localStorageTested = -1; + +function localStorageTest() { + if (localStorageTested !== -1) { + return localStorageTested; + } + + if (typeof Storage !== "undefined" && typeof localStorage === 'object') { + var test = 'test'; + try { + localStorage.setItem(test, test); + localStorage.removeItem(test); + localStorageTested = true; + } + catch (e) { + console.log(e); + localStorageTested = false; + } + } else { + localStorageTested = false; + } + + return localStorageTested; +} + +function loadLocalStorage(name) { + var ret = null; + + try { + if (localStorageTest() === true) { + ret = localStorage.getItem(name); + } else { + console.log('localStorage is not available'); + } + } + catch (error) { + console.log(error); + return null; + } + + if (typeof ret === 'undefined' || ret === null) { + return null; + } + + // console.log('loaded: ' + name.toString() + ' = ' + ret.toString()); + + return ret; +} + +function saveLocalStorage(name, value) { + // console.log('saving: ' + name.toString() + ' = ' + value.toString()); + try { + if (localStorageTest() === true) { + localStorage.setItem(name, value.toString()); + return true; + } + } + catch (error) { + console.log(error); + } + + return false; +} + +function getTheme(def) { + if (urlOptions.mode === 'print') { + return 'white'; + } + + var ret = loadLocalStorage('netdataTheme'); + if (typeof ret === 'undefined' || ret === null || ret === 'undefined') { + return def; + } else { + return ret; + } +} + +function setTheme(theme) { + if (urlOptions.mode === 'print') { + return false; + } + + if (theme === netdataTheme) { + return false; + } + return saveLocalStorage('netdataTheme', theme); +} + +var netdataTheme = getTheme('slate'); +var netdataShowHelp = true; + +if (urlOptions.theme !== null) { + setTheme(urlOptions.theme); + netdataTheme = urlOptions.theme; +} else { + urlOptions.theme = netdataTheme; +} + +if (urlOptions.help !== null) { + saveLocalStorage('options.show_help', urlOptions.help); + netdataShowHelp = urlOptions.help; +} else { + urlOptions.help = loadLocalStorage('options.show_help'); +} + +// -------------------------------------------------------------------- +// natural sorting +// http://www.davekoelle.com/files/alphanum.js - LGPL + +function naturalSortChunkify(t) { + var tz = []; + var x = 0, y = -1, n = 0, i, j; + + while (i = (j = t.charAt(x++)).charCodeAt(0)) { + var m = (i >= 48 && i <= 57); + if (m !== n) { + tz[++y] = ""; + n = m; + } + tz[y] += j; + } + + return tz; +} + +function naturalSortCompare(a, b) { + var aa = naturalSortChunkify(a.toLowerCase()); + var bb = naturalSortChunkify(b.toLowerCase()); + + for (var x = 0; aa[x] && bb[x]; x++) { + if (aa[x] !== bb[x]) { + var c = Number(aa[x]), d = Number(bb[x]); + if (c.toString() === aa[x] && d.toString() === bb[x]) { + return c - d; + } else { + return (aa[x] > bb[x]) ? 1 : -1; + } + } + } + + return aa.length - bb.length; +} + +// -------------------------------------------------------------------- +// saving files to client + +function saveTextToClient(data, filename) { + var blob = new Blob([data], { + type: 'application/octet-stream' + }); + + var url = URL.createObjectURL(blob); + var link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', filename); + + var el = document.getElementById('hiddenDownloadLinks'); + el.innerHTML = ''; + el.appendChild(link); + + setTimeout(function () { + el.removeChild(link); + URL.revokeObjectURL(url); + }, 60); + + link.click(); +} + +function saveObjectToClient(data, filename) { + saveTextToClient(JSON.stringify(data), filename); +} + +// ----------------------------------------------------------------------------- +// registry call back to render my-netdata menu + +function toggleExpandIcon(svgEl) { + if (svgEl.getAttribute('data-icon') === 'caret-down') { + svgEl.setAttribute('data-icon', 'caret-up'); + } else { + svgEl.setAttribute('data-icon', 'caret-down'); + } +} + +function toggleAgentItem(e, guid) { + e.stopPropagation(); + e.preventDefault(); + + toggleExpandIcon(e.currentTarget.children[0]); + + const el = document.querySelector(`.agent-alternate-urls.agent-${guid}`); + if (el) { + el.classList.toggle('collapsed'); + } +} + +// When you stream metrics from netdata to netdata, the recieving netdata now +// has multiple host databases. It's own, and multiple mirrored. Mirrored databases +// can be accessed with <http://localhost:19999/host/NAME/> +const OLD_DASHBOARD_SUFFIX = "old" +let isOldSuffix = true +try { + const currentScriptMainJs = document.currentScript; + const mainJsSrc = currentScriptMainJs.getAttribute("src") + isOldSuffix = mainJsSrc.startsWith("../main.js") +} catch { + console.warn("current script not detecting, assuming the dashboard is running with /old suffix") +} + +function transformWithOldSuffix(url) { + return isOldSuffix ? `../${url}` : url +} + +function renderStreamedHosts(options) { + let html = `<div class="info-item">Databases streamed to this agent</div>`; + + var base = document.location.origin.toString() + + document.location.pathname.toString() + .replace(isOldSuffix ? `/${OLD_DASHBOARD_SUFFIX}` : "", ""); + if (base.endsWith("/host/" + options.hostname + "/")) { + base = base.substring(0, base.length - ("/host/" + options.hostname + "/").toString().length); + } + + if (base.endsWith("/")) { + base = base.substring(0, base.length - 1); + } + + var master = options.hosts[0].hostname; + // We sort a clone of options.hosts, to keep the master as the first element + // for future calls. + var sorted = options.hosts.slice(0).sort(function (a, b) { + if (a.hostname === master) { + return -1; + } + return naturalSortCompare(a.hostname, b.hostname); + }); + + let displayedDatabases = false; + + for (var s of sorted) { + let url, icon; + const hostname = s.hostname; + + if (myNetdataMenuFilterValue !== "") { + if (!hostname.includes(myNetdataMenuFilterValue)) { + continue; + } + } + + displayedDatabases = true; + + if (hostname === master) { + url = isOldSuffix ? `${base}/${OLD_DASHBOARD_SUFFIX}/` : `${base}/`; + icon = 'home'; + } else { + url = isOldSuffix ? `${base}/host/${hostname}/${OLD_DASHBOARD_SUFFIX}/` : `${base}/host/${hostname}/`; + icon = 'window-restore'; + } + + html += ( + `<div class="agent-item"> + <a class="registry_link" href="${url}#" onClick="return gotoHostedModalHandler('${url}');"> + <i class="fas fa-${icon}" style="color: #999;"></i> + </a> + <span class="__title" onClick="return gotoHostedModalHandler('${url}');"> + <a class="registry_link" href="${url}#">${hostname}</a> + </span> + <div></div> + </div>` + ) + } + + if (!displayedDatabases) { + html += ( + `<div class="info-item"> + <i class="fas fa-filter"></i> + <span style="margin-left: 8px">no databases match the filter criteria.<span> + </div>` + ) + } + + return html; +} + +function renderMachines(machinesArray) { + let html = `<div class="info-item">My nodes</div>`; + + if (machinesArray === null) { + let ret = loadLocalStorage("registryCallback"); + if (ret) { + machinesArray = JSON.parse(ret); + console.log("failed to contact the registry - loaded registry data from browser local storage"); + } + } + + let found = false; + let displayedAgents = false; + + const maskedURL = NETDATA.registry.MASKED_DATA; + + if (machinesArray) { + saveLocalStorage("registryCallback", JSON.stringify(machinesArray)); + + var machines = machinesArray.sort(function (a, b) { + return naturalSortCompare(a.name, b.name); + }); + + for (var machine of machines) { + found = true; + + if (myNetdataMenuFilterValue !== "") { + if (!machine.name.includes(myNetdataMenuFilterValue)) { + continue; + } + } + + displayedAgents = true; + + const alternateUrlItems = ( + `<div class="agent-alternate-urls agent-${machine.guid} collapsed"> + ${machine.alternate_urls.reduce((str, url) => { + if (url === maskedURL) { + return str + } + + return str + ( + `<div class="agent-item agent-item--alternate"> + <div></div> + <a href="${url}" title="${url}">${truncateString(url, 64)}</a> + <a href="#" onclick="deleteRegistryModalHandler('${machine.guid}', '${machine.name}', '${url}'); return false;"> + <i class="fas fa-trash" style="color: #777;"></i> + </a> + </div>` + ) + }, + '' + )} + </div>` + ) + + html += ( + `<div class="agent-item agent-${machine.guid}"> + <i class="fas fa-chart-bar" color: #fff"></i> + <span class="__title" onClick="return gotoServerModalHandler('${machine.guid}');"> + <a class="registry_link" href="${machine.url}#">${machine.name}</a> + </span> + <a href="#" onClick="toggleAgentItem(event, '${machine.guid}');"> + <i class="fas fa-caret-down" style="color: #999"></i> + </a> + </div> + ${alternateUrlItems}` + ) + } + + if (found && (!displayedAgents)) { + html += ( + `<div class="info-item"> + <i class="fas fa-filter"></i> + <span style="margin-left: 8px">zero nodes are matching the filter value.<span> + </div>` + ) + } + } + + if (!found) { + if (machines) { + html += ( + `<div class="info-item"> + <a href="https://github.com/netdata/netdata/tree/master/registry#netdata-registry" target="_blank">Your nodes list is empty</a> + </div>` + ) + } else { + html += ( + `<div class="info-item"> + <a href="https://github.com/netdata/netdata/tree/master/registry#netdata-registry" target="_blank">Failed to contact the registry</a> + </div>` + ) + } + + html += `<hr />`; + html += `<div class="info-item">Demo netdata nodes</div>`; + + const demoServers = [ + { url: "//london.netdata.rocks/default.html", title: "UK - London (DigitalOcean.com)" }, + { url: "//newyork.netdata.rocks/default.html", title: "US - New York (DigitalOcean.com)" }, + { url: "//sanfrancisco.netdata.rocks/default.html", title: "US - San Francisco (DigitalOcean.com)" }, + { url: "//atlanta.netdata.rocks/default.html", title: "US - Atlanta (CDN77.com)" }, + { url: "//frankfurt.netdata.rocks/default.html", title: "Germany - Frankfurt (DigitalOcean.com)" }, + { url: "//toronto.netdata.rocks/default.html", title: "Canada - Toronto (DigitalOcean.com)" }, + { url: "//singapore.netdata.rocks/default.html", title: "Japan - Singapore (DigitalOcean.com)" }, + { url: "//bangalore.netdata.rocks/default.html", title: "India - Bangalore (DigitalOcean.com)" }, + + ] + + for (var server of demoServers) { + html += ( + `<div class="agent-item"> + <i class="fas fa-chart-bar" style="color: #fff"></i> + <a href="${server.url}">${server.title}</a> + <div></div> + </div> + ` + ); + } + } + + return html; +} + +function setMyNetdataMenu(html) { + const el = document.getElementById('my-netdata-dropdown-content') + el.innerHTML = html; +} + +function clearMyNetdataMenu() { + setMyNetdataMenu(`<div class="agent-item" style="white-space: nowrap"> + <i class="fas fa-hourglass-half"></i> + Loading, please wait... + <div></div> + </div>`); +} + +function errorMyNetdataMenu() { + setMyNetdataMenu(`<div class="agent-item" style="padding: 0 8px"> + <i class="fas fa-exclamation-triangle" style="color: red"></i> + Cannot load known Netdata agents from Netdata Cloud! Please make sure you have the latest version of Netdata. + </div>`); +} + +function restrictMyNetdataMenu() { + setMyNetdataMenu(`<div class="info-item" style="white-space: nowrap"> + <span>Please <a href="#" onclick="signInDidClick(event); return false">sign in to netdata.cloud</a> to view your nodes!</span> + <div></div> + </div>`); +} + +function openAuthenticatedUrl(url) { + if (isSignedIn()) { + window.open(url); + } else { + window.open(`${NETDATA.registry.cloudBaseURL}/account/sign-in-agent?id=${NETDATA.registry.machine_guid}&name=${encodeURIComponent(NETDATA.registry.hostname)}&origin=${encodeURIComponent(window.location.origin + "/")}&redirect_uri=${encodeURIComponent(window.location.origin + "/" + url)}`); + } +} + +function renderMyNetdataMenu(machinesArray) { + const el = document.getElementById('my-netdata-dropdown-content'); + el.classList.add(`theme-${netdataTheme}`); + + if (machinesArray == registryAgents) { + console.log("Rendering my-netdata menu from registry"); + } else { + console.log("Rendering my-netdata menu from netdata.cloud", machinesArray); + } + + let html = ''; + + if (!isSignedIn()) { + if (!NETDATA.registry.isRegistryEnabled()) { + html += ( + `<div class="info-item" style="white-space: nowrap"> + <span>Please <a href="#" onclick="signInDidClick(event); return false">sign in to netdata.cloud</a> to view your nodes!</span> + <div></div> + </div> + <hr />` + ); + } + } + + if (isSignedIn()) { + html += ( + `<div class="filter-control"> + <input + id="my-netdata-menu-filter-input" + type="text" + placeholder="filter nodes..." + autofocus + autocomplete="off" + value="${myNetdataMenuFilterValue}" + onkeydown="myNetdataFilterDidChange(event)" + /> + <span class="filter-control__clear" onclick="myNetdataFilterClearDidClick(event)"><i class="fas fa-times"></i><span> + </div> + <hr />` + ); + } + + // options.hosts = [ + // { + // hostname: "streamed1", + // }, + // { + // hostname: "streamed2", + // }, + // ] + + if (options.hosts.length > 1) { + html += `<div id="my-netdata-menu-streamed">${renderStreamedHosts(options)}</div><hr />`; + } + + if (isSignedIn() || NETDATA.registry.isRegistryEnabled()) { + html += `<div id="my-netdata-menu-machines">${renderMachines(machinesArray)}</div><hr />`; + } + + if (!isSignedIn()) { + html += ( + `<div class="agent-item"> + <i class="fas fa-cog""></i> + <a href="#" onclick="switchRegistryModalHandler(); return false;">Switch Identity</a> + <div></div> + </div> + <div class="agent-item"> + <i class="fas fa-question-circle""></i> + <a href="https://github.com/netdata/netdata/tree/master/registry#netdata-registry" target="_blank">What is this?</a> + <div></div> + </div>` + ) + } else { + html += ( + `<div class="agent-item"> + <i class="fas fa-tv"></i> + <a onclick="openAuthenticatedUrl('console.html');" target="_blank">Nodes<sup class="beta"> beta</sup></a> + <div></div> + </div> + <div class="agent-item"> + <i class="fas fa-sync"></i> + <a href="#" onclick="showSyncModal(); return false">Synchronize with netdata.cloud</a> + <div></div> + </div> + <div class="agent-item"> + <i class="fas fa-question-circle""></i> + <a href="https://netdata.cloud/about" target="_blank">What is this?</a> + <div></div> + </div>` + ) + } + + el.innerHTML = html; + + gotoServerInit(); +} + +function isdemo() { + if (this_is_demo !== null) { + return this_is_demo; + } + this_is_demo = false; + + try { + if (typeof document.location.hostname === 'string') { + if (document.location.hostname.endsWith('.my-netdata.io') || + document.location.hostname.endsWith('.mynetdata.io') || + document.location.hostname.endsWith('.netdata.rocks') || + document.location.hostname.endsWith('.netdata.ai') || + document.location.hostname.endsWith('.netdata.live') || + document.location.hostname.endsWith('.firehol.org') || + document.location.hostname.endsWith('.netdata.online') || + document.location.hostname.endsWith('.netdata.cloud')) { + this_is_demo = true; + } + } + } + catch (error) { + } + return this_is_demo; +} + +function netdataURL(url, forReload) { + if (typeof url === 'undefined') + // url = document.location.toString(); + { + url = ''; + } + + if (url.indexOf('#') !== -1) { + url = url.substring(0, url.indexOf('#')); + } + + var hash = urlOptions.genHash(forReload); + + // console.log('netdataURL: ' + url + hash); + + return url + hash; +} + +function netdataReload(url) { + document.location = verifyURL(netdataURL(url, true)); + + // since we play with hash + // this is needed to reload the page + location.reload(); +} + +function gotoHostedModalHandler(url) { + document.location = verifyURL(url + urlOptions.genHash()); + return false; +} + +var gotoServerValidateRemaining = 0; +var gotoServerMiddleClick = false; +var gotoServerStop = false; + +function gotoServerValidateUrl(id, guid, url) { + var penalty = 0; + var error = 'failed'; + + if (document.location.toString().startsWith('http://') && url.toString().startsWith('https://')) + // we penalize https only if the current url is http + // to allow the user walk through all its servers. + { + penalty = 500; + } else if (document.location.toString().startsWith('https://') && url.toString().startsWith('http://')) { + error = 'can\'t check'; + } + + var finalURL = netdataURL(url); + + setTimeout(function () { + document.getElementById('gotoServerList').innerHTML += '<tr><td style="padding-left: 20px;"><a href="' + verifyURL(finalURL) + '" target="_blank">' + escapeUserInputHTML(url) + '</a></td><td style="padding-left: 30px;"><code id="' + guid + '-' + id + '-status">checking...</code></td></tr>'; + + NETDATA.registry.hello(url, function (data) { + if (typeof data !== 'undefined' && data !== null && typeof data.machine_guid === 'string' && data.machine_guid === guid) { + // console.log('OK ' + id + ' URL: ' + url); + document.getElementById(guid + '-' + id + '-status').innerHTML = "OK"; + + if (!gotoServerStop) { + gotoServerStop = true; + + if (gotoServerMiddleClick) { + window.open(verifyURL(finalURL), '_blank'); + gotoServerMiddleClick = false; + document.getElementById('gotoServerResponse').innerHTML = '<b>Opening new window to ' + NETDATA.registry.machines[guid].name + '<br/><a href="' + verifyURL(finalURL) + '">' + escapeUserInputHTML(url) + '</a></b><br/>(check your pop-up blocker if it fails)'; + } else { + document.getElementById('gotoServerResponse').innerHTML += 'found it! It is at:<br/><small>' + escapeUserInputHTML(url) + '</small>'; + document.location = verifyURL(finalURL); + $('#gotoServerModal').modal('hide'); + } + } + } else { + if (typeof data !== 'undefined' && data !== null && typeof data.machine_guid === 'string' && data.machine_guid !== guid) { + error = 'wrong machine'; + } + + document.getElementById(guid + '-' + id + '-status').innerHTML = error; + gotoServerValidateRemaining--; + if (gotoServerValidateRemaining <= 0) { + gotoServerMiddleClick = false; + document.getElementById('gotoServerResponse').innerHTML = '<b>Sorry! I cannot find any operational URL for this server</b>'; + } + } + }); + }, (id * 50) + penalty); +} + +function gotoServerModalHandler(guid) { + // console.log('goto server: ' + guid); + + gotoServerStop = false; + var checked = {}; + var len = NETDATA.registry.machines[guid].alternate_urls.length; + var count = 0; + + document.getElementById('gotoServerResponse').innerHTML = ''; + document.getElementById('gotoServerList').innerHTML = ''; + document.getElementById('gotoServerName').innerHTML = NETDATA.registry.machines[guid].name; + $('#gotoServerModal').modal('show'); + + gotoServerValidateRemaining = len; + while (len--) { + var url = NETDATA.registry.machines[guid].alternate_urls[len]; + checked[url] = true; + gotoServerValidateUrl(count++, guid, url); + } + + if (!isSignedIn()) { + // When the registry is enabled, if the user's known URLs are not working + // we consult the registry to get additional URLs. + setTimeout(function () { + if (gotoServerStop === false) { + document.getElementById('gotoServerResponse').innerHTML = '<b>Added all the known URLs for this machine.</b>'; + NETDATA.registry.search(guid, function (data) { + // console.log(data); + len = data.urls.length; + while (len--) { + var url = data.urls[len][1]; + // console.log(url); + if (typeof checked[url] === 'undefined') { + gotoServerValidateRemaining++; + checked[url] = true; + gotoServerValidateUrl(count++, guid, url); + } + } + }); + } + }, 2000); + } + + return false; +} + +function gotoServerInit() { + $(".registry_link").on('click', function (e) { + if (e.which === 2) { + e.preventDefault(); + gotoServerMiddleClick = true; + } else { + gotoServerMiddleClick = false; + } + + return true; + }); +} + +function switchRegistryModalHandler() { + document.getElementById('switchRegistryPersonGUID').value = NETDATA.registry.person_guid; + document.getElementById('switchRegistryURL').innerHTML = NETDATA.registry.server; + document.getElementById('switchRegistryResponse').innerHTML = ''; + $('#switchRegistryModal').modal('show'); +} + +function notifyForSwitchRegistry() { + var n = document.getElementById('switchRegistryPersonGUID').value; + + if (n !== '' && n.length === 36) { + NETDATA.registry.switch(n, function (result) { + if (result !== null) { + $('#switchRegistryModal').modal('hide'); + NETDATA.registry.init(); + } else { + document.getElementById('switchRegistryResponse').innerHTML = "<b>Sorry! The registry rejected your request.</b>"; + } + }); + } else { + document.getElementById('switchRegistryResponse').innerHTML = "<b>The ID you have entered is not a GUID.</b>"; + } +} + +var deleteRegistryGuid = null; +var deleteRegistryUrl = null; + +function deleteRegistryModalHandler(guid, name, url) { + // void (guid); + + deleteRegistryGuid = guid; + deleteRegistryUrl = url; + + document.getElementById('deleteRegistryServerName').innerHTML = name; + document.getElementById('deleteRegistryServerName2').innerHTML = name; + document.getElementById('deleteRegistryServerURL').innerHTML = url; + document.getElementById('deleteRegistryResponse').innerHTML = ''; + + $('#deleteRegistryModal').modal('show'); +} + +function notifyForDeleteRegistry() { + const responseEl = document.getElementById('deleteRegistryResponse'); + + if (deleteRegistryUrl) { + if (isSignedIn()) { + deleteCloudAgentURL(deleteRegistryGuid, deleteRegistryUrl) + .then((count) => { + if (!count) { + responseEl.innerHTML = "<b>Sorry, this command was rejected by netdata.cloud!</b>"; + return; + } + NETDATA.registry.delete(deleteRegistryUrl, function (result) { + if (result === null) { + console.log("Received error from registry", result); + } + + deleteRegistryUrl = null; + $('#deleteRegistryModal').modal('hide'); + NETDATA.registry.init(); + }); + }); + } else { + NETDATA.registry.delete(deleteRegistryUrl, function (result) { + if (result !== null) { + deleteRegistryUrl = null; + $('#deleteRegistryModal').modal('hide'); + NETDATA.registry.init(); + } else { + responseEl.innerHTML = "<b>Sorry, this command was rejected by the registry server!</b>"; + } + }); + } + } +} + +var options = { + menus: {}, + submenu_names: {}, + data: null, + hostname: 'netdata_server', // will be overwritten by the netdata server + version: 'unknown', + release_channel: 'unknown', + hosts: [], + + duration: 0, // the default duration of the charts + update_every: 1, + + chartsPerRow: 0, + // chartsMinWidth: 1450, + chartsHeight: 180, +}; + +function chartsPerRow(total) { + void (total); + + if (options.chartsPerRow === 0) { + return 1; + //var width = Math.floor(total / options.chartsMinWidth); + //if(width === 0) width = 1; + //return width; + } else { + return options.chartsPerRow; + } +} + +function prioritySort(a, b) { + if (a.priority < b.priority) { + return -1; + } + if (a.priority > b.priority) { + return 1; + } + return naturalSortCompare(a.name, b.name); +} + +function sortObjectByPriority(object) { + var idx = {}; + var sorted = []; + + for (var i in object) { + if (!object.hasOwnProperty(i)) { + continue; + } + + if (typeof idx[i] === 'undefined') { + idx[i] = object[i]; + sorted.push(i); + } + } + + sorted.sort(function (a, b) { + if (idx[a].priority < idx[b].priority) { + return -1; + } + if (idx[a].priority > idx[b].priority) { + return 1; + } + return naturalSortCompare(a, b); + }); + + return sorted; +} + +// ---------------------------------------------------------------------------- +// scroll to a section, without changing the browser history + +function scrollToId(hash) { + if (hash && hash !== '' && document.getElementById(hash) !== null) { + var offset = $('#' + hash).offset(); + if (typeof offset !== 'undefined') { + //console.log('scrolling to ' + hash + ' at ' + offset.top.toString()); + $('html, body').animate({ scrollTop: offset.top - 30 }, 0); + } + } + + // we must return false to prevent the default action + return false; +} + +// ---------------------------------------------------------------------------- + +// user editable information +var customDashboard = { + menu: {}, + submenu: {}, + context: {} +}; + +// netdata standard information +var netdataDashboard = { + sparklines_registry: {}, + os: 'unknown', + + menu: {}, + submenu: {}, + context: {}, + + // generate a sparkline + // used in the documentation + sparkline: function (prefix, chart, dimension, units, suffix) { + if (options.data === null || typeof options.data.charts === 'undefined') { + return ''; + } + + if (typeof options.data.charts[chart] === 'undefined') { + return ''; + } + + if (typeof options.data.charts[chart].dimensions === 'undefined') { + return ''; + } + + if (typeof options.data.charts[chart].dimensions[dimension] === 'undefined') { + return ''; + } + + var key = chart + '.' + dimension; + + if (typeof units === 'undefined') { + units = ''; + } + + if (typeof this.sparklines_registry[key] === 'undefined') { + this.sparklines_registry[key] = { count: 1 }; + } else { + this.sparklines_registry[key].count++; + } + + key = key + '.' + this.sparklines_registry[key].count; + + return prefix + '<div class="netdata-container" data-netdata="' + chart + '" data-after="-120" data-width="25%" data-height="15px" data-chart-library="dygraph" data-dygraph-theme="sparkline" data-dimensions="' + dimension + '" data-show-value-of-' + dimension + '-at="' + key + '"></div> (<span id="' + key + '" style="display: inline-block; min-width: 50px; text-align: right;">X</span>' + units + ')' + suffix; + }, + + gaugeChart: function (title, width, dimensions, colors) { + if (typeof colors === 'undefined') { + colors = ''; + } + + if (typeof dimensions === 'undefined') { + dimensions = ''; + } + + return '<div class="netdata-container" data-netdata="CHART_UNIQUE_ID"' + + ' data-dimensions="' + dimensions + '"' + + ' data-chart-library="gauge"' + + ' data-gauge-adjust="width"' + + ' data-title="' + title + '"' + + ' data-width="' + width + '"' + + ' data-before="0"' + + ' data-after="-CHART_DURATION"' + + ' data-points="CHART_DURATION"' + + ' data-colors="' + colors + '"' + + ' role="application"></div>'; + }, + + anyAttribute: function (obj, attr, key, def) { + if (typeof (obj[key]) !== 'undefined') { + var x = obj[key][attr]; + + if (typeof (x) === 'undefined') { + return def; + } + + if (typeof (x) === 'function') { + return x(netdataDashboard.os); + } + + return x; + } + + return def; + }, + + menuTitle: function (chart) { + if (typeof chart.menu_pattern !== 'undefined') { + return (this.anyAttribute(this.menu, 'title', chart.menu_pattern, chart.menu_pattern).toString() + + ' ' + chart.type.slice(-(chart.type.length - chart.menu_pattern.length - 1)).toString()).replace(/_/g, ' '); + } + + return (this.anyAttribute(this.menu, 'title', chart.menu, chart.menu)).toString().replace(/_/g, ' '); + }, + + menuIcon: function (chart) { + if (typeof chart.menu_pattern !== 'undefined') { + return this.anyAttribute(this.menu, 'icon', chart.menu_pattern, '<i class="fas fa-puzzle-piece"></i>').toString(); + } + + return this.anyAttribute(this.menu, 'icon', chart.menu, '<i class="fas fa-puzzle-piece"></i>'); + }, + + menuInfo: function (chart) { + if (typeof chart.menu_pattern !== 'undefined') { + return this.anyAttribute(this.menu, 'info', chart.menu_pattern, null); + } + + return this.anyAttribute(this.menu, 'info', chart.menu, null); + }, + + menuHeight: function (chart) { + if (typeof chart.menu_pattern !== 'undefined') { + return this.anyAttribute(this.menu, 'height', chart.menu_pattern, 1.0); + } + + return this.anyAttribute(this.menu, 'height', chart.menu, 1.0); + }, + + submenuTitle: function (menu, submenu) { + var key = menu + '.' + submenu; + // console.log(key); + var title = this.anyAttribute(this.submenu, 'title', key, submenu).toString().replace(/_/g, ' '); + if (title.length > 28) { + var a = title.substring(0, 13); + var b = title.substring(title.length - 12, title.length); + return a + '...' + b; + } + return title; + }, + + submenuInfo: function (menu, submenu) { + var key = menu + '.' + submenu; + return this.anyAttribute(this.submenu, 'info', key, null); + }, + + submenuHeight: function (menu, submenu, relative) { + var key = menu + '.' + submenu; + return this.anyAttribute(this.submenu, 'height', key, 1.0) * relative; + }, + + contextInfo: function (id) { + var x = this.anyAttribute(this.context, 'info', id, null); + + if (x !== null) { + return '<div class="shorten dashboard-context-info netdata-chart-alignment" role="document">' + x + '</div>'; + } else { + return ''; + } + }, + + contextValueRange: function (id) { + if (typeof this.context[id] !== 'undefined' && typeof this.context[id].valueRange !== 'undefined') { + return this.context[id].valueRange; + } else { + return '[null, null]'; + } + }, + + contextHeight: function (id, def) { + if (typeof this.context[id] !== 'undefined' && typeof this.context[id].height !== 'undefined') { + return def * this.context[id].height; + } else { + return def; + } + }, + + contextDecimalDigits: function (id, def) { + if (typeof this.context[id] !== 'undefined' && typeof this.context[id].decimalDigits !== 'undefined') { + return this.context[id].decimalDigits; + } else { + return def; + } + } +}; + +// ---------------------------------------------------------------------------- + +// enrich the data structure returned by netdata +// to reflect our menu system and content +// TODO: this is a shame - we should fix charts naming (issue #807) +function enrichChartData(chart) { + var parts = chart.type.split('_'); + var tmp = parts[0]; + + switch (tmp) { + case 'ap': + case 'net': + case 'disk': + case 'powersupply': + case 'statsd': + chart.menu = tmp; + break; + + case 'apache': + chart.menu = chart.type; + if (parts.length > 2 && parts[1] === 'cache') { + chart.menu_pattern = tmp + '_' + parts[1]; + } else if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + + case 'bind': + chart.menu = chart.type; + if (parts.length > 2 && parts[1] === 'rndc') { + chart.menu_pattern = tmp + '_' + parts[1]; + } else if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + + case 'cgroup': + chart.menu = chart.type; + if (chart.id.match(/.*[\._\/-:]qemu[\._\/-:]*/) || chart.id.match(/.*[\._\/-:]kvm[\._\/-:]*/)) { + chart.menu_pattern = 'cgqemu'; + } else { + chart.menu_pattern = 'cgroup'; + } + break; + + case 'go': + chart.menu = chart.type; + if (parts.length > 2 && parts[1] === 'expvar') { + chart.menu_pattern = tmp + '_' + parts[1]; + } else if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + + case 'isc': + chart.menu = chart.type; + if (parts.length > 2 && parts[1] === 'dhcpd') { + chart.menu_pattern = tmp + '_' + parts[1]; + } else if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + + case 'ovpn': + chart.menu = chart.type; + if (parts.length > 3 && parts[1] === 'status' && parts[2] === 'log') { + chart.menu_pattern = tmp + '_' + parts[1]; + } else if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + + case 'smartd': + case 'web': + chart.menu = chart.type; + if (parts.length > 2 && parts[1] === 'log') { + chart.menu_pattern = tmp + '_' + parts[1]; + } else if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + + case 'tc': + chart.menu = tmp; + + // find a name for this device from fireqos info + // we strip '_(in|out)' or '(in|out)_' + if (chart.context === 'tc.qos' && (typeof options.submenu_names[chart.family] === 'undefined' || options.submenu_names[chart.family] === chart.family)) { + var n = chart.name.split('.')[1]; + if (n.endsWith('_in')) { + options.submenu_names[chart.family] = n.slice(0, n.lastIndexOf('_in')); + } else if (n.endsWith('_out')) { + options.submenu_names[chart.family] = n.slice(0, n.lastIndexOf('_out')); + } else if (n.startsWith('in_')) { + options.submenu_names[chart.family] = n.slice(3, n.length); + } else if (n.startsWith('out_')) { + options.submenu_names[chart.family] = n.slice(4, n.length); + } else { + options.submenu_names[chart.family] = n; + } + } + + // increase the priority of IFB devices + // to have inbound appear before outbound + if (chart.id.match(/.*-ifb$/)) { + chart.priority--; + } + + break; + + default: + chart.menu = chart.type; + if (parts.length > 1) { + chart.menu_pattern = tmp; + } + break; + } + + chart.submenu = chart.family; +} + +// ---------------------------------------------------------------------------- + +function headMain(os, charts, duration) { + void (os); + + if (urlOptions.mode === 'print') { + return ''; + } + + var head = ''; + + if (typeof charts['system.swap'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.swap"' + + ' data-dimensions="used"' + + ' data-append-options="percentage"' + + ' data-chart-library="easypiechart"' + + ' data-title="Used Swap"' + + ' data-units="%"' + + ' data-easypiechart-max-value="100"' + + ' data-width="9%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-colors="#DD4400"' + + ' role="application"></div>'; + } + + if (typeof charts['system.io'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.io"' + + ' data-dimensions="in"' + + ' data-chart-library="easypiechart"' + + ' data-title="Disk Read"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.io.mainhead"' + + ' role="application"></div>'; + + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.io"' + + ' data-dimensions="out"' + + ' data-chart-library="easypiechart"' + + ' data-title="Disk Write"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.io.mainhead"' + + ' role="application"></div>'; + } + else if (typeof charts['system.pgpgio'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.pgpgio"' + + ' data-dimensions="in"' + + ' data-chart-library="easypiechart"' + + ' data-title="Disk Read"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.pgpgio.mainhead"' + + ' role="application"></div>'; + + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.pgpgio"' + + ' data-dimensions="out"' + + ' data-chart-library="easypiechart"' + + ' data-title="Disk Write"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.pgpgio.mainhead"' + + ' role="application"></div>'; + } + + if (typeof charts['system.cpu'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.cpu"' + + ' data-chart-library="gauge"' + + ' data-title="CPU"' + + ' data-units="%"' + + ' data-gauge-max-value="100"' + + ' data-width="20%"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-colors="' + NETDATA.colors[12] + '"' + + ' role="application"></div>'; + } + + if (typeof charts['system.net'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.net"' + + ' data-dimensions="received"' + + ' data-chart-library="easypiechart"' + + ' data-title="Net Inbound"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.net.mainhead"' + + ' role="application"></div>'; + + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.net"' + + ' data-dimensions="sent"' + + ' data-chart-library="easypiechart"' + + ' data-title="Net Outbound"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.net.mainhead"' + + ' role="application"></div>'; + } + else if (typeof charts['system.ip'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ip"' + + ' data-dimensions="received"' + + ' data-chart-library="easypiechart"' + + ' data-title="IP Inbound"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.ip.mainhead"' + + ' role="application"></div>'; + + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ip"' + + ' data-dimensions="sent"' + + ' data-chart-library="easypiechart"' + + ' data-title="IP Outbound"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.ip.mainhead"' + + ' role="application"></div>'; + } + else if (typeof charts['system.ipv4'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv4"' + + ' data-dimensions="received"' + + ' data-chart-library="easypiechart"' + + ' data-title="IPv4 Inbound"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.ipv4.mainhead"' + + ' role="application"></div>'; + + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv4"' + + ' data-dimensions="sent"' + + ' data-chart-library="easypiechart"' + + ' data-title="IPv4 Outbound"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.ipv4.mainhead"' + + ' role="application"></div>'; + } + else if (typeof charts['system.ipv6'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv6"' + + ' data-dimensions="received"' + + ' data-chart-library="easypiechart"' + + ' data-title="IPv6 Inbound"' + + ' data-units="kbps"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.ipv6.mainhead"' + + ' role="application"></div>'; + + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv6"' + + ' data-dimensions="sent"' + + ' data-chart-library="easypiechart"' + + ' data-title="IPv6 Outbound"' + + ' data-units="kbps"' + + ' data-width="11%"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-common-units="system.ipv6.mainhead"' + + ' role="application"></div>'; + } + + if (typeof charts['system.ram'] !== 'undefined') { + head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ram"' + + ' data-dimensions="used|buffers|active|wired"' // active and wired are FreeBSD stats + + ' data-append-options="percentage"' + + ' data-chart-library="easypiechart"' + + ' data-title="Used RAM"' + + ' data-units="%"' + + ' data-easypiechart-max-value="100"' + + ' data-width="9%"' + + ' data-after="-' + duration.toString() + '"' + + ' data-points="' + duration.toString() + '"' + + ' data-colors="' + NETDATA.colors[7] + '"' + + ' role="application"></div>'; + } + + return head; +} + +function generateHeadCharts(type, chart, duration) { + if (urlOptions.mode === 'print') { + return ''; + } + + var head = ''; + var hcharts = netdataDashboard.anyAttribute(netdataDashboard.context, type, chart.context, []); + if (hcharts.length > 0) { + var hi = 0, hlen = hcharts.length; + while (hi < hlen) { + if (typeof hcharts[hi] === 'function') { + head += hcharts[hi](netdataDashboard.os, chart.id).replace(/CHART_DURATION/g, duration.toString()).replace(/CHART_UNIQUE_ID/g, chart.id); + } else { + head += hcharts[hi].replace(/CHART_DURATION/g, duration.toString()).replace(/CHART_UNIQUE_ID/g, chart.id); + } + hi++; + } + } + return head; +} + +function renderPage(menus, data) { + var div = document.getElementById('charts_div'); + var pcent_width = Math.floor(100 / chartsPerRow($(div).width())); + + // find the proper duration for per-second updates + var duration = Math.round(($(div).width() * pcent_width / 100 * data.update_every / 3) / 60) * 60; + options.duration = duration; + options.update_every = data.update_every; + + var html = ''; + var sidebar = '<ul class="nav dashboard-sidenav" data-spy="affix" id="sidebar_ul">'; + var mainhead = headMain(netdataDashboard.os, data.charts, duration); + + // sort the menus + var main = sortObjectByPriority(menus); + var i = 0, len = main.length; + while (i < len) { + var menu = main[i++]; + + // generate an entry at the main menu + + var menuid = NETDATA.name2id('menu_' + menu); + sidebar += '<li class=""><a href="#' + menuid + '" onClick="return scrollToId(\'' + menuid + '\');">' + menus[menu].icon + ' ' + menus[menu].title + '</a><ul class="nav">'; + html += '<div role="section" class="dashboard-section"><div role="sectionhead"><h1 id="' + menuid + '" role="heading">' + menus[menu].icon + ' ' + menus[menu].title + '</h1></div><div role="section" class="dashboard-subsection">'; + + if (menus[menu].info !== null) { + html += menus[menu].info; + } + + // console.log(' >> ' + menu + ' (' + menus[menu].priority + '): ' + menus[menu].title); + + var shtml = ''; + var mhead = '<div class="netdata-chart-row">' + mainhead; + mainhead = ''; + + // sort the submenus of this menu + var sub = sortObjectByPriority(menus[menu].submenus); + var si = 0, slen = sub.length; + while (si < slen) { + var submenu = sub[si++]; + + // generate an entry at the submenu + var submenuid = NETDATA.name2id('menu_' + menu + '_submenu_' + submenu); + sidebar += '<li class><a href="#' + submenuid + '" onClick="return scrollToId(\'' + submenuid + '\');">' + menus[menu].submenus[submenu].title + '</a></li>'; + shtml += '<div role="section" class="dashboard-section-container" id="' + submenuid + '"><h2 id="' + submenuid + '" class="netdata-chart-alignment" role="heading">' + menus[menu].submenus[submenu].title + '</h2>'; + + if (menus[menu].submenus[submenu].info !== null) { + shtml += '<div class="dashboard-submenu-info netdata-chart-alignment" role="document">' + menus[menu].submenus[submenu].info + '</div>'; + } + + var head = '<div class="netdata-chart-row">'; + var chtml = ''; + + // console.log(' \------- ' + submenu + ' (' + menus[menu].submenus[submenu].priority + '): ' + menus[menu].submenus[submenu].title); + + // sort the charts in this submenu of this menu + menus[menu].submenus[submenu].charts.sort(prioritySort); + var ci = 0, clen = menus[menu].submenus[submenu].charts.length; + while (ci < clen) { + var chart = menus[menu].submenus[submenu].charts[ci++]; + + // generate the submenu heading charts + mhead += generateHeadCharts('mainheads', chart, duration); + head += generateHeadCharts('heads', chart, duration); + + function chartCommonMin(family, context, units) { + var x = netdataDashboard.anyAttribute(netdataDashboard.context, 'commonMin', context, undefined); + if (typeof x !== 'undefined') { + return ' data-common-min="' + family + '/' + context + '/' + units + '"'; + } else { + return ''; + } + } + + function chartCommonMax(family, context, units) { + var x = netdataDashboard.anyAttribute(netdataDashboard.context, 'commonMax', context, undefined); + if (typeof x !== 'undefined') { + return ' data-common-max="' + family + '/' + context + '/' + units + '"'; + } else { + return ''; + } + } + + // generate the chart + if (urlOptions.mode === 'print') { + chtml += '<div role="row" class="dashboard-print-row">'; + } + + chtml += '<div class="netdata-chartblock-container" style="width: ' + pcent_width.toString() + '%;">' + netdataDashboard.contextInfo(chart.context) + '<div class="netdata-container" id="chart_' + NETDATA.name2id(chart.id) + '" data-netdata="' + chart.id + '"' + + ' data-width="100%"' + + ' data-height="' + netdataDashboard.contextHeight(chart.context, options.chartsHeight).toString() + 'px"' + + ' data-dygraph-valuerange="' + netdataDashboard.contextValueRange(chart.context) + '"' + + ' data-before="0"' + + ' data-after="-' + duration.toString() + '"' + + ' data-id="' + NETDATA.name2id(options.hostname + '/' + chart.id) + '"' + + ' data-colors="' + netdataDashboard.anyAttribute(netdataDashboard.context, 'colors', chart.context, '') + '"' + + ' data-decimal-digits="' + netdataDashboard.contextDecimalDigits(chart.context, -1) + '"' + + chartCommonMin(chart.family, chart.context, chart.units) + + chartCommonMax(chart.family, chart.context, chart.units) + + ' role="application"></div></div>'; + + if (urlOptions.mode === 'print') { + chtml += '</div>'; + } + } + + head += '</div>'; + shtml += head + chtml + '</div>'; + } + + mhead += '</div>'; + sidebar += '</ul></li>'; + html += mhead + shtml + '</div></div><hr role="separator"/>'; + } + + const isMemoryModeDbEngine = data.memory_mode === "dbengine"; + + sidebar += '<li class="" style="padding-top:15px;"><a href="https://docs.netdata.cloud/collectors/quickstart/" target="_blank"><i class="fas fa-plus"></i> Add more charts</a></li>'; + sidebar += '<li class=""><a href="https://docs.netdata.cloud/health/quickstart/" target="_blank"><i class="fas fa-plus"></i> Add more alarms</a></li>'; + sidebar += '<li class="" style="margin:20px;color:#666;"><small>Every ' + + ((data.update_every === 1) ? 'second' : data.update_every.toString() + ' seconds') + ', ' + + 'Netdata collects <strong>' + data.dimensions_count.toLocaleString() + '</strong> metrics on ' + + data.hostname.toString() + ', presents them in <strong>' + + data.charts_count.toLocaleString() + '</strong> charts' + + (isMemoryModeDbEngine ? '' : ',') + // oxford comma + ' and monitors them with <strong>' + + data.alarms_count.toLocaleString() + '</strong> alarms.'; + + if (!isMemoryModeDbEngine) { + sidebar += '<br /> <br />Get more history by ' + + '<a href="https://docs.netdata.cloud/docs/configuration-guide/#increase-the-metrics-retention-period" target=_blank>configuring Netdata\'s <strong>history</strong></a> or using the <a href="https://docs.netdata.cloud/database/engine/" target=_blank>DB engine.</a>'; + } + + sidebar += '<br/> <br/><strong>netdata</strong><br/>' + data.version.toString() + '</small></li>'; + + sidebar += '</ul>'; + div.innerHTML = html; + document.getElementById('sidebar').innerHTML = sidebar; + + if (urlOptions.highlight === true) { + NETDATA.globalChartUnderlay.init(null + , urlOptions.highlight_after + , urlOptions.highlight_before + , (urlOptions.after > 0) ? urlOptions.after : null + , (urlOptions.before > 0) ? urlOptions.before : null + ); + } else { + NETDATA.globalChartUnderlay.clear(); + } + + if (urlOptions.mode === 'print') { + printPage(); + } else { + finalizePage(); + } +} + +function renderChartsAndMenu(data) { + options.menus = {}; + options.submenu_names = {}; + + var menus = options.menus; + var charts = data.charts; + var m, menu_key; + + for (var c in charts) { + if (!charts.hasOwnProperty(c)) { + continue; + } + + var chart = charts[c]; + enrichChartData(chart); + m = chart.menu; + + // create the menu + if (typeof menus[m] === 'undefined') { + menus[m] = { + menu_pattern: chart.menu_pattern, + priority: chart.priority, + submenus: {}, + title: netdataDashboard.menuTitle(chart), + icon: netdataDashboard.menuIcon(chart), + info: netdataDashboard.menuInfo(chart), + height: netdataDashboard.menuHeight(chart) * options.chartsHeight + }; + } else { + if (typeof (menus[m].menu_pattern) === 'undefined') { + menus[m].menu_pattern = chart.menu_pattern; + } + + if (chart.priority < menus[m].priority) { + menus[m].priority = chart.priority; + } + } + + menu_key = (typeof (menus[m].menu_pattern) !== 'undefined') ? menus[m].menu_pattern : m; + + // create the submenu + if (typeof menus[m].submenus[chart.submenu] === 'undefined') { + menus[m].submenus[chart.submenu] = { + priority: chart.priority, + charts: [], + title: null, + info: netdataDashboard.submenuInfo(menu_key, chart.submenu), + height: netdataDashboard.submenuHeight(menu_key, chart.submenu, menus[m].height) + }; + } else { + if (chart.priority < menus[m].submenus[chart.submenu].priority) { + menus[m].submenus[chart.submenu].priority = chart.priority; + } + } + + // index the chart in the menu/submenu + menus[m].submenus[chart.submenu].charts.push(chart); + } + + // propagate the descriptive subname given to QoS + // to all the other submenus with the same name + for (var m in menus) { + if (!menus.hasOwnProperty(m)) { + continue; + } + + for (var s in menus[m].submenus) { + if (!menus[m].submenus.hasOwnProperty(s)) { + continue; + } + + // set the family using a name + if (typeof options.submenu_names[s] !== 'undefined') { + menus[m].submenus[s].title = s + ' (' + options.submenu_names[s] + ')'; + } else { + menu_key = (typeof (menus[m].menu_pattern) !== 'undefined') ? menus[m].menu_pattern : m; + menus[m].submenus[s].title = netdataDashboard.submenuTitle(menu_key, s); + } + } + } + + renderPage(menus, data); +} + +// ---------------------------------------------------------------------------- + +function loadJs(url, callback) { + $.ajax({ + url: url.startsWith("http") ? url : transformWithOldSuffix(url), + cache: true, + dataType: "script", + xhrFields: { withCredentials: true } // required for the cookie + }) + .fail(function () { + alert('Cannot load required JS library: ' + url); + }) + .always(function () { + if (typeof callback === 'function') { + callback(); + } + }) +} + +var clipboardLoaded = false; + +function loadClipboard(callback) { + if (clipboardLoaded === false) { + clipboardLoaded = true; + loadJs('lib/clipboard-polyfill-be05dad.js', callback); + } else { + callback(); + } +} + +var bootstrapTableLoaded = false; + +function loadBootstrapTable(callback) { + if (bootstrapTableLoaded === false) { + bootstrapTableLoaded = true; + loadJs('lib/bootstrap-table-1.11.0.min.js', function () { + loadJs('lib/bootstrap-table-export-1.11.0.min.js', function () { + loadJs('lib/tableExport-1.6.0.min.js', callback); + }) + }); + } else { + callback(); + } +} + +var bootstrapSliderLoaded = false; + +function loadBootstrapSlider(callback) { + if (bootstrapSliderLoaded === false) { + bootstrapSliderLoaded = true; + loadJs('lib/bootstrap-slider-10.0.0.min.js', function () { + NETDATA._loadCSS(transformWithOldSuffix("css/bootstrap-slider-10.0.0.min.css")); + callback(); + }); + } else { + callback(); + } +} + +var lzStringLoaded = false; + +function loadLzString(callback) { + if (lzStringLoaded === false) { + lzStringLoaded = true; + loadJs('lib/lz-string-1.4.4.min.js', callback); + } else { + callback(); + } +} + +var pakoLoaded = false; + +function loadPako(callback) { + if (pakoLoaded === false) { + pakoLoaded = true; + loadJs('lib/pako-1.0.6.min.js', callback); + } else { + callback(); + } +} + +// ---------------------------------------------------------------------------- + +function clipboardCopy(text) { + clipboard.writeText(text); +} + +function clipboardCopyBadgeEmbed(url) { + clipboard.writeText('<embed src="' + url + '" type="image/svg+xml" height="20"/>'); +} + +// ---------------------------------------------------------------------------- + +function alarmsUpdateModal() { + var active = '<h3>Raised Alarms</h3><table class="table">'; + var all = '<h3>All Running Alarms</h3><div class="panel-group" id="alarms_all_accordion" role="tablist" aria-multiselectable="true">'; + var footer = '<hr/><a href="https://github.com/netdata/netdata/tree/master/web/api/badges#netdata-badges" target="_blank">netdata badges</a> refresh automatically. Their color indicates the state of the alarm: <span style="color: #e05d44"><b> red </b></span> is critical, <span style="color:#fe7d37"><b> orange </b></span> is warning, <span style="color: #4c1"><b> bright green </b></span> is ok, <span style="color: #9f9f9f"><b> light grey </b></span> is undefined (i.e. no data or no status), <span style="color: #000"><b> black </b></span> is not initialized. You can copy and paste their URLs to embed them in any web page.<br/>netdata can send notifications for these alarms. Check <a href="https://github.com/netdata/netdata/blob/master/health/notifications/health_alarm_notify.conf" target="_blank">this configuration file</a> for more information.'; + + loadClipboard(function () { + }); + + NETDATA.alarms.get('all', function (data) { + options.alarm_families = []; + + alarmsCallback(data); + + if (data === null) { + document.getElementById('alarms_active').innerHTML = + document.getElementById('alarms_all').innerHTML = + document.getElementById('alarms_log').innerHTML = + 'failed to load alarm data!'; + return; + } + + function alarmid4human(id) { + if (id === 0) { + return '-'; + } + + return id.toString(); + } + + function timestamp4human(timestamp, space) { + if (timestamp === 0) { + return '-'; + } + + if (typeof space === 'undefined') { + space = ' '; + } + + var t = new Date(timestamp * 1000); + var now = new Date(); + + if (t.toDateString() === now.toDateString()) { + return t.toLocaleTimeString(); + } + + return t.toLocaleDateString() + space + t.toLocaleTimeString(); + } + + function alarm_lookup_explain(alarm, chart) { + var dimensions = ' of all values '; + + if (chart.dimensions.length > 1) { + dimensions = ' of the sum of all dimensions '; + } + + if (typeof alarm.lookup_dimensions !== 'undefined') { + var d = alarm.lookup_dimensions.replace(/|/g, ','); + var x = d.split(','); + if (x.length > 1) { + dimensions = 'of the sum of dimensions <code>' + alarm.lookup_dimensions + '</code> '; + } else { + dimensions = 'of all values of dimension <code>' + alarm.lookup_dimensions + '</code> '; + } + } + + return '<code>' + alarm.lookup_method + '</code> ' + + dimensions + ', of chart <code>' + alarm.chart + '</code>' + + ', starting <code>' + NETDATA.seconds4human(alarm.lookup_after + alarm.lookup_before, { space: ' ' }) + '</code> and up to <code>' + NETDATA.seconds4human(alarm.lookup_before, { space: ' ' }) + '</code>' + + ((alarm.lookup_options) ? (', with options <code>' + alarm.lookup_options.replace(/ /g, ', ') + '</code>') : '') + + '.'; + } + + function alarm_to_html(alarm, full) { + var chart = options.data.charts[alarm.chart]; + if (typeof (chart) === 'undefined') { + chart = options.data.charts_by_name[alarm.chart]; + if (typeof (chart) === 'undefined') { + // this means the charts loaded are incomplete + // probably netdata was restarted and more alarms + // are now available. + console.log('Cannot find chart ' + alarm.chart + ', you probably need to refresh the page.'); + return ''; + } + } + + var has_alarm = (typeof alarm.warn !== 'undefined' || typeof alarm.crit !== 'undefined'); + var badge_url = NETDATA.alarms.server + '/api/v1/badge.svg?chart=' + alarm.chart + '&alarm=' + alarm.name + '&refresh=auto'; + + var action_buttons = '<br/> <br/>role: <b>' + alarm.recipient + '</b><br/> <br/>' + + '<div class="action-button ripple" title="click to scroll the dashboard to the chart of this alarm" data-toggle="tooltip" data-placement="bottom" onClick="scrollToChartAfterHidingModal(\'' + alarm.chart + '\', ' + alarm.last_status_change * 1000 + ', \'' + alarm.status + '\'); $(\'#alarmsModal\').modal(\'hide\'); return false;"><i class="fab fa-periscope"></i></div>' + + '<div class="action-button ripple" title="click to copy to the clipboard the URL of this badge" data-toggle="tooltip" data-placement="bottom" onClick="clipboardCopy(\'' + badge_url + '\'); return false;"><i class="far fa-copy"></i></div>' + + '<div class="action-button ripple" title="click to copy to the clipboard an auto-refreshing <code>embed</code> html element for this badge" data-toggle="tooltip" data-placement="bottom" onClick="clipboardCopyBadgeEmbed(\'' + badge_url + '\'); return false;"><i class="fas fa-copy"></i></div>'; + + var html = '<tr><td class="text-center" style="vertical-align:middle" width="40%"><b>' + alarm.chart + '</b><br/> <br/><embed src="' + badge_url + '" type="image/svg+xml" height="20"/><br/> <br/><span style="font-size: 18px">' + alarm.info + '</span>' + action_buttons + '</td>' + + '<td><table class="table">' + + ((typeof alarm.warn !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">warning when</td><td><span style="font-family: monospace; color:#fe7d37; font-weight: bold;">' + alarm.warn + '</span></td></tr>') : '') + + ((typeof alarm.crit !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">critical when</td><td><span style="font-family: monospace; color: #e05d44; font-weight: bold;">' + alarm.crit + '</span></td></tr>') : ''); + + if (full === true) { + var units = chart.units; + if (units === '%') { + units = '%'; + } + + html += ((typeof alarm.lookup_after !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">db lookup</td><td>' + alarm_lookup_explain(alarm, chart) + '</td></tr>') : '') + + ((typeof alarm.calc !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">calculation</td><td><span style="font-family: monospace;">' + alarm.calc + '</span></td></tr>') : '') + + ((chart.green !== null) ? ('<tr><td width="10%" style="text-align:right">green threshold</td><td><code>' + chart.green + ' ' + units + '</code></td></tr>') : '') + + ((chart.red !== null) ? ('<tr><td width="10%" style="text-align:right">red threshold</td><td><code>' + chart.red + ' ' + units + '</code></td></tr>') : ''); + } + + if (alarm.warn_repeat_every > 0) { + html += '<tr><td width="10%" style="text-align:right">repeat warning</td><td>' + NETDATA.seconds4human(alarm.warn_repeat_every) + '</td></tr>'; + } + + if (alarm.crit_repeat_every > 0) { + html += '<tr><td width="10%" style="text-align:right">repeat critical</td><td>' + NETDATA.seconds4human(alarm.crit_repeat_every) + '</td></tr>'; + } + + var delay = ''; + if ((alarm.delay_up_duration > 0 || alarm.delay_down_duration > 0) && alarm.delay_multiplier !== 0 && alarm.delay_max_duration > 0) { + if (alarm.delay_up_duration === alarm.delay_down_duration) { + delay += '<small><br/>hysteresis ' + NETDATA.seconds4human(alarm.delay_up_duration, { + space: ' ', + negative_suffix: '' + }); + } else { + delay = '<small><br/>hysteresis '; + if (alarm.delay_up_duration > 0) { + delay += 'on escalation <code>' + NETDATA.seconds4human(alarm.delay_up_duration, { + space: ' ', + negative_suffix: '' + }) + '</code>, '; + } + if (alarm.delay_down_duration > 0) { + delay += 'on recovery <code>' + NETDATA.seconds4human(alarm.delay_down_duration, { + space: ' ', + negative_suffix: '' + }) + '</code>, '; + } + } + if (alarm.delay_multiplier !== 1.0) { + delay += 'multiplied by <code>' + alarm.delay_multiplier.toString() + '</code>'; + delay += ', up to <code>' + NETDATA.seconds4human(alarm.delay_max_duration, { + space: ' ', + negative_suffix: '' + }) + '</code>'; + } + delay += '</small>'; + } + + html += '<tr><td width="10%" style="text-align:right">check every</td><td>' + NETDATA.seconds4human(alarm.update_every, { + space: ' ', + negative_suffix: '' + }) + '</td></tr>' + + ((has_alarm === true) ? ('<tr><td width="10%" style="text-align:right">execute</td><td><span style="font-family: monospace;">' + alarm.exec + '</span>' + delay + '</td></tr>') : '') + + '<tr><td width="10%" style="text-align:right">source</td><td><span style="font-family: monospace;">' + alarm.source + '</span></td></tr>' + + '</table></td></tr>'; + + return html; + } + + function alarm_family_show(id) { + var html = '<table class="table">'; + var family = options.alarm_families[id]; + var len = family.arr.length; + while (len--) { + var alarm = family.arr[len]; + html += alarm_to_html(alarm, true); + } + html += '</table>'; + + $('#alarm_all_' + id.toString()).html(html); + enableTooltipsAndPopovers(); + } + + // find the proper family of each alarm + var x, family, alarm; + var count_active = 0; + var count_all = 0; + var families = {}; + var families_sort = []; + for (x in data.alarms) { + if (!data.alarms.hasOwnProperty(x)) { + continue; + } + + alarm = data.alarms[x]; + family = alarm.family; + + // find the chart + var chart = options.data.charts[alarm.chart]; + if (typeof chart === 'undefined') { + chart = options.data.charts_by_name[alarm.chart]; + } + + // not found - this should never happen! + if (typeof chart === 'undefined') { + console.log('WARNING: alarm ' + x + ' is linked to chart ' + alarm.chart + ', which is not found in the list of chart got from the server.'); + chart = { priority: 9999999 }; + } + else if (typeof chart.menu !== 'undefined' && typeof chart.submenu !== 'undefined') + // the family based on the chart + { + family = chart.menu + ' - ' + chart.submenu; + } + + if (typeof families[family] === 'undefined') { + families[family] = { + name: family, + arr: [], + priority: chart.priority + }; + + families_sort.push(families[family]); + } + + if (chart.priority < families[family].priority) { + families[family].priority = chart.priority; + } + + families[family].arr.unshift(alarm); + } + + // sort the families, like the dashboard menu does + var families_sorted = families_sort.sort(function (a, b) { + if (a.priority < b.priority) { + return -1; + } + if (a.priority > b.priority) { + return 1; + } + return naturalSortCompare(a.name, b.name); + }); + + var i = 0; + var fc = 0; + var len = families_sorted.length; + while (len--) { + family = families_sorted[i++].name; + var active_family_added = false; + var expanded = 'true'; + var collapsed = ''; + var cin = 'in'; + + if (fc !== 0) { + all += "</table></div></div></div>"; + expanded = 'false'; + collapsed = 'class="collapsed"'; + cin = ''; + } + + all += '<div class="panel panel-default"><div class="panel-heading" role="tab" id="alarm_all_heading_' + fc.toString() + '"><h4 class="panel-title"><a ' + collapsed + ' role="button" data-toggle="collapse" data-parent="#alarms_all_accordion" href="#alarm_all_' + fc.toString() + '" aria-expanded="' + expanded + '" aria-controls="alarm_all_' + fc.toString() + '">' + family.toString() + '</a></h4></div><div id="alarm_all_' + fc.toString() + '" class="panel-collapse collapse ' + cin + '" role="tabpanel" aria-labelledby="alarm_all_heading_' + fc.toString() + '" data-alarm-id="' + fc.toString() + '"><div class="panel-body" id="alarm_all_body_' + fc.toString() + '">'; + + options.alarm_families[fc] = families[family]; + + fc++; + + var arr = families[family].arr; + var c = arr.length; + while (c--) { + alarm = arr[c]; + if (alarm.status === 'WARNING' || alarm.status === 'CRITICAL') { + if (!active_family_added) { + active_family_added = true; + active += '<tr><th class="text-center" colspan="2"><h4>' + family + '</h4></th></tr>'; + } + count_active++; + active += alarm_to_html(alarm, true); + } + + count_all++; + } + } + active += "</table>"; + if (families_sorted.length > 0) { + all += "</div></div></div>"; + } + all += "</div>"; + + if (!count_active) { + active += '<div style="width:100%; height: 100px; text-align: center;"><span style="font-size: 50px;"><i class="fas fa-thumbs-up"></i></span><br/>Everything is normal. No raised alarms.</div>'; + } else { + active += footer; + } + + if (!count_all) { + all += "<h4>No alarms are running in this system.</h4>"; + } else { + all += footer; + } + + document.getElementById('alarms_active').innerHTML = active; + document.getElementById('alarms_all').innerHTML = all; + enableTooltipsAndPopovers(); + + if (families_sorted.length > 0) { + alarm_family_show(0); + } + + // register bootstrap events + var $accordion = $('#alarms_all_accordion'); + $accordion.on('show.bs.collapse', function (d) { + var target = $(d.target); + var id = $(target).data('alarm-id'); + alarm_family_show(id); + }); + $accordion.on('hidden.bs.collapse', function (d) { + var target = $(d.target); + var id = $(target).data('alarm-id'); + $('#alarm_all_' + id.toString()).html(''); + }); + + document.getElementById('alarms_log').innerHTML = '<h3>Alarm Log</h3><table id="alarms_log_table"></table>'; + + loadBootstrapTable(function () { + $('#alarms_log_table').bootstrapTable({ + url: NETDATA.alarms.server + '/api/v1/alarm_log?all', + cache: false, + pagination: true, + pageSize: 10, + showPaginationSwitch: false, + search: true, + searchTimeOut: 300, + searchAlign: 'left', + showColumns: true, + showExport: true, + exportDataType: 'basic', + exportOptions: { + fileName: 'netdata_alarm_log' + }, + onClickRow: function (row, $element,field) { + void (field); + void ($element); + let main_url; + let common_url = "&host=" + encodeURIComponent(row['hostname']) + "&chart=" + encodeURIComponent(row['chart']) + "&family=" + encodeURIComponent(row['family']) + "&alarm=" + encodeURIComponent(row['name']) + "&alarm_unique_id=" + row['unique_id'] + "&alarm_id=" + row['alarm_id'] + "&alarm_event_id=" + row['alarm_event_id'] + "&alarm_when=" + row['when']; + if (NETDATA.registry.isUsingGlobalRegistry() && NETDATA.registry.machine_guid != null) { + main_url = "https://netdata.cloud/alarms/redirect?agentID=" + NETDATA.registry.machine_guid + common_url; + } else { + main_url = NETDATA.registry.server + "/goto-host-from-alarm.html?" + common_url ; + } + window.open(main_url,"_blank"); + }, + rowStyle: function (row, index) { + void (index); + + switch (row.status) { + case 'CRITICAL': + return { classes: 'danger' }; + break; + case 'WARNING': + return { classes: 'warning' }; + break; + case 'UNDEFINED': + return { classes: 'info' }; + break; + case 'CLEAR': + return { classes: 'success' }; + break; + } + return {}; + }, + showFooter: false, + showHeader: true, + showRefresh: true, + showToggle: false, + sortable: true, + silentSort: false, + columns: [ + { + field: 'when', + title: 'Event Date', + valign: 'middle', + titleTooltip: 'The date and time the even took place', + formatter: function (value, row, index) { + void (row); + void (index); + return timestamp4human(value, ' '); + }, + align: 'center', + switchable: false, + sortable: true + }, + { + field: 'hostname', + title: 'Host', + valign: 'middle', + titleTooltip: 'The host that generated this event', + align: 'center', + visible: false, + sortable: true + }, + { + field: 'unique_id', + title: 'Unique ID', + titleTooltip: 'The host unique ID for this event', + formatter: function (value, row, index) { + void (row); + void (index); + return alarmid4human(value); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'alarm_id', + title: 'Alarm ID', + titleTooltip: 'The ID of the alarm that generated this event', + formatter: function (value, row, index) { + void (row); + void (index); + return alarmid4human(value); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'alarm_event_id', + title: 'Alarm Event ID', + titleTooltip: 'The incremental ID of this event for the given alarm', + formatter: function (value, row, index) { + void (row); + void (index); + return alarmid4human(value); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'chart', + title: 'Chart', + titleTooltip: 'The chart the alarm is attached to', + align: 'center', + valign: 'middle', + switchable: false, + sortable: true + }, + { + field: 'family', + title: 'Family', + titleTooltip: 'The family of the chart the alarm is attached to', + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'name', + title: 'Alarm', + titleTooltip: 'The alarm name that generated this event', + formatter: function (value, row, index) { + void (row); + void (index); + return value.toString().replace(/_/g, ' '); + }, + align: 'center', + valign: 'middle', + switchable: false, + sortable: true + }, + { + field: 'value_string', + title: 'Friendly Value', + titleTooltip: 'The value of the alarm, that triggered this event', + align: 'right', + valign: 'middle', + sortable: true + }, + { + field: 'old_value_string', + title: 'Friendly Old Value', + titleTooltip: 'The value of the alarm, just before this event', + align: 'right', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'old_value', + title: 'Old Value', + titleTooltip: 'The value of the alarm, just before this event', + formatter: function (value, row, index) { + void (row); + void (index); + return ((value !== null) ? Math.round(value * 100) / 100 : 'NaN').toString(); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'value', + title: 'Value', + titleTooltip: 'The value of the alarm, that triggered this event', + formatter: function (value, row, index) { + void (row); + void (index); + return ((value !== null) ? Math.round(value * 100) / 100 : 'NaN').toString(); + }, + align: 'right', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'units', + title: 'Units', + titleTooltip: 'The units of the value of the alarm', + align: 'left', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'old_status', + title: 'Old Status', + titleTooltip: 'The status of the alarm, just before this event', + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'status', + title: 'Status', + titleTooltip: 'The status of the alarm, that was set due to this event', + align: 'center', + valign: 'middle', + switchable: false, + sortable: true + }, + { + field: 'duration', + title: 'Last Duration', + titleTooltip: 'The duration the alarm was at its previous state, just before this event', + formatter: function (value, row, index) { + void (row); + void (index); + return NETDATA.seconds4human(value, { negative_suffix: '', space: ' ', now: 'no time' }); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'non_clear_duration', + title: 'Raised Duration', + titleTooltip: 'The duration the alarm was raised, just before this event', + formatter: function (value, row, index) { + void (row); + void (index); + return NETDATA.seconds4human(value, { negative_suffix: '', space: ' ', now: 'no time' }); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'recipient', + title: 'Recipient', + titleTooltip: 'The recipient of this event', + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'processed', + title: 'Processed Status', + titleTooltip: 'True when this event is processed', + formatter: function (value, row, index) { + void (row); + void (index); + + if (value === true) { + return 'DONE'; + } else { + return 'PENDING'; + } + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'updated', + title: 'Updated Status', + titleTooltip: 'True when this event has been updated by another event', + formatter: function (value, row, index) { + void (row); + void (index); + + if (value === true) { + return 'UPDATED'; + } else { + return 'CURRENT'; + } + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'updated_by_id', + title: 'Updated By ID', + titleTooltip: 'The unique ID of the event that obsoleted this one', + formatter: function (value, row, index) { + void (row); + void (index); + return alarmid4human(value); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'updates_id', + title: 'Updates ID', + titleTooltip: 'The unique ID of the event obsoleted because of this event', + formatter: function (value, row, index) { + void (row); + void (index); + return alarmid4human(value); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'exec', + title: 'Script', + titleTooltip: 'The script to handle the event notification', + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'exec_run', + title: 'Script Run At', + titleTooltip: 'The date and time the script has been ran', + formatter: function (value, row, index) { + void (row); + void (index); + return timestamp4human(value, ' '); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'exec_code', + title: 'Script Return Value', + titleTooltip: 'The return code of the script', + formatter: function (value, row, index) { + void (row); + void (index); + + if (value === 0) { + return 'OK (returned 0)'; + } else { + return 'FAILED (with code ' + value.toString() + ')'; + } + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'delay', + title: 'Script Delay', + titleTooltip: 'The hysteresis of the notification', + formatter: function (value, row, index) { + void (row); + void (index); + + return NETDATA.seconds4human(value, { negative_suffix: '', space: ' ', now: 'no time' }); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'delay_up_to_timestamp', + title: 'Script Delay Run At', + titleTooltip: 'The date and time the script should be run, after hysteresis', + formatter: function (value, row, index) { + void (row); + void (index); + return timestamp4human(value, ' '); + }, + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'info', + title: 'Description', + titleTooltip: 'A short description of the alarm', + align: 'center', + valign: 'middle', + visible: false, + sortable: true + }, + { + field: 'source', + title: 'Alarm Source', + titleTooltip: 'The source of configuration of the alarm', + align: 'center', + valign: 'middle', + visible: false, + sortable: true + } + ] + }); + // console.log($('#alarms_log_table').bootstrapTable('getOptions')); + }); + }); +} + +function alarmsCallback(data) { + var count = 0, x; + for (x in data.alarms) { + if (!data.alarms.hasOwnProperty(x)) { + continue; + } + + var alarm = data.alarms[x]; + if (alarm.status === 'WARNING' || alarm.status === 'CRITICAL') { + count++; + } + } + + if (count > 0) { + document.getElementById('alarms_count_badge').innerHTML = count.toString(); + } else { + document.getElementById('alarms_count_badge').innerHTML = ''; + } +} + +function initializeDynamicDashboardWithData(data) { + if (data !== null) { + options.hostname = data.hostname; + options.data = data; + options.version = data.version; + options.release_channel = data.release_channel; + netdataDashboard.os = data.os; + + if (typeof data.hosts !== 'undefined') { + options.hosts = data.hosts; + } + + // update the dashboard hostname + document.getElementById('hostname').innerHTML = '<span id="hostnametext">' + options.hostname + ((netdataSnapshotData !== null) ? ' (snap)' : '').toString() + '</span> <strong class="caret">'; + document.getElementById('hostname').href = NETDATA.serverDefault; + document.getElementById('netdataVersion').innerHTML = options.version; + + if (netdataSnapshotData !== null) { + $('#alarmsButton').hide(); + $('#updateButton').hide(); + // $('#loadButton').hide(); + $('#saveButton').hide(); + $('#printButton').hide(); + } + + // update the dashboard title + document.title = options.hostname + ' netdata dashboard'; + + // close the splash screen + $("#loadOverlay").css("display", "none"); + + // create a chart_by_name index + data.charts_by_name = {}; + var charts = data.charts; + var x; + for (x in charts) { + if (!charts.hasOwnProperty(x)) { + continue; + } + + var chart = charts[x]; + data.charts_by_name[chart.name] = chart; + } + + // render all charts + renderChartsAndMenu(data); + + // Ensure MyNetdata menu is rendered with latest host info #5370 + renderMyNetdataMenu(isSignedIn() ? cloudAgents : registryAgents); + } +} + +// an object to keep initilization configuration +// needed due to the async nature of the XSS modal +var initializeConfig = { + url: null, + custom_info: true, +}; + +function loadCustomDashboardInfo(url, callback) { + loadJs(url, function () { + $.extend(true, netdataDashboard, customDashboard); + callback(); + }); +} + +function initializeChartsAndCustomInfo() { + NETDATA.alarms.callback = alarmsCallback; + + // download all the charts the server knows + NETDATA.chartRegistry.downloadAll(initializeConfig.url, function (data) { + if (data !== null) { + if (initializeConfig.custom_info === true && typeof data.custom_info !== 'undefined' && data.custom_info !== "" && netdataSnapshotData === null) { + //console.log('loading custom dashboard decorations from server ' + initializeConfig.url); + loadCustomDashboardInfo(NETDATA.serverDefault + data.custom_info, function () { + initializeDynamicDashboardWithData(data); + }); + } else { + //console.log('not loading custom dashboard decorations from server ' + initializeConfig.url); + initializeDynamicDashboardWithData(data); + } + } + }); +} + +function xssModalDisableXss() { + //console.log('disabling xss checks'); + NETDATA.xss.enabled = false; + NETDATA.xss.enabled_for_data = false; + initializeConfig.custom_info = true; + initializeChartsAndCustomInfo(); + return false; +} + +function xssModalKeepXss() { + //console.log('keeping xss checks'); + NETDATA.xss.enabled = true; + NETDATA.xss.enabled_for_data = true; + initializeConfig.custom_info = false; + initializeChartsAndCustomInfo(); + return false; +} + +function initializeDynamicDashboard(netdata_url) { + if (typeof netdata_url === 'undefined' || netdata_url === null) { + netdata_url = NETDATA.serverDefault; + } + + initializeConfig.url = netdata_url; + + // initialize clickable alarms + NETDATA.alarms.chart_div_offset = -50; + NETDATA.alarms.chart_div_id_prefix = 'chart_'; + NETDATA.alarms.chart_div_animation_duration = 0; + + NETDATA.pause(function () { + if (typeof netdataCheckXSS !== 'undefined' && netdataCheckXSS === true) { + //$("#loadOverlay").css("display","none"); + document.getElementById('netdataXssModalServer').innerText = initializeConfig.url; + $('#xssModal').modal('show'); + } else { + initializeChartsAndCustomInfo(); + } + }); +} + +// ---------------------------------------------------------------------------- + +function versionLog(msg) { + document.getElementById('versionCheckLog').innerHTML = msg; +} + +// New way of checking for updates, based only on versions + +function versionsMatch(v1, v2) { + if (v1 == v2) { + return true; + } else { + let s1 = v1.split('.'); + let s2 = v2.split('.'); + // Check major version + let n1 = parseInt(s1[0].substring(1, 2), 10); + let n2 = parseInt(s2[0].substring(1, 2), 10); + if (n1 < n2) return false; + else if (n1 > n2) return true; + + // Check minor version + n1 = parseInt(s1[1], 10); + n2 = parseInt(s2[1], 10); + if (n1 < n2) return false; + else if (n1 > n2) return true; + + // Split patch: format could be e.g. 0-22-nightly + s1 = s1[2].split('-'); + s2 = s2[2].split('-'); + + n1 = parseInt(s1[0], 10); + n2 = parseInt(s2[0], 10); + if (n1 < n2) return false; + else if (n1 > n2) return true; + + n1 = (s1.length > 1) ? parseInt(s1[1], 10) : 0; + n2 = (s2.length > 1) ? parseInt(s2[1], 10) : 0; + if (n1 < n2) return false; + else return true; + } +} + +function getGithubLatestVersion(callback) { + versionLog('Downloading latest version id from github...'); + + $.ajax({ + url: 'https://api.github.com/repos/netdata/netdata/releases/latest', + async: true, + cache: false + }) + .done(function (data) { + data = data.tag_name.replace(/(\r\n|\n|\r| |\t)/gm, ""); + versionLog('Latest stable version from github is ' + data); + callback(data); + }) + .fail(function () { + versionLog('Failed to download the latest stable version id from github!'); + callback(null); + }); +} + +function getGCSLatestVersion(callback) { + versionLog('Downloading latest version id from GCS...'); + $.ajax({ + url: "https://www.googleapis.com/storage/v1/b/netdata-nightlies/o/latest-version.txt", + async: true, + cache: false + }) + .done(function (response) { + $.ajax({ + url: response.mediaLink, + async: true, + cache: false + }) + .done(function (data) { + data = data.replace(/(\r\n|\n|\r| |\t)/gm, ""); + versionLog('Latest nightly version from GCS is ' + data); + callback(data); + }) + .fail(function (xhr, textStatus, errorThrown) { + versionLog('Failed to download the latest nightly version id from GCS!'); + callback(null); + }); + }) + .fail(function (xhr, textStatus, errorThrown) { + versionLog('Failed to download the latest nightly version from GCS!'); + callback(null); + }); +} + + +function checkForUpdateByVersion(force, callback) { + if (options.release_channel === 'stable') { + getGithubLatestVersion(function (sha2) { + callback(options.version, sha2); + }); + } else { + getGCSLatestVersion(function (sha2) { + callback(options.version, sha2); + }); + } + return null; +} + +function notifyForUpdate(force) { + versionLog('<p>checking for updates...</p>'); + + var now = Date.now(); + + if (typeof force === 'undefined' || force !== true) { + var last = loadLocalStorage('last_update_check'); + + if (typeof last === 'string') { + last = parseInt(last); + } else { + last = 0; + } + + if (now - last < 3600000 * 8) { + // no need to check it - too soon + return; + } + } + + checkForUpdateByVersion(force, function (sha1, sha2) { + var save = false; + + if (sha1 === null) { + save = false; + versionLog('<p><big>Failed to get your netdata version!</big></p><p>You can always get the latest netdata from <a href="https://github.com/netdata/netdata" target="_blank">its github page</a>.</p>'); + } else if (sha2 === null) { + save = false; + versionLog('<p><big>Failed to get the latest netdata version.</big></p><p>You can always get the latest netdata from <a href="https://github.com/netdata/netdata" target="_blank">its github page</a>.</p>'); + } else if (versionsMatch(sha1, sha2)) { + save = true; + versionLog('<p><big>You already have the latest netdata!</big></p><p>No update yet?<br/>We probably need some motivation to keep going on!</p><p>If you haven\'t already, <a href="https://github.com/netdata/netdata" target="_blank">give netdata a <b><i class="fas fa-star"></i></b> at its github page</a>.</p>'); + } else { + save = true; + var compare = 'https://docs.netdata.cloud/changelog/'; + versionLog('<p><big><strong>New version of netdata available!</strong></big></p><p>Latest version: <b><code>' + sha2 + '</code></b></p><p><a href="' + compare + '" target="_blank">Click here for the changes log</a> and<br/><a href="https://github.com/netdata/netdata/tree/master/packaging/installer/UPDATE.md" target="_blank">click here for directions on updating</a> your netdata installation.</p><p>We suggest to review the changes log for new features you may be interested, or important bug fixes you may need.<br/>Keeping your netdata updated is generally a good idea.</p>'); + + document.getElementById('update_badge').innerHTML = '!'; + } + + if (save) { + saveLocalStorage('last_update_check', now.toString()); + } + }); +} + +// ---------------------------------------------------------------------------- +// printing dashboards + +function showPageFooter() { + document.getElementById('footer').style.display = 'block'; +} + +function printPreflight() { + var url = document.location.origin.toString() + document.location.pathname.toString() + document.location.search.toString() + urlOptions.genHash() + ';mode=print'; + var width = 990; + var height = screen.height * 90 / 100; + //console.log(url); + //console.log(document.location); + window.open(url, '', 'width=' + width.toString() + ',height=' + height.toString() + ',menubar=no,toolbar=no,personalbar=no,location=no,resizable=no,scrollbars=yes,status=no,chrome=yes,centerscreen=yes,attention=yes,dialog=yes'); + $('#printPreflightModal').modal('hide'); +} + +function printPage() { + var print_is_rendering = true; + + $('#printModal').on('hide.bs.modal', function (e) { + if (print_is_rendering === true) { + e.preventDefault(); + return false; + } + + return true; + }); + + $('#printModal').on('show.bs.modal', function () { + var print_options = { + stop_updates_when_focus_is_lost: false, + update_only_visible: false, + sync_selection: false, + eliminate_zero_dimensions: false, + pan_and_zoom_data_padding: false, + show_help: false, + legend_toolbox: false, + resize_charts: false, + pixels_per_point: 1 + }; + + var x; + for (x in print_options) { + if (print_options.hasOwnProperty(x)) { + NETDATA.options.current[x] = print_options[x]; + } + } + + NETDATA.parseDom(); + showPageFooter(); + + NETDATA.globalSelectionSync.stop(); + NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], urlOptions.after, urlOptions.before); + // NETDATA.onresize(); + + var el = document.getElementById('printModalProgressBar'); + var eltxt = document.getElementById('printModalProgressBarText'); + + function update_chart(idx) { + var state = NETDATA.options.targets[--idx]; + + var pcent = (NETDATA.options.targets.length - idx) * 100 / NETDATA.options.targets.length; + $(el).css('width', pcent + '%').attr('aria-valuenow', pcent); + eltxt.innerText = Math.round(pcent).toString() + '%, ' + state.id; + + setTimeout(function () { + state.updateChart(function () { + NETDATA.options.targets[idx].resizeForPrint(); + + if (idx > 0) { + update_chart(idx); + } else { + print_is_rendering = false; + $('#printModal').modal('hide'); + window.print(); + window.close(); + } + }) + }, 0); + } + + print_is_rendering = true; + update_chart(NETDATA.options.targets.length); + }); + + $('#printModal').modal('show'); +} + +// -------------------------------------------------------------------- + +function jsonStringifyFn(obj) { + return JSON.stringify(obj, function (key, value) { + return (typeof value === 'function') ? value.toString() : value; + }); +} + +function jsonParseFn(str) { + return JSON.parse(str, function (key, value) { + if (typeof value != 'string') { + return value; + } + return (value.substring(0, 8) == 'function') ? eval('(' + value + ')') : value; + }); +} + +// -------------------------------------------------------------------- + +var snapshotOptions = { + bytes_per_chart: 2048, + compressionDefault: 'pako.deflate.base64', + + compressions: { + 'none': { + bytes_per_point_memory: 5.2, + bytes_per_point_disk: 5.6, + + compress: function (s) { + return s; + }, + + compressed_length: function (s) { + return s.length; + }, + + uncompress: function (s) { + return s; + } + }, + + 'pako.deflate.base64': { + bytes_per_point_memory: 1.8, + bytes_per_point_disk: 1.9, + + compress: function (s) { + return btoa(pako.deflate(s, { to: 'string' })); + }, + + compressed_length: function (s) { + return s.length; + }, + + uncompress: function (s) { + return pako.inflate(atob(s), { to: 'string' }); + } + }, + + 'pako.deflate': { + bytes_per_point_memory: 1.4, + bytes_per_point_disk: 3.2, + + compress: function (s) { + return pako.deflate(s, { to: 'string' }); + }, + + compressed_length: function (s) { + return s.length; + }, + + uncompress: function (s) { + return pako.inflate(s, { to: 'string' }); + } + }, + + 'lzstring.utf16': { + bytes_per_point_memory: 1.7, + bytes_per_point_disk: 2.6, + + compress: function (s) { + return LZString.compressToUTF16(s); + }, + + compressed_length: function (s) { + return s.length * 2; + }, + + uncompress: function (s) { + return LZString.decompressFromUTF16(s); + } + }, + + 'lzstring.base64': { + bytes_per_point_memory: 2.1, + bytes_per_point_disk: 2.3, + + compress: function (s) { + return LZString.compressToBase64(s); + }, + + compressed_length: function (s) { + return s.length; + }, + + uncompress: function (s) { + return LZString.decompressFromBase64(s); + } + }, + + 'lzstring.uri': { + bytes_per_point_memory: 2.1, + bytes_per_point_disk: 2.3, + + compress: function (s) { + return LZString.compressToEncodedURIComponent(s); + }, + + compressed_length: function (s) { + return s.length; + }, + + uncompress: function (s) { + return LZString.decompressFromEncodedURIComponent(s); + } + } + } +}; + +// -------------------------------------------------------------------- +// loading snapshots + +function loadSnapshotModalLog(priority, msg) { + document.getElementById('loadSnapshotStatus').className = "alert alert-" + priority; + document.getElementById('loadSnapshotStatus').innerHTML = msg; +} + +var tmpSnapshotData = null; + +function loadSnapshot() { + $('#loadSnapshotImport').addClass('disabled'); + + if (tmpSnapshotData === null) { + loadSnapshotPreflightEmpty(); + loadSnapshotModalLog('danger', 'no data have been loaded'); + return; + } + + loadPako(function () { + loadLzString(function () { + loadSnapshotModalLog('info', 'Please wait, activating snapshot...'); + $('#loadSnapshotModal').modal('hide'); + + netdataShowAlarms = false; + netdataRegistry = false; + netdataServer = tmpSnapshotData.server; + NETDATA.serverDefault = netdataServer; + + document.getElementById('charts_div').innerHTML = ''; + document.getElementById('sidebar').innerHTML = ''; + NETDATA.globalReset(); + + if (typeof tmpSnapshotData.hash !== 'undefined') { + urlOptions.hash = tmpSnapshotData.hash; + } else { + urlOptions.hash = '#'; + } + + if (typeof tmpSnapshotData.info !== 'undefined') { + var info = jsonParseFn(tmpSnapshotData.info); + if (typeof info.menu !== 'undefined') { + netdataDashboard.menu = info.menu; + } + + if (typeof info.submenu !== 'undefined') { + netdataDashboard.submenu = info.submenu; + } + + if (typeof info.context !== 'undefined') { + netdataDashboard.context = info.context; + } + } + + if (typeof tmpSnapshotData.compression !== 'string') { + tmpSnapshotData.compression = 'none'; + } + + if (typeof snapshotOptions.compressions[tmpSnapshotData.compression] === 'undefined') { + alert('unknown compression method: ' + tmpSnapshotData.compression); + tmpSnapshotData.compression = 'none'; + } + + tmpSnapshotData.uncompress = snapshotOptions.compressions[tmpSnapshotData.compression].uncompress; + netdataSnapshotData = tmpSnapshotData; + + urlOptions.after = tmpSnapshotData.after_ms; + urlOptions.before = tmpSnapshotData.before_ms; + + if (typeof tmpSnapshotData.highlight_after_ms !== 'undefined' + && tmpSnapshotData.highlight_after_ms !== null + && tmpSnapshotData.highlight_after_ms > 0 + && typeof tmpSnapshotData.highlight_before_ms !== 'undefined' + && tmpSnapshotData.highlight_before_ms !== null + && tmpSnapshotData.highlight_before_ms > 0 + ) { + urlOptions.highlight_after = tmpSnapshotData.highlight_after_ms; + urlOptions.highlight_before = tmpSnapshotData.highlight_before_ms; + urlOptions.highlight = true; + } else { + urlOptions.highlight_after = 0; + urlOptions.highlight_before = 0; + urlOptions.highlight = false; + } + + netdataCheckXSS = false; // disable the modal - this does not affect XSS checks, since dashboard.js is already loaded + NETDATA.xss.enabled = true; // we should not do any remote requests, but if we do, check them + NETDATA.xss.enabled_for_data = true; // check also snapshot data - that have been excluded from the initial check, due to compression + loadSnapshotPreflightEmpty(); + initializeDynamicDashboard(); + }); + }); +}; + +function loadSnapshotPreflightFile(file) { + var filename = NETDATA.xss.string(file.name); + var fr = new FileReader(); + fr.onload = function (e) { + document.getElementById('loadSnapshotFilename').innerHTML = filename; + var result = null; + try { + result = NETDATA.xss.checkAlways('snapshot', JSON.parse(e.target.result), /^(snapshot\.info|snapshot\.data)$/); + + //console.log(result); + var date_after = new Date(result.after_ms); + var date_before = new Date(result.before_ms); + + if (typeof result.charts_ok === 'undefined') { + result.charts_ok = 'unknown'; + } + + if (typeof result.charts_failed === 'undefined') { + result.charts_failed = 0; + } + + if (typeof result.compression === 'undefined') { + result.compression = 'none'; + } + + if (typeof result.data_size === 'undefined') { + result.data_size = 0; + } + + document.getElementById('loadSnapshotFilename').innerHTML = '<code>' + filename + '</code>'; + document.getElementById('loadSnapshotHostname').innerHTML = '<b>' + result.hostname + '</b>, netdata version: <b>' + result.netdata_version.toString() + '</b>'; + document.getElementById('loadSnapshotURL').innerHTML = result.url; + document.getElementById('loadSnapshotCharts').innerHTML = result.charts.charts_count.toString() + ' charts, ' + result.charts.dimensions_count.toString() + ' dimensions, ' + result.data_points.toString() + ' points per dimension, ' + Math.round(result.duration_ms / result.data_points).toString() + ' ms per point'; + document.getElementById('loadSnapshotInfo').innerHTML = 'version: <b>' + result.snapshot_version.toString() + '</b>, includes <b>' + result.charts_ok.toString() + '</b> unique chart data queries ' + ((result.charts_failed > 0) ? ('<b>' + result.charts_failed.toString() + '</b> failed') : '').toString() + ', compressed with <code>' + result.compression.toString() + '</code>, data size ' + (Math.round(result.data_size * 100 / 1024 / 1024) / 100).toString() + ' MB'; + document.getElementById('loadSnapshotTimeRange').innerHTML = '<b>' + NETDATA.dateTime.localeDateString(date_after) + ' ' + NETDATA.dateTime.localeTimeString(date_after) + '</b> to <b>' + NETDATA.dateTime.localeDateString(date_before) + ' ' + NETDATA.dateTime.localeTimeString(date_before) + '</b>'; + document.getElementById('loadSnapshotComments').innerHTML = ((result.comments) ? result.comments : '').toString(); + loadSnapshotModalLog('success', 'File loaded, click <b>Import</b> to render it!'); + $('#loadSnapshotImport').removeClass('disabled'); + + tmpSnapshotData = result; + } + catch (e) { + console.log(e); + document.getElementById('loadSnapshotStatus').className = "alert alert-danger"; + document.getElementById('loadSnapshotStatus').innerHTML = "Failed to parse this file!"; + $('#loadSnapshotImport').addClass('disabled'); + } + } + + //console.log(file); + fr.readAsText(file); +}; + +function loadSnapshotPreflightEmpty() { + document.getElementById('loadSnapshotFilename').innerHTML = ''; + document.getElementById('loadSnapshotHostname').innerHTML = ''; + document.getElementById('loadSnapshotURL').innerHTML = ''; + document.getElementById('loadSnapshotCharts').innerHTML = ''; + document.getElementById('loadSnapshotInfo').innerHTML = ''; + document.getElementById('loadSnapshotTimeRange').innerHTML = ''; + document.getElementById('loadSnapshotComments').innerHTML = ''; + loadSnapshotModalLog('success', 'Browse for a snapshot file (or drag it and drop it here), then click <b>Import</b> to render it.'); + $('#loadSnapshotImport').addClass('disabled'); +}; + +var loadSnapshotDragAndDropInitialized = false; + +function loadSnapshotDragAndDropSetup() { + if (loadSnapshotDragAndDropInitialized === false) { + loadSnapshotDragAndDropInitialized = true; + $('#loadSnapshotDragAndDrop') + .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) { + e.preventDefault(); + e.stopPropagation(); + }) + .on('drop', function (e) { + if (e.originalEvent.dataTransfer.files.length) { + loadSnapshotPreflightFile(e.originalEvent.dataTransfer.files.item(0)); + } else { + loadSnapshotPreflightEmpty(); + loadSnapshotModalLog('danger', 'No file selected'); + } + }); + } +}; + +function loadSnapshotPreflight() { + var files = document.getElementById('loadSnapshotSelectFiles').files; + if (files.length <= 0) { + loadSnapshotPreflightEmpty(); + loadSnapshotModalLog('danger', 'No file selected'); + return; + } + + loadSnapshotModalLog('info', 'Loading file...'); + + loadSnapshotPreflightFile(files.item(0)); +} + +// -------------------------------------------------------------------- +// saving snapshots + +var saveSnapshotStop = false; + +function saveSnapshotCancel() { + saveSnapshotStop = true; +} + +var saveSnapshotModalInitialized = false; + +function saveSnapshotModalSetup() { + if (saveSnapshotModalInitialized === false) { + saveSnapshotModalInitialized = true; + $('#saveSnapshotModal') + .on('hide.bs.modal', saveSnapshotCancel) + .on('show.bs.modal', saveSnapshotModalInit) + .on('shown.bs.modal', function () { + $('#saveSnapshotResolutionSlider').find(".slider-handle:first").attr("tabindex", 1); + document.getElementById('saveSnapshotComments').focus(); + }); + } +}; + +function saveSnapshotModalLog(priority, msg) { + document.getElementById('saveSnapshotStatus').className = "alert alert-" + priority; + document.getElementById('saveSnapshotStatus').innerHTML = msg; +} + +function saveSnapshotModalShowExpectedSize() { + var points = Math.round(saveSnapshotViewDuration / saveSnapshotSelectedSecondsPerPoint); + var priority = 'info'; + var msg = 'A moderate snapshot.'; + + var sizemb = Math.round( + (options.data.charts_count * snapshotOptions.bytes_per_chart + + options.data.dimensions_count * points * snapshotOptions.compressions[saveSnapshotCompression].bytes_per_point_disk) + * 10 / 1024 / 1024) / 10; + + var memmb = Math.round( + (options.data.charts_count * snapshotOptions.bytes_per_chart + + options.data.dimensions_count * points * snapshotOptions.compressions[saveSnapshotCompression].bytes_per_point_memory) + * 10 / 1024 / 1024) / 10; + + if (sizemb < 10) { + priority = 'success'; + msg = 'A nice small snapshot!'; + } + if (sizemb > 50) { + priority = 'warning'; + msg = 'Will stress your browser...'; + } + if (sizemb > 100) { + priority = 'danger'; + msg = 'Hm... good luck...'; + } + + saveSnapshotModalLog(priority, 'The snapshot will have ' + points.toString() + ' points per dimension. Expected size on disk ' + sizemb + ' MB, at browser memory ' + memmb + ' MB.<br/>' + msg); +} + +var saveSnapshotCompression = snapshotOptions.compressionDefault; + +function saveSnapshotSetCompression(name) { + saveSnapshotCompression = name; + document.getElementById('saveSnapshotCompressionName').innerHTML = saveSnapshotCompression; + saveSnapshotModalShowExpectedSize(); +} + +var saveSnapshotSlider = null; +var saveSnapshotSelectedSecondsPerPoint = 1; +var saveSnapshotViewDuration = 1; + +function saveSnapshotModalInit() { + $('#saveSnapshotModalProgressSection').hide(); + $('#saveSnapshotResolutionRadio').show(); + saveSnapshotModalLog('info', 'Select resolution and click <b>Save</b>'); + $('#saveSnapshotExport').removeClass('disabled'); + + loadBootstrapSlider(function () { + saveSnapshotViewDuration = options.duration; + var start_ms = Math.round(Date.now() - saveSnapshotViewDuration * 1000); + + if (NETDATA.globalPanAndZoom.isActive() === true) { + saveSnapshotViewDuration = Math.round((NETDATA.globalPanAndZoom.force_before_ms - NETDATA.globalPanAndZoom.force_after_ms) / 1000); + start_ms = NETDATA.globalPanAndZoom.force_after_ms; + } + + var start_date = new Date(start_ms); + var yyyymmddhhssmm = start_date.getFullYear() + NETDATA.zeropad(start_date.getMonth() + 1) + NETDATA.zeropad(start_date.getDate()) + '-' + NETDATA.zeropad(start_date.getHours()) + NETDATA.zeropad(start_date.getMinutes()) + NETDATA.zeropad(start_date.getSeconds()); + + document.getElementById('saveSnapshotFilename').value = 'netdata-' + options.hostname.toString() + '-' + yyyymmddhhssmm.toString() + '-' + saveSnapshotViewDuration.toString() + '.snapshot'; + saveSnapshotSetCompression(saveSnapshotCompression); + + var min = options.update_every; + var max = Math.round(saveSnapshotViewDuration / 100); + + if (NETDATA.globalPanAndZoom.isActive() === false) { + max = Math.round(saveSnapshotViewDuration / 50); + } + + var view = Math.round(saveSnapshotViewDuration / Math.round($(document.getElementById('charts_div')).width() / 2)); + + // console.log('view duration: ' + saveSnapshotViewDuration + ', min: ' + min + ', max: ' + max + ', view: ' + view); + + if (max < 10) { + max = 10; + } + if (max < min) { + max = min; + } + if (view < min) { + view = min; + } + if (view > max) { + view = max; + } + + if (saveSnapshotSlider !== null) { + saveSnapshotSlider.destroy(); + } + + saveSnapshotSlider = new Slider('#saveSnapshotResolutionSlider', { + ticks: [min, view, max], + min: min, + max: max, + step: options.update_every, + value: view, + scale: (max > 100) ? 'logarithmic' : 'linear', + tooltip: 'always', + formatter: function (value) { + if (value < 1) { + value = 1; + } + + if (value < options.data.update_every) { + value = options.data.update_every; + } + + saveSnapshotSelectedSecondsPerPoint = value; + saveSnapshotModalShowExpectedSize(); + + var seconds = ' seconds '; + if (value === 1) { + seconds = ' second '; + } + + return value + seconds + 'per point' + ((value === options.data.update_every) ? ', server default' : '').toString(); + } + }); + }); +} + +function saveSnapshot() { + loadPako(function () { + loadLzString(function () { + saveSnapshotStop = false; + $('#saveSnapshotModalProgressSection').show(); + $('#saveSnapshotResolutionRadio').hide(); + $('#saveSnapshotExport').addClass('disabled'); + + var filename = document.getElementById('saveSnapshotFilename').value; + // console.log(filename); + saveSnapshotModalLog('info', 'Generating snapshot as <code>' + filename.toString() + '</code>'); + + var save_options = { + stop_updates_when_focus_is_lost: false, + update_only_visible: false, + sync_selection: false, + eliminate_zero_dimensions: true, + pan_and_zoom_data_padding: false, + show_help: false, + legend_toolbox: false, + resize_charts: false, + pixels_per_point: 1 + }; + var backedup_options = {}; + + var x; + for (x in save_options) { + if (save_options.hasOwnProperty(x)) { + backedup_options[x] = NETDATA.options.current[x]; + NETDATA.options.current[x] = save_options[x]; + } + } + + var el = document.getElementById('saveSnapshotModalProgressBar'); + var eltxt = document.getElementById('saveSnapshotModalProgressBarText'); + + options.data.charts_by_name = null; + + var saveData = { + hostname: options.hostname, + server: NETDATA.serverDefault, + netdata_version: options.data.version, + snapshot_version: 1, + after_ms: Date.now() - options.duration * 1000, + before_ms: Date.now(), + highlight_after_ms: urlOptions.highlight_after, + highlight_before_ms: urlOptions.highlight_before, + duration_ms: options.duration * 1000, + update_every_ms: options.update_every * 1000, + data_points: 0, + url: ((urlOptions.server !== null) ? urlOptions.server : document.location.origin.toString() + document.location.pathname.toString() + document.location.search.toString()).toString(), + comments: document.getElementById('saveSnapshotComments').value.toString(), + hash: urlOptions.hash, + charts: options.data, + info: jsonStringifyFn({ + menu: netdataDashboard.menu, + submenu: netdataDashboard.submenu, + context: netdataDashboard.context + }), + charts_ok: 0, + charts_failed: 0, + compression: saveSnapshotCompression, + data_size: 0, + data: {} + }; + + if (typeof snapshotOptions.compressions[saveData.compression] === 'undefined') { + alert('unknown compression method: ' + saveData.compression); + saveData.compression = 'none'; + } + + var compress = snapshotOptions.compressions[saveData.compression].compress; + var compressed_length = snapshotOptions.compressions[saveData.compression].compressed_length; + + function pack_api1_v1_chart_data(state) { + if (state.library_name === null || state.data === null) { + return; + } + + var data = state.data; + state.data = null; + data.state = null; + var str = JSON.stringify(data); + + if (typeof str === 'string') { + var cstr = compress(str); + saveData.data[state.chartDataUniqueID()] = cstr; + return compressed_length(cstr); + } else { + return 0; + } + } + + var clearPanAndZoom = false; + if (NETDATA.globalPanAndZoom.isActive() === false) { + NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], saveData.after_ms, saveData.before_ms); + clearPanAndZoom = true; + } + + saveData.after_ms = NETDATA.globalPanAndZoom.force_after_ms; + saveData.before_ms = NETDATA.globalPanAndZoom.force_before_ms; + saveData.duration_ms = saveData.before_ms - saveData.after_ms; + saveData.data_points = Math.round((saveData.before_ms - saveData.after_ms) / (saveSnapshotSelectedSecondsPerPoint * 1000)); + saveSnapshotModalLog('info', 'Generating snapshot with ' + saveData.data_points.toString() + ' data points per dimension...'); + + var charts_count = 0; + var charts_ok = 0; + var charts_failed = 0; + + function saveSnapshotRestore() { + $('#saveSnapshotModal').modal('hide'); + + // restore the options + var x; + for (x in backedup_options) { + if (backedup_options.hasOwnProperty(x)) { + NETDATA.options.current[x] = backedup_options[x]; + } + } + + $(el).css('width', '0%').attr('aria-valuenow', 0); + eltxt.innerText = '0%'; + + if (clearPanAndZoom) { + NETDATA.globalPanAndZoom.clearMaster(); + } + + NETDATA.options.force_data_points = 0; + NETDATA.options.fake_chart_rendering = false; + NETDATA.onscroll_updater_enabled = true; + NETDATA.onresize(); + NETDATA.unpause(); + + $('#saveSnapshotExport').removeClass('disabled'); + } + + NETDATA.globalSelectionSync.stop(); + NETDATA.options.force_data_points = saveData.data_points; + NETDATA.options.fake_chart_rendering = true; + NETDATA.onscroll_updater_enabled = false; + NETDATA.abortAllRefreshes(); + + var size = 0; + var info = ' Resolution: <b>' + saveSnapshotSelectedSecondsPerPoint.toString() + ((saveSnapshotSelectedSecondsPerPoint === 1) ? ' second ' : ' seconds ').toString() + 'per point</b>.'; + + function update_chart(idx) { + if (saveSnapshotStop === true) { + saveSnapshotModalLog('info', 'Cancelled!'); + saveSnapshotRestore(); + return; + } + + var state = NETDATA.options.targets[--idx]; + + var pcent = (NETDATA.options.targets.length - idx) * 100 / NETDATA.options.targets.length; + $(el).css('width', pcent + '%').attr('aria-valuenow', pcent); + eltxt.innerText = Math.round(pcent).toString() + '%, ' + state.id; + + setTimeout(function () { + charts_count++; + state.isVisible(true); + state.current.force_after_ms = saveData.after_ms; + state.current.force_before_ms = saveData.before_ms; + + state.updateChart(function (status, reason) { + state.current.force_after_ms = null; + state.current.force_before_ms = null; + + if (status === true) { + charts_ok++; + // state.log('ok'); + size += pack_api1_v1_chart_data(state); + } else { + charts_failed++; + state.log('failed to be updated: ' + reason); + } + + saveSnapshotModalLog((charts_failed) ? 'danger' : 'info', 'Generated snapshot data size <b>' + (Math.round(size * 100 / 1024 / 1024) / 100).toString() + ' MB</b>. ' + ((charts_failed) ? (charts_failed.toString() + ' charts have failed to be downloaded') : '').toString() + info); + + if (idx > 0) { + update_chart(idx); + } else { + saveData.charts_ok = charts_ok; + saveData.charts_failed = charts_failed; + saveData.data_size = size; + // console.log(saveData.compression + ': ' + (size / (options.data.dimensions_count * Math.round(saveSnapshotViewDuration / saveSnapshotSelectedSecondsPerPoint))).toString()); + + // save it + // console.log(saveData); + saveObjectToClient(saveData, filename); + + if (charts_failed > 0) { + alert(charts_failed.toString() + ' failed to be downloaded'); + } + + saveSnapshotRestore(); + saveData = null; + } + }) + }, 0); + } + + update_chart(NETDATA.options.targets.length); + }); + }); +} + +// -------------------------------------------------------------------- +// activate netdata on the page + +function dashboardSettingsSetup() { + var update_options_modal = function () { + // console.log('update_options_modal'); + + var sync_option = function (option) { + var self = $('#' + option); + + if (self.prop('checked') !== NETDATA.getOption(option)) { + // console.log('switching ' + option.toString()); + self.bootstrapToggle(NETDATA.getOption(option) ? 'on' : 'off'); + } + }; + + var theme_sync_option = function (option) { + var self = $('#' + option); + + self.bootstrapToggle(netdataTheme === 'slate' ? 'on' : 'off'); + }; + var units_sync_option = function (option) { + var self = $('#' + option); + + if (self.prop('checked') !== (NETDATA.getOption('units') === 'auto')) { + self.bootstrapToggle(NETDATA.getOption('units') === 'auto' ? 'on' : 'off'); + } + + if (self.prop('checked') === true) { + $('#settingsLocaleTempRow').show(); + $('#settingsLocaleTimeRow').show(); + } else { + $('#settingsLocaleTempRow').hide(); + $('#settingsLocaleTimeRow').hide(); + } + }; + var temp_sync_option = function (option) { + var self = $('#' + option); + + if (self.prop('checked') !== (NETDATA.getOption('temperature') === 'celsius')) { + self.bootstrapToggle(NETDATA.getOption('temperature') === 'celsius' ? 'on' : 'off'); + } + }; + var timezone_sync_option = function (option) { + var self = $('#' + option); + + document.getElementById('browser_timezone').innerText = NETDATA.options.browser_timezone; + document.getElementById('server_timezone').innerText = NETDATA.options.server_timezone; + document.getElementById('current_timezone').innerText = (NETDATA.options.current.timezone === 'default') ? 'unset, using browser default' : NETDATA.options.current.timezone; + + if (self.prop('checked') === NETDATA.dateTime.using_timezone) { + self.bootstrapToggle(NETDATA.dateTime.using_timezone ? 'off' : 'on'); + } + }; + + sync_option('eliminate_zero_dimensions'); + sync_option('destroy_on_hide'); + sync_option('async_on_scroll'); + sync_option('parallel_refresher'); + sync_option('concurrent_refreshes'); + sync_option('sync_selection'); + sync_option('sync_pan_and_zoom'); + sync_option('stop_updates_when_focus_is_lost'); + sync_option('smooth_plot'); + sync_option('pan_and_zoom_data_padding'); + sync_option('show_help'); + sync_option('seconds_as_time'); + theme_sync_option('netdata_theme_control'); + units_sync_option('units_conversion'); + temp_sync_option('units_temp'); + timezone_sync_option('local_timezone'); + + if (NETDATA.getOption('parallel_refresher') === false) { + $('#concurrent_refreshes_row').hide(); + } else { + $('#concurrent_refreshes_row').show(); + } + }; + NETDATA.setOption('setOptionCallback', update_options_modal); + + // handle options changes + $('#eliminate_zero_dimensions').change(function () { + NETDATA.setOption('eliminate_zero_dimensions', $(this).prop('checked')); + }); + $('#destroy_on_hide').change(function () { + NETDATA.setOption('destroy_on_hide', $(this).prop('checked')); + }); + $('#async_on_scroll').change(function () { + NETDATA.setOption('async_on_scroll', $(this).prop('checked')); + }); + $('#parallel_refresher').change(function () { + NETDATA.setOption('parallel_refresher', $(this).prop('checked')); + }); + $('#concurrent_refreshes').change(function () { + NETDATA.setOption('concurrent_refreshes', $(this).prop('checked')); + }); + $('#sync_selection').change(function () { + NETDATA.setOption('sync_selection', $(this).prop('checked')); + }); + $('#sync_pan_and_zoom').change(function () { + NETDATA.setOption('sync_pan_and_zoom', $(this).prop('checked')); + }); + $('#stop_updates_when_focus_is_lost').change(function () { + urlOptions.update_always = !$(this).prop('checked'); + urlOptions.hashUpdate(); + + NETDATA.setOption('stop_updates_when_focus_is_lost', !urlOptions.update_always); + }); + $('#smooth_plot').change(function () { + NETDATA.setOption('smooth_plot', $(this).prop('checked')); + }); + $('#pan_and_zoom_data_padding').change(function () { + NETDATA.setOption('pan_and_zoom_data_padding', $(this).prop('checked')); + }); + $('#seconds_as_time').change(function () { + NETDATA.setOption('seconds_as_time', $(this).prop('checked')); + }); + $('#local_timezone').change(function () { + if ($(this).prop('checked')) { + selected_server_timezone('default', true); + } else { + selected_server_timezone('default', false); + } + }); + + $('#units_conversion').change(function () { + NETDATA.setOption('units', $(this).prop('checked') ? 'auto' : 'original'); + }); + $('#units_temp').change(function () { + NETDATA.setOption('temperature', $(this).prop('checked') ? 'celsius' : 'fahrenheit'); + }); + + $('#show_help').change(function () { + urlOptions.help = $(this).prop('checked'); + urlOptions.hashUpdate(); + + NETDATA.setOption('show_help', urlOptions.help); + netdataReload(); + }); + + // this has to be the last + // it reloads the page + $('#netdata_theme_control').change(function () { + urlOptions.theme = $(this).prop('checked') ? 'slate' : 'white'; + urlOptions.hashUpdate(); + + if (setTheme(urlOptions.theme)) { + netdataReload(); + } + }); +} + +function scrollDashboardTo() { + if (netdataSnapshotData !== null && typeof netdataSnapshotData.hash !== 'undefined') { + //console.log(netdataSnapshotData.hash); + scrollToId(netdataSnapshotData.hash.replace('#', '')); + } else { + // check if we have to jump to a specific section + scrollToId(urlOptions.hash.replace('#', '')); + + if (urlOptions.chart !== null) { + NETDATA.alarms.scrollToChart(urlOptions.chart); + //urlOptions.hash = '#' + NETDATA.name2id('menu_' + charts[c].menu + '_submenu_' + charts[c].submenu); + //urlOptions.hash = '#chart_' + NETDATA.name2id(urlOptions.chart); + //console.log('hash = ' + urlOptions.hash); + } + } +} + +var modalHiddenCallback = null; + +function scrollToChartAfterHidingModal(chart, alarmDate, alarmStatus) { + modalHiddenCallback = function () { + NETDATA.alarms.scrollToChart(chart, alarmDate); + + if (['WARNING', 'CRITICAL'].includes(alarmStatus)) { + const currentChartState = NETDATA.options.targets.find( + (chartState) => chartState.id === chart, + ) + const twoMinutes = 2 * 60 * 1000 + NETDATA.globalPanAndZoom.setMaster( + currentChartState, + alarmDate - twoMinutes, + alarmDate + twoMinutes, + ) + } + }; +} + +// ---------------------------------------------------------------------------- + +function enableTooltipsAndPopovers() { + $('[data-toggle="tooltip"]').tooltip({ + animated: 'fade', + trigger: 'hover', + html: true, + delay: { show: 500, hide: 0 }, + container: 'body' + }); + $('[data-toggle="popover"]').popover(); +} + +// ---------------------------------------------------------------------------- + +var runOnceOnDashboardLastRun = 0; + +function runOnceOnDashboardWithjQuery() { + if (runOnceOnDashboardLastRun !== 0) { + scrollDashboardTo(); + + // restore the scrollspy at the proper position + $(document.body).scrollspy('refresh'); + $(document.body).scrollspy('process'); + + return; + } + + runOnceOnDashboardLastRun = Date.now(); + + // ------------------------------------------------------------------------ + // bootstrap modals + + // prevent bootstrap modals from scrolling the page + // maintains the current scroll position + // https://stackoverflow.com/a/34754029/4525767 + + var scrollPos = 0; + var modal_depth = 0; // how many modals are currently open + var modal_shown = false; // set to true, if a modal is shown + var netdata_paused_on_modal = false; // set to true, if the modal paused netdata + var scrollspyOffset = $(window).height() / 3; // will be updated below - the offset of scrollspy to select an item + + $('.modal') + .on('show.bs.modal', function () { + if (modal_depth === 0) { + scrollPos = window.scrollY; + + $('body').css({ + overflow: 'hidden', + position: 'fixed', + top: -scrollPos + }); + + modal_shown = true; + + if (NETDATA.options.pauseCallback === null) { + NETDATA.pause(function () { + }); + netdata_paused_on_modal = true; + } else { + netdata_paused_on_modal = false; + } + } + + modal_depth++; + //console.log(urlOptions.after); + + }) + .on('hide.bs.modal', function () { + + modal_depth--; + + if (modal_depth <= 0) { + modal_depth = 0; + + $('body') + .css({ + overflow: '', + position: '', + top: '' + }); + + // scroll to the position we had open before the modal + $('html, body') + .animate({ scrollTop: scrollPos }, 0); + + // unpause netdata, if we paused it + if (netdata_paused_on_modal === true) { + NETDATA.unpause(); + netdata_paused_on_modal = false; + } + + // restore the scrollspy at the proper position + $(document.body).scrollspy('process'); + } + //console.log(urlOptions.after); + }) + .on('hidden.bs.modal', function () { + if (modal_depth === 0) { + modal_shown = false; + } + + if (typeof modalHiddenCallback === 'function') { + modalHiddenCallback(); + } + + modalHiddenCallback = null; + //console.log(urlOptions.after); + }); + + // ------------------------------------------------------------------------ + // sidebar / affix + + if (shouldShowSignInBanner()) { + const el = document.getElementById("sign-in-banner"); + if (el) { + el.style.display = "initial"; + el.classList.add(`theme-${netdataTheme}`); + } + } + + $('#sidebar') + .affix({ + offset: { + top: (isdemo()) ? 150 : 0, + bottom: 0 + } + }) + .on('affixed.bs.affix', function () { + // fix scrolling of very long affix lists + // http://stackoverflow.com/questions/21691585/bootstrap-3-1-0-affix-too-long + + $(this).removeAttr('style'); + }) + .on('affix-top.bs.affix', function () { + // fix bootstrap affix click bug + // https://stackoverflow.com/a/37847981/4525767 + + if (modal_shown) { + return false; + } + }) + .on('activate.bs.scrollspy', function (e) { + // change the URL based on the current position of the screen + + if (modal_shown === false) { + var el = $(e.target); + var hash = el.find('a').attr('href'); + if (typeof hash === 'string' && hash.substring(0, 1) === '#' && urlOptions.hash.startsWith(hash + '_submenu_') === false) { + urlOptions.hash = hash; + urlOptions.hashUpdate(); + } + } + }); + + Ps.initialize(document.getElementById('sidebar'), { + wheelSpeed: 0.5, + wheelPropagation: true, + swipePropagation: true, + minScrollbarLength: null, + maxScrollbarLength: null, + useBothWheelAxes: false, + suppressScrollX: true, + suppressScrollY: false, + scrollXMarginOffset: 0, + scrollYMarginOffset: 0, + theme: 'default' + }); + + // ------------------------------------------------------------------------ + // scrollspy + + if (scrollspyOffset > 250) { + scrollspyOffset = 250; + } + if (scrollspyOffset < 75) { + scrollspyOffset = 75; + } + document.body.setAttribute('data-offset', scrollspyOffset); + + // scroll the dashboard, before activating the scrollspy, so that our + // hash will not be updated before we got the chance to scroll to it + scrollDashboardTo(); + + $(document.body).scrollspy({ + target: '#sidebar', + offset: scrollspyOffset // controls the diff of the <hX> element to the top, to select it + }); + + // ------------------------------------------------------------------------ + // my-netdata menu + + Ps.initialize(document.getElementById('my-netdata-dropdown-content'), { + wheelSpeed: 1, + wheelPropagation: false, + swipePropagation: false, + minScrollbarLength: null, + maxScrollbarLength: null, + useBothWheelAxes: false, + suppressScrollX: true, + suppressScrollY: false, + scrollXMarginOffset: 0, + scrollYMarginOffset: 0, + theme: 'default' + }); + + $('#myNetdataDropdownParent') + .on('show.bs.dropdown', function () { + var hash = urlOptions.genHash(); + $('.registry_link').each(function (idx) { + this.setAttribute('href', this.getAttribute("href").replace(/#.*$/, hash)); + }); + + NETDATA.pause(function () { + }); + }) + .on('shown.bs.dropdown', function () { + Ps.update(document.getElementById('my-netdata-dropdown-content')); + myNetdataMenuDidShow(); + }) + .on('hidden.bs.dropdown', function () { + NETDATA.unpause(); + }); + + $('#deleteRegistryModal') + .on('hidden.bs.modal', function () { + deleteRegistryGuid = null; + }); + + // ------------------------------------------------------------------------ + // update modal + + $('#updateModal') + .on('show.bs.modal', function () { + versionLog('checking, please wait...'); + }) + .on('shown.bs.modal', function () { + notifyForUpdate(true); + }); + + // ------------------------------------------------------------------------ + // alarms modal + + $('#alarmsModal') + .on('shown.bs.modal', function () { + alarmsUpdateModal(); + }) + .on('hidden.bs.modal', function () { + document.getElementById('alarms_active').innerHTML = + document.getElementById('alarms_all').innerHTML = + document.getElementById('alarms_log').innerHTML = + 'loading...'; + }); + + // ------------------------------------------------------------------------ + + dashboardSettingsSetup(); + loadSnapshotDragAndDropSetup(); + saveSnapshotModalSetup(); + showPageFooter(); + + // ------------------------------------------------------------------------ + // https://github.com/viralpatel/jquery.shorten/blob/master/src/jquery.shorten.js + + $.fn.shorten = function (settings) { + "use strict"; + + var config = { + showChars: 750, + minHideChars: 10, + ellipsesText: "...", + moreText: '<i class="fas fa-expand"></i> show more information', + lessText: '<i class="fas fa-compress"></i> show less information', + onLess: function () { + NETDATA.onscroll(); + }, + onMore: function () { + NETDATA.onscroll(); + }, + errMsg: null, + force: false + }; + + if (settings) { + $.extend(config, settings); + } + + if ($(this).data('jquery.shorten') && !config.force) { + return false; + } + $(this).data('jquery.shorten', true); + + $(document).off("click", '.morelink'); + + $(document).on({ + click: function () { + + var $this = $(this); + if ($this.hasClass('less')) { + $this.removeClass('less'); + $this.html(config.moreText); + $this.parent().prev().animate({ 'height': '0' + '%' }, 0, function () { + $this.parent().prev().prev().show(); + }).hide(0, function () { + config.onLess(); + }); + } else { + $this.addClass('less'); + $this.html(config.lessText); + $this.parent().prev().animate({ 'height': '100' + '%' }, 0, function () { + $this.parent().prev().prev().hide(); + }).show(0, function () { + config.onMore(); + }); + } + return false; + } + }, '.morelink'); + + return this.each(function () { + var $this = $(this); + + var content = $this.html(); + var contentlen = $this.text().length; + if (contentlen > config.showChars + config.minHideChars) { + var c = content.substr(0, config.showChars); + if (c.indexOf('<') >= 0) // If there's HTML don't want to cut it + { + var inTag = false; // I'm in a tag? + var bag = ''; // Put the characters to be shown here + var countChars = 0; // Current bag size + var openTags = []; // Stack for opened tags, so I can close them later + var tagName = null; + + for (var i = 0, r = 0; r <= config.showChars; i++) { + if (content[i] === '<' && !inTag) { + inTag = true; + + // This could be "tag" or "/tag" + tagName = content.substring(i + 1, content.indexOf('>', i)); + + // If its a closing tag + if (tagName[0] === '/') { + + if (tagName !== ('/' + openTags[0])) { + config.errMsg = 'ERROR en HTML: the top of the stack should be the tag that closes'; + } else { + openTags.shift(); // Pops the last tag from the open tag stack (the tag is closed in the retult HTML!) + } + + } else { + // There are some nasty tags that don't have a close tag like <br/> + if (tagName.toLowerCase() !== 'br') { + openTags.unshift(tagName); // Add to start the name of the tag that opens + } + } + } + + if (inTag && content[i] === '>') { + inTag = false; + } + + if (inTag) { + bag += content.charAt(i); + } else { + // Add tag name chars to the result + r++; + if (countChars <= config.showChars) { + bag += content.charAt(i); // Fix to ie 7 not allowing you to reference string characters using the [] + countChars++; + } else { + // Now I have the characters needed + if (openTags.length > 0) { + // I have unclosed tags + + //console.log('They were open tags'); + //console.log(openTags); + for (var j = 0; j < openTags.length; j++) { + //console.log('Cierro tag ' + openTags[j]); + bag += '</' + openTags[j] + '>'; // Close all tags that were opened + + // You could shift the tag from the stack to check if you end with an empty stack, that means you have closed all open tags + } + break; + } + } + } + } + c = $('<div/>').html(bag + '<span class="ellip">' + config.ellipsesText + '</span>').html(); + } else { + c += config.ellipsesText; + } + + var html = '<div class="shortcontent">' + c + + '</div><div class="allcontent">' + content + + '</div><span><a href="javascript://nop/" class="morelink">' + config.moreText + '</a></span>'; + + $this.html(html); + $this.find(".allcontent").hide(); // Hide all text + $('.shortcontent p:last', $this).css('margin-bottom', 0); //Remove bottom margin on last paragraph as it's likely shortened + } + }); + }; +} + +function finalizePage() { + // resize all charts - without starting the background thread + // this has to be done while NETDATA is paused + // if we ommit this, the affix menu will be wrong, since all + // the Dom elements are initially zero-sized + NETDATA.parseDom(); + + // ------------------------------------------------------------------------ + + NETDATA.globalPanAndZoom.callback = null; + NETDATA.globalChartUnderlay.callback = null; + + if (urlOptions.pan_and_zoom === true && NETDATA.options.targets.length > 0) { + NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], urlOptions.after, urlOptions.before); + } + + // callback for us to track PanAndZoom operations + NETDATA.globalPanAndZoom.callback = urlOptions.netdataPanAndZoomCallback; + NETDATA.globalChartUnderlay.callback = urlOptions.netdataHighlightCallback; + + // ------------------------------------------------------------------------ + + // let it run (update the charts) + NETDATA.unpause(); + + runOnceOnDashboardWithjQuery(); + $(".shorten").shorten(); + enableTooltipsAndPopovers(); + + if (isdemo()) { + // do not to give errors on netdata demo servers for 60 seconds + NETDATA.options.current.retries_on_data_failures = 60; + + // google analytics when this is used for the home page of the demo sites + // this does not run on user's installations + setTimeout(function () { + (function (i, s, o, g, r, a, m) { + i['GoogleAnalyticsObject'] = r; + i[r] = i[r] || function () { + (i[r].q = i[r].q || []).push(arguments) + }, i[r].l = 1 * new Date(); + a = s.createElement(o), + m = s.getElementsByTagName(o)[0]; + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m) + })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); + + ga('create', 'UA-64295674-3', 'auto'); + ga('send', 'pageview', '/demosite/' + window.location.host); + }, 2000); + } else { + notifyForUpdate(); + } + + if (urlOptions.show_alarms === true) { + setTimeout(function () { + $('#alarmsModal').modal('show'); + }, 1000); + } + + NETDATA.onresizeCallback = function () { + Ps.update(document.getElementById('sidebar')); + Ps.update(document.getElementById('my-netdata-dropdown-content')); + }; + NETDATA.onresizeCallback(); + + if (netdataSnapshotData !== null) { + NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], netdataSnapshotData.after_ms, netdataSnapshotData.before_ms); + } + + //if (urlOptions.nowelcome !== true) { + // setTimeout(function () { + // $('#welcomeModal').modal(); + // }, 2000); + //} + + // var netdataEnded = performance.now(); + // console.log('start up time: ' + (netdataEnded - netdataStarted).toString() + ' ms'); +} + +function resetDashboardOptions() { + var help = NETDATA.options.current.show_help; + + NETDATA.resetOptions(); + if (setTheme('slate')) { + netdataReload(); + } + + if (help !== NETDATA.options.current.show_help) { + netdataReload(); + } +} + +// callback to add the dashboard info to the +// parallel javascript downloader in netdata +var netdataPrepCallback = function () { + NETDATA.requiredCSS.push({ + url: NETDATA.serverStatic + 'css/bootstrap-toggle-2.2.2.min.css', + isAlreadyLoaded: function () { + return false; + } + }); + + NETDATA.requiredJs.push({ + url: NETDATA.serverStatic + 'lib/bootstrap-toggle-2.2.2.min.js', + isAlreadyLoaded: function () { + return false; + } + }); + + NETDATA.requiredJs.push({ + url: NETDATA.serverStatic + 'dashboard_info.js?v20181019-1', + async: false, + isAlreadyLoaded: function () { + return false; + } + }); + + if (isdemo()) { + document.getElementById('masthead').style.display = 'block'; + } else { + if (urlOptions.update_always === true) { + NETDATA.setOption('stop_updates_when_focus_is_lost', !urlOptions.update_always); + } + } +}; + +var selected_server_timezone = function (timezone, status) { + //console.log('called with timezone: ' + timezone + ", status: " + ((typeof status === 'undefined')?'undefined':status).toString()); + + // clear the error + document.getElementById('timezone_error_message').innerHTML = ''; + + if (typeof status === 'undefined') { + // the user selected a timezone from the menu + + NETDATA.setOption('user_set_server_timezone', timezone); + + if (NETDATA.dateTime.init(timezone) === false) { + NETDATA.dateTime.init(); + + if (!$('#local_timezone').prop('checked')) { + $('#local_timezone').bootstrapToggle('on'); + } + + document.getElementById('timezone_error_message').innerHTML = 'Ooops! That timezone was not accepted by your browser. Please open a github issue to help us fix it.'; + NETDATA.setOption('user_set_server_timezone', NETDATA.options.server_timezone); + } else { + if ($('#local_timezone').prop('checked')) { + $('#local_timezone').bootstrapToggle('off'); + } + } + } else if (status === true) { + // the user wants the browser default timezone to be activated + + NETDATA.dateTime.init(); + } else { + // the user wants the server default timezone to be activated + //console.log('found ' + NETDATA.options.current.user_set_server_timezone); + + if (NETDATA.options.current.user_set_server_timezone === 'default') { + NETDATA.options.current.user_set_server_timezone = NETDATA.options.server_timezone; + } + + timezone = NETDATA.options.current.user_set_server_timezone; + + if (NETDATA.dateTime.init(timezone) === false) { + NETDATA.dateTime.init(); + + if (!$('#local_timezone').prop('checked')) { + $('#local_timezone').bootstrapToggle('on'); + } + + document.getElementById('timezone_error_message').innerHTML = 'Sorry. The timezone "' + timezone.toString() + '" is not accepted by your browser. Please select one from the list.'; + NETDATA.setOption('user_set_server_timezone', NETDATA.options.server_timezone); + } + } + + document.getElementById('current_timezone').innerText = (NETDATA.options.current.timezone === 'default') ? 'unset, using browser default' : NETDATA.options.current.timezone; + return false; +}; + +// our entry point +// var netdataStarted = performance.now(); + +var netdataCallback = initializeDynamicDashboard; + +// ================================================================================================= +// netdata.cloud + +let registryAgents = []; + +let cloudAgents = []; + +let myNetdataMenuFilterValue = ""; + +let cloudAccountID = null; + +let cloudAccountName = null; + +let cloudToken = null; + +/// Enforces a maximum string length while retaining the prefix and the postfix of +/// the string. +function truncateString(str, maxLength) { + if (str.length <= maxLength) { + return str; + } + + const spanLength = Math.floor((maxLength - 3) / 2); + return `${str.substring(0, spanLength)}...${str.substring(str.length - spanLength)}`; +} + +// ------------------------------------------------------------------------------------------------- +// netdata.cloud API Client +// ------------------------------------------------------------------------------------------------- + +function isValidAgent(a) { + return a.urls != null && a.urls.length > 0; +} + +// https://github.com/netdata/hub/issues/146 +function getCloudAccountAgents() { + if (!isSignedIn()) { + return []; + } + + return fetch( + `${NETDATA.registry.cloudBaseURL}/api/v1/accounts/${cloudAccountID}/agents`, + { + method: "GET", + mode: "cors", + headers: { + "Authorization": `Bearer ${cloudToken}` + } + } + ).then((response) => { + if (!response.ok) { + throw Error("Cannot fetch known accounts"); + } + return response.json(); + }).then((payload) => { + const agents = payload.result ? payload.result.agents : null; + + if (!agents) { + return []; + } + + return agents.filter((a) => isValidAgent(a)).map((a) => { + return { + "guid": a.id, + "name": a.name, + "url": a.urls[0], + "alternate_urls": a.urls + } + }) + }).catch(function (error) { + console.log(error); + return null; + }); +} + +/** Updates the lastAccessTime and accessCount properties of the agent for the account. */ +function touchAgent() { + if (!isSignedIn()) { + return []; + } + + const touchUrl = `${NETDATA.registry.cloudBaseURL}/api/v1/agents/${NETDATA.registry.machine_guid}/touch?account_id=${cloudAccountID}`; + return fetch( + touchUrl, + { + method: "post", + body: "", + mode: "cors", + headers: { + "Authorization": `Bearer ${cloudToken}` + } + } + ).then((response) => { + if (!response.ok) { + throw Error("Cannot touch agent" + JSON.stringify(response)); + } + return response.json(); + }).then((payload) => { + + }).catch(function (error) { + console.log(error); + return null; + }); +} + +// https://github.com/netdata/hub/issues/128 +function postCloudAccountAgents(agentsToSync) { + if (!isSignedIn()) { + return []; + } + + const maskedURL = NETDATA.registry.MASKED_DATA; + + const agents = agentsToSync.map((a) => { + const urls = a.alternate_urls.filter((url) => url != maskedURL); + + return { + "id": a.guid, + "name": a.name, + "urls": urls + } + }).filter((a) => isValidAgent(a)) + + const payload = { + "accountID": cloudAccountID, + "agents": agents, + "merge": false, + }; + + return fetch( + `${NETDATA.registry.cloudBaseURL}/api/v1/accounts/${cloudAccountID}/agents`, + { + method: "POST", + mode: "cors", + headers: { + "Content-Type": "application/json; charset=utf-8", + "Authorization": `Bearer ${cloudToken}` + }, + body: JSON.stringify(payload) + } + ).then((response) => { + return response.json(); + }).then((payload) => { + const agents = payload.result ? payload.result.agents : null; + + if (!agents) { + return []; + } + + return agents.filter((a) => isValidAgent(a)).map((a) => { + return { + "guid": a.id, + "name": a.name, + "url": a.urls[0], + "alternate_urls": a.urls + } + }) + }); +} + +function deleteCloudAgentURL(agentID, url) { + if (!isSignedIn()) { + return []; + } + + return fetch( + `${NETDATA.registry.cloudBaseURL}/api/v1/accounts/${cloudAccountID}/agents/${agentID}/url?value=${encodeURIComponent(url)}`, + { + method: "DELETE", + mode: "cors", + headers: { + "Content-Type": "application/json; charset=utf-8", + "Authorization": `Bearer ${cloudToken}` + }, + } + ).then((response) => { + return response.json(); + }).then((payload) => { + const count = payload.result ? payload.result.count : 0; + return count; + }); +} + +// ------------------------------------------------------------------------------------------------- + +function signInDidClick(e) { + e.preventDefault(); + e.stopPropagation(); + + if (!NETDATA.registry.isUsingGlobalRegistry()) { + // If user is using a private registry, request his consent for + // synchronizing with cloud. + showSignInModal(); + return; + } + + signIn(); +} + +function shouldShowSignInBanner() { + return false; +} + +function closeSignInBanner() { + localStorage.setItem("signInBannerClosed", "true"); + const el = document.getElementById("sign-in-banner"); + if (el) { + el.style.display = "none"; + } +} + +function closeSignInBannerDidClick(e) { + closeSignInBanner(); +} + +function signOutDidClick(e) { + e.preventDefault(); + e.stopPropagation(); + signOut(); +} + +// ------------------------------------------------------------------------------------------------- + +function updateMyNetdataAfterFilterChange() { + const machinesEl = document.getElementById("my-netdata-menu-machines") + machinesEl.innerHTML = renderMachines(cloudAgents); + + if (options.hosts.length > 1) { + const streamedEl = document.getElementById("my-netdata-menu-streamed") + streamedEl.innerHTML = renderStreamedHosts(options); + } +} + +function myNetdataMenuDidShow() { + const filterEl = document.getElementById("my-netdata-menu-filter-input"); + if (filterEl) { + filterEl.focus(); + } +} + +function myNetdataFilterDidChange(e) { + const inputEl = e.target; + setTimeout(() => { + myNetdataMenuFilterValue = inputEl.value; + updateMyNetdataAfterFilterChange(); + }, 1); +} + +function myNetdataFilterClearDidClick(e) { + e.preventDefault(); + e.stopPropagation(); + + const inputEl = document.getElementById("my-netdata-menu-filter-input"); + inputEl.value = ""; + myNetdataMenuFilterValue = ""; + + updateMyNetdataAfterFilterChange(); + + inputEl.focus(); +} + +// ------------------------------------------------------------------------------------------------- + +function clearCloudVariables() { + cloudAccountID = null; + cloudAccountName = null; + cloudToken = null; +} + +function clearCloudLocalStorageItems() { + localStorage.removeItem("cloud.baseURL"); + localStorage.removeItem("cloud.agentID"); + localStorage.removeItem("cloud.sync"); +} + +function signIn() { + const url = `${NETDATA.registry.cloudBaseURL}/account/sign-in-agent?id=${NETDATA.registry.machine_guid}&name=${encodeURIComponent(NETDATA.registry.hostname)}&origin=${encodeURIComponent(window.location.origin + "/")}`; + window.open(url); +} + +function signOut() { + cloudSSOSignOut(); +} + +function handleMessage(e) { + switch (e.data.type) { + case "sign-in": + handleSignInMessage(e); + break; + + case "sign-out": + handleSignOutMessage(e); + break; + + default: + return; + } +} + +function handleSignInMessage(e) { + closeSignInBanner(); + localStorage.setItem("cloud.baseURL", NETDATA.registry.cloudBaseURL); + + cloudAccountID = e.data.accountID; + cloudAccountName = e.data.accountName; + cloudToken = e.data.token; + + netdataRegistryCallback(registryAgents); + if (e.data.redirectURI && !window.location.href.includes(e.data.redirectURI)) { + // lgtm false-positive - redirectURI does not come from user input, but from iframe callback + window.location.replace(e.data.redirectURI); // lgtm[js/client-side-unvalidated-url-redirection] + } +} + +function handleSignOutMessage(e) { + clearCloudVariables(); + renderMyNetdataMenu(registryAgents); +} + +function isSignedIn() { + return cloudToken != null && cloudAccountID != null; +} + +function sortedArraysEqual(a, b) { + if (a.length != b.length) return false; + + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + + return true; +} + +// If merging is needed returns the merged agents set, otherwise returns null. +function mergeAgents(cloud, local) { + let dirty = false; + + const union = new Map(); + + for (const cagent of cloud) { + union.set(cagent.guid, cagent); + } + + for (const lagent of local) { + const cagent = union.get(lagent.guid); + if (cagent) { + for (const u of lagent.alternate_urls) { + if (u === NETDATA.registry.MASKED_DATA) { // TODO: temp until registry is updated. + continue; + } + + if (!cagent.alternate_urls.includes(u)) { + dirty = true; + cagent.alternate_urls.push(u); + } + } + } else { + dirty = true; + union.set(lagent.guid, lagent); + } + } + + if (dirty) { + return Array.from(union.values()); + } + + return null; +} + +function showSignInModal() { + document.getElementById("sim-registry").innerHTML = NETDATA.registry.server; + $("#signInModal").modal("show"); +} + +function explicitlySignIn() { + $("#signInModal").modal("hide"); + signIn(); +} + +function showSyncModal() { + document.getElementById("sync-registry-modal-registry").innerHTML = NETDATA.registry.server; + $("#syncRegistryModal").modal("show"); +} + +function explicitlySyncAgents() { + $("#syncRegistryModal").modal("hide"); + + const json = localStorage.getItem("cloud.sync"); + const sync = json ? JSON.parse(json) : {}; + delete sync[cloudAccountID]; + localStorage.setItem("cloud.sync", JSON.stringify(sync)); + + NETDATA.registry.init(); +} + +function syncAgents(callback) { + const json = localStorage.getItem("cloud.sync"); + const sync = json ? JSON.parse(json) : {}; + + const currentAgent = { + guid: NETDATA.registry.machine_guid, + name: NETDATA.registry.hostname, + url: NETDATA.serverDefault, + alternate_urls: [NETDATA.serverDefault], + } + + const localAgents = sync[cloudAccountID] + ? [currentAgent] + : registryAgents.concat([currentAgent]); + + console.log("Checking if sync is needed.", localAgents); + + const agentsToSync = mergeAgents(cloudAgents, localAgents); + + if ((!sync[cloudAccountID]) || agentsToSync) { + sync[cloudAccountID] = new Date().getTime(); + localStorage.setItem("cloud.sync", JSON.stringify(sync)); + } + + if (agentsToSync) { + console.log("Synchronizing with netdata.cloud."); + + postCloudAccountAgents(agentsToSync).then((agents) => { + // TODO: clear syncTime on error! + cloudAgents = agents; + callback(cloudAgents); + }); + + return + } + + callback(cloudAgents); +} + +let isCloudSSOInitialized = false; + +function cloudSSOInit() { + const iframeEl = document.getElementById("ssoifrm"); + const url = `${NETDATA.registry.cloudBaseURL}/account/sso-agent?id=${NETDATA.registry.machine_guid}`; + iframeEl.src = url; + isCloudSSOInitialized = true; +} + +function cloudSSOSignOut() { + const iframe = document.getElementById("ssoifrm"); + const url = `${NETDATA.registry.cloudBaseURL}/account/sign-out-agent`; + iframe.src = url; +} + +function initCloud() { + if (!NETDATA.registry.isCloudEnabled) { + clearCloudVariables(); + clearCloudLocalStorageItems(); + return; + } + + if (NETDATA.registry.cloudBaseURL != localStorage.getItem("cloud.baseURL")) { + clearCloudVariables(); + clearCloudLocalStorageItems(); + if (NETDATA.registry.cloudBaseURL) { + localStorage.setItem("cloud.baseURL", NETDATA.registry.cloudBaseURL); + } + } + + if (!isCloudSSOInitialized) { + cloudSSOInit(); + } + + touchAgent(); +} + +// This callback is called after NETDATA.registry is initialized. +function netdataRegistryCallback(machinesArray) { + localStorage.setItem("cloud.agentID", NETDATA.registry.machine_guid); + + initCloud(); + + registryAgents = machinesArray; + + if (isSignedIn()) { + // We call getCloudAccountAgents() here because it requires that + // NETDATA.registry is initialized. + clearMyNetdataMenu(); + getCloudAccountAgents().then((agents) => { + if (!agents) { + errorMyNetdataMenu(); + return; + } + cloudAgents = agents; + syncAgents((agents) => { + const agentsMap = {} + for (const agent of agents) { + agentsMap[agent.guid] = agent; + } + + NETDATA.registry.machines = agentsMap; + NETDATA.registry.machines_array = agents; + + renderMyNetdataMenu(agents); + }); + }); + } else { + renderMyNetdataMenu(machinesArray) + } +}; + +// If we know the cloudBaseURL and agentID from local storage render (eagerly) +// the account ui before receiving the definitive response from the web server. +// This improves the perceived performance. +function tryFastInitCloud() { + const baseURL = localStorage.getItem("cloud.baseURL"); + const agentID = localStorage.getItem("cloud.agentID"); + + if (baseURL && agentID) { + NETDATA.registry.cloudBaseURL = baseURL; + NETDATA.registry.machine_guid = agentID; + NETDATA.registry.isCloudEnabled = true; + + initCloud(); + } +} + +function initializeApp() { + window.addEventListener("message", handleMessage, false); + + // tryFastInitCloud(); +} + +if (document.readyState === "complete") { + initializeApp(); +} else { + document.addEventListener("readystatechange", () => { + if (document.readyState === "complete") { + initializeApp(); + } + }); +} |