varanus 0.1.0.dev0__tar.gz → 0.1.0.dev1__tar.gz

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 (69) hide show
  1. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/PKG-INFO +3 -3
  2. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/pyproject.toml +3 -3
  3. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/http.py +13 -3
  4. varanus-0.1.0.dev1/src/varanus/server/asgi.py +7 -0
  5. varanus-0.1.0.dev1/src/varanus/server/migrations/0002_context_node_error_node_log_node_metric_node_and_more.py +79 -0
  6. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/models.py +24 -0
  7. varanus-0.1.0.dev1/src/varanus/server/search/__init__.py +9 -0
  8. varanus-0.1.0.dev1/src/varanus/server/search/base.py +106 -0
  9. varanus-0.1.0.dev1/src/varanus/server/search/date.py +33 -0
  10. varanus-0.1.0.dev1/src/varanus/server/search/facet.py +59 -0
  11. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/settings.py +2 -2
  12. varanus-0.1.0.dev1/src/varanus/server/static/css/varanus.css +4 -0
  13. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/base.html +3 -1
  14. varanus-0.1.0.dev1/src/varanus/server/templates/search/daterange.html +10 -0
  15. varanus-0.1.0.dev1/src/varanus/server/templates/search/multifacet.html +11 -0
  16. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/base.html +19 -12
  17. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/error.html +1 -0
  18. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/log.html +1 -0
  19. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/metric.html +1 -0
  20. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/query.html +1 -0
  21. varanus-0.1.0.dev1/src/varanus/server/templates/site/details/request.html +44 -0
  22. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/errors.html +6 -4
  23. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/logs.html +6 -4
  24. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/metrics.html +6 -4
  25. varanus-0.1.0.dev1/src/varanus/server/templates/site/overview.html +48 -0
  26. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/queries.html +8 -4
  27. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/site/requests.html +6 -4
  28. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/urls.py +5 -0
  29. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/api.py +7 -0
  30. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/base.py +1 -1
  31. varanus-0.1.0.dev1/src/varanus/server/views/site.py +112 -0
  32. varanus-0.1.0.dev0/src/varanus/server/asgi.py +0 -18
  33. varanus-0.1.0.dev0/src/varanus/server/management/commands/runserver.py +0 -15
  34. varanus-0.1.0.dev0/src/varanus/server/search.py +0 -120
  35. varanus-0.1.0.dev0/src/varanus/server/static/css/varanus.css +0 -0
  36. varanus-0.1.0.dev0/src/varanus/server/templates/site/overview.html +0 -14
  37. varanus-0.1.0.dev0/src/varanus/server/views/site.py +0 -69
  38. varanus-0.1.0.dev0/src/varanus/server/websockets.py +0 -13
  39. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/README.md +0 -0
  40. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/__init__.py +0 -0
  41. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/__init__.py +0 -0
  42. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/apps.py +0 -0
  43. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/client.py +0 -0
  44. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/context.py +0 -0
  45. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/loggers.py +0 -0
  46. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/middleware.py +0 -0
  47. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/__init__.py +0 -0
  48. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/base.py +0 -0
  49. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/client/transport/database.py +0 -0
  50. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/events.py +0 -0
  51. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/__init__.py +0 -0
  52. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/__main__.py +0 -0
  53. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/admin.py +0 -0
  54. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/apps.py +0 -0
  55. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/context_processors.py +0 -0
  56. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/management/__init__.py +0 -0
  57. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/management/commands/__init__.py +0 -0
  58. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/management/commands/migrateall.py +0 -0
  59. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/middleware.py +0 -0
  60. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/migrations/0001_initial.py +0 -0
  61. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/migrations/__init__.py +0 -0
  62. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/router.py +0 -0
  63. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/static/js/varanus.js +0 -0
  64. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/dashboard.html +0 -0
  65. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/templates/registration/login.html +0 -0
  66. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/__init__.py +0 -0
  67. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/views/dashboard.py +0 -0
  68. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/server/wsgi.py +0 -0
  69. {varanus-0.1.0.dev0 → varanus-0.1.0.dev1}/src/varanus/utils.py +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: varanus
