Skip to content

Commit e52689e

Browse files
committed
Enhance management commands activity visualization and tracking
- Improved activity bar visualization with grid background and dynamic height - Added hover tooltips for activity bars with formatted date and execution count - Enhanced daily stats tracking for management commands - Implemented max value calculation for activity bars - Added more detailed and consistent data representation for command statistics
1 parent 024aa6c commit e52689e

File tree

2 files changed

+137
-15
lines changed

2 files changed

+137
-15
lines changed

website/templates/management_commands.html

+77-8
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,27 @@ <h1 class="text-2xl font-bold mb-6">Management Commands</h1>
8484
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ command.run_count|default:"0" }}</td>
8585
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
8686
{% if command.stats_data %}
87-
<div class="flex items-end h-[30px] w-[100px] gap-[1px]"
88-
title="30-day activity">
89-
{% for data in command.stats_data %}
90-
<div class="flex-1 bg-[#e74c3c] min-w-[2px] rounded-t-[1px]"
91-
style="height: {% widthratio data.value 100 30 %}px"
92-
title="{{ data.date }}: {{ data.value }}"></div>
93-
{% endfor %}
87+
<div class="relative">
88+
<!-- Grid background -->
89+
<div class="absolute inset-0 grid grid-rows-4 w-[120px] h-[30px] pointer-events-none">
90+
<div class="border-t border-gray-200"></div>
91+
<div class="border-t border-gray-200"></div>
92+
<div class="border-t border-gray-200"></div>
93+
<div class="border-t border-gray-200"></div>
94+
</div>
95+
<!-- Bars -->
96+
<div class="flex items-end h-[30px] w-[120px] gap-[1px] relative"
97+
title="30-day activity">
98+
{% for data in command.stats_data %}
99+
<div class="flex-1 {% if data.value > 0 %}bg-[#e74c3c] hover:bg-red-700{% else %}bg-gray-200 hover:bg-gray-300{% endif %} min-w-[2px] rounded-t-sm transition-all duration-200"
100+
style="height: {% if data.value > 0 %}{{ data.height_percent|floatformat:0 }}{% else %}1{% endif %}%"
101+
title="{{ data.date }}: {{ data.value }}"></div>
102+
{% endfor %}
103+
</div>
94104
</div>
105+
<div class="text-xs text-gray-500 mt-1">Max: {{ command.max_value }}</div>
95106
{% else %}
96-
<div class="flex items-center justify-center h-[30px] w-[100px] text-gray-400 text-[10px]">No data</div>
107+
<div class="flex items-center justify-center h-[30px] w-[120px] text-gray-400 text-[10px]">No data</div>
97108
{% endif %}
98109
</td>
99110
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@@ -276,6 +287,64 @@ <h4 class="text-lg font-medium text-gray-900 mb-2">Command Results</h4>
276287
submitCommandForm(this);
277288
});
278289
}
290+
291+
// Enhanced tooltips for activity bars
292+
const activityBars = document.querySelectorAll('.flex-1[title]');
293+
activityBars.forEach(bar => {
294+
bar.addEventListener('mouseenter', function() {
295+
const title = this.getAttribute('title');
296+
if (!title) return;
297+
298+
// Parse the title to get date and value
299+
const [date, valueStr] = title.split(': ');
300+
const value = parseInt(valueStr) || 0;
301+
302+
// Format the date
303+
const formattedDate = new Date(date).toLocaleDateString('en-US', {
304+
month: 'short',
305+
day: 'numeric',
306+
year: 'numeric'
307+
});
308+
309+
// Create tooltip
310+
const tooltip = document.createElement('div');
311+
tooltip.className = 'absolute z-10 bg-gray-900 text-white text-xs rounded py-1 px-2 pointer-events-none';
312+
tooltip.innerHTML = `
313+
<div class="font-bold">${formattedDate}</div>
314+
<div class="flex justify-between gap-2">
315+
<span>Executions:</span>
316+
<span class="font-bold">${value}</span>
317+
</div>
318+
`;
319+
tooltip.style.bottom = '40px';
320+
tooltip.style.left = '50%';
321+
tooltip.style.transform = 'translateX(-50%)';
322+
tooltip.style.whiteSpace = 'nowrap';
323+
324+
// Position tooltip
325+
this.style.position = 'relative';
326+
this.appendChild(tooltip);
327+
328+
// Remove title to prevent default tooltip
329+
this.setAttribute('data-original-title', title);
330+
this.removeAttribute('title');
331+
});
332+
333+
bar.addEventListener('mouseleave', function() {
334+
// Restore title
335+
const originalTitle = this.getAttribute('data-original-title');
336+
if (originalTitle) {
337+
this.setAttribute('title', originalTitle);
338+
this.removeAttribute('data-original-title');
339+
}
340+
341+
// Remove tooltip
342+
const tooltip = this.querySelector('div');
343+
if (tooltip) {
344+
this.removeChild(tooltip);
345+
}
346+
});
347+
});
279348
});
280349

