django-cfg 1.2.7__py3-none-any.whl → 1.2.9__py3-none-any.whl
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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/urls.py +2 -2
- django_cfg/modules/django_unfold/callbacks/__init__.py +9 -0
- django_cfg/modules/django_unfold/callbacks/actions.py +50 -0
- django_cfg/modules/django_unfold/callbacks/base.py +98 -0
- django_cfg/modules/django_unfold/callbacks/charts.py +224 -0
- django_cfg/modules/django_unfold/callbacks/commands.py +40 -0
- django_cfg/modules/django_unfold/callbacks/main.py +191 -0
- django_cfg/modules/django_unfold/callbacks/revolution.py +76 -0
- django_cfg/modules/django_unfold/callbacks/statistics.py +240 -0
- django_cfg/modules/django_unfold/callbacks/system.py +180 -0
- django_cfg/modules/django_unfold/callbacks/users.py +65 -0
- django_cfg/modules/django_unfold/models/config.py +10 -3
- django_cfg/modules/django_unfold/tailwind.py +68 -0
- django_cfg/templates/admin/components/action_grid.html +49 -0
- django_cfg/templates/admin/components/card.html +50 -0
- django_cfg/templates/admin/components/data_table.html +67 -0
- django_cfg/templates/admin/components/metric_card.html +39 -0
- django_cfg/templates/admin/components/modal.html +58 -0
- django_cfg/templates/admin/components/progress_bar.html +25 -0
- django_cfg/templates/admin/components/section_header.html +26 -0
- django_cfg/templates/admin/components/stat_item.html +32 -0
- django_cfg/templates/admin/components/stats_grid.html +72 -0
- django_cfg/templates/admin/components/status_badge.html +28 -0
- django_cfg/templates/admin/components/user_avatar.html +27 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +7 -7
- django_cfg/templates/admin/snippets/components/activity_tracker.html +48 -11
- django_cfg/templates/admin/snippets/components/charts_section.html +63 -13
- django_cfg/templates/admin/snippets/components/django_commands.html +196 -72
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -47
- django_cfg/templates/admin/snippets/components/recent_activity.html +28 -38
- django_cfg/templates/admin/snippets/components/recent_users_table.html +22 -53
- django_cfg/templates/admin/snippets/components/stats_cards.html +2 -66
- django_cfg/templates/admin/snippets/components/system_health.html +13 -63
- django_cfg/templates/admin/snippets/components/system_metrics.html +8 -25
- django_cfg/templates/admin/snippets/tabs/commands_tab.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +4 -4
- django_cfg/templates/admin/snippets/zones/zones_table.html +12 -33
- django_cfg/templatetags/django_cfg.py +2 -1
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.9.dist-info}/METADATA +2 -1
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.9.dist-info}/RECORD +44 -24
- django_cfg/modules/django_unfold/callbacks.py +0 -795
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
"""
|
2
|
+
Django Revolution integration callbacks.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import Dict, Any, List, Tuple
|
7
|
+
|
8
|
+
from django.conf import settings
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class RevolutionCallbacks:
|
14
|
+
"""Django Revolution integration callbacks."""
|
15
|
+
|
16
|
+
def get_revolution_zones_data(self) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
17
|
+
"""Get Django Revolution zones data."""
|
18
|
+
try:
|
19
|
+
# Try to get revolution config from Django settings
|
20
|
+
revolution_config = getattr(settings, "DJANGO_REVOLUTION", {})
|
21
|
+
zones = revolution_config.get("zones", {})
|
22
|
+
api_prefix = revolution_config.get("api_prefix", "apix")
|
23
|
+
|
24
|
+
zones_data = []
|
25
|
+
total_apps = 0
|
26
|
+
total_endpoints = 0
|
27
|
+
|
28
|
+
for zone_name, zone_config in zones.items():
|
29
|
+
# Handle both dict and object access
|
30
|
+
if isinstance(zone_config, dict):
|
31
|
+
title = zone_config.get("title", zone_name.title())
|
32
|
+
description = zone_config.get("description", f"{zone_name} zone")
|
33
|
+
apps = zone_config.get("apps", [])
|
34
|
+
public = zone_config.get("public", False)
|
35
|
+
auth_required = zone_config.get("auth_required", True)
|
36
|
+
else:
|
37
|
+
# Handle object access (for ZoneConfig instances)
|
38
|
+
title = getattr(zone_config, "title", zone_name.title())
|
39
|
+
description = getattr(zone_config, "description", f"{zone_name} zone")
|
40
|
+
apps = getattr(zone_config, "apps", [])
|
41
|
+
public = getattr(zone_config, "public", False)
|
42
|
+
auth_required = getattr(zone_config, "auth_required", True)
|
43
|
+
|
44
|
+
# Count actual endpoints by checking URL patterns (simplified estimate)
|
45
|
+
endpoint_count = len(apps) * 3 # Conservative estimate
|
46
|
+
|
47
|
+
zones_data.append({
|
48
|
+
"name": zone_name,
|
49
|
+
"title": title,
|
50
|
+
"description": description,
|
51
|
+
"app_count": len(apps),
|
52
|
+
"endpoint_count": endpoint_count,
|
53
|
+
"status": "active",
|
54
|
+
"public": public,
|
55
|
+
"auth_required": auth_required,
|
56
|
+
"schema_url": f"/schema/{zone_name}/schema/",
|
57
|
+
"swagger_url": f"/schema/{zone_name}/schema/swagger/",
|
58
|
+
"redoc_url": f"/schema/{zone_name}/redoc/",
|
59
|
+
"api_url": f"/{api_prefix}/{zone_name}/",
|
60
|
+
})
|
61
|
+
|
62
|
+
total_apps += len(apps)
|
63
|
+
total_endpoints += endpoint_count
|
64
|
+
|
65
|
+
return zones_data, {
|
66
|
+
"total_apps": total_apps,
|
67
|
+
"total_endpoints": total_endpoints,
|
68
|
+
"total_zones": len(zones),
|
69
|
+
}
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"Error getting revolution zones: {e}")
|
72
|
+
return [], {
|
73
|
+
"total_apps": 0,
|
74
|
+
"total_endpoints": 0,
|
75
|
+
"total_zones": 0,
|
76
|
+
}
|
@@ -0,0 +1,240 @@
|
|
1
|
+
"""
|
2
|
+
Statistics callbacks for dashboard.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import List, Dict, Any
|
7
|
+
from datetime import timedelta
|
8
|
+
|
9
|
+
from django.db.models import Count
|
10
|
+
from django.utils import timezone
|
11
|
+
from django.contrib.auth import get_user_model
|
12
|
+
from django.apps import apps
|
13
|
+
|
14
|
+
from ..models.dashboard import StatCard
|
15
|
+
from ..icons import Icons
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class StatisticsCallbacks:
|
21
|
+
"""Statistics-related callbacks."""
|
22
|
+
|
23
|
+
def _get_user_model(self):
|
24
|
+
"""Get the user model safely."""
|
25
|
+
return get_user_model()
|
26
|
+
|
27
|
+
def get_user_statistics(self) -> List[StatCard]:
|
28
|
+
"""Get user-related statistics as Pydantic models."""
|
29
|
+
try:
|
30
|
+
User = self._get_user_model()
|
31
|
+
|
32
|
+
total_users = User.objects.count()
|
33
|
+
active_users = User.objects.filter(is_active=True).count()
|
34
|
+
new_users_7d = User.objects.filter(
|
35
|
+
date_joined__gte=timezone.now() - timedelta(days=7)
|
36
|
+
).count()
|
37
|
+
staff_users = User.objects.filter(is_staff=True).count()
|
38
|
+
|
39
|
+
return [
|
40
|
+
StatCard(
|
41
|
+
title="Total Users",
|
42
|
+
value=f"{total_users:,}",
|
43
|
+
icon=Icons.PEOPLE,
|
44
|
+
change=f"+{new_users_7d}" if new_users_7d > 0 else None,
|
45
|
+
change_type="positive" if new_users_7d > 0 else "neutral",
|
46
|
+
description="Registered users",
|
47
|
+
),
|
48
|
+
StatCard(
|
49
|
+
title="Active Users",
|
50
|
+
value=f"{active_users:,}",
|
51
|
+
icon=Icons.PERSON,
|
52
|
+
change=(
|
53
|
+
f"{(active_users/total_users*100):.1f}%"
|
54
|
+
if total_users > 0
|
55
|
+
else "0%"
|
56
|
+
),
|
57
|
+
change_type=(
|
58
|
+
"positive" if active_users > total_users * 0.7 else "neutral"
|
59
|
+
),
|
60
|
+
description="Currently active",
|
61
|
+
),
|
62
|
+
StatCard(
|
63
|
+
title="New This Week",
|
64
|
+
value=f"{new_users_7d:,}",
|
65
|
+
icon=Icons.PERSON_ADD,
|
66
|
+
change_type="positive" if new_users_7d > 0 else "neutral",
|
67
|
+
description="Last 7 days",
|
68
|
+
),
|
69
|
+
StatCard(
|
70
|
+
title="Staff Members",
|
71
|
+
value=f"{staff_users:,}",
|
72
|
+
icon=Icons.ADMIN_PANEL_SETTINGS,
|
73
|
+
change=(
|
74
|
+
f"{(staff_users/total_users*100):.1f}%" if total_users > 0 else "0%"
|
75
|
+
),
|
76
|
+
change_type="neutral",
|
77
|
+
description="Administrative access",
|
78
|
+
),
|
79
|
+
]
|
80
|
+
except Exception as e:
|
81
|
+
logger.error(f"Error getting user statistics: {e}")
|
82
|
+
return [
|
83
|
+
StatCard(
|
84
|
+
title="Users",
|
85
|
+
value="N/A",
|
86
|
+
icon=Icons.PEOPLE,
|
87
|
+
description="Data unavailable",
|
88
|
+
)
|
89
|
+
]
|
90
|
+
|
91
|
+
def get_support_statistics(self) -> List[StatCard]:
|
92
|
+
"""Get support ticket statistics as Pydantic models."""
|
93
|
+
try:
|
94
|
+
# Check if support is enabled
|
95
|
+
if not self.is_support_enabled():
|
96
|
+
return []
|
97
|
+
|
98
|
+
from django_cfg.apps.support.models import Ticket
|
99
|
+
|
100
|
+
total_tickets = Ticket.objects.count()
|
101
|
+
open_tickets = Ticket.objects.filter(status='open').count()
|
102
|
+
resolved_tickets = Ticket.objects.filter(status='resolved').count()
|
103
|
+
new_tickets_7d = Ticket.objects.filter(
|
104
|
+
created_at__gte=timezone.now() - timedelta(days=7)
|
105
|
+
).count()
|
106
|
+
|
107
|
+
return [
|
108
|
+
StatCard(
|
109
|
+
title="Total Tickets",
|
110
|
+
value=f"{total_tickets:,}",
|
111
|
+
icon=Icons.SUPPORT_AGENT,
|
112
|
+
change=f"+{new_tickets_7d}" if new_tickets_7d > 0 else None,
|
113
|
+
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
114
|
+
description="All support tickets",
|
115
|
+
),
|
116
|
+
StatCard(
|
117
|
+
title="Open Tickets",
|
118
|
+
value=f"{open_tickets:,}",
|
119
|
+
icon=Icons.PENDING,
|
120
|
+
change=(
|
121
|
+
f"{(open_tickets/total_tickets*100):.1f}%"
|
122
|
+
if total_tickets > 0
|
123
|
+
else "0%"
|
124
|
+
),
|
125
|
+
change_type=(
|
126
|
+
"negative" if open_tickets > total_tickets * 0.3
|
127
|
+
else "positive" if open_tickets == 0
|
128
|
+
else "neutral"
|
129
|
+
),
|
130
|
+
description="Awaiting response",
|
131
|
+
),
|
132
|
+
StatCard(
|
133
|
+
title="Resolved",
|
134
|
+
value=f"{resolved_tickets:,}",
|
135
|
+
icon=Icons.CHECK_CIRCLE,
|
136
|
+
change=(
|
137
|
+
f"{(resolved_tickets/total_tickets*100):.1f}%"
|
138
|
+
if total_tickets > 0
|
139
|
+
else "0%"
|
140
|
+
),
|
141
|
+
change_type="positive",
|
142
|
+
description="Successfully resolved",
|
143
|
+
),
|
144
|
+
StatCard(
|
145
|
+
title="New This Week",
|
146
|
+
value=f"{new_tickets_7d:,}",
|
147
|
+
icon=Icons.NEW_RELEASES,
|
148
|
+
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
149
|
+
description="Last 7 days",
|
150
|
+
),
|
151
|
+
]
|
152
|
+
except Exception as e:
|
153
|
+
logger.error(f"Error getting support statistics: {e}")
|
154
|
+
return [
|
155
|
+
StatCard(
|
156
|
+
title="Support",
|
157
|
+
value="N/A",
|
158
|
+
icon=Icons.SUPPORT_AGENT,
|
159
|
+
description="Data unavailable",
|
160
|
+
)
|
161
|
+
]
|
162
|
+
|
163
|
+
def get_app_statistics(self) -> Dict[str, Any]:
|
164
|
+
"""Get statistics for all apps and their models."""
|
165
|
+
stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
|
166
|
+
|
167
|
+
# Get all installed apps
|
168
|
+
for app_config in apps.get_app_configs():
|
169
|
+
app_label = app_config.label
|
170
|
+
|
171
|
+
# Skip system apps
|
172
|
+
if app_label in ["admin", "contenttypes", "sessions", "auth"]:
|
173
|
+
continue
|
174
|
+
|
175
|
+
app_stats = self._get_app_stats(app_label)
|
176
|
+
if app_stats:
|
177
|
+
stats["apps"][app_label] = app_stats
|
178
|
+
stats["total_records"] += app_stats.get("total_records", 0)
|
179
|
+
stats["total_models"] += app_stats.get("model_count", 0)
|
180
|
+
stats["total_apps"] += 1
|
181
|
+
|
182
|
+
return stats
|
183
|
+
|
184
|
+
def _get_app_stats(self, app_label: str) -> Dict[str, Any]:
|
185
|
+
"""Get statistics for a specific app."""
|
186
|
+
try:
|
187
|
+
app_config = apps.get_app_config(app_label)
|
188
|
+
# Convert generator to list to avoid len() error
|
189
|
+
models_list = list(app_config.get_models())
|
190
|
+
|
191
|
+
if not models_list:
|
192
|
+
return None
|
193
|
+
|
194
|
+
app_stats = {
|
195
|
+
"name": app_config.verbose_name or app_label.title(),
|
196
|
+
"models": {},
|
197
|
+
"total_records": 0,
|
198
|
+
"model_count": len(models_list),
|
199
|
+
}
|
200
|
+
|
201
|
+
for model in models_list:
|
202
|
+
try:
|
203
|
+
# Get model statistics
|
204
|
+
model_stats = self._get_model_stats(model)
|
205
|
+
if model_stats:
|
206
|
+
app_stats["models"][model._meta.model_name] = model_stats
|
207
|
+
app_stats["total_records"] += model_stats.get("count", 0)
|
208
|
+
except Exception:
|
209
|
+
continue
|
210
|
+
|
211
|
+
return app_stats
|
212
|
+
|
213
|
+
except Exception:
|
214
|
+
return None
|
215
|
+
|
216
|
+
def _get_model_stats(self, model) -> Dict[str, Any]:
|
217
|
+
"""Get statistics for a specific model."""
|
218
|
+
try:
|
219
|
+
# Get basic model info
|
220
|
+
model_stats = {
|
221
|
+
"name": model._meta.verbose_name_plural
|
222
|
+
or model._meta.verbose_name
|
223
|
+
or model._meta.model_name,
|
224
|
+
"count": model.objects.count(),
|
225
|
+
"fields_count": len(model._meta.fields),
|
226
|
+
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
227
|
+
}
|
228
|
+
|
229
|
+
return model_stats
|
230
|
+
|
231
|
+
except Exception:
|
232
|
+
return None
|
233
|
+
|
234
|
+
def is_support_enabled(self) -> bool:
|
235
|
+
"""Check if support module is enabled."""
|
236
|
+
try:
|
237
|
+
from django_cfg.apps.support.models import Ticket
|
238
|
+
return True
|
239
|
+
except ImportError:
|
240
|
+
return False
|
@@ -0,0 +1,180 @@
|
|
1
|
+
"""
|
2
|
+
System health and metrics callbacks.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import shutil
|
7
|
+
from typing import List, Dict, Any
|
8
|
+
|
9
|
+
from django.utils import timezone
|
10
|
+
from django.db import connection
|
11
|
+
from django.core.cache import cache
|
12
|
+
|
13
|
+
from ..models.dashboard import SystemHealthItem
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class SystemCallbacks:
|
19
|
+
"""System health and metrics callbacks."""
|
20
|
+
|
21
|
+
def get_system_health(self) -> List[SystemHealthItem]:
|
22
|
+
"""Get system health status as Pydantic models."""
|
23
|
+
health_items = []
|
24
|
+
|
25
|
+
# Database health
|
26
|
+
try:
|
27
|
+
with connection.cursor() as cursor:
|
28
|
+
cursor.execute("SELECT 1")
|
29
|
+
health_items.append(
|
30
|
+
SystemHealthItem(
|
31
|
+
component="database",
|
32
|
+
status="healthy",
|
33
|
+
description="Connection successful",
|
34
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
35
|
+
health_percentage=95,
|
36
|
+
)
|
37
|
+
)
|
38
|
+
except Exception as e:
|
39
|
+
health_items.append(
|
40
|
+
SystemHealthItem(
|
41
|
+
component="database",
|
42
|
+
status="error",
|
43
|
+
description=f"Connection failed: {str(e)[:50]}",
|
44
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
45
|
+
health_percentage=0,
|
46
|
+
)
|
47
|
+
)
|
48
|
+
|
49
|
+
# Cache health
|
50
|
+
try:
|
51
|
+
cache.set("health_check", "ok", 10)
|
52
|
+
if cache.get("health_check") == "ok":
|
53
|
+
health_items.append(
|
54
|
+
SystemHealthItem(
|
55
|
+
component="cache",
|
56
|
+
status="healthy",
|
57
|
+
description="Cache operational",
|
58
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
59
|
+
health_percentage=90,
|
60
|
+
)
|
61
|
+
)
|
62
|
+
else:
|
63
|
+
health_items.append(
|
64
|
+
SystemHealthItem(
|
65
|
+
component="cache",
|
66
|
+
status="warning",
|
67
|
+
description="Cache not responding",
|
68
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
69
|
+
health_percentage=50,
|
70
|
+
)
|
71
|
+
)
|
72
|
+
except Exception:
|
73
|
+
health_items.append(
|
74
|
+
SystemHealthItem(
|
75
|
+
component="cache",
|
76
|
+
status="unknown",
|
77
|
+
description="Cache not configured",
|
78
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
79
|
+
health_percentage=0,
|
80
|
+
)
|
81
|
+
)
|
82
|
+
|
83
|
+
# Storage health
|
84
|
+
try:
|
85
|
+
total, used, free = shutil.disk_usage("/")
|
86
|
+
usage_percentage = (used / total) * 100
|
87
|
+
free_percentage = 100 - usage_percentage
|
88
|
+
|
89
|
+
if free_percentage > 20:
|
90
|
+
status = "healthy"
|
91
|
+
desc = f"Disk space: {free_percentage:.1f}% free"
|
92
|
+
elif free_percentage > 10:
|
93
|
+
status = "warning"
|
94
|
+
desc = f"Low disk space: {free_percentage:.1f}% free"
|
95
|
+
else:
|
96
|
+
status = "error"
|
97
|
+
desc = f"Critical disk space: {free_percentage:.1f}% free"
|
98
|
+
|
99
|
+
health_items.append(
|
100
|
+
SystemHealthItem(
|
101
|
+
component="storage",
|
102
|
+
status=status,
|
103
|
+
description=desc,
|
104
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
105
|
+
health_percentage=int(free_percentage),
|
106
|
+
)
|
107
|
+
)
|
108
|
+
except Exception as e:
|
109
|
+
health_items.append(
|
110
|
+
SystemHealthItem(
|
111
|
+
component="storage",
|
112
|
+
status="error",
|
113
|
+
description="Storage check failed",
|
114
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
115
|
+
health_percentage=0,
|
116
|
+
)
|
117
|
+
)
|
118
|
+
|
119
|
+
# API health
|
120
|
+
health_items.append(
|
121
|
+
SystemHealthItem(
|
122
|
+
component="api",
|
123
|
+
status="healthy",
|
124
|
+
description="API server running",
|
125
|
+
last_check=timezone.now().strftime("%H:%M:%S"),
|
126
|
+
health_percentage=100,
|
127
|
+
)
|
128
|
+
)
|
129
|
+
|
130
|
+
return health_items
|
131
|
+
|
132
|
+
def get_system_metrics(self) -> Dict[str, Any]:
|
133
|
+
"""Get system metrics for dashboard."""
|
134
|
+
metrics = {}
|
135
|
+
|
136
|
+
# Database metrics
|
137
|
+
try:
|
138
|
+
with connection.cursor() as cursor:
|
139
|
+
cursor.execute("SELECT 1")
|
140
|
+
metrics["database"] = {
|
141
|
+
"status": "healthy",
|
142
|
+
"type": "PostgreSQL",
|
143
|
+
"health_percentage": 95,
|
144
|
+
"description": "Connection successful",
|
145
|
+
}
|
146
|
+
except Exception as e:
|
147
|
+
metrics["database"] = {
|
148
|
+
"status": "error",
|
149
|
+
"type": "PostgreSQL",
|
150
|
+
"health_percentage": 0,
|
151
|
+
"description": f"Connection failed: {str(e)}",
|
152
|
+
}
|
153
|
+
|
154
|
+
# Cache metrics
|
155
|
+
try:
|
156
|
+
cache.set("health_check", "ok", 10)
|
157
|
+
cache_result = cache.get("health_check")
|
158
|
+
if cache_result == "ok":
|
159
|
+
metrics["cache"] = {
|
160
|
+
"status": "healthy",
|
161
|
+
"type": "Memory Cache",
|
162
|
+
"health_percentage": 90,
|
163
|
+
"description": "Cache working properly",
|
164
|
+
}
|
165
|
+
else:
|
166
|
+
metrics["cache"] = {
|
167
|
+
"status": "warning",
|
168
|
+
"type": "Memory Cache",
|
169
|
+
"health_percentage": 50,
|
170
|
+
"description": "Cache response delayed",
|
171
|
+
}
|
172
|
+
except Exception as e:
|
173
|
+
metrics["cache"] = {
|
174
|
+
"status": "error",
|
175
|
+
"type": "Memory Cache",
|
176
|
+
"health_percentage": 0,
|
177
|
+
"description": f"Cache error: {str(e)}",
|
178
|
+
}
|
179
|
+
|
180
|
+
return metrics
|
@@ -0,0 +1,65 @@
|
|
1
|
+
"""
|
2
|
+
Users data callbacks.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import List, Dict, Any
|
7
|
+
|
8
|
+
from django.contrib.auth import get_user_model
|
9
|
+
|
10
|
+
from .base import get_user_admin_urls
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class UsersCallbacks:
|
16
|
+
"""Users data callbacks."""
|
17
|
+
|
18
|
+
def _get_user_model(self):
|
19
|
+
"""Get the user model safely."""
|
20
|
+
return get_user_model()
|
21
|
+
|
22
|
+
def get_recent_users(self) -> List[Dict[str, Any]]:
|
23
|
+
"""Get recent users data for template."""
|
24
|
+
try:
|
25
|
+
# Avoid database access during app initialization
|
26
|
+
from django.apps import apps
|
27
|
+
if not apps.ready:
|
28
|
+
return []
|
29
|
+
|
30
|
+
User = self._get_user_model()
|
31
|
+
recent_users = User.objects.select_related().order_by("-date_joined")[:10]
|
32
|
+
|
33
|
+
# Get admin URLs for user model
|
34
|
+
user_admin_urls = get_user_admin_urls()
|
35
|
+
|
36
|
+
return [
|
37
|
+
{
|
38
|
+
"id": user.id,
|
39
|
+
"username": user.username,
|
40
|
+
"email": user.email or "No email",
|
41
|
+
"date_joined": (
|
42
|
+
user.date_joined.strftime("%Y-%m-%d")
|
43
|
+
if user.date_joined
|
44
|
+
else "Unknown"
|
45
|
+
),
|
46
|
+
"is_active": user.is_active,
|
47
|
+
"is_staff": user.is_staff,
|
48
|
+
"is_superuser": user.is_superuser,
|
49
|
+
"last_login": user.last_login,
|
50
|
+
"admin_urls": {
|
51
|
+
"change": (
|
52
|
+
user_admin_urls["change"].format(id=user.id)
|
53
|
+
if user.id
|
54
|
+
else None
|
55
|
+
),
|
56
|
+
"view": (
|
57
|
+
user_admin_urls["view"].format(id=user.id) if user.id else None
|
58
|
+
),
|
59
|
+
},
|
60
|
+
}
|
61
|
+
for user in recent_users
|
62
|
+
]
|
63
|
+
except Exception as e:
|
64
|
+
logger.error(f"Error getting recent users: {e}")
|
65
|
+
return []
|
@@ -467,21 +467,28 @@ class UnfoldConfig(BaseModel):
|
|
467
467
|
if self.tab_configurations:
|
468
468
|
unfold_settings["TABS"] = self.tab_configurations
|
469
469
|
|
470
|
-
# Command interface
|
470
|
+
# Command interface - Enhanced for better UX
|
471
471
|
unfold_settings["COMMAND"] = {
|
472
472
|
"search_models": True,
|
473
473
|
"show_history": True,
|
474
|
+
"search_callback": None, # Can be customized per project
|
474
475
|
}
|
475
476
|
|
476
|
-
# Inject universal CSS variables
|
477
|
+
# Inject universal CSS variables and custom styles
|
477
478
|
if "STYLES" not in unfold_settings:
|
478
479
|
unfold_settings["STYLES"] = []
|
479
480
|
|
480
481
|
# Add our CSS as inline data URI
|
481
482
|
try:
|
482
|
-
from ..tailwind import get_css_variables
|
483
|
+
from ..tailwind import get_css_variables, get_modal_fix_css
|
483
484
|
import base64
|
485
|
+
|
486
|
+
# Base CSS variables
|
484
487
|
css_content = get_css_variables()
|
488
|
+
|
489
|
+
# Add modal scroll fix CSS
|
490
|
+
css_content += get_modal_fix_css()
|
491
|
+
|
485
492
|
css_b64 = base64.b64encode(css_content.encode('utf-8')).decode('utf-8')
|
486
493
|
data_uri = f"data:text/css;base64,{css_b64}"
|
487
494
|
unfold_settings["STYLES"].append(lambda request: data_uri)
|
@@ -198,6 +198,74 @@ def get_css_variables() -> str:
|
|
198
198
|
"""
|
199
199
|
|
200
200
|
|
201
|
+
def get_modal_fix_css() -> str:
|
202
|
+
"""
|
203
|
+
Get CSS fixes for modal scroll issues and other UI improvements.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
str: CSS fixes as string
|
207
|
+
"""
|
208
|
+
return """
|
209
|
+
/* Modal scroll fixes and UI improvements */
|
210
|
+
|
211
|
+
/* Ensure proper modal scroll behavior */
|
212
|
+
.modal-scrollable {
|
213
|
+
max-height: calc(80vh - 8rem);
|
214
|
+
overflow-y: auto;
|
215
|
+
}
|
216
|
+
|
217
|
+
/* Command modal specific fixes */
|
218
|
+
#commandModal .overflow-y-auto {
|
219
|
+
scrollbar-width: thin;
|
220
|
+
scrollbar-color: rgb(156, 163, 175) transparent;
|
221
|
+
}
|
222
|
+
|
223
|
+
#commandModal .overflow-y-auto::-webkit-scrollbar {
|
224
|
+
width: 8px;
|
225
|
+
}
|
226
|
+
|
227
|
+
#commandModal .overflow-y-auto::-webkit-scrollbar-track {
|
228
|
+
background: transparent;
|
229
|
+
}
|
230
|
+
|
231
|
+
#commandModal .overflow-y-auto::-webkit-scrollbar-thumb {
|
232
|
+
background-color: rgb(156, 163, 175);
|
233
|
+
border-radius: 4px;
|
234
|
+
}
|
235
|
+
|
236
|
+
#commandModal .overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
237
|
+
background-color: rgb(107, 114, 128);
|
238
|
+
}
|
239
|
+
|
240
|
+
/* Dark theme scrollbar */
|
241
|
+
.dark #commandModal .overflow-y-auto {
|
242
|
+
scrollbar-color: rgb(75, 85, 99) transparent;
|
243
|
+
}
|
244
|
+
|
245
|
+
.dark #commandModal .overflow-y-auto::-webkit-scrollbar-thumb {
|
246
|
+
background-color: rgb(75, 85, 99);
|
247
|
+
}
|
248
|
+
|
249
|
+
.dark #commandModal .overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
250
|
+
background-color: rgb(107, 114, 128);
|
251
|
+
}
|
252
|
+
|
253
|
+
/* Improved focus states */
|
254
|
+
.focus-visible\\:outline-primary-600:focus-visible {
|
255
|
+
outline: 2px solid rgb(37, 99, 235);
|
256
|
+
outline-offset: 2px;
|
257
|
+
}
|
258
|
+
|
259
|
+
/* Better button transitions */
|
260
|
+
.transition-colors {
|
261
|
+
transition-property: color, background-color, border-color;
|
262
|
+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
263
|
+
transition-duration: 150ms;
|
264
|
+
}
|
265
|
+
|
266
|
+
"""
|
267
|
+
|
268
|
+
|
201
269
|
def get_unfold_colors() -> Dict[str, Any]:
|
202
270
|
"""
|
203
271
|
Get color configuration for Unfold settings.
|