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,359 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
|
|
3
|
+
{% set active_nav = model_name %}
|
|
4
|
+
|
|
5
|
+
{% block extra_css %}
|
|
6
|
+
.breadcrumb {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 8px;
|
|
10
|
+
font-size: 0.85rem;
|
|
11
|
+
color: var(--text-muted);
|
|
12
|
+
margin-bottom: 24px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.breadcrumb a {
|
|
16
|
+
color: var(--primary);
|
|
17
|
+
text-decoration: none;
|
|
18
|
+
font-weight: 500;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.form-header {
|
|
22
|
+
margin-bottom: 32px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.back-link {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: 8px;
|
|
29
|
+
font-size: 0.8rem;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
text-transform: uppercase;
|
|
32
|
+
color: var(--text-muted);
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
margin-bottom: 12px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.form-container {
|
|
38
|
+
max-width: 800px;
|
|
39
|
+
margin-bottom: 32px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.form-grid {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: repeat(2, 1fr);
|
|
45
|
+
gap: 24px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.form-group {
|
|
49
|
+
margin-bottom: 24px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.form-group.full-width {
|
|
53
|
+
grid-column: span 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.form-label {
|
|
57
|
+
display: block;
|
|
58
|
+
font-size: 0.75rem;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
text-transform: uppercase;
|
|
61
|
+
color: var(--text-muted);
|
|
62
|
+
margin-bottom: 8px;
|
|
63
|
+
letter-spacing: 0.05em;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.form-input {
|
|
67
|
+
width: 100%;
|
|
68
|
+
padding: 12px 16px;
|
|
69
|
+
border: 1px solid var(--border);
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
outline: none;
|
|
72
|
+
font-size: 0.95rem;
|
|
73
|
+
transition: all 0.2s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.form-input:focus {
|
|
77
|
+
border-color: var(--primary);
|
|
78
|
+
box-shadow: 0 0 0 4px rgba(0, 137, 167, 0.1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.form-footer {
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: flex-end;
|
|
84
|
+
gap: 12px;
|
|
85
|
+
padding: 20px 32px;
|
|
86
|
+
background-color: #fafafa;
|
|
87
|
+
border-top: 1px solid var(--border);
|
|
88
|
+
border-bottom-left-radius: 12px;
|
|
89
|
+
border-bottom-right-radius: 12px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Toggle Switch */
|
|
93
|
+
.toggle-group {
|
|
94
|
+
display: flex;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
align-items: center;
|
|
97
|
+
padding: 16px;
|
|
98
|
+
background-color: #f8fafc;
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.toggle-info .title {
|
|
104
|
+
font-weight: 700;
|
|
105
|
+
font-size: 0.9rem;
|
|
106
|
+
display: block;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.toggle-info .desc {
|
|
110
|
+
font-size: 0.75rem;
|
|
111
|
+
color: var(--text-muted);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.switch {
|
|
115
|
+
position: relative;
|
|
116
|
+
display: inline-block;
|
|
117
|
+
width: 44px;
|
|
118
|
+
height: 24px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.switch input { opacity: 0; width: 0; height: 0; }
|
|
122
|
+
|
|
123
|
+
.slider {
|
|
124
|
+
position: absolute;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
127
|
+
background-color: #cbd5e1;
|
|
128
|
+
transition: .4s;
|
|
129
|
+
border-radius: 24px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.slider:before {
|
|
133
|
+
position: absolute;
|
|
134
|
+
content: "";
|
|
135
|
+
height: 18px; width: 18px;
|
|
136
|
+
left: 3px; bottom: 3px;
|
|
137
|
+
background-color: white;
|
|
138
|
+
transition: .4s;
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
input:checked + .slider { background-color: var(--primary); }
|
|
143
|
+
input:checked + .slider:before { transform: translateX(20px); }
|
|
144
|
+
|
|
145
|
+
/* Metadata Cards */
|
|
146
|
+
.meta-cards {
|
|
147
|
+
display: grid;
|
|
148
|
+
grid-template-columns: 1fr 1fr;
|
|
149
|
+
gap: 24px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.meta-card {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 16px;
|
|
156
|
+
padding: 20px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.meta-icon {
|
|
160
|
+
width: 40px;
|
|
161
|
+
height: 40px;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
background-color: #f1f5f9;
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
color: var(--primary);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.meta-label {
|
|
171
|
+
font-size: 0.7rem;
|
|
172
|
+
text-transform: uppercase;
|
|
173
|
+
font-weight: 700;
|
|
174
|
+
color: var(--text-muted);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.meta-value {
|
|
178
|
+
font-size: 0.9rem;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
}
|
|
181
|
+
{% endblock %}
|
|
182
|
+
|
|
183
|
+
{% block content %}
|
|
184
|
+
|
|
185
|
+
<div class="breadcrumb">
|
|
186
|
+
<a href="/admin">Dashboard</a>
|
|
187
|
+
<i class="fas fa-chevron-right" style="font-size: 0.6rem;"></i>
|
|
188
|
+
<a href="/admin/{{ model_name }}">{{ model_name|capitalize }}</a>
|
|
189
|
+
<i class="fas fa-chevron-right" style="font-size: 0.6rem;"></i>
|
|
190
|
+
<span>{% if id %}Edit{% else %}Create{% endif %} {{ model_name|capitalize }}</span>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<a href="/admin/{{ model_name }}" class="back-link">
|
|
194
|
+
<i class="fas fa-arrow-left"></i> Back to List
|
|
195
|
+
</a>
|
|
196
|
+
|
|
197
|
+
<div class="form-header">
|
|
198
|
+
<h1 class="page-title">{% if id %}Edit{% else %}Create{% endif %} {{ model_name|capitalize }}{% if id %}: <span id="record-title">Loading...</span>{% endif %}</h1>
|
|
199
|
+
<p class="page-subtitle">{% if id %}Update the details and visibility of this record.{% else %}Define a new record for your database.{% endif %}</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="card form-container" style="padding: 0;">
|
|
203
|
+
<form id="record-form">
|
|
204
|
+
<div style="padding: 32px;">
|
|
205
|
+
<div class="form-grid" id="form-fields">
|
|
206
|
+
<!-- Fields injected via JS -->
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="form-footer">
|
|
211
|
+
<button type="button" class="btn btn-secondary" onclick="location.href='/admin/{{ model_name }}'">Cancel</button>
|
|
212
|
+
<button type="submit" class="btn btn-primary">{% if id %}Save Changes{% else %}Create Record{% endif %}</button>
|
|
213
|
+
</div>
|
|
214
|
+
</form>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{% if id %}
|
|
218
|
+
<div class="meta-cards">
|
|
219
|
+
<div class="card meta-card">
|
|
220
|
+
<div class="meta-icon">
|
|
221
|
+
<i class="far fa-clock"></i>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<div class="meta-label">Last Modified</div>
|
|
225
|
+
<div class="meta-value">Oct 24, 2023 - 14:32 PM</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="card meta-card">
|
|
229
|
+
<div class="meta-icon">
|
|
230
|
+
<i class="far fa-user"></i>
|
|
231
|
+
</div>
|
|
232
|
+
<div>
|
|
233
|
+
<div class="meta-label">Created By</div>
|
|
234
|
+
<div class="meta-value">System Admin (ID: 021)</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
{% endif %}
|
|
239
|
+
|
|
240
|
+
{% endblock %}
|
|
241
|
+
|
|
242
|
+
{% block extra_js %}
|
|
243
|
+
<script>
|
|
244
|
+
const modelName = "{{ model_name }}";
|
|
245
|
+
const recordId = "{{ id or '' }}";
|
|
246
|
+
const apiBaseUrl = "/admin/api";
|
|
247
|
+
|
|
248
|
+
async function loadForm() {
|
|
249
|
+
try {
|
|
250
|
+
// Fetch schema
|
|
251
|
+
const schemaResponse = await fetch(`${apiBaseUrl}/schema`);
|
|
252
|
+
const schema = await schemaResponse.json();
|
|
253
|
+
const modelSchema = schema.models.find(m => m.name === modelName);
|
|
254
|
+
|
|
255
|
+
let initialData = {};
|
|
256
|
+
if (recordId) {
|
|
257
|
+
const response = await fetch(`${apiBaseUrl}/${modelName}/${recordId}`);
|
|
258
|
+
initialData = await response.json();
|
|
259
|
+
|
|
260
|
+
// Update title if possible
|
|
261
|
+
const titleVal = initialData.name || initialData.email || initialData.id;
|
|
262
|
+
document.getElementById('record-title').textContent = titleVal;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const container = document.getElementById('form-fields');
|
|
266
|
+
const readonlyFields = modelSchema.config?.readonly_fields || [];
|
|
267
|
+
container.innerHTML = modelSchema.fields.map(field => {
|
|
268
|
+
const name = field.name;
|
|
269
|
+
const type = field.type.toLowerCase();
|
|
270
|
+
const isBoolean = type.includes('boolean');
|
|
271
|
+
const isReadonly = readonlyFields.includes(name);
|
|
272
|
+
const disabled = isReadonly ? 'disabled' : '';
|
|
273
|
+
|
|
274
|
+
if (name === 'id' && !recordId) return '';
|
|
275
|
+
if (name === 'id') return `<input type="hidden" name="id" value="${initialData[name]}">`;
|
|
276
|
+
|
|
277
|
+
const val = initialData[name] !== undefined ? initialData[name] : '';
|
|
278
|
+
|
|
279
|
+
if (isBoolean) {
|
|
280
|
+
const checked = val === true || val === 'true' ? 'checked' : '';
|
|
281
|
+
return `
|
|
282
|
+
<div class="form-group full-width">
|
|
283
|
+
<div class="toggle-group">
|
|
284
|
+
<div class="toggle-info">
|
|
285
|
+
<span class="title">Active Status</span>
|
|
286
|
+
<span class="desc">Toggle visibility of this record in the system.</span>
|
|
287
|
+
</div>
|
|
288
|
+
<label class="switch">
|
|
289
|
+
<input type="checkbox" name="${name}" ${checked} value="true" ${disabled}>
|
|
290
|
+
<span class="slider"></span>
|
|
291
|
+
</label>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const isFullWidth = name.includes('description') || name.includes('content');
|
|
298
|
+
const isRequired = field.required;
|
|
299
|
+
|
|
300
|
+
return `
|
|
301
|
+
<div class="form-group ${isFullWidth ? 'full-width' : ''}">
|
|
302
|
+
<label class="form-label">
|
|
303
|
+
${name.replace('_', ' ')}
|
|
304
|
+
${isRequired ? '<span style="color: var(--error); margin-left: 4px;">*</span>' : ''}
|
|
305
|
+
</label>
|
|
306
|
+
${isFullWidth ?
|
|
307
|
+
`<textarea class="form-input" name="${name}" rows="4" ${isRequired ? 'required' : ''} ${disabled}>${val}</textarea>` :
|
|
308
|
+
`<input type="text" class="form-input" name="${name}" value="${val}" placeholder="${field.type}" ${isRequired ? 'required' : ''} ${disabled}>`
|
|
309
|
+
}
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
}).join('');
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Error loading form:', error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
document.getElementById('record-form').onsubmit = async (e) => {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
const formData = new FormData(e.target);
|
|
321
|
+
|
|
322
|
+
// Handle checkboxes (ensure false is sent if unchecked)
|
|
323
|
+
const data = Object.fromEntries(formData.entries());
|
|
324
|
+
|
|
325
|
+
// We need to check the schema to see which fields are booleans and handle unchecked state
|
|
326
|
+
const schemaResponse = await fetch(`${apiBaseUrl}/schema`);
|
|
327
|
+
const schema = await schemaResponse.json();
|
|
328
|
+
const modelSchema = schema.models.find(m => m.name === modelName);
|
|
329
|
+
|
|
330
|
+
modelSchema.fields.forEach(field => {
|
|
331
|
+
if (field.type.toLowerCase().includes('boolean') && !data[field.name]) {
|
|
332
|
+
data[field.name] = "false";
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const method = recordId ? 'PUT' : 'POST';
|
|
337
|
+
const url = recordId ? `${apiBaseUrl}/${modelName}/${recordId}` : `${apiBaseUrl}/${modelName}`;
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
method: method,
|
|
342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
343
|
+
body: JSON.stringify(data)
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (response.ok) {
|
|
347
|
+
location.href = `/admin/${modelName}`;
|
|
348
|
+
} else {
|
|
349
|
+
const err = await response.json();
|
|
350
|
+
alert('Error saving record: ' + (err.detail || 'Unknown error'));
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
alert('Error saving record');
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
loadForm();
|
|
358
|
+
</script>
|
|
359
|
+
{% endblock %}
|