3
- Version: 0.1.0.dev0
3
+ Version: 0.1.0.dev1
4
4
  Summary: Django application monitoring.
5
5
  Requires-Dist: httpx>=0.27.0
6
6
  Requires-Dist: msgspec>=0.19.0
7
7
  Requires-Dist: cconf>=1.0.0 ; extra == 'server'
8
8
  Requires-Dist: django~=5.2.0 ; extra == 'server'
9
9
  Requires-Dist: django-passkey-auth>=0.2.0 ; extra == 'server'
10
+ Requires-Dist: granian>=2.4.2 ; extra == 'server'
10
11
  Requires-Dist: psycopg[binary]>=3.2.1 ; extra == 'server'
11
- Requires-Dist: uvicorn>=0.30.6 ; extra == 'server'
12
12
  Requires-Dist: websockets>=13.0 ; extra == 'server'
13
13
  Requires-Dist: whitenoise>=6.7.0 ; extra == 'server'
14
- Requires-Python: >=3.11
14
+ Requires-Python: >=3.13
15
15
  Provides-Extra: server
16
16
  Description-Content-Type: text/markdown
17
17
 
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "varanus"
3
- version = "0.1.0.dev0"
3
+ version = "0.1.0.dev1"
4
4
  description = "Django application monitoring."
5
5
  readme = "README.md"
6
- requires-python = ">=3.11"
6
+ requires-python = ">=3.13"
7
7
  dependencies = [
8
8
  "httpx>=0.27.0",
9
9
  "msgspec>=0.19.0",
@@ -14,8 +14,8 @@ server = [
14
14
  "cconf>=1.0.0",
15
15
  "django~=5.2.0",
16
16
  "django-passkey-auth>=0.2.0",
17
+ "granian>=2.4.2",
17
18
  "psycopg[binary]>=3.2.1",
18
- "uvicorn>=0.30.6",
19
19
  "websockets>=13.0",
20
20
  "whitenoise>=6.7.0",
21
21
  ]
@@ -1,3 +1,5 @@
1
+ import platform
2
+ from typing import Any
1
3
  from urllib.parse import SplitResult
2
4
 
3
5
  import httpx
@@ -17,11 +19,19 @@ class HttpTransport(BaseTransport):
17
19
  headers={
18
20
  "X-Varanus-Key": url.username or "",
19
21
  "X-Varanus-Environment": environment or "",
20
- }
22
+ "X-Varanus-Node": platform.node(),
23
+ },
24
+ timeout=1.0,
21
25
  )
22
26
 
27
+ def request(self, url: str, obj: Any):
28
+ try:
29
+ self.client.post(url, content=msgspec.json.encode(obj))
30
+ except Exception as ex:
31
+ print(f"error sending to {url}: {ex}")
32
+
23
33
  def ping(self, info: events.NodeInfo):
24
- self.client.post(self.ping_url, content=msgspec.json.encode(info))
34
+ self.request(self.ping_url, info)
25
35
 
26
36
  def send(self, event: events.Context):
