From a0ecdbbec2f6b66f54aa32a54572aca22d5c4455 Mon Sep 17 00:00:00 2001 From: Tai Dupree Date: Tue, 14 Apr 2020 08:24:39 -0700 Subject: [PATCH 1/3] [charts] adds new filters ui --- .../views/chartList/ChartList_spec.jsx | 13 +++ .../src/components/ListView/types.ts | 8 +- .../src/views/chartList/ChartList.tsx | 85 ++++++++++++++- .../src/views/datasetList/DatasetList.tsx | 4 +- superset/charts/api.py | 103 ++++++++++++++++++ superset/charts/dao.py | 29 ++++- superset/views/base_api.py | 2 + tests/charts/api_tests.py | 22 ++++ 8 files changed, 256 insertions(+), 10 deletions(-) diff --git a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx index 60c8ccb1f554..faf0c096d1e1 100644 --- a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx @@ -32,6 +32,8 @@ const store = mockStore({}); const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*'; const chartssOwnersEndpoint = 'glob:*/api/v1/chart/related/owners*'; const chartsEndpoint = 'glob:*/api/v1/chart/?*'; +const chartsVizTypesEndpoint = 'glob:*/api/v1/chart/viz_types'; +const chartsDtasourcesEndpoint = 'glob:*/api/v1/chart/datasources'; const mockCharts = [...new Array(3)].map((_, i) => ({ changed_on: new Date().toISOString(), @@ -40,6 +42,7 @@ const mockCharts = [...new Array(3)].map((_, i) => ({ slice_name: `cool chart ${i}`, url: 'url', viz_type: 'bar', + datasource_name: `ds${i}`, })); fetchMock.get(chartsInfoEndpoint, { @@ -60,6 +63,16 @@ fetchMock.get(chartsEndpoint, { chart_count: 3, }); +fetchMock.get(chartsVizTypesEndpoint, { + result: [], + count: 0, +}); + +fetchMock.get(chartsDtasourcesEndpoint, { + result: [], + count: 0, +}); + describe('ChartList', () => { const mockedProps = {}; const wrapper = mount(, { diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index de85949a2f01..397f7a0f08da 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -43,7 +43,13 @@ export type Filters = Filter[]; export interface FilterValue { id: string; operator?: string; - value: string | boolean | number | null | undefined; + value: + | string + | boolean + | number + | null + | undefined + | { datasource_id: number; datasource_type: string }; } export interface FetchDataConfig { diff --git a/superset-frontend/src/views/chartList/ChartList.tsx b/superset-frontend/src/views/chartList/ChartList.tsx index 942028bda7cf..1d63e338dd30 100644 --- a/superset-frontend/src/views/chartList/ChartList.tsx +++ b/superset-frontend/src/views/chartList/ChartList.tsx @@ -33,6 +33,7 @@ import { import withToasts from 'src/messageToasts/enhancers/withToasts'; import PropertiesModal, { Slice } from 'src/explore/components/PropertiesModal'; import Chart from 'src/types/Chart'; +import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; const PAGE_SIZE = 25; @@ -111,6 +112,10 @@ class ChartList extends React.PureComponent { return this.hasPerm('can_delete'); } + get isNewUIEnabled() { + return isFeatureEnabled(FeatureFlag.LIST_VIEWS_NEW_UI); + } + initialSort = [{ id: 'changed_on', desc: true }]; columns = [ @@ -175,6 +180,10 @@ class ChartList extends React.PureComponent { accessor: 'owners', hidden: true, }, + { + accessor: 'datasource', + hidden: true, + }, { Cell: ({ row: { state, original } }: any) => { const handleDelete = () => this.handleChartDelete(original); @@ -311,11 +320,24 @@ class ChartList extends React.PureComponent { }, loading: true, }); - const filterExps = filters.map(({ id: col, operator: opr, value }) => ({ - col, - opr, - value, - })); + const filterExps = filters + .map(({ id: col, operator: opr, value }) => ({ + col, + opr, + value, + })) + .reduce((acc, fltr) => { + if (fltr.col === 'datasource' && typeof fltr.value === 'object') { + const { datasource_id: dsId, datasource_type: dsType } = + fltr.value || {}; + return [ + ...acc, + { ...fltr, col: 'datasource_id', value: dsId }, + { ...fltr, col: 'datasource_type', value: dsType }, + ]; + } + return [...acc, fltr]; + }, []); const queryParams = JSON.stringify({ order_column: sortBy[0].id, @@ -339,8 +361,58 @@ class ChartList extends React.PureComponent { }); }; - updateFilters = () => { + updateFilters = async () => { const { filterOperators, owners } = this.state; + + if (this.isNewUIEnabled) { + const { json: vizTypesJson = {} } = await SupersetClient.get({ + endpoint: '/api/v1/chart/viz_types', + }); + const vizTypes = vizTypesJson?.result; + + const { json: datasourcesJson = {} } = await SupersetClient.get({ + endpoint: '/api/v1/chart/datasources', + }); + const datasources = datasourcesJson?.result; + + this.setState({ + filters: [ + { + Header: 'Owner', + id: 'owners', + input: 'select', + operator: 'rel_m_m', + unfilteredLabel: 'All', + selects: owners.map(({ text: label, value }) => ({ label, value })), + }, + { + Header: 'Viz Type', + id: 'viz_type', + input: 'select', + operator: 'eq', + unfilteredLabel: 'All', + selects: vizTypes, + }, + { + Header: 'Dataset', + id: 'datasource', + input: 'select', + operator: 'eq', + unfilteredLabel: 'All', + selects: datasources, + }, + { + Header: 'Search', + id: 'slice_name', + input: 'search', + operator: 'name_or_description', + }, + ], + }); + + return; + } + const convertFilter = ({ name: label, operator, @@ -435,6 +507,7 @@ class ChartList extends React.PureComponent { initialSort={this.initialSort} filters={filters} bulkActions={bulkActions} + useNewUIFilters={this.isNewUIEnabled} /> ); }} diff --git a/superset-frontend/src/views/datasetList/DatasetList.tsx b/superset-frontend/src/views/datasetList/DatasetList.tsx index 9bbc4f053b72..43821b0efdaa 100644 --- a/superset-frontend/src/views/datasetList/DatasetList.tsx +++ b/superset-frontend/src/views/datasetList/DatasetList.tsx @@ -110,7 +110,7 @@ class DatasetList extends React.PureComponent { }, ([e1, e2]) => { this.props.addDangerToast( - t('An error occurred while fetching Datasets'), + t('An error occurred while fetching datasets'), ); if (e1) { console.error(e1); @@ -326,7 +326,7 @@ class DatasetList extends React.PureComponent { }) .catch(() => { this.props.addDangerToast( - t('An error occurred while fetching Datasets'), + t('An error occurred while fetching datasets'), ); }) .finally(() => { diff --git a/superset/charts/api.py b/superset/charts/api.py index be4f40747b09..e1cd0ad29718 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -40,6 +40,7 @@ ChartUpdateFailedError, ) from superset.charts.commands.update import UpdateChartCommand +from superset.charts.dao import ChartDAO from superset.charts.filters import ChartFilter, ChartNameOrDescriptionFilter from superset.charts.schemas import ( CHART_DATA_SCHEMAS, @@ -73,6 +74,8 @@ class ChartRestApi(BaseSupersetModelRestApi): RouteMethod.RELATED, "bulk_delete", # not using RouteMethod since locally defined "data", + "viz_types", + "datasources", } class_permission_name = "SliceModelView" show_columns = [ @@ -102,6 +105,10 @@ class ChartRestApi(BaseSupersetModelRestApi): "viz_type", "params", "cache_timeout", + "owners.id", + "owners.username", + "owners.first_name", + "owners.last_name", ] order_columns = [ "slice_name", @@ -115,6 +122,8 @@ class ChartRestApi(BaseSupersetModelRestApi): "description", "viz_type", "datasource_name", + "datasource_id", + "datasource_type", "owners", ) base_order = ("changed_on", "desc") @@ -493,3 +502,97 @@ def add_apispec_components(self, api_spec: APISpec) -> None: chart_type.__name__, schema=chart_type, ) super().add_apispec_components(api_spec) + + @expose("/viz_types", methods=["GET"]) + @protect() + @safe + def viz_types(self) -> Response: + """Get unique viz_type values + --- + get: + responses: + 200: + description: charts unique viz_type data + content: + application/json: + schema: + type: object + properties: + count: + type: integer + result: + type: object + properties: + label: + type: string + value: + type: string + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + 422: + $ref: '#/components/responses/422' + 500: + $ref: '#/components/responses/500' + """ + values = ChartDAO.fetch_unique_column_values("viz_type") + result = [{"label": val, "value": val} for val in values] + return self.response(200, count=len(result), result=result) + + @expose("/datasources", methods=["GET"]) + @protect() + @safe + def datasources(self) -> Response: + """Get unique viz_type values + --- + get: + responses: + 200: + description: charts unique datasource data + content: + application/json: + schema: + type: object + properties: + count: + type: integer + result: + type: object + properties: + label: + type: string + value: + type: object + properties: + database_id: + type: integer + database_type: + type: string + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 422: + $ref: '#/components/responses/422' + 500: + $ref: '#/components/responses/500' + """ + datasources = ChartDAO.fetch_unique_datasources() + if not datasources: + return self.response(200, count=0, result=[]) + + result = [ + { + "label": str(ds), + "value": {"datasource_id": ds.id, "datasource_type": ds.type}, + } + for ds in datasources + ] + return self.response(200, count=len(result), result=result) diff --git a/superset/charts/dao.py b/superset/charts/dao.py index 01bfdc6cd640..c32a3d1dfd31 100644 --- a/superset/charts/dao.py +++ b/superset/charts/dao.py @@ -15,15 +15,20 @@ # specific language governing permissions and limitations # under the License. import logging -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from sqlalchemy.exc import SQLAlchemyError from superset.charts.filters import ChartFilter +from superset.connectors.connector_registry import ConnectorRegistry from superset.dao.base import BaseDAO from superset.extensions import db from superset.models.slice import Slice +if TYPE_CHECKING: + # pylint: disable=unused-import + from superset.connectors.base.models import BaseDatasource + logger = logging.getLogger(__name__) @@ -51,3 +56,25 @@ def bulk_delete(models: Optional[List[Slice]], commit: bool = True) -> None: if commit: db.session.rollback() raise ex + + @staticmethod + def fetch_unique_column_values(column_name: str) -> List: + return [ + val[0] + for val in db.session.query(getattr(Slice, column_name)).distinct().all() + ] + + @staticmethod + def fetch_unique_datasources() -> Optional[List["BaseDatasource"]]: + groups = ( + db.session.query(Slice.datasource_type, Slice.datasource_id) + .group_by(Slice.datasource_type, Slice.datasource_id) + .all() + ) + datasources = [ + ConnectorRegistry.get_datasource( + session=db.session, datasource_type=ds[0], datasource_id=ds[1] + ) + for ds in groups + ] + return datasources diff --git a/superset/views/base_api.py b/superset/views/base_api.py index 3f426178d697..60f9d29527c8 100644 --- a/superset/views/base_api.py +++ b/superset/views/base_api.py @@ -78,6 +78,8 @@ class BaseSupersetModelRestApi(ModelRestApi): "thumbnail": "list", "refresh": "edit", "data": "list", + "viz_types": "list", + "datasources": "list", } order_rel_fields: Dict[str, Tuple[str, str]] = {} diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py index 6a78f30eb601..cd39089dfcd4 100644 --- a/tests/charts/api_tests.py +++ b/tests/charts/api_tests.py @@ -688,3 +688,25 @@ def test_query_exec_not_allowed(self): uri = "api/v1/chart/data" rv = self.client.post(uri, json=query_context) self.assertEqual(rv.status_code, 401) + + def test_viz_types(self): + """ + Chart API: Test get viz_types + """ + self.login(username="admin") + uri = "api/v1/chart/viz_types" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual(data["count"], 20) + + def test_datasources(self): + """ + Chart API: Test get datasources + """ + self.login(username="admin") + uri = "api/v1/chart/datasources" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual(data["count"], 4) From 557b28ad2d2c42b57e3c3a4b44ab6ac076686007 Mon Sep 17 00:00:00 2001 From: Tai Dupree Date: Fri, 17 Apr 2020 10:30:29 -0700 Subject: [PATCH 2/3] move null check to be more visible --- superset-frontend/src/views/chartList/ChartList.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/views/chartList/ChartList.tsx b/superset-frontend/src/views/chartList/ChartList.tsx index 1d63e338dd30..0b7771c00b07 100644 --- a/superset-frontend/src/views/chartList/ChartList.tsx +++ b/superset-frontend/src/views/chartList/ChartList.tsx @@ -327,9 +327,12 @@ class ChartList extends React.PureComponent { value, })) .reduce((acc, fltr) => { - if (fltr.col === 'datasource' && typeof fltr.value === 'object') { - const { datasource_id: dsId, datasource_type: dsType } = - fltr.value || {}; + if ( + fltr.col === 'datasource' && + fltr.value && + typeof fltr.value === 'object' + ) { + const { datasource_id: dsId, datasource_type: dsType } = fltr.value; return [ ...acc, { ...fltr, col: 'datasource_id', value: dsId }, From 05774b1d7345aff8c3bc084f1db2c5bf55ac577a Mon Sep 17 00:00:00 2001 From: Tai Dupree Date: Sun, 19 Apr 2020 22:41:43 -0700 Subject: [PATCH 3/3] better filter lists and async filter functionality --- .../components/ListView/ListView_spec.jsx | 90 ++++++++++-------- .../src/components/ListView/Filters.tsx | 93 ++++++++++++------- .../src/components/ListView/types.ts | 2 + .../src/components/StyledSelect.tsx | 29 +++++- .../src/views/chartList/ChartList.tsx | 79 +++++++++------- superset-frontend/src/welcome/App.jsx | 2 + superset/charts/api.py | 45 +-------- superset/charts/dao.py | 22 +---- tests/charts/api_tests.py | 13 +-- 9 files changed, 196 insertions(+), 179 deletions(-) diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx index eba56f1f64da..61aca5f8b695 100644 --- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx +++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx @@ -35,6 +35,10 @@ const mockedProps = { Header: 'ID', sortable: true, }, + { + accessor: 'age', + Header: 'Age', + }, { accessor: 'name', Header: 'Name', @@ -287,6 +291,7 @@ Array [ }); describe('ListView with new UI filters', () => { + const fetchSelectsMock = jest.fn(() => []); const newFiltersProps = { ...mockedProps, useNewUIFilters: true, @@ -304,6 +309,13 @@ describe('ListView with new UI filters', () => { input: 'search', operator: 'ct', }, + { + Header: 'Age', + id: 'age', + input: 'select', + fetchSelects: fetchSelectsMock, + operator: 'eq', + }, ], }; @@ -320,11 +332,15 @@ describe('ListView with new UI filters', () => { expect(wrapper.find(ListViewFilters)).toHaveLength(1); }); + it('fetched selects if function is provided', () => { + expect(fetchSelectsMock).toHaveBeenCalled(); + }); + it('calls fetchData on filter', () => { act(() => { wrapper .find('[data-test="filters-select"]') - .last() + .first() .props() .onChange({ value: 'bar' }); }); @@ -332,7 +348,7 @@ describe('ListView with new UI filters', () => { act(() => { wrapper .find('[data-test="filters-search"]') - .last() + .first() .props() .onChange({ currentTarget: { value: 'something' } }); }); @@ -348,42 +364,42 @@ describe('ListView with new UI filters', () => { }); expect(newFiltersProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": Array [ - Object { - "id": "id", - "operator": "eq", - "value": "bar", - }, - ], - "pageIndex": 0, - "pageSize": 1, - "sortBy": Array [], - }, - ] - `); +Array [ + Object { + "filters": Array [ + Object { + "id": "id", + "operator": "eq", + "value": "bar", + }, + ], + "pageIndex": 0, + "pageSize": 1, + "sortBy": Array [], + }, +] +`); expect(newFiltersProps.fetchData.mock.calls[1]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": Array [ - Object { - "id": "id", - "operator": "eq", - "value": "bar", - }, - Object { - "id": "name", - "operator": "ct", - "value": "something", - }, - ], - "pageIndex": 0, - "pageSize": 1, - "sortBy": Array [], - }, - ] - `); +Array [ + Object { + "filters": Array [ + Object { + "id": "id", + "operator": "eq", + "value": "bar", + }, + Object { + "id": "name", + "operator": "ct", + "value": "something", + }, + ], + "pageIndex": 0, + "pageSize": 1, + "sortBy": Array [], + }, +] +`); }); }); diff --git a/superset-frontend/src/components/ListView/Filters.tsx b/superset-frontend/src/components/ListView/Filters.tsx index 69d35a1440cc..25b2c5b0bd9d 100644 --- a/superset-frontend/src/components/ListView/Filters.tsx +++ b/superset-frontend/src/components/ListView/Filters.tsx @@ -20,7 +20,7 @@ import React, { useState } from 'react'; import styled from '@emotion/styled'; import { withTheme } from 'emotion-theming'; -import StyledSelect from 'src/components/StyledSelect'; +import StyledSelect, { AsyncStyledSelect } from 'src/components/StyledSelect'; import SearchInput from 'src/components/SearchInput'; import { Filter, Filters, FilterValue, InternalFilter } from './types'; @@ -32,6 +32,7 @@ interface SelectFilterProps extends BaseFilter { onSelect: (selected: any) => any; selects: Filter['selects']; emptyLabel?: string; + fetchSelects?: Filter['fetchSelects']; } const FilterContainer = styled.div` @@ -51,11 +52,13 @@ function SelectFilter({ emptyLabel = 'None', initialValue, onSelect, + fetchSelects, }: SelectFilterProps) { const clearFilterSelect = { label: emptyLabel, value: CLEAR_SELECT_FILTER_VALUE, }; + const options = React.useMemo(() => [clearFilterSelect, ...selects], [ emptyLabel, selects, @@ -73,17 +76,34 @@ function SelectFilter({ selected.value === CLEAR_SELECT_FILTER_VALUE ? undefined : selected.value, ); }; + const fetchAndFormatSelects = async () => { + if (!fetchSelects) return { options: [clearFilterSelect] }; + const selectValues = await fetchSelects(); + return { options: [clearFilterSelect, ...selectValues] }; + }; return ( {Header}: - + {fetchSelects ? ( + + ) : ( + + )} ); } @@ -134,33 +154,36 @@ function UIFilters({ }: UIFiltersProps) { return ( - {filters.map(({ Header, input, selects, unfilteredLabel }, index) => { - const initialValue = - internalFilters[index] && internalFilters[index].value; - if (input === 'select') { - return ( - updateFilterValue(index, value)} - /> - ); - } - if (input === 'search') { - return ( - updateFilterValue(index, value)} - /> - ); - } - return null; - })} + {filters.map( + ({ Header, input, selects, unfilteredLabel, fetchSelects }, index) => { + const initialValue = + internalFilters[index] && internalFilters[index].value; + if (input === 'select') { + return ( + updateFilterValue(index, value)} + /> + ); + } + if (input === 'search') { + return ( + updateFilterValue(index, value)} + /> + ); + } + return null; + }, + )} ); } diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 397f7a0f08da..76acae3b7a3e 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -36,6 +36,8 @@ export interface Filter { input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search'; unfilteredLabel?: string; selects?: Select[]; + onFilterOpen?: () => void; + fetchSelects?: () => Promise; } export type Filters = Filter[]; diff --git a/superset-frontend/src/components/StyledSelect.tsx b/superset-frontend/src/components/StyledSelect.tsx index 1a474662e9b3..79d9151fc66d 100644 --- a/superset-frontend/src/components/StyledSelect.tsx +++ b/superset-frontend/src/components/StyledSelect.tsx @@ -18,7 +18,7 @@ */ import styled from '@emotion/styled'; // @ts-ignore -import Select from 'react-select'; +import Select, { Async } from 'react-select'; export default styled(Select)` display: inline; @@ -46,3 +46,30 @@ export default styled(Select)` border-bottom-left-radius: 0; } `; + +export const AsyncStyledSelect = styled(Async)` + display: inline; + &.is-focused:not(.is-open) > .Select-control { + border: none; + box-shadow: none; + } + .Select-control { + display: inline-table; + border: none; + width: 100px; + &:focus, + &:hover { + border: none; + box-shadow: none; + } + + .Select-arrow-zone { + padding-left: 10px; + } + } + .Select-menu-outer { + margin-top: 0; + border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + } +`; diff --git a/superset-frontend/src/views/chartList/ChartList.tsx b/superset-frontend/src/views/chartList/ChartList.tsx index 0b7771c00b07..5b1c5b42c305 100644 --- a/superset-frontend/src/views/chartList/ChartList.tsx +++ b/superset-frontend/src/views/chartList/ChartList.tsx @@ -18,6 +18,7 @@ */ import { SupersetClient } from '@superset-ui/connection'; import { t } from '@superset-ui/translation'; +import { getChartMetadataRegistry } from '@superset-ui/chart'; import moment from 'moment'; import PropTypes from 'prop-types'; import React from 'react'; @@ -48,7 +49,6 @@ interface State { loading: boolean; filterOperators: FilterOperatorMap; filters: Filters; - owners: Array<{ text: string; value: number }>; lastFetchDataConfig: FetchDataConfig | null; permissions: string[]; // for now we need to use the Slice type defined in PropertiesModal. @@ -68,32 +68,31 @@ class ChartList extends React.PureComponent { filters: [], lastFetchDataConfig: null, loading: false, - owners: [], permissions: [], sliceCurrentlyEditing: null, }; componentDidMount() { - Promise.all([ - SupersetClient.get({ - endpoint: `/api/v1/chart/_info`, - }), - SupersetClient.get({ - endpoint: `/api/v1/chart/related/owners`, - }), - ]).then( - ([{ json: infoJson = {} }, { json: ownersJson = {} }]) => { + SupersetClient.get({ + endpoint: `/api/v1/chart/_info`, + }).then( + ({ json: infoJson = {} }) => { this.setState( { filterOperators: infoJson.filters, - owners: ownersJson.result, permissions: infoJson.permissions, }, this.updateFilters, ); }, ([e1, e2]) => { - this.props.addDangerToast(t('An error occurred while fetching Charts')); + this.props.addDangerToast( + t( + 'An error occurred while fetching charts: %s, %s', + e1.message, + e2.message, + ), + ); if (e1) { console.error(e1); } @@ -356,28 +355,44 @@ class ChartList extends React.PureComponent { .then(({ json = {} }) => { this.setState({ charts: json.result, chartCount: json.count }); }) - .catch(() => { - this.props.addDangerToast(t('An error occurred while fetching Charts')); + .catch(e => { + this.props.addDangerToast( + t('An error occurred while fetching charts: %s', e.message), + ); }) .finally(() => { this.setState({ loading: false }); }); }; - updateFilters = async () => { - const { filterOperators, owners } = this.state; - - if (this.isNewUIEnabled) { - const { json: vizTypesJson = {} } = await SupersetClient.get({ - endpoint: '/api/v1/chart/viz_types', + createFetchResource = ( + resource: string, + postProcess?: (value: []) => any[], + ) => async () => { + try { + const { json = {} } = await SupersetClient.get({ + endpoint: resource, }); - const vizTypes = vizTypesJson?.result; + return postProcess ? postProcess(json?.result) : json?.result; + } catch (e) { + this.props.addDangerToast( + t('An error occurred while fetching chart filters: %s', e.message), + ); + } + return []; + }; - const { json: datasourcesJson = {} } = await SupersetClient.get({ - endpoint: '/api/v1/chart/datasources', - }); - const datasources = datasourcesJson?.result; + convertOwners = (owners: any[]) => + owners.map(({ text: label, value }) => ({ label, value })); + + updateFilters = async () => { + const { filterOperators } = this.state; + const fetchOwners = this.createFetchResource( + '/api/v1/chart/related/owners', + this.convertOwners, + ); + if (this.isNewUIEnabled) { this.setState({ filters: [ { @@ -386,7 +401,7 @@ class ChartList extends React.PureComponent { input: 'select', operator: 'rel_m_m', unfilteredLabel: 'All', - selects: owners.map(({ text: label, value }) => ({ label, value })), + fetchSelects: fetchOwners, }, { Header: 'Viz Type', @@ -394,7 +409,9 @@ class ChartList extends React.PureComponent { input: 'select', operator: 'eq', unfilteredLabel: 'All', - selects: vizTypes, + selects: getChartMetadataRegistry() + .keys() + .map(k => ({ label: k, value: k })), }, { Header: 'Dataset', @@ -402,7 +419,7 @@ class ChartList extends React.PureComponent { input: 'select', operator: 'eq', unfilteredLabel: 'All', - selects: datasources, + fetchSelects: this.createFetchResource('/api/v1/chart/datasources'), }, { Header: 'Search', @@ -412,7 +429,6 @@ class ChartList extends React.PureComponent { }, ], }); - return; } @@ -424,6 +440,7 @@ class ChartList extends React.PureComponent { operator: string; }) => ({ label, value: operator }); + const owners = await fetchOwners(); this.setState({ filters: [ { @@ -451,7 +468,7 @@ class ChartList extends React.PureComponent { id: 'owners', input: 'select', operators: filterOperators.owners.map(convertFilter), - selects: owners.map(({ text: label, value }) => ({ label, value })), + selects: owners, }, ], }); diff --git a/superset-frontend/src/welcome/App.jsx b/superset-frontend/src/welcome/App.jsx index 0580ccbb8e42..696f7b2e9b2e 100644 --- a/superset-frontend/src/welcome/App.jsx +++ b/superset-frontend/src/welcome/App.jsx @@ -34,10 +34,12 @@ import DatasetList from 'src/views/datasetList/DatasetList'; import messageToastReducer from '../messageToasts/reducers'; import { initEnhancer } from '../reduxUtils'; import setupApp from '../setup/setupApp'; +import setupPlugins from '../setup/setupPlugins'; import Welcome from './Welcome'; import ToastPresenter from '../messageToasts/containers/ToastPresenter'; setupApp(); +setupPlugins(); const container = document.getElementById('app'); const bootstrap = JSON.parse(container.getAttribute('data-bootstrap')); diff --git a/superset/charts/api.py b/superset/charts/api.py index e1cd0ad29718..f6e8c351cea9 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -503,52 +503,11 @@ def add_apispec_components(self, api_spec: APISpec) -> None: ) super().add_apispec_components(api_spec) - @expose("/viz_types", methods=["GET"]) - @protect() - @safe - def viz_types(self) -> Response: - """Get unique viz_type values - --- - get: - responses: - 200: - description: charts unique viz_type data - content: - application/json: - schema: - type: object - properties: - count: - type: integer - result: - type: object - properties: - label: - type: string - value: - type: string - 400: - $ref: '#/components/responses/400' - 401: - $ref: '#/components/responses/401' - 404: - $ref: '#/components/responses/404' - 500: - $ref: '#/components/responses/500' - 422: - $ref: '#/components/responses/422' - 500: - $ref: '#/components/responses/500' - """ - values = ChartDAO.fetch_unique_column_values("viz_type") - result = [{"label": val, "value": val} for val in values] - return self.response(200, count=len(result), result=result) - @expose("/datasources", methods=["GET"]) @protect() @safe def datasources(self) -> Response: - """Get unique viz_type values + """Get available datasources --- get: responses: @@ -584,7 +543,7 @@ def datasources(self) -> Response: 500: $ref: '#/components/responses/500' """ - datasources = ChartDAO.fetch_unique_datasources() + datasources = ChartDAO.fetch_all_datasources() if not datasources: return self.response(200, count=0, result=[]) diff --git a/superset/charts/dao.py b/superset/charts/dao.py index c32a3d1dfd31..912f33c8e25f 100644 --- a/superset/charts/dao.py +++ b/superset/charts/dao.py @@ -58,23 +58,5 @@ def bulk_delete(models: Optional[List[Slice]], commit: bool = True) -> None: raise ex @staticmethod - def fetch_unique_column_values(column_name: str) -> List: - return [ - val[0] - for val in db.session.query(getattr(Slice, column_name)).distinct().all() - ] - - @staticmethod - def fetch_unique_datasources() -> Optional[List["BaseDatasource"]]: - groups = ( - db.session.query(Slice.datasource_type, Slice.datasource_id) - .group_by(Slice.datasource_type, Slice.datasource_id) - .all() - ) - datasources = [ - ConnectorRegistry.get_datasource( - session=db.session, datasource_type=ds[0], datasource_id=ds[1] - ) - for ds in groups - ] - return datasources + def fetch_all_datasources() -> List["BaseDatasource"]: + return ConnectorRegistry.get_all_datasources(db.session) diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py index cd39089dfcd4..321b31f88991 100644 --- a/tests/charts/api_tests.py +++ b/tests/charts/api_tests.py @@ -689,17 +689,6 @@ def test_query_exec_not_allowed(self): rv = self.client.post(uri, json=query_context) self.assertEqual(rv.status_code, 401) - def test_viz_types(self): - """ - Chart API: Test get viz_types - """ - self.login(username="admin") - uri = "api/v1/chart/viz_types" - rv = self.client.get(uri) - self.assertEqual(rv.status_code, 200) - data = json.loads(rv.data.decode("utf-8")) - self.assertEqual(data["count"], 20) - def test_datasources(self): """ Chart API: Test get datasources @@ -709,4 +698,4 @@ def test_datasources(self): rv = self.client.get(uri) self.assertEqual(rv.status_code, 200) data = json.loads(rv.data.decode("utf-8")) - self.assertEqual(data["count"], 4) + self.assertEqual(data["count"], 6)