281350
// Command modal functions

website/views/core.py

+60-7
Original file line numberDiff line numberDiff line change
@@ -1671,17 +1671,45 @@ def management_commands(request):
16711671

16721672
# Get stats data for the past 30 days if it exists
16731673
stats_data = []
1674+
1675+
# Create a dictionary to store values for each day in the 30-day period
1676+
date_range = []
1677+
date_values = {}
1678+
1679+
# Generate all dates in the 30-day range
1680+
for i in range(30):
1681+
date = (timezone.now() - timezone.timedelta(days=29 - i)).date()
1682+
date_range.append(date)
1683+
date_values[date.isoformat()] = 0
1684+
1685+
# Get actual stats data
16741686
daily_stats = DailyStats.objects.filter(name=name, created__gte=thirty_days_ago).order_by("created")
16751687

1676-
if daily_stats.exists():
1677-
for stat in daily_stats:
1678-
try:
1679-
value = int(stat.value)
1680-
except (ValueError, TypeError):
1681-
value = 0
1682-
stats_data.append({"date": stat.created.date().isoformat(), "value": value})
1688+
# Fill in the values we have
1689+
max_value = 1 # Minimum value to avoid division by zero
1690+
for stat in daily_stats:
1691+
try:
1692+
value = int(stat.value)
1693+
date_key = stat.created.date().isoformat()
1694+
date_values[date_key] = value
1695+
if value > max_value:
1696+
max_value = value
1697+
except (ValueError, TypeError, KeyError):
1698+
pass
1699+
1700+
# Convert to list format for the template
1701+
for date in date_range:
1702+
date_key = date.isoformat()
1703+
stats_data.append(
1704+
{
1705+
"date": date_key,
1706+
"value": date_values.get(date_key, 0),
1707+
"height_percent": (date_values.get(date_key, 0) / max_value) * 100 if max_value > 0 else 0,
1708+
}
1709+
)
16831710

16841711
command_info["stats_data"] = stats_data
1712+
command_info["max_value"] = max_value
16851713
available_commands.append(command_info)
16861714

16871715
commands = sorted(available_commands, key=lambda x: x["name"])
@@ -1785,6 +1813,31 @@ def run_management_command(request):
17851813
log_entry.success = True
17861814
log_entry.save()
17871815

1816+
# Record execution in DailyStats
1817+
try:
1818+
# Get existing stats for today
1819+
today = timezone.now().date()
1820+
daily_stat, created = DailyStats.objects.get_or_create(
1821+
name=command,
1822+
created__date=today,
1823+
defaults={"value": "1", "created": timezone.now(), "modified": timezone.now()},
1824+
)
1825+
1826+
if not created:
1827+
# Increment the value
1828+
try:
1829+
current_value = int(daily_stat.value)
1830+
daily_stat.value = str(current_value + 1)
1831+
daily_stat.modified = timezone.now()
1832+
daily_stat.save()
1833+
except (ValueError, TypeError):
1834+
# If value is not an integer, set it to 1
1835+
daily_stat.value = "1"
1836+
daily_stat.modified = timezone.now()
1837+
daily_stat.save()
1838+
except Exception as stats_error:
1839+
logging.error(f"Error updating DailyStats: {str(stats_error)}")
1840+
17881841
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
17891842
return JsonResponse({"success": True, "output": output})
17901843

0 commit comments

Comments
 (0)