fastapi-lite-admin 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_lite_admin-0.1.6/fastapi_lite_admin.egg-info → fastapi_lite_admin-0.1.7}/PKG-INFO +48 -1
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/README.md +47 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/main.py +19 -1
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/routers/admin.py +32 -1
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/model_detail.html +32 -2
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/model_form.html +298 -1
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7/fastapi_lite_admin.egg-info}/PKG-INFO +48 -1
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/pyproject.toml +1 -1
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/LICENSE +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/__init__.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/config.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/crud.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/registry.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/schema.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/dependencies/db.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/integrations/sqlalchemy.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/dashboard.html +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/layout.html +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/model_list.html +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/views.py +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/SOURCES.txt +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/dependency_links.txt +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/requires.txt +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/top_level.txt +0 -0
- {fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-lite-admin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: A lightweight, pluggable admin panel for FastAPI
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/rishiqwerty/admin-panel-fast-api
|
|
@@ -122,6 +122,7 @@ When calling `admin.register()`, you can configure how each model is represented
|
|
|
122
122
|
| **`date_field`** | `str` | No | Name of the datetime field (e.g. `created_at`). Required to show the "24h Activity" count cards on the dashboard and lists. |
|
|
123
123
|
| **`attention_filter`** | `SQLAlchemy Expression` | No | A SQLAlchemy binary filter expression (e.g., `User.is_active == False` or `Product.stock < 10`) used to calculate and flag rows that require moderator attention. |
|
|
124
124
|
| **`readonly_fields`** | `List[str]` | No | List of columns that cannot be modified or set via creation or updates (e.g., auto-generated columns or timestamps like `id`, `created_at`). |
|
|
125
|
+
| **`file_fields`** | `List[str]` | No | List of column names that should be treated as file upload fields, rendering a drag-and-drop zone. |
|
|
125
126
|
| **`config`** | `Dict[str, Any]` | No | Dictionary containing extra settings. Supports `"display_name"` to override the sidebar label. |
|
|
126
127
|
|
|
127
128
|
---
|
|
@@ -141,6 +142,9 @@ The `Admin` class constructor supports the following parameters for customizatio
|
|
|
141
142
|
| **`dashboard_models`** | `List[str]` | `None` | List of registered model names to display on the dashboard (if you want to restrict which registered models show on the home dashboard). |
|
|
142
143
|
| **`get_logs`** | `Callable` | `None` | An optional callable (async or sync) returning system logs to display on the dashboard activity log feed. |
|
|
143
144
|
| **`logs_config`** | `Dict[str, Any]` | `{"title": "System Activity", "columns": ["level", "timestamp", "message"]}` | Config dictionary to customize dashboard log columns and activity title. |
|
|
145
|
+
| **`upload_dir`** | `str` | `"uploads"` | Local directory path where uploaded files will be stored. |
|
|
146
|
+
| **`upload_url`** | `str` | `"/uploads"` | URL prefix used to serve uploaded files statically. |
|
|
147
|
+
| **`upload_handler`** | `Callable` | `None` | Optional custom upload handler callback for buckets (S3, GCS, Azure). |
|
|
144
148
|
|
|
145
149
|
---
|
|
146
150
|
|
|
@@ -204,6 +208,49 @@ admin = Admin(
|
|
|
204
208
|
)
|
|
205
209
|
```
|
|
206
210
|
|
|
211
|
+
## File Uploads & Cloud Storage
|
|
212
|
+
|
|
213
|
+
`FastAPI Lite Admin` supports rendering drag-and-drop file uploads for designated string fields (e.g., image paths or document URLs).
|
|
214
|
+
|
|
215
|
+
### 1. Default Local Storage
|
|
216
|
+
By default, uploaded files are stored locally in the `uploads/` directory and served statically:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
admin = Admin(
|
|
220
|
+
title="My Admin",
|
|
221
|
+
upload_dir="my_uploads", # Stored in project-root/my_uploads
|
|
222
|
+
upload_url="/static/files" # Served statically at http://localhost:8000/static/files
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 2. Cloud Storage / Buckets (S3, GCS, Azure, etc.)
|
|
227
|
+
If you are deploying to production and storing files in a cloud bucket, you can plug in a custom `upload_handler`:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from fastapi import UploadFile
|
|
231
|
+
|
|
232
|
+
async def my_s3_upload_handler(file: UploadFile) -> str:
|
|
233
|
+
# 1. Upload file.file to S3, GCS, Cloudinary, etc.
|
|
234
|
+
# 2. Return the public URL to be stored in the database
|
|
235
|
+
return f"https://my-bucket.s3.amazonaws.com/{file.filename}"
|
|
236
|
+
|
|
237
|
+
admin = Admin(
|
|
238
|
+
title="Cloud Admin",
|
|
239
|
+
upload_handler=my_s3_upload_handler
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 3. Enabling File Upload in Models
|
|
244
|
+
Pass the `file_fields` parameter when registering your model:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
admin.register(
|
|
248
|
+
model=Product,
|
|
249
|
+
get_db=get_db,
|
|
250
|
+
file_fields=["image_url"] # These will render as drag-and-drop zones
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
207
254
|
---
|
|
208
255
|
|
|
209
256
|
## Running the Example Application
|
|
@@ -101,6 +101,7 @@ When calling `admin.register()`, you can configure how each model is represented
|
|
|
101
101
|
| **`date_field`** | `str` | No | Name of the datetime field (e.g. `created_at`). Required to show the "24h Activity" count cards on the dashboard and lists. |
|
|
102
102
|
| **`attention_filter`** | `SQLAlchemy Expression` | No | A SQLAlchemy binary filter expression (e.g., `User.is_active == False` or `Product.stock < 10`) used to calculate and flag rows that require moderator attention. |
|
|
103
103
|
| **`readonly_fields`** | `List[str]` | No | List of columns that cannot be modified or set via creation or updates (e.g., auto-generated columns or timestamps like `id`, `created_at`). |
|
|
104
|
+
| **`file_fields`** | `List[str]` | No | List of column names that should be treated as file upload fields, rendering a drag-and-drop zone. |
|
|
104
105
|
| **`config`** | `Dict[str, Any]` | No | Dictionary containing extra settings. Supports `"display_name"` to override the sidebar label. |
|
|
105
106
|
|
|
106
107
|
---
|
|
@@ -120,6 +121,9 @@ The `Admin` class constructor supports the following parameters for customizatio
|
|
|
120
121
|
| **`dashboard_models`** | `List[str]` | `None` | List of registered model names to display on the dashboard (if you want to restrict which registered models show on the home dashboard). |
|
|
121
122
|
| **`get_logs`** | `Callable` | `None` | An optional callable (async or sync) returning system logs to display on the dashboard activity log feed. |
|
|
122
123
|
| **`logs_config`** | `Dict[str, Any]` | `{"title": "System Activity", "columns": ["level", "timestamp", "message"]}` | Config dictionary to customize dashboard log columns and activity title. |
|
|
124
|
+
| **`upload_dir`** | `str` | `"uploads"` | Local directory path where uploaded files will be stored. |
|
|
125
|
+
| **`upload_url`** | `str` | `"/uploads"` | URL prefix used to serve uploaded files statically. |
|
|
126
|
+
| **`upload_handler`** | `Callable` | `None` | Optional custom upload handler callback for buckets (S3, GCS, Azure). |
|
|
123
127
|
|
|
124
128
|
---
|
|
125
129
|
|
|
@@ -183,6 +187,49 @@ admin = Admin(
|
|
|
183
187
|
)
|
|
184
188
|
```
|
|
185
189
|
|
|
190
|
+
## File Uploads & Cloud Storage
|
|
191
|
+
|
|
192
|
+
`FastAPI Lite Admin` supports rendering drag-and-drop file uploads for designated string fields (e.g., image paths or document URLs).
|
|
193
|
+
|
|
194
|
+
### 1. Default Local Storage
|
|
195
|
+
By default, uploaded files are stored locally in the `uploads/` directory and served statically:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
admin = Admin(
|
|
199
|
+
title="My Admin",
|
|
200
|
+
upload_dir="my_uploads", # Stored in project-root/my_uploads
|
|
201
|
+
upload_url="/static/files" # Served statically at http://localhost:8000/static/files
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 2. Cloud Storage / Buckets (S3, GCS, Azure, etc.)
|
|
206
|
+
If you are deploying to production and storing files in a cloud bucket, you can plug in a custom `upload_handler`:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from fastapi import UploadFile
|
|
210
|
+
|
|
211
|
+
async def my_s3_upload_handler(file: UploadFile) -> str:
|
|
212
|
+
# 1. Upload file.file to S3, GCS, Cloudinary, etc.
|
|
213
|
+
# 2. Return the public URL to be stored in the database
|
|
214
|
+
return f"https://my-bucket.s3.amazonaws.com/{file.filename}"
|
|
215
|
+
|
|
216
|
+
admin = Admin(
|
|
217
|
+
title="Cloud Admin",
|
|
218
|
+
upload_handler=my_s3_upload_handler
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 3. Enabling File Upload in Models
|
|
223
|
+
Pass the `file_fields` parameter when registering your model:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
admin.register(
|
|
227
|
+
model=Product,
|
|
228
|
+
get_db=get_db,
|
|
229
|
+
file_fields=["image_url"] # These will render as drag-and-drop zones
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
186
233
|
---
|
|
187
234
|
|
|
188
235
|
## Running the Example Application
|
|
@@ -16,7 +16,10 @@ class Admin:
|
|
|
16
16
|
dashboard_template: Optional[str] = None,
|
|
17
17
|
dashboard_models: Optional[List[str]] = None,
|
|
18
18
|
get_logs: Optional[Callable] = None,
|
|
19
|
-
logs_config: Optional[Dict[str, Any]] = None
|
|
19
|
+
logs_config: Optional[Dict[str, Any]] = None,
|
|
20
|
+
upload_dir: str = "uploads",
|
|
21
|
+
upload_url: str = "/uploads",
|
|
22
|
+
upload_handler: Optional[Callable[[Any], Any]] = None
|
|
20
23
|
):
|
|
21
24
|
self.title = title
|
|
22
25
|
self.base_url = base_url
|
|
@@ -30,6 +33,9 @@ class Admin:
|
|
|
30
33
|
"columns": ["level", "timestamp", "message"],
|
|
31
34
|
"title": "System Activity"
|
|
32
35
|
}
|
|
36
|
+
self.upload_dir = upload_dir
|
|
37
|
+
self.upload_url = upload_url
|
|
38
|
+
self.upload_handler = upload_handler
|
|
33
39
|
|
|
34
40
|
# Auth & Permissions
|
|
35
41
|
self.auth_dependency = auth_dependency
|
|
@@ -53,6 +59,7 @@ class Admin:
|
|
|
53
59
|
date_field: Optional[str] = None,
|
|
54
60
|
attention_filter: Optional[Any] = None,
|
|
55
61
|
readonly_fields: Optional[List[str]] = None,
|
|
62
|
+
file_fields: Optional[List[str]] = None,
|
|
56
63
|
config: Optional[Dict[str, Any]] = None
|
|
57
64
|
):
|
|
58
65
|
"""
|
|
@@ -73,6 +80,9 @@ class Admin:
|
|
|
73
80
|
|
|
74
81
|
if readonly_fields:
|
|
75
82
|
config["readonly_fields"] = readonly_fields
|
|
83
|
+
|
|
84
|
+
if file_fields:
|
|
85
|
+
config["file_fields"] = file_fields
|
|
76
86
|
|
|
77
87
|
self.registry.register(model, get_db, config)
|
|
78
88
|
|
|
@@ -80,6 +90,14 @@ class Admin:
|
|
|
80
90
|
"""
|
|
81
91
|
Mount the admin router to the FastAPI application.
|
|
82
92
|
"""
|
|
93
|
+
# Mount upload directory for static access if using default storage
|
|
94
|
+
if not self.upload_handler:
|
|
95
|
+
import os
|
|
96
|
+
from fastapi.staticfiles import StaticFiles
|
|
97
|
+
if not os.path.exists(self.upload_dir):
|
|
98
|
+
os.makedirs(self.upload_dir)
|
|
99
|
+
app.mount(self.upload_url, StaticFiles(directory=self.upload_dir), name="admin_uploads")
|
|
100
|
+
|
|
83
101
|
# Collect all dependencies
|
|
84
102
|
all_deps = list(self.dependencies)
|
|
85
103
|
if self.auth_dependency:
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from typing import Any, Dict, List
|
|
2
|
-
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
2
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
|
3
|
+
import shutil
|
|
4
|
+
import uuid
|
|
5
|
+
import os
|
|
6
|
+
import asyncio
|
|
3
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
8
|
from ..core.schema import generate_admin_schema
|
|
5
9
|
from ..core.crud import CRUDEngine
|
|
@@ -11,6 +15,33 @@ def create_admin_router(admin: Any) -> APIRouter:
|
|
|
11
15
|
async def get_schema():
|
|
12
16
|
return generate_admin_schema(admin.registry)
|
|
13
17
|
|
|
18
|
+
@router.post("/upload", name="admin_upload_file")
|
|
19
|
+
async def upload_file(file: UploadFile = File(...)):
|
|
20
|
+
if admin.upload_handler:
|
|
21
|
+
try:
|
|
22
|
+
if asyncio.iscoroutinefunction(admin.upload_handler):
|
|
23
|
+
url = await admin.upload_handler(file)
|
|
24
|
+
else:
|
|
25
|
+
url = admin.upload_handler(file)
|
|
26
|
+
return {"url": url}
|
|
27
|
+
except Exception as e:
|
|
28
|
+
raise HTTPException(status_code=500, detail=f"Custom upload failed: {str(e)}")
|
|
29
|
+
else:
|
|
30
|
+
if not os.path.exists(admin.upload_dir):
|
|
31
|
+
os.makedirs(admin.upload_dir)
|
|
32
|
+
|
|
33
|
+
ext = os.path.splitext(file.filename)[1]
|
|
34
|
+
filename = f"{uuid.uuid4()}{ext}"
|
|
35
|
+
filepath = os.path.join(admin.upload_dir, filename)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with open(filepath, "wb") as buffer:
|
|
39
|
+
shutil.copyfileobj(file.file, buffer)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}")
|
|
42
|
+
|
|
43
|
+
return {"url": f"{admin.upload_url}/{filename}"}
|
|
44
|
+
|
|
14
45
|
@router.get("/models")
|
|
15
46
|
async def list_models():
|
|
16
47
|
return list(admin.registry.get_models().keys())
|
|
@@ -142,6 +142,12 @@
|
|
|
142
142
|
|
|
143
143
|
async function loadDetail() {
|
|
144
144
|
try {
|
|
145
|
+
// Fetch schema to check file fields
|
|
146
|
+
const schemaResponse = await fetch(`${apiBaseUrl}/schema`);
|
|
147
|
+
const schema = await schemaResponse.json();
|
|
148
|
+
const modelSchema = schema.models.find(m => m.name === modelName);
|
|
149
|
+
const fileFields = modelSchema?.config?.file_fields || [];
|
|
150
|
+
|
|
145
151
|
const response = await fetch(`${apiBaseUrl}/${modelName}/${recordId}`);
|
|
146
152
|
const data = await response.json();
|
|
147
153
|
|
|
@@ -150,10 +156,34 @@
|
|
|
150
156
|
|
|
151
157
|
container.innerHTML = keys.map(key => {
|
|
152
158
|
const val = data[key];
|
|
153
|
-
|
|
159
|
+
|
|
160
|
+
const isFile = fileFields.includes(key);
|
|
161
|
+
const isFullWidth = (typeof val === 'string' && val.length > 50) || isFile;
|
|
154
162
|
|
|
155
163
|
let displayVal = val;
|
|
156
|
-
if (
|
|
164
|
+
if (isFile) {
|
|
165
|
+
if (!val) {
|
|
166
|
+
displayVal = '<span style="color: var(--text-muted);">None</span>';
|
|
167
|
+
} else {
|
|
168
|
+
const isImg = val.match(/\.(jpeg|jpg|gif|png|webp)$/i);
|
|
169
|
+
if (isImg) {
|
|
170
|
+
displayVal = `
|
|
171
|
+
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 8px;">
|
|
172
|
+
<img src="${val}" alt="${key}" style="max-width: 300px; max-height: 300px; object-fit: contain; border-radius: 8px; border: 1px solid var(--border);">
|
|
173
|
+
<a href="${val}" target="_blank" class="file-preview-link" style="color: var(--primary); text-decoration: none; font-size: 0.85rem; font-weight: 500;">
|
|
174
|
+
<i class="fas fa-external-link-alt"></i> View full size
|
|
175
|
+
</a>
|
|
176
|
+
</div>
|
|
177
|
+
`;
|
|
178
|
+
} else {
|
|
179
|
+
displayVal = `
|
|
180
|
+
<a href="${val}" target="_blank" class="file-preview-link" style="display: inline-flex; align-items: center; gap: 8px; color: var(--primary); text-decoration: none; font-weight: 500; margin-top: 4px;">
|
|
181
|
+
<i class="fas fa-file-alt" style="font-size: 1.25rem;"></i> View File (${val.split('/').pop()})
|
|
182
|
+
</a>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else if (typeof val === 'boolean' || key === 'is_active') {
|
|
157
187
|
const isActive = val === true || val === 'true';
|
|
158
188
|
displayVal = `<span class="status-pill ${isActive ? 'active' : 'inactive'}">
|
|
159
189
|
<i class="fas ${isActive ? 'fa-check-circle' : 'fa-times-circle'}"></i>
|
|
@@ -188,6 +188,148 @@
|
|
|
188
188
|
font-weight: 600;
|
|
189
189
|
color: var(--text-main);
|
|
190
190
|
}
|
|
191
|
+
|
|
192
|
+
/* File Upload Styling */
|
|
193
|
+
.file-upload-wrapper {
|
|
194
|
+
position: relative;
|
|
195
|
+
width: 100%;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.file-upload-dragzone {
|
|
199
|
+
border: 2px dashed var(--border);
|
|
200
|
+
border-radius: 12px;
|
|
201
|
+
padding: 32px 20px;
|
|
202
|
+
text-align: center;
|
|
203
|
+
background-color: var(--bg-card);
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: all 0.3s ease;
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.file-upload-dragzone:hover {
|
|
213
|
+
border-color: var(--primary);
|
|
214
|
+
background-color: rgba(99, 102, 241, 0.02);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.file-upload-dragzone.dragover {
|
|
218
|
+
border-color: var(--primary);
|
|
219
|
+
background-color: rgba(99, 102, 241, 0.06);
|
|
220
|
+
transform: scale(0.99);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.file-upload-dragzone.hidden {
|
|
224
|
+
display: none;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.upload-zone-content {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
align-items: center;
|
|
231
|
+
gap: 8px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.upload-icon {
|
|
235
|
+
font-size: 2rem;
|
|
236
|
+
color: var(--text-muted);
|
|
237
|
+
transition: color 0.3s ease;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.file-upload-dragzone:hover .upload-icon {
|
|
241
|
+
color: var(--primary);
|
|
242
|
+
transform: translateY(-2px);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.upload-text {
|
|
246
|
+
font-size: 0.9rem;
|
|
247
|
+
font-weight: 500;
|
|
248
|
+
color: var(--text-main);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.browse-text {
|
|
252
|
+
color: var(--primary);
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.upload-hint {
|
|
257
|
+
font-size: 0.75rem;
|
|
258
|
+
color: var(--text-muted);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* File Indicator (No Preview) */
|
|
262
|
+
.file-preview-container {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
gap: 16px;
|
|
266
|
+
padding: 16px;
|
|
267
|
+
background-color: var(--bg-card);
|
|
268
|
+
border: 1px solid var(--border);
|
|
269
|
+
border-radius: 12px;
|
|
270
|
+
position: relative;
|
|
271
|
+
margin-top: 8px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.file-preview-icon {
|
|
275
|
+
width: 44px;
|
|
276
|
+
height: 44px;
|
|
277
|
+
border-radius: 8px;
|
|
278
|
+
background-color: var(--bg-surface);
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
justify-content: center;
|
|
282
|
+
font-size: 1.25rem;
|
|
283
|
+
color: var(--primary);
|
|
284
|
+
border: 1px solid var(--border);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.file-preview-info {
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
gap: 4px;
|
|
291
|
+
flex: 1;
|
|
292
|
+
min-width: 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.file-preview-name {
|
|
296
|
+
font-size: 0.9rem;
|
|
297
|
+
font-weight: 600;
|
|
298
|
+
color: var(--text-main);
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
overflow: hidden;
|
|
301
|
+
text-overflow: ellipsis;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.file-preview-link {
|
|
305
|
+
font-size: 0.75rem;
|
|
306
|
+
color: var(--primary);
|
|
307
|
+
text-decoration: none;
|
|
308
|
+
font-weight: 500;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.file-preview-link:hover {
|
|
312
|
+
text-decoration: underline;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.btn-remove-file {
|
|
316
|
+
background: none;
|
|
317
|
+
border: none;
|
|
318
|
+
color: var(--text-muted);
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
padding: 8px;
|
|
321
|
+
font-size: 1rem;
|
|
322
|
+
transition: color 0.2s;
|
|
323
|
+
border-radius: 50%;
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
justify-content: center;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.btn-remove-file:hover {
|
|
330
|
+
color: var(--error);
|
|
331
|
+
background-color: rgba(239, 68, 68, 0.1);
|
|
332
|
+
}
|
|
191
333
|
{% endblock %}
|
|
192
334
|
|
|
193
335
|
{% block content %}
|
|
@@ -274,6 +416,8 @@
|
|
|
274
416
|
|
|
275
417
|
const container = document.getElementById('form-fields');
|
|
276
418
|
const readonlyFields = modelSchema.config?.readonly_fields || [];
|
|
419
|
+
const fileFields = modelSchema.config?.file_fields || [];
|
|
420
|
+
|
|
277
421
|
container.innerHTML = modelSchema.fields.map(field => {
|
|
278
422
|
const name = field.name;
|
|
279
423
|
const type = field.type.toLowerCase();
|
|
@@ -285,6 +429,7 @@
|
|
|
285
429
|
if (name === 'id') return `<input type="hidden" name="id" value="${initialData[name]}">`;
|
|
286
430
|
|
|
287
431
|
const val = initialData[name] !== undefined ? initialData[name] : '';
|
|
432
|
+
const isRequired = field.required;
|
|
288
433
|
|
|
289
434
|
if (isBoolean) {
|
|
290
435
|
const checked = val === true || val === 'true' ? 'checked' : '';
|
|
@@ -304,8 +449,46 @@
|
|
|
304
449
|
`;
|
|
305
450
|
}
|
|
306
451
|
|
|
452
|
+
const isFile = fileFields.includes(name);
|
|
453
|
+
if (isFile) {
|
|
454
|
+
const previewHtml = val ? `
|
|
455
|
+
<div class="file-preview-container" id="preview-${name}">
|
|
456
|
+
<div class="file-preview-icon"><i class="fas fa-file-alt"></i></div>
|
|
457
|
+
<div class="file-preview-info">
|
|
458
|
+
<span class="file-preview-name">${val.split('/').pop()}</span>
|
|
459
|
+
<a href="${val}" target="_blank" class="file-preview-link">View file</a>
|
|
460
|
+
</div>
|
|
461
|
+
<button type="button" class="btn-remove-file" onclick="clearFile('${name}')">
|
|
462
|
+
<i class="fas fa-times"></i>
|
|
463
|
+
</button>
|
|
464
|
+
</div>
|
|
465
|
+
` : '';
|
|
466
|
+
|
|
467
|
+
return `
|
|
468
|
+
<div class="form-group full-width">
|
|
469
|
+
<label class="form-label">
|
|
470
|
+
${name.replace('_', ' ')}
|
|
471
|
+
${isRequired ? '<span style="color: var(--error); margin-left: 4px;">*</span>' : ''}
|
|
472
|
+
</label>
|
|
473
|
+
<div class="file-upload-wrapper">
|
|
474
|
+
<input type="hidden" id="input-hidden-${name}" name="${name}" value="${val}" ${isRequired ? 'required' : ''}>
|
|
475
|
+
<div class="file-upload-dragzone ${val ? 'hidden' : ''}" id="dragzone-${name}" onclick="document.getElementById('input-file-${name}').click()">
|
|
476
|
+
<div class="upload-zone-content">
|
|
477
|
+
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
|
478
|
+
<div class="upload-text">Drag & drop a file here, or <span class="browse-text">browse</span></div>
|
|
479
|
+
<div class="upload-hint">Supports any file type</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
<input type="file" id="input-file-${name}" class="hidden-file-input" style="display: none;" onchange="handleFileSelect(event, '${name}')" ${disabled}>
|
|
483
|
+
<div id="preview-wrapper-${name}">
|
|
484
|
+
${previewHtml}
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
|
|
307
491
|
const isFullWidth = name.includes('description') || name.includes('content');
|
|
308
|
-
const isRequired = field.required;
|
|
309
492
|
|
|
310
493
|
return `
|
|
311
494
|
<div class="form-group ${isFullWidth ? 'full-width' : ''}">
|
|
@@ -320,6 +503,11 @@
|
|
|
320
503
|
</div>
|
|
321
504
|
`;
|
|
322
505
|
}).join('');
|
|
506
|
+
|
|
507
|
+
// Setup drag & drop for file fields
|
|
508
|
+
fileFields.forEach(name => {
|
|
509
|
+
setupDragAndDrop(name);
|
|
510
|
+
});
|
|
323
511
|
} catch (error) {
|
|
324
512
|
console.error('Error loading form:', error);
|
|
325
513
|
}
|
|
@@ -364,6 +552,115 @@
|
|
|
364
552
|
}
|
|
365
553
|
};
|
|
366
554
|
|
|
555
|
+
async function handleFileSelect(event, fieldName) {
|
|
556
|
+
const file = event.target.files[0];
|
|
557
|
+
if (!file) return;
|
|
558
|
+
await uploadFile(file, fieldName);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function uploadFile(file, fieldName) {
|
|
562
|
+
const dragzone = document.getElementById(`dragzone-${fieldName}`);
|
|
563
|
+
const previewWrapper = document.getElementById(`preview-wrapper-${fieldName}`);
|
|
564
|
+
const hiddenInput = document.getElementById(`input-hidden-${fieldName}`);
|
|
565
|
+
|
|
566
|
+
// Show uploading state in the dragzone
|
|
567
|
+
const originalContent = dragzone.innerHTML;
|
|
568
|
+
dragzone.innerHTML = `
|
|
569
|
+
<div class="upload-zone-content">
|
|
570
|
+
<i class="fas fa-spinner fa-spin upload-icon" style="color: var(--primary);"></i>
|
|
571
|
+
<div class="upload-text">Uploading ${file.name}...</div>
|
|
572
|
+
</div>
|
|
573
|
+
`;
|
|
574
|
+
|
|
575
|
+
const formData = new FormData();
|
|
576
|
+
formData.append('file', file);
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const response = await fetch(`${apiBaseUrl}/upload`, {
|
|
580
|
+
method: 'POST',
|
|
581
|
+
body: formData
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
if (!response.ok) {
|
|
585
|
+
throw new Error('Upload failed');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const result = await response.json();
|
|
589
|
+
const fileUrl = result.url;
|
|
590
|
+
|
|
591
|
+
// Set value in hidden input
|
|
592
|
+
hiddenInput.value = fileUrl;
|
|
593
|
+
|
|
594
|
+
// Update preview
|
|
595
|
+
previewWrapper.innerHTML = `
|
|
596
|
+
<div class="file-preview-container" id="preview-${fieldName}">
|
|
597
|
+
<div class="file-preview-icon"><i class="fas fa-file-alt"></i></div>
|
|
598
|
+
<div class="file-preview-info">
|
|
599
|
+
<span class="file-preview-name">${file.name}</span>
|
|
600
|
+
<a href="${fileUrl}" target="_blank" class="file-preview-link">View file</a>
|
|
601
|
+
</div>
|
|
602
|
+
<button type="button" class="btn-remove-file" onclick="clearFile('${fieldName}')">
|
|
603
|
+
<i class="fas fa-times"></i>
|
|
604
|
+
</button>
|
|
605
|
+
</div>
|
|
606
|
+
`;
|
|
607
|
+
|
|
608
|
+
// Hide dragzone
|
|
609
|
+
dragzone.classList.add('hidden');
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error('Error uploading file:', error);
|
|
612
|
+
alert('Failed to upload file: ' + error.message);
|
|
613
|
+
// Restore dragzone
|
|
614
|
+
dragzone.innerHTML = originalContent;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function clearFile(fieldName) {
|
|
619
|
+
const dragzone = document.getElementById(`dragzone-${fieldName}`);
|
|
620
|
+
const previewWrapper = document.getElementById(`preview-wrapper-${fieldName}`);
|
|
621
|
+
const hiddenInput = document.getElementById(`input-hidden-${fieldName}`);
|
|
622
|
+
|
|
623
|
+
hiddenInput.value = '';
|
|
624
|
+
previewWrapper.innerHTML = '';
|
|
625
|
+
dragzone.classList.remove('hidden');
|
|
626
|
+
|
|
627
|
+
// Reset file input value so same file can be selected again
|
|
628
|
+
const fileInput = document.getElementById(`input-file-${fieldName}`);
|
|
629
|
+
if (fileInput) {
|
|
630
|
+
fileInput.value = '';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Setup drag and drop for a dragzone
|
|
635
|
+
function setupDragAndDrop(fieldName) {
|
|
636
|
+
const dragzone = document.getElementById(`dragzone-${fieldName}`);
|
|
637
|
+
if (!dragzone) return;
|
|
638
|
+
|
|
639
|
+
['dragenter', 'dragover'].forEach(eventName => {
|
|
640
|
+
dragzone.addEventListener(eventName, (e) => {
|
|
641
|
+
e.preventDefault();
|
|
642
|
+
e.stopPropagation();
|
|
643
|
+
dragzone.classList.add('dragover');
|
|
644
|
+
}, false);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
['dragleave', 'drop'].forEach(eventName => {
|
|
648
|
+
dragzone.addEventListener(eventName, (e) => {
|
|
649
|
+
e.preventDefault();
|
|
650
|
+
e.stopPropagation();
|
|
651
|
+
dragzone.classList.remove('dragover');
|
|
652
|
+
}, false);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
dragzone.addEventListener('drop', async (e) => {
|
|
656
|
+
const dt = e.dataTransfer;
|
|
657
|
+
const file = dt.files[0];
|
|
658
|
+
if (file) {
|
|
659
|
+
await uploadFile(file, fieldName);
|
|
660
|
+
}
|
|
661
|
+
}, false);
|
|
662
|
+
}
|
|
663
|
+
|
|
367
664
|
loadForm();
|
|
368
665
|
</script>
|
|
369
666
|
{% endblock %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-lite-admin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: A lightweight, pluggable admin panel for FastAPI
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/rishiqwerty/admin-panel-fast-api
|
|
@@ -122,6 +122,7 @@ When calling `admin.register()`, you can configure how each model is represented
|
|
|
122
122
|
| **`date_field`** | `str` | No | Name of the datetime field (e.g. `created_at`). Required to show the "24h Activity" count cards on the dashboard and lists. |
|
|
123
123
|
| **`attention_filter`** | `SQLAlchemy Expression` | No | A SQLAlchemy binary filter expression (e.g., `User.is_active == False` or `Product.stock < 10`) used to calculate and flag rows that require moderator attention. |
|
|
124
124
|
| **`readonly_fields`** | `List[str]` | No | List of columns that cannot be modified or set via creation or updates (e.g., auto-generated columns or timestamps like `id`, `created_at`). |
|
|
125
|
+
| **`file_fields`** | `List[str]` | No | List of column names that should be treated as file upload fields, rendering a drag-and-drop zone. |
|
|
125
126
|
| **`config`** | `Dict[str, Any]` | No | Dictionary containing extra settings. Supports `"display_name"` to override the sidebar label. |
|
|
126
127
|
|
|
127
128
|
---
|
|
@@ -141,6 +142,9 @@ The `Admin` class constructor supports the following parameters for customizatio
|
|
|
141
142
|
| **`dashboard_models`** | `List[str]` | `None` | List of registered model names to display on the dashboard (if you want to restrict which registered models show on the home dashboard). |
|
|
142
143
|
| **`get_logs`** | `Callable` | `None` | An optional callable (async or sync) returning system logs to display on the dashboard activity log feed. |
|
|
143
144
|
| **`logs_config`** | `Dict[str, Any]` | `{"title": "System Activity", "columns": ["level", "timestamp", "message"]}` | Config dictionary to customize dashboard log columns and activity title. |
|
|
145
|
+
| **`upload_dir`** | `str` | `"uploads"` | Local directory path where uploaded files will be stored. |
|
|
146
|
+
| **`upload_url`** | `str` | `"/uploads"` | URL prefix used to serve uploaded files statically. |
|
|
147
|
+
| **`upload_handler`** | `Callable` | `None` | Optional custom upload handler callback for buckets (S3, GCS, Azure). |
|
|
144
148
|
|
|
145
149
|
---
|
|
146
150
|
|
|
@@ -204,6 +208,49 @@ admin = Admin(
|
|
|
204
208
|
)
|
|
205
209
|
```
|
|
206
210
|
|
|
211
|
+
## File Uploads & Cloud Storage
|
|
212
|
+
|
|
213
|
+
`FastAPI Lite Admin` supports rendering drag-and-drop file uploads for designated string fields (e.g., image paths or document URLs).
|
|
214
|
+
|
|
215
|
+
### 1. Default Local Storage
|
|
216
|
+
By default, uploaded files are stored locally in the `uploads/` directory and served statically:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
admin = Admin(
|
|
220
|
+
title="My Admin",
|
|
221
|
+
upload_dir="my_uploads", # Stored in project-root/my_uploads
|
|
222
|
+
upload_url="/static/files" # Served statically at http://localhost:8000/static/files
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 2. Cloud Storage / Buckets (S3, GCS, Azure, etc.)
|
|
227
|
+
If you are deploying to production and storing files in a cloud bucket, you can plug in a custom `upload_handler`:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from fastapi import UploadFile
|
|
231
|
+
|
|
232
|
+
async def my_s3_upload_handler(file: UploadFile) -> str:
|
|
233
|
+
# 1. Upload file.file to S3, GCS, Cloudinary, etc.
|
|
234
|
+
# 2. Return the public URL to be stored in the database
|
|
235
|
+
return f"https://my-bucket.s3.amazonaws.com/{file.filename}"
|
|
236
|
+
|
|
237
|
+
admin = Admin(
|
|
238
|
+
title="Cloud Admin",
|
|
239
|
+
upload_handler=my_s3_upload_handler
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 3. Enabling File Upload in Models
|
|
244
|
+
Pass the `file_fields` parameter when registering your model:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
admin.register(
|
|
248
|
+
model=Product,
|
|
249
|
+
get_db=get_db,
|
|
250
|
+
file_fields=["image_url"] # These will render as drag-and-drop zones
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
207
254
|
---
|
|
208
255
|
|
|
209
256
|
## Running the Example Application
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/integrations/sqlalchemy.py
RENAMED
|
File without changes
|
{fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/dashboard.html
RENAMED
|
File without changes
|
{fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/layout.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_lite_admin-0.1.6 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|