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.
Files changed (163) hide show
  1. fastapi_admin_kit/__init__.py +73 -0
  2. fastapi_admin_kit/actions/__init__.py +63 -0
  3. fastapi_admin_kit/actions/base.py +68 -0
  4. fastapi_admin_kit/actions/registry.py +43 -0
  5. fastapi_admin_kit/admin/__init__.py +17 -0
  6. fastapi_admin_kit/admin/admin_config.py +95 -0
  7. fastapi_admin_kit/admin/admin_database.py +138 -0
  8. fastapi_admin_kit/admin/admin_router.py +74 -0
  9. fastapi_admin_kit/admin/admin_template.py +203 -0
  10. fastapi_admin_kit/admin/builtin_models.py +284 -0
  11. fastapi_admin_kit/admin/core.py +1036 -0
  12. fastapi_admin_kit/admin/decorators.py +70 -0
  13. fastapi_admin_kit/admin/state.py +76 -0
  14. fastapi_admin_kit/admin.py +728 -0
  15. fastapi_admin_kit/api/__init__.py +44 -0
  16. fastapi_admin_kit/api/auth.py +342 -0
  17. fastapi_admin_kit/api/crud.py +128 -0
  18. fastapi_admin_kit/api/deps.py +79 -0
  19. fastapi_admin_kit/api/roles.py +128 -0
  20. fastapi_admin_kit/api/schema_generator.py +171 -0
  21. fastapi_admin_kit/api/schemas.py +81 -0
  22. fastapi_admin_kit/api/search.py +132 -0
  23. fastapi_admin_kit/audit/__init__.py +36 -0
  24. fastapi_admin_kit/audit/context.py +62 -0
  25. fastapi_admin_kit/audit/diff.py +77 -0
  26. fastapi_admin_kit/audit/event_bus.py +96 -0
  27. fastapi_admin_kit/audit/events.py +48 -0
  28. fastapi_admin_kit/audit/listener.py +159 -0
  29. fastapi_admin_kit/audit/logger.py +28 -0
  30. fastapi_admin_kit/audit/middleware.py +39 -0
  31. fastapi_admin_kit/audit/models.py +53 -0
  32. fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
  33. fastapi_admin_kit/auth/__init__.py +34 -0
  34. fastapi_admin_kit/auth/backend.py +95 -0
  35. fastapi_admin_kit/auth/csrf.py +240 -0
  36. fastapi_admin_kit/auth/dependencies.py +150 -0
  37. fastapi_admin_kit/auth/identity.py +181 -0
  38. fastapi_admin_kit/auth/models.py +246 -0
  39. fastapi_admin_kit/auth/password.py +35 -0
  40. fastapi_admin_kit/auth/permissions.py +205 -0
  41. fastapi_admin_kit/auth/protocol.py +22 -0
  42. fastapi_admin_kit/auth/ratelimit.py +88 -0
  43. fastapi_admin_kit/auth/router.py +10 -0
  44. fastapi_admin_kit/auth/session.py +79 -0
  45. fastapi_admin_kit/auth/totp.py +83 -0
  46. fastapi_admin_kit/auth/views.py +165 -0
  47. fastapi_admin_kit/cli.py +229 -0
  48. fastapi_admin_kit/config/__init__.py +19 -0
  49. fastapi_admin_kit/config/audit.py +18 -0
  50. fastapi_admin_kit/config/auth.py +54 -0
  51. fastapi_admin_kit/config/behavior.py +27 -0
  52. fastapi_admin_kit/config/nav.py +32 -0
  53. fastapi_admin_kit/config/storage.py +22 -0
  54. fastapi_admin_kit/config/theme.py +215 -0
  55. fastapi_admin_kit/config/ui.py +147 -0
  56. fastapi_admin_kit/dashboard/__init__.py +64 -0
  57. fastapi_admin_kit/db.py +133 -0
  58. fastapi_admin_kit/exceptions.py +5 -0
  59. fastapi_admin_kit/field_types.py +81 -0
  60. fastapi_admin_kit/filters/__init__.py +21 -0
  61. fastapi_admin_kit/filters/base.py +170 -0
  62. fastapi_admin_kit/filters/registry.py +68 -0
  63. fastapi_admin_kit/flash.py +45 -0
  64. fastapi_admin_kit/form/__init__.py +1 -0
  65. fastapi_admin_kit/form/pipeline.py +106 -0
  66. fastapi_admin_kit/inspection/__init__.py +117 -0
  67. fastapi_admin_kit/inspection/registry.py +253 -0
  68. fastapi_admin_kit/inspection.py +115 -0
  69. fastapi_admin_kit/modeladmin.py +375 -0
  70. fastapi_admin_kit/models/__init__.py +7 -0
  71. fastapi_admin_kit/models/base.py +7 -0
  72. fastapi_admin_kit/nav.py +208 -0
  73. fastapi_admin_kit/pagination/__init__.py +14 -0
  74. fastapi_admin_kit/pagination/base.py +40 -0
  75. fastapi_admin_kit/pagination/cursor.py +97 -0
  76. fastapi_admin_kit/pagination/dynamic.py +48 -0
  77. fastapi_admin_kit/pagination/offset.py +42 -0
  78. fastapi_admin_kit/plugins/__init__.py +1 -0
  79. fastapi_admin_kit/py.typed +0 -0
  80. fastapi_admin_kit/registry/__init__.py +5 -0
  81. fastapi_admin_kit/registry/core.py +287 -0
  82. fastapi_admin_kit/registry/validation.py +107 -0
  83. fastapi_admin_kit/registry.py +15 -0
  84. fastapi_admin_kit/router.py +335 -0
  85. fastapi_admin_kit/static/css/admin.css +4736 -0
  86. fastapi_admin_kit/static/css/presets.css +317 -0
  87. fastapi_admin_kit/static/css/tokens.css +217 -0
  88. fastapi_admin_kit/static/css/variables.css +74 -0
  89. fastapi_admin_kit/static/icons/heroicons.svg +160 -0
  90. fastapi_admin_kit/static/js/admin.js +692 -0
  91. fastapi_admin_kit/static/js/htmx-config.js +42 -0
  92. fastapi_admin_kit/storage/__init__.py +6 -0
  93. fastapi_admin_kit/storage/base.py +48 -0
  94. fastapi_admin_kit/storage/local.py +73 -0
  95. fastapi_admin_kit/templates/base.html +142 -0
  96. fastapi_admin_kit/templates/macros/form_fields.html +660 -0
  97. fastapi_admin_kit/templates/macros/icons.html +50 -0
  98. fastapi_admin_kit/templates/macros/table.html +108 -0
  99. fastapi_admin_kit/templates/macros/widgets.html +159 -0
  100. fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
  101. fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
  102. fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
  103. fastapi_admin_kit/templates/pages/audit_log.html +102 -0
  104. fastapi_admin_kit/templates/pages/dashboard.html +295 -0
  105. fastapi_admin_kit/templates/pages/detail.html +183 -0
  106. fastapi_admin_kit/templates/pages/form.html +119 -0
  107. fastapi_admin_kit/templates/pages/list.html +277 -0
  108. fastapi_admin_kit/templates/pages/login.html +85 -0
  109. fastapi_admin_kit/templates/pages/profile/password.html +78 -0
  110. fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
  111. fastapi_admin_kit/templates/pages/role_form.html +75 -0
  112. fastapi_admin_kit/templates/pages/roles/form.html +117 -0
  113. fastapi_admin_kit/templates/pages/roles/list.html +69 -0
  114. fastapi_admin_kit/templates/pages/roles.html +77 -0
  115. fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
  116. fastapi_admin_kit/templates/pages/users/form.html +229 -0
  117. fastapi_admin_kit/templates/pages/users/list.html +83 -0
  118. fastapi_admin_kit/templates/partials/command_palette.html +52 -0
  119. fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
  120. fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
  121. fastapi_admin_kit/templates/partials/head.html +21 -0
  122. fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
  123. fastapi_admin_kit/templates/partials/list_table.html +178 -0
  124. fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
  125. fastapi_admin_kit/templates/partials/pagination.html +82 -0
  126. fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
  127. fastapi_admin_kit/templates/partials/scripts.html +13 -0
  128. fastapi_admin_kit/templates/partials/sidebar.html +94 -0
  129. fastapi_admin_kit/templates/partials/topbar.html +95 -0
  130. fastapi_admin_kit/types.py +145 -0
  131. fastapi_admin_kit/validation.py +43 -0
  132. fastapi_admin_kit/views/__init__.py +78 -0
  133. fastapi_admin_kit/views/audit.py +134 -0
  134. fastapi_admin_kit/views/bulk.py +28 -0
  135. fastapi_admin_kit/views/class_views.py +1040 -0
  136. fastapi_admin_kit/views/context.py +588 -0
  137. fastapi_admin_kit/views/dashboard.py +162 -0
  138. fastapi_admin_kit/views/delete.py +31 -0
  139. fastapi_admin_kit/views/extra.py +65 -0
  140. fastapi_admin_kit/views/factory.py +667 -0
  141. fastapi_admin_kit/views/form.py +159 -0
  142. fastapi_admin_kit/views/list.py +28 -0
  143. fastapi_admin_kit/views/profile.py +219 -0
  144. fastapi_admin_kit/views/protocols.py +54 -0
  145. fastapi_admin_kit/views/renderers.py +634 -0
  146. fastapi_admin_kit/views/roles.py +230 -0
  147. fastapi_admin_kit/views/search.py +31 -0
  148. fastapi_admin_kit/views/settings.py +31 -0
  149. fastapi_admin_kit/views/sidebar.py +101 -0
  150. fastapi_admin_kit/views/totp.py +249 -0
  151. fastapi_admin_kit/views/users.py +347 -0
  152. fastapi_admin_kit/views.py +117 -0
  153. fastapi_admin_kit/widgets/__init__.py +44 -0
  154. fastapi_admin_kit/widgets/base.py +44 -0
  155. fastapi_admin_kit/widgets/inputs.py +363 -0
  156. fastapi_admin_kit/widgets/registry.py +110 -0
  157. fastapi_admin_kit/widgets/relation.py +70 -0
  158. fastapi_admin_kit/widgets/resolver.py +102 -0
  159. fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
  160. fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
  161. fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
  162. fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
  163. 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
+ ]