diff options
Diffstat (limited to 'web/gui/main.js')
-rw-r--r-- | web/gui/main.js | 955 |
1 files changed, 788 insertions, 167 deletions
diff --git a/web/gui/main.js b/web/gui/main.js index a04f406b..b6478f6c 100644 --- a/web/gui/main.js +++ b/web/gui/main.js @@ -1,5 +1,8 @@ // Main JavaScript file for the Netdata GUI. +// Codacy declarations +/* global NETDATA */ + // netdata snapshot data var netdataSnapshotData = null; @@ -453,7 +456,7 @@ function saveObjectToClient(data, filename) { saveTextToClient(JSON.stringify(data), filename); } -// -------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // registry call back to render my-netdata menu function toggleExpandIcon(svgEl) { @@ -476,21 +479,8 @@ function toggleAgentItem(e, guid) { } } -// TODO: consider renaming to `truncateString` - -/// Enforces a maximum string length while retaining the prefix and the postfix of -/// the string. -function clipString(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)}`; -} - -// When you stream metrics from netdata to netdata, the recieving netdata now -// has multiple host databases. It's own, and multiple mirrored. Mirrored databases +// 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/> function renderStreamedHosts(options) { let html = `<div class="info-item">Databases streamed to this agent</div>`; @@ -512,12 +502,22 @@ function renderStreamedHosts(options) { return naturalSortCompare(a.hostname, b.hostname); }); - for (const s of sorted) { + 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 = `base${'/'}`; + url = `${base}/`; icon = 'home'; } else { url = `${base}/host/${hostname}/`; @@ -528,18 +528,33 @@ function renderStreamedHosts(options) { `<div class="agent-item"> <a class="registry_link" href="${url}#" onClick="return gotoHostedModalHandler('${url}');"> <i class="fas fa-${icon}" style="color: #999;"></i> - </a> - <a class="registry_link" href="${url}#" onClick="return gotoHostedModalHandler('${url}');">${hostname}</a> + </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 netdata agents</div>`; + // let html = isSignedIn() + // ? `<div class="info-item">My nodes</div>` + // : `<div class="info-item">My nodes</div>`; + + let html = `<div class="info-item">My nodes</div>`; if (machinesArray === null) { let ret = loadLocalStorage("registryCallback"); @@ -550,6 +565,9 @@ function renderMachines(machinesArray) { } let found = false; + let displayedAgents = false; + + const maskedURL = NETDATA.registry.MASKED_DATA; if (machinesArray) { saveLocalStorage("registryCallback", JSON.stringify(machinesArray)); @@ -558,21 +576,34 @@ function renderMachines(machinesArray) { return naturalSortCompare(a.name, b.name); }); - for (const machine of machines) { + 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) => str + ( - `<div class="agent-item agent-item--alternate"> - <div></div> - <a href="${url}" title="${url}">${clipString(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>` - ), + ${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>` @@ -581,8 +612,8 @@ function renderMachines(machinesArray) { html += ( `<div class="agent-item agent-${machine.guid}"> <i class="fas fa-chart-bar" color: #fff"></i> - <span> - <a class="registry_link" href="${machine.url}#" onClick="return gotoServerModalHandler('${machine.guid}');">${machine.name}</a> + <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> @@ -591,25 +622,34 @@ function renderMachines(machinesArray) { ${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 netdata server list is empty</a> + <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> + <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 agents</div>`; + html += `<div class="info-item">Demo netdata nodes</div>`; const demoServers = [ {url: "//london.netdata.rocks/default.html", title: "UK - London (DigitalOcean.com)"}, @@ -623,10 +663,10 @@ function renderMachines(machinesArray) { ] - for (const server of demoServers) { + for (var server of demoServers) { html += ( `<div class="agent-item"> - <i class="fas fa-chart-bar" color: #fff"></i> + <i class="fas fa-chart-bar" style="color: #fff"></i> <a href="${server.url}">${server.title}</a> <div></div> </div> @@ -638,36 +678,111 @@ function renderMachines(machinesArray) { return html; } -// Populates the my-netdata menu. -function netdataRegistryCallback(machinesArray) { +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="white-space: nowrap"> + <i class="fas fa-exclamation-triangle" style="color: red"></i> + Cannot load known netdata agents from netdata.cloud! + <div></div> + </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 renderMyNetdataMenu(machinesArray) { + const el = document.getElementById('my-netdata-dropdown-content'); + el.classList.add(`theme-${netdataTheme}`); + + if (!isSignedIn()) { + if (!NETDATA.registry.isRegistryEnabled()) { + restrictMyNetdataMenu(); + return; + } + } + + 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()) { + 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 />` + ); + } + if (options.hosts.length > 1) { - html += renderStreamedHosts(options) + `<hr />`; + html += `<div id="my-netdata-menu-streamed">${renderStreamedHosts(options)}</div><hr />`; } - html += renderMachines(machinesArray); - - html += ( - `<hr /> - <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>` - ) + html += `<div id="my-netdata-menu-machines">${renderMachines(machinesArray)}</div>`; + + if (!isSignedIn()) { + html += ( + `<hr /> + <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 += ( + `<hr /> + <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>` + ) + } - const el = document.getElementById('my-netdata-dropdown-content') - el.classList.add(`theme-${netdataTheme}`); el.innerHTML = html; gotoServerInit(); -}; +} function isdemo() { if (this_is_demo !== null) { @@ -762,6 +877,7 @@ function gotoServerValidateUrl(id, guid, url) { } else { document.getElementById('gotoServerResponse').innerHTML += 'found it! It is at:<br/><small>' + escapeUserInputHTML(url) + '</small>'; document.location = verifyURL(finalURL); + $('#gotoServerModal').modal('hide'); } } } else { @@ -800,24 +916,29 @@ function gotoServerModalHandler(guid) { gotoServerValidateUrl(count++, guid, url); } - 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); + 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); + }); + } + }, 2000); + } + return false; } @@ -858,30 +979,55 @@ function notifyForSwitchRegistry() { } } +var deleteRegistryGuid = null; var deleteRegistryUrl = null; function deleteRegistryModalHandler(guid, name, url) { - void (guid); + // 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) { - NETDATA.registry.delete(deleteRegistryUrl, function (result) { - if (result !== null) { - deleteRegistryUrl = null; - $('#deleteRegistryModal').modal('hide'); - NETDATA.registry.init(); - } else { - document.getElementById('deleteRegistryResponse').innerHTML = "<b>Sorry! this command was rejected by the registry server.</b>"; - } - }); + 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>"; + } + }); + } } } @@ -1165,6 +1311,7 @@ function enrichChartData(chart) { case 'ap': case 'net': case 'disk': + case 'powersupply': case 'statsd': chart.menu = tmp; break; @@ -1610,9 +1757,9 @@ function renderPage(menus, data) { html += mhead + shtml + '</div></div><hr role="separator"/>'; } - sidebar += '<li class="" style="padding-top:15px;"><a href="https://github.com/netdata/netdata/blob/master/doc/Add-more-charts-to-netdata.md#add-more-charts-to-netdata" target="_blank"><i class="fas fa-plus"></i> add more charts</a></li>'; + sidebar += '<li class="" style="padding-top:15px;"><a href="https://github.com/netdata/netdata/blob/master/docs/Add-more-charts-to-netdata.md#add-more-charts-to-netdata" target="_blank"><i class="fas fa-plus"></i> add more charts</a></li>'; sidebar += '<li class=""><a href="https://github.com/netdata/netdata/tree/master/health#Health-monitoring" target="_blank"><i class="fas fa-plus"></i> add more alarms</a></li>'; - sidebar += '<li class="" style="margin:20px;color:#666;"><small>netdata on <b>' + data.hostname.toString() + '</b>, collects every ' + ((data.update_every === 1) ? 'second' : data.update_every.toString() + ' seconds') + ' <b>' + data.dimensions_count.toLocaleString() + '</b> metrics, presented as <b>' + data.charts_count.toLocaleString() + '</b> charts and monitored by <b>' + data.alarms_count.toLocaleString() + '</b> alarms, using ' + Math.round(data.rrd_memory_bytes / 1024 / 1024).toLocaleString() + ' MB of memory for ' + NETDATA.seconds4human(data.update_every * data.history, {space: ' '}) + ' of real-time history.<br/> <br/><b>netdata</b><br/>v' + data.version.toString() + '</small></li>'; + sidebar += '<li class="" style="margin:20px;color:#666;"><small>netdata on <b>' + data.hostname.toString() + '</b>, collects every ' + ((data.update_every === 1) ? 'second' : data.update_every.toString() + ' seconds') + ' <b>' + data.dimensions_count.toLocaleString() + '</b> metrics, presented as <b>' + data.charts_count.toLocaleString() + '</b> charts and monitored by <b>' + data.alarms_count.toLocaleString() + '</b> alarms, using ' + Math.round(data.rrd_memory_bytes / 1024 / 1024).toLocaleString() + ' MB of memory for ' + NETDATA.seconds4human(data.update_every * data.history, {space: ' '}) + ' of real-time history.<br/> <br/><b>netdata</b><br/>' + data.version.toString() + '</small></li>'; sidebar += '</ul>'; div.innerHTML = html; document.getElementById('sidebar').innerHTML = sidebar; @@ -1696,7 +1843,7 @@ function renderChartsAndMenu(data) { // propagate the descriptive subname given to QoS // to all the other submenus with the same name - for (m in menus) { + for (var m in menus) { if (!menus.hasOwnProperty(m)) { continue; } @@ -2560,7 +2707,7 @@ function initializeDynamicDashboardWithData(data) { } // update the dashboard hostname - document.getElementById('hostname').innerHTML = options.hostname + ((netdataSnapshotData !== null) ? ' (snap)' : '').toString(); + document.getElementById('hostname').innerHTML = options.hostname + ((netdataSnapshotData !== null) ? ' (snap)' : '').toString() + ' <strong class="caret">'; document.getElementById('hostname').href = NETDATA.serverDefault; document.getElementById('netdataVersion').innerHTML = options.version; @@ -2676,89 +2823,43 @@ function versionLog(msg) { document.getElementById('versionCheckLog').innerHTML = msg; } -function getNetdataCommitIdFromVersion() { - var s = options.version.split('-'); +// New way of checking for updates, based only on versions - if (s.length !== 3) { - return null; - } - if (s[2][0] === 'g') { - var v = s[2].split('_')[0].substring(1, 8); - if (v.length === 7) { - versionLog('Installed git commit id of netdata is ' + v); - document.getElementById('netdataCommitId').innerHTML = v; - return v; - } +function versionsMatch(v1, v2) { + if (v1 == v2) { + return true; + } else { + var s1=v1.split('-'); + var s2=v2.split('-'); + if (s1.length !== s2.length) return false; + if (s1.length === 4) s1.pop(); + if (s2.length === 4) s2.pop(); + return (s1.join('-') === s2.join('-')); } - return null; } -function getNetdataCommitId(force, callback) { - versionLog('Downloading installed git commit id from netdata...'); +function getGithubLatestVersion(callback) { + versionLog('Downloading latest version id from github...'); $.ajax({ - url: 'version.txt', - async: true, - cache: false, - xhrFields: {withCredentials: true} // required for the cookie - }) - .done(function (data) { - data = data.replace(/(\r\n|\n|\r| |\t)/gm, ""); - - var c = getNetdataCommitIdFromVersion(); - if (c !== null && data.length === 40 && data.substring(0, 7) !== c) { - versionLog('Installed files commit id and internal netdata git commit id do not match'); - data = c; - } - - if (data.length >= 7) { - versionLog('Installed git commit id of netdata is ' + data); - document.getElementById('netdataCommitId').innerHTML = data.substring(0, 7); - callback(data); - } - }) - .fail(function () { - versionLog('Failed to download installed git commit id from netdata!'); - - if (force === true) { - var c = getNetdataCommitIdFromVersion(); - if (c === null) { - versionLog('Cannot find the git commit id of netdata.'); - } - callback(c); - } else { - callback(null); - } - }); -} - -function getGithubLatestCommit(callback) { - versionLog('Downloading latest git commit id info from github...'); - - $.ajax({ - url: 'https://api.github.com/repos/netdata/netdata/commits', + url: 'https://api.github.com/repositories/10744183/contents/packaging/version?ref=master', async: true, cache: false }) .done(function (data) { - versionLog('Latest git commit id from github is ' + data[0].sha); - callback(data[0].sha); + data = atob(data.content.replace(/(\r\n|\n|\r| |\t)/gm, "")); + versionLog('Latest version from github is ' + data); + callback(data); }) .fail(function () { - versionLog('Failed to download installed git commit id from github!'); + versionLog('Failed to download the latest version id from github!'); callback(null); }); } -function checkForUpdate(force, callback) { - getNetdataCommitId(force, function (sha1) { - if (sha1 === null) { - callback(null, null); - } - - getGithubLatestCommit(function (sha2) { - callback(sha1, sha2); - }); +function checkForUpdateByVersion(force, callback) { + getGithubLatestVersion(function (sha2) { + callback(options.version, sha2); }); return null; @@ -2784,23 +2885,22 @@ function notifyForUpdate(force) { } } - checkForUpdate(force, function (sha1, sha2) { + checkForUpdateByVersion(force, function (sha1, sha2) { var save = false; if (sha1 === null) { save = false; - versionLog('<p><big>Failed to get your netdata git commit id!</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>'); + 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 git commit id from github.</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 (sha1 === sha2) { + versionLog('<p><big>Failed to get the latest netdata version github.</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/>Probably, we 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://github.com/netdata/netdata/compare/' + sha1.toString() + '...' + sha2.toString(); - - versionLog('<p><big><strong>New version of netdata available!</strong></big></p><p>Latest commit: <b><code>' + sha2.substring(0, 7).toString() + '</code></b></p><p><a href="' + compare + '" target="_blank">Click here for the changes log</a> since your installed version, and<br/><a href="https://github.com/netdata/netdata/tree/master/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>'); + 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 = '!'; } @@ -3968,6 +4068,7 @@ function runOnceOnDashboardWithjQuery() { }) .on('shown.bs.dropdown', function () { Ps.update(document.getElementById('my-netdata-dropdown-content')); + myNetdataMenuDidShow(); }) .on('hidden.bs.dropdown', function () { NETDATA.unpause(); @@ -4106,7 +4207,7 @@ function runOnceOnDashboardWithjQuery() { } } } - + if (inTag && content[i] === '>') { inTag = false; } @@ -4209,7 +4310,7 @@ function finalizePage() { })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-64295674-3', 'auto'); - ga('send', 'pageview'); + ga('send', 'pageview', '/demosite/' + window.location.host); }, 2000); } else { notifyForUpdate(); @@ -4341,3 +4442,523 @@ var selected_server_timezone = function (timezone, status) { // 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; + }); +} + +// 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 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?origin=${encodeURIComponent(window.location.origin + "/")}`; + window.open(url); +} + +function signOut() { + cloudSSOSignOut(); +} + +function renderAccountUI() { + if (!NETDATA.registry.isCloudEnabled) { + return + } + + const container = document.getElementById("account-menu-container"); + if (isSignedIn()) { + container.removeAttribute("title"); + container.removeAttribute("data-original-title"); + container.removeAttribute("data-placement"); + container.innerHTML = ( + `<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span id="amc-account-name"></span> <strong class="caret"></strong></a> + <ul id="cloud-menu" class="dropdown-menu scrollable-menu inpagemenu" role="menu"> + <li> + <a href="#" class="btn" onclick="signOutDidClick(event); return false"> + <i class="fas fa-sign-out-alt"></i> <span class="hidden-sm hidden-md">Sign Out</span> + </a> + </li> + </ul>` + ) + document.getElementById("amc-account-name").textContent = cloudAccountName; // Anti-XSS + } else { + container.setAttribute("data-original-title", "sign in"); + container.setAttribute("data-placement", "bottom"); + container.innerHTML = ( + `<a href="#" class="btn" onclick="signInDidClick(event); return false"> + <i class="fas fa-sign-in-alt"></i> <span class="hidden-sm hidden-md">Sign In</span> + </a>` + ) + } +} + +function handleMessage(e) { + switch (e.data.type) { + case "sign-in": + handleSignInMessage(e); + break; + + case "sign-out": + handleSignOutMessage(e); + break; + + default: + return; + } +} + +function handleSignInMessage(e) { + localStorage.setItem("cloud.baseURL", NETDATA.registry.cloudBaseURL); + + cloudAccountID = e.data.accountID; + cloudAccountName = e.data.accountName; + cloudToken = e.data.token; + + netdataRegistryCallback(registryAgents); +} + +function handleSignOutMessage(e) { + clearCloudVariables(); + renderAccountUI(); + 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(); + } + + renderAccountUI(); +} + +// 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(); + } + }) +} |