dev.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Zentao Dev Tools</title>
  7. <link id="zuiCSS" rel="stylesheet" href="./js/zui3/zui.zentao.css">
  8. <script id="zuiJS" src="./js/zui3/zui.zentao.js"></script>
  9. <style>
  10. .dark {--zt-page-bg: var(--color-gray-50)}
  11. .dtable {--dtable-header-bg: var(--color-special-50); --dtable-border-color: rgba(var(--color-fore-rgb), .1)}
  12. #sqlDetailsTable .dtable-cell[data-col="Query"]:hover {background-color: var(--color-canvas); z-index: 10; height: auto!important; outline: 1px solid var(--color-gray-300); box-shadow: var(--shadow-lg); min-height: 48px!important}
  13. #sqlDetailsTable .dtable-cell[data-col="Query"]:hover .dtable-cell-html {overflow: visible!important;}
  14. #sqlDetailsTable .dtable-cell[data-col="Query"]:hover .dtable-cell-html .whitespace-nowrap {white-space: normal!important;}
  15. </style>
  16. </head>
  17. <body>
  18. <div class="mx-4 mt-4 space-y-2">
  19. <div class="row items-center gap-4">
  20. <h1>Zentao Profiles</h1>
  21. <div class="switch mt-1">
  22. <input type="checkbox" id="autoRefresh">
  23. <label for="autoRefresh">Auto Refresh</label>
  24. </div>
  25. <div class="flex-auto"></div>
  26. <div class="switch mt-1">
  27. <input type="checkbox" id="themeMode">
  28. <label for="themeMode"><i class="icon icon-moon" style="position: absolute; left: 16px; top: 4px; font-size: 12px; opacity: 0.5;"></i></label>
  29. </div>
  30. </div>
  31. <div class="row items-center gap-3 flex-wrap" id="queryForm">
  32. <div class="input-control has-prefix w-32" style="--input-control-prefix: 40px">
  33. <input id="identifier" type="text" class="form-control text-special" placeholder="git name">
  34. <label for="identifier" class="input-control-prefix">User</label>
  35. </div>
  36. <div class="input-control has-prefix w-36" style="--input-control-prefix: 60px">
  37. <input id="request_module" type="text" class="form-control text-special" placeholder="name">
  38. <label for="request_module" class="input-control-prefix">Module</label>
  39. </div>
  40. <div class="input-control has-prefix w-36" style="--input-control-prefix: 60px">
  41. <input id="request_method" type="text" class="form-control text-special" placeholder="name">
  42. <label for="request_method" class="input-control-prefix">Method</label>
  43. </div>
  44. <div class="input-control has-prefix" style="--input-control-prefix: 48px; width: 160px">
  45. <input id="timestamp__gt" type="date" class="form-control text-special" placeholder="">
  46. <label for="timestamp__gt" class="input-control-prefix">Begin</label>
  47. </div>
  48. <div class="input-control has-prefix" style="--input-control-prefix: 40px; width: 152px">
  49. <input id="timestamp__lt" type="date" class="form-control text-special" placeholder="">
  50. <label for="timestamp__lt" class="input-control-prefix">End</label>
  51. </div>
  52. <div class="input-control has-prefix" style="--input-control-prefix: 40px; width: 116px">
  53. <input id="requestId" type="text" class="form-control text-special" placeholder="Request ID">
  54. <label for="requestId" class="input-control-prefix">RID</label>
  55. </div>
  56. <div class="input-control has-prefix flex-auto" style="--input-control-prefix: 60px">
  57. <input id="params" type="text" class="form-control text-special" placeholder="e.g. metrics.backend.totalTime__gt=300&__limit=100">
  58. <label for="params" class="input-control-prefix">Params</label>
  59. </div>
  60. <button type="button" class="btn special" onclick="queryData()"><i class="icon icon-search"></i> Query <kbd class="code light-outline text-canvas text-sm opacity-50 shadow">↵</kbd></button>
  61. </div>
  62. <div class="load-indicator loading" id="table" style="height: calc(100vh - 116px);"></div>
  63. </div>
  64. <div style="display: none">
  65. <?php
  66. $config = new stdClass();
  67. include '../config/config.php';
  68. include '../config/my.php';
  69. if(isset($config->zinTool['guardApi']))
  70. {
  71. /* The next line is to support for using this php file as pure html file. */
  72. echo '<' . 'script' . ">window.guardApi = " . json_encode($config->zinTool['guardApi']) . ';window.guardUser = ' . json_encode($config->zinTool['author']) . ';' . '<' . '/script' . '>';
  73. }
  74. ?>
  75. </div>
  76. <script>
  77. $('#themeMode').on('change', () =>
  78. {
  79. const isDark = $('#themeMode').is(':checked');
  80. $('body').toggleClass('dark', isDark);
  81. localStorage.setItem('themeMode', isDark ? 'dark' : 'light');
  82. });
  83. const localThemeMode = localStorage.getItem('themeMode');
  84. if(localThemeMode === 'dark') $('#themeMode').trigger('click');
  85. else if(localThemeMode !== 'light' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) $('#themeMode').trigger('click');
  86. window.userMap = null;
  87. let table = null;
  88. let autoRefreshTimer = 0;
  89. let autoRefreshInterval = 10000;
  90. const $table = $('#table');
  91. const metricsLevelNames = ['', 'BLOCK', 'WARN', 'PASS'];
  92. const metricsStats = {warning: 0, danger: 0};
  93. const phpErrTypes = {'1': 'error', '2': 'warning', '4': 'parse', '8': 'notice', '16': 'core-error', '32': 'core-warning', '64': 'compile-error', '128': 'compile-warning', '256': 'user-error', '512': 'user-warning', '1024': 'user-notice', '2048': 'strict', '4096': 'recoverable-error', '8192': 'deprecated', '16384': 'user-deprecated', '32767': 'all'};
  94. function getPhpErrType(phpErr)
  95. {
  96. if(typeof phpErr === 'number') phpErr = phpErrTypes[phpErr];
  97. return phpErr;
  98. }
  99. function getErrorType(phpErr) // 'error' | 'warning' | 'info'
  100. {
  101. phpErr = getPhpErrType(phpErr);
  102. if(phpErr.includes('error')) return 'error';
  103. if(phpErr.includes('warning')) return 'warning';
  104. return 'info';
  105. }
  106. function renderErrorDetail(error, index)
  107. {
  108. const phpErrType = getPhpErrType(error.level || error.type);
  109. const errorType = getErrorType(phpErrType);
  110. const classType = errorType === 'info' ? 'secondary' : errorType;
  111. return [
  112. `<details class="border rounded-lg border-${classType}" open>`,
  113. `<summary class="${classType}-pale rounded-lg rounded-b-none px-2 py-1">#${index + 1} <span class="label ${classType} rounded-full size-sm mx-1">${phpErrType.toUpperCase()}</span> <strong>${error.message}</strong></summary>`,
  114. `<div class="text-sm opacity-50 px-2 border-b py-1"><i class="icon icon-file"></i> ${error.file} <strong>Line ${error.line}</strong></div>`,
  115. `<pre>${error.trace}</pre>`,
  116. '</details>',
  117. ].join('\n');
  118. }
  119. function getQueryUrl()
  120. {
  121. const search = new URLSearchParams($('#params').val());
  122. const params = ['identifier', 'request.module', 'request.method', 'timestamp__gt', 'timestamp__lt', 'requestId'].forEach((key) =>
  123. {
  124. let val = $(`#${key.replace(/\./g, '_')}`).val();
  125. if(typeof val === 'string' && val.length)
  126. {
  127. if(key === 'timestamp__gt') val = zui.createDate(zui.formatDate(val, 'yyyy-MM-dd 00:00:00')).getTime();
  128. else if(key === 'timestamp__lt') val = zui.createDate(zui.formatDate(val, 'yyyy-MM-dd 23:59:59')).getTime();
  129. search.set(key, val);
  130. }
  131. });
  132. if(!search.has('__sort')) search.set('__sort', '-timestamp');
  133. if(!search.has('__limit')) search.set('__limit', 200);
  134. const searchString = search.toString();
  135. history.pushState(null, null, `${location.origin}${location.pathname}?${searchString}`);
  136. return `${window.guardApi}?${searchString}`;
  137. }
  138. function loadData()
  139. {
  140. return fetch(getQueryUrl()).then(response => response.json());
  141. }
  142. function getTimeClass(time, danger = 400, warning = 100)
  143. {
  144. if (time < warning) return '';
  145. if (time < danger) return 'warning';
  146. return 'danger';
  147. }
  148. function initTable(data)
  149. {
  150. const cols =
  151. [
  152. {name: 'metricsLevel', title: 'LEVEL', hint: true, width: 60, fixed: 'left', cellClass: 'font-mono text-sm select-all', map: metricsLevelNames, hint: info => info.row.data.requestId, flex: 0, align: 'center', sort: 'number', border: 'right'},
  153. {name: 'identifier', type: Object.keys(window.userMap || {}).length ? 'avatarName' : '', title: 'User', fixed: 'left', flex: 0, width: 80, sort: true, avatarKey: 'identifier_avatar', avatarCodeKey: 'identifier_avatar_code', avatarNameKey: 'identifier_name'},
  154. {name: 'path', type: 'title', title: 'Page', hint: (info) => info.row.data.request.url, link: (info) => ({url: info.row.data.request.url, target: '_blank'}), flex: 1, fixed: 'left', maxWidth: 1000, sort: true},
  155. {name: 'metrics.backend.totalTime', title: 'Back Time', sort: 'number', digits: 1, format: '{0}ms', headerGroup: 'Backend', align: 'right', hint: info =>
  156. {
  157. const timeClass = info.row.data['metrics.backend.totalTimeClass'];
  158. if(timeClass === 'danger') return `BLOCK: > 500ms`;
  159. if(timeClass === 'warning') return `WARN: > 300ms`;
  160. }},
  161. {name: 'metrics.backend.sqlTime', title: 'SQL Time', sort: 'number', format: '{0}ms', digits: 1, headerGroup: 'Backend', align: 'right', hint: info =>
  162. {
  163. const timeClass = info.row.data['metrics.backend.sqlTimeClass'];
  164. if(timeClass === 'danger') return `BLOCK: > 300ms`;
  165. if(timeClass === 'warning') return `WARN: > 200ms`;
  166. }},
  167. {name: 'metrics.backend.sqlCount', title: 'SQLs', sort: 'number', width: 48, link: '#', hint: 'Click to check details', headerGroup: 'Backend', align: 'center'},
  168. {name: 'metrics.backend.sqlDetails.Explain', title: 'Explain', sort: 'number', width: 48, link: '#', hint: 'Click to check explain details', headerGroup: 'Backend', align: 'center'},
  169. {name: 'request.errorCount', title: 'Errors', sort: 'number', width: 48, link: '#', hint: 'Click to check details', headerGroup: 'Backend', align: 'center'},
  170. {name: 'metrics.backend.requestMemory', title: 'Memory', sort: 'number', format: (value) => value ? zui.formatBytes(value) : '', headerGroup: 'Backend', align: 'right'},
  171. {name: 'metrics.backend.phpFileLoaded', title: 'PHP Files', width: 50, sort: 'number', align: 'center', headerGroup: 'Backend'},
  172. {name: 'metrics.frontend.downloadSize', title: 'Transfer Size', sort: 'number', format: (value) => value ? zui.formatBytes(value) : '', headerGroup: 'Frontend', align: 'right'},
  173. {name: 'metrics.frontend.renderTime', title: 'Front Time', sort: 'number', format: '{0}ms', headerGroup: 'Frontend', border: 'left', align: 'right', hint: info =>
  174. {
  175. const timeClass = info.row.data['metrics.frontend.renderTimeClass'];
  176. if(timeClass === 'danger') return `BLOCK: > 100ms`;
  177. if(timeClass === 'warning') return `WARN: > 60ms`;
  178. }},
  179. {name: 'userEnv.browser', title: 'Client Browser', sort: true, align: 'left', headerGroup: 'Frontend', hidden: true},
  180. {name: 'userEnv.system', title: 'Client OS', sort: true, align: 'center', headerGroup: 'Frontend', hidden: true},
  181. {name: 'request.php', title: 'PHP', hint: true, width: 40, align: 'center'},
  182. {name: 'request.xhprof', title: 'Xhprof', link: (info) => ({url: info.row.data.request.xhprof || '#', target: '_blank'}), format: (value) => (value ? 'Open' : ''), hint: true, width: 40},
  183. {name: 'requestId', title: 'RID', hint: true, width: 80, cellClass: 'font-mono text-sm select-all', hint: info => info.row.data.requestId, flex: 0, align: 'left', fixed: 'right'},
  184. {name: 'timestamp', title: 'Date', type: 'datetime', hint: (info) => zui.formatDate(info.row.data.timestamp, 'yyyy-MM-dd hh:mm:ss'), width: 120, sort: 'number', fixed: 'right'},
  185. ];
  186. table = new zui.DTable('#table',
  187. {
  188. className: 'ring ring-gray shadow rounded',
  189. plugins: ['sort', 'sort-col', 'zentao', 'resize'],
  190. width: '100%',
  191. height: '100%',
  192. fixedLeftWidth: 300,
  193. minColWidth: 40,
  194. hoverCol: true,
  195. sort: true,
  196. sortBy: {name: 'timestamp', order: 'desc'},
  197. colResize: true,
  198. rowKey: 'requestId',
  199. footer: [function(result, layout) {
  200. return [
  201. {html: `Total <strong>${layout.allRows.length}</strong>`, className: 'text-gray mr-4'},
  202. metricsStats.danger ? {html: `<div class="font-bold row items-center gap-2 rounded-full px-2 danger" data-toggle="tooltip" data-type="danger" data-title="Black Time &gt; 500ms or SQL time &gt; 300ms or Front time &gt; 100ms" data-placement="top-start"><i class="icon icon-alert"></i>BLOCK <strong>${metricsStats.danger}</strong> </div>`, className: 'text-danger mr-4'} : null,
  203. metricsStats.warning ? {html: `<div class="font-bold row items-center gap-2 rounded-full px-2 warning-pale" data-toggle="tooltip" data-type="warning" data-title="Black Time &gt; 300ms or SQL time &gt; 200ms or Front time &gt; 60ms" data-placement="top-start"><i class="icon icon-alert"></i>WARN <strong>${metricsStats.warning}</strong> </div>`, className: 'text-warning mr-4'} : null,
  204. ];
  205. }],
  206. cols: cols,
  207. data: data,
  208. onRenderCell: function(result, info)
  209. {
  210. const colName = info.col.name;
  211. const rowData = info.row.data;
  212. if(colName === 'metrics.backend.totalTime' && rowData['metrics.backend.totalTimeClass']) result.push({root: true, className: `text-${rowData['metrics.backend.totalTimeClass']}${rowData['metrics.backend.totalTimeClass'] === 'danger' ? ' font-bold bg-danger': ''}`});
  213. if(colName === 'metrics.backend.sqlTime' && rowData['metrics.backend.sqlTimeClass']) result.push({root: true, className: `text-${rowData['metrics.backend.sqlTimeClass']}${rowData['metrics.backend.sqlTimeClass'] === 'danger' ? ' font-bold bg-danger': ''}`});
  214. if(colName === 'request.errorCount' && rowData['request.errorCountClass']) result.push({root: true, className: `text-${rowData['request.errorCountClass']}${rowData['request.errorCountClass'] === 'danger' ? ' font-bold bg-danger': ''}`});
  215. if(colName === 'metrics.frontend.renderTime' && rowData['metrics.frontend.renderTimeClass']) result.push({root: true, className: `text-${rowData['metrics.frontend.renderTimeClass']}${rowData['metrics.frontend.renderTimeClass'] === 'danger' ? ' font-bold bg-danger': ''}`});
  216. else if(colName === 'metricsLevel' && rowData.metricsLevel < 3) result.push({root: true, className: rowData.metricsClass === 'danger' ? 'font-bold bg-danger bg-opacity-100 text-canvas' : 'font-bold bg-warning text-warning bg-opacity-20'});
  217. if(rowData.metricsClass) result.push({root: true, className: `bg-${rowData.metricsClass} bg-opacity-${rowData.metricsClass === 'danger' ? 40 : 10}`});
  218. return result;
  219. },
  220. onCellClick: function(event, info)
  221. {
  222. const data = info.rowInfo.data;
  223. if(info.colName === 'metrics.backend.sqlCount')
  224. {
  225. const sqlDetails = data.metrics.backend.sqlDetails;
  226. const oldVersion = !data.dataVer || data.dataVer < 2;
  227. if(!sqlDetails || !sqlDetails.length) return;
  228. zui.Modal.showError({
  229. title: `SQL Details (${sqlDetails.length})`,
  230. html: true,
  231. size: 'lg',
  232. mono: false,
  233. error: '<div id="sqlDetailsTable"></div>',
  234. onShown() {
  235. zui.create('dtable', '#sqlDetailsTable', {
  236. cols: [
  237. {name: 'Query_ID', title: 'ID', width: 48, sort: 'number'},
  238. {name: 'Duration', title: 'Duration (ms)', align: 'center', width: 112, format: (val) => val * (oldVersion ? 1000 : 1), sort: 'number'},
  239. {name: 'Query', title: 'Query', flex: 1, onRenderCell: (result, info) => {
  240. const detail = info.row.data;
  241. result[0] = {html: `<div class="whitespace-nowrap font-mono select-all">${detail.Query}</div>${detail.Code ? `<div class="text-primary font-mono select-all copy-on-click cursor-pointer">${detail.Code}</div>` : ''}`, className: 'text-sm h-full py-1'};
  242. return result;
  243. }},
  244. ],
  245. rowHeight: 48,
  246. headerHeight: 32,
  247. data: sqlDetails,
  248. sort: true,
  249. rowHover: true,
  250. sortBy: {name: 'Duration', order: 'desc'},
  251. plugins: ['sort'],
  252. });
  253. },
  254. });
  255. return;
  256. }
  257. if(info.colName === 'request.errorCount' && data.request.errorCount)
  258. {
  259. zui.Modal.showError({
  260. title: `Errors (${data.request.errorCount})`,
  261. html: true,
  262. size: 'lg',
  263. mono: false,
  264. error: [
  265. '<div class="space-y-2">',
  266. data.request.errors.map(renderErrorDetail).join('\n'),
  267. '</div>'
  268. ].join('\n'),
  269. });
  270. return;
  271. }
  272. if (info.colName === 'metrics.backend.sqlDetails.Explain')
  273. {
  274. const explains = data.metrics.backend.sqlDetails.filter(x => x.Explain && x.Explain.length);
  275. zui.Modal.showError({
  276. title: `Explain Details (${explains.length})`,
  277. html: true,
  278. size: 'lg',
  279. mono: false,
  280. error: [
  281. '<table class="table w-full canvas ring shadow rounded-lg">',
  282. '<thead class="sticky" style="top: -2px">',
  283. '<tr>',
  284. '<th>id</th>',
  285. '<th>Extra</th>',
  286. '<th>Key</th>',
  287. '<th>Key Len</th>',
  288. '<th>Possible Keys</th>',
  289. '<th>Ref</th>',
  290. '<th>Rows</th>',
  291. '<th>Select Type</th>',
  292. '<th>Table</th>',
  293. '<th>Type</th>',
  294. '</tr>',
  295. '</thead>',
  296. '<tbody>',
  297. explains.map((profile => [
  298. '<tr class="primary-pale">',
  299. `<td colspan="10">#${profile.Query_ID} <strong>${profile.Query}</strong> (${profile.Duration}ms)</td>`,
  300. '</tr>',
  301. profile.Explain.map(explain => [
  302. '<tr>',
  303. `<td>${explain.id}</td>`,
  304. `<td>${explain.Extra}</td>`,
  305. `<td>${explain.key}</td>`,
  306. `<td>${explain.key_len}</td>`,
  307. `<td>${explain.possible_keys}</td>`,
  308. `<td>${explain.ref}</td>`,
  309. `<td>${explain.rows}</td>`,
  310. `<td>${explain.select_type}</td>`,
  311. `<td>${explain.table}</td>`,
  312. `<td>${explain.type}</td>`,
  313. '</tr>',
  314. ].join('\n')).join('\n'),
  315. ].join('\n'))).join('\n'),
  316. '</tbody>',
  317. '</table>'
  318. ].join('\n'),
  319. });
  320. }
  321. console.log('> clicked', data);
  322. }
  323. });
  324. window.table = table;
  325. }
  326. function initData(data)
  327. {
  328. metricsStats.danger = 0;
  329. metricsStats.warning = 0;
  330. data.forEach(row =>
  331. {
  332. row.path = `${row.request.module}-${row.request.method}`;
  333. row['metrics.backend.totalTime'] = row.metrics.backend.totalTime;
  334. row['metrics.backend.sqlCount'] = row.metrics.backend.sqlCount;
  335. row['metrics.backend.sqlDetails.Explain'] = row.metrics.backend.sqlDetails.filter(x => x.Explain && x.Explain.length).length;
  336. row['metrics.backend.sqlTime'] = row.metrics.backend.sqlTime * ((!row.dataVer || row.dataVer < 2) ? 1000 : 1);
  337. row['metrics.backend.requestMemory'] = row.metrics.backend.requestMemory * 1024;
  338. row['metrics.backend.phpFileLoaded'] = row.metrics.backend.phpFileLoaded;
  339. row['metrics.frontend.renderTime'] = row.metrics.frontend.renderTime;
  340. row['metrics.frontend.downloadSize'] = row.metrics.frontend.downloadSize;
  341. row['userEnv.browser'] = row.userEnv.browser;
  342. row['userEnv.system'] = row.userEnv.system;
  343. row['request.xhprof'] = row.request.xhprof;
  344. row['request.php'] = row.request.php;
  345. row['request.errorCount'] = row.request.errorCount;
  346. const totalTimeClass = getTimeClass(row.metrics.backend.totalTime || 0, 500, 300);
  347. const sqlTimeClass = getTimeClass(row.metrics.backend.sqlTime || 0, 300, 200);
  348. const renderTimeClass = getTimeClass(row.metrics.frontend.renderTime || 0, 100, 60);
  349. const errorCountClass = row.request.errorCount ? 'danger' : '';
  350. const classList = [totalTimeClass, sqlTimeClass, renderTimeClass, errorCountClass];
  351. row['metrics.backend.totalTimeClass'] = totalTimeClass;
  352. row['metrics.backend.sqlTimeClass'] = sqlTimeClass;
  353. row['metrics.frontend.renderTimeClass'] = renderTimeClass;
  354. row['request.errorCountClass'] = renderTimeClass;
  355. row.metricsClass = classList.includes('danger') ? 'danger' : (classList.includes('warning') ? 'warning' : '');
  356. row.metricsLevel = classList.includes('danger') ? 1 : (classList.includes('warning') ? 2 : 3);
  357. if(window.userMap)
  358. {
  359. const user = window.userMap[row.identifier.toLowerCase()];
  360. if(user)
  361. {
  362. row.identifier_avatar = user.avatar;
  363. row.identifier_avatar_code = user.id;
  364. row.identifier_name = user.realname || user.account;
  365. }
  366. }
  367. if(row.metricsClass) metricsStats[row.metricsClass]++;
  368. return row;
  369. });
  370. return data;
  371. }
  372. function queryData(options)
  373. {
  374. options = options || {};
  375. if(!options.noLoading) $table.addClass('loading');
  376. loadData().then(data =>
  377. {
  378. $table.removeClass('loading');
  379. data = initData(data);
  380. if(table) return table.render({data});
  381. initTable(data);
  382. }).catch(error => {
  383. $table.removeClass('loading');
  384. zui.Modal.showError({error: error});
  385. console.error(error);
  386. });
  387. }
  388. function getAPI()
  389. {
  390. if(window.guardApi) return Promise.resolve(window.guardApi);
  391. return fetch('./dev.json').then(response => response.json()).then(json => {
  392. window.guardApi = json.api;
  393. if(json.user) window.guardUser = json.user;
  394. if(json.userMap) window.userMap = json.userMap;
  395. return json.api;
  396. });
  397. }
  398. function setAutoRefreshTimer()
  399. {
  400. if(autoRefreshTimer)
  401. {
  402. clearTimeout(autoRefreshTimer);
  403. autoRefreshTimer = 0;
  404. }
  405. if($('#autoRefresh').is(':checked'))
  406. {
  407. const interval = autoRefreshInterval || 10000;
  408. const tryLoad = () => {
  409. if($table.hasClass('loading')) return;
  410. queryData({noLoading: true});
  411. };
  412. autoRefreshTimer = setInterval(tryLoad, interval);
  413. tryLoad();
  414. $('#autoRefresh+label').text(`Auto Refresh (every ${(interval/1000)}s)`);
  415. }
  416. else $('#autoRefresh+label').text('Auto Refresh');
  417. }
  418. ((function()
  419. {
  420. const search = new URLSearchParams(window.location.search);
  421. const params = [];
  422. const keyMap = {
  423. user : 'identifier',
  424. module: 'request.module',
  425. method: 'request.method',
  426. begin : 'timestamp__gt',
  427. end : 'timestamp__lt',
  428. rid : 'requestId',
  429. };
  430. for (const entry of search)
  431. {
  432. const key = keyMap[entry[0]] || entry[0];
  433. const value = entry[1];
  434. if(key === 'api')
  435. {
  436. window.guardApi = value;
  437. }
  438. else if(key === 'autoRefresh')
  439. {
  440. autoRefreshInterval = +value;
  441. if(autoRefreshInterval) $('#autoRefresh').prop('checked', true);
  442. }
  443. else if(key === 'date')
  444. {
  445. const date = zui.createDate(value);
  446. $('#timestamp__gt').val(zui.formatDate(date, 'yyyy-M-d'));
  447. $('#timestamp__lt').val(zui.formatDate(date, 'yyyy-M-d'));
  448. }
  449. else if(key === 'params')
  450. {
  451. params.push(value);
  452. }
  453. else
  454. {
  455. const $ele = $(`#${key.replace(/\./g, '_')}`);
  456. if($ele.length)
  457. {
  458. $ele.val(key.startsWith('timestamp__') ? zui.formatDate(+value, 'yyyy-M-d') : value);
  459. }
  460. else params.push(`${key}=${encodeURIComponent(value)}`);
  461. }
  462. $('#params').val(params.join('&'));
  463. }
  464. const now = new Date();
  465. if(!$('#timestamp__gt').val()) $('#timestamp__gt').val(zui.formatDate(now, 'yyyy-M-d'));
  466. if(!$('#timestamp__lt').val()) $('#timestamp__lt').val(zui.formatDate(now, 'yyyy-M-d'));
  467. getAPI().then(() =>
  468. {
  469. if(window.guardUser && !$('#identifier').val()) $('#identifier').val(window.guardUser);
  470. queryData();
  471. });
  472. $('#queryForm').on('keyup', (event) => {if(event.key === 'Enter') queryData();});
  473. $('#timestamp__lt').on('change', event =>
  474. {
  475. const endDate = zui.createDate(event.target.value);
  476. const beginDate = zui.createDate($('#timestamp__gt').val());
  477. if(endDate < beginDate) $('#timestamp__gt').val(event.target.value);
  478. });
  479. $('#timestamp__gt').on('change', event =>
  480. {
  481. const beginDate = zui.createDate(event.target.value);
  482. const endDate = zui.createDate($('#timestamp__lt').val());
  483. if(beginDate > endDate) $('#timestamp__lt').val(event.target.value);
  484. });
  485. $('#autoRefresh').on('change', setAutoRefreshTimer);
  486. $(document).on('click', '.copy-on-click', function()
  487. {
  488. const text = $(this).text();
  489. navigator.clipboard.writeText(text);
  490. zui.Messager.show({type: 'success', content: 'Copied!'});
  491. });
  492. setAutoRefreshTimer();
  493. })());
  494. </script>
  495. </body>
  496. </html>