pg_insights 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +183 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/pg_insights/application.js +436 -0
- data/app/assets/javascripts/pg_insights/health.js +104 -0
- data/app/assets/javascripts/pg_insights/results/chart_renderer.js +126 -0
- data/app/assets/javascripts/pg_insights/results/table_manager.js +378 -0
- data/app/assets/javascripts/pg_insights/results/view_toggles.js +25 -0
- data/app/assets/javascripts/pg_insights/results.js +13 -0
- data/app/assets/stylesheets/pg_insights/application.css +750 -0
- data/app/assets/stylesheets/pg_insights/health.css +501 -0
- data/app/assets/stylesheets/pg_insights/results.css +682 -0
- data/app/controllers/pg_insights/application_controller.rb +4 -0
- data/app/controllers/pg_insights/health_controller.rb +110 -0
- data/app/controllers/pg_insights/insights_controller.rb +77 -0
- data/app/controllers/pg_insights/queries_controller.rb +44 -0
- data/app/helpers/pg_insights/application_helper.rb +4 -0
- data/app/helpers/pg_insights/insights_helper.rb +190 -0
- data/app/jobs/pg_insights/application_job.rb +4 -0
- data/app/jobs/pg_insights/health_check_job.rb +45 -0
- data/app/jobs/pg_insights/health_check_scheduler_job.rb +52 -0
- data/app/jobs/pg_insights/recurring_health_checks_job.rb +49 -0
- data/app/models/pg_insights/application_record.rb +5 -0
- data/app/models/pg_insights/health_check_result.rb +46 -0
- data/app/models/pg_insights/query.rb +10 -0
- data/app/services/pg_insights/health_check_service.rb +298 -0
- data/app/services/pg_insights/insight_query_service.rb +21 -0
- data/app/views/layouts/pg_insights/application.html.erb +58 -0
- data/app/views/pg_insights/health/index.html.erb +324 -0
- data/app/views/pg_insights/insights/_chart_view.html.erb +25 -0
- data/app/views/pg_insights/insights/_column_panel.html.erb +18 -0
- data/app/views/pg_insights/insights/_query_examples.html.erb +32 -0
- data/app/views/pg_insights/insights/_query_panel.html.erb +36 -0
- data/app/views/pg_insights/insights/_result.html.erb +15 -0
- data/app/views/pg_insights/insights/_results_info.html.erb +19 -0
- data/app/views/pg_insights/insights/_results_panel.html.erb +13 -0
- data/app/views/pg_insights/insights/_results_table.html.erb +45 -0
- data/app/views/pg_insights/insights/_stats_view.html.erb +3 -0
- data/app/views/pg_insights/insights/_table_controls.html.erb +21 -0
- data/app/views/pg_insights/insights/_table_view.html.erb +5 -0
- data/app/views/pg_insights/insights/index.html.erb +5 -0
- data/config/default_queries.yml +85 -0
- data/config/routes.rb +22 -0
- data/lib/generators/pg_insights/clean_generator.rb +74 -0
- data/lib/generators/pg_insights/install_generator.rb +176 -0
- data/lib/pg_insights/engine.rb +40 -0
- data/lib/pg_insights/version.rb +3 -0
- data/lib/pg_insights.rb +83 -0
- data/lib/tasks/pg_insights.rake +172 -0
- metadata +124 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function() {
|
2
|
+
initializeHealthDashboard();
|
3
|
+
});
|
4
|
+
|
5
|
+
function initializeHealthDashboard() {
|
6
|
+
initializeSmoothScrolling();
|
7
|
+
initializeSectionHighlighting();
|
8
|
+
initializeResponsive();
|
9
|
+
}
|
10
|
+
function initializeSmoothScrolling() {
|
11
|
+
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
12
|
+
|
13
|
+
anchorLinks.forEach(link => {
|
14
|
+
link.addEventListener('click', function(e) {
|
15
|
+
const targetId = this.getAttribute('href').substring(1);
|
16
|
+
const targetElement = document.getElementById(targetId);
|
17
|
+
|
18
|
+
if (targetElement) {
|
19
|
+
e.preventDefault();
|
20
|
+
|
21
|
+
if (this.classList.contains('stat-card-link')) {
|
22
|
+
this.classList.add('clicked');
|
23
|
+
setTimeout(() => {
|
24
|
+
this.classList.remove('clicked');
|
25
|
+
}, 200);
|
26
|
+
}
|
27
|
+
|
28
|
+
const headerOffset = 20;
|
29
|
+
const elementPosition = targetElement.getBoundingClientRect().top;
|
30
|
+
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
31
|
+
|
32
|
+
window.scrollTo({
|
33
|
+
top: offsetPosition,
|
34
|
+
behavior: 'smooth'
|
35
|
+
});
|
36
|
+
|
37
|
+
highlightSection(targetElement);
|
38
|
+
}
|
39
|
+
});
|
40
|
+
});
|
41
|
+
}
|
42
|
+
|
43
|
+
function initializeSectionHighlighting() {
|
44
|
+
if (window.location.hash) {
|
45
|
+
const targetElement = document.querySelector(window.location.hash);
|
46
|
+
if (targetElement) {
|
47
|
+
setTimeout(() => {
|
48
|
+
highlightSection(targetElement);
|
49
|
+
}, 500);
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
function highlightSection(element) {
|
55
|
+
const previousHighlighted = document.querySelector('.health-section.highlighted');
|
56
|
+
if (previousHighlighted) {
|
57
|
+
previousHighlighted.classList.remove('highlighted');
|
58
|
+
}
|
59
|
+
|
60
|
+
element.classList.add('highlighted');
|
61
|
+
|
62
|
+
setTimeout(() => {
|
63
|
+
element.classList.remove('highlighted');
|
64
|
+
}, 2000);
|
65
|
+
}
|
66
|
+
function initializeResponsive() {
|
67
|
+
let resizeTimeout;
|
68
|
+
|
69
|
+
window.addEventListener('resize', function() {
|
70
|
+
clearTimeout(resizeTimeout);
|
71
|
+
resizeTimeout = setTimeout(function() {
|
72
|
+
adjustForScreenSize();
|
73
|
+
}, 250);
|
74
|
+
});
|
75
|
+
|
76
|
+
adjustForScreenSize();
|
77
|
+
}
|
78
|
+
|
79
|
+
function adjustForScreenSize() {
|
80
|
+
const screenWidth = window.innerWidth;
|
81
|
+
const queryTexts = document.querySelectorAll('.query-text');
|
82
|
+
|
83
|
+
queryTexts.forEach(queryText => {
|
84
|
+
if (screenWidth <= 768) {
|
85
|
+
queryText.style.whiteSpace = 'normal';
|
86
|
+
queryText.style.wordBreak = 'break-word';
|
87
|
+
} else {
|
88
|
+
queryText.style.whiteSpace = 'nowrap';
|
89
|
+
queryText.style.wordBreak = 'normal';
|
90
|
+
}
|
91
|
+
});
|
92
|
+
}
|
93
|
+
function formatNumber(num) {
|
94
|
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
95
|
+
}
|
96
|
+
|
97
|
+
function truncateText(text, maxLength) {
|
98
|
+
if (text.length <= maxLength) {
|
99
|
+
return text;
|
100
|
+
}
|
101
|
+
return text.substring(0, maxLength - 3) + '...';
|
102
|
+
}
|
103
|
+
|
104
|
+
|
@@ -0,0 +1,126 @@
|
|
1
|
+
function initChartRendering() {
|
2
|
+
const chartTypeSelect = document.getElementById('chartType');
|
3
|
+
const chartDataElement = document.querySelector('[data-chart-data]');
|
4
|
+
|
5
|
+
if (!chartTypeSelect || !chartDataElement) return;
|
6
|
+
|
7
|
+
try {
|
8
|
+
const chartData = JSON.parse(chartDataElement.dataset.chartData);
|
9
|
+
|
10
|
+
chartTypeSelect.addEventListener('change', function() {
|
11
|
+
renderChart(this.value, chartData);
|
12
|
+
});
|
13
|
+
|
14
|
+
// Initial render
|
15
|
+
renderChart('bar', chartData);
|
16
|
+
} catch (e) {
|
17
|
+
console.error('Failed to parse chart data:', e);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
function renderChart(type, data) {
|
22
|
+
const container = document.getElementById('dynamicChart');
|
23
|
+
if (!container || !data || !data.chartData) return;
|
24
|
+
|
25
|
+
const containerRect = container.getBoundingClientRect();
|
26
|
+
const containerHeight = Math.max(250, containerRect.height - 20);
|
27
|
+
const containerWidth = containerRect.width - 20;
|
28
|
+
|
29
|
+
const options = {
|
30
|
+
height: containerHeight + 'px',
|
31
|
+
width: containerWidth + 'px',
|
32
|
+
colors: ["#00979D", "#00838a", "#00767a", "#006064", "#004d4f"],
|
33
|
+
responsive: true,
|
34
|
+
maintainAspectRatio: false,
|
35
|
+
library: {
|
36
|
+
responsive: true,
|
37
|
+
maintainAspectRatio: false,
|
38
|
+
interaction: {
|
39
|
+
intersect: false
|
40
|
+
},
|
41
|
+
plugins: {
|
42
|
+
legend: {
|
43
|
+
display: true,
|
44
|
+
position: 'bottom',
|
45
|
+
labels: {
|
46
|
+
fontSize: 11,
|
47
|
+
fontColor: '#6b7280',
|
48
|
+
padding: 10
|
49
|
+
}
|
50
|
+
}
|
51
|
+
},
|
52
|
+
scales: {
|
53
|
+
x: {
|
54
|
+
ticks: {
|
55
|
+
maxRotation: 45,
|
56
|
+
minRotation: 0,
|
57
|
+
font: {
|
58
|
+
size: 10
|
59
|
+
}
|
60
|
+
}
|
61
|
+
},
|
62
|
+
y: {
|
63
|
+
ticks: {
|
64
|
+
font: {
|
65
|
+
size: 10
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
};
|
72
|
+
|
73
|
+
container.innerHTML = '';
|
74
|
+
|
75
|
+
try {
|
76
|
+
setTimeout(function() {
|
77
|
+
var chartInstance;
|
78
|
+
|
79
|
+
switch(type) {
|
80
|
+
case 'line':
|
81
|
+
chartInstance = new Chartkick.LineChart(container, data.chartData, options);
|
82
|
+
break;
|
83
|
+
case 'bar':
|
84
|
+
chartInstance = new Chartkick.BarChart(container, data.chartData, options);
|
85
|
+
break;
|
86
|
+
case 'pie':
|
87
|
+
chartInstance = new Chartkick.PieChart(container, data.chartData, {
|
88
|
+
...options,
|
89
|
+
library: {
|
90
|
+
...options.library,
|
91
|
+
plugins: {
|
92
|
+
legend: {
|
93
|
+
display: true,
|
94
|
+
position: 'right',
|
95
|
+
labels: {
|
96
|
+
fontSize: 10,
|
97
|
+
fontColor: '#6b7280',
|
98
|
+
padding: 8
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
});
|
104
|
+
break;
|
105
|
+
case 'area':
|
106
|
+
chartInstance = new Chartkick.AreaChart(container, data.chartData, options);
|
107
|
+
break;
|
108
|
+
default:
|
109
|
+
chartInstance = new Chartkick.BarChart(container, data.chartData, options);
|
110
|
+
}
|
111
|
+
|
112
|
+
if (chartInstance && chartInstance.getChart) {
|
113
|
+
setTimeout(function() {
|
114
|
+
const chart = chartInstance.getChart();
|
115
|
+
if (chart && chart.resize) {
|
116
|
+
chart.resize();
|
117
|
+
}
|
118
|
+
}, 100);
|
119
|
+
}
|
120
|
+
}, 50);
|
121
|
+
|
122
|
+
} catch (error) {
|
123
|
+
console.error('Chart rendering error:', error);
|
124
|
+
container.innerHTML = '<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #64748b; text-align: center; padding: 20px;"><div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">⚠️</div><h3 style="margin: 0 0 8px 0; color: #374151;">Chart Error</h3><p style="margin: 0 0 8px 0; font-size: 14px;">Unable to render chart</p><small style="opacity: 0.7;">' + error.message + '</small></div>';
|
125
|
+
}
|
126
|
+
}
|
@@ -0,0 +1,378 @@
|
|
1
|
+
function TableManager() {
|
2
|
+
this.table = document.getElementById('resultsTable');
|
3
|
+
this.tableScroll = document.getElementById('tableScroll');
|
4
|
+
this.columnPanel = document.getElementById('columnPanel');
|
5
|
+
this.originalColumnWidths = new Map();
|
6
|
+
this.isResizing = false;
|
7
|
+
|
8
|
+
if (this.table) {
|
9
|
+
this.init();
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
TableManager.prototype.init = function() {
|
14
|
+
this.setupColumnToggles();
|
15
|
+
this.setupTableControls();
|
16
|
+
this.setupScrollIndicators();
|
17
|
+
this.setupColumnResizing();
|
18
|
+
this.setupKeyboardNavigation();
|
19
|
+
this.detectColumnTypes();
|
20
|
+
};
|
21
|
+
|
22
|
+
TableManager.prototype.setupColumnToggles = function() {
|
23
|
+
var self = this;
|
24
|
+
var toggleBtn = document.getElementById('toggleColumns');
|
25
|
+
var showAllBtn = document.getElementById('showAllColumns');
|
26
|
+
var hideAllBtn = document.getElementById('hideAllColumns');
|
27
|
+
var columnToggles = document.querySelectorAll('.column-toggle');
|
28
|
+
|
29
|
+
if (toggleBtn && this.columnPanel) {
|
30
|
+
toggleBtn.addEventListener('click', function() {
|
31
|
+
var isVisible = self.columnPanel.style.display !== 'none';
|
32
|
+
self.columnPanel.style.display = isVisible ? 'none' : 'block';
|
33
|
+
toggleBtn.textContent = isVisible ? '👁️ Show/Hide Columns' : '👁️ Hide Panel';
|
34
|
+
});
|
35
|
+
}
|
36
|
+
|
37
|
+
if (showAllBtn) {
|
38
|
+
showAllBtn.addEventListener('click', function() {
|
39
|
+
columnToggles.forEach(function(toggle) {
|
40
|
+
toggle.checked = true;
|
41
|
+
self.toggleColumn(toggle.dataset.column, true);
|
42
|
+
});
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
if (hideAllBtn) {
|
47
|
+
hideAllBtn.addEventListener('click', function() {
|
48
|
+
columnToggles.forEach(function(toggle) {
|
49
|
+
toggle.checked = false;
|
50
|
+
self.toggleColumn(toggle.dataset.column, false);
|
51
|
+
});
|
52
|
+
});
|
53
|
+
}
|
54
|
+
|
55
|
+
columnToggles.forEach(function(toggle) {
|
56
|
+
toggle.addEventListener('change', function() {
|
57
|
+
self.toggleColumn(toggle.dataset.column, toggle.checked);
|
58
|
+
});
|
59
|
+
});
|
60
|
+
};
|
61
|
+
|
62
|
+
TableManager.prototype.toggleColumn = function(columnIndex, show) {
|
63
|
+
var columns = document.querySelectorAll('[data-column="' + columnIndex + '"]');
|
64
|
+
columns.forEach(function(col) {
|
65
|
+
if (show) {
|
66
|
+
col.classList.remove('column-hidden');
|
67
|
+
col.style.display = '';
|
68
|
+
} else {
|
69
|
+
col.classList.add('column-hidden');
|
70
|
+
col.style.display = 'none';
|
71
|
+
}
|
72
|
+
});
|
73
|
+
};
|
74
|
+
|
75
|
+
TableManager.prototype.setupTableControls = function() {
|
76
|
+
var self = this;
|
77
|
+
var fitBtn = document.getElementById('fitColumns');
|
78
|
+
var resetBtn = document.getElementById('resetTable');
|
79
|
+
|
80
|
+
if (fitBtn) {
|
81
|
+
fitBtn.addEventListener('click', function(e) {
|
82
|
+
e.preventDefault();
|
83
|
+
self.fitColumns();
|
84
|
+
});
|
85
|
+
}
|
86
|
+
|
87
|
+
if (resetBtn) {
|
88
|
+
resetBtn.addEventListener('click', function(e) {
|
89
|
+
e.preventDefault();
|
90
|
+
self.resetTable();
|
91
|
+
});
|
92
|
+
}
|
93
|
+
};
|
94
|
+
|
95
|
+
TableManager.prototype.fitColumns = function() {
|
96
|
+
// Check if required elements exist
|
97
|
+
if (!this.table || !this.tableScroll) {
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
|
101
|
+
// Get all non-row-number headers that are visible
|
102
|
+
var headers = this.table.querySelectorAll('th:not(.row-num):not(.column-hidden)');
|
103
|
+
|
104
|
+
if (headers.length === 0) {
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
|
108
|
+
// Calculate available width (subtract row number column and padding)
|
109
|
+
var rowNumWidth = 60; // Width for row number column
|
110
|
+
var scrollbarWidth = 20; // Account for scrollbar
|
111
|
+
var padding = 20; // Additional padding
|
112
|
+
var containerWidth = this.tableScroll.clientWidth - rowNumWidth - scrollbarWidth - padding;
|
113
|
+
|
114
|
+
// Ensure minimum total width and calculate per-column width
|
115
|
+
var minColumnWidth = 100;
|
116
|
+
var maxColumnWidth = 300;
|
117
|
+
var columnWidth = Math.max(minColumnWidth, Math.min(maxColumnWidth, containerWidth / headers.length));
|
118
|
+
|
119
|
+
// Set table layout to fixed for consistent column sizing
|
120
|
+
this.table.style.tableLayout = 'fixed';
|
121
|
+
|
122
|
+
// Apply width to headers and corresponding cells
|
123
|
+
headers.forEach(function(header, index) {
|
124
|
+
var columnIndex = header.getAttribute('data-column');
|
125
|
+
|
126
|
+
// Set header width
|
127
|
+
header.style.width = columnWidth + 'px';
|
128
|
+
header.style.minWidth = columnWidth + 'px';
|
129
|
+
header.style.maxWidth = columnWidth + 'px';
|
130
|
+
|
131
|
+
// Set corresponding cell widths
|
132
|
+
var cells = document.querySelectorAll('td[data-column="' + columnIndex + '"]:not(.column-hidden)');
|
133
|
+
cells.forEach(function(cell) {
|
134
|
+
cell.style.width = columnWidth + 'px';
|
135
|
+
cell.style.minWidth = columnWidth + 'px';
|
136
|
+
cell.style.maxWidth = columnWidth + 'px';
|
137
|
+
});
|
138
|
+
});
|
139
|
+
};
|
140
|
+
|
141
|
+
TableManager.prototype.resetTable = function() {
|
142
|
+
// Reset table layout
|
143
|
+
if (this.table) {
|
144
|
+
this.table.style.tableLayout = '';
|
145
|
+
}
|
146
|
+
|
147
|
+
// Reset all column and cell styles
|
148
|
+
var allColumns = this.table.querySelectorAll('th, td');
|
149
|
+
allColumns.forEach(function(col) {
|
150
|
+
col.style.width = '';
|
151
|
+
col.style.minWidth = '';
|
152
|
+
col.style.maxWidth = '';
|
153
|
+
col.style.display = '';
|
154
|
+
col.classList.remove('column-hidden');
|
155
|
+
});
|
156
|
+
|
157
|
+
// Show all hidden columns
|
158
|
+
var hiddenColumns = this.table.querySelectorAll('.column-hidden');
|
159
|
+
hiddenColumns.forEach(function(col) {
|
160
|
+
col.classList.remove('column-hidden');
|
161
|
+
col.style.display = '';
|
162
|
+
});
|
163
|
+
|
164
|
+
// Reset all column toggles to checked state
|
165
|
+
var toggles = document.querySelectorAll('.column-toggle');
|
166
|
+
toggles.forEach(function(toggle) {
|
167
|
+
toggle.checked = true;
|
168
|
+
});
|
169
|
+
|
170
|
+
// Hide column panel
|
171
|
+
if (this.columnPanel) {
|
172
|
+
this.columnPanel.style.display = 'none';
|
173
|
+
|
174
|
+
// Also reset the toggle button text
|
175
|
+
var toggleBtn = document.getElementById('toggleColumns');
|
176
|
+
if (toggleBtn) {
|
177
|
+
toggleBtn.innerHTML = '<span class="btn-icon">👁️</span> Show/Hide Columns';
|
178
|
+
}
|
179
|
+
}
|
180
|
+
};
|
181
|
+
|
182
|
+
TableManager.prototype.setupScrollIndicators = function() {
|
183
|
+
if (!this.tableScroll) return;
|
184
|
+
|
185
|
+
var self = this;
|
186
|
+
var scrollIndicatorH = document.getElementById('scrollIndicatorH');
|
187
|
+
var scrollIndicatorV = document.getElementById('scrollIndicatorV');
|
188
|
+
var scrollTimeout;
|
189
|
+
|
190
|
+
this.tableScroll.addEventListener('scroll', function() {
|
191
|
+
self.updateScrollIndicators();
|
192
|
+
|
193
|
+
if (scrollIndicatorH) scrollIndicatorH.classList.add('visible');
|
194
|
+
if (scrollIndicatorV) scrollIndicatorV.classList.add('visible');
|
195
|
+
|
196
|
+
clearTimeout(scrollTimeout);
|
197
|
+
scrollTimeout = setTimeout(function() {
|
198
|
+
if (scrollIndicatorH) scrollIndicatorH.classList.remove('visible');
|
199
|
+
if (scrollIndicatorV) scrollIndicatorV.classList.remove('visible');
|
200
|
+
}, 1000);
|
201
|
+
});
|
202
|
+
|
203
|
+
this.updateScrollIndicators();
|
204
|
+
};
|
205
|
+
|
206
|
+
TableManager.prototype.updateScrollIndicators = function() {
|
207
|
+
var scrollIndicatorH = document.getElementById('scrollIndicatorH');
|
208
|
+
var scrollIndicatorV = document.getElementById('scrollIndicatorV');
|
209
|
+
var scrollThumbH = document.getElementById('scrollThumbH');
|
210
|
+
var scrollThumbV = document.getElementById('scrollThumbV');
|
211
|
+
|
212
|
+
if (!this.tableScroll) return;
|
213
|
+
|
214
|
+
var scrollLeft = this.tableScroll.scrollLeft;
|
215
|
+
var scrollTop = this.tableScroll.scrollTop;
|
216
|
+
var scrollWidth = this.tableScroll.scrollWidth;
|
217
|
+
var scrollHeight = this.tableScroll.scrollHeight;
|
218
|
+
var clientWidth = this.tableScroll.clientWidth;
|
219
|
+
var clientHeight = this.tableScroll.clientHeight;
|
220
|
+
|
221
|
+
if (scrollIndicatorH && scrollThumbH && scrollWidth > clientWidth) {
|
222
|
+
var thumbWidth = (clientWidth / scrollWidth) * 100;
|
223
|
+
var thumbLeft = (scrollLeft / (scrollWidth - clientWidth)) * (100 - thumbWidth);
|
224
|
+
|
225
|
+
scrollThumbH.style.width = thumbWidth + '%';
|
226
|
+
scrollThumbH.style.left = thumbLeft + '%';
|
227
|
+
}
|
228
|
+
|
229
|
+
if (scrollIndicatorV && scrollThumbV && scrollHeight > clientHeight) {
|
230
|
+
var thumbHeight = (clientHeight / scrollHeight) * 100;
|
231
|
+
var thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (100 - thumbHeight);
|
232
|
+
|
233
|
+
scrollThumbV.style.height = thumbHeight + '%';
|
234
|
+
scrollThumbV.style.top = thumbTop + '%';
|
235
|
+
}
|
236
|
+
};
|
237
|
+
|
238
|
+
TableManager.prototype.setupColumnResizing = function() {
|
239
|
+
var self = this;
|
240
|
+
var headers = this.table.querySelectorAll('th:not(.row-num)');
|
241
|
+
|
242
|
+
headers.forEach(function(header, index) {
|
243
|
+
var resizeHandle = document.createElement('div');
|
244
|
+
resizeHandle.className = 'resize-handle';
|
245
|
+
header.appendChild(resizeHandle);
|
246
|
+
|
247
|
+
var startX, startWidth;
|
248
|
+
|
249
|
+
resizeHandle.addEventListener('mousedown', function(e) {
|
250
|
+
self.isResizing = true;
|
251
|
+
startX = e.clientX;
|
252
|
+
startWidth = parseInt(document.defaultView.getComputedStyle(header).width, 10);
|
253
|
+
|
254
|
+
document.addEventListener('mousemove', handleMouseMove);
|
255
|
+
document.addEventListener('mouseup', handleMouseUp);
|
256
|
+
|
257
|
+
e.preventDefault();
|
258
|
+
});
|
259
|
+
|
260
|
+
var handleMouseMove = function(e) {
|
261
|
+
if (!self.isResizing) return;
|
262
|
+
|
263
|
+
var width = startWidth + e.clientX - startX;
|
264
|
+
var minWidth = 80;
|
265
|
+
var maxWidth = 500;
|
266
|
+
var newWidth = Math.max(minWidth, Math.min(maxWidth, width));
|
267
|
+
|
268
|
+
header.style.width = newWidth + 'px';
|
269
|
+
header.style.minWidth = newWidth + 'px';
|
270
|
+
header.style.maxWidth = newWidth + 'px';
|
271
|
+
|
272
|
+
var cells = self.table.querySelectorAll('td[data-column="' + (index + 1) + '"]');
|
273
|
+
cells.forEach(function(cell) {
|
274
|
+
cell.style.width = newWidth + 'px';
|
275
|
+
cell.style.minWidth = newWidth + 'px';
|
276
|
+
cell.style.maxWidth = newWidth + 'px';
|
277
|
+
});
|
278
|
+
};
|
279
|
+
|
280
|
+
var handleMouseUp = function() {
|
281
|
+
self.isResizing = false;
|
282
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
283
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
284
|
+
};
|
285
|
+
});
|
286
|
+
};
|
287
|
+
|
288
|
+
TableManager.prototype.setupKeyboardNavigation = function() {
|
289
|
+
var self = this;
|
290
|
+
|
291
|
+
this.tableScroll.addEventListener('keydown', function(e) {
|
292
|
+
if (!e.target.closest('.results-table')) return;
|
293
|
+
|
294
|
+
var scrollAmount = 50;
|
295
|
+
|
296
|
+
switch(e.key) {
|
297
|
+
case 'ArrowLeft':
|
298
|
+
self.tableScroll.scrollLeft -= scrollAmount;
|
299
|
+
e.preventDefault();
|
300
|
+
break;
|
301
|
+
case 'ArrowRight':
|
302
|
+
self.tableScroll.scrollLeft += scrollAmount;
|
303
|
+
e.preventDefault();
|
304
|
+
break;
|
305
|
+
case 'ArrowUp':
|
306
|
+
self.tableScroll.scrollTop -= scrollAmount;
|
307
|
+
e.preventDefault();
|
308
|
+
break;
|
309
|
+
case 'ArrowDown':
|
310
|
+
self.tableScroll.scrollTop += scrollAmount;
|
311
|
+
e.preventDefault();
|
312
|
+
break;
|
313
|
+
case 'Home':
|
314
|
+
if (e.ctrlKey) {
|
315
|
+
self.tableScroll.scrollTop = 0;
|
316
|
+
self.tableScroll.scrollLeft = 0;
|
317
|
+
} else {
|
318
|
+
self.tableScroll.scrollLeft = 0;
|
319
|
+
}
|
320
|
+
e.preventDefault();
|
321
|
+
break;
|
322
|
+
case 'End':
|
323
|
+
if (e.ctrlKey) {
|
324
|
+
self.tableScroll.scrollTop = self.tableScroll.scrollHeight;
|
325
|
+
self.tableScroll.scrollLeft = self.tableScroll.scrollWidth;
|
326
|
+
} else {
|
327
|
+
self.tableScroll.scrollLeft = self.tableScroll.scrollWidth;
|
328
|
+
}
|
329
|
+
e.preventDefault();
|
330
|
+
break;
|
331
|
+
}
|
332
|
+
});
|
333
|
+
};
|
334
|
+
|
335
|
+
TableManager.prototype.detectColumnTypes = function() {
|
336
|
+
var headers = this.table.querySelectorAll('th:not(.row-num)');
|
337
|
+
|
338
|
+
headers.forEach(function(header, index) {
|
339
|
+
var cells = document.querySelectorAll('td[data-column="' + (index + 1) + '"] .cell-content');
|
340
|
+
var typeSpan = header.querySelector('.header-type');
|
341
|
+
|
342
|
+
if (!typeSpan || cells.length === 0) return;
|
343
|
+
|
344
|
+
var numericCount = 0;
|
345
|
+
var dateCount = 0;
|
346
|
+
var sampleSize = Math.min(10, cells.length);
|
347
|
+
|
348
|
+
for (var i = 0; i < sampleSize; i++) {
|
349
|
+
var cell = cells[i];
|
350
|
+
var value = cell.textContent.trim();
|
351
|
+
|
352
|
+
if (value && value !== 'NULL' && value !== 'empty') {
|
353
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
354
|
+
numericCount++;
|
355
|
+
} else if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
356
|
+
dateCount++;
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
var numericRatio = numericCount / sampleSize;
|
362
|
+
var dateRatio = dateCount / sampleSize;
|
363
|
+
|
364
|
+
if (numericRatio > 0.7) {
|
365
|
+
typeSpan.textContent = 'number';
|
366
|
+
typeSpan.style.background = '#dcfce7';
|
367
|
+
typeSpan.style.color = '#166534';
|
368
|
+
} else if (dateRatio > 0.7) {
|
369
|
+
typeSpan.textContent = 'date';
|
370
|
+
typeSpan.style.background = '#e0f2fe';
|
371
|
+
typeSpan.style.color = '#0c4a6e';
|
372
|
+
} else {
|
373
|
+
typeSpan.textContent = 'text';
|
374
|
+
typeSpan.style.background = '#f1f5f9';
|
375
|
+
typeSpan.style.color = '#64748b';
|
376
|
+
}
|
377
|
+
});
|
378
|
+
};
|
@@ -0,0 +1,25 @@
|
|
1
|
+
function initViewToggles() {
|
2
|
+
const toggleBtns = document.querySelectorAll('.toggle-btn');
|
3
|
+
const views = {
|
4
|
+
table: document.getElementById('table-view'),
|
5
|
+
chart: document.getElementById('chart-view'),
|
6
|
+
stats: document.getElementById('stats-view')
|
7
|
+
};
|
8
|
+
|
9
|
+
toggleBtns.forEach(function(btn) {
|
10
|
+
btn.addEventListener('click', function() {
|
11
|
+
const targetView = this.dataset.view;
|
12
|
+
|
13
|
+
// Update active button
|
14
|
+
toggleBtns.forEach(function(b) { b.classList.remove('active'); });
|
15
|
+
this.classList.add('active');
|
16
|
+
|
17
|
+
// Show/hide views
|
18
|
+
Object.keys(views).forEach(function(viewName) {
|
19
|
+
if (views[viewName]) {
|
20
|
+
views[viewName].style.display = viewName === targetView ? 'block' : 'none';
|
21
|
+
}
|
22
|
+
});
|
23
|
+
});
|
24
|
+
});
|
25
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
//= require_tree ./results
|
2
|
+
|
3
|
+
document.addEventListener('DOMContentLoaded', function() {
|
4
|
+
if (!document.querySelector('.results-section')) return;
|
5
|
+
|
6
|
+
initViewToggles();
|
7
|
+
initChartRendering();
|
8
|
+
initTableManager();
|
9
|
+
});
|
10
|
+
|
11
|
+
function initTableManager() {
|
12
|
+
var tableManager = new TableManager();
|
13
|
+
}
|