27
- self.client.post(self.event_url, content=msgspec.json.encode(event))
37
+ self.request(self.event_url, event)
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.asgi import get_asgi_application
4
+
5
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "varanus.server.settings")
6
+
7
+ application = get_asgi_application()
@@ -0,0 +1,79 @@
1
+ # Generated by Django 5.2.8 on 2025-11-16 01:38
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("varanus", "0001_initial"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="context",
15
+ name="node",
16
+ field=models.ForeignKey(
17
+ blank=True,
18
+ null=True,
19
+ on_delete=django.db.models.deletion.SET_NULL,
20
+ related_name="%(class)ss",
21
+ to="varanus.node",
22
+ ),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="error",
26
+ name="node",
27
+ field=models.ForeignKey(
28
+ blank=True,
29
+ null=True,
30
+ on_delete=django.db.models.deletion.SET_NULL,
31
+ related_name="%(class)ss",
32
+ to="varanus.node",
33
+ ),
34
+ ),
35
+ migrations.AddField(
36
+ model_name="log",
37
+ name="node",
38
+ field=models.ForeignKey(
39
+ blank=True,
40
+ null=True,
41
+ on_delete=django.db.models.deletion.SET_NULL,
42
+ related_name="%(class)ss",
43
+ to="varanus.node",
44
+ ),
45
+ ),
46
+ migrations.AddField(
47
+ model_name="metric",
48
+ name="node",
49
+ field=models.ForeignKey(
50
+ blank=True,
51
+ null=True,
52
+ on_delete=django.db.models.deletion.SET_NULL,
53
+ related_name="%(class)ss",
54
+ to="varanus.node",
55
+ ),
56
+ ),
57
+ migrations.AddField(
58
+ model_name="query",
59
+ name="node",
60
+ field=models.ForeignKey(
61
+ blank=True,
62
+ null=True,
63
+ on_delete=django.db.models.deletion.SET_NULL,
64
+ related_name="%(class)ss",
65
+ to="varanus.node",
66
+ ),
67
+ ),
68
+ migrations.AddField(
69
+ model_name="request",
70
+ name="node",
71
+ field=models.ForeignKey(
72
+ blank=True,
73
+ null=True,
74
+ on_delete=django.db.models.deletion.SET_NULL,
75
+ related_name="%(class)ss",
76
+ to="varanus.node",
77
+ ),
78
+ ),
79
+ ]
@@ -7,6 +7,7 @@ from typing import Self
7
7
  import msgspec
8
8
  import sqlparse
9
9
  from django.conf import settings
10
+ from django.contrib.contenttypes.models import ContentType
10
11
  from django.core.management import call_command
11
12
  from django.core.validators import RegexValidator
12
13
  from django.db import connections, models
@@ -256,6 +257,13 @@ class EventModel(EnvironmentModel):
256
257
  event_id = models.UUIDField(db_index=True)
257
258
  timestamp = models.DateTimeField(default=timezone.now)
258
259
  tags = models.JSONField(default=dict, blank=True)
260
+ node = models.ForeignKey(
261
+ Node,
262
+ on_delete=models.SET_NULL,
263
+ related_name="%(class)ss",
264
+ null=True,
265
+ blank=True,
266
+ )
259
267
 
260
268
  class Meta:
261
269
  abstract = True
@@ -265,6 +273,18 @@ class EventModel(EnvironmentModel):
265
273
  def from_event(cls, event: events.Event, **extra):
266
274
  raise NotImplementedError()
267
275
 
276
+ def get_absolute_url(self):
277
+ ct = ContentType.objects.get_for_model(self)
278
+ return reverse(
279
+ "event-details",
280
+ kwargs={
281
+ "slug": self.site.slug,
282
+ "event_id": self.event_id,
283
+ "model": ct.model,
284
+ "pk": self.pk,
285
+ },
286
+ )
287
+
268
288
 
269
289
  class FingerprintModel(models.Model):
270
290
  fingerprint = models.CharField(max_length=64, db_index=True)
@@ -529,6 +549,10 @@ class Query(ContextualModel, FingerprintModel):
529
549
  return stripped
530
550
  return stripped[:79] + "…"
531
551
 
552
+ @property
553
+ def sql_formatted(self):
554
+ return sqlparse.format(self.sql, reindent=True, keyword_case="upper")
555
+
532
556
  def fingerprint_parts(self):
533
557
  return self.sql.split()
534
558
 
