mirror of https://github.com/apache/superset.git
refactor: Removes the filters set feature (#26369)
This commit is contained in:
parent
7ca6d8c880
commit
9387c4c16f
|
@ -89,7 +89,6 @@ These features flags currently default to True and **will be removed in a future
|
||||||
- DASHBOARD_CACHE
|
- DASHBOARD_CACHE
|
||||||
- DASHBOARD_FILTERS_EXPERIMENTAL
|
- DASHBOARD_FILTERS_EXPERIMENTAL
|
||||||
- DASHBOARD_NATIVE_FILTERS
|
- DASHBOARD_NATIVE_FILTERS
|
||||||
- DASHBOARD_NATIVE_FILTERS_SET
|
|
||||||
- ENABLE_EXPLORE_JSON_CSRF_PROTECTION
|
- ENABLE_EXPLORE_JSON_CSRF_PROTECTION
|
||||||
- ENABLE_TEMPLATE_REMOVE_FILTERS
|
- ENABLE_TEMPLATE_REMOVE_FILTERS
|
||||||
- GENERIC_CHART_AXES
|
- GENERIC_CHART_AXES
|
||||||
|
|
|
@ -1,216 +1,212 @@
|
||||||
||Admin|Alpha|Gamma|SQL_LAB|
|
| | Admin | Alpha | Gamma | SQL_LAB |
|
||||||
|---|---|---|---|---|
|
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
|
||||||
|Permission/role description|Admins have all possible rights, including granting or revoking rights from other users and altering other people’s slices and dashboards.| Alpha users have access to all data sources, but they cannot grant or revoke access from other users. They are also limited to altering the objects that they own. Alpha users can add and alter data sources.|Gamma users have limited access. They can only consume data coming from data sources they have been given access to through another complementary role. They only have access to view the slices and dashboards made from data sources that they have access to. Currently Gamma users are not able to alter or add data sources. We assume that they are mostly content consumers, though they can create slices and dashboards.|The sql_lab role grants access to SQL Lab. Note that while Admin users have access to all databases by default, both Alpha and Gamma users need to be given access on a per database basis.||
|
| Permission/role description | Admins have all possible rights, including granting or revoking rights from other users and altering other people’s slices and dashboards. | Alpha users have access to all data sources, but they cannot grant or revoke access from other users. They are also limited to altering the objects that they own. Alpha users can add and alter data sources. | Gamma users have limited access. They can only consume data coming from data sources they have been given access to through another complementary role. They only have access to view the slices and dashboards made from data sources that they have access to. Currently Gamma users are not able to alter or add data sources. We assume that they are mostly content consumers, though they can create slices and dashboards. | The sql_lab role grants access to SQL Lab. Note that while Admin users have access to all databases by default, both Alpha and Gamma users need to be given access on a per database basis. | |
|
||||||
|can read on SavedQuery|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can read on SavedQuery | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can write on SavedQuery|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can write on SavedQuery | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can read on CssTemplate|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on CssTemplate | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on CssTemplate|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on CssTemplate | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on ReportSchedule|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on ReportSchedule | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on ReportSchedule|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on ReportSchedule | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on Chart|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on Chart | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on Chart|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on Chart | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on Annotation|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on Annotation | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on Annotation|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on Annotation | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on Dataset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on Dataset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can write on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can read on Log|:heavy_check_mark:|O|O|O|
|
| can read on Log | :heavy_check_mark: | O | O | O |
|
||||||
|can write on Log|:heavy_check_mark:|O|O|O|
|
| can write on Log | :heavy_check_mark: | O | O | O |
|
||||||
|can read on Dashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on Dashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on Dashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on Dashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on Database|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can read on Database | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can write on Database|:heavy_check_mark:|O|O|O|
|
| can write on Database | :heavy_check_mark: | O | O | O |
|
||||||
|can read on Query|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can read on Query | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can this form get on ResetPasswordView|:heavy_check_mark:|O|O|O|
|
| can this form get on ResetPasswordView | :heavy_check_mark: | O | O | O |
|
||||||
|can this form post on ResetPasswordView|:heavy_check_mark:|O|O|O|
|
| can this form post on ResetPasswordView | :heavy_check_mark: | O | O | O |
|
||||||
|can this form get on ResetMyPasswordView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form get on ResetMyPasswordView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form post on ResetMyPasswordView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form post on ResetMyPasswordView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form get on UserInfoEditView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form get on UserInfoEditView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form post on UserInfoEditView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form post on UserInfoEditView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| can show on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can edit on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| can edit on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can delete on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| can delete on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can add on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| can add on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can list on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| can list on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can userinfo on UserDBModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can userinfo on UserDBModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|resetmypassword on UserDBModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| resetmypassword on UserDBModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|resetpasswords on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| resetpasswords on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|userinfoedit on UserDBModelView|:heavy_check_mark:|O|O|O|
|
| userinfoedit on UserDBModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can show on RoleModelView|:heavy_check_mark:|O|O|O|
|
| can show on RoleModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can edit on RoleModelView|:heavy_check_mark:|O|O|O|
|
| can edit on RoleModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can delete on RoleModelView|:heavy_check_mark:|O|O|O|
|
| can delete on RoleModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can add on RoleModelView|:heavy_check_mark:|O|O|O|
|
| can add on RoleModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can list on RoleModelView|:heavy_check_mark:|O|O|O|
|
| can list on RoleModelView | :heavy_check_mark: | O | O | O |
|
||||||
|copyrole on RoleModelView|:heavy_check_mark:|O|O|O|
|
| copyrole on RoleModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can get on OpenApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get on OpenApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on SwaggerView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on SwaggerView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can get on MenuApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get on MenuApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on AsyncEventsRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on AsyncEventsRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can invalidate on CacheRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can invalidate on CacheRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can function names on Database|:heavy_check_mark:|O|O|O|
|
| can function names on Database | :heavy_check_mark: | O | O | O |
|
||||||
|can query form data on Api|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can query form data on Api | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can query on Api|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can query on Api | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can time range on Api|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can time range on Api | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form get on CsvToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form get on CsvToDatabaseView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form post on CsvToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form post on CsvToDatabaseView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form get on ExcelToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form get on ExcelToDatabaseView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form post on ExcelToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form post on ExcelToDatabaseView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can external metadata on Datasource|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can external metadata on Datasource | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can save on Datasource|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can save on Datasource | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can get on Datasource|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get on Datasource | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can shortner on R|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can shortner on R | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can my queries on SqlLab|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can my queries on SqlLab | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can log on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can log on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can schemas access for csv upload on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can schemas access for csv upload on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can import dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can import dashboards on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can schemas on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can schemas on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can sqllab history on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can sqllab history on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can publish on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can publish on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can csv on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can csv on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can slice on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can slice on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can sync druid source on Superset|:heavy_check_mark:|O|O|O|
|
| can sync druid source on Superset | :heavy_check_mark: | O | O | O |
|
||||||
|can explore on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can explore on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can approve on Superset|:heavy_check_mark:|O|O|O|
|
| can approve on Superset | :heavy_check_mark: | O | O | O |
|
||||||
|can explore json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can explore json on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can fetch datasource metadata on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can fetch datasource metadata on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can csrf token on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can csrf token on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can sqllab on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can sqllab on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can select star on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can select star on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can warm up cache on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can warm up cache on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can sqllab table viz on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can sqllab table viz on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can profile on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can profile on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can available domains on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can available domains on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can request access on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can request access on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can dashboard on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can dashboard on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can post on TableSchemaView|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can post on TableSchemaView | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can expanded on TableSchemaView|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can expanded on TableSchemaView | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can delete on TableSchemaView|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can delete on TableSchemaView | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can get on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can get on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can post on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can post on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can delete query on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can delete query on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can migrate query on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can migrate query on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can activate on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can activate on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can delete on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can delete on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can put on TabStateView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can put on TabStateView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can read on SecurityRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can read on SecurityRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|menu access on Security|:heavy_check_mark:|O|O|O|
|
| menu access on Security | :heavy_check_mark: | O | O | O |
|
||||||
|menu access on List Users|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on List Users | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on List Roles|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on List Roles | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Action Log|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Action Log | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Manage|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| menu access on Manage | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|menu access on Annotation Layers|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Annotation Layers | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on CSS Templates|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| menu access on CSS Templates | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|menu access on Import Dashboards|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Import Dashboards | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Data|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Data | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Databases|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Databases | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Datasets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Datasets | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Upload a CSV|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| menu access on Upload a CSV | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|menu access on Upload Excel|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Upload Excel | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Charts|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Charts | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Dashboards|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Dashboards | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on SQL Lab|:heavy_check_mark:|O|O|:heavy_check_mark:|
|
| menu access on SQL Lab | :heavy_check_mark: | O | O | :heavy_check_mark: |
|
||||||
|menu access on SQL Editor|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| menu access on SQL Editor | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|menu access on Saved Queries|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| menu access on Saved Queries | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|menu access on Query Search|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| menu access on Query Search | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|all datasource access on all_datasource_access|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| all datasource access on all_datasource_access | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|all database access on all_database_access|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| all database access on all_database_access | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|all query access on all_query_access|:heavy_check_mark:|O|O|O|
|
| all query access on all_query_access | :heavy_check_mark: | O | O | O |
|
||||||
|can edit on UserOAuthModelView|:heavy_check_mark:|O|O|O|
|
| can edit on UserOAuthModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can list on UserOAuthModelView|:heavy_check_mark:|O|O|O|
|
| can list on UserOAuthModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can show on UserOAuthModelView|:heavy_check_mark:|O|O|O|
|
| can show on UserOAuthModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can userinfo on UserOAuthModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can userinfo on UserOAuthModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can add on UserOAuthModelView|:heavy_check_mark:|O|O|O|
|
| can add on UserOAuthModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can delete on UserOAuthModelView|:heavy_check_mark:|O|O|O|
|
| can delete on UserOAuthModelView | :heavy_check_mark: | O | O | O |
|
||||||
|userinfoedit on UserOAuthModelView|:heavy_check_mark:|O|O|O|
|
| userinfoedit on UserOAuthModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can write on DynamicPlugin|:heavy_check_mark:|O|O|O|
|
| can write on DynamicPlugin | :heavy_check_mark: | O | O | O |
|
||||||
|can edit on DynamicPlugin|:heavy_check_mark:|O|O|O|
|
| can edit on DynamicPlugin | :heavy_check_mark: | O | O | O |
|
||||||
|can list on DynamicPlugin|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on DynamicPlugin | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on DynamicPlugin|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on DynamicPlugin | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can download on DynamicPlugin|:heavy_check_mark:|O|O|O|
|
| can download on DynamicPlugin | :heavy_check_mark: | O | O | O |
|
||||||
|can add on DynamicPlugin|:heavy_check_mark:|O|O|O|
|
| can add on DynamicPlugin | :heavy_check_mark: | O | O | O |
|
||||||
|can delete on DynamicPlugin|:heavy_check_mark:|O|O|O|
|
| can delete on DynamicPlugin | :heavy_check_mark: | O | O | O |
|
||||||
|can edit on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| can edit on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can list on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| can list on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can show on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| can show on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can download on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| can download on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can add on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| can add on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can delete on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| can delete on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|muldelete on RowLevelSecurityFiltersModelView|:heavy_check_mark:|O|O|O|
|
| muldelete on RowLevelSecurityFiltersModelView | :heavy_check_mark: | O | O | O |
|
||||||
|can external metadata by name on Datasource|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can external metadata by name on Datasource | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can get value on KV|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get value on KV | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can store on KV|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can store on KV | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can tagged objects on TagView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can tagged objects on TagView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can suggestions on TagView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can suggestions on TagView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can get on TagView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get on TagView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can post on TagView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can post on TagView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can delete on TagView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can delete on TagView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can edit on DashboardEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can edit on DashboardEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on DashboardEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on DashboardEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on DashboardEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on DashboardEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can add on DashboardEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can add on DashboardEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can delete on DashboardEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can delete on DashboardEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|muldelete on DashboardEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| muldelete on DashboardEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can edit on SliceEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can edit on SliceEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on SliceEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on SliceEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on SliceEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on SliceEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can add on SliceEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can add on SliceEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can delete on SliceEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can delete on SliceEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|muldelete on SliceEmailScheduleView|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| muldelete on SliceEmailScheduleView | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can edit on AlertModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can edit on AlertModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on AlertModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on AlertModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on AlertModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on AlertModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can add on AlertModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can add on AlertModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can delete on AlertModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can delete on AlertModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on AlertLogModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on AlertLogModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on AlertLogModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on AlertLogModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on AlertObservationModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can list on AlertObservationModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can show on AlertObservationModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can show on AlertObservationModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Row Level Security|:heavy_check_mark:|O|O|O|
|
| menu access on Row Level Security | :heavy_check_mark: | O | O | O |
|
||||||
|menu access on Access requests|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Access requests | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Home|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Home | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Plugins|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Plugins | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Dashboard Email Schedules|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Dashboard Email Schedules | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Chart Emails|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Chart Emails | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Alerts|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Alerts | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Alerts & Report|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Alerts & Report | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Scan New Datasources|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Scan New Datasources | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can share dashboard on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can share dashboard on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can share chart on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can share chart on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can list on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form get on ColumnarToDatabaseView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can add on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can this form post on ColumnarToDatabaseView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can delete on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| menu access on Upload a Columnar file | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can edit on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can export on Chart | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form get on ColumnarToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on DashboardFilterStateRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can this form post on ColumnarToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on DashboardFilterStateRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|menu access on Upload a Columnar file|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on DashboardPermalinkRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can export on Chart|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on DashboardPermalinkRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on DashboardFilterStateRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can delete embedded on Dashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on DashboardFilterStateRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can set embedded on Dashboard | :heavy_check_mark: | O | O | O |
|
||||||
|can write on DashboardPermalinkRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can export on Dashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on DashboardPermalinkRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get embedded on Dashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can delete embedded on Dashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can export on Database | :heavy_check_mark: | O | O | O |
|
||||||
|can set embedded on Dashboard|:heavy_check_mark:|O|O|O|
|
| can export on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can export on Dashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can write on ExploreFormDataRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can get embedded on Dashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on ExploreFormDataRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can export on Database|:heavy_check_mark:|O|O|O|
|
| can write on ExplorePermalinkRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can export on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can read on ExplorePermalinkRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on ExploreFormDataRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can export on ImportExportRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on ExploreFormDataRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can import on ImportExportRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can write on ExplorePermalinkRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can export on SavedQuery | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|can read on ExplorePermalinkRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can dashboard permalink on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can export on ImportExportRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can grant guest token on SecurityRestApi | :heavy_check_mark: | O | O | O |
|
||||||
|can import on ImportExportRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on AdvancedDataType | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can export on SavedQuery|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
| can read on EmbeddedDashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can dashboard permalink on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can duplicate on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can grant guest token on SecurityRestApi|:heavy_check_mark:|O|O|O|
|
| can read on Explore | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can read on AdvancedDataType|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can samples on Datasource | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can read on EmbeddedDashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can read on AvailableDomains | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can duplicate on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can get or create dataset on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can read on Explore|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get column values on Datasource | :heavy_check_mark: | :heavy_check_mark: | O | O |
|
||||||
|can samples on Datasource|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can export csv on SQLLab | :heavy_check_mark: | O | O | :heavy_check_mark: |
|
||||||
|can read on AvailableDomains|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
| can get results on SQLLab | :heavy_check_mark: | O | O | :heavy_check_mark: |
|
||||||
|can get or create dataset on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can execute sql query on SQLLab | :heavy_check_mark: | O | O | :heavy_check_mark: |
|
||||||
|can get column values on Datasource|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
| can recent activity on Log | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
|
||||||
|can export csv on SQLLab|:heavy_check_mark:|O|O|:heavy_check_mark:|
|
|
||||||
|can get results on SQLLab|:heavy_check_mark:|O|O|:heavy_check_mark:|
|
|
||||||
|can execute sql query on SQLLab|:heavy_check_mark:|O|O|:heavy_check_mark:|
|
|
||||||
|can recent activity on Log|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ assists people when migrating to a new version.
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [26369](https://github.com/apache/superset/issues/26369): Removes the Filter Sets feature including the deprecated `DASHBOARD_NATIVE_FILTERS_SET` feature flag and all related API endpoints. The feature is permanently removed as it was not being actively maintained, it was not widely used, and it was full of bugs. We also considered that if we were to provide a similar feature, it would be better to re-implement it from scratch given the amount of technical debt that the current implementation has. The previous value of the feature flag was `False` and now the feature is permanently removed.
|
||||||
- [26343](https://github.com/apache/superset/issues/26343): Removes the deprecated `ENABLE_EXPLORE_DRAG_AND_DROP` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled.
|
- [26343](https://github.com/apache/superset/issues/26343): Removes the deprecated `ENABLE_EXPLORE_DRAG_AND_DROP` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled.
|
||||||
- [26331](https://github.com/apache/superset/issues/26331): Removes the deprecated `DISABLE_DATASET_SOURCE_EDIT` feature flag. The previous value of the feature flag was `False` and now the feature is permanently removed.
|
- [26331](https://github.com/apache/superset/issues/26331): Removes the deprecated `DISABLE_DATASET_SOURCE_EDIT` feature flag. The previous value of the feature flag was `False` and now the feature is permanently removed.
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -54,17 +54,6 @@ export type DataMaskState = { [id: string]: DataMask };
|
||||||
export type DataMaskWithId = { id: string } & DataMask;
|
export type DataMaskWithId = { id: string } & DataMask;
|
||||||
export type DataMaskStateWithId = { [filterId: string]: DataMaskWithId };
|
export type DataMaskStateWithId = { [filterId: string]: DataMaskWithId };
|
||||||
|
|
||||||
export type FilterSet = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
nativeFilters: Filters;
|
|
||||||
dataMask: DataMaskStateWithId;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FilterSets = {
|
|
||||||
[filtersSetId: string]: FilterSet;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Filter = {
|
export type Filter = {
|
||||||
cascadeParentIds: string[];
|
cascadeParentIds: string[];
|
||||||
defaultDataMask: DataMask;
|
defaultDataMask: DataMask;
|
||||||
|
@ -133,7 +122,6 @@ export type PartialFilters = {
|
||||||
|
|
||||||
export type NativeFiltersState = {
|
export type NativeFiltersState = {
|
||||||
filters: Filters;
|
filters: Filters;
|
||||||
filterSets: FilterSets;
|
|
||||||
focusedFilterId?: string;
|
focusedFilterId?: string;
|
||||||
hoveredFilterId?: string;
|
hoveredFilterId?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,7 +30,6 @@ export enum FeatureFlag {
|
||||||
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
|
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
|
||||||
CONFIRM_DASHBOARD_DIFF = 'CONFIRM_DASHBOARD_DIFF',
|
CONFIRM_DASHBOARD_DIFF = 'CONFIRM_DASHBOARD_DIFF',
|
||||||
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
|
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
|
||||||
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
|
|
||||||
DASHBOARD_VIRTUALIZATION = 'DASHBOARD_VIRTUALIZATION',
|
DASHBOARD_VIRTUALIZATION = 'DASHBOARD_VIRTUALIZATION',
|
||||||
DASHBOARD_RBAC = 'DASHBOARD_RBAC',
|
DASHBOARD_RBAC = 'DASHBOARD_RBAC',
|
||||||
DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
|
|
||||||
export const nativeFilters: NativeFiltersState = {
|
export const nativeFilters: NativeFiltersState = {
|
||||||
filterSets: {},
|
|
||||||
filters: {
|
filters: {
|
||||||
'NATIVE_FILTER-e7Q8zKixx': {
|
'NATIVE_FILTER-e7Q8zKixx': {
|
||||||
id: 'NATIVE_FILTER-e7Q8zKixx',
|
id: 'NATIVE_FILTER-e7Q8zKixx',
|
||||||
|
|
|
@ -166,5 +166,5 @@ export const stateWithoutNativeFilters = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dataMask: {},
|
dataMask: {},
|
||||||
nativeFilters: { filters: {}, filterSets: {} },
|
nativeFilters: { filters: {} },
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,10 +47,6 @@ export const URL_PARAMS = {
|
||||||
name: 'native_filters_key',
|
name: 'native_filters_key',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
filterSet: {
|
|
||||||
name: 'filter_set',
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
showFilters: {
|
showFilters: {
|
||||||
name: 'show_filters',
|
name: 'show_filters',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
@ -16,14 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import { FilterConfiguration, Filters, makeApi } from '@superset-ui/core';
|
||||||
import {
|
|
||||||
FilterConfiguration,
|
|
||||||
Filters,
|
|
||||||
FilterSet,
|
|
||||||
FilterSets,
|
|
||||||
makeApi,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import {
|
import {
|
||||||
|
@ -32,8 +25,7 @@ import {
|
||||||
} from 'src/dataMask/actions';
|
} from 'src/dataMask/actions';
|
||||||
import { HYDRATE_DASHBOARD } from './hydrate';
|
import { HYDRATE_DASHBOARD } from './hydrate';
|
||||||
import { dashboardInfoChanged } from './dashboardInfo';
|
import { dashboardInfoChanged } from './dashboardInfo';
|
||||||
import { FilterSetFullData } from '../reducers/types';
|
import { DashboardInfo } from '../types';
|
||||||
import { DashboardInfo, RootState } from '../types';
|
|
||||||
|
|
||||||
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
|
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
|
||||||
export interface SetFilterConfigBegin {
|
export interface SetFilterConfigBegin {
|
||||||
|
@ -56,61 +48,6 @@ export interface SetInScopeStatusOfFilters {
|
||||||
type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS;
|
type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS;
|
||||||
filterConfig: FilterConfiguration;
|
filterConfig: FilterConfiguration;
|
||||||
}
|
}
|
||||||
export const SET_FILTER_SETS_BEGIN = 'SET_FILTER_SETS_BEGIN';
|
|
||||||
export interface SetFilterSetsBegin {
|
|
||||||
type: typeof SET_FILTER_SETS_BEGIN;
|
|
||||||
}
|
|
||||||
export const SET_FILTER_SETS_COMPLETE = 'SET_FILTER_SETS_COMPLETE';
|
|
||||||
export interface SetFilterSetsComplete {
|
|
||||||
type: typeof SET_FILTER_SETS_COMPLETE;
|
|
||||||
filterSets: FilterSet[];
|
|
||||||
}
|
|
||||||
export const SET_FILTER_SETS_FAIL = 'SET_FILTER_SETS_FAIL';
|
|
||||||
export interface SetFilterSetsFail {
|
|
||||||
type: typeof SET_FILTER_SETS_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CREATE_FILTER_SET_BEGIN = 'CREATE_FILTER_SET_BEGIN';
|
|
||||||
export interface CreateFilterSetBegin {
|
|
||||||
type: typeof CREATE_FILTER_SET_BEGIN;
|
|
||||||
}
|
|
||||||
export const CREATE_FILTER_SET_COMPLETE = 'CREATE_FILTER_SET_COMPLETE';
|
|
||||||
export interface CreateFilterSetComplete {
|
|
||||||
type: typeof CREATE_FILTER_SET_COMPLETE;
|
|
||||||
filterSet: FilterSet;
|
|
||||||
}
|
|
||||||
export const CREATE_FILTER_SET_FAIL = 'CREATE_FILTER_SET_FAIL';
|
|
||||||
export interface CreateFilterSetFail {
|
|
||||||
type: typeof CREATE_FILTER_SET_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DELETE_FILTER_SET_BEGIN = 'DELETE_FILTER_SET_BEGIN';
|
|
||||||
export interface DeleteFilterSetBegin {
|
|
||||||
type: typeof DELETE_FILTER_SET_BEGIN;
|
|
||||||
}
|
|
||||||
export const DELETE_FILTER_SET_COMPLETE = 'DELETE_FILTER_SET_COMPLETE';
|
|
||||||
export interface DeleteFilterSetComplete {
|
|
||||||
type: typeof DELETE_FILTER_SET_COMPLETE;
|
|
||||||
filterSet: FilterSet;
|
|
||||||
}
|
|
||||||
export const DELETE_FILTER_SET_FAIL = 'DELETE_FILTER_SET_FAIL';
|
|
||||||
export interface DeleteFilterSetFail {
|
|
||||||
type: typeof DELETE_FILTER_SET_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UPDATE_FILTER_SET_BEGIN = 'UPDATE_FILTER_SET_BEGIN';
|
|
||||||
export interface UpdateFilterSetBegin {
|
|
||||||
type: typeof UPDATE_FILTER_SET_BEGIN;
|
|
||||||
}
|
|
||||||
export const UPDATE_FILTER_SET_COMPLETE = 'UPDATE_FILTER_SET_COMPLETE';
|
|
||||||
export interface UpdateFilterSetComplete {
|
|
||||||
type: typeof UPDATE_FILTER_SET_COMPLETE;
|
|
||||||
filterSet: FilterSet;
|
|
||||||
}
|
|
||||||
export const UPDATE_FILTER_SET_FAIL = 'UPDATE_FILTER_SET_FAIL';
|
|
||||||
export interface UpdateFilterSetFail {
|
|
||||||
type: typeof UPDATE_FILTER_SET_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setFilterConfiguration =
|
export const setFilterConfiguration =
|
||||||
(filterConfig: FilterConfiguration) =>
|
(filterConfig: FilterConfiguration) =>
|
||||||
|
@ -213,7 +150,6 @@ export const setInScopeStatusOfFilters =
|
||||||
type BootstrapData = {
|
type BootstrapData = {
|
||||||
nativeFilters: {
|
nativeFilters: {
|
||||||
filters: Filters;
|
filters: Filters;
|
||||||
filterSets: FilterSets;
|
|
||||||
filtersState: object;
|
filtersState: object;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -223,134 +159,6 @@ export interface SetBootstrapData {
|
||||||
data: BootstrapData;
|
data: BootstrapData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFilterSets =
|
|
||||||
(dashboardId: number) => async (dispatch: Dispatch) => {
|
|
||||||
const fetchFilterSets = makeApi<
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
count: number;
|
|
||||||
ids: number[];
|
|
||||||
result: FilterSetFullData[];
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
method: 'GET',
|
|
||||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: SET_FILTER_SETS_BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetchFilterSets(null);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: SET_FILTER_SETS_COMPLETE,
|
|
||||||
filterSets: response.ids.map((id, i) => ({
|
|
||||||
...response.result[i].params,
|
|
||||||
id,
|
|
||||||
name: response.result[i].name,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createFilterSet =
|
|
||||||
(filterSet: Omit<FilterSet, 'id'>) =>
|
|
||||||
async (dispatch: Function, getState: () => RootState) => {
|
|
||||||
const dashboardId = getState().dashboardInfo.id;
|
|
||||||
const postFilterSets = makeApi<
|
|
||||||
Partial<FilterSetFullData & { json_metadata: any }>,
|
|
||||||
{
|
|
||||||
count: number;
|
|
||||||
ids: number[];
|
|
||||||
result: FilterSetFullData[];
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
method: 'POST',
|
|
||||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: CREATE_FILTER_SET_BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & { name?: string } =
|
|
||||||
{
|
|
||||||
...filterSet,
|
|
||||||
};
|
|
||||||
|
|
||||||
delete serverFilterSet.name;
|
|
||||||
|
|
||||||
await postFilterSets({
|
|
||||||
name: filterSet.name,
|
|
||||||
owner_type: 'Dashboard',
|
|
||||||
owner_id: dashboardId,
|
|
||||||
json_metadata: JSON.stringify(serverFilterSet),
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: CREATE_FILTER_SET_COMPLETE,
|
|
||||||
});
|
|
||||||
dispatch(getFilterSets(dashboardId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateFilterSet =
|
|
||||||
(filterSet: FilterSet) =>
|
|
||||||
async (dispatch: Function, getState: () => RootState) => {
|
|
||||||
const dashboardId = getState().dashboardInfo.id;
|
|
||||||
const postFilterSets = makeApi<
|
|
||||||
Partial<FilterSetFullData & { json_metadata: any }>,
|
|
||||||
{}
|
|
||||||
>({
|
|
||||||
method: 'PUT',
|
|
||||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSet.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_FILTER_SET_BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & {
|
|
||||||
name?: string;
|
|
||||||
id?: number;
|
|
||||||
} = {
|
|
||||||
...filterSet,
|
|
||||||
};
|
|
||||||
|
|
||||||
delete serverFilterSet.id;
|
|
||||||
delete serverFilterSet.name;
|
|
||||||
|
|
||||||
await postFilterSets({
|
|
||||||
name: filterSet.name,
|
|
||||||
json_metadata: JSON.stringify(serverFilterSet),
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_FILTER_SET_COMPLETE,
|
|
||||||
});
|
|
||||||
dispatch(getFilterSets(dashboardId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteFilterSet =
|
|
||||||
(filterSetId: number) =>
|
|
||||||
async (dispatch: Function, getState: () => RootState) => {
|
|
||||||
const dashboardId = getState().dashboardInfo.id;
|
|
||||||
const deleteFilterSets = makeApi<{}, {}>({
|
|
||||||
method: 'DELETE',
|
|
||||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSetId}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: DELETE_FILTER_SET_BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
await deleteFilterSets({});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: DELETE_FILTER_SET_COMPLETE,
|
|
||||||
});
|
|
||||||
dispatch(getFilterSets(dashboardId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SET_FOCUSED_NATIVE_FILTER = 'SET_FOCUSED_NATIVE_FILTER';
|
export const SET_FOCUSED_NATIVE_FILTER = 'SET_FOCUSED_NATIVE_FILTER';
|
||||||
export interface SetFocusedNativeFilter {
|
export interface SetFocusedNativeFilter {
|
||||||
type: typeof SET_FOCUSED_NATIVE_FILTER;
|
type: typeof SET_FOCUSED_NATIVE_FILTER;
|
||||||
|
@ -416,22 +224,10 @@ export type AnyFilterAction =
|
||||||
| SetFilterConfigBegin
|
| SetFilterConfigBegin
|
||||||
| SetFilterConfigComplete
|
| SetFilterConfigComplete
|
||||||
| SetFilterConfigFail
|
| SetFilterConfigFail
|
||||||
| SetFilterSetsBegin
|
|
||||||
| SetFilterSetsComplete
|
|
||||||
| SetFilterSetsFail
|
|
||||||
| SetInScopeStatusOfFilters
|
| SetInScopeStatusOfFilters
|
||||||
| SetBootstrapData
|
| SetBootstrapData
|
||||||
| SetFocusedNativeFilter
|
| SetFocusedNativeFilter
|
||||||
| UnsetFocusedNativeFilter
|
| UnsetFocusedNativeFilter
|
||||||
| SetHoveredNativeFilter
|
| SetHoveredNativeFilter
|
||||||
| UnsetHoveredNativeFilter
|
| UnsetHoveredNativeFilter
|
||||||
| CreateFilterSetBegin
|
|
||||||
| CreateFilterSetComplete
|
|
||||||
| CreateFilterSetFail
|
|
||||||
| DeleteFilterSetBegin
|
|
||||||
| DeleteFilterSetComplete
|
|
||||||
| DeleteFilterSetFail
|
|
||||||
| UpdateFilterSetBegin
|
|
||||||
| UpdateFilterSetComplete
|
|
||||||
| UpdateFilterSetFail
|
|
||||||
| UpdateCascadeParentIds;
|
| UpdateCascadeParentIds;
|
||||||
|
|
|
@ -78,7 +78,6 @@ import {
|
||||||
BUILDER_SIDEPANEL_WIDTH,
|
BUILDER_SIDEPANEL_WIDTH,
|
||||||
CLOSED_FILTER_BAR_WIDTH,
|
CLOSED_FILTER_BAR_WIDTH,
|
||||||
FILTER_BAR_HEADER_HEIGHT,
|
FILTER_BAR_HEADER_HEIGHT,
|
||||||
FILTER_BAR_TABS_HEIGHT,
|
|
||||||
MAIN_HEADER_HEIGHT,
|
MAIN_HEADER_HEIGHT,
|
||||||
OPEN_FILTER_BAR_MAX_WIDTH,
|
OPEN_FILTER_BAR_MAX_WIDTH,
|
||||||
OPEN_FILTER_BAR_WIDTH,
|
OPEN_FILTER_BAR_WIDTH,
|
||||||
|
@ -463,18 +462,12 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
|
||||||
threshold: [1],
|
threshold: [1],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter sets depend on native filters
|
|
||||||
const filterSetEnabled =
|
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) &&
|
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS);
|
|
||||||
|
|
||||||
const showFilterBar =
|
const showFilterBar =
|
||||||
(crossFiltersEnabled || nativeFiltersEnabled) && !editMode;
|
(crossFiltersEnabled || nativeFiltersEnabled) && !editMode;
|
||||||
|
|
||||||
const offset =
|
const offset =
|
||||||
FILTER_BAR_HEADER_HEIGHT +
|
FILTER_BAR_HEADER_HEIGHT +
|
||||||
(isSticky || standaloneMode ? 0 : MAIN_HEADER_HEIGHT) +
|
(isSticky || standaloneMode ? 0 : MAIN_HEADER_HEIGHT);
|
||||||
(filterSetEnabled ? FILTER_BAR_TABS_HEIGHT : 0);
|
|
||||||
|
|
||||||
const filterBarHeight = `calc(100vh - ${offset}px)`;
|
const filterBarHeight = `calc(100vh - ${offset}px)`;
|
||||||
const filterBarOffset = dashboardFiltersOpen ? 0 : barTopOffset + 20;
|
const filterBarOffset = dashboardFiltersOpen ? 0 : barTopOffset + 20;
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css } from '@superset-ui/core';
|
import { css } from '@superset-ui/core';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { getFilterValueForDisplay } from 'src/dashboard/components/nativeFilters/FilterBar/FilterSets/utils';
|
import { getFilterValueForDisplay } from 'src/dashboard/components/nativeFilters/utils';
|
||||||
import {
|
import {
|
||||||
FilterValue,
|
FilterValue,
|
||||||
FilterItem,
|
FilterItem,
|
||||||
|
|
|
@ -25,9 +25,7 @@ import * as mockCore from '@superset-ui/core';
|
||||||
import { testWithId } from 'src/utils/testUtils';
|
import { testWithId } from 'src/utils/testUtils';
|
||||||
import { FeatureFlag, Preset } from '@superset-ui/core';
|
import { FeatureFlag, Preset } from '@superset-ui/core';
|
||||||
import { TimeFilterPlugin, SelectFilterPlugin } from 'src/filters/components';
|
import { TimeFilterPlugin, SelectFilterPlugin } from 'src/filters/components';
|
||||||
import { DATE_FILTER_TEST_KEY } from 'src/explore/components/controls/DateFilterControl';
|
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { waitFor } from '@testing-library/react';
|
|
||||||
import { FilterBarOrientation } from 'src/dashboard/types';
|
import { FilterBarOrientation } from 'src/dashboard/types';
|
||||||
import { FILTER_BAR_TEST_ID } from './utils';
|
import { FILTER_BAR_TEST_ID } from './utils';
|
||||||
import FilterBar from '.';
|
import FilterBar from '.';
|
||||||
|
@ -74,12 +72,10 @@ const getTestId = testWithId<string>(FILTER_BAR_TEST_ID, true);
|
||||||
const getModalTestId = testWithId<string>(FILTERS_CONFIG_MODAL_TEST_ID, true);
|
const getModalTestId = testWithId<string>(FILTERS_CONFIG_MODAL_TEST_ID, true);
|
||||||
|
|
||||||
const FILTER_NAME = 'Time filter 1';
|
const FILTER_NAME = 'Time filter 1';
|
||||||
const FILTER_SET_NAME = 'New filter set';
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
|
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFilterFlow = async () => {
|
const addFilterFlow = async () => {
|
||||||
|
@ -95,38 +91,6 @@ const addFilterFlow = async () => {
|
||||||
// await screen.findByText('All filters (1)');
|
// await screen.findByText('All filters (1)');
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFilterSetFlow = async () => {
|
|
||||||
// add filter set
|
|
||||||
userEvent.click(screen.getByText('Filter sets (0)'));
|
|
||||||
|
|
||||||
// check description
|
|
||||||
expect(screen.getByText('Filters (1)')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(FILTER_NAME)).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(screen.getAllByText('No filter').length).toBe(1);
|
|
||||||
|
|
||||||
// apply filters
|
|
||||||
expect(screen.getByTestId(getTestId('new-filter-set-button'))).toBeEnabled();
|
|
||||||
|
|
||||||
// create filter set
|
|
||||||
userEvent.click(screen.getByText('Create new filter set'));
|
|
||||||
userEvent.click(screen.getByText('Create'));
|
|
||||||
|
|
||||||
// check filter set created
|
|
||||||
expect(await screen.findByRole('img', { name: 'check' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId(getTestId('filter-set-wrapper'))).toHaveAttribute(
|
|
||||||
'data-selected',
|
|
||||||
'true',
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeFilterValue = async () => {
|
|
||||||
userEvent.click(screen.getAllByText('No filter')[0]);
|
|
||||||
userEvent.click(screen.getByDisplayValue('Last day'));
|
|
||||||
expect(await screen.findByText(/2021-04-13/)).toBeInTheDocument();
|
|
||||||
userEvent.click(screen.getByTestId(DATE_FILTER_TEST_KEY.applyButton));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('FilterBar', () => {
|
describe('FilterBar', () => {
|
||||||
new MainPreset().register();
|
new MainPreset().register();
|
||||||
const toggleFiltersBar = jest.fn();
|
const toggleFiltersBar = jest.fn();
|
||||||
|
@ -157,30 +121,6 @@ describe('FilterBar', () => {
|
||||||
"cascadeParentIds":[],
|
"cascadeParentIds":[],
|
||||||
"scope":{"rootPath":["ROOT_ID"],"excluded":[]}
|
"scope":{"rootPath":["ROOT_ID"],"excluded":[]}
|
||||||
}],
|
}],
|
||||||
"filter_sets_configuration":[{
|
|
||||||
"name":"${FILTER_SET_NAME}",
|
|
||||||
"id":"${json.filter_sets_configuration?.[0].id}",
|
|
||||||
"nativeFilters":{
|
|
||||||
"${filterId}":{
|
|
||||||
"id":"${filterId}",
|
|
||||||
"name":"${FILTER_NAME}",
|
|
||||||
"filterType":"filter_time",
|
|
||||||
"targets":[{}],
|
|
||||||
"defaultDataMask":{"filterState":{},"extraFormData":{}},
|
|
||||||
"controlValues":{},
|
|
||||||
"cascadeParentIds":[],
|
|
||||||
"scope":{"rootPath":["ROOT_ID"],"excluded":[]}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dataMask":{
|
|
||||||
"${filterId}":{
|
|
||||||
"extraFormData":{},
|
|
||||||
"filterState":{},
|
|
||||||
"ownState":{},
|
|
||||||
"id":"${filterId}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -353,7 +293,6 @@ describe('FilterBar', () => {
|
||||||
it('create filter and apply it flow', async () => {
|
it('create filter and apply it flow', async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
|
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
|
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
|
||||||
};
|
};
|
||||||
renderWrapper(openedBarProps, stateWithoutNativeFilters);
|
renderWrapper(openedBarProps, stateWithoutNativeFilters);
|
||||||
|
@ -363,88 +302,4 @@ describe('FilterBar', () => {
|
||||||
|
|
||||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// disable due to filter sets not detecting changes in metadata properly
|
|
||||||
it.skip('add and apply filter set', async () => {
|
|
||||||
// @ts-ignore
|
|
||||||
global.featureFlags = {
|
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
|
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
|
|
||||||
};
|
|
||||||
renderWrapper(openedBarProps, stateWithoutNativeFilters);
|
|
||||||
|
|
||||||
await addFilterFlow();
|
|
||||||
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
|
||||||
|
|
||||||
await addFilterSetFlow();
|
|
||||||
|
|
||||||
// change filter
|
|
||||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
|
||||||
userEvent.click(await screen.findByText('All filters (1)'));
|
|
||||||
await changeFilterValue();
|
|
||||||
await waitFor(() => expect(screen.getAllByText('Last day').length).toBe(2));
|
|
||||||
|
|
||||||
// apply new filter value
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// applying filter set
|
|
||||||
userEvent.click(screen.getByText('Filter Sets (1)'));
|
|
||||||
expect(
|
|
||||||
await screen.findByText('Create new filter set'),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByTestId(getTestId('filter-set-wrapper')),
|
|
||||||
).not.toHaveAttribute('data-selected', 'true');
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('filter-set-wrapper')));
|
|
||||||
userEvent.click(screen.getAllByText('Filters (1)')[1]);
|
|
||||||
expect(await screen.findByText('No filter')).toBeInTheDocument();
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
|
||||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
// disable due to filter sets not detecting changes in metadata properly
|
|
||||||
it.skip('add and edit filter set', async () => {
|
|
||||||
// @ts-ignore
|
|
||||||
global.featureFlags = {
|
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
|
|
||||||
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
|
|
||||||
};
|
|
||||||
renderWrapper(openedBarProps, stateWithoutNativeFilters);
|
|
||||||
|
|
||||||
await addFilterFlow();
|
|
||||||
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
|
||||||
|
|
||||||
await addFilterSetFlow();
|
|
||||||
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('filter-set-menu-button')));
|
|
||||||
userEvent.click(screen.getByText('Edit'));
|
|
||||||
|
|
||||||
await changeFilterValue();
|
|
||||||
|
|
||||||
// apply new changes and save them
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(
|
|
||||||
screen.getByTestId(getTestId('filter-set-edit-save')),
|
|
||||||
).toBeDisabled(),
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
|
||||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
|
||||||
|
|
||||||
expect(screen.getByTestId(getTestId('filter-set-edit-save'))).toBeEnabled();
|
|
||||||
userEvent.click(screen.getByTestId(getTestId('filter-set-edit-save')));
|
|
||||||
expect(screen.queryByText('Save')).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
Object.values(
|
|
||||||
JSON.parse(mockApi.mock.calls[2][0].json_metadata)
|
|
||||||
.filter_sets_configuration[0].dataMask as object,
|
|
||||||
)[0]?.filterState?.value,
|
|
||||||
).toBe('Last day');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { mockStore } from 'spec/fixtures/mockStore';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import EditSection, { EditSectionProps } from './EditSection';
|
|
||||||
|
|
||||||
const createProps = () => ({
|
|
||||||
filterSetId: 1,
|
|
||||||
dataMaskSelected: {
|
|
||||||
DefaultsID: {
|
|
||||||
filterState: {
|
|
||||||
value: 'value',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCancel: jest.fn(),
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setup = (props: EditSectionProps) => (
|
|
||||||
<Provider store={mockStore}>
|
|
||||||
<EditSection {...props} />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
test('should render', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const { container } = render(setup(mockedProps));
|
|
||||||
expect(container).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the title', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Editing filter set:')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the set name', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Set name')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render a textbox', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should change the set name', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
const textbox = screen.getByRole('textbox');
|
|
||||||
userEvent.clear(textbox);
|
|
||||||
userEvent.type(textbox, 'New name');
|
|
||||||
expect(textbox).toHaveValue('New name');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the enter icon', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByRole('img', { name: 'enter' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the Cancel button', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should cancel', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
const cancelBtn = screen.getByText('Cancel');
|
|
||||||
expect(mockedProps.onCancel).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(cancelBtn);
|
|
||||||
expect(mockedProps.onCancel).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the Save button', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the Save button as disabled', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const saveDisabledProps = {
|
|
||||||
...mockedProps,
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
render(setup(saveDisabledProps));
|
|
||||||
expect(screen.getByText('Save').parentElement).toBeDisabled();
|
|
||||||
});
|
|
|
@ -1,171 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React, { FC, useMemo, useState } from 'react';
|
|
||||||
import { DataMaskState, HandlerFunction, styled, t } from '@superset-ui/core';
|
|
||||||
import { Typography, AntdTooltip } from 'src/components';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import Button from 'src/components/Button';
|
|
||||||
import { updateFilterSet } from 'src/dashboard/actions/nativeFilters';
|
|
||||||
import Icons from 'src/components/Icons';
|
|
||||||
import { ActionButtons } from './Footer';
|
|
||||||
import { useNativeFiltersDataMask, useFilters, useFilterSets } from '../state';
|
|
||||||
import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils';
|
|
||||||
import { useFilterSetNameDuplicated } from './state';
|
|
||||||
import { getFilterBarTestId } from '../utils';
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
grid-gap: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
background: ${({ theme }) => theme.colors.primary.light4};
|
|
||||||
padding: ${({ theme }) => theme.gridUnit * 2}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled(Typography.Text)`
|
|
||||||
color: ${({ theme }) => theme.colors.primary.dark2};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Warning = styled(Typography.Text)`
|
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
|
||||||
& .anticon {
|
|
||||||
padding: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ActionButton = styled.div<{ disabled?: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
& button {
|
|
||||||
${({ disabled }) => `pointer-events: ${disabled ? 'none' : 'all'}`};
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type EditSectionProps = {
|
|
||||||
filterSetId: number;
|
|
||||||
dataMaskSelected: DataMaskState;
|
|
||||||
onCancel: HandlerFunction;
|
|
||||||
disabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditSection: FC<EditSectionProps> = ({
|
|
||||||
filterSetId,
|
|
||||||
onCancel,
|
|
||||||
dataMaskSelected,
|
|
||||||
disabled,
|
|
||||||
}) => {
|
|
||||||
const dataMaskApplied = useNativeFiltersDataMask();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const filterSets = useFilterSets();
|
|
||||||
const filters = useFilters();
|
|
||||||
const filterSetFilterValues = Object.values(filterSets);
|
|
||||||
|
|
||||||
const [filterSetName, setFilterSetName] = useState(
|
|
||||||
filterSets[filterSetId].name,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isFilterSetNameDuplicated = useFilterSetNameDuplicated(
|
|
||||||
filterSetName,
|
|
||||||
filterSets[filterSetId].name,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
dispatch(
|
|
||||||
updateFilterSet({
|
|
||||||
id: filterSetId,
|
|
||||||
name: filterSetName,
|
|
||||||
nativeFilters: filters,
|
|
||||||
dataMask: { ...dataMaskApplied },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const foundFilterSet = useMemo(
|
|
||||||
() =>
|
|
||||||
findExistingFilterSet({
|
|
||||||
dataMaskSelected,
|
|
||||||
filterSetFilterValues,
|
|
||||||
}),
|
|
||||||
[dataMaskSelected, filterSetFilterValues],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isDuplicateFilterSet =
|
|
||||||
foundFilterSet && foundFilterSet.id !== filterSetId;
|
|
||||||
|
|
||||||
const resultDisabled =
|
|
||||||
disabled || isDuplicateFilterSet || isFilterSetNameDuplicated;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Title strong>{t('Editing filter set:')}</Title>
|
|
||||||
<Title
|
|
||||||
editable={{
|
|
||||||
editing: true,
|
|
||||||
icon: <span />,
|
|
||||||
onChange: setFilterSetName,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filterSetName}
|
|
||||||
</Title>
|
|
||||||
<ActionButtons>
|
|
||||||
<Button
|
|
||||||
ghost
|
|
||||||
buttonStyle="tertiary"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={onCancel}
|
|
||||||
data-test="filter-set-edit-cancel"
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<AntdTooltip
|
|
||||||
placement="right"
|
|
||||||
title={
|
|
||||||
(isFilterSetNameDuplicated &&
|
|
||||||
t('Filter set with this name already exists')) ||
|
|
||||||
(isDuplicateFilterSet && t('Filter set already exists')) ||
|
|
||||||
(disabled && APPLY_FILTERS_HINT)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ActionButton disabled={resultDisabled}>
|
|
||||||
<Button
|
|
||||||
disabled={resultDisabled}
|
|
||||||
buttonStyle="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={handleSave}
|
|
||||||
{...getFilterBarTestId('filter-set-edit-save')}
|
|
||||||
>
|
|
||||||
{t('Save')}
|
|
||||||
</Button>
|
|
||||||
</ActionButton>
|
|
||||||
</AntdTooltip>
|
|
||||||
</ActionButtons>
|
|
||||||
{isDuplicateFilterSet && (
|
|
||||||
<Warning mark>
|
|
||||||
<Icons.WarningOutlined iconSize="m" />
|
|
||||||
{t('This filter set is identical to: "%s"', foundFilterSet?.name)}
|
|
||||||
</Warning>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditSection;
|
|
|
@ -1,100 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
|
||||||
import { mockStore } from 'spec/fixtures/mockStore';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import FilterSetUnit, { FilterSetUnitProps } from './FilterSetUnit';
|
|
||||||
|
|
||||||
const createProps = () => ({
|
|
||||||
editMode: true,
|
|
||||||
setFilterSetName: jest.fn(),
|
|
||||||
onDelete: jest.fn(),
|
|
||||||
onEdit: jest.fn(),
|
|
||||||
onRebuild: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
function openDropdown() {
|
|
||||||
const dropdownIcon = screen.getAllByRole('img', { name: '' })[0];
|
|
||||||
userEvent.click(dropdownIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = (props: FilterSetUnitProps) => (
|
|
||||||
<Provider store={mockStore}>
|
|
||||||
<FilterSetUnit {...props} />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
test('should render', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const { container } = render(setup(mockedProps));
|
|
||||||
expect(container).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the edit button', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const editModeOffProps = {
|
|
||||||
...mockedProps,
|
|
||||||
editMode: false,
|
|
||||||
};
|
|
||||||
render(setup(editModeOffProps));
|
|
||||||
expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the menu', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
openDropdown();
|
|
||||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
||||||
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
|
|
||||||
expect(screen.getByText('Edit')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Rebuild')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Delete')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should edit', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
openDropdown();
|
|
||||||
const editBtn = screen.getByText('Edit');
|
|
||||||
expect(mockedProps.onEdit).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(editBtn);
|
|
||||||
expect(mockedProps.onEdit).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should delete', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
openDropdown();
|
|
||||||
const deleteBtn = screen.getByText('Delete');
|
|
||||||
expect(mockedProps.onDelete).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(deleteBtn);
|
|
||||||
expect(mockedProps.onDelete).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should rebuild', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
openDropdown();
|
|
||||||
const rebuildBtn = screen.getByText('Rebuild');
|
|
||||||
expect(mockedProps.onRebuild).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(rebuildBtn);
|
|
||||||
expect(mockedProps.onRebuild).toHaveBeenCalled();
|
|
||||||
});
|
|
|
@ -1,144 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { AntdDropdown, Typography } from 'src/components';
|
|
||||||
import { Menu } from 'src/components/Menu';
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
import {
|
|
||||||
DataMaskState,
|
|
||||||
FilterSet,
|
|
||||||
HandlerFunction,
|
|
||||||
styled,
|
|
||||||
useTheme,
|
|
||||||
t,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import Icons from 'src/components/Icons';
|
|
||||||
import Button from 'src/components/Button';
|
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
|
||||||
import FiltersHeader from './FiltersHeader';
|
|
||||||
import { getFilterBarTestId } from '../utils';
|
|
||||||
|
|
||||||
const HeaderButton = styled(Button)`
|
|
||||||
padding: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TitleText = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const IconsBlock = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
& > *,
|
|
||||||
& > button.superset-button {
|
|
||||||
${({ theme }) => `margin-left: ${theme.gridUnit * 2}px`};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type FilterSetUnitProps = {
|
|
||||||
editMode?: boolean;
|
|
||||||
isApplied?: boolean;
|
|
||||||
filterSet?: FilterSet;
|
|
||||||
filterSetName?: string;
|
|
||||||
dataMaskSelected?: DataMaskState;
|
|
||||||
setFilterSetName?: (name: string) => void;
|
|
||||||
onDelete?: HandlerFunction;
|
|
||||||
onEdit?: HandlerFunction;
|
|
||||||
onRebuild?: HandlerFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
|
||||||
editMode,
|
|
||||||
setFilterSetName,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
filterSetName,
|
|
||||||
dataMaskSelected,
|
|
||||||
filterSet,
|
|
||||||
isApplied,
|
|
||||||
onRebuild,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const menu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item onClick={onEdit}>{t('Edit')}</Menu.Item>
|
|
||||||
<Menu.Item onClick={onRebuild}>
|
|
||||||
<Tooltip placement="right" title={t('Remove invalid filters')}>
|
|
||||||
{t('Rebuild')}
|
|
||||||
</Tooltip>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item onClick={onDelete} danger>
|
|
||||||
{t('Delete')}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TitleText>
|
|
||||||
<Typography.Text
|
|
||||||
strong
|
|
||||||
editable={{
|
|
||||||
editing: editMode,
|
|
||||||
icon: <span />,
|
|
||||||
onChange: setFilterSetName,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filterSet?.name ?? filterSetName}
|
|
||||||
</Typography.Text>
|
|
||||||
<IconsBlock>
|
|
||||||
{isApplied && (
|
|
||||||
<Icons.CheckOutlined
|
|
||||||
iconSize="m"
|
|
||||||
iconColor={theme.colors.success.base}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{onDelete && (
|
|
||||||
<AntdDropdown
|
|
||||||
overlay={menu}
|
|
||||||
placement="bottomRight"
|
|
||||||
trigger={['click']}
|
|
||||||
>
|
|
||||||
<HeaderButton
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
{...getFilterBarTestId('filter-set-menu-button')}
|
|
||||||
buttonStyle="link"
|
|
||||||
buttonSize="xsmall"
|
|
||||||
>
|
|
||||||
<Icons.EllipsisOutlined iconSize="m" />
|
|
||||||
</HeaderButton>
|
|
||||||
</AntdDropdown>
|
|
||||||
)}
|
|
||||||
</IconsBlock>
|
|
||||||
</TitleText>
|
|
||||||
<FiltersHeader
|
|
||||||
filterSet={filterSet}
|
|
||||||
dataMask={filterSet?.dataMask ?? dataMaskSelected}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterSetUnit;
|
|
|
@ -1,68 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
|
||||||
import { mockStore } from 'spec/fixtures/mockStore';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import FilterSets, { FilterSetsProps } from '.';
|
|
||||||
import { TabIds } from '../types';
|
|
||||||
|
|
||||||
const createProps = () => ({
|
|
||||||
disabled: false,
|
|
||||||
tab: TabIds.FilterSets,
|
|
||||||
dataMaskSelected: {
|
|
||||||
DefaultsID: {
|
|
||||||
filterState: {
|
|
||||||
value: 'value',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onEditFilterSet: jest.fn(),
|
|
||||||
onFilterSelectionChange: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const setup = (props: FilterSetsProps) => (
|
|
||||||
<Provider store={mockStore}>
|
|
||||||
<FilterSets {...props} />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
test('should render', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const { container } = render(setup(mockedProps));
|
|
||||||
expect(container).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the default title', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('New filter set')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the right number of filters', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Filters (1)')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the filters', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Set name')).toBeInTheDocument();
|
|
||||||
});
|
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
|
||||||
import { mockStore } from 'spec/fixtures/mockStore';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import FiltersHeader, { FiltersHeaderProps } from './FiltersHeader';
|
|
||||||
|
|
||||||
const mockedProps = {
|
|
||||||
dataMask: {
|
|
||||||
DefaultsID: {
|
|
||||||
filterState: {
|
|
||||||
value: 'value',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const setup = (props: FiltersHeaderProps) => (
|
|
||||||
<Provider store={mockStore}>
|
|
||||||
<FiltersHeader {...props} />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
test('should render', () => {
|
|
||||||
const { container } = render(setup(mockedProps));
|
|
||||||
expect(container).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the right number of filters', () => {
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('Filters (1)')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the name and value', () => {
|
|
||||||
render(setup(mockedProps));
|
|
||||||
expect(screen.getByText('test:')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('value')).toBeInTheDocument();
|
|
||||||
});
|
|
|
@ -1,156 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
import {
|
|
||||||
DataMaskState,
|
|
||||||
FilterSet,
|
|
||||||
isNativeFilter,
|
|
||||||
styled,
|
|
||||||
t,
|
|
||||||
useTheme,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { Typography, AntdTooltip, AntdCollapse } from 'src/components';
|
|
||||||
import Icons from 'src/components/Icons';
|
|
||||||
import { areObjectsEqual } from 'src/reduxUtils';
|
|
||||||
import { getFilterValueForDisplay } from './utils';
|
|
||||||
import { useFilters } from '../state';
|
|
||||||
import { getFilterBarTestId } from '../utils';
|
|
||||||
|
|
||||||
const FilterHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCollapse = styled(AntdCollapse)`
|
|
||||||
&.ant-collapse-ghost > .ant-collapse-item {
|
|
||||||
& > .ant-collapse-content > .ant-collapse-content-box {
|
|
||||||
padding: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
|
||||||
}
|
|
||||||
& > .ant-collapse-header {
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: flex-end;
|
|
||||||
max-width: max-content;
|
|
||||||
& .ant-collapse-arrow {
|
|
||||||
position: static;
|
|
||||||
padding-left: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledFilterRow = styled.div`
|
|
||||||
padding-top: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
padding-bottom: ${({ theme }) => theme.gridUnit}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type FiltersHeaderProps = {
|
|
||||||
dataMask?: DataMaskState;
|
|
||||||
filterSet?: FilterSet;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FiltersHeader: FC<FiltersHeaderProps> = ({ dataMask, filterSet }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const filters = useFilters();
|
|
||||||
const filterValues = Object.values(filters).filter(isNativeFilter);
|
|
||||||
|
|
||||||
let resultFilters = filterValues ?? [];
|
|
||||||
if (filterSet?.nativeFilters) {
|
|
||||||
resultFilters = Object.values(filterSet?.nativeFilters).filter(
|
|
||||||
isNativeFilter,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFiltersHeader = () => (
|
|
||||||
<FilterHeader>
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
{t('Filters (%d)', resultFilters.length)}
|
|
||||||
</Typography.Text>
|
|
||||||
</FilterHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getFilterRow = ({ id, name }: { id: string; name: string }) => {
|
|
||||||
const changedFilter =
|
|
||||||
filterSet &&
|
|
||||||
!areObjectsEqual(
|
|
||||||
filters[id]?.controlValues,
|
|
||||||
filterSet?.nativeFilters?.[id]?.controlValues,
|
|
||||||
{
|
|
||||||
ignoreUndefined: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const removedFilter = !Object.keys(filters).includes(id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AntdTooltip
|
|
||||||
title={
|
|
||||||
(removedFilter &&
|
|
||||||
t(
|
|
||||||
"This filter doesn't exist in dashboard. It will not be applied.",
|
|
||||||
)) ||
|
|
||||||
(changedFilter &&
|
|
||||||
t('Filter metadata changed in dashboard. It will not be applied.'))
|
|
||||||
}
|
|
||||||
placement="bottomLeft"
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
<StyledFilterRow data-test="filter-info">
|
|
||||||
<Typography.Text strong delete={removedFilter} mark={changedFilter}>
|
|
||||||
{name}:
|
|
||||||
</Typography.Text>
|
|
||||||
<Typography.Text delete={removedFilter} mark={changedFilter}>
|
|
||||||
{getFilterValueForDisplay(dataMask?.[id]?.filterState?.value) || (
|
|
||||||
<Typography.Text type="secondary">{t('None')}</Typography.Text>
|
|
||||||
)}
|
|
||||||
</Typography.Text>
|
|
||||||
</StyledFilterRow>
|
|
||||||
</AntdTooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExpandIcon = ({ isActive }: { isActive: boolean }) => {
|
|
||||||
const color = theme.colors.grayscale.base;
|
|
||||||
const Icon = isActive ? Icons.CaretUpOutlined : Icons.CaretDownOutlined;
|
|
||||||
return <Icon iconColor={color} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledCollapse
|
|
||||||
ghost
|
|
||||||
expandIconPosition="right"
|
|
||||||
defaultActiveKey={!filterSet ? ['filters'] : undefined}
|
|
||||||
expandIcon={getExpandIcon}
|
|
||||||
>
|
|
||||||
<AntdCollapse.Panel
|
|
||||||
{...getFilterBarTestId('collapse-filter-set-description')}
|
|
||||||
header={getFiltersHeader()}
|
|
||||||
key="filters"
|
|
||||||
>
|
|
||||||
{resultFilters.map(getFilterRow)}
|
|
||||||
</AntdCollapse.Panel>
|
|
||||||
</StyledCollapse>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FiltersHeader;
|
|
|
@ -1,94 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
|
||||||
import Footer from './Footer';
|
|
||||||
|
|
||||||
const createProps = () => ({
|
|
||||||
filterSetName: 'Set name',
|
|
||||||
disabled: false,
|
|
||||||
editMode: false,
|
|
||||||
onCancel: jest.fn(),
|
|
||||||
onEdit: jest.fn(),
|
|
||||||
onCreate: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const editModeProps = {
|
|
||||||
...createProps(),
|
|
||||||
editMode: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
test('should render', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const { container } = render(<Footer {...mockedProps} />, { useRedux: true });
|
|
||||||
expect(container).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render a button', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(<Footer {...mockedProps} />, { useRedux: true });
|
|
||||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Create new filter set')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render a disabled button', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
const disabledProps = {
|
|
||||||
...mockedProps,
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
render(<Footer {...disabledProps} />, { useRedux: true });
|
|
||||||
expect(screen.getByRole('button')).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should edit', () => {
|
|
||||||
const mockedProps = createProps();
|
|
||||||
render(<Footer {...mockedProps} />, { useRedux: true });
|
|
||||||
const btn = screen.getByRole('button');
|
|
||||||
expect(mockedProps.onEdit).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(btn);
|
|
||||||
expect(mockedProps.onEdit).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the Create button', () => {
|
|
||||||
render(<Footer {...editModeProps} />, { useRedux: true });
|
|
||||||
expect(screen.getByText('Create')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should create', () => {
|
|
||||||
render(<Footer {...editModeProps} />, { useRedux: true });
|
|
||||||
const createBtn = screen.getByText('Create');
|
|
||||||
expect(editModeProps.onCreate).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(createBtn);
|
|
||||||
expect(editModeProps.onCreate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the Cancel button', () => {
|
|
||||||
render(<Footer {...editModeProps} />, { useRedux: true });
|
|
||||||
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should cancel', () => {
|
|
||||||
render(<Footer {...editModeProps} />, { useRedux: true });
|
|
||||||
const cancelBtn = screen.getByText('Cancel');
|
|
||||||
expect(editModeProps.onCancel).not.toHaveBeenCalled();
|
|
||||||
userEvent.click(cancelBtn);
|
|
||||||
expect(editModeProps.onCancel).toHaveBeenCalled();
|
|
||||||
});
|
|
|
@ -1,120 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { t, styled } from '@superset-ui/core';
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
import Button from 'src/components/Button';
|
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
|
||||||
import { APPLY_FILTERS_HINT } from './utils';
|
|
||||||
import { useFilterSetNameDuplicated } from './state';
|
|
||||||
import { getFilterBarTestId } from '../utils';
|
|
||||||
|
|
||||||
export type FooterProps = {
|
|
||||||
filterSetName: string;
|
|
||||||
disabled: boolean;
|
|
||||||
editMode: boolean;
|
|
||||||
onCancel: () => void;
|
|
||||||
onEdit: () => void;
|
|
||||||
onCreate: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ActionButton = styled.div<{ disabled: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
& button {
|
|
||||||
${({ disabled }) => `pointer-events: ${disabled ? 'none' : 'all'}`};
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ActionButtons = styled.div`
|
|
||||||
display: grid;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
grid-gap: 10px;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Footer: FC<FooterProps> = ({
|
|
||||||
onCancel,
|
|
||||||
editMode,
|
|
||||||
onEdit,
|
|
||||||
onCreate,
|
|
||||||
disabled,
|
|
||||||
filterSetName,
|
|
||||||
}) => {
|
|
||||||
const isFilterSetNameDuplicated = useFilterSetNameDuplicated(filterSetName);
|
|
||||||
|
|
||||||
const isCreateDisabled =
|
|
||||||
!filterSetName || isFilterSetNameDuplicated || disabled;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{editMode ? (
|
|
||||||
<ActionButtons>
|
|
||||||
<Button
|
|
||||||
buttonStyle="tertiary"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={onCancel}
|
|
||||||
data-test="filter-set-cancel-button"
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Tooltip
|
|
||||||
placement="right"
|
|
||||||
title={
|
|
||||||
(!filterSetName && t('Please filter set name')) ||
|
|
||||||
(isFilterSetNameDuplicated &&
|
|
||||||
t('Filter set with this name already exists')) ||
|
|
||||||
(disabled && APPLY_FILTERS_HINT)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ActionButton disabled={isCreateDisabled}>
|
|
||||||
<Button
|
|
||||||
disabled={isCreateDisabled}
|
|
||||||
buttonStyle="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={onCreate}
|
|
||||||
{...getFilterBarTestId('create-filter-set-button')}
|
|
||||||
>
|
|
||||||
{t('Create')}
|
|
||||||
</Button>
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButtons>
|
|
||||||
) : (
|
|
||||||
<Tooltip placement="bottom" title={disabled && APPLY_FILTERS_HINT}>
|
|
||||||
<ActionButton disabled={disabled}>
|
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
buttonStyle="tertiary"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={onEdit}
|
|
||||||
{...getFilterBarTestId('new-filter-set-button')}
|
|
||||||
>
|
|
||||||
{t('Create new filter set')}
|
|
||||||
</Button>
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
|
|
@ -1,281 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
DataMask,
|
|
||||||
DataMaskState,
|
|
||||||
DataMaskWithId,
|
|
||||||
Filter,
|
|
||||||
Filters,
|
|
||||||
FilterSet,
|
|
||||||
HandlerFunction,
|
|
||||||
styled,
|
|
||||||
t,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import {
|
|
||||||
createFilterSet,
|
|
||||||
deleteFilterSet,
|
|
||||||
updateFilterSet,
|
|
||||||
} from 'src/dashboard/actions/nativeFilters';
|
|
||||||
import { areObjectsEqual } from 'src/reduxUtils';
|
|
||||||
import { findExistingFilterSet } from './utils';
|
|
||||||
import { useFilters, useNativeFiltersDataMask, useFilterSets } from '../state';
|
|
||||||
import Footer from './Footer';
|
|
||||||
import FilterSetUnit from './FilterSetUnit';
|
|
||||||
import { getFilterBarTestId } from '../utils';
|
|
||||||
import { TabIds } from '../types';
|
|
||||||
|
|
||||||
const FilterSetsWrapper = styled.div`
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
// 108px padding to make room for buttons with position: absolute
|
|
||||||
padding-bottom: ${({ theme }) => theme.gridUnit * 27}px;
|
|
||||||
|
|
||||||
& button.superset-button {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
& input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FilterSetUnitWrapper = styled.div<{
|
|
||||||
onClick?: HandlerFunction;
|
|
||||||
'data-selected'?: boolean;
|
|
||||||
}>`
|
|
||||||
${({ theme, 'data-selected': selected, onClick }) => `
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-gap: ${theme.gridUnit}px;
|
|
||||||
border-bottom: 1px solid ${theme.colors.grayscale.light2};
|
|
||||||
padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 4}px;
|
|
||||||
cursor: ${!onClick ? 'auto' : 'pointer'};
|
|
||||||
background: ${selected ? theme.colors.primary.light5 : 'transparent'};
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type FilterSetsProps = {
|
|
||||||
disabled: boolean;
|
|
||||||
tab: TabIds;
|
|
||||||
dataMaskSelected: DataMaskState;
|
|
||||||
onEditFilterSet: (id: number) => void;
|
|
||||||
onFilterSelectionChange: (
|
|
||||||
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
|
||||||
dataMask: Partial<DataMask>,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_FILTER_SET_NAME = t('New filter set');
|
|
||||||
|
|
||||||
const FilterSets: React.FC<FilterSetsProps> = ({
|
|
||||||
dataMaskSelected,
|
|
||||||
onEditFilterSet,
|
|
||||||
disabled,
|
|
||||||
onFilterSelectionChange,
|
|
||||||
tab,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
|
||||||
const dataMaskApplied = useNativeFiltersDataMask();
|
|
||||||
const filterSets = useFilterSets();
|
|
||||||
const filterSetFilterValues = Object.values(filterSets);
|
|
||||||
const filters = useFilters();
|
|
||||||
const filterValues = Object.values(filters) as Filter[];
|
|
||||||
const [selectedFiltersSetId, setSelectedFiltersSetId] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tab === TabIds.AllFilters) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!filterSetFilterValues.length) {
|
|
||||||
setSelectedFiltersSetId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundFilterSet = findExistingFilterSet({
|
|
||||||
dataMaskSelected,
|
|
||||||
filterSetFilterValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedFiltersSetId(foundFilterSet?.id ?? null);
|
|
||||||
}, [tab, dataMaskSelected, filterSetFilterValues]);
|
|
||||||
|
|
||||||
const isFilterMissingOrContainsInvalidMetadata = (
|
|
||||||
id: string,
|
|
||||||
filterSet?: FilterSet,
|
|
||||||
) =>
|
|
||||||
!filterValues.find(filter => filter?.id === id) ||
|
|
||||||
!areObjectsEqual(
|
|
||||||
filters[id]?.controlValues,
|
|
||||||
filterSet?.nativeFilters?.[id]?.controlValues,
|
|
||||||
{
|
|
||||||
ignoreUndefined: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const takeFilterSet = (id: number, event?: MouseEvent) => {
|
|
||||||
const localTarget = event?.target as HTMLDivElement;
|
|
||||||
if (localTarget) {
|
|
||||||
const parent = localTarget.closest(
|
|
||||||
`[data-anchor=${getFilterBarTestId('filter-set-wrapper', true)}]`,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
parent?.querySelector('.ant-collapse-header')?.contains(localTarget) ||
|
|
||||||
localTarget?.closest('.ant-dropdown')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSelectedFiltersSetId(id);
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterSet = filterSets[id];
|
|
||||||
|
|
||||||
(Object.values(filterSet?.dataMask) ?? []).forEach(
|
|
||||||
(dataMask: DataMaskWithId) => {
|
|
||||||
const { extraFormData, filterState, id } = dataMask;
|
|
||||||
if (isFilterMissingOrContainsInvalidMetadata(id, filterSet)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onFilterSelectionChange({ id }, { extraFormData, filterState });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRebuild = (id: number) => {
|
|
||||||
const filterSet = filterSets[id];
|
|
||||||
// We need remove invalid filters from filter set
|
|
||||||
const newFilters = Object.values(filterSet?.dataMask ?? {})
|
|
||||||
.filter(dataMask => {
|
|
||||||
const { id } = dataMask as DataMaskWithId;
|
|
||||||
return !isFilterMissingOrContainsInvalidMetadata(id, filterSet);
|
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(prev, next: DataMaskWithId) => ({
|
|
||||||
...prev,
|
|
||||||
[next.id]: filters[next.id],
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedFilterSet: FilterSet = {
|
|
||||||
...filterSet,
|
|
||||||
nativeFilters: newFilters as Filters,
|
|
||||||
dataMask: Object.keys(newFilters).reduce(
|
|
||||||
(prev, nextFilterId) => ({
|
|
||||||
...prev,
|
|
||||||
[nextFilterId]: filterSet.dataMask?.[nextFilterId],
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
dispatch(updateFilterSet(updatedFilterSet));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (id: number) => {
|
|
||||||
takeFilterSet(id);
|
|
||||||
onEditFilterSet(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteFilterSet = (filterSetId: number) => {
|
|
||||||
dispatch(deleteFilterSet(filterSetId));
|
|
||||||
if (filterSetId === selectedFiltersSetId) {
|
|
||||||
setSelectedFiltersSetId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setEditMode(false);
|
|
||||||
setFilterSetName(DEFAULT_FILTER_SET_NAME);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateFilterSet = () => {
|
|
||||||
const newFilterSet: Omit<FilterSet, 'id'> = {
|
|
||||||
name: filterSetName.trim(),
|
|
||||||
nativeFilters: filters,
|
|
||||||
dataMask: Object.keys(filters).reduce(
|
|
||||||
(prev, nextFilterId) => ({
|
|
||||||
...prev,
|
|
||||||
[nextFilterId]: dataMaskApplied[nextFilterId],
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
dispatch(createFilterSet(newFilterSet));
|
|
||||||
setEditMode(false);
|
|
||||||
setFilterSetName(DEFAULT_FILTER_SET_NAME);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterSetsWrapper>
|
|
||||||
{!selectedFiltersSetId && (
|
|
||||||
<FilterSetUnitWrapper>
|
|
||||||
<FilterSetUnit
|
|
||||||
dataMaskSelected={dataMaskSelected}
|
|
||||||
editMode={editMode}
|
|
||||||
setFilterSetName={setFilterSetName}
|
|
||||||
filterSetName={filterSetName}
|
|
||||||
/>
|
|
||||||
<Footer
|
|
||||||
filterSetName={filterSetName.trim()}
|
|
||||||
disabled={disabled}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
editMode={editMode}
|
|
||||||
onEdit={() => setEditMode(true)}
|
|
||||||
onCreate={handleCreateFilterSet}
|
|
||||||
/>
|
|
||||||
</FilterSetUnitWrapper>
|
|
||||||
)}
|
|
||||||
{filterSetFilterValues.map(filterSet => (
|
|
||||||
<FilterSetUnitWrapper
|
|
||||||
{...getFilterBarTestId('filter-set-wrapper')}
|
|
||||||
data-anchor={getFilterBarTestId('filter-set-wrapper', true)}
|
|
||||||
data-selected={filterSet.id === selectedFiltersSetId}
|
|
||||||
onClick={
|
|
||||||
(e =>
|
|
||||||
takeFilterSet(filterSet.id, e as MouseEvent)) as HandlerFunction
|
|
||||||
}
|
|
||||||
key={filterSet.id}
|
|
||||||
>
|
|
||||||
<FilterSetUnit
|
|
||||||
isApplied={filterSet.id === selectedFiltersSetId && !disabled}
|
|
||||||
onDelete={() => handleDeleteFilterSet(filterSet.id)}
|
|
||||||
onEdit={() => handleEdit(filterSet.id)}
|
|
||||||
onRebuild={() => handleRebuild(filterSet.id)}
|
|
||||||
dataMaskSelected={dataMaskSelected}
|
|
||||||
filterSet={filterSet}
|
|
||||||
/>
|
|
||||||
</FilterSetUnitWrapper>
|
|
||||||
))}
|
|
||||||
</FilterSetsWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterSets;
|
|
|
@ -1,37 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useFilterSets } from '../state';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const useFilterSetNameDuplicated = (
|
|
||||||
filterSetName: string,
|
|
||||||
ignoreName?: string,
|
|
||||||
) => {
|
|
||||||
const filterSets = useFilterSets();
|
|
||||||
const filterSetFilterValues = Object.values(filterSets);
|
|
||||||
const isFilterSetNameDuplicated = useMemo(
|
|
||||||
() => !!filterSetFilterValues.find(({ name }) => name === filterSetName),
|
|
||||||
[filterSetFilterValues, filterSetName],
|
|
||||||
);
|
|
||||||
if (ignoreName === filterSetName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isFilterSetNameDuplicated;
|
|
||||||
};
|
|
|
@ -1,136 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { FilterSet } from '@superset-ui/core';
|
|
||||||
import { findExistingFilterSet } from '.';
|
|
||||||
|
|
||||||
const createDataMaskSelected = () => ({
|
|
||||||
filterId: { filterState: { value: 'value-1' } },
|
|
||||||
filterId2: { filterState: { value: 'value-2' } },
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should find correct filter', () => {
|
|
||||||
const dataMaskSelected = createDataMaskSelected();
|
|
||||||
const filterSetFilterValues: FilterSet[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'name-01',
|
|
||||||
nativeFilters: {},
|
|
||||||
dataMask: {
|
|
||||||
filterId: { id: 'filterId', filterState: { value: 'value-1' } },
|
|
||||||
filterId2: { id: 'filterId2', filterState: { value: 'value-2' } },
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const response = findExistingFilterSet({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
});
|
|
||||||
expect(response).toEqual({
|
|
||||||
dataMask: {
|
|
||||||
filterId: { id: 'filterId', filterState: { value: 'value-1' } },
|
|
||||||
filterId2: { id: 'filterId2', filterState: { value: 'value-2' } },
|
|
||||||
},
|
|
||||||
id: 1,
|
|
||||||
name: 'name-01',
|
|
||||||
nativeFilters: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return undefined when nativeFilters has less values', () => {
|
|
||||||
const dataMaskSelected = createDataMaskSelected();
|
|
||||||
const filterSetFilterValues = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'name-01',
|
|
||||||
nativeFilters: {},
|
|
||||||
dataMask: {
|
|
||||||
filterId: { id: 'filterId', filterState: { value: 'value-1' } },
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const response = findExistingFilterSet({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
});
|
|
||||||
expect(response).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return undefined when nativeFilters has different values', () => {
|
|
||||||
const dataMaskSelected = createDataMaskSelected();
|
|
||||||
const filterSetFilterValues: FilterSet[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'name-01',
|
|
||||||
nativeFilters: {},
|
|
||||||
dataMask: {
|
|
||||||
filterId: { id: 'filterId', filterState: { value: 'value-1' } },
|
|
||||||
filterId2: { id: 'filterId2', filterState: { value: 'value-1' } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const response = findExistingFilterSet({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
});
|
|
||||||
expect(response).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return undefined when dataMask:{}', () => {
|
|
||||||
const dataMaskSelected = createDataMaskSelected();
|
|
||||||
const filterSetFilterValues = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'name-01',
|
|
||||||
nativeFilters: {},
|
|
||||||
dataMask: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const response = findExistingFilterSet({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
});
|
|
||||||
expect(response).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return undefined when dataMask is empty}', () => {
|
|
||||||
const dataMaskSelected = createDataMaskSelected();
|
|
||||||
const filterSetFilterValues: FilterSet[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'name-01',
|
|
||||||
nativeFilters: {},
|
|
||||||
dataMask: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const response = findExistingFilterSet({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
});
|
|
||||||
expect(response).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return undefined when filterSetFilterValues is []', () => {
|
|
||||||
const dataMaskSelected = createDataMaskSelected();
|
|
||||||
const filterSetFilterValues: FilterSet[] = [];
|
|
||||||
const response = findExistingFilterSet({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
});
|
|
||||||
expect(response).toBeUndefined();
|
|
||||||
});
|
|
|
@ -1,24 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { generateFiltersSetId } from '.';
|
|
||||||
|
|
||||||
test('Should follow the pattern "FILTERS_SET-"', () => {
|
|
||||||
const id = generateFiltersSetId();
|
|
||||||
expect(id.startsWith('FILTERS_SET-', 0)).toBe(true);
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { getFilterValueForDisplay } from '.';
|
|
||||||
|
|
||||||
test('Should return "" when value is null or undefined', () => {
|
|
||||||
expect(getFilterValueForDisplay(null)).toBe('');
|
|
||||||
expect(getFilterValueForDisplay(undefined)).toBe('');
|
|
||||||
expect(getFilterValueForDisplay()).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return "string value" when value is string or number', () => {
|
|
||||||
expect(getFilterValueForDisplay(123)).toBe('123');
|
|
||||||
expect(getFilterValueForDisplay('123')).toBe('123');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return a string with values separated by commas', () => {
|
|
||||||
expect(getFilterValueForDisplay(['a', 'b', 'c'])).toBe('a, b, c');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return a JSON.stringify from objects', () => {
|
|
||||||
expect(getFilterValueForDisplay({ any: 'value' })).toBe('{"any":"value"}');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should return an error message when the type is invalid', () => {
|
|
||||||
expect(getFilterValueForDisplay(true as any)).toBe('Unknown value');
|
|
||||||
});
|
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import shortid from 'shortid';
|
|
||||||
import { DataMaskState, FilterSet, t } from '@superset-ui/core';
|
|
||||||
import { areObjectsEqual } from 'src/reduxUtils';
|
|
||||||
|
|
||||||
export const generateFiltersSetId = () => `FILTERS_SET-${shortid.generate()}`;
|
|
||||||
|
|
||||||
export const APPLY_FILTERS_HINT = t('Please apply filter changes');
|
|
||||||
|
|
||||||
export const getFilterValueForDisplay = (
|
|
||||||
value?: string[] | null | string | number | object,
|
|
||||||
): string => {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (typeof value === 'string' || typeof value === 'number') {
|
|
||||||
return `${value}`;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.join(', ');
|
|
||||||
}
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
return t('Unknown value');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findExistingFilterSet = ({
|
|
||||||
filterSetFilterValues,
|
|
||||||
dataMaskSelected,
|
|
||||||
}: {
|
|
||||||
filterSetFilterValues: FilterSet[];
|
|
||||||
dataMaskSelected: DataMaskState;
|
|
||||||
}) =>
|
|
||||||
filterSetFilterValues.find(({ dataMask: dataMaskFromFilterSet = {} }) => {
|
|
||||||
const dataMaskSelectedEntries = Object.entries(dataMaskSelected);
|
|
||||||
return dataMaskSelectedEntries.every(([id, filterFromSelectedFilters]) => {
|
|
||||||
const isEqual = areObjectsEqual(
|
|
||||||
filterFromSelectedFilters.filterState,
|
|
||||||
dataMaskFromFilterSet?.[id]?.filterState,
|
|
||||||
{ ignoreUndefined: true, ignoreNull: true },
|
|
||||||
);
|
|
||||||
const hasSamePropsNumber =
|
|
||||||
dataMaskSelectedEntries.length ===
|
|
||||||
Object.keys(dataMaskFromFilterSet ?? {}).length;
|
|
||||||
return isEqual && hasSamePropsNumber;
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -28,23 +28,12 @@ import React, {
|
||||||
createContext,
|
createContext,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import {
|
import { FeatureFlag, isFeatureEnabled, styled, t } from '@superset-ui/core';
|
||||||
FeatureFlag,
|
|
||||||
HandlerFunction,
|
|
||||||
isFeatureEnabled,
|
|
||||||
isNativeFilter,
|
|
||||||
styled,
|
|
||||||
t,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { AntdTabs } from 'src/components';
|
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import { EmptyStateSmall } from 'src/components/EmptyState';
|
import { EmptyStateSmall } from 'src/components/EmptyState';
|
||||||
import { getFilterBarTestId } from './utils';
|
import { getFilterBarTestId } from './utils';
|
||||||
import { TabIds, VerticalBarProps } from './types';
|
import { VerticalBarProps } from './types';
|
||||||
import FilterSets from './FilterSets';
|
|
||||||
import { useFilterSets } from './state';
|
|
||||||
import EditSection from './FilterSets/EditSection';
|
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import FilterControls from './FilterControls/FilterControls';
|
import FilterControls from './FilterControls/FilterControls';
|
||||||
import CrossFiltersVertical from './CrossFilters/Vertical';
|
import CrossFiltersVertical from './CrossFilters/Vertical';
|
||||||
|
@ -117,22 +106,6 @@ const StyledFilterIcon = styled(Icons.Filter)`
|
||||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTabs = styled(AntdTabs)`
|
|
||||||
& .ant-tabs-nav-list {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
& .ant-tabs-tab {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .ant-tabs-nav .ant-tabs-nav-operations {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FilterBarEmptyStateContainer = styled.div`
|
const FilterBarEmptyStateContainer = styled.div`
|
||||||
margin-top: ${({ theme }) => theme.gridUnit * 8}px;
|
margin-top: ${({ theme }) => theme.gridUnit * 8}px;
|
||||||
`;
|
`;
|
||||||
|
@ -151,18 +124,12 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
|
||||||
filtersOpen,
|
filtersOpen,
|
||||||
filterValues,
|
filterValues,
|
||||||
height,
|
height,
|
||||||
isDisabled,
|
|
||||||
isInitialized,
|
isInitialized,
|
||||||
offset,
|
offset,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
toggleFiltersBar,
|
toggleFiltersBar,
|
||||||
width,
|
width,
|
||||||
}) => {
|
}) => {
|
||||||
const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null);
|
|
||||||
const filterSets = useFilterSets();
|
|
||||||
const filterSetFilterValues = Object.values(filterSets);
|
|
||||||
const [tab, setTab] = useState(TabIds.AllFilters);
|
|
||||||
const nativeFilterValues = filterValues.filter(isNativeFilter);
|
|
||||||
const [isScrolling, setIsScrolling] = useState(false);
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
const timeout = useRef<any>();
|
const timeout = useRef<any>();
|
||||||
|
|
||||||
|
@ -195,8 +162,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
|
||||||
[height],
|
[height],
|
||||||
);
|
);
|
||||||
|
|
||||||
const numberOfFilters = nativeFilterValues.length;
|
|
||||||
|
|
||||||
const filterControls = useMemo(
|
const filterControls = useMemo(
|
||||||
() =>
|
() =>
|
||||||
filterValues.length === 0 ? (
|
filterValues.length === 0 ? (
|
||||||
|
@ -223,62 +188,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
|
||||||
[canEdit, dataMaskSelected, filterValues.length, onSelectionChange],
|
[canEdit, dataMaskSelected, filterValues.length, onSelectionChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterSetsTabs = useMemo(
|
|
||||||
() => (
|
|
||||||
<StyledTabs
|
|
||||||
centered
|
|
||||||
onChange={setTab as HandlerFunction}
|
|
||||||
defaultActiveKey={TabIds.AllFilters}
|
|
||||||
activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
|
|
||||||
>
|
|
||||||
<AntdTabs.TabPane
|
|
||||||
tab={t('All filters (%(filterCount)d)', {
|
|
||||||
filterCount: numberOfFilters,
|
|
||||||
})}
|
|
||||||
key={TabIds.AllFilters}
|
|
||||||
css={tabPaneStyle}
|
|
||||||
>
|
|
||||||
{editFilterSetId && (
|
|
||||||
<EditSection
|
|
||||||
dataMaskSelected={dataMaskSelected}
|
|
||||||
disabled={!isDisabled}
|
|
||||||
onCancel={() => setEditFilterSetId(null)}
|
|
||||||
filterSetId={editFilterSetId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{filterControls}
|
|
||||||
</AntdTabs.TabPane>
|
|
||||||
<AntdTabs.TabPane
|
|
||||||
disabled={!!editFilterSetId}
|
|
||||||
tab={t('Filter sets (%(filterSetCount)d)', {
|
|
||||||
filterSetCount: filterSetFilterValues.length,
|
|
||||||
})}
|
|
||||||
key={TabIds.FilterSets}
|
|
||||||
css={tabPaneStyle}
|
|
||||||
>
|
|
||||||
<FilterSets
|
|
||||||
onEditFilterSet={setEditFilterSetId}
|
|
||||||
disabled={!isDisabled}
|
|
||||||
dataMaskSelected={dataMaskSelected}
|
|
||||||
tab={tab}
|
|
||||||
onFilterSelectionChange={onSelectionChange}
|
|
||||||
/>
|
|
||||||
</AntdTabs.TabPane>
|
|
||||||
</StyledTabs>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
dataMaskSelected,
|
|
||||||
editFilterSetId,
|
|
||||||
filterControls,
|
|
||||||
filterSetFilterValues.length,
|
|
||||||
isDisabled,
|
|
||||||
numberOfFilters,
|
|
||||||
onSelectionChange,
|
|
||||||
tab,
|
|
||||||
tabPaneStyle,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const crossFilters = useMemo(
|
const crossFilters = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? (
|
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? (
|
||||||
|
@ -293,11 +202,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
|
||||||
[actions],
|
[actions],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter sets depend on native filters
|
|
||||||
const filterSetEnabled =
|
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) &&
|
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterBarScrollContext.Provider value={isScrolling}>
|
<FilterBarScrollContext.Provider value={isScrolling}>
|
||||||
<BarWrapper
|
<BarWrapper
|
||||||
|
@ -326,11 +230,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
|
||||||
<div css={{ height }}>
|
<div css={{ height }}>
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
) : filterSetEnabled ? (
|
|
||||||
<>
|
|
||||||
{crossFilters}
|
|
||||||
{filterSetsTabs}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div css={tabPaneStyle} onScroll={onScroll}>
|
<div css={tabPaneStyle} onScroll={onScroll}>
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -304,7 +304,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||||
filtersOpen={verticalConfig.filtersOpen}
|
filtersOpen={verticalConfig.filtersOpen}
|
||||||
filterValues={filterValues}
|
filterValues={filterValues}
|
||||||
isInitialized={isInitialized}
|
isInitialized={isInitialized}
|
||||||
isDisabled={isApplyDisabled}
|
|
||||||
height={verticalConfig.height}
|
height={verticalConfig.height}
|
||||||
offset={verticalConfig.offset}
|
offset={verticalConfig.offset}
|
||||||
onSelectionChange={handleFilterSelectionChange}
|
onSelectionChange={handleFilterSelectionChange}
|
||||||
|
|
|
@ -24,17 +24,11 @@ import {
|
||||||
DataMaskWithId,
|
DataMaskWithId,
|
||||||
Filter,
|
Filter,
|
||||||
Filters,
|
Filters,
|
||||||
FilterSets as FilterSetsType,
|
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { ChartsState, RootState } from 'src/dashboard/types';
|
import { ChartsState, RootState } from 'src/dashboard/types';
|
||||||
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
|
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
|
||||||
|
|
||||||
export const useFilterSets = () =>
|
|
||||||
useSelector<any, FilterSetsType>(
|
|
||||||
state => state.nativeFilters.filterSets || {},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useFilters = () => {
|
export const useFilters = () => {
|
||||||
const preselectedNativeFilters = useSelector<any, Filters>(
|
const preselectedNativeFilters = useSelector<any, Filters>(
|
||||||
state => state.dashboardState?.preselectNativeFilters,
|
state => state.dashboardState?.preselectNativeFilters,
|
||||||
|
|
|
@ -58,11 +58,4 @@ export type HorizontalBarProps = CommonFiltersBarProps & {
|
||||||
|
|
||||||
export type VerticalBarProps = Omit<FiltersBarProps, 'orientation'> &
|
export type VerticalBarProps = Omit<FiltersBarProps, 'orientation'> &
|
||||||
CommonFiltersBarProps &
|
CommonFiltersBarProps &
|
||||||
VerticalBarConfig & {
|
VerticalBarConfig;
|
||||||
isDisabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum TabIds {
|
|
||||||
AllFilters = 'allFilters',
|
|
||||||
FilterSets = 'filterSets',
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
Filter,
|
Filter,
|
||||||
getChartMetadataRegistry,
|
getChartMetadataRegistry,
|
||||||
QueryFormData,
|
QueryFormData,
|
||||||
|
t,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { DashboardLayout } from 'src/dashboard/types';
|
import { DashboardLayout } from 'src/dashboard/types';
|
||||||
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
|
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
|
||||||
|
@ -234,3 +235,21 @@ export const findTabsWithChartsInScope = (
|
||||||
}
|
}
|
||||||
return tabsInScope;
|
return tabsInScope;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFilterValueForDisplay = (
|
||||||
|
value?: string[] | null | string | number | object,
|
||||||
|
): string => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' || typeof value === 'number') {
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.join(', ');
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return t('Unknown value');
|
||||||
|
};
|
||||||
|
|
|
@ -21,9 +21,7 @@ import { Global } from '@emotion/react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
CategoricalColorNamespace,
|
CategoricalColorNamespace,
|
||||||
FeatureFlag,
|
|
||||||
getSharedLabelColor,
|
getSharedLabelColor,
|
||||||
isFeatureEnabled,
|
|
||||||
SharedLabelColorSource,
|
SharedLabelColorSource,
|
||||||
t,
|
t,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
@ -43,7 +41,6 @@ import injectCustomCss from 'src/dashboard/util/injectCustomCss';
|
||||||
import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
|
import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { getFilterSets } from 'src/dashboard/actions/nativeFilters';
|
|
||||||
import { setDatasetsStatus } from 'src/dashboard/actions/dashboardState';
|
import { setDatasetsStatus } from 'src/dashboard/actions/dashboardState';
|
||||||
import {
|
import {
|
||||||
getFilterValue,
|
getFilterValue,
|
||||||
|
@ -104,11 +101,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
const readyToRender = Boolean(dashboard && charts);
|
const readyToRender = Boolean(dashboard && charts);
|
||||||
const { dashboard_title, css, metadata, id = 0 } = dashboard || {};
|
const { dashboard_title, css, metadata, id = 0 } = dashboard || {};
|
||||||
|
|
||||||
// Filter sets depend on native filters
|
|
||||||
const filterSetEnabled =
|
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) &&
|
|
||||||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// mark tab id as redundant when user closes browser tab - a new id will be
|
// mark tab id as redundant when user closes browser tab - a new id will be
|
||||||
// generated next time user opens a dashboard and the old one won't be reused
|
// generated next time user opens a dashboard and the old one won't be reused
|
||||||
|
@ -158,10 +150,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
if (readyToRender) {
|
if (readyToRender) {
|
||||||
if (!isDashboardHydrated.current) {
|
if (!isDashboardHydrated.current) {
|
||||||
isDashboardHydrated.current = true;
|
isDashboardHydrated.current = true;
|
||||||
if (filterSetEnabled) {
|
|
||||||
// only initialize filterset once
|
|
||||||
dispatch(getFilterSets(id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
hydrateDashboard({
|
hydrateDashboard({
|
||||||
|
|
|
@ -33,14 +33,6 @@ export const mockDataMaskInfo: DataMaskStateWithId = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const nativeFiltersInfo: NativeFiltersState = {
|
export const nativeFiltersInfo: NativeFiltersState = {
|
||||||
filterSets: {
|
|
||||||
'1': {
|
|
||||||
id: 1,
|
|
||||||
name: 'Set name',
|
|
||||||
nativeFilters: {},
|
|
||||||
dataMask: mockDataMaskInfo,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filters: {
|
filters: {
|
||||||
DefaultsID: {
|
DefaultsID: {
|
||||||
cascadeParentIds: [],
|
cascadeParentIds: [],
|
||||||
|
|
|
@ -20,26 +20,19 @@ import {
|
||||||
AnyFilterAction,
|
AnyFilterAction,
|
||||||
SET_FILTER_CONFIG_COMPLETE,
|
SET_FILTER_CONFIG_COMPLETE,
|
||||||
SET_IN_SCOPE_STATUS_OF_FILTERS,
|
SET_IN_SCOPE_STATUS_OF_FILTERS,
|
||||||
SET_FILTER_SETS_COMPLETE,
|
|
||||||
SET_FOCUSED_NATIVE_FILTER,
|
SET_FOCUSED_NATIVE_FILTER,
|
||||||
UNSET_FOCUSED_NATIVE_FILTER,
|
UNSET_FOCUSED_NATIVE_FILTER,
|
||||||
SET_HOVERED_NATIVE_FILTER,
|
SET_HOVERED_NATIVE_FILTER,
|
||||||
UNSET_HOVERED_NATIVE_FILTER,
|
UNSET_HOVERED_NATIVE_FILTER,
|
||||||
UPDATE_CASCADE_PARENT_IDS,
|
UPDATE_CASCADE_PARENT_IDS,
|
||||||
} from 'src/dashboard/actions/nativeFilters';
|
} from 'src/dashboard/actions/nativeFilters';
|
||||||
import {
|
import { FilterConfiguration, NativeFiltersState } from '@superset-ui/core';
|
||||||
FilterSet,
|
|
||||||
FilterConfiguration,
|
|
||||||
NativeFiltersState,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
||||||
|
|
||||||
export function getInitialState({
|
export function getInitialState({
|
||||||
filterSetsConfig,
|
|
||||||
filterConfig,
|
filterConfig,
|
||||||
state: prevState,
|
state: prevState,
|
||||||
}: {
|
}: {
|
||||||
filterSetsConfig?: FilterSet[];
|
|
||||||
filterConfig?: FilterConfiguration;
|
filterConfig?: FilterConfiguration;
|
||||||
state?: NativeFiltersState;
|
state?: NativeFiltersState;
|
||||||
}): NativeFiltersState {
|
}): NativeFiltersState {
|
||||||
|
@ -55,17 +48,6 @@ export function getInitialState({
|
||||||
} else {
|
} else {
|
||||||
state.filters = prevState?.filters ?? {};
|
state.filters = prevState?.filters ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterSetsConfig) {
|
|
||||||
const filterSets = {};
|
|
||||||
filterSetsConfig.forEach(filtersSet => {
|
|
||||||
const { id } = filtersSet;
|
|
||||||
filterSets[id] = filtersSet;
|
|
||||||
});
|
|
||||||
state.filterSets = filterSets;
|
|
||||||
} else {
|
|
||||||
state.filterSets = prevState?.filterSets ?? {};
|
|
||||||
}
|
|
||||||
state.focusedFilterId = undefined;
|
state.focusedFilterId = undefined;
|
||||||
return state as NativeFiltersState;
|
return state as NativeFiltersState;
|
||||||
}
|
}
|
||||||
|
@ -73,7 +55,6 @@ export function getInitialState({
|
||||||
export default function nativeFilterReducer(
|
export default function nativeFilterReducer(
|
||||||
state: NativeFiltersState = {
|
state: NativeFiltersState = {
|
||||||
filters: {},
|
filters: {},
|
||||||
filterSets: {},
|
|
||||||
},
|
},
|
||||||
action: AnyFilterAction,
|
action: AnyFilterAction,
|
||||||
) {
|
) {
|
||||||
|
@ -81,19 +62,12 @@ export default function nativeFilterReducer(
|
||||||
case HYDRATE_DASHBOARD:
|
case HYDRATE_DASHBOARD:
|
||||||
return {
|
return {
|
||||||
filters: action.data.nativeFilters.filters,
|
filters: action.data.nativeFilters.filters,
|
||||||
filterSets: action.data.nativeFilters.filterSets,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
case SET_FILTER_CONFIG_COMPLETE:
|
case SET_FILTER_CONFIG_COMPLETE:
|
||||||
case SET_IN_SCOPE_STATUS_OF_FILTERS:
|
case SET_IN_SCOPE_STATUS_OF_FILTERS:
|
||||||
return getInitialState({ filterConfig: action.filterConfig, state });
|
return getInitialState({ filterConfig: action.filterConfig, state });
|
||||||
|
|
||||||
case SET_FILTER_SETS_COMPLETE:
|
|
||||||
return getInitialState({
|
|
||||||
filterSetsConfig: action.filterSets,
|
|
||||||
state,
|
|
||||||
});
|
|
||||||
|
|
||||||
case SET_FOCUSED_NATIVE_FILTER:
|
case SET_FOCUSED_NATIVE_FILTER:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import componentTypes from 'src/dashboard/util/componentTypes';
|
import componentTypes from 'src/dashboard/util/componentTypes';
|
||||||
import { JsonObject } from '@superset-ui/core';
|
|
||||||
|
|
||||||
export enum Scoping {
|
export enum Scoping {
|
||||||
All = 'All',
|
All = 'All',
|
||||||
|
@ -80,16 +79,3 @@ export type LayoutItem = {
|
||||||
width: number;
|
width: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FilterSetFullData = {
|
|
||||||
changed_by_fk: string | null;
|
|
||||||
changed_on: string | null;
|
|
||||||
created_by_fk: string | null;
|
|
||||||
created_on: string | null;
|
|
||||||
dashboard_id: number;
|
|
||||||
description: string | null;
|
|
||||||
name: string;
|
|
||||||
owner_id: number;
|
|
||||||
owner_type: string;
|
|
||||||
params: JsonObject;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
|
@ -1,87 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import logging
|
|
||||||
from typing import cast, Optional
|
|
||||||
|
|
||||||
from flask_appbuilder.models.sqla import Model
|
|
||||||
|
|
||||||
from superset import security_manager
|
|
||||||
from superset.commands.dashboard.exceptions import DashboardNotFoundError
|
|
||||||
from superset.commands.dashboard.filter_set.exceptions import (
|
|
||||||
FilterSetForbiddenError,
|
|
||||||
FilterSetNotFoundError,
|
|
||||||
)
|
|
||||||
from superset.common.not_authorized_object import NotAuthorizedException
|
|
||||||
from superset.daos.dashboard import DashboardDAO
|
|
||||||
from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE
|
|
||||||
from superset.models.dashboard import Dashboard
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from superset.utils.core import get_user_id
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterSetCommand:
|
|
||||||
# pylint: disable=C0103
|
|
||||||
_dashboard: Dashboard
|
|
||||||
_filter_set_id: Optional[int]
|
|
||||||
_filter_set: Optional[FilterSet]
|
|
||||||
|
|
||||||
def __init__(self, dashboard_id: int):
|
|
||||||
self._dashboard_id = dashboard_id
|
|
||||||
|
|
||||||
def run(self) -> Model:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _validate_filterset_dashboard_exists(self) -> None:
|
|
||||||
self._dashboard = DashboardDAO.get_by_id_or_slug(str(self._dashboard_id))
|
|
||||||
if not self._dashboard:
|
|
||||||
raise DashboardNotFoundError()
|
|
||||||
|
|
||||||
def validate_exist_filter_use_cases_set(self) -> None: # pylint: disable=C0103
|
|
||||||
self._validate_filter_set_exists_and_set_when_exists()
|
|
||||||
self.check_ownership()
|
|
||||||
|
|
||||||
def _validate_filter_set_exists_and_set_when_exists(self) -> None:
|
|
||||||
self._filter_set = self._dashboard.filter_sets.get(
|
|
||||||
cast(int, self._filter_set_id), None
|
|
||||||
)
|
|
||||||
if not self._filter_set:
|
|
||||||
raise FilterSetNotFoundError(str(self._filter_set_id))
|
|
||||||
|
|
||||||
def check_ownership(self) -> None:
|
|
||||||
try:
|
|
||||||
if not security_manager.is_admin():
|
|
||||||
filter_set: FilterSet = cast(FilterSet, self._filter_set)
|
|
||||||
if filter_set.owner_type == USER_OWNER_TYPE:
|
|
||||||
if get_user_id() != filter_set.owner_id:
|
|
||||||
raise FilterSetForbiddenError(
|
|
||||||
str(self._filter_set_id),
|
|
||||||
"The user is not the owner of the filter_set",
|
|
||||||
)
|
|
||||||
elif not security_manager.is_owner(self._dashboard):
|
|
||||||
raise FilterSetForbiddenError(
|
|
||||||
str(self._filter_set_id),
|
|
||||||
"The user is not an owner of the filter_set's dashboard",
|
|
||||||
)
|
|
||||||
except NotAuthorizedException as err:
|
|
||||||
raise FilterSetForbiddenError(
|
|
||||||
str(self._filter_set_id),
|
|
||||||
"user not authorized to access the filterset",
|
|
||||||
) from err
|
|
||||||
except FilterSetForbiddenError as err:
|
|
||||||
raise err
|
|
|
@ -1,76 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from flask_appbuilder.models.sqla import Model
|
|
||||||
|
|
||||||
from superset import security_manager
|
|
||||||
from superset.commands.dashboard.filter_set.base import BaseFilterSetCommand
|
|
||||||
from superset.commands.dashboard.filter_set.exceptions import (
|
|
||||||
DashboardIdInconsistencyError,
|
|
||||||
FilterSetCreateFailedError,
|
|
||||||
UserIsNotDashboardOwnerError,
|
|
||||||
)
|
|
||||||
from superset.daos.dashboard import FilterSetDAO
|
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_ID_FIELD,
|
|
||||||
DASHBOARD_OWNER_TYPE,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
)
|
|
||||||
from superset.utils.core import get_user_id
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateFilterSetCommand(BaseFilterSetCommand):
|
|
||||||
# pylint: disable=C0103
|
|
||||||
def __init__(self, dashboard_id: int, data: dict[str, Any]):
|
|
||||||
super().__init__(dashboard_id)
|
|
||||||
self._properties = data.copy()
|
|
||||||
|
|
||||||
def run(self) -> Model:
|
|
||||||
self.validate()
|
|
||||||
self._properties[DASHBOARD_ID_FIELD] = self._dashboard.id
|
|
||||||
return FilterSetDAO.create(attributes=self._properties, commit=True)
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
|
||||||
self._validate_filterset_dashboard_exists()
|
|
||||||
if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE:
|
|
||||||
self._validate_owner_id_is_dashboard_id()
|
|
||||||
self._validate_user_is_the_dashboard_owner()
|
|
||||||
else:
|
|
||||||
self._validate_owner_id_exists()
|
|
||||||
|
|
||||||
def _validate_owner_id_exists(self) -> None:
|
|
||||||
owner_id = self._properties[OWNER_ID_FIELD]
|
|
||||||
if not (get_user_id() == owner_id or security_manager.get_user_by_id(owner_id)):
|
|
||||||
raise FilterSetCreateFailedError(
|
|
||||||
str(self._dashboard_id), "owner_id does not exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _validate_user_is_the_dashboard_owner(self) -> None:
|
|
||||||
if not security_manager.is_owner(self._dashboard):
|
|
||||||
raise UserIsNotDashboardOwnerError(str(self._dashboard_id))
|
|
||||||
|
|
||||||
def _validate_owner_id_is_dashboard_id(self) -> None:
|
|
||||||
if (
|
|
||||||
self._properties.get(OWNER_ID_FIELD, self._dashboard_id)
|
|
||||||
!= self._dashboard_id
|
|
||||||
):
|
|
||||||
raise DashboardIdInconsistencyError(str(self._dashboard_id))
|
|
|
@ -1,54 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from superset.commands.dashboard.filter_set.base import BaseFilterSetCommand
|
|
||||||
from superset.commands.dashboard.filter_set.exceptions import (
|
|
||||||
FilterSetDeleteFailedError,
|
|
||||||
FilterSetForbiddenError,
|
|
||||||
FilterSetNotFoundError,
|
|
||||||
)
|
|
||||||
from superset.daos.dashboard import FilterSetDAO
|
|
||||||
from superset.daos.exceptions import DAODeleteFailedError
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteFilterSetCommand(BaseFilterSetCommand):
|
|
||||||
def __init__(self, dashboard_id: int, filter_set_id: int):
|
|
||||||
super().__init__(dashboard_id)
|
|
||||||
self._filter_set_id = filter_set_id
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self.validate()
|
|
||||||
assert self._filter_set
|
|
||||||
|
|
||||||
try:
|
|
||||||
FilterSetDAO.delete([self._filter_set])
|
|
||||||
except DAODeleteFailedError as err:
|
|
||||||
raise FilterSetDeleteFailedError(str(self._filter_set_id), "") from err
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
|
||||||
self._validate_filterset_dashboard_exists()
|
|
||||||
try:
|
|
||||||
self.validate_exist_filter_use_cases_set()
|
|
||||||
except FilterSetNotFoundError as err:
|
|
||||||
if FilterSetDAO.find_by_id(self._filter_set_id): # type: ignore
|
|
||||||
raise FilterSetForbiddenError(
|
|
||||||
f"the filter-set does not related to dashboard {self._dashboard_id}"
|
|
||||||
) from err
|
|
||||||
raise err
|
|
|
@ -1,94 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from flask_babel import lazy_gettext as _
|
|
||||||
|
|
||||||
from superset.commands.exceptions import (
|
|
||||||
CreateFailedError,
|
|
||||||
DeleteFailedError,
|
|
||||||
ForbiddenError,
|
|
||||||
ObjectNotFoundError,
|
|
||||||
UpdateFailedError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetNotFoundError(ObjectNotFoundError):
|
|
||||||
def __init__(
|
|
||||||
self, filterset_id: Optional[str] = None, exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__("FilterSet", filterset_id, exception)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetCreateFailedError(CreateFailedError):
|
|
||||||
base_message = 'CreateFilterSetCommand of dashboard "%s" failed: '
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, dashboard_id: str, reason: str = "", exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__((self.base_message % dashboard_id) + reason, exception)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetUpdateFailedError(UpdateFailedError):
|
|
||||||
base_message = 'UpdateFilterSetCommand of filter_set "%s" failed: '
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__((self.base_message % filterset_id) + reason, exception)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetDeleteFailedError(DeleteFailedError):
|
|
||||||
base_message = 'DeleteFilterSetCommand of filter_set "%s" failed: '
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__((self.base_message % filterset_id) + reason, exception)
|
|
||||||
|
|
||||||
|
|
||||||
class UserIsNotDashboardOwnerError(FilterSetCreateFailedError):
|
|
||||||
reason = (
|
|
||||||
"cannot create dashboard owner filterset based when"
|
|
||||||
" the user is not the dashboard owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, dashboard_id: str, exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__(dashboard_id, self.reason, exception)
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardIdInconsistencyError(FilterSetCreateFailedError):
|
|
||||||
reason = (
|
|
||||||
"cannot create dashboard owner filterset based when the"
|
|
||||||
" ownerid is not the dashboard id"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, dashboard_id: str, exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__(dashboard_id, self.reason, exception)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetForbiddenError(ForbiddenError):
|
|
||||||
message_format = 'Changing FilterSet "{}" is forbidden: {}'
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
|
|
||||||
) -> None:
|
|
||||||
super().__init__(_(self.message_format.format(filterset_id, reason)), exception)
|
|
|
@ -1,53 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from flask_appbuilder.models.sqla import Model
|
|
||||||
|
|
||||||
from superset.commands.dashboard.filter_set.base import BaseFilterSetCommand
|
|
||||||
from superset.commands.dashboard.filter_set.exceptions import FilterSetUpdateFailedError
|
|
||||||
from superset.daos.dashboard import FilterSetDAO
|
|
||||||
from superset.daos.exceptions import DAOUpdateFailedError
|
|
||||||
from superset.dashboards.filter_sets.consts import OWNER_ID_FIELD, OWNER_TYPE_FIELD
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateFilterSetCommand(BaseFilterSetCommand):
|
|
||||||
def __init__(self, dashboard_id: int, filter_set_id: int, data: dict[str, Any]):
|
|
||||||
super().__init__(dashboard_id)
|
|
||||||
self._filter_set_id = filter_set_id
|
|
||||||
self._properties = data.copy()
|
|
||||||
|
|
||||||
def run(self) -> Model:
|
|
||||||
try:
|
|
||||||
self.validate()
|
|
||||||
assert self._filter_set
|
|
||||||
|
|
||||||
if (
|
|
||||||
OWNER_TYPE_FIELD in self._properties
|
|
||||||
and self._properties[OWNER_TYPE_FIELD] == "Dashboard"
|
|
||||||
):
|
|
||||||
self._properties[OWNER_ID_FIELD] = self._dashboard_id
|
|
||||||
return FilterSetDAO.update(self._filter_set, self._properties, commit=True)
|
|
||||||
except DAOUpdateFailedError as err:
|
|
||||||
raise FilterSetUpdateFailedError(str(self._filter_set_id), "") from err
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
|
||||||
self._validate_filterset_dashboard_exists()
|
|
||||||
self.validate_exist_filter_use_cases_set()
|
|
|
@ -443,8 +443,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
|
||||||
"ESCAPE_MARKDOWN_HTML": False,
|
"ESCAPE_MARKDOWN_HTML": False,
|
||||||
"DASHBOARD_NATIVE_FILTERS": True, # deprecated
|
"DASHBOARD_NATIVE_FILTERS": True, # deprecated
|
||||||
"DASHBOARD_CROSS_FILTERS": True,
|
"DASHBOARD_CROSS_FILTERS": True,
|
||||||
# Feature is under active development and breaking changes are expected
|
|
||||||
"DASHBOARD_NATIVE_FILTERS_SET": False, # deprecated
|
|
||||||
"DASHBOARD_FILTERS_EXPERIMENTAL": False, # deprecated
|
"DASHBOARD_FILTERS_EXPERIMENTAL": False, # deprecated
|
||||||
"DASHBOARD_VIRTUALIZATION": False,
|
"DASHBOARD_VIRTUALIZATION": False,
|
||||||
"GLOBAL_ASYNC_QUERIES": False,
|
"GLOBAL_ASYNC_QUERIES": False,
|
||||||
|
|
|
@ -31,21 +31,12 @@ from superset.commands.dashboard.exceptions import (
|
||||||
DashboardNotFoundError,
|
DashboardNotFoundError,
|
||||||
)
|
)
|
||||||
from superset.daos.base import BaseDAO
|
from superset.daos.base import BaseDAO
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_ID_FIELD,
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
NAME_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
)
|
|
||||||
from superset.dashboards.filters import DashboardAccessFilter, is_uuid
|
from superset.dashboards.filters import DashboardAccessFilter, is_uuid
|
||||||
from superset.exceptions import SupersetSecurityException
|
from superset.exceptions import SupersetSecurityException
|
||||||
from superset.extensions import db
|
from superset.extensions import db
|
||||||
from superset.models.core import FavStar, FavStarClassName
|
from superset.models.core import FavStar, FavStarClassName
|
||||||
from superset.models.dashboard import Dashboard, id_or_slug_filter
|
from superset.models.dashboard import Dashboard, id_or_slug_filter
|
||||||
from superset.models.embedded_dashboard import EmbeddedDashboard
|
from superset.models.embedded_dashboard import EmbeddedDashboard
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.utils.core import get_user_id
|
from superset.utils.core import get_user_id
|
||||||
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
|
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
|
||||||
|
@ -387,29 +378,3 @@ class EmbeddedDashboardDAO(BaseDAO[EmbeddedDashboard]):
|
||||||
At least, until we are ok with more than one embedded item per dashboard.
|
At least, until we are ok with more than one embedded item per dashboard.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Use EmbeddedDashboardDAO.upsert() instead.")
|
raise NotImplementedError("Use EmbeddedDashboardDAO.upsert() instead.")
|
||||||
|
|
||||||
|
|
||||||
class FilterSetDAO(BaseDAO[FilterSet]):
|
|
||||||
@classmethod
|
|
||||||
def create(
|
|
||||||
cls,
|
|
||||||
item: FilterSet | None = None,
|
|
||||||
attributes: dict[str, Any] | None = None,
|
|
||||||
commit: bool = True,
|
|
||||||
) -> FilterSet:
|
|
||||||
if not item:
|
|
||||||
item = FilterSet()
|
|
||||||
|
|
||||||
if attributes:
|
|
||||||
setattr(item, NAME_FIELD, attributes[NAME_FIELD])
|
|
||||||
setattr(item, JSON_METADATA_FIELD, attributes[JSON_METADATA_FIELD])
|
|
||||||
setattr(item, DESCRIPTION_FIELD, attributes.get(DESCRIPTION_FIELD, None))
|
|
||||||
setattr(
|
|
||||||
item,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
attributes.get(OWNER_ID_FIELD, attributes[DASHBOARD_ID_FIELD]),
|
|
||||||
)
|
|
||||||
setattr(item, OWNER_TYPE_FIELD, attributes[OWNER_TYPE_FIELD])
|
|
||||||
setattr(item, DASHBOARD_ID_FIELD, attributes[DASHBOARD_ID_FIELD])
|
|
||||||
|
|
||||||
return super().create(item, commit=commit)
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
|
@ -1,381 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import logging
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from flask import request, Response
|
|
||||||
from flask_appbuilder.api import (
|
|
||||||
expose,
|
|
||||||
get_list_schema,
|
|
||||||
permission_name,
|
|
||||||
protect,
|
|
||||||
rison,
|
|
||||||
safe,
|
|
||||||
)
|
|
||||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
|
||||||
from marshmallow import ValidationError
|
|
||||||
|
|
||||||
from superset.commands.dashboard.exceptions import DashboardNotFoundError
|
|
||||||
from superset.commands.dashboard.filter_set.create import CreateFilterSetCommand
|
|
||||||
from superset.commands.dashboard.filter_set.delete import DeleteFilterSetCommand
|
|
||||||
from superset.commands.dashboard.filter_set.exceptions import (
|
|
||||||
FilterSetCreateFailedError,
|
|
||||||
FilterSetDeleteFailedError,
|
|
||||||
FilterSetForbiddenError,
|
|
||||||
FilterSetNotFoundError,
|
|
||||||
FilterSetUpdateFailedError,
|
|
||||||
UserIsNotDashboardOwnerError,
|
|
||||||
)
|
|
||||||
from superset.commands.dashboard.filter_set.update import UpdateFilterSetCommand
|
|
||||||
from superset.commands.exceptions import ObjectNotFoundError
|
|
||||||
from superset.daos.dashboard import DashboardDAO
|
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_FIELD,
|
|
||||||
DASHBOARD_ID_FIELD,
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
FILTER_SET_API_PERMISSIONS_NAME,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
NAME_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
OWNER_OBJECT_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
PARAMS_PROPERTY,
|
|
||||||
)
|
|
||||||
from superset.dashboards.filter_sets.filters import FilterSetFilter
|
|
||||||
from superset.dashboards.filter_sets.schemas import (
|
|
||||||
FilterSetPostSchema,
|
|
||||||
FilterSetPutSchema,
|
|
||||||
)
|
|
||||||
from superset.extensions import event_logger
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from superset.views.base_api import (
|
|
||||||
BaseSupersetModelRestApi,
|
|
||||||
requires_json,
|
|
||||||
statsd_metrics,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetRestApi(BaseSupersetModelRestApi):
|
|
||||||
# pylint: disable=arguments-differ
|
|
||||||
include_route_methods = {"get_list", "put", "post", "delete"}
|
|
||||||
datamodel = SQLAInterface(FilterSet)
|
|
||||||
resource_name = "dashboard"
|
|
||||||
class_permission_name = FILTER_SET_API_PERMISSIONS_NAME
|
|
||||||
allow_browser_login = True
|
|
||||||
csrf_exempt = False
|
|
||||||
add_exclude_columns = [
|
|
||||||
"id",
|
|
||||||
OWNER_OBJECT_FIELD,
|
|
||||||
DASHBOARD_FIELD,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
]
|
|
||||||
add_model_schema = FilterSetPostSchema()
|
|
||||||
edit_model_schema = FilterSetPutSchema()
|
|
||||||
edit_exclude_columns = [
|
|
||||||
"id",
|
|
||||||
OWNER_OBJECT_FIELD,
|
|
||||||
DASHBOARD_FIELD,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
]
|
|
||||||
list_columns = [
|
|
||||||
"id",
|
|
||||||
"created_on",
|
|
||||||
"changed_on",
|
|
||||||
"created_by_fk",
|
|
||||||
"changed_by_fk",
|
|
||||||
NAME_FIELD,
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
DASHBOARD_ID_FIELD,
|
|
||||||
PARAMS_PROPERTY,
|
|
||||||
]
|
|
||||||
show_columns = [
|
|
||||||
"id",
|
|
||||||
NAME_FIELD,
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
DASHBOARD_ID_FIELD,
|
|
||||||
PARAMS_PROPERTY,
|
|
||||||
]
|
|
||||||
search_columns = ["id", NAME_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD]
|
|
||||||
base_filters = [[OWNER_ID_FIELD, FilterSetFilter, ""]]
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.datamodel.get_search_columns_list = lambda: []
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def _init_properties(self) -> None:
|
|
||||||
super(BaseSupersetModelRestApi, self)._init_properties()
|
|
||||||
|
|
||||||
@expose("/<int:dashboard_id>/filtersets", methods=("GET",))
|
|
||||||
@protect()
|
|
||||||
@safe
|
|
||||||
@permission_name("get")
|
|
||||||
@rison(get_list_schema)
|
|
||||||
def get_list(self, dashboard_id: int, **kwargs: Any) -> Response:
|
|
||||||
"""Get a dashboard's list of filter sets.
|
|
||||||
---
|
|
||||||
get:
|
|
||||||
summary: Get a dashboard's list of filter sets
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: dashboard_id
|
|
||||||
description: The id of the dashboard
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: FilterSets
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
description: Name of the Filter set
|
|
||||||
type: string
|
|
||||||
json_metadata:
|
|
||||||
description: metadata of the filter set
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
description: A description field of the filter set
|
|
||||||
type: string
|
|
||||||
owner_id:
|
|
||||||
description: A description field of the filter set
|
|
||||||
type: integer
|
|
||||||
owner_type:
|
|
||||||
description: the Type of the owner ( Dashboard/User)
|
|
||||||
type: integer
|
|
||||||
parameters:
|
|
||||||
description: JSON schema defining the needed parameters
|
|
||||||
302:
|
|
||||||
description: Redirects to the current digest
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
"""
|
|
||||||
if not DashboardDAO.find_by_id(cast(int, dashboard_id)):
|
|
||||||
return self.response(404, message=f"dashboard '{dashboard_id}' not found")
|
|
||||||
rison_data = kwargs.setdefault("rison", {})
|
|
||||||
rison_data.setdefault("filters", [])
|
|
||||||
rison_data["filters"].append(
|
|
||||||
{"col": "dashboard_id", "opr": "eq", "value": str(dashboard_id)}
|
|
||||||
)
|
|
||||||
return self.get_list_headless(**kwargs)
|
|
||||||
|
|
||||||
@expose("/<int:dashboard_id>/filtersets", methods=("POST",))
|
|
||||||
@protect()
|
|
||||||
@safe
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
@requires_json
|
|
||||||
def post(self, dashboard_id: int) -> Response:
|
|
||||||
"""Create a new dashboard's filter set.
|
|
||||||
---
|
|
||||||
post:
|
|
||||||
summary: Create a new dashboard's filter set
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: dashboard_id
|
|
||||||
description: The id of the dashboard
|
|
||||||
requestBody:
|
|
||||||
description: Filter set schema
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
|
||||||
responses:
|
|
||||||
201:
|
|
||||||
description: Filter set added
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: number
|
|
||||||
result:
|
|
||||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
|
||||||
302:
|
|
||||||
description: Redirects to the current digest
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
item = self.add_model_schema.load(request.json)
|
|
||||||
new_model = CreateFilterSetCommand(dashboard_id, item).run()
|
|
||||||
return self.response(
|
|
||||||
201, **self.show_model_schema.dump(new_model, many=False)
|
|
||||||
)
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
except UserIsNotDashboardOwnerError:
|
|
||||||
return self.response_403()
|
|
||||||
except FilterSetCreateFailedError as error:
|
|
||||||
return self.response_400(message=error.message)
|
|
||||||
except DashboardNotFoundError:
|
|
||||||
return self.response_404()
|
|
||||||
|
|
||||||
@expose("/<int:dashboard_id>/filtersets/<int:pk>", methods=("PUT",))
|
|
||||||
@protect()
|
|
||||||
@safe
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put",
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
@requires_json
|
|
||||||
def put(self, dashboard_id: int, pk: int) -> Response:
|
|
||||||
"""Update a dashboard's filter set.
|
|
||||||
---
|
|
||||||
put:
|
|
||||||
summary: Update a dashboard's filter set
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: dashboard_id
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: pk
|
|
||||||
requestBody:
|
|
||||||
description: Filter set schema
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.put'
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Filter set changed
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: number
|
|
||||||
result:
|
|
||||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.put'
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
403:
|
|
||||||
$ref: '#/components/responses/403'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
422:
|
|
||||||
$ref: '#/components/responses/422'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
item = self.edit_model_schema.load(request.json)
|
|
||||||
changed_model = UpdateFilterSetCommand(dashboard_id, pk, item).run()
|
|
||||||
return self.response(
|
|
||||||
200, **self.show_model_schema.dump(changed_model, many=False)
|
|
||||||
)
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
except (
|
|
||||||
ObjectNotFoundError,
|
|
||||||
FilterSetForbiddenError,
|
|
||||||
FilterSetUpdateFailedError,
|
|
||||||
) as err:
|
|
||||||
logger.error(err)
|
|
||||||
return self.response(err.status)
|
|
||||||
|
|
||||||
@expose("/<int:dashboard_id>/filtersets/<int:pk>", methods=("DELETE",))
|
|
||||||
@protect()
|
|
||||||
@safe
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete",
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
def delete(self, dashboard_id: int, pk: int) -> Response:
|
|
||||||
"""Delete a dashboard's filter set.
|
|
||||||
---
|
|
||||||
delete:
|
|
||||||
summary: Delete a dashboard's filter set
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: dashboard_id
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: pk
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Filter set deleted
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
403:
|
|
||||||
$ref: '#/components/responses/403'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
422:
|
|
||||||
$ref: '#/components/responses/422'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
DeleteFilterSetCommand(dashboard_id, pk).run()
|
|
||||||
return self.response(200, id=dashboard_id)
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
except FilterSetNotFoundError:
|
|
||||||
return self.response(200)
|
|
||||||
except (
|
|
||||||
ObjectNotFoundError,
|
|
||||||
FilterSetForbiddenError,
|
|
||||||
FilterSetDeleteFailedError,
|
|
||||||
) as err:
|
|
||||||
logger.error(err)
|
|
||||||
return self.response(err.status)
|
|
|
@ -1,58 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING
|
|
||||||
|
|
||||||
from sqlalchemy import and_, or_
|
|
||||||
|
|
||||||
from superset import security_manager
|
|
||||||
from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, USER_OWNER_TYPE
|
|
||||||
from superset.models.dashboard import dashboard_user
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from superset.utils.core import get_user_id
|
|
||||||
from superset.views.base import BaseFilter
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sqlalchemy.orm.query import Query
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods)
|
|
||||||
def apply(self, query: Query, value: Any) -> Query:
|
|
||||||
if security_manager.is_admin():
|
|
||||||
return query
|
|
||||||
|
|
||||||
filter_set_ids_by_dashboard_owners = ( # pylint: disable=C0103
|
|
||||||
query.from_self(FilterSet.id)
|
|
||||||
.join(dashboard_user, FilterSet.owner_id == dashboard_user.c.dashboard_id)
|
|
||||||
.filter(
|
|
||||||
and_(
|
|
||||||
FilterSet.owner_type == DASHBOARD_OWNER_TYPE,
|
|
||||||
dashboard_user.c.user_id == get_user_id(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return query.filter(
|
|
||||||
or_(
|
|
||||||
and_(
|
|
||||||
FilterSet.owner_type == USER_OWNER_TYPE,
|
|
||||||
FilterSet.owner_id == get_user_id(),
|
|
||||||
),
|
|
||||||
FilterSet.id.in_(filter_set_ids_by_dashboard_owners),
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -1,97 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from marshmallow import fields, post_load, Schema, ValidationError
|
|
||||||
from marshmallow.validate import Length, OneOf
|
|
||||||
|
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_OWNER_TYPE,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
USER_OWNER_TYPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class JsonMetadataSchema(Schema):
|
|
||||||
nativeFilters = fields.Mapping(required=True, allow_none=False)
|
|
||||||
dataMask = fields.Mapping(required=False, allow_none=False)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetSchema(Schema):
|
|
||||||
json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
|
|
||||||
|
|
||||||
def _validate_json_meta_data(self, json_meta_data: str) -> None:
|
|
||||||
try:
|
|
||||||
self.json_metadata_schema.loads(json_meta_data)
|
|
||||||
except Exception as ex:
|
|
||||||
raise ValidationError("failed to parse json_metadata to json") from ex
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetPostSchema(FilterSetSchema):
|
|
||||||
json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
|
|
||||||
name = fields.String(
|
|
||||||
required=True,
|
|
||||||
allow_none=False,
|
|
||||||
validate=Length(0, 500),
|
|
||||||
)
|
|
||||||
description = fields.String(
|
|
||||||
required=False, allow_none=True, validate=[Length(1, 1000)]
|
|
||||||
)
|
|
||||||
json_metadata = fields.String(allow_none=False, required=True)
|
|
||||||
|
|
||||||
owner_type = fields.String(
|
|
||||||
required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])
|
|
||||||
)
|
|
||||||
owner_id = fields.Int(required=False)
|
|
||||||
|
|
||||||
@post_load
|
|
||||||
def validate(
|
|
||||||
self, data: Mapping[Any, Any], *, many: Any, partial: Any
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
self._validate_json_meta_data(data[JSON_METADATA_FIELD])
|
|
||||||
if data[OWNER_TYPE_FIELD] == USER_OWNER_TYPE and OWNER_ID_FIELD not in data:
|
|
||||||
raise ValidationError("owner_id is mandatory when owner_type is User")
|
|
||||||
return cast(dict[str, Any], data)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSetPutSchema(FilterSetSchema):
|
|
||||||
name = fields.String(required=False, allow_none=False, validate=Length(0, 500))
|
|
||||||
description = fields.String(
|
|
||||||
required=False, allow_none=False, validate=[Length(1, 1000)]
|
|
||||||
)
|
|
||||||
json_metadata = fields.String(required=False, allow_none=False)
|
|
||||||
owner_type = fields.String(
|
|
||||||
allow_none=False, required=False, validate=OneOf([DASHBOARD_OWNER_TYPE])
|
|
||||||
)
|
|
||||||
|
|
||||||
@post_load
|
|
||||||
def validate(
|
|
||||||
self, data: Mapping[Any, Any], *, many: Any, partial: Any
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
if JSON_METADATA_FIELD in data:
|
|
||||||
self._validate_json_meta_data(data[JSON_METADATA_FIELD])
|
|
||||||
return cast(dict[str, Any], data)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_pair(first_field: str, second_field: str, data: dict[str, Any]) -> None:
|
|
||||||
if first_field in data and second_field not in data:
|
|
||||||
raise ValidationError(
|
|
||||||
f"{first_field} must be included alongside {second_field}"
|
|
||||||
)
|
|
|
@ -113,8 +113,6 @@ class DashboardJSONMetadataSchema(Schema):
|
||||||
# global_chart_configuration keeps data about global cross-filter scoping
|
# global_chart_configuration keeps data about global cross-filter scoping
|
||||||
# for charts - can be overridden by chart_configuration for each chart
|
# for charts - can be overridden by chart_configuration for each chart
|
||||||
global_chart_configuration = fields.Dict()
|
global_chart_configuration = fields.Dict()
|
||||||
# filter_sets_configuration is for dashboard-native filters
|
|
||||||
filter_sets_configuration = fields.List(fields.Dict(), allow_none=True)
|
|
||||||
timed_refresh_immune_slices = fields.List(fields.Integer())
|
timed_refresh_immune_slices = fields.List(fields.Integer())
|
||||||
# deprecated wrt dashboard-native filters
|
# deprecated wrt dashboard-native filters
|
||||||
filter_scopes = fields.Dict()
|
filter_scopes = fields.Dict()
|
||||||
|
|
|
@ -135,7 +135,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
)
|
)
|
||||||
from superset.css_templates.api import CssTemplateRestApi
|
from superset.css_templates.api import CssTemplateRestApi
|
||||||
from superset.dashboards.api import DashboardRestApi
|
from superset.dashboards.api import DashboardRestApi
|
||||||
from superset.dashboards.filter_sets.api import FilterSetRestApi
|
|
||||||
from superset.dashboards.filter_state.api import DashboardFilterStateRestApi
|
from superset.dashboards.filter_state.api import DashboardFilterStateRestApi
|
||||||
from superset.dashboards.permalink.api import DashboardPermalinkRestApi
|
from superset.dashboards.permalink.api import DashboardPermalinkRestApi
|
||||||
from superset.databases.api import DatabaseRestApi
|
from superset.databases.api import DatabaseRestApi
|
||||||
|
@ -222,7 +221,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
||||||
appbuilder.add_api(ExploreRestApi)
|
appbuilder.add_api(ExploreRestApi)
|
||||||
appbuilder.add_api(ExploreFormDataRestApi)
|
appbuilder.add_api(ExploreFormDataRestApi)
|
||||||
appbuilder.add_api(ExplorePermalinkRestApi)
|
appbuilder.add_api(ExplorePermalinkRestApi)
|
||||||
appbuilder.add_api(FilterSetRestApi)
|
|
||||||
appbuilder.add_api(ImportExportRestApi)
|
appbuilder.add_api(ImportExportRestApi)
|
||||||
appbuilder.add_api(QueryRestApi)
|
appbuilder.add_api(QueryRestApi)
|
||||||
appbuilder.add_api(ReportScheduleRestApi)
|
appbuilder.add_api(ReportScheduleRestApi)
|
||||||
|
|
|
@ -14,17 +14,30 @@
|
||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
USER_OWNER_TYPE = "User"
|
"""drop_filter_sets_table
|
||||||
DASHBOARD_OWNER_TYPE = "Dashboard"
|
|
||||||
|
|
||||||
NAME_FIELD = "name"
|
Revision ID: 59a1450b3c10
|
||||||
DESCRIPTION_FIELD = "description"
|
Revises: 65a167d4c62e
|
||||||
JSON_METADATA_FIELD = "json_metadata"
|
Create Date: 2023-12-27 13:14:27.268232
|
||||||
OWNER_ID_FIELD = "owner_id"
|
|
||||||
OWNER_TYPE_FIELD = "owner_type"
|
|
||||||
DASHBOARD_ID_FIELD = "dashboard_id"
|
|
||||||
OWNER_OBJECT_FIELD = "owner_object"
|
|
||||||
DASHBOARD_FIELD = "dashboard"
|
|
||||||
PARAMS_PROPERTY = "params"
|
|
||||||
|
|
||||||
FILTER_SET_API_PERMISSIONS_NAME = "FilterSets"
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "59a1450b3c10"
|
||||||
|
down_revision = "65a167d4c62e"
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
module = import_module(
|
||||||
|
"superset.migrations.versions.2021-03-29_11-15_3ebe0993c770_filterset_table"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
module.downgrade()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
module.upgrade()
|
|
@ -55,7 +55,6 @@ from superset.connectors.sqla.models import (
|
||||||
)
|
)
|
||||||
from superset.daos.datasource import DatasourceDAO
|
from superset.daos.datasource import DatasourceDAO
|
||||||
from superset.extensions import cache_manager
|
from superset.extensions import cache_manager
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
|
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.user_attributes import UserAttribute
|
from superset.models.user_attributes import UserAttribute
|
||||||
|
@ -63,7 +62,6 @@ from superset.tasks.thumbnails import cache_dashboard_thumbnail
|
||||||
from superset.tasks.utils import get_current_user
|
from superset.tasks.utils import get_current_user
|
||||||
from superset.thumbnails.digest import get_dashboard_digest
|
from superset.thumbnails.digest import get_dashboard_digest
|
||||||
from superset.utils import core as utils
|
from superset.utils import core as utils
|
||||||
from superset.utils.core import get_user_id
|
|
||||||
from superset.utils.decorators import debounce
|
from superset.utils.decorators import debounce
|
||||||
|
|
||||||
metadata = Model.metadata # pylint: disable=no-member
|
metadata = Model.metadata # pylint: disable=no-member
|
||||||
|
@ -185,9 +183,6 @@ class Dashboard(AuditMixinNullable, ImportExportMixin, Model):
|
||||||
back_populates="dashboard",
|
back_populates="dashboard",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
_filter_sets = relationship(
|
|
||||||
"FilterSet", back_populates="dashboard", cascade="all, delete"
|
|
||||||
)
|
|
||||||
export_fields = [
|
export_fields = [
|
||||||
"dashboard_title",
|
"dashboard_title",
|
||||||
"position_json",
|
"position_json",
|
||||||
|
@ -231,28 +226,6 @@ class Dashboard(AuditMixinNullable, ImportExportMixin, Model):
|
||||||
.all()
|
.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
|
||||||
def filter_sets(self) -> dict[int, FilterSet]:
|
|
||||||
return {fs.id: fs for fs in self._filter_sets}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filter_sets_lst(self) -> dict[int, FilterSet]:
|
|
||||||
if security_manager.is_admin():
|
|
||||||
return self._filter_sets
|
|
||||||
filter_sets_by_owner_type: dict[str, list[Any]] = {"Dashboard": [], "User": []}
|
|
||||||
for fs in self._filter_sets:
|
|
||||||
filter_sets_by_owner_type[fs.owner_type].append(fs)
|
|
||||||
user_filter_sets = list(
|
|
||||||
filter(
|
|
||||||
lambda filter_set: filter_set.owner_id == get_user_id(),
|
|
||||||
filter_sets_by_owner_type["User"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
fs.id: fs
|
|
||||||
for fs in user_filter_sets + filter_sets_by_owner_type["Dashboard"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def charts(self) -> list[str]:
|
def charts(self) -> list[str]:
|
||||||
return [slc.chart for slc in self.slices]
|
return [slc.chart for slc in self.slices]
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from flask_appbuilder import Model
|
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy_utils import generic_relationship
|
|
||||||
|
|
||||||
from superset import app, db
|
|
||||||
from superset.models.helpers import AuditMixinNullable
|
|
||||||
|
|
||||||
metadata = Model.metadata # pylint: disable=no-member
|
|
||||||
config = app.config
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSet(Model, AuditMixinNullable):
|
|
||||||
__tablename__ = "filter_sets"
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
name = Column(String(500), nullable=False, unique=True)
|
|
||||||
description = Column(Text, nullable=True)
|
|
||||||
json_metadata = Column(Text, nullable=False)
|
|
||||||
dashboard_id = Column(Integer, ForeignKey("dashboards.id"))
|
|
||||||
dashboard = relationship("Dashboard", back_populates="_filter_sets")
|
|
||||||
owner_id = Column(Integer, nullable=False)
|
|
||||||
owner_type = Column(String(255), nullable=False)
|
|
||||||
owner_object = generic_relationship(owner_type, owner_id)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"FilterSet<{self.name or self.id}>"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self) -> str:
|
|
||||||
return f"/api/filtersets/{self.id}/"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sqla_metadata(self) -> None:
|
|
||||||
# pylint: disable=no-member
|
|
||||||
with self.get_sqla_engine_with_context() as engine:
|
|
||||||
meta = MetaData(bind=engine)
|
|
||||||
meta.reflect()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def changed_by_name(self) -> str:
|
|
||||||
if not self.changed_by:
|
|
||||||
return ""
|
|
||||||
return str(self.changed_by)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"id": self.id,
|
|
||||||
"name": self.name,
|
|
||||||
"description": self.description,
|
|
||||||
"params": self.params,
|
|
||||||
"dashboard_id": self.dashboard_id,
|
|
||||||
"owner_type": self.owner_type,
|
|
||||||
"owner_id": self.owner_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, _id: int) -> FilterSet:
|
|
||||||
session = db.session()
|
|
||||||
qry = session.query(FilterSet).filter(_id)
|
|
||||||
return qry.one_or_none()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_name(cls, name: str) -> FilterSet:
|
|
||||||
session = db.session()
|
|
||||||
qry = session.query(FilterSet).filter(FilterSet.name == name)
|
|
||||||
return qry.one_or_none()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_dashboard_id(cls, dashboard_id: int) -> FilterSet:
|
|
||||||
session = db.session()
|
|
||||||
qry = session.query(FilterSet).filter(FilterSet.dashboard_id == dashboard_id)
|
|
||||||
return qry.all()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def params(self) -> dict[str, Any]:
|
|
||||||
if self.json_metadata:
|
|
||||||
return json.loads(self.json_metadata)
|
|
||||||
return {}
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
|
@ -1,286 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from collections.abc import Generator
|
|
||||||
from typing import Any, TYPE_CHECKING
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from superset import db, security_manager as sm
|
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
NAME_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
USER_OWNER_TYPE,
|
|
||||||
)
|
|
||||||
from superset.models.dashboard import Dashboard
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.consts import (
|
|
||||||
ADMIN_USERNAME_FOR_TEST,
|
|
||||||
DASHBOARD_OWNER_USERNAME,
|
|
||||||
FILTER_SET_OWNER_USERNAME,
|
|
||||||
REGULAR_USER,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.superset_factory_util import (
|
|
||||||
create_dashboard,
|
|
||||||
create_database,
|
|
||||||
create_datasource_table,
|
|
||||||
create_slice,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.test_app import app
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from flask.ctx import AppContext
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
from flask_appbuilder.security.manager import BaseSecurityManager
|
|
||||||
from flask_appbuilder.security.sqla.models import (
|
|
||||||
PermissionView,
|
|
||||||
Role,
|
|
||||||
User,
|
|
||||||
ViewMenu,
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from superset.connectors.sqla.models import SqlaTable
|
|
||||||
from superset.models.core import Database
|
|
||||||
from superset.models.slice import Slice
|
|
||||||
|
|
||||||
|
|
||||||
security_manager: BaseSecurityManager = sm
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="module")
|
|
||||||
def test_users() -> Generator[dict[str, int], None, None]:
|
|
||||||
usernames = [
|
|
||||||
ADMIN_USERNAME_FOR_TEST,
|
|
||||||
DASHBOARD_OWNER_USERNAME,
|
|
||||||
FILTER_SET_OWNER_USERNAME,
|
|
||||||
REGULAR_USER,
|
|
||||||
]
|
|
||||||
with app.app_context():
|
|
||||||
filter_set_role = build_filter_set_role()
|
|
||||||
admin_role: Role = security_manager.find_role("Admin")
|
|
||||||
usernames_to_ids = create_test_users(admin_role, filter_set_role, usernames)
|
|
||||||
yield usernames_to_ids
|
|
||||||
delete_users(usernames_to_ids)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_users(usernames_to_ids: dict[str, int]) -> None:
|
|
||||||
for username in usernames_to_ids.keys():
|
|
||||||
db.session.delete(security_manager.find_user(username))
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def create_test_users(
|
|
||||||
admin_role: Role, filter_set_role: Role, usernames: list[str]
|
|
||||||
) -> dict[str, int]:
|
|
||||||
users: list[User] = []
|
|
||||||
for username in usernames:
|
|
||||||
user = build_user(username, filter_set_role, admin_role)
|
|
||||||
users.append(user)
|
|
||||||
return {user.username: user.id for user in users}
|
|
||||||
|
|
||||||
|
|
||||||
def build_user(username: str, filter_set_role: Role, admin_role: Role) -> User:
|
|
||||||
roles_to_add = (
|
|
||||||
[admin_role] if username == ADMIN_USERNAME_FOR_TEST else [filter_set_role]
|
|
||||||
)
|
|
||||||
user: User = security_manager.add_user(
|
|
||||||
username, "test", "test", username, roles_to_add, password="general"
|
|
||||||
)
|
|
||||||
if not user:
|
|
||||||
user = security_manager.find_user(username)
|
|
||||||
if user is None:
|
|
||||||
raise Exception(f"Failed to build the user {username}")
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def build_filter_set_role() -> Role:
|
|
||||||
filter_set_role: Role = security_manager.add_role("filter_set_role")
|
|
||||||
filterset_view_name: ViewMenu = security_manager.find_view_menu("FilterSets")
|
|
||||||
all_datasource_view_name: ViewMenu = security_manager.find_view_menu(
|
|
||||||
"all_datasource_access"
|
|
||||||
)
|
|
||||||
pvms: list[PermissionView] = security_manager.find_permissions_view_menu(
|
|
||||||
filterset_view_name
|
|
||||||
) + security_manager.find_permissions_view_menu(all_datasource_view_name)
|
|
||||||
for pvm in pvms:
|
|
||||||
security_manager.add_permission_role(filter_set_role, pvm)
|
|
||||||
return filter_set_role
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def client() -> Generator[FlaskClient[Any], None, None]:
|
|
||||||
with app.test_client() as client:
|
|
||||||
yield client
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def dashboard(app_context) -> Generator[Dashboard, None, None]:
|
|
||||||
dashboard_owner_user = security_manager.find_user(DASHBOARD_OWNER_USERNAME)
|
|
||||||
database = create_database("test_database_filter_sets")
|
|
||||||
datasource = create_datasource_table(
|
|
||||||
name="test_datasource", database=database, owners=[dashboard_owner_user]
|
|
||||||
)
|
|
||||||
slice_ = create_slice(
|
|
||||||
datasource=datasource, name="test_slice", owners=[dashboard_owner_user]
|
|
||||||
)
|
|
||||||
dashboard = create_dashboard(
|
|
||||||
dashboard_title="test_dashboard",
|
|
||||||
published=True,
|
|
||||||
slices=[slice_],
|
|
||||||
owners=[dashboard_owner_user],
|
|
||||||
)
|
|
||||||
db.session.add(dashboard)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
yield dashboard
|
|
||||||
|
|
||||||
db.session.delete(dashboard)
|
|
||||||
db.session.delete(slice_)
|
|
||||||
db.session.delete(datasource)
|
|
||||||
db.session.delete(database)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def dashboard_id(dashboard: Dashboard) -> Generator[int, None, None]:
|
|
||||||
yield dashboard.id
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def filtersets(
|
|
||||||
dashboard_id: int, test_users: dict[str, int], dumped_valid_json_metadata: str
|
|
||||||
) -> Generator[dict[str, list[FilterSet]], None, None]:
|
|
||||||
first_filter_set = FilterSet(
|
|
||||||
name="filter_set_1_of_" + str(dashboard_id),
|
|
||||||
dashboard_id=dashboard_id,
|
|
||||||
json_metadata=dumped_valid_json_metadata,
|
|
||||||
owner_id=dashboard_id,
|
|
||||||
owner_type="Dashboard",
|
|
||||||
)
|
|
||||||
second_filter_set = FilterSet(
|
|
||||||
name="filter_set_2_of_" + str(dashboard_id),
|
|
||||||
json_metadata=dumped_valid_json_metadata,
|
|
||||||
dashboard_id=dashboard_id,
|
|
||||||
owner_id=dashboard_id,
|
|
||||||
owner_type="Dashboard",
|
|
||||||
)
|
|
||||||
third_filter_set = FilterSet(
|
|
||||||
name="filter_set_3_of_" + str(dashboard_id),
|
|
||||||
json_metadata=dumped_valid_json_metadata,
|
|
||||||
dashboard_id=dashboard_id,
|
|
||||||
owner_id=test_users[FILTER_SET_OWNER_USERNAME],
|
|
||||||
owner_type="User",
|
|
||||||
)
|
|
||||||
fourth_filter_set = FilterSet(
|
|
||||||
name="filter_set_4_of_" + str(dashboard_id),
|
|
||||||
json_metadata=dumped_valid_json_metadata,
|
|
||||||
dashboard_id=dashboard_id,
|
|
||||||
owner_id=test_users[FILTER_SET_OWNER_USERNAME],
|
|
||||||
owner_type="User",
|
|
||||||
)
|
|
||||||
db.session.add(first_filter_set)
|
|
||||||
db.session.add(second_filter_set)
|
|
||||||
db.session.add(third_filter_set)
|
|
||||||
db.session.add(fourth_filter_set)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
yield {
|
|
||||||
"Dashboard": [first_filter_set, second_filter_set],
|
|
||||||
FILTER_SET_OWNER_USERNAME: [third_filter_set, fourth_filter_set],
|
|
||||||
}
|
|
||||||
|
|
||||||
db.session.delete(first_filter_set)
|
|
||||||
db.session.delete(second_filter_set)
|
|
||||||
db.session.delete(third_filter_set)
|
|
||||||
db.session.delete(fourth_filter_set)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def filterset_id(filtersets: dict[str, list[FilterSet]]) -> int:
|
|
||||||
return filtersets["Dashboard"][0].id
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def valid_json_metadata() -> dict[str, Any]:
|
|
||||||
return {"nativeFilters": {}}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def dumped_valid_json_metadata(valid_json_metadata: dict[str, Any]) -> str:
|
|
||||||
return json.dumps(valid_json_metadata)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def exists_user_id() -> int:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def valid_filter_set_data_for_create(
|
|
||||||
dashboard_id: int, dumped_valid_json_metadata: str, exists_user_id: int
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
name = "test_filter_set_of_dashboard_" + str(dashboard_id)
|
|
||||||
return {
|
|
||||||
NAME_FIELD: name,
|
|
||||||
DESCRIPTION_FIELD: "description of " + name,
|
|
||||||
JSON_METADATA_FIELD: dumped_valid_json_metadata,
|
|
||||||
OWNER_TYPE_FIELD: USER_OWNER_TYPE,
|
|
||||||
OWNER_ID_FIELD: exists_user_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def valid_filter_set_data_for_update(
|
|
||||||
dashboard_id: int, dumped_valid_json_metadata: str, exists_user_id: int
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
name = "name_changed_test_filter_set_of_dashboard_" + str(dashboard_id)
|
|
||||||
return {
|
|
||||||
NAME_FIELD: name,
|
|
||||||
DESCRIPTION_FIELD: "changed description of " + name,
|
|
||||||
JSON_METADATA_FIELD: dumped_valid_json_metadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def not_exists_dashboard_id(dashboard_id: int) -> Generator[int, None, None]:
|
|
||||||
yield dashboard_id + 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def not_exists_user_id() -> int:
|
|
||||||
return 99999
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def dashboard_based_filter_set_dict(
|
|
||||||
filtersets: dict[str, list[FilterSet]]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return filtersets["Dashboard"][0].to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def user_based_filter_set_dict(
|
|
||||||
filtersets: dict[str, list[FilterSet]]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return filtersets[FILTER_SET_OWNER_USERNAME][0].to_dict()
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
FILTER_SET_URI = "api/v1/dashboard/{dashboard_id}/filtersets"
|
|
||||||
|
|
||||||
ADMIN_USERNAME_FOR_TEST = "admin@filterset.com"
|
|
||||||
DASHBOARD_OWNER_USERNAME = "dash_owner_user@filterset.com"
|
|
||||||
FILTER_SET_OWNER_USERNAME = "fs_owner_user@filterset.com"
|
|
||||||
REGULAR_USER = "regular_user@filterset.com"
|
|
|
@ -1,629 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
|
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_OWNER_TYPE,
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
NAME_FIELD,
|
|
||||||
OWNER_ID_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
USER_OWNER_TYPE,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.consts import (
|
|
||||||
ADMIN_USERNAME_FOR_TEST,
|
|
||||||
DASHBOARD_OWNER_USERNAME,
|
|
||||||
FILTER_SET_OWNER_USERNAME,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.utils import (
|
|
||||||
call_create_filter_set,
|
|
||||||
get_filter_set_by_dashboard_id,
|
|
||||||
get_filter_set_by_name,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.test_app import login
|
|
||||||
|
|
||||||
|
|
||||||
def assert_filterset_was_not_created(filter_set_data: dict[str, Any]) -> None:
|
|
||||||
assert get_filter_set_by_name(str(filter_set_data["name"])) is None
|
|
||||||
|
|
||||||
|
|
||||||
def assert_filterset_was_created(filter_set_data: dict[str, Any]) -> None:
|
|
||||||
assert get_filter_set_by_name(filter_set_data["name"]) is not None
|
|
||||||
|
|
||||||
|
|
||||||
class TestCreateFilterSetsApi:
|
|
||||||
def test_with_extra_field__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create["extra"] = "val"
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json["message"]["extra"][0] == "Unknown field."
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_id_field__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create["id"] = 1
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json["message"]["id"][0] == "Unknown field."
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_dashboard_not_exists__404(
|
|
||||||
self,
|
|
||||||
not_exists_dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# act
|
|
||||||
login(client, "admin")
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, not_exists_dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_without_name__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create.pop(NAME_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert get_filter_set_by_dashboard_id(dashboard_id) == []
|
|
||||||
|
|
||||||
def test_with_none_name__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[NAME_FIELD] = None
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_int_as_name__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[NAME_FIELD] = 4
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_without_description__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create.pop(DESCRIPTION_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_none_description__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[DESCRIPTION_FIELD] = None
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_int_as_description__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[DESCRIPTION_FIELD] = 1
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_without_json_metadata__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create.pop(JSON_METADATA_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_invalid_json_metadata__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[DESCRIPTION_FIELD] = {}
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_without_owner_type__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create.pop(OWNER_TYPE_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_invalid_owner_type__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = "OTHER_TYPE"
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_without_owner_id_when_owner_type_is_user__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create.pop(OWNER_ID_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_without_owner_id_when_owner_type_is_dashboard__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create.pop(OWNER_ID_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_with_not_exists_owner__400(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
not_exists_user_id: int,
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = not_exists_user_id
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_is_admin__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
ADMIN_USERNAME_FOR_TEST
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
DASHBOARD_OWNER_USERNAME
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_is_regular_user__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
FILTER_SET_OWNER_USERNAME
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_type_is_dashboard__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
ADMIN_USERNAME_FOR_TEST
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
DASHBOARD_OWNER_USERNAME
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
FILTER_SET_OWNER_USERNAME
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_is_admin__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
ADMIN_USERNAME_FOR_TEST
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
DASHBOARD_OWNER_USERNAME
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_is_regular_user__201(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
|
||||||
FILTER_SET_OWNER_USERNAME
|
|
||||||
]
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert_filterset_was_created(valid_filter_set_data_for_create)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
valid_filter_set_data_for_create: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
|
||||||
valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_create_filter_set(
|
|
||||||
client, dashboard_id, valid_filter_set_data_for_create
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
|
|
@ -1,210 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING
|
|
||||||
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_OWNER_USERNAME,
|
|
||||||
FILTER_SET_OWNER_USERNAME,
|
|
||||||
REGULAR_USER,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.utils import (
|
|
||||||
call_delete_filter_set,
|
|
||||||
collect_all_ids,
|
|
||||||
get_filter_set_by_name,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.test_app import login
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
|
|
||||||
|
|
||||||
def assert_filterset_was_not_deleted(filter_set_dict: dict[str, Any]) -> None:
|
|
||||||
assert get_filter_set_by_name(filter_set_dict["name"]) is not None
|
|
||||||
|
|
||||||
|
|
||||||
def assert_filterset_deleted(filter_set_dict: dict[str, Any]) -> None:
|
|
||||||
assert get_filter_set_by_name(filter_set_dict["name"]) is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteFilterSet:
|
|
||||||
def test_with_dashboard_exists_filterset_not_exists__200(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
|
||||||
|
|
||||||
response = call_delete_filter_set(client, {"id": filter_set_id}, dashboard_id)
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
def test_with_dashboard_not_exists_filterset_not_exists__404(
|
|
||||||
self,
|
|
||||||
not_exists_dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
|
||||||
|
|
||||||
response = call_delete_filter_set(
|
|
||||||
client, {"id": filter_set_id}, not_exists_dashboard_id
|
|
||||||
)
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
def test_with_dashboard_not_exists_filterset_exists__404(
|
|
||||||
self,
|
|
||||||
not_exists_dashboard_id: int,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, not_exists_dashboard_id
|
|
||||||
)
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert_filterset_was_not_deleted(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_type_is_user__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_deleted(user_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_type_is_dashboard__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_deleted(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_is_other_user_403(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_deleted(user_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_deleted(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_filterset_owner__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_deleted(user_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_type_is_user__403(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, REGULAR_USER)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_deleted(user_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, REGULAR_USER)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_delete_filter_set(client, dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_deleted(dashboard_based_filter_set_dict)
|
|
|
@ -1,132 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING
|
|
||||||
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_OWNER_USERNAME,
|
|
||||||
FILTER_SET_OWNER_USERNAME,
|
|
||||||
REGULAR_USER,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.utils import (
|
|
||||||
call_get_filter_sets,
|
|
||||||
collect_all_ids,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.test_app import login
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetFilterSetsApi:
|
|
||||||
def test_with_dashboard_not_exists__404(
|
|
||||||
self,
|
|
||||||
not_exists_dashboard_id: int,
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_get_filter_sets(client, not_exists_dashboard_id)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
def test_dashboards_without_filtersets__200(
|
|
||||||
self, dashboard_id: int, client: FlaskClient[Any]
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_get_filter_sets(client, dashboard_id)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.is_json and response.json["count"] == 0
|
|
||||||
|
|
||||||
def test_when_caller_admin__200(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
expected_ids: set[int] = collect_all_ids(filtersets)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_get_filter_sets(client, dashboard_id)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.is_json and set(response.json["ids"]) == expected_ids
|
|
||||||
|
|
||||||
def test_when_caller_dashboard_owner__200(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
expected_ids = collect_all_ids(filtersets["Dashboard"])
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_get_filter_sets(client, dashboard_id)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.is_json and set(response.json["ids"]) == expected_ids
|
|
||||||
|
|
||||||
def test_when_caller_filterset_owner__200(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
expected_ids = collect_all_ids(filtersets[FILTER_SET_OWNER_USERNAME])
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_get_filter_sets(client, dashboard_id)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.is_json and set(response.json["ids"]) == expected_ids
|
|
||||||
|
|
||||||
def test_when_caller_regular_user__200(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[int]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, REGULAR_USER)
|
|
||||||
expected_ids: set[int] = set()
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_get_filter_sets(client, dashboard_id)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.is_json and set(response.json["ids"]) == expected_ids
|
|
|
@ -1,520 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import Any, TYPE_CHECKING
|
|
||||||
|
|
||||||
from superset.dashboards.filter_sets.consts import (
|
|
||||||
DESCRIPTION_FIELD,
|
|
||||||
JSON_METADATA_FIELD,
|
|
||||||
NAME_FIELD,
|
|
||||||
OWNER_TYPE_FIELD,
|
|
||||||
PARAMS_PROPERTY,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.consts import (
|
|
||||||
DASHBOARD_OWNER_USERNAME,
|
|
||||||
FILTER_SET_OWNER_USERNAME,
|
|
||||||
REGULAR_USER,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.utils import (
|
|
||||||
call_update_filter_set,
|
|
||||||
collect_all_ids,
|
|
||||||
get_filter_set_by_name,
|
|
||||||
)
|
|
||||||
from tests.integration_tests.test_app import login
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
|
|
||||||
|
|
||||||
def merge_two_filter_set_dict(
|
|
||||||
first: dict[Any, Any], second: dict[Any, Any]
|
|
||||||
) -> dict[Any, Any]:
|
|
||||||
for d in [first, second]:
|
|
||||||
if JSON_METADATA_FIELD in d:
|
|
||||||
if PARAMS_PROPERTY not in d:
|
|
||||||
d.setdefault(PARAMS_PROPERTY, json.loads(d[JSON_METADATA_FIELD]))
|
|
||||||
d.pop(JSON_METADATA_FIELD)
|
|
||||||
return {**first, **second}
|
|
||||||
|
|
||||||
|
|
||||||
def assert_filterset_was_not_updated(filter_set_dict: dict[str, Any]) -> None:
|
|
||||||
assert filter_set_dict == get_filter_set_by_name(filter_set_dict["name"]).to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
def assert_filterset_updated(
|
|
||||||
filter_set_dict_before: dict[str, Any], data_updated: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
expected_data = merge_two_filter_set_dict(filter_set_dict_before, data_updated)
|
|
||||||
assert expected_data == get_filter_set_by_name(expected_data["name"]).to_dict()
|
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateFilterSet:
|
|
||||||
def test_with_dashboard_exists_filterset_not_exists__404(
|
|
||||||
self,
|
|
||||||
dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
|
||||||
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, {"id": filter_set_id}, {}, dashboard_id
|
|
||||||
)
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
def test_with_dashboard_not_exists_filterset_not_exists__404(
|
|
||||||
self,
|
|
||||||
not_exists_dashboard_id: int,
|
|
||||||
filtersets: dict[str, list[FilterSet]],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
|
||||||
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, {"id": filter_set_id}, {}, not_exists_dashboard_id
|
|
||||||
)
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
def test_with_dashboard_not_exists_filterset_exists__404(
|
|
||||||
self,
|
|
||||||
not_exists_dashboard_id: int,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, {}, not_exists_dashboard_id
|
|
||||||
)
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_extra_field__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update["extra"] = "val"
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json["message"]["extra"][0] == "Unknown field."
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_id_field__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update["id"] = 1
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json["message"]["id"][0] == "Unknown field."
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_none_name__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[NAME_FIELD] = None
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_int_as_name__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[NAME_FIELD] = 4
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_without_name__200(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update.pop(NAME_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_none_description__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[DESCRIPTION_FIELD] = None
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_int_as_description__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[DESCRIPTION_FIELD] = 1
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_without_description__200(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update.pop(DESCRIPTION_FIELD, None)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_invalid_json_metadata__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[DESCRIPTION_FIELD] = {}
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_json_metadata__200(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
valid_json_metadata: dict[Any, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_json_metadata["nativeFilters"] = {"changed": "changed"}
|
|
||||||
valid_filter_set_data_for_update[JSON_METADATA_FIELD] = json.dumps(
|
|
||||||
valid_json_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_invalid_owner_type__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "OTHER_TYPE"
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_user_owner_type__400(
|
|
||||||
self,
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "User"
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_with_dashboard_owner_type__200(
|
|
||||||
self,
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "Dashboard"
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
user_based_filter_set_dict["owner_id"] = user_based_filter_set_dict[
|
|
||||||
"dashboard_id"
|
|
||||||
]
|
|
||||||
assert_filterset_updated(
|
|
||||||
user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_type_is_user__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_when_caller_is_admin_and_owner_type_is_dashboard__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, "admin")
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_is_other_user_403(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_updated(user_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, DASHBOARD_OWNER_USERNAME)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_when_caller_is_filterset_owner__200(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, FILTER_SET_OWNER_USERNAME)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert_filterset_updated(
|
|
||||||
user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_type_is_user__403(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
user_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, REGULAR_USER)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_updated(user_based_filter_set_dict)
|
|
||||||
|
|
||||||
def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
|
|
||||||
self,
|
|
||||||
test_users: dict[str, int],
|
|
||||||
dashboard_based_filter_set_dict: dict[str, Any],
|
|
||||||
valid_filter_set_data_for_update: dict[str, Any],
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
):
|
|
||||||
# arrange
|
|
||||||
login(client, REGULAR_USER)
|
|
||||||
|
|
||||||
# act
|
|
||||||
response = call_update_filter_set(
|
|
||||||
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
|
||||||
)
|
|
||||||
|
|
||||||
# assert
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
|
|
@ -1,102 +0,0 @@
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING
|
|
||||||
|
|
||||||
from superset.models.filter_set import FilterSet
|
|
||||||
from tests.integration_tests.dashboards.filter_sets.consts import FILTER_SET_URI
|
|
||||||
from tests.integration_tests.test_app import app
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from flask import Response
|
|
||||||
from flask.testing import FlaskClient
|
|
||||||
|
|
||||||
|
|
||||||
def call_create_filter_set(
|
|
||||||
client: FlaskClient[Any], dashboard_id: int, data: dict[str, Any]
|
|
||||||
) -> Response:
|
|
||||||
uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
|
|
||||||
return client.post(uri, json=data)
|
|
||||||
|
|
||||||
|
|
||||||
def call_get_filter_sets(client: FlaskClient[Any], dashboard_id: int) -> Response:
|
|
||||||
uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
|
|
||||||
return client.get(uri)
|
|
||||||
|
|
||||||
|
|
||||||
def call_delete_filter_set(
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
filter_set_dict_to_update: dict[str, Any],
|
|
||||||
dashboard_id: int | None = None,
|
|
||||||
) -> Response:
|
|
||||||
dashboard_id = (
|
|
||||||
dashboard_id
|
|
||||||
if dashboard_id is not None
|
|
||||||
else filter_set_dict_to_update["dashboard_id"]
|
|
||||||
)
|
|
||||||
uri = "{}/{}".format(
|
|
||||||
FILTER_SET_URI.format(dashboard_id=dashboard_id),
|
|
||||||
filter_set_dict_to_update["id"],
|
|
||||||
)
|
|
||||||
return client.delete(uri)
|
|
||||||
|
|
||||||
|
|
||||||
def call_update_filter_set(
|
|
||||||
client: FlaskClient[Any],
|
|
||||||
filter_set_dict_to_update: dict[str, Any],
|
|
||||||
data: dict[str, Any],
|
|
||||||
dashboard_id: int | None = None,
|
|
||||||
) -> Response:
|
|
||||||
dashboard_id = (
|
|
||||||
dashboard_id
|
|
||||||
if dashboard_id is not None
|
|
||||||
else filter_set_dict_to_update["dashboard_id"]
|
|
||||||
)
|
|
||||||
uri = "{}/{}".format(
|
|
||||||
FILTER_SET_URI.format(dashboard_id=dashboard_id),
|
|
||||||
filter_set_dict_to_update["id"],
|
|
||||||
)
|
|
||||||
return client.put(uri, json=data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_filter_set_by_name(name: str) -> FilterSet:
|
|
||||||
with app.app_context():
|
|
||||||
return FilterSet.get_by_name(name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_filter_set_by_id(id_: int) -> FilterSet:
|
|
||||||
with app.app_context():
|
|
||||||
return FilterSet.get(id_)
|
|
||||||
|
|
||||||
|
|
||||||
def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet:
|
|
||||||
with app.app_context():
|
|
||||||
return FilterSet.get_by_dashboard_id(dashboard_id)
|
|
||||||
|
|
||||||
|
|
||||||
def collect_all_ids(
|
|
||||||
filtersets: dict[str, list[FilterSet]] | list[FilterSet]
|
|
||||||
) -> set[int]:
|
|
||||||
if isinstance(filtersets, dict):
|
|
||||||
filtersets_lists: list[list[FilterSet]] = list(filtersets.values())
|
|
||||||
ids: set[int] = set()
|
|
||||||
lst: list[FilterSet]
|
|
||||||
for lst in filtersets_lists:
|
|
||||||
ids.update(set(map(lambda fs: fs.id, lst)))
|
|
||||||
return ids
|
|
||||||
return set(map(lambda fs: fs.id, filtersets))
|
|
Loading…
Reference in New Issue