mirror of https://github.com/apache/superset.git
fix: Add database search in available charts on dashboard. (#19244)
* add database search * resolve lint issue * add test for sliceEntities actions * fix lint issue * add licence into test * fix pipeline broken * fix sort by recent * resolve comment
This commit is contained in:
parent
4bfa622d02
commit
962252030b
|
@ -24,6 +24,12 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
|
|
||||||
export const SET_ALL_SLICES = 'SET_ALL_SLICES';
|
export const SET_ALL_SLICES = 'SET_ALL_SLICES';
|
||||||
|
const FETCH_SLICES_PAGE_SIZE = 200;
|
||||||
|
|
||||||
|
export function getDatasourceParameter(datasourceId, datasourceType) {
|
||||||
|
return `${datasourceId}__${datasourceType}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function setAllSlices(slices) {
|
export function setAllSlices(slices) {
|
||||||
return { type: SET_ALL_SLICES, payload: { slices } };
|
return { type: SET_ALL_SLICES, payload: { slices } };
|
||||||
}
|
}
|
||||||
|
@ -38,96 +44,142 @@ export function fetchAllSlicesFailed(error) {
|
||||||
return { type: FETCH_ALL_SLICES_FAILED, payload: { error } };
|
return { type: FETCH_ALL_SLICES_FAILED, payload: { error } };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDatasourceParameter(datasourceId, datasourceType) {
|
export function fetchSlices(
|
||||||
return `${datasourceId}__${datasourceType}`;
|
userId,
|
||||||
|
excludeFilterBox,
|
||||||
|
dispatch,
|
||||||
|
filter_value,
|
||||||
|
sortColumn = 'changed_on',
|
||||||
|
slices = {},
|
||||||
|
) {
|
||||||
|
const additional_filters = filter_value
|
||||||
|
? [{ col: 'slice_name', opr: 'chart_all_text', value: filter_value }]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const cloneSlices = { ...slices };
|
||||||
|
|
||||||
|
return SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/chart/?q=${rison.encode({
|
||||||
|
columns: [
|
||||||
|
'changed_on_delta_humanized',
|
||||||
|
'changed_on_utc',
|
||||||
|
'datasource_id',
|
||||||
|
'datasource_type',
|
||||||
|
'datasource_url',
|
||||||
|
'datasource_name_text',
|
||||||
|
'description_markeddown',
|
||||||
|
'description',
|
||||||
|
'id',
|
||||||
|
'params',
|
||||||
|
'slice_name',
|
||||||
|
'url',
|
||||||
|
'viz_type',
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
{ col: 'owners', opr: 'rel_m_m', value: userId },
|
||||||
|
...additional_filters,
|
||||||
|
],
|
||||||
|
page_size: FETCH_SLICES_PAGE_SIZE,
|
||||||
|
order_column:
|
||||||
|
sortColumn === 'changed_on' ? 'changed_on_delta_humanized' : sortColumn,
|
||||||
|
order_direction: sortColumn === 'changed_on' ? 'desc' : 'asc',
|
||||||
|
})}`,
|
||||||
|
})
|
||||||
|
.then(({ json }) => {
|
||||||
|
let { result } = json;
|
||||||
|
// disable add filter_box viz to dashboard
|
||||||
|
if (excludeFilterBox) {
|
||||||
|
result = result.filter(slice => slice.viz_type !== 'filter_box');
|
||||||
|
}
|
||||||
|
result.forEach(slice => {
|
||||||
|
let form_data = JSON.parse(slice.params);
|
||||||
|
form_data = {
|
||||||
|
...form_data,
|
||||||
|
// force using datasource stored in relational table prop
|
||||||
|
datasource:
|
||||||
|
getDatasourceParameter(
|
||||||
|
slice.datasource_id,
|
||||||
|
slice.datasource_type,
|
||||||
|
) || form_data.datasource,
|
||||||
|
};
|
||||||
|
cloneSlices[slice.id] = {
|
||||||
|
slice_id: slice.id,
|
||||||
|
slice_url: slice.url,
|
||||||
|
slice_name: slice.slice_name,
|
||||||
|
form_data,
|
||||||
|
datasource_name: slice.datasource_name_text,
|
||||||
|
datasource_url: slice.datasource_url,
|
||||||
|
datasource_id: slice.datasource_id,
|
||||||
|
datasource_type: slice.datasource_type,
|
||||||
|
changed_on: new Date(slice.changed_on_utc).getTime(),
|
||||||
|
description: slice.description,
|
||||||
|
description_markdown: slice.description_markeddown,
|
||||||
|
viz_type: slice.viz_type,
|
||||||
|
modified: slice.changed_on_delta_humanized,
|
||||||
|
changed_on_humanized: slice.changed_on_delta_humanized,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return dispatch(setAllSlices(cloneSlices));
|
||||||
|
})
|
||||||
|
.catch(errorResponse =>
|
||||||
|
getClientErrorObject(errorResponse).then(({ error }) => {
|
||||||
|
dispatch(
|
||||||
|
fetchAllSlicesFailed(error || t('Could not fetch all saved charts')),
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
addDangerToast(
|
||||||
|
t('Sorry there was an error fetching saved charts: ') + error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FETCH_SLICES_PAGE_SIZE = 200;
|
|
||||||
export function fetchAllSlices(userId, excludeFilterBox = false) {
|
export function fetchAllSlices(userId, excludeFilterBox = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { sliceEntities } = getState();
|
const { sliceEntities } = getState();
|
||||||
if (sliceEntities.lastUpdated === 0) {
|
if (sliceEntities.lastUpdated === 0) {
|
||||||
dispatch(fetchAllSlicesStarted());
|
dispatch(fetchAllSlicesStarted());
|
||||||
|
return fetchSlices(userId, excludeFilterBox, dispatch, undefined);
|
||||||
return SupersetClient.get({
|
|
||||||
endpoint: `/api/v1/chart/?q=${rison.encode({
|
|
||||||
columns: [
|
|
||||||
'changed_on_delta_humanized',
|
|
||||||
'changed_on_utc',
|
|
||||||
'datasource_id',
|
|
||||||
'datasource_type',
|
|
||||||
'datasource_url',
|
|
||||||
'datasource_name_text',
|
|
||||||
'description_markeddown',
|
|
||||||
'description',
|
|
||||||
'id',
|
|
||||||
'params',
|
|
||||||
'slice_name',
|
|
||||||
'url',
|
|
||||||
'viz_type',
|
|
||||||
],
|
|
||||||
filters: [{ col: 'owners', opr: 'rel_m_m', value: userId }],
|
|
||||||
page_size: FETCH_SLICES_PAGE_SIZE,
|
|
||||||
order_column: 'changed_on_delta_humanized',
|
|
||||||
order_direction: 'desc',
|
|
||||||
})}`,
|
|
||||||
})
|
|
||||||
.then(({ json }) => {
|
|
||||||
const slices = {};
|
|
||||||
let { result } = json;
|
|
||||||
// disable add filter_box viz to dashboard
|
|
||||||
if (excludeFilterBox) {
|
|
||||||
result = result.filter(slice => slice.viz_type !== 'filter_box');
|
|
||||||
}
|
|
||||||
result.forEach(slice => {
|
|
||||||
let form_data = JSON.parse(slice.params);
|
|
||||||
form_data = {
|
|
||||||
...form_data,
|
|
||||||
// force using datasource stored in relational table prop
|
|
||||||
datasource:
|
|
||||||
getDatasourceParameter(
|
|
||||||
slice.datasource_id,
|
|
||||||
slice.datasource_type,
|
|
||||||
) || form_data.datasource,
|
|
||||||
};
|
|
||||||
slices[slice.id] = {
|
|
||||||
slice_id: slice.id,
|
|
||||||
slice_url: slice.url,
|
|
||||||
slice_name: slice.slice_name,
|
|
||||||
form_data,
|
|
||||||
datasource_name: slice.datasource_name_text,
|
|
||||||
datasource_url: slice.datasource_url,
|
|
||||||
datasource_id: slice.datasource_id,
|
|
||||||
datasource_type: slice.datasource_type,
|
|
||||||
changed_on: new Date(slice.changed_on_utc).getTime(),
|
|
||||||
description: slice.description,
|
|
||||||
description_markdown: slice.description_markeddown,
|
|
||||||
viz_type: slice.viz_type,
|
|
||||||
modified: slice.changed_on_delta_humanized,
|
|
||||||
changed_on_humanized: slice.changed_on_delta_humanized,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return dispatch(setAllSlices(slices));
|
|
||||||
})
|
|
||||||
.catch(
|
|
||||||
errorResponse =>
|
|
||||||
console.log(errorResponse) ||
|
|
||||||
getClientErrorObject(errorResponse).then(({ error }) => {
|
|
||||||
dispatch(
|
|
||||||
fetchAllSlicesFailed(
|
|
||||||
error || t('Could not fetch all saved charts'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
addDangerToast(
|
|
||||||
t('Sorry there was an error fetching saved charts: ') + error,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch(setAllSlices(sliceEntities.slices));
|
return dispatch(setAllSlices(sliceEntities.slices));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchSortedSlices(
|
||||||
|
userId,
|
||||||
|
excludeFilterBox = false,
|
||||||
|
order_column,
|
||||||
|
) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(fetchAllSlicesStarted());
|
||||||
|
return fetchSlices(
|
||||||
|
userId,
|
||||||
|
excludeFilterBox,
|
||||||
|
dispatch,
|
||||||
|
undefined,
|
||||||
|
order_column,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchFilteredSlices(
|
||||||
|
userId,
|
||||||
|
excludeFilterBox = false,
|
||||||
|
filter_value,
|
||||||
|
) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(fetchAllSlicesStarted());
|
||||||
|
const { sliceEntities } = getState();
|
||||||
|
return fetchSlices(
|
||||||
|
userId,
|
||||||
|
excludeFilterBox,
|
||||||
|
dispatch,
|
||||||
|
filter_value,
|
||||||
|
undefined,
|
||||||
|
sliceEntities.slices,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/**
|
||||||
|
* 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 sinon from 'sinon';
|
||||||
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FETCH_ALL_SLICES_STARTED,
|
||||||
|
fetchSortedSlices,
|
||||||
|
fetchFilteredSlices,
|
||||||
|
fetchAllSlices,
|
||||||
|
} from './sliceEntities';
|
||||||
|
|
||||||
|
describe('slice entity actions', () => {
|
||||||
|
const mockState = {
|
||||||
|
sliceEntities: { slices: {} },
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setup(stateOverrides) {
|
||||||
|
const state = { ...mockState, ...stateOverrides };
|
||||||
|
const getState = sinon.spy(() => state);
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
|
||||||
|
return { getState, dispatch, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
let spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spy = sinon.spy(SupersetClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchSortedSlices', () => {
|
||||||
|
it('should dispatch an fetchAllSlicesStarted action', async () => {
|
||||||
|
const { dispatch } = setup();
|
||||||
|
const thunk1 = fetchSortedSlices('userId', false, 'orderColumn');
|
||||||
|
await thunk1(dispatch);
|
||||||
|
expect(dispatch.getCall(0).args[0]).toEqual({
|
||||||
|
type: FETCH_ALL_SLICES_STARTED,
|
||||||
|
});
|
||||||
|
expect(spy.get.callCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchFilteredSlices', () => {
|
||||||
|
it('should dispatch an fetchAllSlicesStarted action', async () => {
|
||||||
|
const { dispatch, getState } = setup();
|
||||||
|
const thunk1 = fetchFilteredSlices('userId', false, 'filter_value');
|
||||||
|
await thunk1(dispatch, getState);
|
||||||
|
expect(dispatch.getCall(0).args[0]).toEqual({
|
||||||
|
type: FETCH_ALL_SLICES_STARTED,
|
||||||
|
});
|
||||||
|
expect(spy.get.callCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAllSlices', () => {
|
||||||
|
it('should not trigger fetchSlices when sliceEntities lastUpdate is not 0', async () => {
|
||||||
|
const { dispatch, getState } = setup({
|
||||||
|
sliceEntities: { slices: {}, lastUpdated: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const thunk1 = fetchAllSlices('userId', false, 'filter_value');
|
||||||
|
await thunk1(dispatch, getState);
|
||||||
|
|
||||||
|
expect(spy.get.callCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger fetchSlices when sliceEntities lastUpdate is 0', async () => {
|
||||||
|
const { dispatch, getState } = setup({
|
||||||
|
sliceEntities: { slices: {}, lastUpdated: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const thunk1 = fetchAllSlices('userId', false, 'filter_value');
|
||||||
|
await thunk1(dispatch, getState);
|
||||||
|
|
||||||
|
expect(spy.get.callCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -43,6 +43,7 @@ import {
|
||||||
} from 'src/dashboard/util/constants';
|
} from 'src/dashboard/util/constants';
|
||||||
import { slicePropShape } from 'src/dashboard/util/propShapes';
|
import { slicePropShape } from 'src/dashboard/util/propShapes';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||||
|
import _ from 'lodash';
|
||||||
import AddSliceCard from './AddSliceCard';
|
import AddSliceCard from './AddSliceCard';
|
||||||
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
||||||
import DragDroppable from './dnd/DragDroppable';
|
import DragDroppable from './dnd/DragDroppable';
|
||||||
|
@ -144,7 +145,6 @@ class SliceAdder extends React.Component {
|
||||||
this.rowRenderer = this.rowRenderer.bind(this);
|
this.rowRenderer = this.rowRenderer.bind(this);
|
||||||
this.searchUpdated = this.searchUpdated.bind(this);
|
this.searchUpdated = this.searchUpdated.bind(this);
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.handleSelect = this.handleSelect.bind(this);
|
this.handleSelect = this.handleSelect.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,9 +194,17 @@ class SliceAdder extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(ev) {
|
handleChange = _.debounce(value => {
|
||||||
this.searchUpdated(ev.target.value);
|
this.searchUpdated(value);
|
||||||
}
|
|
||||||
|
const { userId, filterboxMigrationState } = this.props;
|
||||||
|
this.slicesRequest = this.props.fetchFilteredSlices(
|
||||||
|
userId,
|
||||||
|
isFeatureEnabled(FeatureFlag.ENABLE_FILTER_BOX_MIGRATION) &&
|
||||||
|
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
searchUpdated(searchTerm) {
|
searchUpdated(searchTerm) {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
|
@ -216,6 +224,14 @@ class SliceAdder extends React.Component {
|
||||||
sortBy,
|
sortBy,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { userId, filterboxMigrationState } = this.props;
|
||||||
|
this.slicesRequest = this.props.fetchSortedSlices(
|
||||||
|
userId,
|
||||||
|
isFeatureEnabled(FeatureFlag.ENABLE_FILTER_BOX_MIGRATION) &&
|
||||||
|
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||||
|
sortBy,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
rowRenderer({ key, index, style }) {
|
rowRenderer({ key, index, style }) {
|
||||||
|
@ -292,7 +308,7 @@ class SliceAdder extends React.Component {
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('Filter your charts')}
|
placeholder={t('Filter your charts')}
|
||||||
className="search-input"
|
className="search-input"
|
||||||
onChange={this.handleChange}
|
onChange={ev => this.handleChange(ev.target.value)}
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
data-test="dashboard-charts-filter-search-input"
|
data-test="dashboard-charts-filter-search-input"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -36,6 +36,7 @@ describe('SliceAdder', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...mockSliceEntities,
|
...mockSliceEntities,
|
||||||
fetchAllSlices: () => {},
|
fetchAllSlices: () => {},
|
||||||
|
fetchSortedSlices: () => {},
|
||||||
selectedSliceIds: [127, 128],
|
selectedSliceIds: [127, 128],
|
||||||
userId: '1',
|
userId: '1',
|
||||||
height: 100,
|
height: 100,
|
||||||
|
|
|
@ -19,7 +19,11 @@
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchAllSlices } from '../actions/sliceEntities';
|
import {
|
||||||
|
fetchAllSlices,
|
||||||
|
fetchSortedSlices,
|
||||||
|
fetchFilteredSlices,
|
||||||
|
} from '../actions/sliceEntities';
|
||||||
import SliceAdder from '../components/SliceAdder';
|
import SliceAdder from '../components/SliceAdder';
|
||||||
|
|
||||||
function mapStateToProps(
|
function mapStateToProps(
|
||||||
|
@ -44,6 +48,8 @@ function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(
|
return bindActionCreators(
|
||||||
{
|
{
|
||||||
fetchAllSlices,
|
fetchAllSlices,
|
||||||
|
fetchSortedSlices,
|
||||||
|
fetchFilteredSlices,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue