refactor: Removes the filters set feature (#26369)

This commit is contained in:
Michael S. Molina 2024-01-16 12:42:35 -03:00 committed by GitHub
parent 7ca6d8c880
commit 9387c4c16f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 619 additions and 7094 deletions

View File

@ -89,7 +89,6 @@ These features flags currently default to True and **will be removed in a future
- DASHBOARD_CACHE
- DASHBOARD_FILTERS_EXPERIMENTAL
- DASHBOARD_NATIVE_FILTERS
- DASHBOARD_NATIVE_FILTERS_SET
- ENABLE_EXPLORE_JSON_CSRF_PROTECTION
- ENABLE_TEMPLATE_REMOVE_FILTERS
- GENERIC_CHART_AXES

View File

@ -1,216 +1,212 @@
||Admin|Alpha|Gamma|SQL_LAB|
|---|---|---|---|---|
|Permission/role description|Admins have all possible rights, including granting or revoking rights from other users and altering other peoples 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 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 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 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 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 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 write on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|can read 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 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 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 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 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 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 show 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 add 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|
|resetmypassword on UserDBModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|resetpasswords 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 edit 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 list 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 post 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 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 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 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 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:|
|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 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 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 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 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 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 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 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 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 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 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|
|can edit 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 userinfo on UserOAuthModelView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can add 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|
|can write 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 show on DynamicPlugin|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can download 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 edit 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 download 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|
|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 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 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 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 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 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 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|
|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 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 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|
|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 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 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 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 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|
|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 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 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 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 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 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 add on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can delete on FilterSets|: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 this form get on ColumnarToDatabaseView|: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|
|menu access on Upload a Columnar file|: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 write on DashboardFilterStateRestApi|: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 write on DashboardPermalinkRestApi|: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 delete embedded on Dashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can set embedded on Dashboard|:heavy_check_mark:|O|O|O|
|can export on Dashboard|: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 export on Database|:heavy_check_mark:|O|O|O|
|can export on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|can write on ExploreFormDataRestApi|: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 write on ExplorePermalinkRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can read on ExplorePermalinkRestApi|: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 import on ImportExportRestApi|: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 dashboard permalink on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can grant guest token on SecurityRestApi|:heavy_check_mark:|O|O|O|
|can read on AdvancedDataType|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can read on EmbeddedDashboard|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can duplicate 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 samples on Datasource|:heavy_check_mark:|:heavy_check_mark:|O|O|
|can read on AvailableDomains|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|can get or create dataset on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|
|can get column values on Datasource|:heavy_check_mark:|:heavy_check_mark:|O|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|
| | Admin | Alpha | Gamma | SQL_LAB |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| Permission/role description | Admins have all possible rights, including granting or revoking rights from other users and altering other peoples 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 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 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 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 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 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 write on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
| can read 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 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 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 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 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 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 show 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 add 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 |
| resetmypassword on UserDBModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| resetpasswords 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 edit 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 list 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 post 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 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 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 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 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: |
| 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 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 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 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 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 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 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 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 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 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 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 |
| can edit 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 userinfo on UserOAuthModelView | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can add 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 |
| can write 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 show on DynamicPlugin | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can download 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 edit 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 download 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 |
| 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 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 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 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 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 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 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 |
| 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 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 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 |
| 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 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 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 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 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 |
| 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 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 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 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 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 chart on Superset | :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 this form post on ColumnarToDatabaseView | :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 export on Chart | :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 read on DashboardFilterStateRestApi | :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 read on DashboardPermalinkRestApi | :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 set embedded on Dashboard | :heavy_check_mark: | O | O | O |
| can export on Dashboard | :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 export on Database | :heavy_check_mark: | O | O | O |
| can export on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
| can write on ExploreFormDataRestApi | :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 write on ExplorePermalinkRestApi | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can read on ExplorePermalinkRestApi | :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 import on ImportExportRestApi | :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 dashboard permalink on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can grant guest token on SecurityRestApi | :heavy_check_mark: | O | O | O |
| can read on AdvancedDataType | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can read on EmbeddedDashboard | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can duplicate 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 samples on Datasource | :heavy_check_mark: | :heavy_check_mark: | O | O |
| can read on AvailableDomains | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can get or create dataset on Dataset | :heavy_check_mark: | :heavy_check_mark: | O | O |
| can get column values on Datasource | :heavy_check_mark: | :heavy_check_mark: | O | 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 |

View File

@ -31,6 +31,7 @@ assists people when migrating to a new version.
### 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.
- [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

View File

@ -54,17 +54,6 @@ export type DataMaskState = { [id: string]: DataMask };
export type DataMaskWithId = { id: string } & DataMask;
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 = {
cascadeParentIds: string[];
defaultDataMask: DataMask;
@ -133,7 +122,6 @@ export type PartialFilters = {
export type NativeFiltersState = {
filters: Filters;
filterSets: FilterSets;
focusedFilterId?: string;
hoveredFilterId?: string;
};

View File

@ -30,7 +30,6 @@ export enum FeatureFlag {
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
CONFIRM_DASHBOARD_DIFF = 'CONFIRM_DASHBOARD_DIFF',
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
DASHBOARD_VIRTUALIZATION = 'DASHBOARD_VIRTUALIZATION',
DASHBOARD_RBAC = 'DASHBOARD_RBAC',
DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT',

View File

@ -24,7 +24,6 @@ import {
} from '@superset-ui/core';
export const nativeFilters: NativeFiltersState = {
filterSets: {},
filters: {
'NATIVE_FILTER-e7Q8zKixx': {
id: 'NATIVE_FILTER-e7Q8zKixx',

View File

@ -166,5 +166,5 @@ export const stateWithoutNativeFilters = {
},
},
dataMask: {},
nativeFilters: { filters: {}, filterSets: {} },
nativeFilters: { filters: {} },
};

View File

@ -47,10 +47,6 @@ export const URL_PARAMS = {
name: 'native_filters_key',
type: 'string',
},
filterSet: {
name: 'filter_set',
type: 'string',
},
showFilters: {
name: 'show_filters',
type: 'boolean',

View File

@ -16,14 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
FilterConfiguration,
Filters,
FilterSet,
FilterSets,
makeApi,
} from '@superset-ui/core';
import { FilterConfiguration, Filters, makeApi } from '@superset-ui/core';
import { Dispatch } from 'redux';
import { cloneDeep } from 'lodash';
import {
@ -32,8 +25,7 @@ import {
} from 'src/dataMask/actions';
import { HYDRATE_DASHBOARD } from './hydrate';
import { dashboardInfoChanged } from './dashboardInfo';
import { FilterSetFullData } from '../reducers/types';
import { DashboardInfo, RootState } from '../types';
import { DashboardInfo } from '../types';
export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
export interface SetFilterConfigBegin {
@ -56,61 +48,6 @@ export interface SetInScopeStatusOfFilters {
type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS;
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 =
(filterConfig: FilterConfiguration) =>
@ -213,7 +150,6 @@ export const setInScopeStatusOfFilters =
type BootstrapData = {
nativeFilters: {
filters: Filters;
filterSets: FilterSets;
filtersState: object;
};
};
@ -223,134 +159,6 @@ export interface SetBootstrapData {
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 interface SetFocusedNativeFilter {
type: typeof SET_FOCUSED_NATIVE_FILTER;
@ -416,22 +224,10 @@ export type AnyFilterAction =
| SetFilterConfigBegin
| SetFilterConfigComplete
| SetFilterConfigFail
| SetFilterSetsBegin
| SetFilterSetsComplete
| SetFilterSetsFail
| SetInScopeStatusOfFilters
| SetBootstrapData
| SetFocusedNativeFilter
| UnsetFocusedNativeFilter
| SetHoveredNativeFilter
| UnsetHoveredNativeFilter
| CreateFilterSetBegin
| CreateFilterSetComplete
| CreateFilterSetFail
| DeleteFilterSetBegin
| DeleteFilterSetComplete
| DeleteFilterSetFail
| UpdateFilterSetBegin
| UpdateFilterSetComplete
| UpdateFilterSetFail
| UpdateCascadeParentIds;

View File

@ -78,7 +78,6 @@ import {
BUILDER_SIDEPANEL_WIDTH,
CLOSED_FILTER_BAR_WIDTH,
FILTER_BAR_HEADER_HEIGHT,
FILTER_BAR_TABS_HEIGHT,
MAIN_HEADER_HEIGHT,
OPEN_FILTER_BAR_MAX_WIDTH,
OPEN_FILTER_BAR_WIDTH,
@ -463,18 +462,12 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
threshold: [1],
});
// Filter sets depend on native filters
const filterSetEnabled =
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) &&
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS);
const showFilterBar =
(crossFiltersEnabled || nativeFiltersEnabled) && !editMode;
const offset =
FILTER_BAR_HEADER_HEIGHT +
(isSticky || standaloneMode ? 0 : MAIN_HEADER_HEIGHT) +
(filterSetEnabled ? FILTER_BAR_TABS_HEIGHT : 0);
(isSticky || standaloneMode ? 0 : MAIN_HEADER_HEIGHT);
const filterBarHeight = `calc(100vh - ${offset}px)`;
const filterBarOffset = dashboardFiltersOpen ? 0 : barTopOffset + 20;

View File

@ -20,7 +20,7 @@
import React, { FC } from 'react';
import { css } from '@superset-ui/core';
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 {
FilterValue,
FilterItem,

View File

@ -25,9 +25,7 @@ import * as mockCore from '@superset-ui/core';
import { testWithId } from 'src/utils/testUtils';
import { FeatureFlag, Preset } from '@superset-ui/core';
import { TimeFilterPlugin, SelectFilterPlugin } from 'src/filters/components';
import { DATE_FILTER_TEST_KEY } from 'src/explore/components/controls/DateFilterControl';
import fetchMock from 'fetch-mock';
import { waitFor } from '@testing-library/react';
import { FilterBarOrientation } from 'src/dashboard/types';
import { FILTER_BAR_TEST_ID } from './utils';
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 FILTER_NAME = 'Time filter 1';
const FILTER_SET_NAME = 'New filter set';
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
};
const addFilterFlow = async () => {
@ -95,38 +91,6 @@ const addFilterFlow = async () => {
// 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', () => {
new MainPreset().register();
const toggleFiltersBar = jest.fn();
@ -157,30 +121,6 @@ describe('FilterBar', () => {
"cascadeParentIds":[],
"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 () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
};
renderWrapper(openedBarProps, stateWithoutNativeFilters);
@ -363,88 +302,4 @@ describe('FilterBar', () => {
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');
});
});

View File

@ -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();
});

View File

@ -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;

View File

@ -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();
});

View File

@ -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;

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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}:&nbsp;
</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;

View File

@ -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();
});

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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();
});

