django-cfg 1.2.6__py3-none-any.whl → 1.2.8__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/agents/admin/execution_admin.py +9 -2
- django_cfg/apps/agents/admin/registry_admin.py +10 -2
- django_cfg/apps/agents/admin/toolsets_admin.py +14 -3
- django_cfg/apps/knowbase/admin/archive_admin.py +3 -2
- django_cfg/apps/knowbase/admin/chat_admin.py +3 -2
- django_cfg/apps/knowbase/admin/document_admin.py +11 -2
- django_cfg/apps/knowbase/admin/external_data_admin.py +2 -1
- django_cfg/apps/urls.py +2 -2
- django_cfg/modules/django_import_export/__init__.py +12 -5
- 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 +18 -18
- 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.6.dist-info → django_cfg-1.2.8.dist-info}/METADATA +2 -1
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/RECORD +52 -32
- django_cfg/modules/django_unfold/callbacks.py +0 -795
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/licenses/LICENSE +0 -0
@@ -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.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Action Grid Component -->
|
4
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-{{ cols|default:'3' }} xl:grid-cols-{{ xl_cols|default:'4' }} gap-4">
|
5
|
+
{% for action in actions %}
|
6
|
+
<a href="{{ action.link }}"
|
7
|
+
class="group flex items-center p-4 bg-white dark:bg-base-900 rounded-lg border border-base-200 dark:border-base-700 hover:border-primary-600 dark:hover:border-primary-500 hover:shadow-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
|
8
|
+
|
9
|
+
<!-- Icon -->
|
10
|
+
<div class="flex-shrink-0 mr-4">
|
11
|
+
<div class="p-2 rounded-lg transition-colors duration-200
|
12
|
+
{% if action.color == 'primary' %}bg-primary-100 dark:bg-primary-900/20 group-hover:bg-primary-200 dark:group-hover:bg-primary-800/30
|
13
|
+
{% elif action.color == 'success' %}bg-green-100 dark:bg-green-900/20 group-hover:bg-green-200 dark:group-hover:bg-green-800/30
|
14
|
+
{% elif action.color == 'warning' %}bg-amber-100 dark:bg-amber-900/20 group-hover:bg-amber-200 dark:group-hover:bg-amber-800/30
|
15
|
+
{% elif action.color == 'danger' %}bg-red-100 dark:bg-red-900/20 group-hover:bg-red-200 dark:group-hover:bg-red-800/30
|
16
|
+
{% else %}bg-base-100 dark:bg-base-800 group-hover:bg-base-200 dark:group-hover:bg-base-700{% endif %}">
|
17
|
+
|
18
|
+
<span class="material-icons text-lg
|
19
|
+
{% if action.color == 'primary' %}text-primary-600 dark:text-primary-400
|
20
|
+
{% elif action.color == 'success' %}text-green-600 dark:text-green-400
|
21
|
+
{% elif action.color == 'warning' %}text-amber-600 dark:text-amber-400
|
22
|
+
{% elif action.color == 'danger' %}text-red-600 dark:text-red-400
|
23
|
+
{% else %}text-font-subtle-light dark:text-font-subtle-dark{% endif %}">
|
24
|
+
{{ action.icon }}
|
25
|
+
</span>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<!-- Content -->
|
30
|
+
<div class="flex-1 min-w-0">
|
31
|
+
<h3 class="text-sm font-medium text-font-default-light dark:text-font-default-dark group-hover:text-primary-600 dark:group-hover:text-primary-500 transition-colors duration-200">
|
32
|
+
{{ action.title }}
|
33
|
+
</h3>
|
34
|
+
{% if action.description %}
|
35
|
+
<p class="text-xs text-font-subtle-light dark:text-font-subtle-dark mt-1 line-clamp-2">
|
36
|
+
{{ action.description }}
|
37
|
+
</p>
|
38
|
+
{% endif %}
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<!-- Arrow -->
|
42
|
+
<div class="flex-shrink-0 ml-2">
|
43
|
+
<span class="material-icons text-base-400 dark:text-base-500 group-hover:text-primary-600 dark:group-hover:text-primary-500 transition-colors duration-200">
|
44
|
+
arrow_forward
|
45
|
+
</span>
|
46
|
+
</div>
|
47
|
+
</a>
|
48
|
+
{% endfor %}
|
49
|
+
</div>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Enhanced Card Component following Unfold patterns -->
|
4
|
+
<div class="bg-white dark:bg-base-900 rounded-lg border border-base-200 dark:border-base-700 shadow-xs overflow-hidden {% if hover %}hover:shadow-md hover:border-base-300 dark:hover:border-base-600 transition-all duration-200{% endif %}{% if class %} {{ class }}{% endif %}">
|
5
|
+
|
6
|
+
<!-- Card Header -->
|
7
|
+
{% if title or header_actions %}
|
8
|
+
<div class="p-6 border-b border-base-200 dark:border-base-700 {% if header_bg %}{{ header_bg }}{% else %}bg-base-50 dark:bg-base-800{% endif %}">
|
9
|
+
<div class="flex items-center justify-between">
|
10
|
+
{% if title %}
|
11
|
+
<div class="flex items-center space-x-3">
|
12
|
+
{% if icon %}
|
13
|
+
<div class="flex items-center justify-center w-8 h-8 bg-{{ icon_color|default:'primary' }}-100 dark:bg-{{ icon_color|default:'primary' }}-900/20 rounded-lg">
|
14
|
+
<span class="material-icons text-{{ icon_color|default:'primary' }}-600 dark:text-{{ icon_color|default:'primary' }}-400 text-lg">{{ icon }}</span>
|
15
|
+
</div>
|
16
|
+
{% endif %}
|
17
|
+
<div>
|
18
|
+
<h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark">
|
19
|
+
{{ title }}
|
20
|
+
</h3>
|
21
|
+
{% if subtitle %}
|
22
|
+
<p class="text-sm text-font-subtle-light dark:text-font-subtle-dark mt-1">
|
23
|
+
{{ subtitle }}
|
24
|
+
</p>
|
25
|
+
{% endif %}
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
{% endif %}
|
29
|
+
|
30
|
+
{% if header_actions %}
|
31
|
+
<div class="flex items-center space-x-2">
|
32
|
+
{{ header_actions }}
|
33
|
+
</div>
|
34
|
+
{% endif %}
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
{% endif %}
|
38
|
+
|
39
|
+
<!-- Card Body -->
|
40
|
+
<div class="p-6{% if compact %} p-4{% endif %}">
|
41
|
+
{{ content }}
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<!-- Card Footer -->
|
45
|
+
{% if footer %}
|
46
|
+
<div class="px-6 py-4 border-t border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-800">
|
47
|
+
{{ footer }}
|
48
|
+
</div>
|
49
|
+
{% endif %}
|
50
|
+
</div>
|
@@ -0,0 +1,67 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Data Table Component -->
|
4
|
+
<div class="bg-white dark:bg-base-900 rounded-lg border border-base-200 dark:border-base-700 overflow-hidden shadow-xs">
|
5
|
+
|
6
|
+
<!-- Table Header -->
|
7
|
+
{% if title or actions %}
|
8
|
+
<div class="px-6 py-4 border-b border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-800">
|
9
|
+
<div class="flex items-center justify-between">
|
10
|
+
{% if title %}
|
11
|
+
<div class="flex items-center">
|
12
|
+
{% if icon %}
|
13
|
+
<span class="material-icons text-primary-600 mr-2">{{ icon }}</span>
|
14
|
+
{% endif %}
|
15
|
+
<h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark">{{ title }}</h3>
|
16
|
+
</div>
|
17
|
+
{% endif %}
|
18
|
+
|
19
|
+
{% if actions %}
|
20
|
+
<div class="flex items-center space-x-2">
|
21
|
+
{{ actions }}
|
22
|
+
</div>
|
23
|
+
{% endif %}
|
24
|
+
</div>
|
25
|
+
</div>
|
26
|
+
{% endif %}
|
27
|
+
|
28
|
+
<!-- Table Content -->
|
29
|
+
<div class="overflow-x-auto">
|
30
|
+
<table class="w-full">
|
31
|
+
{% if headers %}
|
32
|
+
<thead class="bg-base-50 dark:bg-base-800">
|
33
|
+
<tr>
|
34
|
+
{% for header in headers %}
|
35
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-font-subtle-light dark:text-font-subtle-dark uppercase tracking-wider">
|
36
|
+
{{ header }}
|
37
|
+
</th>
|
38
|
+
{% endfor %}
|
39
|
+
</tr>
|
40
|
+
</thead>
|
41
|
+
{% endif %}
|
42
|
+
|
43
|
+
<tbody class="bg-white dark:bg-base-900 divide-y divide-base-200 dark:divide-base-700">
|
44
|
+
{% if rows %}
|
45
|
+
{% for row in rows %}
|
46
|
+
<tr class="hover:bg-base-100 dark:hover:bg-base-800 transition-colors duration-150">
|
47
|
+
{% for cell in row %}
|
48
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
49
|
+
{{ cell }}
|
50
|
+
</td>
|
51
|
+
{% endfor %}
|
52
|
+
</tr>
|
53
|
+
{% endfor %}
|
54
|
+
{% else %}
|
55
|
+
<tr>
|
56
|
+
<td colspan="{{ headers|length|default:'5' }}" class="px-6 py-8 text-center">
|
57
|
+
<div class="flex flex-col items-center">
|
58
|
+
<span class="material-icons text-4xl text-base-400 dark:text-base-500 mb-2">{{ empty_icon|default:'inbox' }}</span>
|
59
|
+
<p class="text-font-subtle-light dark:text-font-subtle-dark">{{ empty_message|default:'No data available' }}</p>
|
60
|
+
</div>
|
61
|
+
</td>
|
62
|
+
</tr>
|
63
|
+
{% endif %}
|
64
|
+
</tbody>
|
65
|
+
</table>
|
66
|
+
</div>
|
67
|
+
</div>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Metric Card Component -->
|
4
|
+
<div class="bg-white dark:bg-base-900 rounded-lg p-5 border border-base-200 dark:border-base-700 hover:shadow-md transition-shadow duration-200">
|
5
|
+
<div class="flex items-center justify-between">
|
6
|
+
<div class="flex items-center space-x-3">
|
7
|
+
<div class="flex-shrink-0">
|
8
|
+
<span class="material-icons
|
9
|
+
{% if status == 'healthy' or status == 'success' %}text-green-600 dark:text-green-400
|
10
|
+
{% elif status == 'warning' %}text-amber-600 dark:text-amber-400
|
11
|
+
{% elif status == 'error' or status == 'failed' %}text-red-600 dark:text-red-400
|
12
|
+
{% elif status == 'info' %}text-blue-600 dark:text-blue-400
|
13
|
+
{% else %}text-primary-600 dark:text-primary-400{% endif %}">
|
14
|
+
{{ icon }}
|
15
|
+
</span>
|
16
|
+
</div>
|
17
|
+
<div>
|
18
|
+
<div class="text-sm font-medium text-font-default-light dark:text-font-default-dark">
|
19
|
+
{{ title }}
|
20
|
+
</div>
|
21
|
+
{% if description %}
|
22
|
+
<div class="text-xs text-font-subtle-light dark:text-font-subtle-dark mt-1">
|
23
|
+
{{ description }}
|
24
|
+
</div>
|
25
|
+
{% endif %}
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<div class="flex-shrink-0">
|
30
|
+
{% include 'admin/components/status_badge.html' with status=status text=status_text %}
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
{% if progress %}
|
35
|
+
<div class="mt-4">
|
36
|
+
{% include 'admin/components/progress_bar.html' with value=progress title=progress_title description=progress_description %}
|
37
|
+
</div>
|
38
|
+
{% endif %}
|
39
|
+
</div>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Reusable Modal Component based on Unfold patterns -->
|
4
|
+
<div id="{{ modal_id|default:'modal' }}" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden z-50">
|
5
|
+
<div class="flex items-center justify-center min-h-screen p-4">
|
6
|
+
<div class="bg-white dark:bg-base-900 rounded-lg shadow-2xl max-w-{{ max_width|default:'4xl' }} w-full max-h-[80vh] flex flex-col border border-base-200 dark:border-base-700">
|
7
|
+
|
8
|
+
<!-- Modal Header -->
|
9
|
+
{% if title or closable %}
|
10
|
+
<div class="flex items-center justify-between p-4 border-b border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-800">
|
11
|
+
{% if title %}
|
12
|
+
<div class="flex items-center space-x-3">
|
13
|
+
{% if icon %}
|
14
|
+
<div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/20 rounded-lg">
|
15
|
+
<span class="material-icons text-primary-600 dark:text-primary-400">{{ icon }}</span>
|
16
|
+
</div>
|
17
|
+
{% endif %}
|
18
|
+
<div>
|
19
|
+
<h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark">
|
20
|
+
{{ title }}
|
21
|
+
</h3>
|
22
|
+
{% if subtitle %}
|
23
|
+
<p class="text-sm text-font-subtle-light dark:text-font-subtle-dark">
|
24
|
+
{{ subtitle }}
|
25
|
+
</p>
|
26
|
+
{% endif %}
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
{% endif %}
|
30
|
+
|
31
|
+
{% if closable %}
|
32
|
+
<button onclick="{{ close_function|default:'closeModal()' }}" class="p-2 text-font-subtle-light dark:text-font-subtle-dark hover:text-font-default-light dark:hover:text-font-default-dark hover:bg-base-100 dark:hover:bg-base-700 rounded-lg transition-colors">
|
33
|
+
<span class="material-icons">close</span>
|
34
|
+
</button>
|
35
|
+
{% endif %}
|
36
|
+
</div>
|
37
|
+
{% endif %}
|
38
|
+
|
39
|
+
<!-- Modal Body -->
|
40
|
+
<div class="flex-1 overflow-hidden p-4">
|
41
|
+
{% if scrollable %}
|
42
|
+
<div class="bg-base-100 dark:bg-base-800 rounded-lg border border-base-200 dark:border-base-700 h-full overflow-y-auto p-4">
|
43
|
+
{{ content }}
|
44
|
+
</div>
|
45
|
+
{% else %}
|
46
|
+
{{ content }}
|
47
|
+
{% endif %}
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<!-- Modal Footer -->
|
51
|
+
{% if footer %}
|
52
|
+
<div class="flex items-center justify-between p-4 border-t border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-800">
|
53
|
+
{{ footer }}
|
54
|
+
</div>
|
55
|
+
{% endif %}
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Progress Bar Component -->
|
4
|
+
<div class="w-full">
|
5
|
+
{% if title %}
|
6
|
+
<div class="flex items-center justify-between mb-2">
|
7
|
+
<span class="text-sm font-medium text-font-default-light dark:text-font-default-dark">{{ title }}</span>
|
8
|
+
<span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">{{ value }}%</span>
|
9
|
+
</div>
|
10
|
+
{% endif %}
|
11
|
+
|
12
|
+
<div class="w-full bg-base-200 dark:bg-base-700 rounded-full h-2">
|
13
|
+
<div class="h-2 rounded-full transition-all duration-300
|
14
|
+
{% if value >= 80 %}bg-green-500
|
15
|
+
{% elif value >= 60 %}bg-amber-500
|
16
|
+
{% elif value >= 40 %}bg-orange-500
|
17
|
+
{% else %}bg-red-500{% endif %}"
|
18
|
+
style="width: {{ value|default:0 }}%">
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
{% if description %}
|
23
|
+
<p class="text-xs text-font-subtle-light dark:text-font-subtle-dark mt-1">{{ description }}</p>
|
24
|
+
{% endif %}
|
25
|
+
</div>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{% load unfold %}
|
2
|
+
|
3
|
+
<!-- Section Header Component -->
|
4
|
+
<div class="flex items-center mb-6">
|
5
|
+
{% if icon %}
|
6
|
+
<div class="flex items-center justify-center w-8 h-8 bg-{{ icon_color|default:'primary' }}-100 dark:bg-{{ icon_color|default:'primary' }}-900/20 rounded-lg mr-3">
|
7
|
+
<span class="material-icons text-{{ icon_color|default:'primary' }}-600 dark:text-{{ icon_color|default:'primary' }}-400 text-lg">{{ icon }}</span>
|
8
|
+
</div>
|
9
|
+
{% endif %}
|
10
|
+
|
11
|
+
<h2 class="text-xl font-semibold text-font-important-light dark:text-font-important-dark">
|
12
|
+
{{ title }}
|
13
|
+
</h2>
|
14
|
+
|
15
|
+
{% if badge %}
|
16
|
+
<span class="ml-auto text-xs text-font-subtle-light dark:text-font-subtle-dark bg-base-100 dark:bg-base-800 px-2 py-1 rounded-full">
|
17
|
+
{{ badge }}
|
18
|
+
</span>
|
19
|
+
{% endif %}
|
20
|
+
|
21
|
+
{% if actions %}
|
22
|
+
<div class="ml-auto flex items-center space-x-2">
|
23
|
+
{{ actions }}
|
24
|
+
</div>
|
25
|
+
{% endif %}
|
26
|
+
</div>
|