@@ -0,0 +1,9 @@
1
+ from .base import Search
2
+ from .date import DateRange
3
+ from .facet import MultiFacet
4
+
5
+ __all__ = [
6
+ "Search",
7
+ "DateRange",
8
+ "MultiFacet",
9
+ ]
@@ -0,0 +1,106 @@
1
+ from typing import ClassVar, Mapping, Self
2
+
3
+ from django.db.models import QuerySet
4
+ from django.http import HttpRequest
5
+ from django.template import loader
6
+ from django.utils.datastructures import MultiValueDict
7
+
8
+ type StringValues = dict[str, list[str]]
9
+
10
+
11
+ def string_data(data) -> StringValues:
12
+ if isinstance(data, MultiValueDict):
13
+ return {str(key): [str(v) for v in values] for key, values in data.lists()}
14
+ elif isinstance(data, Mapping):
15
+ return {
16
+ str(key): (
17
+ [str(v) for v in values]
18
+ if isinstance(values, (list, tuple))
19
+ else ([] if values is None else [str(values)])
20
+ )
21
+ for key, values in data.items()
22
+ }
23
+ return {}
24
+
25
+
26
+ class SearchField:
27
+ template_name: str
28
+
29
+ def __init__(self, label=None, field_name=None):
30
+ self.name = ""
31
+ self.label = label or ""
32
+ self.field_name = field_name or ""
33
+ self.prefix = ""
34
+ self.id = ""
35
+
36
+ def __set_name__(self, owner, name: str):
37
+ # print("__set_name__", owner, name)
38
+ assert issubclass(owner, Search)
39
+ self.name = name
40
+ self.prefix = f"{name}_"
41
+ self.id = f"id_{name}"
42
+ if not self.label:
43
+ self.label = name.capitalize()
44
+ if not self.field_name:
45
+ self.field_name = name
46
+ if not hasattr(owner, "_fields"):
47
+ owner._fields = []
48
+ owner._fields.append(self)
49
+
50
+ def __get__(self, instance: "Search", owner=None) -> StringValues | Self:
51
+ # print("__get__", instance, owner)
52
+ if owner is None:
53
+ return self
54
+ return {
55
+ key[len(self.prefix) :]: values
56
+ for key, values in instance._data.items()
57
+ if key.startswith(self.prefix)
58
+ }
59
+
60
+ def __set__(self, instance, value):
61
+ print("__set__", instance, value)
62
+
63
+ def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
64
+ raise NotImplementedError()
65
+
66
+ def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
67
+ return {
68
+ "field": self,
69
+ }
70
+
71
+ def render(
72
+ self,
73
+ queryset: QuerySet,
74
+ field_data: StringValues,
75
+ request: HttpRequest | None = None,
76
+ ) -> str:
77
+ return loader.render_to_string(
78
+ self.template_name,
79
+ self.get_context(queryset, field_data),
80
+ request,
81
+ )
82
+
83
+
84
+ class Search:
85
+ _fields: ClassVar[list[SearchField]]
86
+
87
+ def __init__(self, queryset: QuerySet, data=None):
88
+ self._queryset = queryset
89
+ self._data = string_data(data)
90
+
91
+ def queryset(self, for_field=None):
92
+ qs = self._queryset
93
+ for field in self._fields:
94
+ if field == for_field:
95
+ continue
96
+ field_data = getattr(self, field.name)
97
+ qs = field.apply(qs, field_data)
98
+ return qs
99
+
100
+ def render(self, request: HttpRequest | None = None) -> str:
101
+ fragments = []
102
+ for field in self._fields:
103
+ qs = self.queryset(for_field=field)
104
+ field_data = getattr(self, field.name)
105
+ fragments.append(field.render(qs, field_data, request))
106
+ return "".join(fragments)
@@ -0,0 +1,33 @@
1
+ import datetime
2
+
3
+ from django.db.models import QuerySet
4
+ from django.utils.dateparse import parse_date
5
+
6
+ from .base import SearchField, StringValues
7
+
8
+
9
+ def date_value(field_data: StringValues, name: str) -> datetime.date | None:
10
+ if name not in field_data:
11
+ return None
12
+ if not field_data[name]:
13
+ return None
14
+ return parse_date(field_data[name][0])
15
+
16
+
17
+ class DateRange(SearchField):
18
+ template_name = "search/daterange.html"
19
+
20
+ def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
21
+ filters = {}
22
+ if start := date_value(field_data, "start"):
23
+ filters[f"{self.field_name}__date__gte"] = start
24
+ if end := date_value(field_data, "end"):
25
+ filters[f"{self.field_name}__date__lte"] = end
26
+ return queryset.filter(**filters)
27
+
28
+ def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
29
+ return {
30
+ **super().get_context(queryset, field_data),
31
+ "start": date_value(field_data, "start"),
32
+ "end": date_value(field_data, "end"),
33
+ }
@@ -0,0 +1,59 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from django.db.models import Count, QuerySet
4
+
5
+ from .base import SearchField, StringValues
6
+
7
+
8
+ class Choice:
9
+ id: str
10
+ value: str
11
+ label: str
12
+ count: int
13
+
14
+ def __init__(self, field: SearchField, value: Any):
15
+ self.value = self.stringify(value)
16
+ self.label = self.value
17
+ self.id = field.id + "_" + self.value.replace(" ", "")
18
+ self.count = 0
19
+
20
+ @classmethod
21
+ def stringify(cls, value):
22
+ return str(value)
23
+
24
+
25
+ class Facet(SearchField):
26
+ choice_class: ClassVar[type[Choice]] = Choice
27
+
28
+ def get_choices(self, queryset, with_counts=True):
29
+ choices = [
30
+ self.choice_class(self, c)
31
+ for c in queryset.order_by(self.field_name)
32
+ .values_list(self.field_name, flat=True)
33
+ .distinct(self.field_name)
34
+ ]
35
+ if with_counts:
36
+ counts = {
37
+ self.choice_class.stringify(r[self.field_name]): r["num"]
38
+ for r in queryset.values(self.field_name).annotate(num=Count("*"))
39
+ }
40
+ for c in choices:
41
+ c.count = counts.get(c.value, 0)
42
+ return choices
43
+
44
+
45
+ class MultiFacet(Facet):
46
+ template_name = "search/multifacet.html"
47
+
48
+ def apply(self, queryset: QuerySet, field_data: StringValues) -> QuerySet:
49
+ filters = {}
50
+ if selected := field_data.get("s", []):
51
+ filters[f"{self.field_name}__in"] = selected
52
+ return queryset.filter(**filters)
53
+
54
+ def get_context(self, queryset: QuerySet, field_data: StringValues) -> dict:
55
+ return {
56
+ **super().get_context(queryset, field_data),
57
+ "choices": self.get_choices(queryset),
58
+ "selected": field_data.get("s", []),
59
+ }
@@ -48,9 +48,9 @@ INSTALLED_APPS = [
48
48
  "django.contrib.contenttypes",
49
49
  "django.contrib.sessions",
50
50
  "django.contrib.messages",
51
- # Put this before staticfiles, so we use our runserver command.
52
- "varanus.server.apps.VaranusServer",
51
+ "whitenoise.runserver_nostatic",
53
52
  "django.contrib.staticfiles",
53
+ "varanus.server.apps.VaranusServer",
54
54
  "cconf",
55
55
  "passkeys",
56
56
  ]
