Skip to content

Commit 20d0827

Browse files
committed
Replace DashboardWidgetAdmin with widget_action decorator. (no backward compatibility).
1 parent 4d310e1 commit 20d0827

41 files changed

Lines changed: 2798 additions & 1486 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/build.py

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ def read_cls_docstring(cls):
4242

4343
def get_versions():
4444
return [
45+
{
46+
"version": "0.4.2",
47+
"changes": [
48+
"Replace DashboardWidgetAdmin with widget_action decorator. (no backward compatibility)",
49+
"Fix mobile view.",
50+
],
51+
},
4552
{
4653
"version": "0.4.1",
4754
"changes": [
@@ -372,8 +379,6 @@ def get_sections():
372379

373380

374381
def get_page_context(page_url):
375-
from docs.code.dashboard import djangoorm as dashboard_djangoorm
376-
from docs.code.dashboard import tortoise as dashboard_tortoise
377382
from docs.code.inlines import tortoise as inlines_tortoise
378383
from docs.code.models import tortoise as models_tortoise
379384
from docs.code.quick_tutorial import django as quick_tutorial_django
@@ -384,8 +389,17 @@ def get_page_context(page_url):
384389
from docs.code.quick_tutorial import sqlalchemy as quick_tutorial_sqlalchemy
385390
from docs.code.quick_tutorial import tortoise as quick_tutorial_tortoise
386391

387-
from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, InlineModelAdmin, ModelAdmin, WidgetType
392+
from fastadmin import InlineModelAdmin, ModelAdmin, WidgetType, widget_action
388393
from fastadmin.models.base import BaseModelAdmin
394+
from fastadmin.models.schemas import (
395+
WidgetActionArgumentProps,
396+
WidgetActionChartProps,
397+
WidgetActionFilter,
398+
WidgetActionInputSchema,
399+
WidgetActionProps,
400+
WidgetActionResponseSchema,
401+
WidgetActionType,
402+
)
389403

390404
match page_url:
391405
case "#introduction":
@@ -553,61 +567,63 @@ def get_page_context(page_url):
553567
},
554568
]
555569
# widgets
556-
case "#registering-widgets":
570+
case "#widget-methods-and-attributes":
557571
return [
558572
{
559-
"type": "text-lead",
560-
"content": "Register dashboard widgets",
573+
"type": "text",
574+
"content": (
575+
"Use the @widget_action decorator on ModelAdmin methods to declare "
576+
"dashboard widgets and actions. The decorator attaches metadata used "
577+
"to render chart widgets and action forms on the dashboard."
578+
),
561579
},
580+
{"type": "code-python", "content": inspect.getsource(widget_action)},
562581
{
563-
"type": "tabs",
564-
"id": "register_dashboard_widgets",
565-
"content": [
566-
{
567-
"name": "Tortoise ORM",
568-
"id": "dashboard_tortoise_orm",
569-
"content": [{"type": "code-python", "content": inspect.getsource(dashboard_tortoise)}],
570-
},
571-
{
572-
"name": "Django ORM",
573-
"id": "dashboard_django_orm",
574-
"content": [{"type": "code-python", "content": inspect.getsource(dashboard_djangoorm)}],
575-
},
576-
{
577-
"name": "SQLAlchemy",
578-
"id": "dashboard_sql_alchemy",
579-
"content": [{"type": "alert-info", "content": "See the Tortoise ORM example above."}],
580-
},
581-
{
582-
"name": "Pony ORM",
583-
"id": "dashboard_pony_orm",
584-
"content": [{"type": "alert-info", "content": "See the Tortoise ORM example above."}],
585-
},
586-
],
582+
"type": "text",
583+
"content": (
584+
"Widget actions use the following types for configuration "
585+
"(chart props, arguments, filters and payload schema):"
586+
),
587587
},
588-
]
589-
case "#widget-methods-and-attributes":
590-
return [
591588
{
592-
"type": "text",
593-
"content": "The following methods and attributes are available for dashboard widget admins:",
589+
"type": "code-python",
590+
"content": "\n\n".join(
591+
[
592+
inspect.getsource(WidgetActionChartProps),
593+
inspect.getsource(WidgetActionArgumentProps),
594+
inspect.getsource(WidgetActionProps),
595+
inspect.getsource(WidgetActionFilter),
596+
inspect.getsource(WidgetActionInputSchema),
597+
inspect.getsource(WidgetActionResponseSchema),
598+
]
599+
),
594600
},
595-
{"type": "code-python", "content": inspect.getsource(DashboardWidgetAdmin)},
596601
{
597602
"type": "alert-warning",
598-
"content": f"See <a href='{ANTD_CHARTS_EXAMPLES}' target='_blank'>antd charts</a> for <code>x_field_filter_widget_props</code>.",
603+
"content": (
604+
"If you call @widget_action() without arguments, FastAdmin uses the "
605+
"documented defaults: tab='Default', title='Action', "
606+
"widget_action_type=WidgetActionType.Action and no props or filters."
607+
),
599608
},
600609
]
601610
case "#widget-chart-types":
602611
return [
603612
{
604613
"type": "text",
605-
"content": "The FastAdmin dashboard supports the following widget types:",
614+
"content": (
615+
"The FastAdmin dashboard supports the following widget action types for charts and actions:"
616+
),
606617
},
607-
{"type": "code-python", "content": inspect.getsource(DashboardWidgetType)},
618+
{"type": "code-python", "content": inspect.getsource(WidgetActionType)},
608619
{
609620
"type": "alert-warning",
610-
"content": f"See <a href='{ANTD_CHARTS_EXAMPLES}' target='_blank'>antd charts</a> for more details (e.g. how they look).",
621+
"content": (
622+
"Chart-* widget actions render charts on the dashboard; the Action "
623+
"type renders a simple action widget. See "
624+
f"<a href='{ANTD_CHARTS_EXAMPLES}' target='_blank'>antd charts</a> "
625+
"for more details (e.g. how they look)."
626+
),
611627
},
612628
]
613629
# models

docs/code/dashboard/__init__.py

Whitespace-only changes.

docs/code/dashboard/djangoorm.py

Lines changed: 0 additions & 70 deletions
This file was deleted.

docs/code/dashboard/tortoise.py

Lines changed: 0 additions & 67 deletions
This file was deleted.

docs/code/models/tortoise.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
from tortoise import fields
55
from tortoise.models import Model
66

7-
from fastadmin import TortoiseModelAdmin, WidgetType, action, register
7+
from fastadmin import TortoiseModelAdmin, WidgetType, action, register, widget_action
8+
from fastadmin.models.schemas import (
9+
WidgetActionArgumentProps,
10+
WidgetActionChartProps,
11+
WidgetActionFilter,
12+
WidgetActionInputSchema,
13+
WidgetActionProps,
14+
WidgetActionResponseSchema,
15+
WidgetActionType,
16+
)
817

918

1019
class ModelUser(Model):
@@ -38,7 +47,7 @@ class UserAdmin(TortoiseModelAdmin):
3847
)
3948
formfield_overrides = { # noqa: RUF012
4049
"username": (WidgetType.SlugInput, {"required": True}),
41-
"password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
50+
"hash_password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
4251
"avatar_url": (
4352
WidgetType.UploadImage,
4453
{
@@ -52,9 +61,13 @@ class UserAdmin(TortoiseModelAdmin):
5261
"activate",
5362
"deactivate",
5463
)
64+
widget_actions = (
65+
"users_chart",
66+
"users_action",
67+
)
5568

5669
async def authenticate(self, username: str, password: str) -> int | None:
57-
user = await self.model_cls.filter(phone=username, is_superuser=True).first()
70+
user = await self.model_cls.filter(username=username, is_superuser=True).first()
5871
if not user:
5972
return None
6073
if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
@@ -80,3 +93,68 @@ async def upload_file(self, field_name: str, file_name: str, file_content: bytes
8093
# save file to media directory or s3/filestorage, then return the file url
8194
url = f"/media/{file_name}"
8295
return url
96+
97+
@widget_action(
98+
widget_action_type=WidgetActionType.ChartLine,
99+
widget_action_props=WidgetActionChartProps(x_field="x", y_field="y"),
100+
widget_action_filters=[
101+
WidgetActionFilter(
102+
field_name="x",
103+
widget_type=WidgetType.DatePicker,
104+
),
105+
],
106+
tab="Analytics",
107+
title="Users over time",
108+
description="Line chart of active users over time",
109+
width=24,
110+
)
111+
async def users_chart(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
112+
# In a real project you would aggregate using payload.query filters.
113+
return WidgetActionResponseSchema(
114+
data=[
115+
{
116+
"x": "2026-01-01",
117+
"y": 10,
118+
},
119+
{
120+
"x": "2026-01-02",
121+
"y": 15,
122+
},
123+
{
124+
"x": "2026-01-03",
125+
"y": 7,
126+
},
127+
],
128+
)
129+
130+
@widget_action(
131+
widget_action_type=WidgetActionType.Action,
132+
widget_action_props=WidgetActionProps(
133+
arguments=[
134+
WidgetActionArgumentProps(
135+
name="since",
136+
widget_type=WidgetType.DatePicker,
137+
widget_props={
138+
"required": True,
139+
},
140+
),
141+
],
142+
),
143+
tab="Data",
144+
title="Export users",
145+
description="Return a simple table of users created since a date",
146+
width=12,
147+
)
148+
async def users_action(self, payload: WidgetActionInputSchema) -> WidgetActionResponseSchema:
149+
return WidgetActionResponseSchema(
150+
data=[
151+
{
152+
"id": 1,
153+
"username": "alice",
154+
},
155+
{
156+
"id": 2,
157+
"username": "bob",
158+
},
159+
],
160+
)

0 commit comments

Comments
 (0)