nestjs-profiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +123 -0
  2. package/dist/libs/nestjs-profiler/package.json +34 -0
  3. package/dist/libs/nestjs-profiler/src/analyzers/explain-analyzer.d.ts +5 -0
  4. package/dist/libs/nestjs-profiler/src/analyzers/explain-analyzer.js +39 -0
  5. package/dist/libs/nestjs-profiler/src/analyzers/explain-analyzer.js.map +1 -0
  6. package/dist/libs/nestjs-profiler/src/assets/favicon.ico +0 -0
  7. package/dist/libs/nestjs-profiler/src/assets/logo.png +0 -0
  8. package/dist/libs/nestjs-profiler/src/collectors/cache-collector.d.ts +18 -0
  9. package/dist/libs/nestjs-profiler/src/collectors/cache-collector.js +178 -0
  10. package/dist/libs/nestjs-profiler/src/collectors/cache-collector.js.map +1 -0
  11. package/dist/libs/nestjs-profiler/src/collectors/log-collector.d.ts +12 -0
  12. package/dist/libs/nestjs-profiler/src/collectors/log-collector.js +68 -0
  13. package/dist/libs/nestjs-profiler/src/collectors/log-collector.js.map +1 -0
  14. package/dist/libs/nestjs-profiler/src/collectors/mongo-collector.d.ts +19 -0
  15. package/dist/libs/nestjs-profiler/src/collectors/mongo-collector.js +203 -0
  16. package/dist/libs/nestjs-profiler/src/collectors/mongo-collector.js.map +1 -0
  17. package/dist/libs/nestjs-profiler/src/collectors/mysql-collector.d.ts +12 -0
  18. package/dist/libs/nestjs-profiler/src/collectors/mysql-collector.js +131 -0
  19. package/dist/libs/nestjs-profiler/src/collectors/mysql-collector.js.map +1 -0
  20. package/dist/libs/nestjs-profiler/src/collectors/postgres-collector.d.ts +15 -0
  21. package/dist/libs/nestjs-profiler/src/collectors/postgres-collector.js +205 -0
  22. package/dist/libs/nestjs-profiler/src/collectors/postgres-collector.js.map +1 -0
  23. package/dist/libs/nestjs-profiler/src/common/profiler-options.interface.d.ts +20 -0
  24. package/dist/libs/nestjs-profiler/src/common/profiler-options.interface.js +3 -0
  25. package/dist/libs/nestjs-profiler/src/common/profiler-options.interface.js.map +1 -0
  26. package/dist/libs/nestjs-profiler/src/common/profiler.model.d.ts +67 -0
  27. package/dist/libs/nestjs-profiler/src/common/profiler.model.js +3 -0
  28. package/dist/libs/nestjs-profiler/src/common/profiler.model.js.map +1 -0
  29. package/dist/libs/nestjs-profiler/src/controllers/profiler.controller.d.ts +33 -0
  30. package/dist/libs/nestjs-profiler/src/controllers/profiler.controller.js +237 -0
  31. package/dist/libs/nestjs-profiler/src/controllers/profiler.controller.js.map +1 -0
  32. package/dist/libs/nestjs-profiler/src/index.d.ts +5 -0
  33. package/dist/libs/nestjs-profiler/src/index.js +22 -0
  34. package/dist/libs/nestjs-profiler/src/index.js.map +1 -0
  35. package/dist/libs/nestjs-profiler/src/interceptors/request-profiler.interceptor.d.ts +11 -0
  36. package/dist/libs/nestjs-profiler/src/interceptors/request-profiler.interceptor.js +104 -0
  37. package/dist/libs/nestjs-profiler/src/interceptors/request-profiler.interceptor.js.map +1 -0
  38. package/dist/libs/nestjs-profiler/src/middleware/profiler.middleware.d.ts +4 -0
  39. package/dist/libs/nestjs-profiler/src/middleware/profiler.middleware.js +21 -0
  40. package/dist/libs/nestjs-profiler/src/middleware/profiler.middleware.js.map +1 -0
  41. package/dist/libs/nestjs-profiler/src/profiler-logger.d.ts +12 -0
  42. package/dist/libs/nestjs-profiler/src/profiler-logger.js +61 -0
  43. package/dist/libs/nestjs-profiler/src/profiler-logger.js.map +1 -0
  44. package/dist/libs/nestjs-profiler/src/profiler.module.d.ts +7 -0
  45. package/dist/libs/nestjs-profiler/src/profiler.module.js +105 -0
  46. package/dist/libs/nestjs-profiler/src/profiler.module.js.map +1 -0
  47. package/dist/libs/nestjs-profiler/src/services/entity-explorer.service.d.ts +19 -0
  48. package/dist/libs/nestjs-profiler/src/services/entity-explorer.service.js +110 -0
  49. package/dist/libs/nestjs-profiler/src/services/entity-explorer.service.js.map +1 -0
  50. package/dist/libs/nestjs-profiler/src/services/profiler.service.d.ts +31 -0
  51. package/dist/libs/nestjs-profiler/src/services/profiler.service.js +218 -0
  52. package/dist/libs/nestjs-profiler/src/services/profiler.service.js.map +1 -0
  53. package/dist/libs/nestjs-profiler/src/services/route-explorer.service.d.ts +15 -0
  54. package/dist/libs/nestjs-profiler/src/services/route-explorer.service.js +93 -0
  55. package/dist/libs/nestjs-profiler/src/services/route-explorer.service.js.map +1 -0
  56. package/dist/libs/nestjs-profiler/src/services/template-builder.service.d.ts +27 -0
  57. package/dist/libs/nestjs-profiler/src/services/template-builder.service.js +349 -0
  58. package/dist/libs/nestjs-profiler/src/services/template-builder.service.js.map +1 -0
  59. package/dist/libs/nestjs-profiler/src/services/view.service.d.ts +17 -0
  60. package/dist/libs/nestjs-profiler/src/services/view.service.js +197 -0
  61. package/dist/libs/nestjs-profiler/src/services/view.service.js.map +1 -0
  62. package/dist/libs/nestjs-profiler/src/storage/in-memory-profiler-storage.d.ts +9 -0
  63. package/dist/libs/nestjs-profiler/src/storage/in-memory-profiler-storage.js +27 -0
  64. package/dist/libs/nestjs-profiler/src/storage/in-memory-profiler-storage.js.map +1 -0
  65. package/dist/libs/nestjs-profiler/src/storage/profiler-storage.interface.d.ts +6 -0
  66. package/dist/libs/nestjs-profiler/src/storage/profiler-storage.interface.js +3 -0
  67. package/dist/libs/nestjs-profiler/src/storage/profiler-storage.interface.js.map +1 -0
  68. package/dist/libs/nestjs-profiler/src/views/cache.html +50 -0
  69. package/dist/libs/nestjs-profiler/src/views/dashboard.html +73 -0
  70. package/dist/libs/nestjs-profiler/src/views/detail.html +66 -0
  71. package/dist/libs/nestjs-profiler/src/views/entities.html +76 -0
  72. package/dist/libs/nestjs-profiler/src/views/js/dashboard.js +24 -0
  73. package/dist/libs/nestjs-profiler/src/views/js/entities.js +37 -0
  74. package/dist/libs/nestjs-profiler/src/views/js/layout-config.js +15 -0
  75. package/dist/libs/nestjs-profiler/src/views/js/layout.js +143 -0
  76. package/dist/libs/nestjs-profiler/src/views/js/queries.js +28 -0
  77. package/dist/libs/nestjs-profiler/src/views/layout.html +252 -0
  78. package/dist/libs/nestjs-profiler/src/views/logs.html +37 -0
  79. package/dist/libs/nestjs-profiler/src/views/not_found.html +15 -0
  80. package/dist/libs/nestjs-profiler/src/views/partials/cache_row.html +20 -0
  81. package/dist/libs/nestjs-profiler/src/views/partials/detail_query_row.html +33 -0
  82. package/dist/libs/nestjs-profiler/src/views/partials/entity_row.html +39 -0
  83. package/dist/libs/nestjs-profiler/src/views/partials/log_row.html +19 -0
  84. package/dist/libs/nestjs-profiler/src/views/partials/metadata_sidebar.html +27 -0
  85. package/dist/libs/nestjs-profiler/src/views/partials/pagination.html +9 -0
  86. package/dist/libs/nestjs-profiler/src/views/partials/query_row.html +34 -0
  87. package/dist/libs/nestjs-profiler/src/views/partials/request_row.html +25 -0
  88. package/dist/libs/nestjs-profiler/src/views/partials/route_row.html +12 -0
  89. package/dist/libs/nestjs-profiler/src/views/queries.html +65 -0
  90. package/dist/libs/nestjs-profiler/src/views/routes.html +41 -0
  91. package/package.json +125 -0
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=profiler-storage.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiler-storage.interface.js","sourceRoot":"","sources":["../../../../../libs/nestjs-profiler/src/storage/profiler-storage.interface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
2
+ <div class="px-6 py-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
3
+ <div class="flex items-center space-x-4">
4
+ <h2 class="text-lg font-semibold text-gray-800">Cache Operations</h2>
5
+ <div class="relative">
6
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
7
+ <svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
9
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
10
+ </svg>
11
+ </div>
12
+ <input id="search" name="search"
13
+ class="block w-64 pl-9 pr-3 py-1.5 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
14
+ placeholder="Search cache..." type="search">
15
+ </div>
16
+ </div>
17
+ <div class="text-sm text-gray-500">
18
+ Total: <span class="font-bold text-gray-800">{{ totalCacheOps }}</span>
19
+ </div>
20
+ </div>
21
+ <div class="overflow-x-auto">
22
+ <table class="min-w-full divide-y divide-gray-200">
23
+ <thead class="bg-gray-50">
24
+ <tr>
25
+ <th scope="col"
26
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Operation
27
+ </th>
28
+ <th scope="col"
29
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Key</th>
30
+ <th scope="col"
31
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Result
32
+ </th>
33
+ <th scope="col"
34
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Store
35
+ </th>
36
+ <th scope="col"
37
+ class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Duration
38
+ </th>
39
+ <th scope="col"
40
+ class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Time
41
+ </th>
42
+ </tr>
43
+ </thead>
44
+ <tbody class="bg-white divide-y divide-gray-200">
45
+ {{{ rows }}}
46
+ </tbody>
47
+ </table>
48
+ </div>
49
+ {{{ emptyState }}}
50
+ </div>
@@ -0,0 +1,73 @@
1
+
2
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
3
+ <div class="p-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
4
+ <div class="flex items-center space-x-4">
5
+ <h2 class="font-semibold text-gray-700">HTTP Requests</h2>
6
+ <div class="relative">
7
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
8
+ <svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
10
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
11
+ </svg>
12
+ </div>
13
+ <input id="search" name="search"
14
+ class="block w-64 pl-9 pr-3 py-1.5 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
15
+ placeholder="Search requests..." type="search">
16
+ </div>
17
+ </div>
18
+ <span class="text-xs text-gray-500 bg-white border border-gray-200 px-2 py-1 rounded-md">Last 100
19
+ requests</span>
20
+ </div>
21
+ <div class="overflow-x-auto">
22
+ <table class="w-full text-left border-collapse" id="requestsTable">
23
+ <thead class="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider font-medium">
24
+ <tr>
25
+ <th class="p-4 border-b border-gray-200 w-16">Method</th>
26
+ <th class="p-4 border-b border-gray-200">URI</th>
27
+ <th class="p-4 border-b border-gray-200 w-24">Status</th>
28
+ <th class="p-4 border-b border-gray-200 w-24">Duration</th>
29
+ <th class="p-4 border-b border-gray-200 w-24 text-center">Queries</th>
30
+ <th class="p-4 border-b border-gray-200 w-32 text-right cursor-pointer hover:bg-gray-100 select-none group"
31
+ onclick="toggleSort()">
32
+ <div class="flex items-center justify-end gap-1">
33
+ Happened
34
+ <div class="flex flex-col text-gray-400">
35
+ <svg id="sortAsc" class="w-2 h-2 opacity-100" fill="currentColor" viewBox="0 0 24 24">
36
+ <path d="M7 14l5-5 5 5z" />
37
+ </svg>
38
+ <svg id="sortDesc" class="w-2 h-2 opacity-50" fill="currentColor" viewBox="0 0 24 24">
39
+ <path d="M7 10l5 5 5-5z" />
40
+ </svg>
41
+ </div>
42
+ </div>
43
+ </th>
44
+ </tr>
45
+ </thead>
46
+ <tbody class="divide-y divide-gray-100" id="tableBody">
47
+ {{{ rows }}}
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ {{{ emptyState }}}
52
+
53
+ <script>
54
+ function toggleSort() {
55
+ const table = document.getElementById('tableBody');
56
+ const rows = Array.from(table.rows);
57
+ rows.reverse().forEach(row => table.appendChild(row));
58
+
59
+ const asc = document.getElementById('sortAsc');
60
+ const desc = document.getElementById('sortDesc');
61
+
62
+ if (asc.classList.contains('text-indigo-600')) {
63
+ asc.classList.remove('text-indigo-600', 'opacity-100'); asc.classList.add('opacity-50');
64
+ desc.classList.add('text-indigo-600', 'opacity-100'); desc.classList.remove('opacity-50');
65
+ } else {
66
+ asc.classList.add('text-indigo-600', 'opacity-100'); asc.classList.remove('opacity-50');
67
+ desc.classList.remove('text-indigo-600', 'opacity-100'); desc.classList.add('opacity-50');
68
+ }
69
+ }
70
+ </script>
71
+
72
+
73
+ </div>
@@ -0,0 +1,66 @@
1
+ <div class="mb-6 flex items-center justify-between">
2
+ <div>
3
+ <a href="/__profiler"
4
+ class="text-sm font-medium text-gray-500 hover:text-gray-700 flex items-center mb-2 transition-colors">
5
+ <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18">
7
+ </path>
8
+ </svg>
9
+ Back to Requests
10
+ </a>
11
+ <div class="flex items-center space-x-3">
12
+ <span class="px-3 py-1 rounded-md text-white text-sm font-bold tracking-wide {{{ statusColor }}}">{{ method
13
+ }}</span>
14
+ <h1 class="text-2xl font-bold text-gray-800 truncate max-w-2xl">{{ url }}</h1>
15
+ </div>
16
+ </div>
17
+ <div class="text-right">
18
+ <div class="text-sm text-gray-500 mb-1">Time</div>
19
+ <div class="text-base font-semibold text-gray-800">{{ timeAgo }}</div>
20
+ </div>
21
+ </div>
22
+
23
+ {{{ exceptionView }}}
24
+
25
+ <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
26
+ <!-- Main Content -->
27
+ <div class="lg:col-span-3">
28
+
29
+ {{{ timingBar }}}
30
+
31
+ <div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden mb-6">
32
+ <div class="border-b border-gray-200 bg-gray-50 px-4 py-3 flex justify-between items-center">
33
+ <h2 class="text-sm font-bold text-gray-600 uppercase tracking-widest">Request Details</h2>
34
+ </div>
35
+ <div class="p-0">
36
+ <div class="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-gray-200">
37
+ <!-- Headers -->
38
+ <div class="p-4">
39
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Headers</h3>
40
+ <div class="overflow-x-auto">
41
+ {{{ headersTable }}}
42
+ </div>
43
+ </div>
44
+ <!-- Body -->
45
+ <div class="p-4 bg-gray-50 md:bg-white">
46
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Request Body</h3>
47
+ {{{ bodyView }}}
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="bg-gray-50 rounded-lg border border-gray-200 p-4 mb-4">
54
+ <h2 class="text-sm font-bold text-gray-500 uppercase tracking-widest mb-4">Executed Queries ({{ queryCount
55
+ }})</h2>
56
+ {{{ queries }}}
57
+ </div>
58
+
59
+ {{{ cacheSection }}}
60
+ </div>
61
+
62
+ <!-- Sidebar: Meta Info -->
63
+ <div class="lg:col-span-1 space-y-4">
64
+ {{{ sidebar }}}
65
+ </div>
66
+ </div>
@@ -0,0 +1,76 @@
1
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
2
+ <div class="px-6 py-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
3
+ <div class="flex items-center space-x-4">
4
+ <h2 class="text-lg font-semibold text-gray-800">Entity Explorer</h2>
5
+ <div class="relative">
6
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
7
+ <svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
9
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
10
+ </svg>
11
+ </div>
12
+ <input id="search" name="search"
13
+ class="block w-64 pl-9 pr-3 py-1.5 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
14
+ placeholder="Search entities..." type="search">
15
+ </div>
16
+ </div>
17
+ <div class="text-sm text-gray-500">
18
+ Found <span class="font-bold text-gray-800">{{ totalEntities }}</span> entities
19
+ </div>
20
+ </div>
21
+
22
+ <div class="overflow-x-auto">
23
+ <table class="w-full text-left border-collapse">
24
+ <thead class="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider font-medium">
25
+ <tr>
26
+ <th class="p-4 border-b border-gray-200 w-1/4">Entity Name</th>
27
+ <th class="p-4 border-b border-gray-200 w-32">Type</th>
28
+ <th class="p-4 border-b border-gray-200 w-1/4">Database</th>
29
+ <th class="p-4 border-b border-gray-200 w-32">Connection</th>
30
+ <th class="p-4 border-b border-gray-200 w-1/4">Table/Collection</th>
31
+ <th class="p-4 border-b border-gray-200 w-24">Columns</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody class="divide-y divide-gray-100">
35
+ {{{ rows }}}
36
+ </tbody>
37
+ </table>
38
+ </div>
39
+
40
+ {{{ emptyState }}}
41
+ </div>
42
+
43
+ <script>
44
+ function toggleEntityRow(id) {
45
+ const row = document.getElementById(id);
46
+ const icon = document.getElementById('icon-' + id);
47
+ const contentDiv = document.getElementById('content-' + id);
48
+
49
+ if (!row || !icon) return;
50
+
51
+ // Toggle visibility
52
+ if (row.classList.contains('hidden')) {
53
+ row.classList.remove('hidden');
54
+ icon.classList.add('rotate-90');
55
+
56
+ // Populate if empty (lazy render)
57
+ if (contentDiv && contentDiv.children.length === 0) {
58
+ try {
59
+ const columns = JSON.parse(row.dataset.columns || '[]');
60
+ if (columns.length === 0) {
61
+ contentDiv.innerHTML = '<span class="text-gray-400 italic">No columns defined</span>';
62
+ } else {
63
+ contentDiv.innerHTML = columns.map(col =>
64
+ `<div class="bg-white px-2 py-1 rounded border border-gray-200 truncate" title="${col}">${col}</div>`
65
+ ).join('');
66
+ }
67
+ } catch (e) {
68
+ console.error('Error parsing columns', e);
69
+ }
70
+ }
71
+ } else {
72
+ row.classList.add('hidden');
73
+ icon.classList.remove('rotate-90');
74
+ }
75
+ }
76
+ </script>
@@ -0,0 +1,24 @@
1
+ // Dashboard interactions (requests list)
2
+ // Expose toggleSort globally because it's invoked via inline onclick attribute in HTML
3
+ window.toggleSort = function toggleSort() {
4
+ const table = document.getElementById('tableBody');
5
+ if (!table) return;
6
+ const rows = Array.from(table.rows);
7
+ rows.reverse().forEach((row) => table.appendChild(row));
8
+
9
+ const asc = document.getElementById('sortAsc');
10
+ const desc = document.getElementById('sortDesc');
11
+ if (!asc || !desc) return;
12
+
13
+ if (asc.classList.contains('text-indigo-600')) {
14
+ asc.classList.remove('text-indigo-600', 'opacity-100');
15
+ asc.classList.add('opacity-50');
16
+ desc.classList.add('text-indigo-600', 'opacity-100');
17
+ desc.classList.remove('opacity-50');
18
+ } else {
19
+ asc.classList.add('text-indigo-600', 'opacity-100');
20
+ asc.classList.remove('opacity-50');
21
+ desc.classList.remove('text-indigo-600', 'opacity-100');
22
+ desc.classList.add('opacity-50');
23
+ }
24
+ };
@@ -0,0 +1,37 @@
1
+ // Entities page interactions
2
+ // Expose toggleEntityRow globally because it is invoked from HTML
3
+ window.toggleEntityRow = function toggleEntityRow(id) {
4
+ const row = document.getElementById(id);
5
+ const icon = document.getElementById('icon-' + id);
6
+ const contentDiv = document.getElementById('content-' + id);
7
+
8
+ if (!row || !icon) return;
9
+
10
+ // Toggle visibility
11
+ if (row.classList.contains('hidden')) {
12
+ row.classList.remove('hidden');
13
+ icon.classList.add('rotate-90');
14
+
15
+ // Populate if empty (lazy render)
16
+ if (contentDiv && contentDiv.children.length === 0) {
17
+ try {
18
+ const columns = JSON.parse(row.dataset.columns || '[]');
19
+ if (columns.length === 0) {
20
+ contentDiv.innerHTML = '<span class="text-gray-400 italic">No columns defined</span>';
21
+ } else {
22
+ contentDiv.innerHTML = columns
23
+ .map(
24
+ (col) =>
25
+ `<div class="bg-white px-2 py-1 rounded border border-gray-200 truncate" title="${col}">${col}</div>`
26
+ )
27
+ .join('');
28
+ }
29
+ } catch (e) {
30
+ console.error('Error parsing columns', e);
31
+ }
32
+ }
33
+ } else {
34
+ row.classList.add('hidden');
35
+ icon.classList.remove('rotate-90');
36
+ }
37
+ };
@@ -0,0 +1,15 @@
1
+ // Tailwind CDN configuration
2
+ // Kept separate to allow caching and to keep HTML clean
3
+ // Note: This file is loaded after the Tailwind CDN script as in the original layout
4
+ // to preserve existing behavior.
5
+ if (typeof tailwind !== 'undefined') {
6
+ tailwind.config = {
7
+ theme: {
8
+ extend: {
9
+ fontFamily: {
10
+ sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
11
+ },
12
+ },
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,143 @@
1
+ // Shared layout interactions: search filter and simple SPA-like navigation
2
+ (function () {
3
+ document.addEventListener('DOMContentLoaded', () => {
4
+ const attachSearch = () => {
5
+ const searchInput = document.getElementById('search');
6
+ if (!searchInput) return;
7
+ searchInput.addEventListener('input', (e) => {
8
+ const term = String(e.target.value || '').toLowerCase();
9
+ const rows = document.querySelectorAll('tbody tr');
10
+ rows.forEach((row) => {
11
+ const text = row.textContent.toLowerCase();
12
+ row.style.display = text.includes(term) ? '' : 'none';
13
+ });
14
+ });
15
+ };
16
+
17
+ attachSearch();
18
+
19
+ // SPA Navigation
20
+ const mainContent = document.querySelector('main');
21
+ const navLinks = document.querySelectorAll('nav a');
22
+
23
+ const updateActiveLink = (path) => {
24
+ navLinks.forEach((link) => {
25
+ // Reset classes
26
+ link.className =
27
+ 'flex items-center px-2 py-2 text-sm font-medium rounded-md group text-slate-300 hover:bg-slate-800 hover:text-white';
28
+ const svg = link.querySelector('svg');
29
+ if (svg)
30
+ svg.setAttribute(
31
+ 'class',
32
+ 'mr-3 flex-shrink-0 h-6 w-6 text-slate-400 group-hover:text-white'
33
+ );
34
+
35
+ // Check match
36
+ const href = link.getAttribute('href');
37
+ if (href === path) {
38
+ link.className =
39
+ 'flex items-center px-2 py-2 text-sm font-medium rounded-md group bg-indigo-600 text-white';
40
+ if (svg)
41
+ svg.setAttribute(
42
+ 'class',
43
+ 'mr-3 flex-shrink-0 h-6 w-6 text-indigo-300'
44
+ );
45
+ }
46
+ });
47
+ };
48
+
49
+ // Load and execute any <script> tags inside a container that was injected via innerHTML
50
+ const executeScriptsIn = async (container) => {
51
+ if (!container) return;
52
+ // Track already loaded external scripts to avoid duplicates across navigations
53
+ window.__profilerLoadedScripts = window.__profilerLoadedScripts || new Set();
54
+
55
+ const scripts = Array.from(container.querySelectorAll('script'));
56
+
57
+ // Load external scripts sequentially to preserve order
58
+ for (const oldScript of scripts) {
59
+ const src = oldScript.getAttribute('src');
60
+ if (src) {
61
+ if (!window.__profilerLoadedScripts.has(src)) {
62
+ await new Promise((resolve, reject) => {
63
+ const s = document.createElement('script');
64
+ s.src = src;
65
+ s.onload = () => {
66
+ window.__profilerLoadedScripts.add(src);
67
+ resolve();
68
+ };
69
+ s.onerror = reject;
70
+ document.body.appendChild(s);
71
+ });
72
+ }
73
+ } else if (oldScript.textContent && oldScript.textContent.trim()) {
74
+ // Recreate inline scripts so they execute
75
+ const s = document.createElement('script');
76
+ s.text = oldScript.textContent;
77
+ document.body.appendChild(s);
78
+ }
79
+ }
80
+ };
81
+
82
+ const navigate = async (url) => {
83
+ if (!mainContent) {
84
+ window.location.href = url;
85
+ return;
86
+ }
87
+ try {
88
+ // Show loading state if needed
89
+ mainContent.style.opacity = '0.5';
90
+
91
+ const response = await fetch(url);
92
+ const text = await response.text();
93
+
94
+ // Parse HTML
95
+ const parser = new DOMParser();
96
+ const doc = parser.parseFromString(text, 'text/html');
97
+
98
+ // Replace Main Content
99
+ const newMain = doc.querySelector('main');
100
+ const newContent = newMain ? newMain.innerHTML : '';
101
+ mainContent.innerHTML = newContent;
102
+ mainContent.style.opacity = '1';
103
+
104
+ // Update Title
105
+ document.title = doc.title;
106
+
107
+ // Execute any scripts contained in the newly injected content (page-specific JS)
108
+ await executeScriptsIn(mainContent);
109
+
110
+ // Re-attach listeners (like search)
111
+ attachSearch();
112
+
113
+ // Update Active State
114
+ updateActiveLink(url);
115
+ } catch (e) {
116
+ console.error('Navigation failed', e);
117
+ window.location.href = url; // Fallback
118
+ }
119
+ };
120
+
121
+ document.body.addEventListener('click', (e) => {
122
+ const link = e.target.closest('a');
123
+ if (
124
+ link &&
125
+ link.href &&
126
+ link.href.includes('/__profiler') &&
127
+ !link.getAttribute('target')
128
+ ) {
129
+ const url = new URL(link.href);
130
+ if (url.origin === window.location.origin) {
131
+ e.preventDefault();
132
+ const path = url.pathname + url.search;
133
+ history.pushState({}, '', path);
134
+ navigate(path);
135
+ }
136
+ }
137
+ });
138
+
139
+ window.addEventListener('popstate', () => {
140
+ navigate(window.location.pathname + window.location.search);
141
+ });
142
+ });
143
+ })();
@@ -0,0 +1,28 @@
1
+ // Queries page interactions
2
+ document.addEventListener('DOMContentLoaded', () => {
3
+ // Duration Sorting
4
+ const durationHeader = document.getElementById('duration-header');
5
+ const tbody = document.querySelector('tbody');
6
+ let durationAsc = false; // Default is descending (from backend)
7
+
8
+ if (durationHeader && tbody) {
9
+ durationHeader.addEventListener('click', () => {
10
+ durationAsc = !durationAsc;
11
+ const rows = Array.from(tbody.querySelectorAll('tr'));
12
+
13
+ rows.sort((a, b) => {
14
+ const aNode = a.querySelector('[data-duration]');
15
+ const bNode = b.querySelector('[data-duration]');
16
+ const durA = parseFloat((aNode && aNode.dataset.duration) || '0');
17
+ const durB = parseFloat((bNode && bNode.dataset.duration) || '0');
18
+ return durationAsc ? durA - durB : durB - durA;
19
+ });
20
+
21
+ rows.forEach((row) => tbody.appendChild(row));
22
+
23
+ // Update Arrow indication
24
+ const arrow = durationHeader.querySelector('span');
25
+ if (arrow) arrow.textContent = durationAsc ? '↑' : '↓';
26
+ });
27
+ }
28
+ });