@@ -0,0 +1,4 @@
1
+ .table-header th {
2
+ background-color: var(--bs-secondary-bg-subtle) !important;
3
+ border-color: var(--bs-secondary-border-subtle) !important;
4
+ }
@@ -2,7 +2,7 @@
2
2
  {% load i18n %}
3
3
 
4
4
  <!doctype html>
5
- <html lang="en" data-bs-theme="light">
5
+ <html lang="en" data-bs-theme="dark">
6
6
  <head>
7
7
  <meta charset="utf-8" />
8
8
  <meta http-equiv="x-ua-compatible" content="ie=edge" />
@@ -68,6 +68,8 @@
68
68
  </footer>
69
69
 
70
70
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/js/bootstrap.bundle.min.js"></script>
71
+ <script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
72
+
71
73
  <script src="{% static 'js/varanus.js' %}"></script>
72
74
 
73
75
  {% block javascript %}{% endblock javascript %}
@@ -0,0 +1,10 @@
1
+ <div class="card mb-3">
2
+ <div class="card-header">{{ field.label }}</div>
3
+ <div class="card-body">
4
+ <div class="input-group">
5
+ <input type="date" class="form-control" placeholder="From" name="{{ field.prefix }}start" value="{{ start.isoformat|default:'' }}" />
6
+ <span class="input-group-text">-</span>
7
+ <input type="date" class="form-control" placeholder="To" name="{{ field.prefix }}end" value="{{ end.isoformat|default:'' }}" />
8
+ </div>
9
+ </div>
10
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="card mb-3">
2
+ <div class="card-header">{{ field.label }}</div>
3
+ <div class="card-body">
4
+ {% for c in choices %}
5
+ <div class="form-check">
6
+ <input type="checkbox" id="{{ c.id }}" class="form-check-input" name="{{ field.prefix }}s" value="{{ c.value }}"{% if c.value in selected %} checked{% endif %} />
7
+ <label for="{{ c.id }}">{{ c.label }} ({{ c.count }})</label>
8
+ </div>
9
+ {% endfor %}
10
+ </div>
11
+ </div>
@@ -25,21 +25,28 @@
25
25
  </li>
