fastapi-lite-admin 0.1.5__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.
Files changed (25) hide show
  1. {fastapi_lite_admin-0.1.5/fastapi_lite_admin.egg-info → fastapi_lite_admin-0.1.7}/PKG-INFO +51 -4
  2. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/README.md +50 -3
  3. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/main.py +19 -1
  4. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/routers/admin.py +32 -1
  5. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/model_detail.html +32 -2
  6. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/model_form.html +298 -1
  7. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7/fastapi_lite_admin.egg-info}/PKG-INFO +51 -4
  8. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/pyproject.toml +1 -1
  9. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/LICENSE +0 -0
  10. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/__init__.py +0 -0
  11. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/config.py +0 -0
  12. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/crud.py +0 -0
  13. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/registry.py +0 -0
  14. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/core/schema.py +0 -0
  15. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/dependencies/db.py +0 -0
  16. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/integrations/sqlalchemy.py +0 -0
  17. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/dashboard.html +0 -0
  18. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/layout.html +0 -0
  19. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/templates/model_list.html +0 -0
  20. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_admin_lite/ui/views.py +0 -0
  21. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/SOURCES.txt +0 -0
  22. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/dependency_links.txt +0 -0
  23. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/requires.txt +0 -0
  24. {fastapi_lite_admin-0.1.5 → fastapi_lite_admin-0.1.7}/fastapi_lite_admin.egg-info/top_level.txt +0 -0
  25. {fastapi_lite_admin-0.1.5 → 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.5
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
@@ -26,13 +26,13 @@ A premium, lightweight, pluggable admin panel for FastAPI and SQLAlchemy.
26
26
  ## Screenshots
27
27
 
28
28
  ### 🖥️ Dashboard / System Overview
29
- ![Dashboard](docs/images/dashboard.png)
29
+ ![Dashboard](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/dashboard.png)
30
30
 
31
31
  ### 📊 Model List View
32
- ![Model List](docs/images/model_list.png)
32
+ ![Model List](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/model_list.png)
33
33
 
34
34
  ### 📝 Edit/Create Record Form
35
- ![Model Form](docs/images/model_form.png)
35
+ ![Model Form](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/model_form.png)
36
36
 
37
37
  ## Features
38
38
 
@@ -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
@@ -5,13 +5,13 @@ A premium, lightweight, pluggable admin panel for FastAPI and SQLAlchemy.
5
5
  ## Screenshots
6
6
 
7
7
  ### 🖥️ Dashboard / System Overview
8
- ![Dashboard](docs/images/dashboard.png)
8
+ ![Dashboard](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/dashboard.png)
9
9
 
10
10
  ### 📊 Model List View
11
- ![Model List](docs/images/model_list.png)
11
+ ![Model List](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/model_list.png)
12
12
 
13
13
  ### 📝 Edit/Create Record Form
14
- ![Model Form](docs/images/model_form.png)
14
+ ![Model Form](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/model_form.png)
15
15
 
16
16
  ## Features
17
17
 
@@ -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
- const isFullWidth = typeof val === 'string' && val.length > 50;
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 (typeof val === 'boolean' || key === 'is_active') {
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.5
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
@@ -26,13 +26,13 @@ A premium, lightweight, pluggable admin panel for FastAPI and SQLAlchemy.
26
26
  ## Screenshots
27
27
 
28
28
  ### 🖥️ Dashboard / System Overview
29
- ![Dashboard](docs/images/dashboard.png)
29
+ ![Dashboard](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/dashboard.png)
30
30
 
31
31
  ### 📊 Model List View
32
- ![Model List](docs/images/model_list.png)
32
+ ![Model List](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/model_list.png)
33
33
 
34
34
  ### 📝 Edit/Create Record Form
35
- ![Model Form](docs/images/model_form.png)
35
+ ![Model Form](https://raw.githubusercontent.com/rishiqwerty/admin-panel-fast-api/add-docs-for-setting-up-admin-panel/docs/images/model_form.png)
36
36
 
37
37
  ## Features
38
38
 
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-lite-admin"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "A lightweight, pluggable admin panel for FastAPI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"