Skip to content

Commit ada0ce4

Browse files
Improve admin graph lazy loading UI
1 parent 6b66764 commit ada0ce4

7 files changed

Lines changed: 645 additions & 156 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ DRF_API_LOGGER_DATABASE = True
160160
- 🎛️ **Smart Filtering**: Filter by date, status code, HTTP method, and performance
161161
- 📈 **Visual Analytics**: Built-in performance charts and statistics
162162

163+
Admin graphs are collapsed by default and loaded on demand when opened, keeping
164+
the log list fast and focused during routine investigation. Each graph has its
165+
own control and fetches its backend data only when opened; graph data requests
166+
time out after 30 seconds.
167+
163168
### Admin Dashboard Screenshots
164169

165170
**Admin Home**

docs/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ Enable database storage for API logs:
110110
DRF_API_LOGGER_DATABASE = True # Default: False
111111
112112
Logs will be available in the Django Admin Panel with search, filtering, and analytics charts.
113+
Admin graphs are collapsed by default and loaded on demand when opened, keeping
114+
the log list fast and focused during routine investigation. Each graph has its
115+
own control and fetches its backend data only when opened; graph data requests
116+
time out after 30 seconds.
113117

114118
.. note::
115119

drf_api_logger/admin.py

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from django.conf import settings
44
from django.contrib import admin
55
from django.db.models import Count, Avg
6-
from django.http import HttpResponse
6+
from django.http import Http404, HttpResponse, JsonResponse
7+
from django.urls import path
78
from django.utils.html import escape
89
from django.utils.safestring import mark_safe
910
from drf_api_logger.utils import database_log_enabled
@@ -390,47 +391,90 @@ def response_prettified(self, obj):
390391
change_form_template = 'change_form.html'
391392
date_hierarchy = 'added_on'
392393

394+
def get_urls(self):
395+
urls = super().get_urls()
396+
opts = self.model._meta
397+
custom_urls = [
398+
path(
399+
'chart-data/<slug:chart_name>/',
400+
self.admin_site.admin_view(self.chart_data_view),
401+
name='{}_{}_chart_data'.format(opts.app_label, opts.model_name),
402+
),
403+
]
404+
return custom_urls + urls
405+
393406
def changelist_view(self, request, extra_context=None):
394407
"""
395-
Override to inject custom chart data for status codes and analytics into the context.
408+
Add lightweight chart configuration without precomputing chart data.
396409
"""
397410
response = super(APILogsAdmin, self).changelist_view(request, extra_context)
398411
try:
399-
filtered_query_set = response.context_data["cl"].queryset
412+
response.context_data
400413
except Exception:
401414
return response
402415

403-
# Aggregate logs by date
404-
analytics_model = filtered_query_set.values('added_on__date').annotate(
405-
total=Count('id')
406-
).order_by('total')
407-
408-
# Count each unique status code
409-
status_code_count_mode = filtered_query_set.values('id').values('status_code').annotate(
410-
total=Count('id')).order_by('status_code')
411-
412-
status_code_count_keys = [item.get('status_code') for item in status_code_count_mode]
413-
status_code_count_values = [item.get('total') for item in status_code_count_mode]
414-
415-
# Add chart data to context
416-
extra_context = dict(
417-
analytics=analytics_model,
418-
status_code_count_keys=status_code_count_keys,
419-
status_code_count_values=status_code_count_values
420-
)
421-
422-
# Add profiling chart data when profiling is enabled
423-
if self._DRF_API_LOGGER_ENABLE_PROFILING:
424-
profiled_qs = filtered_query_set.filter(sql_query_count__isnull=False)
425-
sql_distribution = profiled_qs.values('added_on__date').annotate(
426-
avg_queries=Avg('sql_query_count')
427-
).order_by('added_on__date')
428-
extra_context['sql_distribution'] = list(sql_distribution)
429-
extra_context['profiling_enabled'] = True
430-
431-
response.context_data.update(extra_context)
416+
response.context_data.update({
417+
'profiling_enabled': bool(
418+
getattr(settings, 'DRF_API_LOGGER_ENABLE_PROFILING', False)
419+
),
420+
})
432421
return response
433422

423+
def chart_data_view(self, request, chart_name):
424+
queryset = self.get_changelist_instance(request).queryset
425+
if chart_name == 'api-calls-by-day':
426+
return JsonResponse(self._api_calls_by_day_chart_data(queryset))
427+
if chart_name == 'api-calls-by-status-code':
428+
return JsonResponse(self._api_calls_by_status_code_chart_data(queryset))
429+
if chart_name == 'sql-queries-by-day':
430+
if not getattr(settings, 'DRF_API_LOGGER_ENABLE_PROFILING', False):
431+
raise Http404('Chart is not available when profiling is disabled.')
432+
return JsonResponse(self._sql_queries_by_day_chart_data(queryset))
433+
raise Http404('Unknown chart.')
434+
435+
def _api_calls_by_day_chart_data(self, queryset):
436+
rows = queryset.values('added_on__date').annotate(
437+
total=Count('id')
438+
).order_by('added_on__date')
439+
return {
440+
'chart': 'api-calls-by-day',
441+
'label': 'Number of API calls',
442+
'data': [
443+
{
444+
'x': item['added_on__date'].isoformat(),
445+
'y': item['total'],
446+
}
447+
for item in rows
448+
],
449+
}
450+
451+
def _api_calls_by_status_code_chart_data(self, queryset):
452+
rows = queryset.values('status_code').annotate(
453+
total=Count('id')
454+
).order_by('status_code')
455+
return {
456+
'chart': 'api-calls-by-status-code',
457+
'label': 'Number of API calls by status code',
458+
'labels': [item['status_code'] for item in rows],
459+
'values': [item['total'] for item in rows],
460+
}
461+
462+
def _sql_queries_by_day_chart_data(self, queryset):
463+
rows = queryset.filter(sql_query_count__isnull=False).values(
464+
'added_on__date'
465+
).annotate(avg_queries=Avg('sql_query_count')).order_by('added_on__date')
466+
return {
467+
'chart': 'sql-queries-by-day',
468+
'label': 'Avg SQL queries per request',
469+
'data': [
470+
{
471+
'x': item['added_on__date'].isoformat(),
472+
'y': round(float(item['avg_queries']), 1),
473+
}
474+
for item in rows
475+
],
476+
}
477+
434478
def get_queryset(self, request):
435479
"""
436480
Ensure the queryset uses the correct database as configured in settings.

0 commit comments

Comments
 (0)