26
26
  </ul>
27
27
 
28
- <!--
29
28
  <div class="container-fluid">
30
- <form action="" method="GET">
31
- {% for search_field, form_fields in filter_form.fieldsets.items %}
32
- {% for field in form_fields %}
33
- {{ field }}
34
- {% endfor %}
35
- {% endfor %}
36
- <button>{% trans "Filter" %}</button>
37
- </form>
29
+ <div class="row">
30
+ <div class="col-md-3">
31
+ {% block search %}
32
+ <form action="" method="get">
33
+ {{ search.render|safe }}
34
+ <button class="btn btn-primary">Filter</button>
35
+ </form>
36
+ {% endblock search %}
37
+ </div>
38
+ <div class="col-md-9">
39
+ {% block page %}{% endblock page %}
40
+ </div>
41
+ </div>
38
42
  </div>
39
- -->
40
43
 
41
- <div class="container-fluid">
42
- {% block page %}{% endblock page %}
44
+ <div class="offcanvas offcanvas-end" style="width:33%" tabindex="-1" id="inspector">
45
+ <div class="offcanvas-header">
46
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
47
+ </div>
48
+ <div class="offcanvas-body" id="inspector-content">
49
+ </div>
43
50
  </div>
44
51
  {% endblock content %}
45
52
 
@@ -0,0 +1 @@
1
+ <code><pre>{{ object.sql_formatted }}</pre></code>
@@ -0,0 +1,44 @@
1
+ {% load i18n %}
2
+
3
+ <table class="table table-sm">
4
+ <tbody>
5
+ <tr>
6
+ <th>{% trans "Host" %}</th>
7
+ <td>{{ object.host }}</td>
8
+ </tr>
9
+ <tr>
10
+ <th>{% trans "Node" %}</th>
11
+ <td>{{ object.node }}</td>
12
+ </tr>
13
+ <tr>
14
+ <th>{% trans "Request" %}</th>
15
+ <td>{{ object.method }} {{ object.path }}</td>
16
+ </tr>
17
+ <tr>
18
+ <th>{% trans "Status" %}</th>
19
+ <td>{{ object.http_status }}</td>
20
+ </tr>
21
+ <tr>
22
+ <th>{% trans "IP" %}</th>
23
+ <td>{{ object.ip }}</td>
24
+ </tr>
25
+ <tr>
26
+ <th>{% trans "User" %}</th>
27
+ <td>{{ object.user }}</td>
28
+ </tr>
29
+ </tbody>
30
+ </table>
31
+
32
+ {% if object.headers %}
33
+ <h5>{% trans "Headers" %}</h5>
34
+ <table class="table table-sm">
35
+ <tbody>
36
+ {% for name, value in object.headers.items %}
37
+ <tr>
38
+ <th>{{ name }}</th>
39
+ <td>{{ value }}</th>
40
+ </tr>
41
+ {% endfor %}
42
+ </tbody>
43
+ </table>
44
+ {% endif %}
@@ -3,8 +3,8 @@
3
3
  {% load i18n %}