View File

@ -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);
});

View File

@ -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');
});

View File

@ -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;
});
});

View File

@ -28,23 +28,12 @@ import React, {
createContext,
} from 'react';
import cx from 'classnames';
import {
FeatureFlag,
HandlerFunction,
isFeatureEnabled,
isNativeFilter,
styled,
t,
} from '@superset-ui/core';
import { FeatureFlag, isFeatureEnabled, styled, t } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { AntdTabs } from 'src/components';
import Loading from 'src/components/Loading';
import { EmptyStateSmall } from 'src/components/EmptyState';
import { getFilterBarTestId } from './utils';
import { TabIds, VerticalBarProps } from './types';
import FilterSets from './FilterSets';
import { useFilterSets } from './state';
import EditSection from './FilterSets/EditSection';
import { VerticalBarProps } from './types';
import Header from './Header';
import FilterControls from './FilterControls/FilterControls';
import CrossFiltersVertical from './CrossFilters/Vertical';
@ -117,22 +106,6 @@ const StyledFilterIcon = styled(Icons.Filter)`
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`
margin-top: ${({ theme }) => theme.gridUnit * 8}px;
`;
@ -151,18 +124,12 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
filtersOpen,
filterValues,
height,
isDisabled,
isInitialized,
offset,
onSelectionChange,
toggleFiltersBar,
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 timeout = useRef<any>();
@ -195,8 +162,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
[height],
);
const numberOfFilters = nativeFilterValues.length;
const filterControls = useMemo(
() =>
filterValues.length === 0 ? (
@ -223,62 +188,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
[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(
() =>
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ? (
@ -293,11 +202,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
[actions],
);
// Filter sets depend on native filters
const filterSetEnabled =
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) &&
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS);
return (
<FilterBarScrollContext.Provider value={isScrolling}>
<BarWrapper
@ -326,11 +230,6 @@ const VerticalFilterBar: React.FC<VerticalBarProps> = ({
<div css={{ height }}>
<Loading />
</div>
) : filterSetEnabled ? (
<>
{crossFilters}
{filterSetsTabs}
</>
) : (
<div css={tabPaneStyle} onScroll={onScroll}>
<>

View File

@ -304,7 +304,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({
filtersOpen={verticalConfig.filtersOpen}
filterValues={filterValues}
isInitialized={isInitialized}
isDisabled={isApplyDisabled}
height={verticalConfig.height}
offset={verticalConfig.offset}
onSelectionChange={handleFilterSelectionChange}

View File

@ -24,17 +24,11 @@ import {
DataMaskWithId,
Filter,
Filters,
FilterSets as FilterSetsType,
} from '@superset-ui/core';
import { useEffect, useMemo, useState } from 'react';
import { ChartsState, RootState } from 'src/dashboard/types';
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
export const useFilterSets = () =>
useSelector<any, FilterSetsType>(
state => state.nativeFilters.filterSets || {},
);
export const useFilters = () => {
const preselectedNativeFilters = useSelector<any, Filters>(
state => state.dashboardState?.preselectNativeFilters,

View File

@ -58,11 +58,4 @@ export type HorizontalBarProps = CommonFiltersBarProps & {
export type VerticalBarProps = Omit<FiltersBarProps, 'orientation'> &
CommonFiltersBarProps &
VerticalBarConfig & {
isDisabled: boolean;
};
export enum TabIds {
AllFilters = 'allFilters',
FilterSets = 'filterSets',
}
VerticalBarConfig;

View File

@ -28,6 +28,7 @@ import {
Filter,
getChartMetadataRegistry,
QueryFormData,
t,
} from '@superset-ui/core';
import { DashboardLayout } from 'src/dashboard/types';
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
@ -234,3 +235,21 @@ export const findTabsWithChartsInScope = (
}
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');
};

View File

@ -21,9 +21,7 @@ import { Global } from '@emotion/react';
import { useHistory } from 'react-router-dom';
import {
CategoricalColorNamespace,
FeatureFlag,
getSharedLabelColor,
isFeatureEnabled,
SharedLabelColorSource,
t,
useTheme,
@ -43,7 +41,6 @@ import injectCustomCss from 'src/dashboard/util/injectCustomCss';
import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { getFilterSets } from 'src/dashboard/actions/nativeFilters';
import { setDatasetsStatus } from 'src/dashboard/actions/dashboardState';
import {
getFilterValue,
@ -104,11 +101,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
const readyToRender = Boolean(dashboard && charts);
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(() => {
// 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
@ -158,10 +150,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
if (readyToRender) {
if (!isDashboardHydrated.current) {
isDashboardHydrated.current = true;
if (filterSetEnabled) {
// only initialize filterset once
dispatch(getFilterSets(id));
}
}
dispatch(
hydrateDashboard({

View File

@ -33,14 +33,6 @@ export const mockDataMaskInfo: DataMaskStateWithId = {
};
export const nativeFiltersInfo: NativeFiltersState = {
filterSets: {
'1': {
id: 1,
name: 'Set name',
nativeFilters: {},
dataMask: mockDataMaskInfo,
},
},
filters: {
DefaultsID: {
cascadeParentIds: [],

View File

@ -20,26 +20,19 @@ import {
AnyFilterAction,
SET_FILTER_CONFIG_COMPLETE,
SET_IN_SCOPE_STATUS_OF_FILTERS,
SET_FILTER_SETS_COMPLETE,
SET_FOCUSED_NATIVE_FILTER,
UNSET_FOCUSED_NATIVE_FILTER,
SET_HOVERED_NATIVE_FILTER,
UNSET_HOVERED_NATIVE_FILTER,
UPDATE_CASCADE_PARENT_IDS,
} from 'src/dashboard/actions/nativeFilters';
import {
FilterSet,
FilterConfiguration,
NativeFiltersState,
} from '@superset-ui/core';
import { FilterConfiguration, NativeFiltersState } from '@superset-ui/core';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
export function getInitialState({
filterSetsConfig,
filterConfig,
state: prevState,
}: {
filterSetsConfig?: FilterSet[];
filterConfig?: FilterConfiguration;
state?: NativeFiltersState;
}): NativeFiltersState {
@ -55,17 +48,6 @@ export function getInitialState({
} else {
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;
return state as NativeFiltersState;
}
@ -73,7 +55,6 @@ export function getInitialState({
export default function nativeFilterReducer(
state: NativeFiltersState = {
filters: {},
filterSets: {},
},
action: AnyFilterAction,
) {
@ -81,19 +62,12 @@ export default function nativeFilterReducer(
case HYDRATE_DASHBOARD:
return {
filters: action.data.nativeFilters.filters,
filterSets: action.data.nativeFilters.filterSets,
};
case SET_FILTER_CONFIG_COMPLETE:
case SET_IN_SCOPE_STATUS_OF_FILTERS:
return getInitialState({ filterConfig: action.filterConfig, state });
case SET_FILTER_SETS_COMPLETE:
return getInitialState({
filterSetsConfig: action.filterSets,
state,
});
case SET_FOCUSED_NATIVE_FILTER:
return {
...state,

View File

@ -18,7 +18,6 @@
*/
import componentTypes from 'src/dashboard/util/componentTypes';
import { JsonObject } from '@superset-ui/core';
export enum Scoping {
All = 'All',
@ -80,16 +79,3 @@ export type LayoutItem = {
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;
};

View File

@ -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.

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -443,8 +443,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
"ESCAPE_MARKDOWN_HTML": False,
"DASHBOARD_NATIVE_FILTERS": True, # deprecated
"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_VIRTUALIZATION": False,
"GLOBAL_ASYNC_QUERIES": False,

View File

@ -31,21 +31,12 @@ from superset.commands.dashboard.exceptions import (
DashboardNotFoundError,
)
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.exceptions import SupersetSecurityException
from superset.extensions import db
from superset.models.core import FavStar, FavStarClassName
from superset.models.dashboard import Dashboard, id_or_slug_filter
from superset.models.embedded_dashboard import EmbeddedDashboard
from superset.models.filter_set import FilterSet
from superset.models.slice import Slice
from superset.utils.core import get_user_id
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.
"""
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)

View File

@ -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.

View File

@ -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)

View File

@ -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),
)
)

View File

@ -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}"
)

View File

@ -113,8 +113,6 @@ class DashboardJSONMetadataSchema(Schema):
# global_chart_configuration keeps data about global cross-filter scoping
# for charts - can be overridden by chart_configuration for each chart
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())
# deprecated wrt dashboard-native filters
filter_scopes = fields.Dict()

View File

@ -135,7 +135,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
)
from superset.css_templates.api import CssTemplateRestApi
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.permalink.api import DashboardPermalinkRestApi
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(ExploreFormDataRestApi)
appbuilder.add_api(ExplorePermalinkRestApi)
appbuilder.add_api(FilterSetRestApi)
appbuilder.add_api(ImportExportRestApi)
appbuilder.add_api(QueryRestApi)
appbuilder.add_api(ReportScheduleRestApi)

View File

@ -14,17 +14,30 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
USER_OWNER_TYPE = "User"
DASHBOARD_OWNER_TYPE = "Dashboard"
"""drop_filter_sets_table
NAME_FIELD = "name"
DESCRIPTION_FIELD = "description"
JSON_METADATA_FIELD = "json_metadata"
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"
Revision ID: 59a1450b3c10
Revises: 65a167d4c62e
Create Date: 2023-12-27 13:14:27.268232
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()

View File

@ -55,7 +55,6 @@ from superset.connectors.sqla.models import (
)
from superset.daos.datasource import DatasourceDAO
from superset.extensions import cache_manager
from superset.models.filter_set import FilterSet
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
from superset.models.slice import Slice
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.thumbnails.digest import get_dashboard_digest
from superset.utils import core as utils
from superset.utils.core import get_user_id
from superset.utils.decorators import debounce
metadata = Model.metadata # pylint: disable=no-member
@ -185,9 +183,6 @@ class Dashboard(AuditMixinNullable, ImportExportMixin, Model):
back_populates="dashboard",
cascade="all, delete-orphan",
)
_filter_sets = relationship(
"FilterSet", back_populates="dashboard", cascade="all, delete"
)
export_fields = [
"dashboard_title",
"position_json",
@ -231,28 +226,6 @@ class Dashboard(AuditMixinNullable, ImportExportMixin, Model):
.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
def charts(self) -> list[str]:
return [slc.chart for slc in self.slices]

View File

@ -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 {}

View File

@ -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.

View File

@ -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()

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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))