fastapi-lite-admin 0.1.0__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.
- fastapi_admin_lite/__init__.py +3 -0
- fastapi_admin_lite/core/config.py +14 -0
- fastapi_admin_lite/core/crud.py +115 -0
- fastapi_admin_lite/core/registry.py +39 -0
- fastapi_admin_lite/core/schema.py +27 -0
- fastapi_admin_lite/dependencies/db.py +7 -0
- fastapi_admin_lite/integrations/sqlalchemy.py +22 -0
- fastapi_admin_lite/main.py +103 -0
- fastapi_admin_lite/routers/admin.py +75 -0
- fastapi_admin_lite/ui/templates/dashboard.html +236 -0
- fastapi_admin_lite/ui/templates/layout.html +329 -0
- fastapi_admin_lite/ui/templates/model_detail.html +170 -0
- fastapi_admin_lite/ui/templates/model_form.html +359 -0
- fastapi_admin_lite/ui/templates/model_list.html +440 -0
- fastapi_admin_lite/ui/views.py +154 -0
- fastapi_lite_admin-0.1.0.dist-info/METADATA +41 -0
- fastapi_lite_admin-0.1.0.dist-info/RECORD +19 -0
- fastapi_lite_admin-0.1.0.dist-info/WHEEL +5 -0
- fastapi_lite_admin-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
|
|
3
|
+
{% set active_nav = model_name %}
|
|
4
|
+
|
|
5
|
+
{% block extra_css %}
|
|
6
|
+
.header-actions {
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
align-items: center;
|
|
10
|
+
margin-bottom: 24px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.table-card {
|
|
14
|
+
padding: 0;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
margin-bottom: 32px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.data-table {
|
|
20
|
+
width: 100%;
|
|
21
|
+
border-collapse: collapse;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.data-table th {
|
|
25
|
+
text-align: left;
|
|
26
|
+
padding: 6px 12px;
|
|
27
|
+
font-size: 0.75rem;
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
text-transform: uppercase;
|
|
30
|
+
color: var(--text-muted);
|
|
31
|
+
border-bottom: 1px solid var(--border);
|
|
32
|
+
background-color: #f1f5f9;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.data-table td {
|
|
36
|
+
padding: 4px 12px;
|
|
37
|
+
font-size: 0.8rem;
|
|
38
|
+
border-bottom: 1px solid var(--border);
|
|
39
|
+
color: #334155;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.status-badge {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 4px;
|
|
46
|
+
font-size: 0.7rem;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.status-dot {
|
|
51
|
+
width: 4px;
|
|
52
|
+
height: 4px;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.status-active .status-dot { background-color: var(--success); }
|
|
57
|
+
.status-active { color: #065f46; }
|
|
58
|
+
|
|
59
|
+
.status-inactive .status-dot { background-color: var(--text-muted); }
|
|
60
|
+
.status-inactive { color: var(--text-muted); }
|
|
61
|
+
|
|
62
|
+
.action-btns {
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 6px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.action-btn {
|
|
68
|
+
background: none;
|
|
69
|
+
border: none;
|
|
70
|
+
color: var(--primary);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
font-size: 0.75rem;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
transition: color 0.2s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.action-btn:hover {
|
|
78
|
+
color: var(--text-main);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.action-btn.delete:hover {
|
|
82
|
+
color: var(--error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Footer Stats */
|
|
86
|
+
.table-footer-stats {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: repeat(3, 1fr);
|
|
89
|
+
gap: 24px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.footer-stat-card {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 16px;
|
|
96
|
+
padding: 20px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.footer-stat-icon {
|
|
100
|
+
width: 40px;
|
|
101
|
+
height: 40px;
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
font-size: 1.2rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.footer-stat-info .label {
|
|
110
|
+
font-size: 0.7rem;
|
|
111
|
+
text-transform: uppercase;
|
|
112
|
+
font-weight: 700;
|
|
113
|
+
color: var(--text-muted);
|
|
114
|
+
margin-bottom: 2px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.footer-stat-info .value {
|
|
118
|
+
font-size: 1.25rem;
|
|
119
|
+
font-weight: 700;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Pagination */
|
|
123
|
+
.pagination {
|
|
124
|
+
padding: 16px 24px;
|
|
125
|
+
display: flex;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
align-items: center;
|
|
128
|
+
border-top: 1px solid var(--border);
|
|
129
|
+
background-color: #fafafa;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.pagination-info {
|
|
133
|
+
font-size: 0.85rem;
|
|
134
|
+
color: var(--text-muted);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.pagination-btns {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 16px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.page-size-selector {
|
|
144
|
+
font-size: 0.85rem;
|
|
145
|
+
color: var(--text-muted);
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.page-size-selector select {
|
|
152
|
+
padding: 4px 8px;
|
|
153
|
+
border: 1px solid var(--border);
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
outline: none;
|
|
156
|
+
background: white;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.activity-badge {
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 6px;
|
|
163
|
+
padding: 2px 8px;
|
|
164
|
+
background-color: #f1f5f9;
|
|
165
|
+
color: var(--text-muted);
|
|
166
|
+
border-radius: 12px;
|
|
167
|
+
font-size: 0.75rem;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
border: 1px solid var(--border);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.activity-badge.active {
|
|
173
|
+
background-color: #ecfeff;
|
|
174
|
+
color: var(--primary);
|
|
175
|
+
border-color: #cffafe;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.activity-badge i {
|
|
179
|
+
font-size: 0.7rem;
|
|
180
|
+
}
|
|
181
|
+
{% endblock %}
|
|
182
|
+
|
|
183
|
+
{% block content %}
|
|
184
|
+
|
|
185
|
+
<div class="header-actions">
|
|
186
|
+
<div>
|
|
187
|
+
<h1 class="page-title">{{ model_name|capitalize }}</h1>
|
|
188
|
+
<div style="display: flex; align-items: center; gap: 10px; margin-top: 4px;">
|
|
189
|
+
<p class="page-subtitle" style="margin: 0;">Manage and monitor your {{ model_name }} records.</p>
|
|
190
|
+
<div class="activity-badge {% if has_date_field %}active{% endif %}" title="Activity in last 24 hours">
|
|
191
|
+
<i class="fas {% if has_date_field %}fa-bolt{% else %}fa-circle-info{% endif %}"></i>
|
|
192
|
+
{% if has_date_field %}
|
|
193
|
+
{{ recent_count }} new today
|
|
194
|
+
{% else %}
|
|
195
|
+
date_field not set
|
|
196
|
+
{% endif %}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<button class="btn btn-primary" onclick="location.href='/admin/{{ model_name }}/new'">
|
|
201
|
+
<i class="fas fa-plus"></i> Create {{ model_name|capitalize }}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="card table-card">
|
|
206
|
+
<table class="data-table">
|
|
207
|
+
<thead id="table-head">
|
|
208
|
+
<!-- Columns injected via JS -->
|
|
209
|
+
</thead>
|
|
210
|
+
<tbody id="table-body">
|
|
211
|
+
<!-- Loading indicator -->
|
|
212
|
+
<tr>
|
|
213
|
+
<td colspan="100%" style="text-align: center; padding: 48px; color: var(--text-muted);">
|
|
214
|
+
<i class="fas fa-circle-notch fa-spin"></i> Loading {{ model_name }} data...
|
|
215
|
+
</td>
|
|
216
|
+
</tr>
|
|
217
|
+
</tbody>
|
|
218
|
+
</table>
|
|
219
|
+
|
|
220
|
+
<div class="pagination">
|
|
221
|
+
<div class="pagination-info" id="pagination-info">
|
|
222
|
+
Showing 0-0 of 0 {{ model_name }}
|
|
223
|
+
</div>
|
|
224
|
+
<div class="pagination-btns">
|
|
225
|
+
<div class="page-size-selector">
|
|
226
|
+
<span>Per page:</span>
|
|
227
|
+
<select id="page-size-select">
|
|
228
|
+
<option value="10">10</option>
|
|
229
|
+
<option value="20">20</option>
|
|
230
|
+
<option value="30">30</option>
|
|
231
|
+
<option value="50">50</option>
|
|
232
|
+
</select>
|
|
233
|
+
</div>
|
|
234
|
+
<div style="display: flex; gap: 8px;">
|
|
235
|
+
<button class="btn btn-secondary btn-sm" id="prev-btn">Previous</button>
|
|
236
|
+
<button class="btn btn-secondary btn-sm" id="next-btn">Next</button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="table-footer-stats">
|
|
243
|
+
<div class="card footer-stat-card">
|
|
244
|
+
<div class="footer-stat-icon" style="background-color: #e0f2fe; color: #0369a1;">
|
|
245
|
+
<i class="fas fa-user-group"></i>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="footer-stat-info">
|
|
248
|
+
<div class="label">Total {{ model_name|capitalize }}</div>
|
|
249
|
+
<div class="value" id="total-count">0</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="card footer-stat-card">
|
|
253
|
+
<div class="footer-stat-icon" style="background-color: #e0e7ff; color: #4338ca;">
|
|
254
|
+
<i class="fas fa-bolt"></i>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="footer-stat-info">
|
|
257
|
+
<div class="label">New Today</div>
|
|
258
|
+
<div class="value">{% if has_date_field %}{{ recent_count }}{% else %}--{% endif %}</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="card footer-stat-card">
|
|
262
|
+
<div class="footer-stat-icon" style="background-color: #fee2e2; color: #b91c1c;">
|
|
263
|
+
<i class="fas fa-triangle-exclamation"></i>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="footer-stat-info">
|
|
266
|
+
<div class="label">Attention Needed</div>
|
|
267
|
+
<div class="value">{% if has_attention_filter %}{{ attention_count }}{% else %}--{% endif %}</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{% endblock %}
|
|
273
|
+
|
|
274
|
+
{% block extra_js %}
|
|
275
|
+
<script>
|
|
276
|
+
const modelName = "{{ model_name }}";
|
|
277
|
+
const apiBaseUrl = "/admin/api";
|
|
278
|
+
let currentPage = 1;
|
|
279
|
+
let pageSize = 10;
|
|
280
|
+
let currentSortField = 'id';
|
|
281
|
+
let currentSortDir = 'desc';
|
|
282
|
+
|
|
283
|
+
async function loadData() {
|
|
284
|
+
try {
|
|
285
|
+
// 1. Fetch Schema to get configuration
|
|
286
|
+
const schemaResponse = await fetch(`${apiBaseUrl}/schema`);
|
|
287
|
+
const schema = await schemaResponse.json();
|
|
288
|
+
const modelSchema = schema.models.find(m => m.name === modelName);
|
|
289
|
+
const listDisplay = modelSchema ? modelSchema.list_display : null;
|
|
290
|
+
|
|
291
|
+
// 2. Fetch Data
|
|
292
|
+
const skip = (currentPage - 1) * pageSize;
|
|
293
|
+
const url = `${apiBaseUrl}/${modelName}?skip=${skip}&limit=${pageSize}&order_by=${currentSortField}&order_dir=${currentSortDir}`;
|
|
294
|
+
const response = await fetch(url);
|
|
295
|
+
const result = await response.json();
|
|
296
|
+
const data = result.data;
|
|
297
|
+
const total = result.total;
|
|
298
|
+
|
|
299
|
+
document.getElementById('total-count').textContent = total;
|
|
300
|
+
|
|
301
|
+
const start = data.length > 0 ? skip + 1 : 0;
|
|
302
|
+
const end = skip + data.length;
|
|
303
|
+
document.getElementById('pagination-info').textContent = `Showing ${start}-${end} of ${total} ${modelName}`;
|
|
304
|
+
|
|
305
|
+
// Update button states
|
|
306
|
+
document.getElementById('prev-btn').disabled = currentPage === 1;
|
|
307
|
+
document.getElementById('next-btn').disabled = end >= total;
|
|
308
|
+
|
|
309
|
+
if (data.length === 0 && currentPage === 1) {
|
|
310
|
+
document.getElementById('table-body').innerHTML = '<tr><td colspan="100%" style="text-align: center; padding: 48px;">No records found</td></tr>';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 3. Determine columns to show
|
|
315
|
+
let columns = [];
|
|
316
|
+
if (listDisplay && listDisplay.length > 0) {
|
|
317
|
+
columns = listDisplay;
|
|
318
|
+
} else {
|
|
319
|
+
columns = Object.keys(data[0]);
|
|
320
|
+
if (columns.includes('id')) {
|
|
321
|
+
columns = ['id', ...columns.filter(c => c !== 'id')];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const headRow = document.getElementById('table-head');
|
|
326
|
+
headRow.innerHTML = '<tr>' +
|
|
327
|
+
columns.map(col => {
|
|
328
|
+
const isSorted = currentSortField === col;
|
|
329
|
+
const icon = isSorted ? (currentSortDir === 'asc' ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort';
|
|
330
|
+
return `
|
|
331
|
+
<th onclick="toggleSort('${col}')" style="cursor: pointer; user-select: none;">
|
|
332
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
333
|
+
${col.replace('_', ' ')}
|
|
334
|
+
<i class="fas ${icon}" style="font-size: 0.7rem; color: ${isSorted ? 'var(--primary)' : '#ccc'}"></i>
|
|
335
|
+
</div>
|
|
336
|
+
</th>
|
|
337
|
+
`;
|
|
338
|
+
}).join('') +
|
|
339
|
+
'<th>Actions</th></tr>';
|
|
340
|
+
|
|
341
|
+
const body = document.getElementById('table-body');
|
|
342
|
+
body.innerHTML = data.map(row => `
|
|
343
|
+
<tr>
|
|
344
|
+
${columns.map(col => {
|
|
345
|
+
const val = row[col];
|
|
346
|
+
|
|
347
|
+
// Special rendering for specific fields
|
|
348
|
+
if (col === 'id') return `<td style="font-family: monospace; color: var(--text-muted); font-size: 0.8rem;">#${val}</td>`;
|
|
349
|
+
|
|
350
|
+
if (col === 'email' || col === 'name') {
|
|
351
|
+
const name = row['name'] || (row['email'] ? row['email'].split('@')[0] : 'Item');
|
|
352
|
+
return `
|
|
353
|
+
<td>
|
|
354
|
+
<span style="font-weight: 600;">${val}</span>
|
|
355
|
+
${col === 'name' && row['email'] ? `<span style="font-size: 0.7rem; color: var(--text-muted); margin-left: 4px;">(${row['email']})</span>` : ''}
|
|
356
|
+
</td>
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (typeof val === 'boolean' || col === 'is_active') {
|
|
361
|
+
const isActive = val === true || val === 'true';
|
|
362
|
+
return `
|
|
363
|
+
<td>
|
|
364
|
+
<span class="status-badge ${isActive ? 'status-active' : 'status-inactive'}">
|
|
365
|
+
<div class="status-dot"></div>
|
|
366
|
+
${isActive ? 'Active' : 'Inactive'}
|
|
367
|
+
</span>
|
|
368
|
+
</td>
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return `<td>${val}</td>`;
|
|
373
|
+
}).join('')}
|
|
374
|
+
<td>
|
|
375
|
+
<div class="action-btns">
|
|
376
|
+
<button class="action-btn" onclick="location.href='/admin/${modelName}/${row.id}/detail'" title="View">
|
|
377
|
+
<i class="far fa-eye"></i>
|
|
378
|
+
</button>
|
|
379
|
+
<button class="action-btn" onclick="location.href='/admin/${modelName}/${row.id}'" title="Edit">
|
|
380
|
+
<i class="fas fa-pencil-alt"></i>
|
|
381
|
+
</button>
|
|
382
|
+
<button class="action-btn delete" onclick="deleteRecord('${row.id}')" title="Delete">
|
|
383
|
+
<i class="far fa-trash-alt"></i>
|
|
384
|
+
</button>
|
|
385
|
+
</div>
|
|
386
|
+
</td>
|
|
387
|
+
</tr>
|
|
388
|
+
`).join('');
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error('Error loading data:', error);
|
|
391
|
+
document.getElementById('table-body').innerHTML = '<tr><td colspan="100%" style="text-align: center; padding: 48px; color: var(--error);">Error loading data</td></tr>';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function toggleSort(field) {
|
|
396
|
+
if (currentSortField === field) {
|
|
397
|
+
currentSortDir = currentSortDir === 'asc' ? 'desc' : 'asc';
|
|
398
|
+
} else {
|
|
399
|
+
currentSortField = field;
|
|
400
|
+
currentSortDir = 'asc';
|
|
401
|
+
}
|
|
402
|
+
currentPage = 1; // Reset to first page on sort
|
|
403
|
+
loadData();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function deleteRecord(id) {
|
|
407
|
+
if (!confirm('Are you sure you want to delete this record?')) return;
|
|
408
|
+
try {
|
|
409
|
+
const response = await fetch(`${apiBaseUrl}/${modelName}/${id}`, { method: 'DELETE' });
|
|
410
|
+
if (response.ok) {
|
|
411
|
+
loadData();
|
|
412
|
+
} else {
|
|
413
|
+
alert('Failed to delete record');
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
alert('Error deleting record');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
document.getElementById('prev-btn').addEventListener('click', () => {
|
|
421
|
+
if (currentPage > 1) {
|
|
422
|
+
currentPage--;
|
|
423
|
+
loadData();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
document.getElementById('next-btn').addEventListener('click', () => {
|
|
428
|
+
currentPage++;
|
|
429
|
+
loadData();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
document.getElementById('page-size-select').addEventListener('change', (e) => {
|
|
433
|
+
pageSize = parseInt(e.target.value);
|
|
434
|
+
currentPage = 1;
|
|
435
|
+
loadData();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
loadData();
|
|
439
|
+
</script>
|
|
440
|
+
{% endblock %}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from fastapi import APIRouter, Request
|
|
3
|
+
from fastapi.responses import HTMLResponse
|
|
4
|
+
from fastapi.templating import Jinja2Templates
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
# Get path to templates directory relative to this file
|
|
8
|
+
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
|
9
|
+
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
|
10
|
+
|
|
11
|
+
def create_ui_router(admin: Any) -> APIRouter:
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
@router.get("/", response_class=HTMLResponse)
|
|
15
|
+
async def dashboard(request: Request):
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
models = admin.registry.get_models()
|
|
18
|
+
model_names = list(models.keys())
|
|
19
|
+
|
|
20
|
+
# Collect real counts for each model
|
|
21
|
+
stats = []
|
|
22
|
+
for name, reg in models.items():
|
|
23
|
+
# Skip if user has specified a subset of models for the dashboard
|
|
24
|
+
if admin.dashboard_models and name not in admin.dashboard_models:
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
db_gen = reg.get_db()
|
|
28
|
+
db = next(db_gen)
|
|
29
|
+
try:
|
|
30
|
+
# 1. Get Total Count
|
|
31
|
+
total_count = db.query(reg.model).count()
|
|
32
|
+
|
|
33
|
+
# 2. Get 24h Count if date_field is set
|
|
34
|
+
recent_count = None
|
|
35
|
+
date_field = reg.config.get("date_field")
|
|
36
|
+
|
|
37
|
+
if date_field and hasattr(reg.model, date_field):
|
|
38
|
+
yesterday = datetime.now() - timedelta(hours=24)
|
|
39
|
+
recent_count = db.query(reg.model).filter(
|
|
40
|
+
getattr(reg.model, date_field) >= yesterday
|
|
41
|
+
).count()
|
|
42
|
+
|
|
43
|
+
stats.append({
|
|
44
|
+
"name": reg.config.get("display_name") or name.capitalize(),
|
|
45
|
+
"count": total_count,
|
|
46
|
+
"recent_count": recent_count,
|
|
47
|
+
"model_name": name,
|
|
48
|
+
"has_date_field": bool(date_field)
|
|
49
|
+
})
|
|
50
|
+
finally:
|
|
51
|
+
# Handle generators properly
|
|
52
|
+
try:
|
|
53
|
+
next(db_gen)
|
|
54
|
+
except StopIteration:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
# Fetch Logs
|
|
58
|
+
logs = []
|
|
59
|
+
if admin.get_logs:
|
|
60
|
+
try:
|
|
61
|
+
logs = admin.get_logs()
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Error fetching logs: {e}")
|
|
64
|
+
|
|
65
|
+
return templates.TemplateResponse(
|
|
66
|
+
request=request,
|
|
67
|
+
name=admin.dashboard_template or "dashboard.html",
|
|
68
|
+
context={
|
|
69
|
+
"models": model_names,
|
|
70
|
+
"stats": stats,
|
|
71
|
+
"admin_title": admin.title,
|
|
72
|
+
"logs": logs,
|
|
73
|
+
"logs_config": admin.logs_config
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@router.get("/{model_name}", response_class=HTMLResponse)
|
|
78
|
+
async def model_list(request: Request, model_name: str):
|
|
79
|
+
from datetime import datetime, timedelta
|
|
80
|
+
reg = admin.registry.get_model(model_name)
|
|
81
|
+
if not reg:
|
|
82
|
+
raise HTTPException(status_code=404, detail="Model not found")
|
|
83
|
+
|
|
84
|
+
models = list(admin.registry.get_models().keys())
|
|
85
|
+
|
|
86
|
+
# Calculate 24h stats for this specific model
|
|
87
|
+
recent_count = None
|
|
88
|
+
date_field = reg.config.get("date_field")
|
|
89
|
+
|
|
90
|
+
if date_field and hasattr(reg.model, date_field):
|
|
91
|
+
db_gen = reg.get_db()
|
|
92
|
+
db = next(db_gen)
|
|
93
|
+
try:
|
|
94
|
+
yesterday = datetime.now() - timedelta(hours=24)
|
|
95
|
+
recent_count = db.query(reg.model).filter(
|
|
96
|
+
getattr(reg.model, date_field) >= yesterday
|
|
97
|
+
).count()
|
|
98
|
+
finally:
|
|
99
|
+
try: next(db_gen)
|
|
100
|
+
except StopIteration: pass
|
|
101
|
+
|
|
102
|
+
# Calculate Attention count
|
|
103
|
+
attention_count = None
|
|
104
|
+
attn_filter = reg.config.get("attention_filter")
|
|
105
|
+
if attn_filter is not None:
|
|
106
|
+
db_gen = reg.get_db()
|
|
107
|
+
db = next(db_gen)
|
|
108
|
+
try:
|
|
109
|
+
attention_count = db.query(reg.model).filter(attn_filter).count()
|
|
110
|
+
finally:
|
|
111
|
+
try: next(db_gen)
|
|
112
|
+
except StopIteration: pass
|
|
113
|
+
|
|
114
|
+
return templates.TemplateResponse(
|
|
115
|
+
request=request,
|
|
116
|
+
name="model_list.html",
|
|
117
|
+
context={
|
|
118
|
+
"model_name": model_name,
|
|
119
|
+
"models": models,
|
|
120
|
+
"recent_count": recent_count,
|
|
121
|
+
"attention_count": attention_count,
|
|
122
|
+
"has_date_field": bool(date_field),
|
|
123
|
+
"has_attention_filter": attn_filter is not None
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@router.get("/{model_name}/new", response_class=HTMLResponse)
|
|
128
|
+
async def model_create(request: Request, model_name: str):
|
|
129
|
+
models = list(admin.registry.get_models().keys())
|
|
130
|
+
return templates.TemplateResponse(
|
|
131
|
+
request=request,
|
|
132
|
+
name="model_form.html",
|
|
133
|
+
context={"model_name": model_name, "id": None, "models": models}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@router.get("/{model_name}/{id}", response_class=HTMLResponse)
|
|
137
|
+
async def model_edit(request: Request, model_name: str, id: str):
|
|
138
|
+
models = list(admin.registry.get_models().keys())
|
|
139
|
+
return templates.TemplateResponse(
|
|
140
|
+
request=request,
|
|
141
|
+
name="model_form.html",
|
|
142
|
+
context={"model_name": model_name, "id": id, "models": models}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@router.get("/{model_name}/{id}/detail", response_class=HTMLResponse)
|
|
146
|
+
async def model_detail(request: Request, model_name: str, id: str):
|
|
147
|
+
models = list(admin.registry.get_models().keys())
|
|
148
|
+
return templates.TemplateResponse(
|
|
149
|
+
request=request,
|
|
150
|
+
name="model_detail.html",
|
|
151
|
+
context={"model_name": model_name, "id": id, "models": models}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return router
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-lite-admin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, pluggable admin panel for FastAPI
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: fastapi>=0.100.0
|
|
9
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: jinja2>=3.1.0
|
|
12
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: httpx; extra == "dev"
|
|
16
|
+
Requires-Dist: uvicorn; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# FastAPI Lite Admin
|
|
19
|
+
|
|
20
|
+
A premium, lightweight, pluggable admin panel for FastAPI and SQLAlchemy.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Zero-config CRUD**: Automatically generate admin interfaces for your models.
|
|
25
|
+
- **ORM Agnostic**: Initial support for SQLAlchemy, designed to support others.
|
|
26
|
+
- **API First**: All admin actions are available via a REST API.
|
|
27
|
+
- **Lightweight UI**: Simple Jinja2 templates for the dashboard and forms.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
(Documentation coming soon)
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Install dependencies
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
|
|
39
|
+
# Run example
|
|
40
|
+
python -m example.main
|
|
41
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
fastapi_admin_lite/__init__.py,sha256=HUejMXcpfadaZXn62qtBvCV0rE7QAaX4GYBO_KbdSOw,45
|
|
2
|
+
fastapi_admin_lite/main.py,sha256=sRh3x6t-ryKdzpSXBpqMabTDrWdQnyhmJznf5Zagfqk,3743
|
|
3
|
+
fastapi_admin_lite/core/config.py,sha256=g-WGUlkL1OV4rmtvh3dr6S2xzotyWLF6MIbaSLN7ur0,493
|
|
4
|
+
fastapi_admin_lite/core/crud.py,sha256=3QL33KyrNFqc3icUSWA-JPu496uzzlfWU9Rh6R8xP5M,3626
|
|
5
|
+
fastapi_admin_lite/core/registry.py,sha256=8SqoTmCXkJAsvu4yADquachD9Prtc2zKm35p_NftDvw,1099
|
|
6
|
+
fastapi_admin_lite/core/schema.py,sha256=O-Fmo2EFRK1YL5-xx-Ywe2YbSfyCXqd7Y5fGA0qbax0,904
|
|
7
|
+
fastapi_admin_lite/dependencies/db.py,sha256=FcOZK2gY9QsvVz_ZQMKK5_Bei2yzhL1U8vizrcpefAo,271
|
|
8
|
+
fastapi_admin_lite/integrations/sqlalchemy.py,sha256=KGX3q8ugxSLn2273MxdsknRvJTFHg1sJGNGcpDkw_ck,750
|
|
9
|
+
fastapi_admin_lite/routers/admin.py,sha256=TqI24W8GXhOzzKLGisbSIFwve-YjAgrxcl7OOlVBimg,3270
|
|
10
|
+
fastapi_admin_lite/ui/views.py,sha256=BHkBzOlH2Eg-CfYq_ZT2iL7W9NniB_d58WN4p1CJI0E,5793
|
|
11
|
+
fastapi_admin_lite/ui/templates/dashboard.html,sha256=-c2z3_mP5X33FTDgOguzsFKqmiClqRBoRUnyADgMLKA,6275
|
|
12
|
+
fastapi_admin_lite/ui/templates/layout.html,sha256=_BcFkBVySv8JmXPKTPT91zC2ycZUKUOjoW9Wg9SeH5I,9046
|
|
13
|
+
fastapi_admin_lite/ui/templates/model_detail.html,sha256=q_Wb0XjwTJBclBnEVhLNpGYcvWmQVCDtBewUhZmQNBo,5409
|
|
14
|
+
fastapi_admin_lite/ui/templates/model_form.html,sha256=p4rc_TZYvsMFgGK-Oa2_Iu2_JbfsL1g-6hveLaOV74Q,11196
|
|
15
|
+
fastapi_admin_lite/ui/templates/model_list.html,sha256=DdCT3qrMGuHLIFJQph5Vyp-EIuycWkbjEAGgM7qECmg,14739
|
|
16
|
+
fastapi_lite_admin-0.1.0.dist-info/METADATA,sha256=CJdw8l2srR0mcT92nGlud0fOVIy6m0qmNbJxpc1NzR0,1065
|
|
17
|
+
fastapi_lite_admin-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
fastapi_lite_admin-0.1.0.dist-info/top_level.txt,sha256=3tguJI_ZKi_gI3OEFdNwBR4PfzdNBR1ZBtKm4Wp3rsc,19
|
|
19
|
+
fastapi_lite_admin-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_admin_lite
|