diff --git a/admin_ui/src/components/RowFilter.vue b/admin_ui/src/components/RowFilter.vue
index acc2ad2e..79a0662d 100644
--- a/admin_ui/src/components/RowFilter.vue
+++ b/admin_ui/src/components/RowFilter.vue
@@ -7,7 +7,7 @@
@@ -31,9 +31,6 @@ export default Vue.extend({
schema() {
return this.$store.state.schema
},
- tableName() {
- return this.$store.state.currentTableName
- },
},
methods: {
closeSideBar() {
diff --git a/admin_ui/src/components/RowFormSearch.vue b/admin_ui/src/components/RowFormSearch.vue
index 595b0642..6967f602 100644
--- a/admin_ui/src/components/RowFormSearch.vue
+++ b/admin_ui/src/components/RowFormSearch.vue
@@ -1,68 +1,41 @@
-
-
-
+
@@ -76,20 +49,11 @@ export default Vue.extend({
props: {
row: Object,
schema: Object,
- isFilter: {
- type: Boolean,
- default: false,
- },
},
components: {
InputField,
KeySearch,
},
- data() {
- return {
- keySelectIDs: {},
- }
- },
methods: {
getValue(propertyTitle: string) {
let value = this.row
@@ -109,14 +73,3 @@ export default Vue.extend({
},
})
-
-
\ No newline at end of file
diff --git a/admin_ui/src/main.less b/admin_ui/src/main.less
index 26214c14..e0da49a6 100644
--- a/admin_ui/src/main.less
+++ b/admin_ui/src/main.less
@@ -1,4 +1,4 @@
-@import "./vars.less";
+@import './vars.less';
html {
height: 100%;
@@ -10,7 +10,7 @@ body {
min-height: 100%;
margin: 0;
position: relative;
- font-family: "Avenir", Helvetica, Arial, sans-serif;
+ font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -165,15 +165,20 @@ body {
min-height: 100%;
}
+h1 {
+ font-size: 1.6rem;
+ font-weight: bold;
+}
+
label {
display: block;
padding-bottom: 0.2rem;
padding-top: 0.8rem;
}
-input[type="text"],
-input[type="number"],
-input[type="password"],
+input[type='text'],
+input[type='number'],
+input[type='password'],
select,
textarea {
box-sizing: border-box;
@@ -182,9 +187,9 @@ textarea {
margin-bottom: 0.5rem;
}
-input[type="text"],
-input[type="number"],
-input[type="password"],
+input[type='text'],
+input[type='number'],
+input[type='password'],
button,
select,
textarea {
@@ -200,7 +205,7 @@ textarea {
select {
appearance: none;
-webkit-appearance: none;
- background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
+ background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat, repeat;
background-position: right 0.7em top 50%, 0 0;
background-size: 0.65em auto, 100%;
@@ -241,4 +246,3 @@ a {
svg {
padding-right: 0.3rem;
}
-
diff --git a/docs/source/table_config/images/with_table_config.jpg b/docs/source/table_config/images/with_visible_columns.jpg
similarity index 100%
rename from docs/source/table_config/images/with_table_config.jpg
rename to docs/source/table_config/images/with_visible_columns.jpg
diff --git a/docs/source/table_config/images/with_visible_filters.jpg b/docs/source/table_config/images/with_visible_filters.jpg
new file mode 100644
index 00000000..49429e27
Binary files /dev/null and b/docs/source/table_config/images/with_visible_filters.jpg differ
diff --git a/docs/source/table_config/images/without_table_config.jpg b/docs/source/table_config/images/without_visible_columns.jpg
similarity index 100%
rename from docs/source/table_config/images/without_table_config.jpg
rename to docs/source/table_config/images/without_visible_columns.jpg
diff --git a/docs/source/table_config/images/without_visible_filters.jpg b/docs/source/table_config/images/without_visible_filters.jpg
new file mode 100644
index 00000000..9f63d820
Binary files /dev/null and b/docs/source/table_config/images/without_visible_filters.jpg differ
diff --git a/docs/source/table_config/index.rst b/docs/source/table_config/index.rst
index 46113424..60d75da7 100644
--- a/docs/source/table_config/index.rst
+++ b/docs/source/table_config/index.rst
@@ -13,8 +13,16 @@ Alternatively, you can pass in ``TableConfig`` instances (or mix and match
them).
By passing in a ``TableConfig`` you have extra control over how the UI behaves
-for that table. For example, we can set which columns are visible in the list
-view:
+for that table. This is particularly useful when you have a ``Table`` with lots
+of columns.
+
+In the future, ``TableConfig`` will be extended to allow finer grained control
+over the UI.
+
+visible_columns
+---------------
+
+We can set which columns are visible in the list view:
.. code-block:: python
@@ -26,17 +34,36 @@ view:
Here is the UI when just passing in a ``Table``:
-.. image:: ./images/without_table_config.jpg
+.. image:: ./images/without_visible_columns.jpg
-Here is the UI when just passing in a ``TableConfig`` instance instead (less
+Here is the UI when just passing in a ``TableConfig`` instance instead (fewer
columns are visible):
-.. image:: ./images/with_table_config.jpg
+.. image:: ./images/with_visible_columns.jpg
-This is really useful when you have a ``Table`` with lots of columns.
+visible_filters
+---------------
-In the future, ``TableConfig`` will be extended to allow finer grained control
-over the UI.
+We can set which columns are visible in the filter sidebar:
+
+.. code-block:: python
+
+ from piccolo_admin.endpoints import TableConfig
+
+ movie_config = TableConfig(Movie, visible_filters=[
+ Movie.name, Movie.rating, Movie.director,
+ ])
+
+ create_admin(Director, movie_config)
+
+Here is the UI when just passing in a ``Table``:
+
+.. image:: ./images/without_visible_filters.jpg
+
+Here is the UI when just passing in a ``TableConfig`` instance instead (fewer
+filters are visible in the sidebar):
+
+.. image:: ./images/with_visible_filters.jpg
Source
------
diff --git a/piccolo_admin/endpoints.py b/piccolo_admin/endpoints.py
index 320d41be..38070950 100644
--- a/piccolo_admin/endpoints.py
+++ b/piccolo_admin/endpoints.py
@@ -66,12 +66,21 @@ class TableConfig:
You can specify this instead of ``visible_columns``, in which case all
of the ``Table`` columns except the ones specified will be shown in the
list view.
+ :param visible_filters:
+ If specified, only these columns will be shown in the filter sidebar
+ of the UI. This is useful when you have a lot of columns.
+ :param exclude_visible_filters:
+ You can specify this instead of ``visible_filters``, in which case all
+ of the ``Table`` columns except the ones specified will be shown in the
+ filter sidebar.
"""
table_class: t.Type[Table]
visible_columns: t.Optional[t.List[Column]] = None
exclude_visible_columns: t.Optional[t.List[Column]] = None
+ visible_filters: t.Optional[t.List[Column]] = None
+ exclude_visible_filters: t.Optional[t.List[Column]] = None
def __post_init__(self):
if self.visible_columns and self.exclude_visible_columns:
@@ -80,23 +89,47 @@ def __post_init__(self):
"``exclude_visible_columns``."
)
- def get_visible_columns(self) -> t.List[Column]:
- if self.visible_columns and not self.exclude_visible_columns:
- return self.visible_columns
+ if self.visible_filters and self.exclude_visible_filters:
+ raise ValueError(
+ "Only specify ``visible_filters`` or "
+ "``exclude_visible_filters``."
+ )
+
+ def _get_columns(
+ self,
+ include_columns: t.Optional[t.List[Column]],
+ exclude_columns: t.Optional[t.List[Column]],
+ all_columns: t.List[Column],
+ ) -> t.List[Column]:
+ if include_columns and not exclude_columns:
+ return include_columns
- if self.exclude_visible_columns and not self.visible_columns:
- column_names = (i._meta.name for i in self.exclude_visible_columns)
- return [
- i
- for i in self.table_class._meta.columns
- if i._meta.name not in column_names
- ]
+ if exclude_columns and not include_columns:
+ column_names = (i._meta.name for i in exclude_columns)
+ return [i for i in all_columns if i._meta.name not in column_names]
- return self.table_class._meta.columns
+ return all_columns
+
+ def get_visible_columns(self) -> t.List[Column]:
+ return self._get_columns(
+ include_columns=self.visible_columns,
+ exclude_columns=self.exclude_visible_columns,
+ all_columns=self.table_class._meta.columns,
+ )
def get_visible_column_names(self) -> t.Tuple[str, ...]:
return tuple(i._meta.name for i in self.get_visible_columns())
+ def get_visible_filters(self) -> t.List[Column]:
+ return self._get_columns(
+ include_columns=self.visible_filters,
+ exclude_columns=self.exclude_visible_filters,
+ all_columns=self.table_class._meta.columns,
+ )
+
+ def get_visible_filter_names(self) -> t.Tuple[str, ...]:
+ return tuple(i._meta.name for i in self.get_visible_filters())
+
@dataclass
class FormConfig:
@@ -148,7 +181,8 @@ def my_endpoint(request: Request, data: MyModel):
name: str
pydantic_model: t.Type[BaseModel]
endpoint: t.Callable[
- [Request, pydantic.BaseModel], t.Union[str, None, t.Coroutine],
+ [Request, pydantic.BaseModel],
+ t.Union[str, None, t.Coroutine],
]
description: t.Optional[str] = None
@@ -226,6 +260,7 @@ def __init__(
for table_config in table_configs:
table_class = table_config.table_class
visible_column_names = table_config.get_visible_column_names()
+ visible_filter_names = table_config.get_visible_filter_names()
FastAPIWrapper(
root_url=f"/tables/{table_class._meta.tablename}/",
fastapi_app=api_app,
@@ -234,7 +269,8 @@ def __init__(
read_only=read_only,
page_size=page_size,
schema_extra={
- "visible_column_names": visible_column_names
+ "visible_column_names": visible_column_names,
+ "visible_filter_names": visible_filter_names,
},
),
fastapi_kwargs=FastAPIKwargs(
@@ -368,7 +404,8 @@ async def get_root(self, request: Request) -> HTMLResponse:
def get_user(self, request: Request) -> UserResponseModel:
return UserResponseModel(
- username=request.user.display_name, user_id=request.user.user_id,
+ username=request.user.display_name,
+ user_id=request.user.user_id,
)
###########################################################################
@@ -393,7 +430,9 @@ def get_single_form(self, form_slug: str) -> FormConfigResponseModel:
raise HTTPException(status_code=404, detail="No such form found")
else:
return FormConfigResponseModel(
- name=form.name, slug=form.slug, description=form.description,
+ name=form.name,
+ slug=form.slug,
+ description=form.description,
)
def get_single_form_schema(self, form_slug: str) -> t.Dict[str, t.Any]:
diff --git a/piccolo_admin/example.py b/piccolo_admin/example.py
index 60dfba7d..b30a2d41 100644
--- a/piccolo_admin/example.py
+++ b/piccolo_admin/example.py
@@ -185,11 +185,20 @@ async def booking_endpoint(request, data):
Movie.rating,
Movie.director,
],
+ visible_filters=[
+ Movie.name,
+ Movie.rating,
+ Movie.director,
+ ],
)
director_config = TableConfig(
table_class=Director,
- visible_columns=[Director._meta.primary_key, Director.name],
+ visible_columns=[
+ Director._meta.primary_key,
+ Director.name,
+ Director.gender,
+ ],
)
APP = create_admin(
diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py
index 973ef27c..d05220a8 100644
--- a/tests/test_endpoints.py
+++ b/tests/test_endpoints.py
@@ -1,7 +1,13 @@
from unittest import TestCase
from piccolo.apps.user.tables import BaseUser
-from piccolo.columns.column_types import ForeignKey, Text, Timestamp, Varchar
+from piccolo.columns.column_types import (
+ ForeignKey,
+ Integer,
+ Text,
+ Timestamp,
+ Varchar,
+)
from piccolo.table import Table
from piccolo_api.session_auth.tables import SessionsBase
from starlette.testclient import TestClient
@@ -33,6 +39,7 @@ class Post(Table):
name = Varchar(length=100)
content = Text()
created = Timestamp()
+ rating = Integer()
class TestTableConfig(TestCase):
@@ -42,7 +49,8 @@ def test_visible_columns(self):
visible_columns=[Post._meta.primary_key, Post.name],
)
self.assertEqual(
- post_table.get_visible_column_names(), ("id", "name"),
+ post_table.get_visible_column_names(),
+ ("id", "name"),
)
def test_exclude_visible_columns(self):
@@ -52,7 +60,7 @@ def test_exclude_visible_columns(self):
)
self.assertEqual(
tuple(i._meta.name for i in post_table.get_visible_columns()),
- ("content", "created"),
+ ("content", "created", "rating"),
)
def test_visible_exclude_columns_error(self):
@@ -64,6 +72,35 @@ def test_visible_exclude_columns_error(self):
)
post_table.get_visible_columns()
+ def test_visible_filters(self):
+ post_table = TableConfig(
+ table_class=Post,
+ visible_filters=[Post.name, Post.rating],
+ )
+ self.assertEqual(
+ post_table.get_visible_filter_names(),
+ ("name", "rating"),
+ )
+
+ def test_exclude_visible_filters(self):
+ post_table = TableConfig(
+ table_class=Post,
+ exclude_visible_filters=[Post._meta.primary_key, Post.name],
+ )
+ self.assertEqual(
+ tuple(i._meta.name for i in post_table.get_visible_filters()),
+ ("content", "created", "rating"),
+ )
+
+ def test_visible_filters_error(self):
+ with self.assertRaises(ValueError):
+ post_table = TableConfig(
+ table_class=Post,
+ visible_filters=[Post.name],
+ exclude_visible_filters=[Post.name, Post.rating],
+ )
+ post_table.get_visible_filters()
+
class TestAdminRouter(TestCase):
def test_get_meta(self):
@@ -120,7 +157,9 @@ def test_forms(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
- "/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
+ "/auth/login/",
+ json=payload,
+ headers={"X-CSRFToken": csrftoken},
)
#######################################################################
@@ -193,7 +232,9 @@ def test_post_form_success(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
- "/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
+ "/auth/login/",
+ json=payload,
+ headers={"X-CSRFToken": csrftoken},
)
#######################################################################
# Post a form
@@ -241,7 +282,9 @@ def test_post_form_fail(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
- "/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
+ "/auth/login/",
+ json=payload,
+ headers={"X-CSRFToken": csrftoken},
)
#######################################################################
# Post a form with errors
@@ -288,7 +331,9 @@ def test_tables(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
- "/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
+ "/auth/login/",
+ json=payload,
+ headers={"X-CSRFToken": csrftoken},
)
#######################################################################
@@ -297,7 +342,8 @@ def test_tables(self):
response = client.get("/api/tables/")
self.assertEqual(response.status_code, 200)
self.assertEqual(
- response.json(), ["movie", "director", "studio"],
+ response.json(),
+ ["movie", "director", "studio"],
)
def test_get_user(self):
@@ -313,7 +359,9 @@ def test_get_user(self):
# Login
payload = dict(csrftoken=csrftoken, **self.credentials)
client.post(
- "/auth/login/", json=payload, headers={"X-CSRFToken": csrftoken},
+ "/auth/login/",
+ json=payload,
+ headers={"X-CSRFToken": csrftoken},
)
#######################################################################
@@ -322,6 +370,6 @@ def test_get_user(self):
response = client.get("/api/user/")
self.assertEqual(response.status_code, 200)
self.assertEqual(
- response.json(), {"username": "Bob", "user_id": "1"},
+ response.json(),
+ {"username": "Bob", "user_id": "1"},
)
-