4
4
 
5
5
  {% block page %}
6
- <table class="table">
7
- <thead>
6
+ <table class="table table-bordered">
7
+ <thead class="table-header">
8
8
  <tr>
9
9
  <th>{% trans "Context" %}</th>
10
10
  <th>{% trans "Kind" %}</th>
@@ -14,9 +14,11 @@
14
14
  </tr>
15
15
  </thead>
16
16
  <tbody>
17
- {% for e in errors %}
17
+ {% for e in results %}
18
18
  <tr>
19
- <td>{{ e.context }}</td>
19
+ <td>
20
+ <a href="{{ e.get_absolute_url }}" data-bs-toggle="offcanvas" data-bs-target="#inspector" hx-get="{{ e.get_absolute_url }}" hx-target="#inspector-content">{{ e.context }}</a>
21
+ </td>
20
22
  <td>{{ e.kind }}</td>
21
23
  <td>{{ e.message }}</td>
22
24
  <td>{{ e.module }}</td>
@@ -3,8 +3,8 @@
3
3
  {% load i18n %}
4
4
 
5
5
  {% block page %}
6
- <table class="table">
7
- <thead>
6
+ <table class="table table-bordered">
7
+ <thead class="table-header">
8
8
  <tr>
9
9
  <th>{% trans "Context" %}</th>
10
10
  <th>{% trans "Level" %}</th>
@@ -15,9 +15,11 @@
15
15
  </tr>
16
16
  </thead>
17
17
  <tbody>
18
- {% for log in logs %}
18
+ {% for log in results %}
19
19
  <tr>
20
- <td>{{ log.context }}</td>
20
+ <td>
21
+ <a href="{{ log.get_absolute_url }}" data-bs-toggle="offcanvas" data-bs-target="#inspector" hx-get="{{ log.get_absolute_url }}" hx-target="#inspector-content">{{ log.context }}</a>
22
+ </td>
21
23
  <td>{{ log.level_name }}</td>
22
24
  <td>{{ log.message }}</td>
23
25
  <td>{{ log.name }}</td>
@@ -3,8 +3,8 @@
3
3
  {% load i18n %}
4
4
 
5
5
  {% block page %}
6
- <table class="table">
7
- <thead>
6
+ <table class="table table-bordered">
7
+ <thead class="table-header">
8
8
  <tr>
9
9
  <th>{% trans "Context" %}</th>
10
10
  <th>{% trans "Name" %}</th>
@@ -17,9 +17,11 @@
17
17
  </tr>
18
18
  </thead>
19
19
  <tbody>
20
- {% for m in metrics %}
20
+ {% for m in results %}
21
21
  <tr>
22
- <td>{{ m.context }}</td>
22
+ <td>
23
+ <a href="{{ m.get_absolute_url }}" data-bs-toggle="offcanvas" data-bs-target="#inspector" hx-get="{{ m.get_absolute_url }}" hx-target="#inspector-content">{{ m.context }}</a>
24
+ </td>
23
25
  <td>{{ m.name }}</td>
24
26
  <td>{{ m.agg_count }}</td>
25
27
  <td>{{ m.agg_sum }}</td>