BxDolStudioDashboard.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup UnaStudio UNA Studio
  7. * @{
  8. */
  9. class BxDolStudioDashboard extends BxTemplStudioWidget
  10. {
  11. protected $aBlocks;
  12. protected $aItemsCache;
  13. protected $aItemsHTools;
  14. protected $aAuditReport = null; // the report getAuditReport() settled on in this request
  15. protected $aItemsQueues;
  16. function __construct()
  17. {
  18. parent::__construct('dashboard');
  19. $this->oDb = new BxDolStudioDashboardQuery();
  20. $this->aBlocks = array(
  21. 'space' => 'serviceGetBlockSpace',
  22. 'htools' => 'serviceGetBlockHostTools',
  23. );
  24. // The queues the Queues block lists: the count queries and, where a queue can be emptied, its clear query.
  25. $this->aItemsQueues = array(
  26. 'emails' => array(
  27. 'name' => '_sys_queue_emails',
  28. 'icon' => 'mail',
  29. 'all' => "SELECT COUNT(*) FROM `sys_queue_email`",
  30. 'action' => "DELETE FROM `sys_queue_email`"
  31. ),
  32. 'push' => array(
  33. 'name' => '_sys_queue_push',
  34. 'icon' => 'bell',
  35. 'all' => "SELECT COUNT(*) FROM `sys_queue_push`",
  36. 'action' => "DELETE FROM `sys_queue_push`"
  37. ),
  38. 'transcoding' => array(
  39. 'name' => '_sys_queue_transcoding',
  40. 'icon' => 'film',
  41. 'all' => "SELECT COUNT(*) FROM `sys_transcoder_queue`",
  42. 'failed' => "SELECT COUNT(*) FROM `sys_transcoder_queue` WHERE `status` = 'failed'",
  43. 'action' => "DELETE FROM `sys_transcoder_queue` WHERE `status` != 'processing'"
  44. ),
  45. );
  46. $this->aItemsQueues = array_merge($this->aItemsQueues, $this->getModulesQueues());
  47. // last: the files waiting to be removed from storage, a queue that is watched but never emptied by hand
  48. $this->aItemsQueues['storage'] = array(
  49. 'name' => '_sys_queue_storage',
  50. 'icon' => 'hard-drive',
  51. 'all' => "SELECT COUNT(*) FROM `sys_storage_deletions`"
  52. );
  53. $this->aItemsCache = array (
  54. array('name' => 'all'),
  55. array('name' => 'content'),
  56. array('name' => 'db'),
  57. array('name' => 'template'),
  58. array('name' => 'less'),
  59. array('name' => 'css'),
  60. array('name' => 'js'),
  61. array('name' => 'purifier'),
  62. array('name' => 'opcache'),
  63. array('name' => 'custom')
  64. );
  65. $this->aItemsHTools = array (
  66. 'PHP' => 'requirementsPHP',
  67. 'MySQL' => 'requirementsMySQL',
  68. 'Web Server' => 'requirementsWebServer',
  69. );
  70. //--- Check actions ---//
  71. if(($sAction = bx_get('dbd_action')) !== false) {
  72. $sAction = bx_process_input($sAction);
  73. $aResult = array('code' => 1, 'message' => _t('_adm_err_cannot_process_action'));
  74. // actions that change something are only taken from a POST (dashboard.js posts them)
  75. if(in_array($sAction, array('set_option', 'perform_upgrade', 'clear_cache', 'clear_queue', 'run_audit')) && $_SERVER['REQUEST_METHOD'] != 'POST')
  76. $sAction = '';
  77. switch($sAction) {
  78. case 'get_block':
  79. $sValue = bx_get('dbd_value');
  80. if($sValue === false)
  81. break;
  82. $sValue = bx_process_input($sValue);
  83. if(!isset($this->aBlocks[$sValue]))
  84. break;
  85. $aBlock = $this->{$this->aBlocks[$sValue]}(true);
  86. if(!empty($aBlock['content']))
  87. $aResult = array('code' => 0, 'data' => $aBlock['content']);
  88. break;
  89. case 'check_for_upgrade':
  90. $aResult = array('code' => 0, 'data' => $this->getPageCodeVersionAvailable());
  91. break;
  92. case 'set_option':
  93. // The Version block's switches: only the update options, each with its 'on' and 'off' value.
  94. $aOptions = array(
  95. 'sys_autoupdate' => array('on', ''),
  96. 'sys_autoupdate_force_modified_files' => array('on', ''),
  97. 'sys_upgrade_channel' => array('beta', 'stable')
  98. );
  99. $sName = bx_get('dbd_value');
  100. if($sName === false)
  101. break;
  102. $sName = bx_process_input($sName);
  103. if(!isset($aOptions[$sName]))
  104. break;
  105. $oDb = BxDolDb::getInstance();
  106. $oDb->setParam($sName, $aOptions[$sName][(int)bx_get('dbd_state') == 1 ? 0 : 1]);
  107. $oDb->cacheParamsClear();
  108. $aResult = array('code' => 0, 'message' => '');
  109. break;
  110. case 'perform_upgrade':
  111. $oUpgrader = bx_instance('BxDolUpgrader');
  112. if(!$oUpgrader->prepare(false)){
  113. $aResult = array('code' => 1, 'message' => $oUpgrader->getError());
  114. }
  115. else{
  116. BxDolModuleQuery::getInstance()->updateModule(array('updated' => time()), array('name' => 'system'));
  117. $aResult = array('code' => 0, 'message' => _t('_adm_dbd_msg_upgrade_started', BX_DOL_URL_STUDIO));
  118. }
  119. break;
  120. case 'clear_cache':
  121. $sValue = bx_get('dbd_value');
  122. if($sValue === false)
  123. break;
  124. $sValue = bx_process_input($sValue);
  125. $oCacheUtilities = BxDolCacheUtilities::getInstance();
  126. switch ($sValue) {
  127. case 'all':
  128. $aResult = false;
  129. foreach($this->aItemsCache as $aItem) {
  130. if($aItem['name'] == 'all')
  131. continue;
  132. $aResultClear = $oCacheUtilities->clear($aItem['name']);
  133. if($aResultClear === false)
  134. continue;
  135. $aResult = $aResultClear;
  136. if(isset($aResult['code']) && $aResult['code'] != 0)
  137. break;
  138. }
  139. break;
  140. case 'content':
  141. case 'db':
  142. case 'template':
  143. case 'less':
  144. case 'css':
  145. case 'js':
  146. case 'purifier':
  147. case 'opcache':
  148. case 'custom':
  149. $aResult = $oCacheUtilities->clear($sValue);
  150. break;
  151. default:
  152. $aResult = array('code' => 1, 'message' => _t('_error occured'));
  153. }
  154. if($aResult === false)
  155. $aResult['data'] = MsgBox(_t('_adm_dbd_msg_c_all_disabled'));
  156. else if(isset($aResult['code']) && $aResult['code'] == 0) {
  157. bx_alert('system', 'clear_cache', 0, 0, array('type' => $sValue));
  158. $aResult['data'] = $this->getCacheChartData(false);
  159. }
  160. break;
  161. case 'clear_queue':
  162. $sValue = bx_get('dbd_value');
  163. if($sValue === false)
  164. break;
  165. $sValue = bx_process_input($sValue);
  166. $aActions = [];
  167. if($sValue == 'all') {
  168. foreach($this->aItemsQueues as $aQueue)
  169. if(!empty($aQueue['action']))
  170. $aActions[] = $aQueue['action'];
  171. }
  172. else if(!empty($this->aItemsQueues[$sValue]['action']))
  173. $aActions[] = $this->aItemsQueues[$sValue]['action'];
  174. if(empty($aActions))
  175. break;
  176. foreach($aActions as $sQuery)
  177. BxDolDb::getInstance()->query($sQuery);
  178. $aResult = array('code' => 0, 'message' => _t('_adm_dbd_msg_q_clean_success'), 'data' => $this->getQueuesChartData(false));
  179. break;
  180. case 'run_audit':
  181. // a fresh audit, then the Server block redrawn from it (dashboard.js reopens the Audit tab)
  182. $this->getAuditReport(true);
  183. $aBlock = $this->serviceGetBlockHostTools(true);
  184. $aResult = array('code' => 0, 'data' => $aBlock['content']);
  185. break;
  186. case 'server_audit':
  187. header( 'Content-type: text/html; charset=utf-8' );
  188. echo $this->getBlockHostToolsAudit();
  189. exit;
  190. }
  191. if(!empty($aResult['message'])) {
  192. // the outcome dialog (dashboard.js popup): the message beside an icon tile coloured by the outcome, with a close button
  193. $sType = !empty($aResult['type']) ? $aResult['type'] : ((int)$aResult['code'] == 0 ? 'success' : 'error');
  194. $aIcons = array('success' => 'circle-check', 'error' => 'circle-x', 'info' => 'info');
  195. $aResult['message'] = BxDolStudioTemplate::getInstance()->parseHtmlByName('page_action_result.html', array(
  196. 'type' => $sType,
  197. 'icon' => $this->getChartIcon(isset($aIcons[$sType]) ? $aIcons[$sType] : 'info')['svg'],
  198. 'icon_close' => $this->getChartIcon('x')['svg'],
  199. 'content' => $aResult['message']
  200. ));
  201. $aResult['message'] = BxTemplStudioFunctions::getInstance()->transBox('', $aResult['message']);
  202. }
  203. echo json_encode($aResult);
  204. exit;
  205. }
  206. }
  207. /**
  208. * CPU, memory and disk usage in percent for the Server block's gauges, read from what PHP and the kernel expose
  209. * (load average per core, /proc/meminfo, disk_*_space); a metric that isn't readable on this host is null.
  210. */
  211. /**
  212. * The last server audit (BxDolStudioToolsAudit::getReport(): {time, sections}), kept in the sys_audit_report option; run
  213. * afresh when asked, when there is none yet, or when the stored one is older than a day.
  214. */
  215. public function getAuditReport($bFresh = false)
  216. {
  217. if(!$bFresh && $this->aAuditReport !== null)
  218. return $this->aAuditReport;
  219. $aReport = $bFresh ? false : json_decode((string)getParam('sys_audit_report'), true);
  220. if(empty($aReport['sections']) || time() - (int)$aReport['time'] >= 86400) {
  221. $oAudit = new BxDolStudioToolsAudit();
  222. $aReport = $oAudit->getReport();
  223. // reloading the options (rather than only dropping their cache) keeps getParam() working for the rest of this request
  224. $oDb = BxDolDb::getInstance();
  225. $oDb->setParam('sys_audit_report', json_encode($aReport));
  226. $oDb->cacheParams(true);
  227. }
  228. return $this->aAuditReport = $aReport;
  229. }
  230. /**
  231. * The Server block's gauges: each is null when the host does not expose it, or ['percent' => the arc's share, 'value' =>
  232. * the figure shown in the ring, 'unit' => a unit shown small under it, '' when the figure carries its own].
  233. */
  234. protected function getServerMetrics()
  235. {
  236. $aMetrics = ['cpu' => null, 'memory' => null, 'disk' => null, 'network' => null];
  237. $fnPercent = function($iPercent) {
  238. return ['percent' => $iPercent, 'value' => $iPercent . '%', 'unit' => ''];
  239. };
  240. if(function_exists('sys_getloadavg') && ($aLoad = @sys_getloadavg()) !== false && isset($aLoad[0])) {
  241. $iCores = 0;
  242. if(is_readable('/proc/cpuinfo') && ($sCpuInfo = @file_get_contents('/proc/cpuinfo')) !== false)
  243. $iCores = preg_match_all('/^processor\s*:/m', $sCpuInfo);
  244. if($iCores < 1)
  245. $iCores = 1;
  246. $aMetrics['cpu'] = $fnPercent((int)min(100, round($aLoad[0] / $iCores * 100)));
  247. }
  248. if(is_readable('/proc/meminfo') && ($sMemInfo = @file_get_contents('/proc/meminfo')) !== false && preg_match('/^MemTotal:\s*(\d+)/m', $sMemInfo, $aTotal) && preg_match('/^MemAvailable:\s*(\d+)/m', $sMemInfo, $aAvailable) && (int)$aTotal[1] > 0)
  249. $aMetrics['memory'] = $fnPercent((int)round(((int)$aTotal[1] - (int)$aAvailable[1]) / (int)$aTotal[1] * 100));
  250. $fDiskTotal = @disk_total_space(BX_DIRECTORY_PATH_ROOT);
  251. $fDiskFree = @disk_free_space(BX_DIRECTORY_PATH_ROOT);
  252. if($fDiskTotal !== false && $fDiskFree !== false && $fDiskTotal > 0)
  253. $aMetrics['disk'] = $fnPercent((int)round(($fDiskTotal - $fDiskFree) / $fDiskTotal * 100));
  254. // Network: the throughput over a 250 ms sample of the interface counters (this runs in the block's own ajax request, so the
  255. // pause is not felt), shown as a bit rate; the arc is its share of the link speed, 1 Gbit/s when the driver reports none.
  256. if(($aStart = $this->getNetworkCounters()) !== false) {
  257. $iStarted = hrtime(true);
  258. usleep(250000);
  259. $aEnd = $this->getNetworkCounters();
  260. $fSeconds = (hrtime(true) - $iStarted) / 1e9;
  261. if($aEnd !== false && $fSeconds > 0) {
  262. $fBits = max(0, $aEnd[0] - $aStart[0]) * 8 / $fSeconds;
  263. $fLink = ($aEnd[1] > 0 ? $aEnd[1] : 1000) * 1e6;
  264. $aMetrics['network'] = ['percent' => (int)min(100, round($fBits / $fLink * 100))] + $this->formatBitRate($fBits);
  265. }
  266. }
  267. return $aMetrics;
  268. }
  269. /**
  270. * @return array|false [bytes received plus sent over every interface but the loopback, the fastest of their link speeds in Mbit/s (0 when none reports one)]
  271. */
  272. protected function getNetworkCounters()
  273. {
  274. if(!is_readable('/proc/net/dev') || ($sDev = @file_get_contents('/proc/net/dev')) === false)
  275. return false;
  276. $iBytes = $iSpeed = 0;
  277. foreach(explode("\n", $sDev) as $sLine) {
  278. // "eth0: <rx bytes> <7 more receive counters> <tx bytes> ..."
  279. if(!preg_match('/^\s*([^:\s]+):\s*(\d+)(?:\s+\d+){7}\s+(\d+)/', $sLine, $aMatch) || $aMatch[1] == 'lo')
  280. continue;
  281. $iBytes += (int)$aMatch[2] + (int)$aMatch[3];
  282. $iSpeed = max($iSpeed, (int)@file_get_contents('/sys/class/net/' . $aMatch[1] . '/speed'));
  283. }
  284. return [$iBytes, $iSpeed];
  285. }
  286. /**
  287. * @return array ['value' => the figure, one decimal under 10, 'unit' => kbps, Mbps or Gbps]
  288. */
  289. protected function formatBitRate($fBits)
  290. {
  291. foreach(['gbps' => 1e9, 'mbps' => 1e6, 'kbps' => 1e3] as $sUnit => $fDivider) {
  292. if($fBits < $fDivider && $sUnit != 'kbps')
  293. continue;
  294. $fValue = $fBits / $fDivider;
  295. return ['value' => $fValue < 10 ? number_format($fValue, 1) : (string)round($fValue), 'unit' => _t('_adm_dbd_txt_ht_' . $sUnit)];
  296. }
  297. }
  298. protected function getDbSize()
  299. {
  300. $iTotalSize = 0;
  301. $oDb = BxDolDb::getInstance();
  302. $aTables = $oDb->getAll('SHOW TABLE STATUS');
  303. foreach($aTables as $aTable)
  304. $iTotalSize += $aTable['Data_length'] + $aTable['Index_length'];
  305. return $iTotalSize;
  306. }
  307. protected function getFolderSize($sPath)
  308. {
  309. $iTotalSize = 0;
  310. $aFiles = scandir($sPath);
  311. $sPath = rtrim($sPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
  312. foreach($aFiles as $sFile) {
  313. if(is_dir($sPath . $sFile))
  314. if($sFile != '.' && $sFile != '..')
  315. $iTotalSize += $this->getFolderSize($sPath . $sFile);
  316. else
  317. $iTotalSize += filesize($sPath . $sFile);
  318. }
  319. return $iTotalSize;
  320. }
  321. protected function getCacheChartData($bAsString = true)
  322. {
  323. $oCacheUtilities = BxDolCacheUtilities::getInstance();
  324. $aIcons = array('content' => 'file-text', 'db' => 'database', 'template' => 'layout-template', 'less' => 'braces', 'css' => 'palette', 'js' => 'code', 'purifier' => 'shield-check', 'opcache' => 'zap', 'custom' => 'box');
  325. $aItems = array();
  326. $iTotal = 0;
  327. foreach($this->aItemsCache as $aItem) {
  328. if($aItem['name'] == 'all')
  329. continue;
  330. $iSize = $oCacheUtilities->size($aItem['name']);
  331. if($iSize === false)
  332. continue;
  333. $iTotal += $iSize;
  334. $aItems[] = array(
  335. 'name' => bx_process_output(_t('_adm_dbd_txt_c_' . $aItem['name'])),
  336. 'bytes' => $iSize,
  337. 'size' => _t_format_size($iSize),
  338. 'icon' => $this->getChartIcon(isset($aIcons[$aItem['name']]) ? $aIcons[$aItem['name']] : 'box'),
  339. // the row's own clear button (dashboard.js showChart) when this cache can be cleared
  340. 'type' => $aItem['name'],
  341. 'clear' => $oCacheUtilities->isEnabled($aItem['name']) ? bx_process_output(_t('_adm_dbd_txt_c_clear_' . $aItem['name'])) : ''
  342. );
  343. }
  344. if(empty($aItems))
  345. return false;
  346. // The hero shows the chart under its label, its three largest caches written under the bar, and the "clear all" button (dashboard.js showChart).
  347. $aChartData = $this->getChartData($aItems, $iTotal);
  348. $aChartData['label'] = bx_process_output(_t('_adm_dbd_txt_c_all'));
  349. $aChartData['top'] = 3;
  350. $aChartData['clear_all'] = bx_process_output(_t('_adm_dbd_txt_c_clear_all'));
  351. $aChartData['icon_clear'] = $this->getChartIcon('trash');
  352. return $bAsString ? $this->getChartJson($aChartData) : $aChartData;
  353. }
  354. /**
  355. * Queues that installed modules add to the Queues block. A module implements serviceGetDashboardQueues() and returns
  356. * [id => ['name' => lang key, 'icon' => Lucide name, 'all' => COUNT query, 'failed' => COUNT query (optional),
  357. * 'action' => DELETE query (optional: without it the queue is listed but cannot be cleared)]]; the ids are prefixed
  358. * with the module name here, so 'clear_queue' can only ever run a query a module registered.
  359. */
  360. protected function getModulesQueues()
  361. {
  362. $aResult = array();
  363. $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'modules'));
  364. foreach($aModules as $aModule) {
  365. if(empty($aModule['name']) || !bx_is_srv($aModule['name'], 'get_dashboard_queues'))
  366. continue;
  367. $aQueues = bx_srv($aModule['name'], 'get_dashboard_queues');
  368. if(!is_array($aQueues))
  369. continue;
  370. foreach($aQueues as $sId => $aQueue) {
  371. if(!is_array($aQueue) || empty($aQueue['name']) || empty($aQueue['all']))
  372. continue;
  373. $aResult[$aModule['name'] . '_' . $sId] = array_merge(array('icon' => 'list', 'module' => $aModule['name']), $aQueue);
  374. }
  375. }
  376. return $aResult;
  377. }
  378. /**
  379. * The Queues block's list: a row per queue with its icon, the number of queued jobs, a note when some failed and a clear
  380. * button where the queue can be cleared; the headline totals the queued and failed jobs. No share bar (dashboard.js showChart).
  381. */
  382. protected function getQueuesChartData($bAsString = true)
  383. {
  384. $oDb = BxDolDb::getInstance();
  385. $aItems = array();
  386. $iQueued = $iFailed = 0;
  387. foreach($this->aItemsQueues as $sId => $aQueue) {
  388. $iAll = !empty($aQueue['all']) ? (int)$oDb->getOne($aQueue['all']) : 0;
  389. $iFail = !empty($aQueue['failed']) ? (int)$oDb->getOne($aQueue['failed']) : 0;
  390. $iQueued += $iAll;
  391. $iFailed += $iFail;
  392. $sName = _t($aQueue['name']);
  393. $aItems[] = array(
  394. 'name' => bx_process_output($sName),
  395. 'bytes' => $iAll,
  396. 'size' => $iAll,
  397. 'note' => $iFail > 0 ? bx_process_output(_t('_adm_dbd_txt_q_failed', $iFail)) : '',
  398. 'note_alert' => true, // failed jobs: the note is a warning, not a count
  399. 'icon' => $this->getChartIcon($aQueue['icon']),
  400. 'type' => $sId,
  401. 'clear' => !empty($aQueue['action']) ? bx_process_output(_t('_adm_dbd_txt_q_clear_item', $sName)) : ''
  402. );
  403. }
  404. if(empty($aItems))
  405. return false;
  406. // The hero: the chart under its label, and the "clear all" button under the bar (dashboard.js showChart), like the Cache block's.
  407. $aChartData = array(
  408. 'items' => $aItems,
  409. 'used_bytes' => $iQueued, // the bar (one slice per queue) is scaled to the queued total
  410. 'label' => bx_process_output(_t('_adm_dbd_txt_q_all')),
  411. 'top' => 3, // the three busiest queues under the bar, like the Cache block's three largest caches
  412. 'top_zero' => true, // queues are usually empty; the line still shows the first counts
  413. 'clear_all' => bx_process_output(_t('_adm_dbd_txt_q_clear_all')),
  414. 'action' => 'clearQueue',
  415. 'used' => bx_process_output(_t('_adm_dbd_txt_q_queued', $iQueued)),
  416. 'total' => $iFailed > 0 ? bx_process_output(_t('_adm_dbd_txt_q_failed', $iFailed)) : '',
  417. 'total_fail' => $iFailed > 0,
  418. 'icon_clear' => $this->getChartIcon('trash')
  419. );
  420. return $bAsString ? $this->getChartJson($aChartData) : $aChartData;
  421. }
  422. /**
  423. * Chart data for the share charts: rows, the used size headline and, when known, the capacity the bar is scaled to.
  424. * Texts are HTML-escaped here because dashboard.js inserts them as HTML.
  425. */
  426. protected function getChartData($aItems, $iUsed, $fTotal = 0)
  427. {
  428. return array(
  429. 'items' => $aItems,
  430. 'used_bytes' => $iUsed,
  431. 'used' => bx_process_output(_t('_adm_dbd_txt_chart_used', _t_format_size($iUsed))),
  432. 'total_bytes' => $fTotal,
  433. 'total' => $fTotal > 0 ? bx_process_output(_t('_adm_dbd_txt_chart_total', _t_format_size($fTotal))) : ''
  434. );
  435. }
  436. /**
  437. * Chart rows from labelled sizes, biggest first: name (HTML-escaped, dashboard.js inserts it as HTML), bytes, formatted size, icon,
  438. * and, when given, the page the row links to and a note shown before the size (a file count).
  439. */
  440. protected function getChartItems($aItems)
  441. {
  442. usort($aItems, function($a, $b) {
  443. return $b['value'] <=> $a['value'];
  444. });
  445. $aResult = array();
  446. foreach($aItems as $aItem)
  447. $aResult[] = array(
  448. 'name' => bx_process_output(strip_tags($aItem['label'])),
  449. 'bytes' => $aItem['value'],
  450. 'size' => _t_format_size($aItem['value']),
  451. 'icon' => $aItem['icon'],
  452. 'url' => isset($aItem['url']) ? $aItem['url'] : '',
  453. 'note' => isset($aItem['note']) ? bx_process_output($aItem['note']) : ''
  454. );
  455. return $aResult;
  456. }
  457. /**
  458. * Chart data as a JSON literal that is safe inside the block's inline <script>.
  459. */
  460. protected function getChartJson($aChartData)
  461. {
  462. return json_encode($aChartData, JSON_HEX_TAG | JSON_HEX_AMP);
  463. }
  464. /**
  465. * A chart row's icon: a module's own Studio icon by URL, or a Lucide glyph the chart draws on a gray tile.
  466. */
  467. protected function getChartIcon($sIcon, $bUrl = false)
  468. {
  469. if($bUrl)
  470. return array('url' => $sIcon);
  471. $oIconset = BxDolIconset::getObjectInstance('sys_lucide');
  472. $sSvg = $oIconset ? $oIconset->getIconHtml($sIcon) : false;
  473. return array('svg' => $sSvg !== false ? $sSvg : '');
  474. }
  475. }
  476. /** @} */