fastapi-admin-kit 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_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{# partials/pagination.html — reusable pagination component #}
|
|
2
|
+
{% macro render_pagination(page, total_pages, total_items, per_page, next_cursor=None, has_next=false, pagination_mode="offset") %}
|
|
3
|
+
{% if pagination_mode and "cursor" in pagination_mode %}
|
|
4
|
+
{# Cursor-based pagination #}
|
|
5
|
+
<div class="pagination">
|
|
6
|
+
<div class="pagination-info">
|
|
7
|
+
{% if has_next or total_items > per_page %}
|
|
8
|
+
{% if page is defined and page > 1 %}
|
|
9
|
+
Showing cursor-based results (page {{ page }})
|
|
10
|
+
{% else %}
|
|
11
|
+
Showing cursor-based results
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% else %}
|
|
14
|
+
Showing all {{ total_items | int }} results
|
|
15
|
+
{% endif %}
|
|
16
|
+
</div>
|
|
17
|
+
<div class="pagination-links">
|
|
18
|
+
{% if has_next %}
|
|
19
|
+
<a href="?after={{ next_cursor }}" class="pagination-link" title="Next page">
|
|
20
|
+
{{ icon("chevron-right") }}
|
|
21
|
+
</a>
|
|
22
|
+
{% else %}
|
|
23
|
+
<span class="pagination-link pagination-link--disabled">
|
|
24
|
+
{{ icon("chevron-right") }}
|
|
25
|
+
</span>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
{% else %}
|
|
30
|
+
{# Offset-based pagination (default) #}
|
|
31
|
+
<div class="pagination">
|
|
32
|
+
<div class="pagination-info">
|
|
33
|
+
Showing {{ ((page - 1) * per_page + 1) | int }} to {{ (page * per_page) if page < total_pages else total_items | int }} of {{ total_items | int }} results
|
|
34
|
+
</div>
|
|
35
|
+
<div class="pagination-links">
|
|
36
|
+
{% if page > 1 %}
|
|
37
|
+
<a href="?page={{ page - 1 }}" class="pagination-link" title="Previous page">
|
|
38
|
+
{{ icon("chevron-left") }}
|
|
39
|
+
</a>
|
|
40
|
+
{% else %}
|
|
41
|
+
<span class="pagination-link pagination-link--disabled">
|
|
42
|
+
{{ icon("chevron-left") }}
|
|
43
|
+
</span>
|
|
44
|
+
{% endif %}
|
|
45
|
+
|
|
46
|
+
{% set start_page = [1, page - 2] | max %}
|
|
47
|
+
{% set end_page = [total_pages, page + 2] | min %}
|
|
48
|
+
|
|
49
|
+
{% if start_page > 1 %}
|
|
50
|
+
<a href="?page=1" class="pagination-link">1</a>
|
|
51
|
+
{% if start_page > 2 %}
|
|
52
|
+
<span class="pagination-ellipsis">...</span>
|
|
53
|
+
{% endif %}
|
|
54
|
+
{% endif %}
|
|
55
|
+
|
|
56
|
+
{% for p in range(start_page, end_page + 1) %}
|
|
57
|
+
<a href="?page={{ p }}"
|
|
58
|
+
class="pagination-link {% if p == page %}pagination-link--active{% endif %}">
|
|
59
|
+
{{ p }}
|
|
60
|
+
</a>
|
|
61
|
+
{% endfor %}
|
|
62
|
+
|
|
63
|
+
{% if end_page < total_pages %}
|
|
64
|
+
{% if end_page < total_pages - 1 %}
|
|
65
|
+
<span class="pagination-ellipsis">...</span>
|
|
66
|
+
{% endif %}
|
|
67
|
+
<a href="?page={{ total_pages }}" class="pagination-link">{{ total_pages }}</a>
|
|
68
|
+
{% endif %}
|
|
69
|
+
|
|
70
|
+
{% if page < total_pages %}
|
|
71
|
+
<a href="?page={{ page + 1 }}" class="pagination-link" title="Next page">
|
|
72
|
+
{{ icon("chevron-right") }}
|
|
73
|
+
</a>
|
|
74
|
+
{% else %}
|
|
75
|
+
<span class="pagination-link pagination-link--disabled">
|
|
76
|
+
{{ icon("chevron-right") }}
|
|
77
|
+
</span>
|
|
78
|
+
{% endif %}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
{% endif %}
|
|
82
|
+
{% endmacro %}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{# partials/permission_widget.html
|
|
2
|
+
Args: search_url, perm_data (JSON-serializable dict)
|
|
3
|
+
#}
|
|
4
|
+
<div x-data="permissionWidget('{{ search_url }}', {{ perm_data | tojson | forceescape }})">
|
|
5
|
+
<input type="hidden" name="perm_data" :value="serializedPermData">
|
|
6
|
+
|
|
7
|
+
<div class="space-y-3">
|
|
8
|
+
<template x-for="(table, index) in selectedTables" :key="table.id">
|
|
9
|
+
<div class="border border-[var(--color-gray-200)] rounded-lg overflow-hidden">
|
|
10
|
+
<div class="flex items-center justify-between px-4 py-3 bg-[var(--color-gray-50)] cursor-pointer hover:bg-[var(--color-gray-100)]"
|
|
11
|
+
@click="toggleExpand(table.id)">
|
|
12
|
+
<div class="flex items-center gap-3">
|
|
13
|
+
<span class="text-sm font-medium text-[var(--color-gray-900)]" x-text="table.label"></span>
|
|
14
|
+
<span class="text-xs text-[var(--color-gray-500)]" x-text="table.id"></span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex items-center gap-2">
|
|
17
|
+
<span class="text-xs px-2 py-0.5 rounded-full"
|
|
18
|
+
:class="permData[table.id]?.view || permData[table.id]?.create || permData[table.id]?.edit || permData[table.id]?.delete
|
|
19
|
+
? 'bg-green-100 text-green-700'
|
|
20
|
+
: 'bg-[var(--color-gray-100)] text-[var(--color-gray-500)]'"
|
|
21
|
+
x-text="(permData[table.id]?.view ? 'V ' : '') + (permData[table.id]?.create ? 'C ' : '') + (permData[table.id]?.edit ? 'E ' : '') + (permData[table.id]?.delete ? 'D ' : '') || 'None'">
|
|
22
|
+
</span>
|
|
23
|
+
<button type="button" @click.stop="toggleExpand(table.id)"
|
|
24
|
+
class="text-[var(--color-gray-400)] hover:text-[var(--color-gray-600)]">
|
|
25
|
+
<svg class="w-4 h-4 transition-transform" :class="expandedTable === table.id ? 'rotate-180' : ''"
|
|
26
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" @click.stop="removeTable(index)"
|
|
31
|
+
class="text-[var(--color-gray-400)] hover:text-red-500 ml-1">
|
|
32
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
33
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div x-show="expandedTable === table.id" x-collapse class="px-4 py-4 bg-white">
|
|
40
|
+
<div class="flex items-center justify-between mb-3">
|
|
41
|
+
<span class="text-xs font-medium text-[var(--color-gray-500)] uppercase tracking-wide">Actions</span>
|
|
42
|
+
<div class="flex gap-2">
|
|
43
|
+
<button type="button" @click="toggleAllActions(table.id, true)"
|
|
44
|
+
class="text-xs text-[var(--color-primary-600)] hover:text-[var(--color-primary-500)]">All</button>
|
|
45
|
+
<span class="text-[var(--color-gray-300)]">|</span>
|
|
46
|
+
<button type="button" @click="toggleAllActions(table.id, false)"
|
|
47
|
+
class="text-xs text-[var(--color-gray-500)] hover:text-[var(--color-gray-700)]">None</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
51
|
+
<template x-for="action in ['view', 'create', 'edit', 'delete']" :key="action">
|
|
52
|
+
<label class="flex items-center gap-2 p-2 rounded border border-[var(--color-gray-200)] cursor-pointer hover:bg-[var(--color-gray-50)]">
|
|
53
|
+
<input type="checkbox"
|
|
54
|
+
:checked="permData[table.id]?.[action]"
|
|
55
|
+
@change="permData[table.id][action] = $event.target.checked"
|
|
56
|
+
class="rounded border-[var(--color-gray-300)] text-[var(--color-primary-600)] focus:ring-[var(--color-primary-500)]">
|
|
57
|
+
<span class="text-sm capitalize" x-text="action"></span>
|
|
58
|
+
</label>
|
|
59
|
+
</template>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="mt-4" style="position: relative;">
|
|
67
|
+
<div class="flex items-center gap-2">
|
|
68
|
+
<div class="flex-1 relative">
|
|
69
|
+
<input type="text" x-model="searchQuery" @input="search()" @focus="open = true" @click.outside="open = false"
|
|
70
|
+
placeholder="Search tables to add..."
|
|
71
|
+
class="form-input w-full">
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div x-show="open && results.length > 0" x-cloak
|
|
75
|
+
style="position: absolute; z-index: 50; width: 100%; margin-top: 4px; background: var(--surface-base); border: 1px solid var(--surface-border); border-radius: var(--radius-md); box-shadow: var(--shadow-md); max-height: 200px; overflow-y: auto;">
|
|
76
|
+
<template x-for="result in results" :key="result.id">
|
|
77
|
+
<div @click="addTable(result)"
|
|
78
|
+
style="padding: 8px 12px; cursor: pointer; font-size: 0.875rem;"
|
|
79
|
+
onmouseover="this.style.background='var(--surface-hover)'" onmouseout="this.style.background=''">
|
|
80
|
+
<span x-text="result.label"></span>
|
|
81
|
+
<span class="text-xs text-[var(--color-gray-400)] ml-2" x-text="result.id"></span>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script src="/static/js/admin.js?v={{ static_version }}"></script>
|
|
2
|
+
<script src="/static/js/htmx-config.js"></script>
|
|
3
|
+
<script>
|
|
4
|
+
// Mobile menu button visibility
|
|
5
|
+
(function() {
|
|
6
|
+
var mql = window.matchMedia('(max-width: 1024px)');
|
|
7
|
+
var btn = document.getElementById('mobile-menu-btn');
|
|
8
|
+
function check(e) { btn.style.display = e.matches ? 'inline-flex' : 'none'; }
|
|
9
|
+
mql.addEventListener('change', check);
|
|
10
|
+
check(mql);
|
|
11
|
+
})();
|
|
12
|
+
</script>
|
|
13
|
+
{% block scripts %}{% endblock %}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{# partials/sidebar.html — navigation sidebar with collapsible groups #}
|
|
2
|
+
{% set _ui = ui_config | default({}) %}
|
|
3
|
+
<aside class="admin-sidebar sidebar--{{ _ui.sidebar_style | default('default') }}{% if _ui.sidebar_position | default('left') == 'right' %} sidebar--right{% endif %}"
|
|
4
|
+
:class="{ 'open': sidebarOpen, 'collapsed': sidebarCollapsed && window.innerWidth >= 1024 }"
|
|
5
|
+
:style="sidebarCollapsed && window.innerWidth >= 1024 ? 'width: var(--sidebar-collapsed-width)' : 'width: var(--sidebar-width)'"
|
|
6
|
+
x-show="true">
|
|
7
|
+
|
|
8
|
+
<nav class="sidebar-nav">
|
|
9
|
+
<ul class="sidebar-nav-list">
|
|
10
|
+
{# Dashboard link #}
|
|
11
|
+
{% if dashboard_visible | default(true) %}
|
|
12
|
+
<li class="sidebar-nav-item">
|
|
13
|
+
<a href="{{ admin_path | default('/admin') }}/"
|
|
14
|
+
class="nav-link {% if request.url.path == admin_path | default('/admin') ~ '/' %}active{% endif %}">
|
|
15
|
+
{{ icon("home", size="18px") }}
|
|
16
|
+
<span class="nav-link-label">Dashboard</span>
|
|
17
|
+
<span class="nav-tooltip" x-show="sidebarCollapsed">Dashboard</span>
|
|
18
|
+
</a>
|
|
19
|
+
</li>
|
|
20
|
+
{% endif %}
|
|
21
|
+
|
|
22
|
+
{# Nav Groups — collapsible #}
|
|
23
|
+
|
|
24
|
+
{% for group in nav_groups %}
|
|
25
|
+
<li class="sidebar-section" x-data="{ open: {{ 'true' if not group.collapsed_by_default else 'false' }} }">
|
|
26
|
+
<button type="button" @click="open = !open"
|
|
27
|
+
class="sidebar-section-toggle">
|
|
28
|
+
<span class="sidebar-section-label">{{ group.label }}</span>
|
|
29
|
+
{{ icon("chevron-right", size="14px") }}
|
|
30
|
+
</button>
|
|
31
|
+
<ul class="sidebar-section-items" x-show="open" x-collapse x-cloak>
|
|
32
|
+
{% for item in group.items %}
|
|
33
|
+
<li class="sidebar-nav-item">
|
|
34
|
+
<a href="{{ item.url }}"
|
|
35
|
+
class="nav-link {% if not item.icon %}nav-link--no-icon{% endif %} {% if request.url.path.startswith(item.url) %}active{% endif %}">
|
|
36
|
+
{% if item.icon %}
|
|
37
|
+
{{ icon(item.icon, size="18px") }}
|
|
38
|
+
{% endif %}
|
|
39
|
+
<span class="nav-link-label">{{ item.label }}</span>
|
|
40
|
+
{% if item.badge_fn %}
|
|
41
|
+
{% set badge = item.badge_fn() %}
|
|
42
|
+
{% if badge %}
|
|
43
|
+
<span class="nav-link-badge">{{ badge }}</span>
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% endif %}
|
|
46
|
+
<span class="nav-tooltip" x-show="sidebarCollapsed">{{ item.label }}</span>
|
|
47
|
+
</a>
|
|
48
|
+
{% if item.children %}
|
|
49
|
+
<ul class="sidebar-sub-items">
|
|
50
|
+
{% for child in item.children %}
|
|
51
|
+
<li class="sidebar-nav-item sidebar-nav-item--child">
|
|
52
|
+
<a href="{{ child.url }}"
|
|
53
|
+
class="nav-link nav-link--child {% if request.url.path.startswith(child.url) %}active{% endif %}">
|
|
54
|
+
{% if child.icon %}
|
|
55
|
+
{{ icon(child.icon, size="16px") }}
|
|
56
|
+
{% endif %}
|
|
57
|
+
<span class="nav-link-label">{{ child.label }}</span>
|
|
58
|
+
</a>
|
|
59
|
+
</li>
|
|
60
|
+
{% endfor %}
|
|
61
|
+
</ul>
|
|
62
|
+
{% endif %}
|
|
63
|
+
</li>
|
|
64
|
+
{% endfor %}
|
|
65
|
+
</ul>
|
|
66
|
+
</li>
|
|
67
|
+
{% endfor %}
|
|
68
|
+
</ul>
|
|
69
|
+
</nav>
|
|
70
|
+
|
|
71
|
+
{# Settings at bottom #}
|
|
72
|
+
{% if settings_visible | default(true) %}
|
|
73
|
+
<div class="sidebar-bottom">
|
|
74
|
+
<ul class="sidebar-nav-list">
|
|
75
|
+
<li class="sidebar-nav-item">
|
|
76
|
+
<a href="{{ admin_path | default('/admin') }}/settings/"
|
|
77
|
+
class="nav-link {% if request.url.path.startswith(admin_path | default('/admin') ~ '/settings') %}active{% endif %}">
|
|
78
|
+
{{ icon("cog-6-tooth", size="18px") }}
|
|
79
|
+
<span class="nav-link-label">Settings</span>
|
|
80
|
+
<span class="nav-tooltip" x-show="sidebarCollapsed">Settings</span>
|
|
81
|
+
</a>
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
{% endif %}
|
|
86
|
+
</aside>
|
|
87
|
+
|
|
88
|
+
{# Mobile sidebar overlay #}
|
|
89
|
+
<div class="admin-sidebar-overlay"
|
|
90
|
+
:class="{ 'open': sidebarOpen }"
|
|
91
|
+
@click="sidebarOpen = false"
|
|
92
|
+
x-show="window.innerWidth < 1024 && sidebarOpen"
|
|
93
|
+
style="display: none;">
|
|
94
|
+
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{# partials/topbar.html — top navigation bar matching reference #}
|
|
2
|
+
{% set _ui = ui_config | default({}) %}
|
|
3
|
+
<header class="admin-topbar topbar--{{ _ui.topbar_style | default('default') }}">
|
|
4
|
+
{# Left zone: collapse toggle, logo #}
|
|
5
|
+
<div class="topbar-left">
|
|
6
|
+
<button class="collapse-toggle"
|
|
7
|
+
@click="if (window.innerWidth < 1024) { toggleMobileSidebar(); } else { sidebarCollapsed = !sidebarCollapsed; }"
|
|
8
|
+
title="Toggle sidebar">
|
|
9
|
+
{{ icon("bars-3", size="18px") }}
|
|
10
|
+
</button>
|
|
11
|
+
|
|
12
|
+
<a href="{{ admin_path | default('/admin') }}/" class="topbar-logo">
|
|
13
|
+
{% if admin_config.logo_url %}
|
|
14
|
+
<img src="{{ admin_config.logo_url }}" alt="Logo" style="height: 28px; max-width: 120px;">
|
|
15
|
+
{% else %}
|
|
16
|
+
<span class="topbar-logo-icon">
|
|
17
|
+
{{ icon("shield-check", size="24px") }}
|
|
18
|
+
</span>
|
|
19
|
+
<span>{{ admin_config.title | default("Admin") }}</span>
|
|
20
|
+
{% endif %}
|
|
21
|
+
</a>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{# Center zone: search bar #}
|
|
25
|
+
<div class="topbar-center">
|
|
26
|
+
<div class="topbar-search" @click="$dispatch('open-command-palette')">
|
|
27
|
+
{{ icon("magnifying-glass", size="16px") }}
|
|
28
|
+
<span class="topbar-search-placeholder">Search anything...</span>
|
|
29
|
+
<span class="topbar-search-shortcut">
|
|
30
|
+
<kbd>⌘</kbd><kbd>K</kbd>
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{# Right zone: environment, notifications, dark toggle, user #}
|
|
36
|
+
<div class="topbar-right">
|
|
37
|
+
{% if _ui.environment_label | default('') %}
|
|
38
|
+
<span class="badge badge-{{ _ui.environment_color | default('info') }}">
|
|
39
|
+
{{ _ui.environment_label }}
|
|
40
|
+
</span>
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
{# Dark mode toggle #}
|
|
44
|
+
<button class="icon-btn"
|
|
45
|
+
@click="$store.theme.toggle()"
|
|
46
|
+
title="Toggle dark mode">
|
|
47
|
+
<span x-show="!$store.theme.dark">{{ icon("moon", size="18px") }}</span>
|
|
48
|
+
<span x-show="$store.theme.dark" style="display: none;">{{ icon("sun", size="18px") }}</span>
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
{# Notifications #}
|
|
52
|
+
<button class="icon-btn" title="Notifications">
|
|
53
|
+
{{ icon("bell", size="18px") }}
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
{# User avatar dropdown #}
|
|
57
|
+
<div class="topbar-user" x-data="{ open: false }">
|
|
58
|
+
<button @click="open = !open" @click.outside="open = false"
|
|
59
|
+
class="topbar-user-btn">
|
|
60
|
+
<div class="user-avatar">
|
|
61
|
+
{{ current_user.full_name[0] | upper if current_user and current_user.full_name else "A" }}
|
|
62
|
+
</div>
|
|
63
|
+
<div class="topbar-user-info">
|
|
64
|
+
<span class="topbar-user-name">{{ current_user.full_name | default("Admin") }}</span>
|
|
65
|
+
<span class="topbar-user-role">{{ "Superuser" if current_user and current_user.is_superuser else "Admin" }}</span>
|
|
66
|
+
</div>
|
|
67
|
+
{{ icon("chevron-down", size="14px") }}
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
<div x-show="open"
|
|
71
|
+
x-transition:enter="transition ease-out duration-100"
|
|
72
|
+
x-transition:enter-start="opacity-0 scale-95"
|
|
73
|
+
x-transition:enter-end="opacity-100 scale-100"
|
|
74
|
+
x-transition:leave="transition ease-in duration-75"
|
|
75
|
+
x-transition:leave-start="opacity-100 scale-100"
|
|
76
|
+
x-transition:leave-end="opacity-0 scale-95"
|
|
77
|
+
class="user-dropdown"
|
|
78
|
+
style="display: none;">
|
|
79
|
+
<div class="user-dropdown-header">
|
|
80
|
+
<div class="user-dropdown-name">{{ current_user.full_name | default("Admin") }}</div>
|
|
81
|
+
<div class="user-dropdown-email">{{ current_user.email | default("") }}</div>
|
|
82
|
+
</div>
|
|
83
|
+
<a href="{{ admin_path | default('/admin') }}/profile/" class="user-dropdown-item">
|
|
84
|
+
Profile
|
|
85
|
+
</a>
|
|
86
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/logout/">
|
|
87
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
88
|
+
<button type="submit" class="user-dropdown-item danger">
|
|
89
|
+
Sign out
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</header>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Core data structures for FastAPI Admin Kit — ColumnMeta, RelationMeta, FieldMeta, etc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ColumnMeta:
|
|
11
|
+
"""Metadata for a single SQLAlchemy column."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
type: Any # SQLAlchemy type instance
|
|
15
|
+
nullable: bool = True
|
|
16
|
+
primary_key: bool = False
|
|
17
|
+
foreign_keys: list = field(default_factory=list)
|
|
18
|
+
default: Any = None
|
|
19
|
+
server_default: Any = None
|
|
20
|
+
index: bool = False
|
|
21
|
+
unique: bool = False
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_foreign_key(self) -> bool:
|
|
25
|
+
return bool(self.foreign_keys)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RelationMeta:
|
|
30
|
+
"""Metadata for a single SQLAlchemy relationship."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
direction: str # MANYTOONE, ONETOMANY, MANYTOMANY
|
|
34
|
+
target_model: type
|
|
35
|
+
uselist: bool = True
|
|
36
|
+
back_populates: str | None = None
|
|
37
|
+
secondary: Any = None # association table for many-to-many
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class FieldMeta:
|
|
42
|
+
"""Metadata for a form field — drives widget rendering and validation."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
label: str
|
|
46
|
+
required: bool
|
|
47
|
+
readonly: bool = False
|
|
48
|
+
help_text: str | None = None
|
|
49
|
+
placeholder: str | None = None
|
|
50
|
+
extra: dict = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class PermissionSet:
|
|
55
|
+
"""Set of boolean permissions for a single model."""
|
|
56
|
+
|
|
57
|
+
can_view: bool = False
|
|
58
|
+
can_create: bool = False
|
|
59
|
+
can_edit: bool = False
|
|
60
|
+
can_delete: bool = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class FieldsetSpec:
|
|
65
|
+
"""Defines a logical grouping of fields within a form."""
|
|
66
|
+
|
|
67
|
+
title: str | None = None
|
|
68
|
+
collapsed: bool = False
|
|
69
|
+
fields: list[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class SeedRole:
|
|
74
|
+
"""Defines a role to be seeded on first startup."""
|
|
75
|
+
|
|
76
|
+
name: str
|
|
77
|
+
description: str = ""
|
|
78
|
+
permissions: dict[str, dict[str, bool]] = field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class FieldRenderContext:
|
|
83
|
+
meta: FieldMeta
|
|
84
|
+
widget_macro: str
|
|
85
|
+
widget_context: dict = field(default_factory=dict)
|
|
86
|
+
errors: list[str] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class FieldsetContext:
|
|
91
|
+
title: str | None = None
|
|
92
|
+
collapsed: bool = False
|
|
93
|
+
fields: list[FieldRenderContext] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class FormContext:
|
|
98
|
+
model_name: str
|
|
99
|
+
verbose_name: str
|
|
100
|
+
is_create: bool
|
|
101
|
+
obj: Any = None
|
|
102
|
+
fieldsets: list[FieldsetContext] = field(default_factory=list)
|
|
103
|
+
errors: dict[str, list[str]] = field(default_factory=dict)
|
|
104
|
+
values: dict[str, Any] = field(default_factory=dict)
|
|
105
|
+
action_url: str = ""
|
|
106
|
+
list_url: str = ""
|
|
107
|
+
can_delete: bool = False
|
|
108
|
+
permissions: PermissionSet = field(default_factory=PermissionSet)
|
|
109
|
+
readonly: bool = False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class ExtraField:
|
|
114
|
+
name: str
|
|
115
|
+
label: str = ""
|
|
116
|
+
widget: Any = None
|
|
117
|
+
default: Any = None
|
|
118
|
+
required: bool = False
|
|
119
|
+
required_on_create: bool | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class TabConfig:
|
|
124
|
+
"""Configuration for changelist tabs."""
|
|
125
|
+
title: str
|
|
126
|
+
url: str = ""
|
|
127
|
+
permission: str | None = None
|
|
128
|
+
is_active: bool = False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class TableSection:
|
|
133
|
+
"""Expandable section showing a related table."""
|
|
134
|
+
title: str
|
|
135
|
+
related_model: Any = None
|
|
136
|
+
related_field: str = ""
|
|
137
|
+
list_display: list[str] = field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class TemplateSection:
|
|
142
|
+
"""Expandable section rendering a custom template."""
|
|
143
|
+
title: str
|
|
144
|
+
template: str = ""
|
|
145
|
+
context_fn: Any = None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""FormValidator — three-level validation runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FormValidator:
|
|
9
|
+
def run(
|
|
10
|
+
self,
|
|
11
|
+
registered: Any,
|
|
12
|
+
parsed: dict[str, Any],
|
|
13
|
+
obj: Any = None,
|
|
14
|
+
) -> dict[str, list[str]]:
|
|
15
|
+
errors: dict[str, list[str]] = {}
|
|
16
|
+
|
|
17
|
+
for field_meta in registered.form_fields:
|
|
18
|
+
if field_meta.readonly:
|
|
19
|
+
continue
|
|
20
|
+
widget = registered.get_widget(field_meta.name)
|
|
21
|
+
value = parsed.get(field_meta.name)
|
|
22
|
+
|
|
23
|
+
widget_errors = widget.validate(value, field_meta)
|
|
24
|
+
if widget_errors:
|
|
25
|
+
errors[field_meta.name] = widget_errors
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
validator_fn = getattr(
|
|
29
|
+
registered.admin, f"validate_{field_meta.name}", None
|
|
30
|
+
)
|
|
31
|
+
if validator_fn:
|
|
32
|
+
result = validator_fn(value, obj=obj)
|
|
33
|
+
if result:
|
|
34
|
+
errors[field_meta.name] = (
|
|
35
|
+
[result] if isinstance(result, str) else list(result)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not errors and hasattr(registered.admin, "validate"):
|
|
39
|
+
obj_errors = registered.admin.validate(parsed, obj=obj)
|
|
40
|
+
for fname, err in obj_errors.items():
|
|
41
|
+
errors[fname] = [err] if isinstance(err, str) else list(err)
|
|
42
|
+
|
|
43
|
+
return errors
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Views package — re-exports ModelAdmin and route factories."""
|
|
2
|
+
|
|
3
|
+
from fastapi_admin_kit.modeladmin import ModelAdmin
|
|
4
|
+
from fastapi_admin_kit.views.bulk import bulk_factory
|
|
5
|
+
from fastapi_admin_kit.views.class_views import (
|
|
6
|
+
BaseView,
|
|
7
|
+
BulkView,
|
|
8
|
+
CreateView,
|
|
9
|
+
DeleteView,
|
|
10
|
+
EditView,
|
|
11
|
+
ListView,
|
|
12
|
+
SearchView,
|
|
13
|
+
_resolve_view_class,
|
|
14
|
+
)
|
|
15
|
+
from fastapi_admin_kit.views.context import DisplayColumn, ViewContextBuilder
|
|
16
|
+
from fastapi_admin_kit.views.delete import delete_factory
|
|
17
|
+
from fastapi_admin_kit.views.extra import AdminExtra
|
|
18
|
+
from fastapi_admin_kit.views.factory import ViewFactory
|
|
19
|
+
from fastapi_admin_kit.views.form import (
|
|
20
|
+
create_form_factory,
|
|
21
|
+
create_submit_factory,
|
|
22
|
+
edit_form_factory,
|
|
23
|
+
edit_submit_factory,
|
|
24
|
+
)
|
|
25
|
+
from fastapi_admin_kit.views.list import list_view_factory
|
|
26
|
+
from fastapi_admin_kit.views.search import search_factory
|
|
27
|
+
from fastapi_admin_kit.views.sidebar import inject_sidebar_context
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_model_router(registered):
|
|
31
|
+
"""Create a model router - backward compatible wrapper around build_model_router."""
|
|
32
|
+
from fastapi_admin_kit.router import build_model_router
|
|
33
|
+
|
|
34
|
+
return build_model_router(registered)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ModelAdmin",
|
|
39
|
+
"ViewContextBuilder",
|
|
40
|
+
"ViewFactory",
|
|
41
|
+
"DisplayColumn",
|
|
42
|
+
"inject_sidebar_context",
|
|
43
|
+
"_resolve_view_class",
|
|
44
|
+
# Protocols (ISP: small focused interfaces)
|
|
45
|
+
"QueryProvider",
|
|
46
|
+
"FormParser",
|
|
47
|
+
"HTMLRenderer",
|
|
48
|
+
"APIRenderer",
|
|
49
|
+
# Concrete implementations (SRP: each does one thing)
|
|
50
|
+
"DefaultQueryProvider",
|
|
51
|
+
"HTMLFormParser",
|
|
52
|
+
"JSONBodyParser",
|
|
53
|
+
"ListHTMLRenderer",
|
|
54
|
+
"FormHTMLRenderer",
|
|
55
|
+
"ListAPIRenderer",
|
|
56
|
+
"ItemAPIRenderer",
|
|
57
|
+
"DeleteAPIRenderer",
|
|
58
|
+
# View classes (OCP: extend without modifying)
|
|
59
|
+
"BaseView",
|
|
60
|
+
"ListView",
|
|
61
|
+
"CreateView",
|
|
62
|
+
"EditView",
|
|
63
|
+
"DeleteView",
|
|
64
|
+
"BulkView",
|
|
65
|
+
"SearchView",
|
|
66
|
+
# Per-model assets
|
|
67
|
+
"AdminExtra",
|
|
68
|
+
# Backward-compatible factory functions
|
|
69
|
+
"list_view_factory",
|
|
70
|
+
"create_form_factory",
|
|
71
|
+
"create_submit_factory",
|
|
72
|
+
"edit_form_factory",
|
|
73
|
+
"edit_submit_factory",
|
|
74
|
+
"delete_factory",
|
|
75
|
+
"bulk_factory",
|
|
76
|
+
"search_factory",
|
|
77
|
+
"create_model_router",
|
|
78
|
+
]
|