nestjs-profiler 1.0.11 → 1.0.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestjs-profiler",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "A NestJS module for profiling HTTP requests, database queries, and cache operations. Inspired by Symfony Profiler, it provides a web-based dashboard to inspect request duration, executed queries, log messages, and explain plans for slow queries.",
5
5
  "author": "Mohamed Raslan",
6
6
  "main": "./index.js",
@@ -16,6 +16,15 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
+ "files": [
20
+ "**/*.js",
21
+ "**/*.d.ts",
22
+ "**/*.js.map",
23
+ "src/assets",
24
+ "src/views",
25
+ "package.json",
26
+ "README.md"
27
+ ],
19
28
  "peerDependencies": {
20
29
  "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
21
30
  "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
@@ -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
+ });
@@ -0,0 +1,252 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full bg-gray-50">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{{ title }}} - NestJS Profiler</title>
8
+ <link rel="icon" type="image/x-icon" href="/__profiler/assets/favicon.ico?v=1">
9
+ <!-- Tailwind CSS from CDN for styling -->
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <script>
12
+ tailwind.config = {
13
+ theme: {
14
+ extend: {
15
+ fontFamily: {
16
+ sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
17
+ }
18
+ }
19
+ }
20
+ }
21
+ </script>
22
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
23
+ <style>
24
+ body {
25
+ font-family: 'Inter', sans-serif;
26
+ }
27
+
28
+ /* Custom scrollbar for better look */
29
+ ::-webkit-scrollbar {
30
+ width: 8px;
31
+ height: 8px;
32
+ }
33
+
34
+ ::-webkit-scrollbar-track {
35
+ background: transparent;
36
+ }
37
+
38
+ ::-webkit-scrollbar-thumb {
39
+ background: #cbd5e1;
40
+ border-radius: 4px;
41
+ }
42
+
43
+ ::-webkit-scrollbar-thumb:hover {
44
+ background: #94a3b8;
45
+ }
46
+ </style>
47
+ </head>
48
+
49
+ <body class="h-full">
50
+ <div class="min-h-full flex">
51
+ <!-- Sidebar -->
52
+ <div class="w-64 bg-slate-900 text-white flex-shrink-0 flex flex-col fixed h-full inset-y-0 z-10">
53
+ <div class="flex items-center px-6 h-16 bg-slate-800">
54
+ <a href="/__profiler" class="flex items-center gap-3">
55
+ <img src="/__profiler/assets/logo.png" alt="NestJS Profiler" class="h-8 w-auto">
56
+ <span class="text-white font-bold text-lg tracking-tight">NestJS Profiler</span>
57
+ </a>
58
+ </div>
59
+ <nav class="flex-1 px-4 py-6 space-y-1">
60
+ <a href="/__profiler"
61
+ class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ requestsActive }}}">
62
+ <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ requestsIconClass }}}" fill="none" viewBox="0 0 24 24"
63
+ stroke="currentColor">
64
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
65
+ d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
66
+ </svg>
67
+ Requests
68
+ </a>
69
+ <a href="/__profiler/view/queries"
70
+ class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ queriesActive }}}">
71
+ <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ queriesIconClass }}}" fill="none" viewBox="0 0 24 24"
72
+ stroke="currentColor">
73
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
74
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
75
+ </svg>
76
+ Database (Queries)
77
+ </a>
78
+ <a href="/__profiler/view/logs"
79
+ class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ logsActive }}}">
80
+ <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ logsIconClass }}}" fill="none" viewBox="0 0 24 24"
81
+ stroke="currentColor">
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
83
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
84
+ </svg>
85
+ Logs
86
+ </a>
87
+ <a href="/__profiler/view/entities"
88
+ class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ entitiesActive }}}">
89
+ <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ entitiesIconClass }}}" fill="none" viewBox="0 0 24 24"
90
+ stroke="currentColor">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
92
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
93
+ </svg>
94
+ Entities
95
+ </a>
96
+ <a href="/__profiler/view/routes"
97
+ class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ routesActive }}}">
98
+ <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ routesIconClass }}}" fill="none" viewBox="0 0 24 24"
99
+ stroke="currentColor">
100
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
101
+ d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
102
+ </svg>
103
+ Routes
104
+ </a>
105
+ <a href="/__profiler/view/cache"
106
+ class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ cacheActive }}}">
107
+ <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ cacheIconClass }}}" fill="none" viewBox="0 0 24 24"
108
+ stroke="currentColor">
109
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
110
+ d="M13 10V3L4 14h7v7l9-11h-7z" />
111
+ </svg>
112
+ Cache
113
+ </a>
114
+ </nav>
115
+ <div class="flex-shrink-0 flex bg-slate-800 p-4">
116
+ <a href="#" class="flex-shrink-0 w-full group block">
117
+ <div class="flex items-center">
118
+ <div class="ml-3">
119
+ <p class="text-xs font-medium text-white">v1.0.0</p>
120
+ <p class="text-xs font-medium text-slate-300">nestjs-profiler</p>
121
+ </div>
122
+ </div>
123
+ </a>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Main Content -->
128
+ <div class="flex-1 flex flex-col pl-64">
129
+ <div class="flex items-center px-6 h-16 bg-white border-b border-gray-200 flex justify-end">
130
+ <div class="ml-4 flex items-center md:ml-6">
131
+ <button onclick="window.location.reload()"
132
+ class="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
133
+ title="Refresh Page">
134
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
135
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
136
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
137
+ </svg>
138
+ </button>
139
+ </div>
140
+ </div>
141
+ <script>
142
+ document.addEventListener('DOMContentLoaded', () => {
143
+ const searchInput = document.getElementById('search');
144
+ if (searchInput) {
145
+ searchInput.addEventListener('input', (e) => {
146
+ const term = e.target.value.toLowerCase();
147
+ const rows = document.querySelectorAll('tbody tr');
148
+
149
+ rows.forEach(row => {
150
+ const text = row.textContent.toLowerCase();
151
+ if (text.includes(term)) {
152
+ row.style.display = '';
153
+ } else {
154
+ row.style.display = 'none';
155
+ }
156
+ });
157
+ });
158
+ }
159
+
160
+ // SPA Navigation
161
+ const mainContent = document.querySelector('main');
162
+ const navLinks = document.querySelectorAll('nav a');
163
+ const sidebar = document.querySelector('nav');
164
+
165
+ const updateActiveLink = (path) => {
166
+ navLinks.forEach(link => {
167
+ // Reset classes
168
+ link.className = 'flex items-center px-2 py-2 text-sm font-medium rounded-md group text-slate-300 hover:bg-slate-800 hover:text-white';
169
+ const svg = link.querySelector('svg');
170
+ if (svg) svg.setAttribute('class', 'mr-3 flex-shrink-0 h-6 w-6 text-slate-400 group-hover:text-white');
171
+
172
+ // Check match
173
+ const href = link.getAttribute('href');
174
+ if (href === path) {
175
+ link.className = 'flex items-center px-2 py-2 text-sm font-medium rounded-md group bg-indigo-600 text-white';
176
+ if (svg) svg.setAttribute('class', 'mr-3 flex-shrink-0 h-6 w-6 text-indigo-300');
177
+ }
178
+ });
179
+ };
180
+
181
+ const navigate = async (url) => {
182
+ try {
183
+ // Show loading state if needed
184
+ mainContent.style.opacity = '0.5';
185
+
186
+ const response = await fetch(url);
187
+ const text = await response.text();
188
+
189
+ // Parse HTML
190
+ const parser = new DOMParser();
191
+ const doc = parser.parseFromString(text, 'text/html');
192
+
193
+ // Replace Main Content
194
+ const newContent = doc.querySelector('main').innerHTML;
195
+ mainContent.innerHTML = newContent;
196
+ mainContent.style.opacity = '1';
197
+
198
+ // Update Title
199
+ document.title = doc.title;
200
+
201
+ // Re-attach listeners (like search)
202
+ const newSearchInput = document.getElementById('search');
203
+ if (newSearchInput) {
204
+ newSearchInput.addEventListener('input', (e) => {
205
+ const term = e.target.value.toLowerCase();
206
+ const rows = document.querySelectorAll('tbody tr');
207
+ rows.forEach(row => {
208
+ const text = row.textContent.toLowerCase();
209
+ row.style.display = text.includes(term) ? '' : 'none';
210
+ });
211
+ });
212
+ }
213
+
214
+ // Update Active State
215
+ updateActiveLink(url);
216
+
217
+ } catch (e) {
218
+ console.error('Navigation failed', e);
219
+ window.location.href = url; // Fallback
220
+ }
221
+ };
222
+
223
+ document.body.addEventListener('click', (e) => {
224
+ const link = e.target.closest('a');
225
+ if (link && link.href && link.href.includes('/__profiler') && !link.getAttribute('target')) {
226
+ const url = new URL(link.href);
227
+ if (url.origin === window.location.origin) {
228
+ e.preventDefault();
229
+ const path = url.pathname + url.search;
230
+ history.pushState({}, '', path);
231
+ navigate(path);
232
+ }
233
+ }
234
+ });
235
+
236
+ window.addEventListener('popstate', () => {
237
+ navigate(window.location.pathname + window.location.search);
238
+ });
239
+ });
240
+ </script>
241
+ <main class="flex-1">
242
+ <div class="py-6">
243
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
244
+ {{{ content }}}
245
+ </div>
246
+ </div>
247
+ </main>
248
+ </div>
249
+ </div>
250
+ </body>
251
+
252
+ </html>
@@ -0,0 +1,37 @@
1
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex flex-col h-full">
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">Logs</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 logs..." type="search">
15
+ </div>
16
+ </div>
17
+ <div class="text-sm text-gray-500">
18
+ <span class="font-bold text-gray-800">{{ totalLogs }}</span> entries
19
+ </div>
20
+ </div>
21
+ <div class="overflow-x-auto">
22
+ <table class="w-full text-left border-collapse">
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-24">Level</th>
26
+ <th class="p-4 border-b border-gray-200">Message</th>
27
+ <th class="p-4 border-b border-gray-200 w-48">Request</th>
28
+ <th class="p-4 border-b border-gray-200 w-32 text-right">Happened</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody class="divide-y divide-gray-100">
32
+ {{{ rows }}}
33
+ </tbody>
34
+ </table>
35
+ </div>
36
+ {{{ pagination }}}
37
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="text-center py-20">
2
+ <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-100 mb-6">
3
+ <svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
5
+ d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
6
+ </svg>
7
+ </div>
8
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">Profile Not Found</h2>
9
+ <p class="text-gray-500 mb-8 max-w-md mx-auto">The requested profile <code>{{ id }}</code> could not be found. This
10
+ usually happens if the server was restarted (clearing in-memory storage).</p>
11
+ <a href="/__profiler"
12
+ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
13
+ Back to Dashboard
14
+ </a>
15
+ </div>
@@ -0,0 +1,20 @@
1
+ <tr class="hover:bg-gray-50 transition-colors">
2
+ <td class="px-6 py-4 whitespace-nowrap">
3
+ {{{ operationBadge }}}
4
+ </td>
5
+ <td class="px-6 py-4">
6
+ <div class="text-sm text-gray-900 font-mono break-all max-w-md truncate" title="{{ key }}">{{ key }}</div>
7
+ </td>
8
+ <td class="px-6 py-4 whitespace-nowrap">
9
+ {{{ resultBadge }}}
10
+ </td>
11
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
12
+ {{ store }}
13
+ </td>
14
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-500">
15
+ {{ duration }}ms
16
+ </td>
17
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-400">
18
+ {{ timeAgo }}
19
+ </td>
20
+ </tr>
@@ -0,0 +1,33 @@
1
+ <details class="border border-gray-200 rounded-lg mb-4 bg-white shadow-sm group" {{ open }}>
2
+ <summary
3
+ class="flex justify-between items-center p-4 cursor-pointer list-none select-none hover:bg-gray-50 transition-colors">
4
+ <div class="flex items-center space-x-2">
5
+ <svg class="w-4 h-4 text-gray-400 transition-transform group-open:rotate-90" fill="none"
6
+ stroke="currentColor" viewBox="0 0 24 24">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
8
+ </svg>
9
+ <span class="text-xs font-bold text-gray-500 uppercase tracking-widest">Query #{{ index }}</span>
10
+ {{{ dbBadge }}}
11
+ {{{ opBadge }}}
12
+ {{{ tagsBadges }}}
13
+ {{{ connection }}}
14
+ </div>
15
+ <div class="flex items-center space-x-2">
16
+ {{{ planType }}}
17
+ <span class="text-xs font-medium {{{ durationClass }}} px-2 py-1 rounded">
18
+ {{ duration }}ms
19
+ </span>
20
+ <span class="text-xs text-gray-400 w-16 text-right">{{ rowCount }} rows</span>
21
+ </div>
22
+ </summary>
23
+ <div class="p-4 pt-0 border-t border-gray-100 mt-2">
24
+ <div class="bg-gray-900 rounded-md p-3 overflow-x-auto mb-2 group relative">
25
+ <code class="text-gray-100 font-mono text-sm block whitespace-pre-wrap">{{ query }}</code>
26
+ </div>
27
+ {{{ duplicationWarning }}}
28
+ {{{ params }}}
29
+ {{{ filter }}}
30
+ {{{ explainPlan }}}
31
+ {{{ error }}}
32
+ </div>
33
+ </details>
@@ -0,0 +1,39 @@
1
+ <tr class="hover:bg-gray-50 border-b border-gray-100 cursor-pointer transition-colors"
2
+ onclick="toggleEntityRow('{{ rowId }}')">
3
+ <td class="p-4 align-middle">
4
+ <div class="flex items-center">
5
+ <svg id="icon-{{ rowId }}" class="w-4 h-4 mr-2 text-gray-400 transform transition-transform duration-200"
6
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
8
+ </svg>
9
+ <span class="font-medium text-gray-900">{{ name }}</span>
10
+ </div>
11
+ </td>
12
+ <td class="p-4 align-middle">
13
+ {{{ typeBadge }}}
14
+ </td>
15
+ <td class="p-4 align-middle text-gray-500 text-sm">
16
+ {{ database }}
17
+ </td>
18
+ <td class="p-4 align-middle">
19
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
20
+ {{ connection }}
21
+ </span>
22
+ </td>
23
+ <td class="p-4 align-middle text-gray-500 font-mono text-xs">
24
+ {{ tableName }}
25
+ </td>
26
+ <td class="p-4 align-middle text-gray-400 text-xs">
27
+ {{ columnsCount }} columns
28
+ </td>
29
+ </tr>
30
+ <tr id="{{ rowId }}" class="hidden bg-gray-50" data-columns='{{ columnsJson }}'>
31
+ <td colspan="6" class="p-4 border-b border-gray-100">
32
+ <div class="pl-6">
33
+ <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Properties / Columns</h4>
34
+ <div class="grid grid-cols-4 gap-2 text-sm text-gray-700 font-mono" id="content-{{ rowId }}">
35
+ <!-- JS will populate this -->
36
+ </div>
37
+ </div>
38
+ </td>
39
+ </tr>
@@ -0,0 +1,19 @@
1
+ <tr class="hover:bg-gray-50 border-b border-gray-100">
2
+ <td class="p-4 align-middle whitespace-nowrap">
3
+ <span
4
+ class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset {{{ levelColor }}}">{{
5
+ level }}</span>
6
+ </td>
7
+ <td class="p-4 align-middle">
8
+ <span class="text-sm text-gray-900 break-all">{{ message }}</span>
9
+ {{{ context }}}
10
+ </td>
11
+ <td class="p-4 align-middle whitespace-nowrap">
12
+ <a href="/__profiler/{{ requestId }}" class="text-xs text-blue-500 hover:underline">
13
+ {{ requestMethod }} {{ requestUrl }}
14
+ </a>
15
+ </td>
16
+ <td class="p-4 align-middle text-gray-400 text-xs whitespace-nowrap text-right">
17
+ {{ timeAgo }}
18
+ </td>
19
+ </tr>
@@ -0,0 +1,27 @@
1
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
2
+ <div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
3
+ <h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider">Metadata</h3>
4
+ </div>
5
+ <div class="divide-y divide-gray-100">
6
+ <div class="p-3">
7
+ <span class="block text-xs text-gray-400 mb-1">Status Code</span>
8
+ <span class="block font-mono text-sm {{{ statusColor }}}">{{ statusCode }}</span>
9
+ </div>
10
+ <div class="p-3">
11
+ <span class="block text-xs text-gray-400 mb-1">Duration</span>
12
+ <span class="block font-mono text-sm text-gray-700">{{ duration }}ms</span>
13
+ </div>
14
+ <div class="p-3">
15
+ <span class="block text-xs text-gray-400 mb-1">Response Memory</span>
16
+ <span class="block font-mono text-sm text-gray-700">{{ memory }}</span>
17
+ </div>
18
+ <div class="p-3">
19
+ <span class="block text-xs text-gray-400 mb-1">Controller</span>
20
+ <span class="block font-mono text-sm text-gray-700 break-all">{{ controller }}</span>
21
+ </div>
22
+ <div class="p-3">
23
+ <span class="block text-xs text-gray-400 mb-1">Handler</span>
24
+ <span class="block font-mono text-sm text-gray-700">{{ handler }}</span>
25
+ </div>
26
+ </div>
27
+ </div>
@@ -0,0 +1,9 @@
1
+ <div class="p-4 border-t border-gray-200 bg-gray-50 flex justify-between items-center">
2
+ <div class="text-xs text-gray-500">
3
+ Page {{ currentPage }} of {{ totalPages }}
4
+ </div>
5
+ <div class="flex space-x-2">
6
+ {{{ previousPage }}}
7
+ {{{ nextPage }}}
8
+ </div>
9
+ </div>
@@ -0,0 +1,34 @@
1
+ <tr class="hover:bg-gray-50 border-b border-gray-100">
2
+ <td class="p-4 align-middle w-1/2">
3
+ <details class="group">
4
+ <summary
5
+ class="cursor-pointer font-medium text-gray-700 hover:text-indigo-600 flex items-center outline-none list-none">
6
+ <svg class="w-4 h-4 mr-2 text-gray-400 group-open:rotate-90 transition-transform" fill="none"
7
+ viewBox="0 0 24 24" stroke="currentColor">
8
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
9
+ </svg>
10
+ <span class="font-mono text-sm truncate max-w-xl">
11
+ <span class="font-bold uppercase mr-1 pl-1 pr-1 bg-gray-100 rounded text-gray-600"
12
+ style="font-size: 0.75rem;">{{ requestMethod }}</span> {{ requestUrl }}
13
+ </span>
14
+ <a href="/__profiler/{{ requestId }}" class="ml-2 text-gray-400 hover:text-blue-500"
15
+ title="Go to Request">
16
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
17
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
18
+ d="M13 5l7 7-7 7M5 5l7 7-7 7" />
19
+ </svg>
20
+ </a>
21
+ </summary>
22
+ <div class="mt-2 ml-6">
23
+ <code
24
+ class="text-sm text-indigo-600 font-mono block whitespace-pre-wrap max-h-96 overflow-y-auto bg-gray-50 p-2 rounded border border-gray-100">{{ sql }}</code>
25
+ </div>
26
+ </details>
27
+ </td>
28
+ <td class="p-4 align-middle whitespace-nowrap" data-duration="{{ duration }}">
29
+ <span class="text-sm text-gray-500 font-medium">{{ duration }}ms</span>
30
+ </td>
31
+ <td class="p-4 align-middle text-gray-400 text-sm whitespace-nowrap text-right">
32
+ {{ timeAgo }}
33
+ </td>
34
+ </tr>
@@ -0,0 +1,25 @@
1
+ <tr class="hover:bg-gray-50 border-b border-gray-100">
2
+ <td class="p-4 align-middle">
3
+ {{{ methodBadge }}}
4
+ </td>
5
+ <td class="p-4 align-middle w-full">
6
+ <a href="/__profiler/{{ id }}"
7
+ class="text-indigo-600 hover:text-indigo-900 font-medium block truncate max-w-xl transition-colors">
8
+ {{ url }}
9
+ </a>
10
+ </td>
11
+ <td class="p-4 align-middle whitespace-nowrap">
12
+ <span class="px-2.5 py-0.5 rounded-full text-xs font-medium {{{ statusClass }}}">
13
+ {{ statusCode }}
14
+ </span>
15
+ </td>
16
+ <td class="p-4 align-middle text-gray-500 text-sm whitespace-nowrap">
17
+ {{ duration }}ms
18
+ </td>
19
+ <td class="p-4 align-middle text-gray-500 text-sm whitespace-nowrap text-center">
20
+ {{{ queriesCount }}}
21
+ </td>
22
+ <td class="p-4 align-middle text-gray-400 text-xs whitespace-nowrap text-right">
23
+ {{ timeAgo }}
24
+ </td>
25
+ </tr>
@@ -0,0 +1,12 @@
1
+ <tr class="hover:bg-gray-50 transition-colors">
2
+ <td class="px-6 py-4 whitespace-nowrap">
3
+ {{{ methodBadge }}}
4
+ </td>
5
+ <td class="px-6 py-4 whitespace-nowrap">
6
+ <code class="text-xs font-mono text-indigo-600 bg-indigo-50 px-2 py-1 rounded">{{ path }}</code>
7
+ </td>
8
+ <td class="px-6 py-4 whitespace-nowrap">
9
+ <div class="text-sm font-medium text-gray-900">{{ controller }}</div>
10
+ <div class="text-xs text-gray-500 mt-0.5">{{ handler }}()</div>
11
+ </td>
12
+ </tr>
@@ -0,0 +1,65 @@
1
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
2
+ <div class="p-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="font-semibold text-gray-700">Executed Queries</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 queries..." type="search">
15
+ </div>
16
+ </div>
17
+ <span class="text-xs text-gray-500 bg-white border border-gray-200 px-2 py-1 rounded-md">Last 100 requests
18
+ context</span>
19
+ </div>
20
+ <div class="overflow-x-auto">
21
+ <table class="w-full text-left border-collapse">
22
+ <thead class="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider font-medium">
23
+ <tr>
24
+ <th class="p-4 border-b border-gray-200">Request / Query</th>
25
+ <th id="duration-header"
26
+ class="p-4 border-b border-gray-200 w-32 cursor-pointer hover:bg-gray-100 select-none"
27
+ title="Click to sort">Duration <span class="text-xs">↓</span></th>
28
+ <th class="p-4 border-b border-gray-200 w-32 text-right">Happened</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody class="divide-y divide-gray-100">
32
+ {{{ rows }}}
33
+ </tbody>
34
+ </table>
35
+ </div>
36
+ </div>
37
+
38
+ <script>
39
+ document.addEventListener('DOMContentLoaded', () => {
40
+ // Duration Sorting
41
+ const durationHeader = document.getElementById('duration-header');
42
+ const tbody = document.querySelector('tbody');
43
+ let durationAsc = false; // Default is descending (from backend)
44
+
45
+ if (durationHeader && tbody) {
46
+ durationHeader.addEventListener('click', () => {
47
+ durationAsc = !durationAsc;
48
+ const rows = Array.from(tbody.querySelectorAll('tr'));
49
+
50
+ rows.sort((a, b) => {
51
+ const durA = parseFloat(a.querySelector('[data-duration]')?.dataset.duration || 0);
52
+ const durB = parseFloat(b.querySelector('[data-duration]')?.dataset.duration || 0);
53
+
54
+ return durationAsc ? durA - durB : durB - durA;
55
+ });
56
+
57
+ rows.forEach(row => tbody.appendChild(row));
58
+
59
+ // Update Arrow indication
60
+ const arrow = durationHeader.querySelector('span');
61
+ if (arrow) arrow.textContent = durationAsc ? '↑' : '↓';
62
+ });
63
+ }
64
+ });
65
+ </script>
@@ -0,0 +1,41 @@
1
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
2
+ <div class="p-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="font-semibold text-gray-700">Routes 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 routes..." type="search">
15
+ </div>
16
+ </div>
17
+ <span class="text-xs text-gray-500 bg-white border border-gray-200 px-2 py-1 rounded-md">
18
+ {{ totalRoutes }} Routes Found
19
+ </span>
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">Method
27
+ </th>
28
+ <th scope="col"
29
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Path</th>
30
+ <th scope="col"
31
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
32
+ Controller</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody class="bg-white divide-y divide-gray-200">
36
+ {{{ rows }}}
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+ {{{ emptyState }}}
41
+ </div>