[charts] adds new filters ui (#9530)
* [charts] adds new filters ui * move null check to be more visible * better filter lists and async filter functionality
This commit is contained in:
parent
9cf33e9f9d
commit
0b999e3b91
|
|
@ -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 [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(<ChartList {...mockedProps} />, {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<FilterContainer>
|
||||
<Title>{Header}:</Title>
|
||||
<StyledSelect
|
||||
data-test="filters-select"
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
clearable={false}
|
||||
/>
|
||||
{fetchSelects ? (
|
||||
<AsyncStyledSelect
|
||||
data-test="filters-select"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
loadOptions={fetchAndFormatSelects}
|
||||
placeholder={initialValue || emptyLabel}
|
||||
loadingPlaceholder="Loading..."
|
||||
clearable={false}
|
||||
/>
|
||||
) : (
|
||||
<StyledSelect
|
||||
data-test="filters-select"
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
clearable={false}
|
||||
/>
|
||||
)}
|
||||
</FilterContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -134,33 +154,36 @@ function UIFilters({
|
|||
}: UIFiltersProps) {
|
||||
return (
|
||||
<FilterWrapper>
|
||||
{filters.map(({ Header, input, selects, unfilteredLabel }, index) => {
|
||||
const initialValue =
|
||||
internalFilters[index] && internalFilters[index].value;
|
||||
if (input === 'select') {
|
||||
return (
|
||||
<SelectFilter
|
||||
key={Header}
|
||||
Header={Header}
|
||||
selects={selects}
|
||||
emptyLabel={unfilteredLabel}
|
||||
initialValue={initialValue}
|
||||
onSelect={(value: any) => updateFilterValue(index, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input === 'search') {
|
||||
return (
|
||||
<SearchFilter
|
||||
key={Header}
|
||||
Header={Header}
|
||||
initialValue={initialValue}
|
||||
onSubmit={(value: string) => updateFilterValue(index, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{filters.map(
|
||||
({ Header, input, selects, unfilteredLabel, fetchSelects }, index) => {
|
||||
const initialValue =
|
||||
internalFilters[index] && internalFilters[index].value;
|
||||
if (input === 'select') {
|
||||
return (
|
||||
<SelectFilter
|
||||
key={Header}
|
||||
Header={Header}
|
||||
selects={selects}
|
||||
emptyLabel={unfilteredLabel}
|
||||
initialValue={initialValue}
|
||||
fetchSelects={fetchSelects}
|
||||
onSelect={(value: any) => updateFilterValue(index, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input === 'search') {
|
||||
return (
|
||||
<SearchFilter
|
||||
key={Header}
|
||||
Header={Header}
|
||||
initialValue={initialValue}
|
||||
onSubmit={(value: string) => updateFilterValue(index, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
)}
|
||||
</FilterWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export interface Filter {
|
|||
input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search';
|
||||
unfilteredLabel?: string;
|
||||
selects?: Select[];
|
||||
onFilterOpen?: () => void;
|
||||
fetchSelects?: () => Promise<Select[]>;
|
||||
}
|
||||
|
||||
export type Filters = Filter[];
|
||||
|
|
@ -43,7 +45,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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -33,6 +34,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;
|
||||
|
||||
|
|
@ -47,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.
|
||||
|
|
@ -67,32 +68,31 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
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);
|
||||
}
|
||||
|
|
@ -111,6 +111,10 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
return this.hasPerm('can_delete');
|
||||
}
|
||||
|
||||
get isNewUIEnabled() {
|
||||
return isFeatureEnabled(FeatureFlag.LIST_VIEWS_NEW_UI);
|
||||
}
|
||||
|
||||
initialSort = [{ id: 'changed_on', desc: true }];
|
||||
|
||||
columns = [
|
||||
|
|
@ -175,6 +179,10 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
accessor: 'owners',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
accessor: 'datasource',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { state, original } }: any) => {
|
||||
const handleDelete = () => this.handleChartDelete(original);
|
||||
|
|
@ -311,11 +319,27 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
},
|
||||
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' &&
|
||||
fltr.value &&
|
||||
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,
|
||||
|
|
@ -331,16 +355,83 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
.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 = () => {
|
||||
const { filterOperators, owners } = this.state;
|
||||
createFetchResource = (
|
||||
resource: string,
|
||||
postProcess?: (value: []) => any[],
|
||||
) => async () => {
|
||||
try {
|
||||
const { json = {} } = await SupersetClient.get({
|
||||
endpoint: resource,
|
||||
});
|
||||
return postProcess ? postProcess(json?.result) : json?.result;
|
||||
} catch (e) {
|
||||
this.props.addDangerToast(
|
||||
t('An error occurred while fetching chart filters: %s', e.message),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
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: [
|
||||
{
|
||||
Header: 'Owner',
|
||||
id: 'owners',
|
||||
input: 'select',
|
||||
operator: 'rel_m_m',
|
||||
unfilteredLabel: 'All',
|
||||
fetchSelects: fetchOwners,
|
||||
},
|
||||
{
|
||||
Header: 'Viz Type',
|
||||
id: 'viz_type',
|
||||
input: 'select',
|
||||
operator: 'eq',
|
||||
unfilteredLabel: 'All',
|
||||
selects: getChartMetadataRegistry()
|
||||
.keys()
|
||||
.map(k => ({ label: k, value: k })),
|
||||
},
|
||||
{
|
||||
Header: 'Dataset',
|
||||
id: 'datasource',
|
||||
input: 'select',
|
||||
operator: 'eq',
|
||||
unfilteredLabel: 'All',
|
||||
fetchSelects: this.createFetchResource('/api/v1/chart/datasources'),
|
||||
},
|
||||
{
|
||||
Header: 'Search',
|
||||
id: 'slice_name',
|
||||
input: 'search',
|
||||
operator: 'name_or_description',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const convertFilter = ({
|
||||
name: label,
|
||||
operator,
|
||||
|
|
@ -349,6 +440,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
operator: string;
|
||||
}) => ({ label, value: operator });
|
||||
|
||||
const owners = await fetchOwners();
|
||||
this.setState({
|
||||
filters: [
|
||||
{
|
||||
|
|
@ -376,7 +468,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
id: 'owners',
|
||||
input: 'select',
|
||||
operators: filterOperators.owners.map(convertFilter),
|
||||
selects: owners.map(({ text: label, value }) => ({ label, value })),
|
||||
selects: owners,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -435,6 +527,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
|||
initialSort={this.initialSort}
|
||||
filters={filters}
|
||||
bulkActions={bulkActions}
|
||||
useNewUIFilters={this.isNewUIEnabled}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
},
|
||||
([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<Props, State> {
|
|||
})
|
||||
.catch(() => {
|
||||
this.props.addDangerToast(
|
||||
t('An error occurred while fetching Datasets'),
|
||||
t('An error occurred while fetching datasets'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from superset.charts.commands.exceptions import (
|
|||
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,
|
||||
|
|
@ -77,6 +78,8 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
RouteMethod.RELATED,
|
||||
"bulk_delete", # not using RouteMethod since locally defined
|
||||
"data",
|
||||
"viz_types",
|
||||
"datasources",
|
||||
}
|
||||
class_permission_name = "SliceModelView"
|
||||
show_columns = [
|
||||
|
|
@ -106,6 +109,10 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
"viz_type",
|
||||
"params",
|
||||
"cache_timeout",
|
||||
"owners.id",
|
||||
"owners.username",
|
||||
"owners.first_name",
|
||||
"owners.last_name",
|
||||
]
|
||||
order_columns = [
|
||||
"slice_name",
|
||||
|
|
@ -119,6 +126,8 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
"description",
|
||||
"viz_type",
|
||||
"datasource_name",
|
||||
"datasource_id",
|
||||
"datasource_type",
|
||||
"owners",
|
||||
)
|
||||
base_order = ("changed_on", "desc")
|
||||
|
|
@ -503,3 +512,56 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
chart_type.__name__, schema=chart_type,
|
||||
)
|
||||
super().add_apispec_components(api_spec)
|
||||
|
||||
@expose("/datasources", methods=["GET"])
|
||||
@protect()
|
||||
@safe
|
||||
def datasources(self) -> Response:
|
||||
"""Get available datasources
|
||||
---
|
||||
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_all_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)
|
||||
|
|
|
|||
|
|
@ -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,7 @@ class ChartDAO(BaseDAO):
|
|||
if commit:
|
||||
db.session.rollback()
|
||||
raise ex
|
||||
|
||||
@staticmethod
|
||||
def fetch_all_datasources() -> List["BaseDatasource"]:
|
||||
return ConnectorRegistry.get_all_datasources(db.session)
|
||||
|
|
|
|||
|
|
@ -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]] = {}
|
||||
|
|
|
|||
|
|
@ -689,3 +689,14 @@ class ChartApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
|||
uri = "api/v1/chart/data"
|
||||
rv = self.post_assert_metric(uri, query_context, "data")
|
||||
self.assertEqual(rv.status_code, 401)
|
||||
|
||||
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"], 6)
|
||||
|
|
|
|||
Loading…
Reference in New Issue