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:
Smart-Codi 2022-07-07 18:24:47 +01:00 committed by GitHub
parent 4bfa622d02
commit 962252030b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 264 additions and 87 deletions

View File

@ -24,6 +24,12 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
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) {
return { type: SET_ALL_SLICES, payload: { slices } };
}
@ -38,96 +44,142 @@ export function fetchAllSlicesFailed(error) {
return { type: FETCH_ALL_SLICES_FAILED, payload: { error } };
}
export function getDatasourceParameter(datasourceId, datasourceType) {
return `${datasourceId}__${datasourceType}`;
export function fetchSlices(
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) {
return (dispatch, getState) => {
const { sliceEntities } = getState();
if (sliceEntities.lastUpdated === 0) {
dispatch(fetchAllSlicesStarted());
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 fetchSlices(userId, excludeFilterBox, dispatch, undefined);
}
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,
);
};
}

View File

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

View File

@ -43,6 +43,7 @@ import {
} from 'src/dashboard/util/constants';
import { slicePropShape } from 'src/dashboard/util/propShapes';
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
import _ from 'lodash';
import AddSliceCard from './AddSliceCard';
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
import DragDroppable from './dnd/DragDroppable';
@ -144,7 +145,6 @@ class SliceAdder extends React.Component {
this.rowRenderer = this.rowRenderer.bind(this);
this.searchUpdated = this.searchUpdated.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSelect = this.handleSelect.bind(this);
}
@ -194,9 +194,17 @@ class SliceAdder extends React.Component {
}
}
handleChange(ev) {
this.searchUpdated(ev.target.value);
}
handleChange = _.debounce(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) {
this.setState(prevState => ({
@ -216,6 +224,14 @@ class SliceAdder extends React.Component {
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 }) {
@ -292,7 +308,7 @@ class SliceAdder extends React.Component {
<Input
placeholder={t('Filter your charts')}
className="search-input"
onChange={this.handleChange}
onChange={ev => this.handleChange(ev.target.value)}
onKeyPress={this.handleKeyPress}
data-test="dashboard-charts-filter-search-input"
/>

View File

@ -36,6 +36,7 @@ describe('SliceAdder', () => {
const props = {
...mockSliceEntities,
fetchAllSlices: () => {},
fetchSortedSlices: () => {},
selectedSliceIds: [127, 128],
userId: '1',
height: 100,

View File

@ -19,7 +19,11 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { fetchAllSlices } from '../actions/sliceEntities';
import {
fetchAllSlices,
fetchSortedSlices,
fetchFilteredSlices,
} from '../actions/sliceEntities';
import SliceAdder from '../components/SliceAdder';
function mapStateToProps(
@ -44,6 +48,8 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
fetchAllSlices,
fetchSortedSlices,
fetchFilteredSlices,
},
dispatch,
);