openadmin-dev 0.7.3__tar.gz → 0.7.5__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.
- openadmin_dev-0.7.5/Makefile +45 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/PKG-INFO +47 -21
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/README.md +46 -20
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/analytics.py +24 -6
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/authors.py +14 -4
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/genres.py +36 -5
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/health.py +18 -3
- openadmin_dev-0.7.5/examples/admin/weekdays.py +131 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/lib/database.py +1 -1
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/lib/models.py +10 -10
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/main.py +2 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/admin_page.py +57 -3
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/admin_panel.py +4 -5
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/bar_chart.py +7 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/component.py +11 -4
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/pie_chart.py +8 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/section.py +1 -1
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/table.py +3 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/__init__.py +33 -9
- openadmin_dev-0.7.5/openadmin/spec/action.py +23 -0
- openadmin_dev-0.7.5/openadmin/spec/bar_chart.py +52 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/colors.py +27 -0
- openadmin_dev-0.7.5/openadmin/spec/component.py +26 -0
- openadmin_dev-0.7.3/openadmin/spec/pie_chart.py → openadmin_dev-0.7.5/openadmin/spec/form.py +5 -2
- openadmin_dev-0.7.3/openadmin/spec/bar_chart.py → openadmin_dev-0.7.5/openadmin/spec/markdown.py +2 -2
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/page.py +1 -2
- openadmin_dev-0.7.5/openadmin/spec/pie_chart.py +53 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/property.py +1 -2
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/section.py +1 -2
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/spec.py +1 -2
- openadmin_dev-0.7.5/openadmin/spec/table.py +41 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/pyproject.toml +9 -2
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/uv.lock +259 -1
- openadmin_dev-0.7.3/Makefile +0 -45
- openadmin_dev-0.7.3/openadmin/spec/action.py +0 -23
- openadmin_dev-0.7.3/openadmin/spec/component.py +0 -27
- openadmin_dev-0.7.3/openadmin/spec/form.py +0 -23
- openadmin_dev-0.7.3/openadmin/spec/markdown.py +0 -20
- openadmin_dev-0.7.3/openadmin/spec/table.py +0 -22
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/ISSUE_TEMPLATE/chore.md +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/branch-name/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/dependency-cves/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/format/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/license/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/lint/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/security/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/spell-check/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/test/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/typecheck/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/actions/unused-code/action.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/workflows/ci.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.github/workflows/publish.yml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.gitignore +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/.python-version +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/LICENSE +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/LICENSES/AGPL-3.0-or-later.txt +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/REUSE.toml +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/docs/assets/logo.png +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/__init__.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/__init__.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/books.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/overview.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/publishers.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/reports.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/admin/tags.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/lib/__init__.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/lib/lifespan.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/examples/lib/seed.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/__init__.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/action.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/area_chart.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/counters.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/deps.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/form.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/line_chart.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/markdown.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/page_protocol.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/pagination_params.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/stat.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/fastapi/utils.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/area_chart.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/http_methods.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/icons.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/line_chart.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/property_type.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/openadmin/spec/stat.py +0 -0
- {openadmin_dev-0.7.3 → openadmin_dev-0.7.5}/tests/test_hello.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
fix/license:
|
|
2
|
+
@ uv run reuse download --all
|
|
3
|
+
@ uv run reuse annotate --license AGPL-3.0-or-later --copyright "OpenAdmin" --recursive --skip-unrecognised openadmin/
|
|
4
|
+
@ uv run reuse annotate --license AGPL-3.0-or-later --copyright "OpenAdmin" --recursive --skip-unrecognised tests/
|
|
5
|
+
@ uv run reuse annotate --license AGPL-3.0-or-later --copyright "OpenAdmin" --recursive --skip-unrecognised examples/
|
|
6
|
+
|
|
7
|
+
fix/format:
|
|
8
|
+
@ uv run ruff format .
|
|
9
|
+
|
|
10
|
+
fix/lint:
|
|
11
|
+
@ uv run ruff check --fix .
|
|
12
|
+
|
|
13
|
+
fix: fix/license fix/format fix/lint
|
|
14
|
+
|
|
15
|
+
check/format:
|
|
16
|
+
@ uv run ruff format --check .
|
|
17
|
+
|
|
18
|
+
check/lint:
|
|
19
|
+
@ uv run ruff check .
|
|
20
|
+
|
|
21
|
+
check/typing:
|
|
22
|
+
@ uv run pyright .
|
|
23
|
+
|
|
24
|
+
check/cves:
|
|
25
|
+
@ uv audit --preview-features audit
|
|
26
|
+
|
|
27
|
+
check/security:
|
|
28
|
+
@ uv run bandit -r openadmin -q
|
|
29
|
+
|
|
30
|
+
check/unused:
|
|
31
|
+
@ uv run vulture openadmin --min-confidence 80
|
|
32
|
+
|
|
33
|
+
check/spell:
|
|
34
|
+
@ uv run codespell .
|
|
35
|
+
|
|
36
|
+
check/license:
|
|
37
|
+
@ uv run reuse lint
|
|
38
|
+
|
|
39
|
+
check/test:
|
|
40
|
+
@ uv run pytest
|
|
41
|
+
|
|
42
|
+
check: check/format check/lint check/typing check/cves check/security check/unused check/spell check/license check/test
|
|
43
|
+
|
|
44
|
+
dev/run:
|
|
45
|
+
@ PYTHONPATH=. uv run fastapi dev examples/main.py --host 0.0.0.0 --port $${PORT:-8000} --reload
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openadmin-dev
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Project-URL: Homepage, https://github.com/openadmin-team/openadmin-py
|
|
6
6
|
Project-URL: Issues, https://github.com/openadmin-team/openadmin-py/issues
|
|
@@ -60,16 +60,21 @@ admin = AdminPanel()
|
|
|
60
60
|
# Define a page
|
|
61
61
|
page = AdminPage("Dashboard")
|
|
62
62
|
|
|
63
|
+
|
|
63
64
|
@page.stat("Total Users")
|
|
64
65
|
async def total_users() -> Stat:
|
|
65
66
|
return Stat(value=1_024)
|
|
66
67
|
|
|
68
|
+
|
|
67
69
|
@page.table("Recent Users")
|
|
68
70
|
async def recent_users() -> Table:
|
|
69
|
-
return Table(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
return Table(
|
|
72
|
+
data=[
|
|
73
|
+
{"id": 1, "name": "Alice", "role": "admin"},
|
|
74
|
+
{"id": 2, "name": "Bob", "role": "viewer"},
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
73
78
|
|
|
74
79
|
# Register the page and mount the panel
|
|
75
80
|
admin.include_page(page)
|
|
@@ -87,6 +92,7 @@ Display a single numeric or boolean value.
|
|
|
87
92
|
```python
|
|
88
93
|
from openadmin.types import Stat
|
|
89
94
|
|
|
95
|
+
|
|
90
96
|
@page.stat("Active Sessions")
|
|
91
97
|
async def active_sessions() -> Stat:
|
|
92
98
|
return Stat(value=42)
|
|
@@ -100,6 +106,7 @@ Paginated tables with optional search. Use the built-in dependency types to rece
|
|
|
100
106
|
from openadmin.fastapi import PaginationParamsDep, SearchQueryDep
|
|
101
107
|
from openadmin.types import Table
|
|
102
108
|
|
|
109
|
+
|
|
103
110
|
@page.table("Users")
|
|
104
111
|
async def users_table(
|
|
105
112
|
pagination: PaginationParamsDep,
|
|
@@ -117,18 +124,23 @@ Attach per-row action buttons by including `__actions__` in each row:
|
|
|
117
124
|
```python
|
|
118
125
|
from openadmin.types import Action, Table, TableRow
|
|
119
126
|
|
|
127
|
+
|
|
120
128
|
@page.table("Users")
|
|
121
129
|
async def users_table() -> Table:
|
|
122
|
-
return Table(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
return Table(
|
|
131
|
+
data=[
|
|
132
|
+
TableRow(
|
|
133
|
+
id=1,
|
|
134
|
+
name="Alice",
|
|
135
|
+
__actions__=[
|
|
136
|
+
Action(color="danger", method="DELETE", url="/users/1", body=None),
|
|
137
|
+
Action(
|
|
138
|
+
color="info", method="POST", url="/users/1/reset", body=None
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
)
|
|
132
144
|
```
|
|
133
145
|
|
|
134
146
|
### Charts
|
|
@@ -138,6 +150,7 @@ All chart types share the same structure: a `data` list of dicts and a `config`
|
|
|
138
150
|
```python
|
|
139
151
|
from openadmin.types import BarChart, PieChart
|
|
140
152
|
|
|
153
|
+
|
|
141
154
|
@page.bar_chart("Sales by Region", "Total sales per region this quarter")
|
|
142
155
|
async def sales_chart() -> BarChart:
|
|
143
156
|
return BarChart(
|
|
@@ -148,11 +161,12 @@ async def sales_chart() -> BarChart:
|
|
|
148
161
|
config={"sales": {"label": "Sales", "color": "#6366f1"}},
|
|
149
162
|
)
|
|
150
163
|
|
|
164
|
+
|
|
151
165
|
@page.pie_chart("User Roles", "Breakdown of user roles")
|
|
152
166
|
async def roles_chart() -> PieChart:
|
|
153
167
|
return PieChart(
|
|
154
168
|
data=[
|
|
155
|
-
{"segment": "Admin",
|
|
169
|
+
{"segment": "Admin", "count": 5},
|
|
156
170
|
{"segment": "Editor", "count": 20},
|
|
157
171
|
{"segment": "Viewer", "count": 75},
|
|
158
172
|
],
|
|
@@ -178,10 +192,12 @@ Forms collect user input and submit it to your endpoint. Mark a form hidden if i
|
|
|
178
192
|
```python
|
|
179
193
|
from pydantic import BaseModel
|
|
180
194
|
|
|
195
|
+
|
|
181
196
|
class InvitePayload(BaseModel):
|
|
182
197
|
email: str
|
|
183
198
|
role: str
|
|
184
199
|
|
|
200
|
+
|
|
185
201
|
@page.form_post("Invite User", "Send an invitation email to a new user")
|
|
186
202
|
async def invite_user(payload: InvitePayload):
|
|
187
203
|
# send invite...
|
|
@@ -209,27 +225,37 @@ from openadmin.types import Stat, Table, BarChart
|
|
|
209
225
|
|
|
210
226
|
page = AdminPage("Library")
|
|
211
227
|
|
|
228
|
+
|
|
212
229
|
@page.stat("Total Books")
|
|
213
230
|
async def total_books(session: AsyncSessionDep) -> Stat:
|
|
214
231
|
result = await session.execute(select(func.count()).select_from(Book))
|
|
215
232
|
return Stat(value=result.scalar_one())
|
|
216
233
|
|
|
234
|
+
|
|
217
235
|
@page.table("Books")
|
|
218
236
|
async def books_table(
|
|
219
237
|
session: AsyncSessionDep,
|
|
220
238
|
pagination: PaginationParamsDep,
|
|
221
239
|
search: SearchQueryDep,
|
|
222
240
|
) -> Table:
|
|
223
|
-
stmt =
|
|
241
|
+
stmt = (
|
|
242
|
+
select(Book)
|
|
243
|
+
.offset(pagination.page * pagination.per_page)
|
|
244
|
+
.limit(pagination.per_page)
|
|
245
|
+
)
|
|
224
246
|
books = (await session.execute(stmt)).scalars().all()
|
|
225
247
|
return Table(data=[{"title": b.title, "year": b.published_year} for b in books])
|
|
226
248
|
|
|
249
|
+
|
|
227
250
|
@page.bar_chart("Books per Genre", "Number of books in each genre")
|
|
228
251
|
async def books_per_genre(session: AsyncSessionDep) -> BarChart:
|
|
229
|
-
rows = (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
rows = (
|
|
253
|
+
await session.execute(
|
|
254
|
+
select(Genre.name, func.count().label("books"))
|
|
255
|
+
.join(BookToGenre)
|
|
256
|
+
.group_by(Genre.id)
|
|
257
|
+
)
|
|
258
|
+
).all()
|
|
233
259
|
return BarChart(
|
|
234
260
|
data=[{"genre": name, "books": count} for name, count in rows],
|
|
235
261
|
config={"books": {"label": "Books", "color": "#6366f1"}},
|
|
@@ -45,16 +45,21 @@ admin = AdminPanel()
|
|
|
45
45
|
# Define a page
|
|
46
46
|
page = AdminPage("Dashboard")
|
|
47
47
|
|
|
48
|
+
|
|
48
49
|
@page.stat("Total Users")
|
|
49
50
|
async def total_users() -> Stat:
|
|
50
51
|
return Stat(value=1_024)
|
|
51
52
|
|
|
53
|
+
|
|
52
54
|
@page.table("Recent Users")
|
|
53
55
|
async def recent_users() -> Table:
|
|
54
|
-
return Table(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
return Table(
|
|
57
|
+
data=[
|
|
58
|
+
{"id": 1, "name": "Alice", "role": "admin"},
|
|
59
|
+
{"id": 2, "name": "Bob", "role": "viewer"},
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
|
|
58
63
|
|
|
59
64
|
# Register the page and mount the panel
|
|
60
65
|
admin.include_page(page)
|
|
@@ -72,6 +77,7 @@ Display a single numeric or boolean value.
|
|
|
72
77
|
```python
|
|
73
78
|
from openadmin.types import Stat
|
|
74
79
|
|
|
80
|
+
|
|
75
81
|
@page.stat("Active Sessions")
|
|
76
82
|
async def active_sessions() -> Stat:
|
|
77
83
|
return Stat(value=42)
|
|
@@ -85,6 +91,7 @@ Paginated tables with optional search. Use the built-in dependency types to rece
|
|
|
85
91
|
from openadmin.fastapi import PaginationParamsDep, SearchQueryDep
|
|
86
92
|
from openadmin.types import Table
|
|
87
93
|
|
|
94
|
+
|
|
88
95
|
@page.table("Users")
|
|
89
96
|
async def users_table(
|
|
90
97
|
pagination: PaginationParamsDep,
|
|
@@ -102,18 +109,23 @@ Attach per-row action buttons by including `__actions__` in each row:
|
|
|
102
109
|
```python
|
|
103
110
|
from openadmin.types import Action, Table, TableRow
|
|
104
111
|
|
|
112
|
+
|
|
105
113
|
@page.table("Users")
|
|
106
114
|
async def users_table() -> Table:
|
|
107
|
-
return Table(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
return Table(
|
|
116
|
+
data=[
|
|
117
|
+
TableRow(
|
|
118
|
+
id=1,
|
|
119
|
+
name="Alice",
|
|
120
|
+
__actions__=[
|
|
121
|
+
Action(color="danger", method="DELETE", url="/users/1", body=None),
|
|
122
|
+
Action(
|
|
123
|
+
color="info", method="POST", url="/users/1/reset", body=None
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
]
|
|
128
|
+
)
|
|
117
129
|
```
|
|
118
130
|
|
|
119
131
|
### Charts
|
|
@@ -123,6 +135,7 @@ All chart types share the same structure: a `data` list of dicts and a `config`
|
|
|
123
135
|
```python
|
|
124
136
|
from openadmin.types import BarChart, PieChart
|
|
125
137
|
|
|
138
|
+
|
|
126
139
|
@page.bar_chart("Sales by Region", "Total sales per region this quarter")
|
|
127
140
|
async def sales_chart() -> BarChart:
|
|
128
141
|
return BarChart(
|
|
@@ -133,11 +146,12 @@ async def sales_chart() -> BarChart:
|
|
|
133
146
|
config={"sales": {"label": "Sales", "color": "#6366f1"}},
|
|
134
147
|
)
|
|
135
148
|
|
|
149
|
+
|
|
136
150
|
@page.pie_chart("User Roles", "Breakdown of user roles")
|
|
137
151
|
async def roles_chart() -> PieChart:
|
|
138
152
|
return PieChart(
|
|
139
153
|
data=[
|
|
140
|
-
{"segment": "Admin",
|
|
154
|
+
{"segment": "Admin", "count": 5},
|
|
141
155
|
{"segment": "Editor", "count": 20},
|
|
142
156
|
{"segment": "Viewer", "count": 75},
|
|
143
157
|
],
|
|
@@ -163,10 +177,12 @@ Forms collect user input and submit it to your endpoint. Mark a form hidden if i
|
|
|
163
177
|
```python
|
|
164
178
|
from pydantic import BaseModel
|
|
165
179
|
|
|
180
|
+
|
|
166
181
|
class InvitePayload(BaseModel):
|
|
167
182
|
email: str
|
|
168
183
|
role: str
|
|
169
184
|
|
|
185
|
+
|
|
170
186
|
@page.form_post("Invite User", "Send an invitation email to a new user")
|
|
171
187
|
async def invite_user(payload: InvitePayload):
|
|
172
188
|
# send invite...
|
|
@@ -194,27 +210,37 @@ from openadmin.types import Stat, Table, BarChart
|
|
|
194
210
|
|
|
195
211
|
page = AdminPage("Library")
|
|
196
212
|
|
|
213
|
+
|
|
197
214
|
@page.stat("Total Books")
|
|
198
215
|
async def total_books(session: AsyncSessionDep) -> Stat:
|
|
199
216
|
result = await session.execute(select(func.count()).select_from(Book))
|
|
200
217
|
return Stat(value=result.scalar_one())
|
|
201
218
|
|
|
219
|
+
|
|
202
220
|
@page.table("Books")
|
|
203
221
|
async def books_table(
|
|
204
222
|
session: AsyncSessionDep,
|
|
205
223
|
pagination: PaginationParamsDep,
|
|
206
224
|
search: SearchQueryDep,
|
|
207
225
|
) -> Table:
|
|
208
|
-
stmt =
|
|
226
|
+
stmt = (
|
|
227
|
+
select(Book)
|
|
228
|
+
.offset(pagination.page * pagination.per_page)
|
|
229
|
+
.limit(pagination.per_page)
|
|
230
|
+
)
|
|
209
231
|
books = (await session.execute(stmt)).scalars().all()
|
|
210
232
|
return Table(data=[{"title": b.title, "year": b.published_year} for b in books])
|
|
211
233
|
|
|
234
|
+
|
|
212
235
|
@page.bar_chart("Books per Genre", "Number of books in each genre")
|
|
213
236
|
async def books_per_genre(session: AsyncSessionDep) -> BarChart:
|
|
214
|
-
rows = (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
rows = (
|
|
238
|
+
await session.execute(
|
|
239
|
+
select(Genre.name, func.count().label("books"))
|
|
240
|
+
.join(BookToGenre)
|
|
241
|
+
.group_by(Genre.id)
|
|
242
|
+
)
|
|
243
|
+
).all()
|
|
218
244
|
return BarChart(
|
|
219
245
|
data=[{"genre": name, "books": count} for name, count in rows],
|
|
220
246
|
config={"books": {"label": "Books", "color": "#6366f1"}},
|
|
@@ -17,27 +17,45 @@ page = AdminPage(
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
@page.stat(
|
|
20
|
+
@page.stat(
|
|
21
|
+
"Earliest Publication",
|
|
22
|
+
icon="sun",
|
|
23
|
+
description="This is earliest publications for the last quorder",
|
|
24
|
+
color="red",
|
|
25
|
+
)
|
|
21
26
|
async def get_earliest_publication(session: AsyncSessionDep) -> spec.Stat:
|
|
22
27
|
result = await session.execute(
|
|
23
28
|
select(func.min(models.Book.published_year)).where(
|
|
24
29
|
models.Book.published_year.isnot(None)
|
|
25
30
|
)
|
|
26
31
|
)
|
|
27
|
-
return
|
|
32
|
+
return result.scalar_one_or_none()
|
|
28
33
|
|
|
29
34
|
|
|
30
|
-
@page.stat(
|
|
31
|
-
|
|
35
|
+
@page.stat(
|
|
36
|
+
"Latest Publication",
|
|
37
|
+
description="Most recent publication year found in the catalog",
|
|
38
|
+
)
|
|
39
|
+
async def get_latest_publication(session: AsyncSessionDep) -> spec.Stat:
|
|
32
40
|
result = await session.execute(
|
|
33
41
|
select(func.max(models.Book.published_year)).where(
|
|
34
42
|
models.Book.published_year.isnot(None)
|
|
35
43
|
)
|
|
36
44
|
)
|
|
37
|
-
return result.scalar_one_or_none() or "N/A"
|
|
38
45
|
|
|
46
|
+
return {
|
|
47
|
+
"value": result.scalar_one_or_none() or "N/A",
|
|
48
|
+
"color": "blue",
|
|
49
|
+
"icon": "clock",
|
|
50
|
+
}
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
|
|
53
|
+
@page.stat(
|
|
54
|
+
"Books with Summary",
|
|
55
|
+
icon="book-check",
|
|
56
|
+
color="teal",
|
|
57
|
+
description="Number of books that include a summary",
|
|
58
|
+
)
|
|
41
59
|
async def get_books_with_summary(session: AsyncSessionDep) -> int:
|
|
42
60
|
result = await session.execute(
|
|
43
61
|
select(func.count(models.Book.id)).where(models.Book.summary.isnot(None))
|
|
@@ -19,13 +19,18 @@ page = AdminPage(
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
@page.stat("Total Authors")
|
|
22
|
+
@page.stat("Total Authors", description="Total number of authors in the catalog")
|
|
23
23
|
async def get_total_authors(session: AsyncSessionDep) -> spec.StatResponse:
|
|
24
24
|
result = await session.execute(select(func.count(models.Author.id)))
|
|
25
|
-
return {"value": result.scalar_one(), "icon": "
|
|
25
|
+
return {"value": result.scalar_one(), "icon": "users", "color": "indigo"}
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
@page.stat(
|
|
28
|
+
@page.stat(
|
|
29
|
+
"Authors with Bio",
|
|
30
|
+
icon="user-check",
|
|
31
|
+
color="emerald",
|
|
32
|
+
description="Authors that have a biography on file",
|
|
33
|
+
)
|
|
29
34
|
async def get_authors_with_bio(session: AsyncSessionDep) -> int:
|
|
30
35
|
result = await session.execute(
|
|
31
36
|
select(func.count(models.Author.id)).where(models.Author.bio.isnot(None))
|
|
@@ -33,7 +38,12 @@ async def get_authors_with_bio(session: AsyncSessionDep) -> int:
|
|
|
33
38
|
return result.scalar_one()
|
|
34
39
|
|
|
35
40
|
|
|
36
|
-
@page.stat(
|
|
41
|
+
@page.stat(
|
|
42
|
+
"Avg Books per Author",
|
|
43
|
+
icon="library",
|
|
44
|
+
color="amber",
|
|
45
|
+
description="Average number of books published per author",
|
|
46
|
+
)
|
|
37
47
|
async def get_avg_books_per_author(session: AsyncSessionDep) -> float:
|
|
38
48
|
subq = (
|
|
39
49
|
select(func.count(models.Book.id).label("cnt"))
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import func, select
|
|
6
6
|
|
|
7
|
+
from openadmin import spec
|
|
7
8
|
from openadmin.fastapi import AdminPage
|
|
8
9
|
from openadmin.fastapi.deps import PageDep
|
|
9
10
|
|
|
@@ -43,7 +44,12 @@ async def get_books_without_genre(session: AsyncSessionDep) -> int:
|
|
|
43
44
|
return result.scalar_one()
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
@page.table(
|
|
47
|
+
@page.table(
|
|
48
|
+
"All Genres",
|
|
49
|
+
description="All genres sorted by book count",
|
|
50
|
+
icon="flower",
|
|
51
|
+
color="green",
|
|
52
|
+
)
|
|
47
53
|
async def get_all_genres(session: AsyncSessionDep, pagination: PageDep):
|
|
48
54
|
stmt = (
|
|
49
55
|
select(models.Genre, func.count(models.BookToGenre.book_id).label("book_count"))
|
|
@@ -55,12 +61,35 @@ async def get_all_genres(session: AsyncSessionDep, pagination: PageDep):
|
|
|
55
61
|
)
|
|
56
62
|
result = await session.execute(stmt)
|
|
57
63
|
return [
|
|
58
|
-
{
|
|
64
|
+
{
|
|
65
|
+
"id": g.id,
|
|
66
|
+
"name": g.name,
|
|
67
|
+
"book_count": count,
|
|
68
|
+
"__veiw__": f"{g.id} | {g.name}",
|
|
69
|
+
}
|
|
70
|
+
for g, count in result.all()
|
|
59
71
|
]
|
|
60
72
|
|
|
61
73
|
|
|
62
|
-
@page.pie_chart(
|
|
63
|
-
|
|
74
|
+
@page.pie_chart(
|
|
75
|
+
"Genre Distribution 1",
|
|
76
|
+
description="Share of books across all genres",
|
|
77
|
+
name_key="name",
|
|
78
|
+
value_key="count",
|
|
79
|
+
)
|
|
80
|
+
async def get_genre_distribution_1(session: AsyncSessionDep):
|
|
81
|
+
stmt = (
|
|
82
|
+
select(models.Genre, func.count(models.BookToGenre.book_id).label("count"))
|
|
83
|
+
.join(models.BookToGenre, models.BookToGenre.genre_id == models.Genre.id)
|
|
84
|
+
.group_by(models.Genre.id)
|
|
85
|
+
.order_by(func.count(models.BookToGenre.book_id).desc())
|
|
86
|
+
)
|
|
87
|
+
result = await session.execute(stmt)
|
|
88
|
+
return [{**g, "count": count} for g, count in result.all()]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@page.pie_chart("Genre Distribution 2", description="Share of books across all genres")
|
|
92
|
+
async def get_genre_distribution_2(session: AsyncSessionDep) -> spec.PieChart:
|
|
64
93
|
stmt = (
|
|
65
94
|
select(models.Genre.name, func.count(models.BookToGenre.book_id).label("count"))
|
|
66
95
|
.join(models.BookToGenre, models.BookToGenre.genre_id == models.Genre.id)
|
|
@@ -68,7 +97,9 @@ async def get_genre_distribution(session: AsyncSessionDep):
|
|
|
68
97
|
.order_by(func.count(models.BookToGenre.book_id).desc())
|
|
69
98
|
)
|
|
70
99
|
result = await session.execute(stmt)
|
|
71
|
-
return
|
|
100
|
+
return {
|
|
101
|
+
"data": [{"name": row.name, "value": row.count} for row in result],
|
|
102
|
+
}
|
|
72
103
|
|
|
73
104
|
|
|
74
105
|
@page.bar_chart("Books per Genre", description="Absolute book count for each genre")
|
|
@@ -9,17 +9,32 @@ page = AdminPage(
|
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
@page.stat(
|
|
12
|
+
@page.stat(
|
|
13
|
+
"API Uptime (%)",
|
|
14
|
+
icon="check-circle",
|
|
15
|
+
color="green",
|
|
16
|
+
description="Percentage of successful API requests over the last 30 days",
|
|
17
|
+
)
|
|
13
18
|
async def get_api_uptime() -> float:
|
|
14
19
|
return 99.97
|
|
15
20
|
|
|
16
21
|
|
|
17
|
-
@page.stat(
|
|
22
|
+
@page.stat(
|
|
23
|
+
"Avg Response Time (ms)",
|
|
24
|
+
icon="gauge",
|
|
25
|
+
color="blue",
|
|
26
|
+
description="Average API response time across all endpoints",
|
|
27
|
+
)
|
|
18
28
|
async def get_avg_response_time() -> int:
|
|
19
29
|
return 84
|
|
20
30
|
|
|
21
31
|
|
|
22
|
-
@page.stat(
|
|
32
|
+
@page.stat(
|
|
33
|
+
"Active Connections",
|
|
34
|
+
icon="plug",
|
|
35
|
+
color="violet",
|
|
36
|
+
description="Current number of open client connections",
|
|
37
|
+
)
|
|
23
38
|
async def get_active_connections() -> int:
|
|
24
39
|
return 12
|
|
25
40
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 OpenAdmin
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from openadmin import fastapi, spec
|
|
9
|
+
|
|
10
|
+
WEEKDAYS: list[dict[str, Any]] = [
|
|
11
|
+
{"day": "Monday", "books": 5, "coffe": 5},
|
|
12
|
+
{"day": "Tuesday", "books": 3, "coffe": 6},
|
|
13
|
+
{"day": "Wednesday", "books": 7, "coffe": 7},
|
|
14
|
+
{"day": "Thursday", "books": 9, "coffe": 8},
|
|
15
|
+
{"day": "Friday", "books": 10, "coffe": 9},
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
page = fastapi.AdminPage(
|
|
19
|
+
"Week Days",
|
|
20
|
+
icon="calendar",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@page.pie_chart(
|
|
25
|
+
"How many books I have read 1",
|
|
26
|
+
description="Books read per weekday, week 1",
|
|
27
|
+
name_key="day",
|
|
28
|
+
value_key="books",
|
|
29
|
+
config={
|
|
30
|
+
item["day"]: {
|
|
31
|
+
"name": item["day"],
|
|
32
|
+
"color": spec.COLORS[num + 10 % len(spec.COLORS)],
|
|
33
|
+
}
|
|
34
|
+
for num, item in enumerate(WEEKDAYS)
|
|
35
|
+
},
|
|
36
|
+
color="violet",
|
|
37
|
+
icon="book-open",
|
|
38
|
+
caption="5 day streak",
|
|
39
|
+
caption_description="Reading every day this work week",
|
|
40
|
+
caption_icon="flame",
|
|
41
|
+
)
|
|
42
|
+
def get_books_1() -> spec.PieChart:
|
|
43
|
+
return WEEKDAYS
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@page.pie_chart(
|
|
47
|
+
"How many books I have read 2",
|
|
48
|
+
description="Books read per weekday, week 2",
|
|
49
|
+
name_key="day",
|
|
50
|
+
value_key="books",
|
|
51
|
+
caption="3 day streak",
|
|
52
|
+
caption_description="Reading every day this work week",
|
|
53
|
+
caption_icon="flame",
|
|
54
|
+
)
|
|
55
|
+
def get_books_2() -> spec.PieChart:
|
|
56
|
+
return {
|
|
57
|
+
"data": WEEKDAYS,
|
|
58
|
+
"color": "amber",
|
|
59
|
+
"icon": "a-arrow-up",
|
|
60
|
+
"config": {
|
|
61
|
+
item["day"]: {
|
|
62
|
+
"name": item["day"],
|
|
63
|
+
"color": spec.COLORS[num + 10 % len(spec.COLORS)],
|
|
64
|
+
}
|
|
65
|
+
for num, item in enumerate(WEEKDAYS)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@page.pie_chart(
|
|
71
|
+
"How many books I have read 3",
|
|
72
|
+
description="Books read per weekday, all pie chart options set",
|
|
73
|
+
color="violet",
|
|
74
|
+
icon="book-open",
|
|
75
|
+
caption="5 day streak",
|
|
76
|
+
caption_description="Reading every day this work week",
|
|
77
|
+
caption_icon="flame",
|
|
78
|
+
config={
|
|
79
|
+
item["day"]: {
|
|
80
|
+
"name": item["day"],
|
|
81
|
+
"color": spec.COLORS[num + 10 % len(spec.COLORS)],
|
|
82
|
+
}
|
|
83
|
+
for num, item in enumerate(WEEKDAYS)
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
def get_books_3() -> spec.PieChart:
|
|
87
|
+
return [{"name": day["day"], "value": day["value"]} for day in WEEKDAYS]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@page.bar_chart(
|
|
91
|
+
"Books & Coffee 1",
|
|
92
|
+
description="Books read and coffee cups per weekday, week 1",
|
|
93
|
+
data_key="day",
|
|
94
|
+
config={
|
|
95
|
+
"books": {"name": "Books", "color": "violet", "icon": "book-open"},
|
|
96
|
+
"coffe": {"name": "Coffee", "color": "amber", "icon": "coffee"},
|
|
97
|
+
},
|
|
98
|
+
color="violet",
|
|
99
|
+
icon="book-open",
|
|
100
|
+
caption="5 day streak",
|
|
101
|
+
caption_description="Reading every day this work week",
|
|
102
|
+
caption_icon="flame",
|
|
103
|
+
)
|
|
104
|
+
def get_books_and_coffe_1() -> spec.BarChart:
|
|
105
|
+
return WEEKDAYS
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@page.bar_chart(
|
|
109
|
+
"Books & Coffee 2",
|
|
110
|
+
description="Books read and coffee cups per weekday, week 2",
|
|
111
|
+
data_key="day",
|
|
112
|
+
config={
|
|
113
|
+
"books": {"name": "Books", "color": "violet", "icon": "book-open"},
|
|
114
|
+
"coffe": {"name": "Coffee", "color": "amber", "icon": "coffee"},
|
|
115
|
+
},
|
|
116
|
+
color="amber",
|
|
117
|
+
icon="a-arrow-up",
|
|
118
|
+
caption="3 day streak",
|
|
119
|
+
caption_description="Reading every day this work week",
|
|
120
|
+
caption_icon="flame",
|
|
121
|
+
)
|
|
122
|
+
def get_books_and_coffe_2() -> spec.BarChart:
|
|
123
|
+
return {
|
|
124
|
+
"data": WEEKDAYS,
|
|
125
|
+
"color": "amber",
|
|
126
|
+
"icon": "a-arrow-up",
|
|
127
|
+
"config": {
|
|
128
|
+
"books": {"name": "Books", "color": "violet", "icon": "book-open"},
|
|
129
|
+
"coffe": {"name": "Coffee", "color": "amber", "icon": "coffee"},
|
|
130
|
+
},
|
|
131
|
+
}
|