From 962252030bb320ed0a798e732473d8b0eda3a848 Mon Sep 17 00:00:00 2001 From: Smart-Codi Date: Thu, 7 Jul 2022 18:24:47 +0100 Subject: [PATCH] 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 --- .../src/dashboard/actions/sliceEntities.js | 214 +++++++++++------- .../dashboard/actions/sliceEntities.test.js | 102 +++++++++ .../src/dashboard/components/SliceAdder.jsx | 26 ++- .../dashboard/components/SliceAdder.test.jsx | 1 + .../src/dashboard/containers/SliceAdder.jsx | 8 +- 5 files changed, 264 insertions(+), 87 deletions(-) create mode 100644 superset-frontend/src/dashboard/actions/sliceEntities.test.js diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.js b/superset-frontend/src/dashboard/actions/sliceEntities.js index be11160520..176b84fa2b 100644 --- a/superset-frontend/src/dashboard/actions/sliceEntities.js +++ b/superset-frontend/src/dashboard/actions/sliceEntities.js @@ -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, + ); + }; +} diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.test.js b/superset-frontend/src/dashboard/actions/sliceEntities.test.js new file mode 100644 index 0000000000..0fe9365fc9 --- /dev/null +++ b/superset-frontend/src/dashboard/actions/sliceEntities.test.js @@ -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); + }); + }); +}); diff --git a/superset-frontend/src/dashboard/components/SliceAdder.jsx b/superset-frontend/src/dashboard/components/SliceAdder.jsx index 22f8038ee4..95f9180a33 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.jsx @@ -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 { this.handleChange(ev.target.value)} onKeyPress={this.handleKeyPress} data-test="dashboard-charts-filter-search-input" /> diff --git a/superset-frontend/src/dashboard/components/SliceAdder.test.jsx b/superset-frontend/src/dashboard/components/SliceAdder.test.jsx index 16703d0505..7bfd971cb7 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.test.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.test.jsx @@ -36,6 +36,7 @@ describe('SliceAdder', () => { const props = { ...mockSliceEntities, fetchAllSlices: () => {}, + fetchSortedSlices: () => {}, selectedSliceIds: [127, 128], userId: '1', height: 100, diff --git a/superset-frontend/src/dashboard/containers/SliceAdder.jsx b/superset-frontend/src/dashboard/containers/SliceAdder.jsx index 078ded23d8..580dedc2f8 100644 --- a/superset-frontend/src/dashboard/containers/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/containers/SliceAdder.jsx @@ -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, );