dashboard.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. /**
  2. * Copyright (c) UNA, Inc - https://una.io
  3. * MIT License - https://opensource.org/licenses/MIT
  4. *
  5. * @defgroup UnaStudio UNA Studio
  6. * @{
  7. */
  8. function BxDolStudioDashboard(oOptions) {
  9. this.sActionsUrl = oOptions.sActionUrl;
  10. this.aStatusTitles = oOptions.aStatusTitles || {}; // ok / warn / fail / undef, as words
  11. this.sObjName = oOptions.sObjName == undefined ? 'oBxDolStudioDashboard' : oOptions.sObjName;
  12. this.sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
  13. this.iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
  14. var $this = this;
  15. $(document).ready(function() {
  16. // one polite live region for everything the page finishes asynchronously (see announce)
  17. $('<div id="bx-dbd-status" class="sr-only" role="status" aria-live="polite"></div>').appendTo($('#bx-std-page-content').first().length ? '#bx-std-page-content' : 'body');
  18. $('.bx-dbd-block-content').bxProcessHtml();
  19. });
  20. }
  21. /**
  22. * Polite live region of the page; cleared first so the same text is announced again.
  23. */
  24. BxDolStudioDashboard.prototype.announce = function(sText) {
  25. var oStatus = $('#bx-dbd-status');
  26. if(!oStatus.length || !sText)
  27. return;
  28. oStatus.text('');
  29. setTimeout(function() {
  30. oStatus.text(sText);
  31. }, 50);
  32. };
  33. /**
  34. * Block busy state while it fetches (aria-busy on .bx-std-block). The visible indicator is the pressed button's (loadingButton); a fetch
  35. * nothing was pressed for (the Server block's first load, a tab's first opening) shows the same wave inside the panel it fills (getLoader).
  36. */
  37. BxDolStudioDashboard.prototype.loading = function(sDivId, bShow, oButton) {
  38. $('#' + sDivId).closest('.bx-std-block').attr('aria-busy', bShow ? 'true' : 'false');
  39. if(oButton)
  40. this.loadingButton(oButton, bShow);
  41. };
  42. /**
  43. * Trigger button loader: the Studio-wide one (page.js bx_std_loading_btn).
  44. */
  45. BxDolStudioDashboard.prototype.loadingButton = function(oButton, bShow) {
  46. bx_std_loading_btn(oButton, bShow);
  47. };
  48. /**
  49. * The tile wave centred in a panel whose content is on its way (dashboard.css .bx-dbd-loading).
  50. */
  51. BxDolStudioDashboard.prototype.getLoader = function() {
  52. return '<div class="bx-dbd-loading" aria-hidden="true"><span class="bx-std-btn-loading"></span></div>';
  53. };
  54. /**
  55. * Updates block: ask the server for the latest versions and fill the status rows. Runs when the page opens and from the
  56. * hero's "Check for updates" button, which shows the loader (and takes no clicks) while a check is in flight.
  57. */
  58. BxDolStudioDashboard.prototype.checkForUpgrade = function() {
  59. var $this = this;
  60. var sDivId = 'bx-dbd-version';
  61. var oButton = $('#' + sDivId + ' .bx-dbd-version-check');
  62. $this.loading(sDivId, true, oButton);
  63. $.get(
  64. this.sActionsUrl,
  65. {
  66. dbd_action: 'check_for_upgrade',
  67. _t: new Date().getTime()
  68. },
  69. function(oData) {
  70. var oInfo = oData && oData.data ? oData.data : {};
  71. $this.setVersionRow('stable', oInfo.stable);
  72. $this.setVersionRow('beta', oInfo.beta);
  73. $this.setVersionRow('apps', oInfo.apps);
  74. // the rows changed silently: read them out, name, value and status
  75. var aOut = [];
  76. $('#' + sDivId + ' .bx-dbd-htools-item[id]').each(function() {
  77. var sName = $(this).find('.bx-dbd-htools-name').contents().first().text().trim();
  78. var sStatus = $(this).find('.bx-dbd-htools-status').text().trim();
  79. aOut.push(sName + ' ' + $(this).find('.bx-dbd-htools-value').text().trim() + (sStatus ? ' (' + sStatus.replace(/^,\s*/, '') + ')' : ''));
  80. });
  81. $this.announce(aOut.join(', '));
  82. // the Update button lives in the block caption, outside the block content
  83. if(oInfo.upgrade != undefined && parseInt(oInfo.upgrade) == 1)
  84. $('#' + sDivId).closest('.bx-std-block').find('.bx-dbd-version-update').show();
  85. },
  86. 'json'
  87. ).always(function() {
  88. $this.loading(sDivId, false, oButton);
  89. });
  90. };
  91. /**
  92. * Updates block: fill a status row (Latest Stable, Latest Beta, Apps) from the update check: {status, name?, value}.
  93. */
  94. BxDolStudioDashboard.prototype.setVersionRow = function(sRow, oRow) {
  95. var oItem = $('#bx-dbd-version-' + sRow);
  96. if(!oItem.length || !oRow)
  97. return;
  98. oItem.removeClass('bx-dbd-htools-item-ok bx-dbd-htools-item-warn bx-dbd-htools-item-fail bx-dbd-htools-item-undef').addClass('bx-dbd-htools-item-' + oRow.status);
  99. if(oRow.name)
  100. oItem.find('.bx-dbd-htools-name').contents().first().replaceWith(document.createTextNode(oRow.name));
  101. oItem.find('.bx-dbd-htools-status').text(this.aStatusTitles[oRow.status] ? ', ' + this.aStatusTitles[oRow.status] : '');
  102. oItem.find('.bx-dbd-htools-value').html(oRow.value);
  103. };
  104. /**
  105. * Version block: an update switch changed (the site's switch script has already flipped the checkbox and the on/off
  106. * classes); save the option and flip everything back if the save fails.
  107. */
  108. BxDolStudioDashboard.prototype.toggleOption = function(sName, oInput) {
  109. var $this = this;
  110. var oSwitch = $(oInput).closest('.bx-switcher-cont');
  111. var bOn = oInput.checked;
  112. var fSet = function(bState) {
  113. oInput.checked = bState;
  114. oSwitch.toggleClass('on', bState).toggleClass('off', !bState);
  115. oSwitch.find('[role="switch"]').attr('aria-checked', bState ? 'true' : 'false');
  116. };
  117. fSet(bOn);
  118. $.post(
  119. this.sActionsUrl,
  120. {
  121. dbd_action: 'set_option',
  122. dbd_value: sName,
  123. dbd_state: bOn ? 1 : 0,
  124. _t: new Date().getTime()
  125. },
  126. function(oData) {
  127. if(oData && oData.code == 0)
  128. return;
  129. fSet(!bOn);
  130. if(oData && oData.message)
  131. $this.popup(oData.message);
  132. },
  133. 'json'
  134. ).fail(function() {
  135. fSet(!bOn);
  136. });
  137. };
  138. BxDolStudioDashboard.prototype.performUpgrade = function() {
  139. var $this = this;
  140. var oDate = new Date();
  141. var sDivId = 'bx-dbd-version';
  142. var oButton = $('#' + sDivId).closest('.bx-std-block').find('.bx-dbd-version-update'); // the caption's Update button
  143. $this.loading(sDivId, true, oButton);
  144. $.post(
  145. this.sActionsUrl,
  146. {
  147. dbd_action: 'perform_upgrade',
  148. _t: oDate.getTime()
  149. },
  150. function(oData) {
  151. $this.loading(sDivId, false, oButton);
  152. if(!oData.message)
  153. return;
  154. $this.popup(oData.message);
  155. },
  156. 'json'
  157. );
  158. };
  159. BxDolStudioDashboard.prototype.checkHostParams = function() {
  160. var $this = this;
  161. $('#bx-dbd-htools').html(this.getLoader());
  162. this.getBlockContent('htools', function(oData) {
  163. if(oData.data)
  164. $this.replaceHostTools(oData.data);
  165. });
  166. };
  167. /**
  168. * Server block: swap in freshly rendered content (the Status tab shown, the Audit tab to be loaded again) and let the tabs be used.
  169. */
  170. BxDolStudioDashboard.prototype.replaceHostTools = function(sContent) {
  171. var sDivId = 'bx-dbd-htools';
  172. $('#' + sDivId).replaceWith(sContent);
  173. var oBlock = $('#' + sDivId).bxProcessHtml().closest('.bx-std-block').attr('aria-busy', 'false');
  174. // the tabs stayed usable while the block loaded: show whichever one is selected (the fresh panels open on Status)
  175. var oSelected = oBlock.find('[role="tablist"] [role="tab"][aria-selected="true"]');
  176. if(oSelected.length)
  177. this.hostToolsTab(oSelected.attr('aria-controls').replace(sDivId + '-', ''), oSelected);
  178. // the gauges arrived silently: read them out
  179. var aOut = [];
  180. $('#' + sDivId + ' .bx-dbd-gauge-ring[aria-label]').each(function() {
  181. aOut.push($(this).attr('aria-label'));
  182. });
  183. this.announce(aOut.join(', '));
  184. };
  185. /**
  186. * Server block, Audit tab: run the audit again, redraw the block from the new report and come back to the Audit tab.
  187. */
  188. BxDolStudioDashboard.prototype.runAudit = function(oButton) {
  189. var $this = this;
  190. var sDivId = 'bx-dbd-htools';
  191. oButton = $(oButton);
  192. $this.loading(sDivId, true, oButton);
  193. $.post(
  194. this.sActionsUrl,
  195. {
  196. dbd_action: 'run_audit'
  197. },
  198. function(oData) {
  199. if(!oData || !oData.data)
  200. return;
  201. $this.replaceHostTools(oData.data);
  202. $this.hostToolsTab('audit');
  203. },
  204. 'json'
  205. ).always(function() {
  206. // the block is either replaced (the button with it) or left as it was, with its button usable again
  207. $this.loading(sDivId, false, oButton);
  208. });
  209. };
  210. /**
  211. * Server block, Audit tab: fold or unfold a section's rows.
  212. */
  213. BxDolStudioDashboard.prototype.auditToggle = function(oButton) {
  214. var bOpen = $(oButton).attr('aria-expanded') != 'true';
  215. $(oButton).attr('aria-expanded', bOpen ? 'true' : 'false');
  216. $('#' + $(oButton).attr('aria-controls')).prop('hidden', !bOpen);
  217. };
  218. BxDolStudioDashboard.prototype.getBlockContent = function(sType, onComplete) {
  219. var $this = this;
  220. var oDate = new Date();
  221. var sDivId = 'bx-dbd-' + sType;
  222. $this.loading(sDivId, true);
  223. $.get(
  224. this.sActionsUrl,
  225. {
  226. dbd_action: 'get_block',
  227. dbd_value: sType,
  228. _t: oDate.getTime()
  229. },
  230. function(oData) {
  231. $this.loading(sDivId, false);
  232. if(typeof onComplete == 'function')
  233. return onComplete(oData);
  234. if(!oData.data)
  235. return;
  236. $('#' + sDivId).replaceWith(oData.data);
  237. },
  238. 'json'
  239. );
  240. };
  241. BxDolStudioDashboard.prototype.initChart = function(sType, oData) {
  242. this.showChart(sType, oData);
  243. };
  244. /**
  245. * Share chart drawn in place, no library: a hero panel with the "used" headline (and the capacity when known) over a stacked
  246. * bar with one slice per item, then a row per item with its icon, size, colour mark and, when the item can be cleared on its
  247. * own, a trash button.
  248. * oData is {items: [{name, bytes, size, icon: {url|svg}, type?, clear?, note?, note_alert?, url?}], used, used_bytes, total, total_bytes,
  249. * total_fail?, icon_clear?, bar?, action?}. With bar: false (the Queues block) it is a plain list: the headline and the
  250. * rows, without the bar and the colour marks; a row's note sits before its value (in red with note_alert), a row with a url
  251. * is a link to the page that manages it, and action names the method a row's clear button calls (clearCache by default).
  252. * A chart with a label (the Cache block) gets a compact label / stat line over its bar instead of the big headline, top
  253. * names that many of its largest items under the bar (top_zero lists items at zero too), and clear_all puts a "clear all" button under it. Several charts at once (the Space block:
  254. * local and remote storage) come as {charts: [{label, items, used, total, ...}], groups: [{items}]}: the hero stacks a
  255. * labelled chart per entry, the list shows one group of rows per chart, separated by a hairline; a row marked placeholder
  256. * has no colour mark.
  257. */
  258. BxDolStudioDashboard.prototype.showChart = function(sType, oData) {
  259. var $this = this;
  260. var oChart = $('#bx-dbd-' + sType + ' .bx-dbd-chart');
  261. if(!oChart.length || !oData)
  262. return;
  263. var aCharts = oData.charts || [oData];
  264. var aGroups = oData.groups || [{items: oData.items || []}];
  265. var bRows = aCharts.length > 1 || !!aCharts[0].label;
  266. var bBar = oData.bar !== false;
  267. var sClear = oData.action || 'clearCache';
  268. var aColors = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f97316', '#ec4899', '#84cc16', '#06b6d4', '#f59e0b', '#ef4444', '#9ca3af'];
  269. var fAttr = function(s) { return String(s).replace(/"/g, '&quot;'); };
  270. var sHero = '';
  271. for(var c = 0; c < aCharts.length; c++) {
  272. var oC = aCharts[c];
  273. var aItems = oC.items || [];
  274. var fBase = oC.total_bytes > 0 ? oC.total_bytes : oC.used_bytes;
  275. var sSlices = '', sLabel = '';
  276. for(var i = 0; i < aItems.length; i++) {
  277. var oItem = aItems[i];
  278. var fShare = fBase > 0 ? oItem.bytes / fBase * 100 : 0;
  279. // Each slice names its item in the tooltip below.
  280. if(bBar && oItem.bytes > 0)
  281. sSlices += '<span class="bx-dbd-chart-slice" style="width:' + fShare.toFixed(2) + '%;background:' + aColors[i % aColors.length] + '" data-label="' + fAttr(oItem.name + ': ' + oItem.size) + '"></span>';
  282. sLabel += (sLabel ? ', ' : '') + oItem.name + ' ' + oItem.size;
  283. }
  284. // An unlabelled chart: the big headline. Labelled: a compact label / stat line over the bar.
  285. var sHead = bRows
  286. ? '<div class="bx-dbd-chart-head bx-dbd-chart-head-sm"><span class="bx-dbd-chart-label">' + oC.label + '</span><span class="bx-dbd-chart-stat">' + oC.used + (oC.total ? ' &middot; ' + oC.total : '') + '</span></div>'
  287. : '<div class="bx-dbd-chart-head"><span class="bx-dbd-chart-used">' + oC.used + '</span>' + (oC.total ? '<span class="bx-dbd-chart-total' + (oC.total_fail ? ' bx-dbd-chart-total-fail' : '') + '">' + oC.total + '</span>' : '') + '</div>';
  288. var sBar = bBar ? '<div class="bx-dbd-chart-bar" role="img" aria-label="' + fAttr((oC.label ? oC.label + ': ' : '') + (sLabel || oC.used)) + '">' + sSlices + '</div>' : '';
  289. // The largest items, biggest first (the Cache block lists its caches in a fixed order), written under the bar like the audit's outcome counts.
  290. var aTop = [];
  291. var aBySize = oC.top ? aItems.slice().sort(function(a, b) { return b.bytes - a.bytes; }) : [];
  292. for(var j = 0; j < aBySize.length && aTop.length < oC.top; j++)
  293. if(aBySize[j].bytes > 0 || oC.top_zero)
  294. aTop.push(aBySize[j].name + ' ' + aBySize[j].size);
  295. var sSummary = aTop.length ? '<div class="bx-dbd-chart-summary">' + aTop.join(' &middot; ') + '</div>' : '';
  296. sHero += bRows ? '<div class="bx-dbd-chart-row">' + sHead + sBar + sSummary + '</div>' : sHead + sBar + sSummary;
  297. }
  298. // The "clear all" button follows the chart in a foot row.
  299. if(oData.clear_all)
  300. sHero += '<div class="bx-dbd-chart-foot"><button type="button" class="bx-btn bx-dbd-chart-clear-all">' + oData.clear_all + '</button></div>';
  301. var sList = '';
  302. for(var g = 0; g < aGroups.length; g++) {
  303. var aRows = aGroups[g].items || [];
  304. var sRows = '';
  305. for(var i = 0; i < aRows.length; i++) {
  306. var oRow = aRows[i];
  307. // An app's own icon, or a Lucide glyph on the same gray tile the block headers use; both carry the tile's hairline ring.
  308. var sIcon = '';
  309. if(oRow.icon && oRow.icon.url)
  310. sIcon = '<span class="bx-dbd-tile bx-dbd-tile-app" aria-hidden="true"><img src="' + oRow.icon.url + '" alt=""></span>';
  311. else
  312. sIcon = '<span class="bx-dbd-tile" aria-hidden="true">' + (oRow.icon && oRow.icon.svg ? oRow.icon.svg : '') + '</span>';
  313. // A row that can be cleared on its own ends with an icon-only trash button (the label is its accessible name).
  314. var sAction = '';
  315. if(oRow.clear && oRow.type && oData.icon_clear && oData.icon_clear.svg)
  316. sAction = '<button type="button" class="bx-btn bx-dbd-chart-action" data-type="' + oRow.type + '" title="' + fAttr(oRow.clear) + '" aria-label="' + fAttr(oRow.clear) + '">' + oData.icon_clear.svg + '</button>';
  317. // A mark in the row's bar colour, right after the value, ties the row to its slice; a note (a file count, or failed jobs) sits before the value.
  318. var sMark = bBar && !oRow.placeholder ? '<span class="bx-dbd-chart-mark" style="background:' + aColors[i % aColors.length] + '" aria-hidden="true"></span>' : '';
  319. var sNote = oRow.note ? '<span class="bx-dbd-chart-note' + (oRow.note_alert ? ' bx-dbd-chart-note-alert' : '') + '">' + oRow.note + '</span>' : '';
  320. var sCells = sIcon + '<span class="bx-dbd-chart-name">' + oRow.name + '</span>' + sNote + '<span class="bx-dbd-chart-size">' + oRow.size + '</span>' + sMark + sAction;
  321. sRows += oRow.url ? '<a class="bx-dbd-chart-item bx-dbd-chart-link" href="' + fAttr(oRow.url) + '">' + sCells + '</a>' : '<div class="bx-dbd-chart-item">' + sCells + '</div>';
  322. }
  323. sList += '<div class="bx-dbd-chart-group">' + sRows + '</div>';
  324. }
  325. oChart.html(
  326. '<div class="bx-dbd-hero bx-dbd-chart-hero' + (bRows ? ' bx-dbd-chart-hero-rows' : '') + '">' + sHero + '</div>' +
  327. '<div class="bx-dbd-chart-list">' + sList + '</div>'
  328. );
  329. this.bindChartTips(oChart);
  330. if(typeof $this[sClear] != 'function')
  331. return;
  332. oChart.find('.bx-dbd-chart-action').on('click', function() {
  333. $this[sClear]($(this).data('type'), this);
  334. return false;
  335. });
  336. oChart.find('.bx-dbd-chart-clear-all').on('click', function() {
  337. $this[sClear]('all', this);
  338. return false;
  339. });
  340. };
  341. /**
  342. * Hovering a bar slice shows its data-label above the bar, in a tip that lives in the hero (the bar clips to its rounded
  343. * corners). Binds every bar under oScope: a chart block's, or the Audit tab's outcome bar.
  344. */
  345. BxDolStudioDashboard.prototype.bindChartTips = function(oScope) {
  346. var oHero = oScope.find('.bx-dbd-chart-hero');
  347. if(!oHero.length)
  348. return;
  349. var oTip = $('<div class="bx-dbd-chart-tip" role="tooltip" hidden></div>').appendTo(oHero);
  350. oScope.find('.bx-dbd-chart-slice').on('mouseenter', function() {
  351. var oSlice = $(this);
  352. var oBar = oSlice.parent();
  353. oTip.text(oSlice.data('label')).prop('hidden', false);
  354. var iHalf = oTip.outerWidth() / 2;
  355. var iLeft = oSlice.offset().left - oHero.offset().left + oSlice.outerWidth() / 2;
  356. oTip.css({
  357. left: Math.min(Math.max(iLeft, iHalf), oHero.innerWidth() - iHalf),
  358. top: oBar.offset().top - oHero.offset().top - oTip.outerHeight() - 6
  359. });
  360. }).on('mouseleave', function() {
  361. oTip.prop('hidden', true);
  362. });
  363. };
  364. /**
  365. * Clear one item (or 'all') of a chart block and redraw it from the returned data; a string comes back when the block has nothing left to chart.
  366. */
  367. BxDolStudioDashboard.prototype.clearChartItem = function(sBlock, sAction, sType, oButton) {
  368. var $this = this;
  369. var oDate = new Date();
  370. var sDivId = 'bx-dbd-' + sBlock;
  371. $this.loading(sDivId, true, oButton);
  372. $.post(
  373. this.sActionsUrl,
  374. {
  375. dbd_action: sAction,
  376. dbd_value: sType,
  377. _t: oDate.getTime()
  378. },
  379. function(oData) {
  380. $this.loading(sDivId, false, oButton);
  381. if(oData.message != undefined && oData.message.length > 0)
  382. $this.popup(oData.message, function() {
  383. // the control that was used is redrawn with the chart: hand focus to the block's first control
  384. return $('#' + sDivId).find('button, a[href]').first().get(0);
  385. });
  386. if(oData.data == undefined)
  387. return;
  388. if(typeof oData.data == 'object')
  389. $this.showChart(sBlock, oData.data);
  390. else if(typeof oData.data == 'string')
  391. $('#' + sDivId).html(oData.data);
  392. },
  393. 'json'
  394. );
  395. };
  396. BxDolStudioDashboard.prototype.clearCache = function(sType, oButton) {
  397. this.clearChartItem('cache', 'clear_cache', sType, oButton);
  398. };
  399. BxDolStudioDashboard.prototype.clearQueue = function(sType, oButton) {
  400. this.clearChartItem('queues', 'clear_queue', sType, oButton);
  401. };
  402. /**
  403. * Server block: show one tab's panel. Audit and permissions panels fetch their report the first time they open.
  404. */
  405. BxDolStudioDashboard.prototype.hostToolsTab = function(sTab, oTab) {
  406. var $this = this;
  407. var sDivId = 'bx-dbd-htools';
  408. var oBlock = $('#' + sDivId).closest('.bx-std-block');
  409. var oPanel = $('#' + sDivId + '-' + sTab);
  410. if(!oPanel.length)
  411. return;
  412. oTab = oTab ? $(oTab) : $('#' + sDivId + '-tab-' + sTab);
  413. oBlock.find('[role="tablist"] [role="tab"]').attr({'aria-selected': 'false', 'tabindex': '-1'});
  414. oTab.attr({'aria-selected': 'true', 'tabindex': '0'});
  415. oBlock.find('.bx-dbd-htools-panel').prop('hidden', true);
  416. oPanel.prop('hidden', false);
  417. var sAction = oPanel.data('action');
  418. if(!sAction || oPanel.data('loaded'))
  419. return;
  420. oPanel.data('loaded', true).html($this.getLoader());
  421. $this.loading(sDivId, true);
  422. $.get(
  423. this.sActionsUrl,
  424. {
  425. dbd_action: sAction,
  426. _t: new Date().getTime()
  427. },
  428. function(sData) {
  429. $this.loading(sDivId, false);
  430. if(!sData.length) {
  431. oPanel.data('loaded', false).empty();
  432. return;
  433. }
  434. oPanel.html(sData).bxProcessHtml();
  435. $this.bindChartTips(oPanel);
  436. },
  437. 'html'
  438. ).fail(function() {
  439. // the tab can be tried again after a failed request
  440. $this.loading(sDivId, false);
  441. oPanel.data('loaded', false).empty();
  442. });
  443. };
  444. /**
  445. * Arrow keys move between the Server block's tabs, as a tablist expects.
  446. */
  447. BxDolStudioDashboard.prototype.hostToolsTabKey = function(oEvent, oTab) {
  448. var iStep = oEvent.key == 'ArrowRight' ? 1 : (oEvent.key == 'ArrowLeft' ? -1 : 0);
  449. var bEdge = oEvent.key == 'Home' || oEvent.key == 'End';
  450. if(!iStep && !bEdge)
  451. return;
  452. var oTabs = $(oTab).closest('[role="tablist"]').find('[role="tab"]');
  453. var oNext = bEdge ? oTabs.eq(oEvent.key == 'Home' ? 0 : oTabs.length - 1) : $(oTabs.get((oTabs.index(oTab) + iStep + oTabs.length) % oTabs.length));
  454. oEvent.preventDefault();
  455. oNext.focus();
  456. this.hostToolsTab(oNext.attr('aria-controls').replace('bx-dbd-htools-', ''), oNext);
  457. };
  458. /**
  459. * A message in a modal popup. The dialog is named by its text and takes focus, so it is read out and Escape closes it;
  460. * afterwards focus goes back to the control that was used, or to what fFocusBack() returns when that control is gone.
  461. */
  462. BxDolStudioDashboard.prototype.popup = function(sValue, fFocusBack) {
  463. var sId = 'bx-std-dbd-popup';
  464. var oLast = document.activeElement;
  465. $('#' + sId).remove();
  466. $('<div id="' + sId + '" style="display: none;"></div>').prependTo('body').html(sValue);
  467. $('#' + sId).dolPopup({
  468. closeElement: '.bx-std-action-result-close, .bx-popup-element-close', // the outcome dialog's own close button (it carries no site popup class, whose hover rule would move it)
  469. onShow: function(oPopup) {
  470. var oDialog = oPopup.find('[role="dialog"]').first();
  471. if(!oDialog.length)
  472. oDialog = oPopup;
  473. // an outcome dialog (page_action_result.html) is an alert dialog named by its message, and focus lands on its Close button
  474. var oText = oPopup.find('.bx-std-action-result-cnt').first();
  475. if(oText.length) {
  476. if(!oText.attr('id'))
  477. oText.attr('id', sId + '-text');
  478. oDialog.attr({'role': 'alertdialog', 'aria-labelledby': oText.attr('id')});
  479. }
  480. else
  481. oDialog.attr('aria-label', oPopup.text().trim().replace(/\s+/g, ' ').slice(0, 200));
  482. oDialog.attr('tabindex', '-1');
  483. var oClose = oPopup.find('.bx-std-action-result-close').first();
  484. (oClose.length ? oClose : oDialog).get(0).focus();
  485. },
  486. onHide: function() {
  487. var oBack = oLast && document.contains(oLast) && oLast !== document.body ? oLast : (typeof fFocusBack == 'function' ? fFocusBack() : null);
  488. if(oBack)
  489. oBack.focus();
  490. }
  491. });
  492. };
  493. /**
  494. * Users block: the chart in the hero, under the charted series' figure for the period and its title, and the rows under it as its legend,
  495. * each showing its own figure for the period; on the hero's right, how many of the charted series are online now. oData is {period,
  496. * series_default, periods: {name: {title, label, type}}, series: {name: {title, online, periods: {name: [{label, title, count, total?}]}}},
  497. * txt_peak}; everything comes with the block, so the period
  498. * tabs in the block caption (dbd_users_tabs.html) and the rows only redraw. 'bars' periods: a bar per bucket, as tall as its share of the
  499. * period's peak, the newest one solid, each naming its bucket and count in the slice tooltip. 'line' (the Total tab, first and selected): the
  500. * running total as a line over the whole period with the area under it tinted, a transparent column per bucket carrying the tooltip.
  501. */
  502. BxDolStudioDashboard.prototype.initUsersChart = function(oData) {
  503. this.oUsersData = oData;
  504. this.sUsersPeriod = oData && oData.period ? oData.period : 'all';
  505. this.sUsersSeries = oData && oData.series_default ? oData.series_default : 'accounts';
  506. this.usersDraw();
  507. };
  508. /**
  509. * A period tab in the block caption was chosen.
  510. */
  511. BxDolStudioDashboard.prototype.usersPeriod = function(sPeriod, oTab) {
  512. var oData = this.oUsersData;
  513. if(!oData || !oData.periods || !oData.periods[sPeriod])
  514. return;
  515. var oBlock = $('#bx-dbd-users').closest('.bx-std-block');
  516. oTab = oTab ? $(oTab) : oBlock.find('[role="tab"][data-period="' + sPeriod + '"]');
  517. oBlock.find('[role="tab"]').attr({'aria-selected': 'false', 'tabindex': '-1'});
  518. oTab.attr({'aria-selected': 'true', 'tabindex': '0'});
  519. oBlock.find('#bx-dbd-users-chart').attr('aria-labelledby', oTab.attr('id') || null);
  520. this.sUsersPeriod = sPeriod;
  521. this.usersDraw();
  522. };
  523. /**
  524. * A row was pressed: its series is the one charted.
  525. */
  526. BxDolStudioDashboard.prototype.usersSeries = function(sSeries, oRow) {
  527. var oData = this.oUsersData;
  528. if(!oData || !oData.series || !oData.series[sSeries])
  529. return;
  530. var oBlock = $('#bx-dbd-users').closest('.bx-std-block');
  531. oBlock.find('.bx-dbd-users-row').attr('aria-pressed', 'false');
  532. (oRow ? $(oRow) : oBlock.find('.bx-dbd-users-row[data-series="' + sSeries + '"]')).attr('aria-pressed', 'true');
  533. this.sUsersSeries = sSeries;
  534. this.usersDraw();
  535. };
  536. /**
  537. * Draw the chosen series over the chosen period.
  538. */
  539. BxDolStudioDashboard.prototype.usersDraw = function() {
  540. var oData = this.oUsersData;
  541. var oPeriod = oData && oData.periods ? oData.periods[this.sUsersPeriod] : null;
  542. var oSeries = oData && oData.series ? oData.series[this.sUsersSeries] : null;
  543. if(!oPeriod || !oSeries)
  544. return;
  545. var oBlock = $('#bx-dbd-users').closest('.bx-std-block');
  546. var fAttr = function(s) { return String(s).replace(/"/g, '&quot;'); };
  547. var aItems = (oSeries.periods && oSeries.periods[this.sUsersPeriod]) || [], iMax = 0, iTotal = 0;
  548. for(var i = 0; i < aItems.length; i++) {
  549. iTotal += aItems[i].count;
  550. iMax = Math.max(iMax, aItems[i].count);
  551. }
  552. var bLine = oPeriod.type == 'line';
  553. var sChart = '';
  554. if(bLine) {
  555. // the running total, scaled to the chart's 100 units with 4 kept above the highest point; one bucket still draws as a flat line
  556. var iLast = aItems.length ? aItems[aItems.length - 1].total : 0;
  557. var iSteps = Math.max(aItems.length - 1, 1), iW = iSteps * 10, iH = 100;
  558. var aPoints = [], sHits = '';
  559. for(var j = 0; j < aItems.length; j++) {
  560. var oItem = aItems[j];
  561. var fY = iLast > 0 ? iH - oItem.total / iLast * (iH - 4) : iH;
  562. aPoints.push((aItems.length > 1 ? j * 10 : 0) + ',' + fY.toFixed(1));
  563. if(aItems.length == 1)
  564. aPoints.push(iW + ',' + fY.toFixed(1));
  565. sHits += '<span class="bx-dbd-chart-slice bx-dbd-users-hit" data-label="' + fAttr(oItem.title + ': ' + oItem.total) + '"></span>';
  566. }
  567. sChart = '<svg class="bx-dbd-users-line-svg" viewBox="0 0 ' + iW + ' ' + iH + '" preserveAspectRatio="none" aria-hidden="true">' +
  568. '<polygon class="bx-dbd-users-line-area" points="0,' + iH + ' ' + aPoints.join(' ') + ' ' + iW + ',' + iH + '"></polygon>' +
  569. '<polyline class="bx-dbd-users-line-path" points="' + aPoints.join(' ') + '" vector-effect="non-scaling-stroke"></polyline>' +
  570. '</svg><div class="bx-dbd-users-hits">' + sHits + '</div>';
  571. iTotal = iLast;
  572. }
  573. else {
  574. for(var k = 0; k < aItems.length; k++) {
  575. var oBar = aItems[k];
  576. var fShare = iMax > 0 ? oBar.count / iMax * 100 : 0;
  577. sChart += '<span class="bx-dbd-chart-slice bx-dbd-users-bar' + (k == aItems.length - 1 ? ' bx-dbd-users-bar-last' : '') + '" style="height:' + fShare.toFixed(1) + '%" data-label="' + fAttr(oBar.title + ': ' + oBar.count) + '"></span>';
  578. }
  579. }
  580. // every series row shows its figure for the period (the running total on the Total tab), and the hero names the charted one with its figure
  581. var sPeriod = this.sUsersPeriod;
  582. var fFigure = function(oOne) {
  583. var aList = (oOne.periods && oOne.periods[sPeriod]) || [], iSum = 0;
  584. for(var n = 0; n < aList.length; n++)
  585. iSum += aList[n].count;
  586. return bLine && aList.length ? aList[aList.length - 1].total : iSum;
  587. };
  588. for(var sName in oData.series)
  589. oBlock.find('.bx-dbd-users-row[data-series="' + sName + '"] .bx-dbd-htools-value').text(fFigure(oData.series[sName]).toLocaleString());
  590. oBlock.find('.bx-dbd-users-total').text(iTotal.toLocaleString());
  591. oBlock.find('.bx-dbd-users-caption').text(oSeries.title);
  592. // on the right, how many of the charted series are online now (a live figure, the same for every period)
  593. var iOnline = parseInt(oSeries.online) || 0;
  594. oBlock.find('.bx-dbd-users-online-count').text(iOnline.toLocaleString());
  595. oBlock.find('.bx-dbd-users-online').toggleClass('bx-dbd-users-online-on', iOnline > 0);
  596. // the chart's accessible name: the series over the period, its total and its peak bucket's count
  597. var sPeak = iMax > 0 && oData.txt_peak ? ', ' + oData.txt_peak.replace('{0}', iMax) : '';
  598. oBlock.find('.bx-dbd-users-bars').toggleClass('bx-dbd-users-line', bLine).toggleClass('bx-dbd-users-bars-dense', !bLine && aItems.length > 14).attr('aria-label', fAttr(oSeries.title + ', ' + oPeriod.label + ': ' + iTotal + sPeak)).html(sChart);
  599. // the tooltip is bound to the slices, so it is rebuilt with them
  600. oBlock.find('.bx-dbd-chart-tip').remove();
  601. this.bindChartTips(oBlock);
  602. };
  603. /**
  604. * Arrow keys move between the period tabs, as a tablist expects.
  605. */
  606. BxDolStudioDashboard.prototype.usersPeriodKey = function(oEvent, oTab) {
  607. var iStep = oEvent.key == 'ArrowRight' ? 1 : (oEvent.key == 'ArrowLeft' ? -1 : 0);
  608. var bEdge = oEvent.key == 'Home' || oEvent.key == 'End';
  609. if(!iStep && !bEdge)
  610. return;
  611. var oTabs = $(oTab).closest('[role="tablist"]').find('[role="tab"]');
  612. var oNext = bEdge ? oTabs.eq(oEvent.key == 'Home' ? 0 : oTabs.length - 1) : $(oTabs.get((oTabs.index(oTab) + iStep + oTabs.length) % oTabs.length));
  613. oEvent.preventDefault();
  614. oNext.focus();
  615. this.usersPeriod(oNext.data('period'), oNext);
  616. };
  617. /** @} */