diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts index 4251b6a7ae..50fd153146 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts @@ -752,7 +752,7 @@ describe('Dashboard edit', () => { }); it('should remove added charts', () => { - dragComponent('Pivot Table'); + dragComponent('Unicode Cloud'); cy.getBySel('dashboard-component-chart-holder').should('have.length', 1); cy.getBySel('dashboard-delete-component-button').click(); cy.getBySel('dashboard-component-chart-holder').should('have.length', 0); diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.js b/superset-frontend/src/dashboard/actions/sliceEntities.js deleted file mode 100644 index 99a0627211..0000000000 --- a/superset-frontend/src/dashboard/actions/sliceEntities.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * 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. - */ -/* eslint camelcase: 0 */ -import { FeatureFlag, SupersetClient, t } from '@superset-ui/core'; -import rison from 'rison'; - -import { addDangerToast } from 'src/components/MessageToasts/actions'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; -import { isFeatureEnabled } from 'src/featureFlags'; - -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 } }; -} - -export const FETCH_ALL_SLICES_STARTED = 'FETCH_ALL_SLICES_STARTED'; -export function fetchAllSlicesStarted() { - return { type: FETCH_ALL_SLICES_STARTED }; -} - -export const FETCH_ALL_SLICES_FAILED = 'FETCH_ALL_SLICES_FAILED'; -export function fetchAllSlicesFailed(error) { - return { type: FETCH_ALL_SLICES_FAILED, payload: { error } }; -} - -export function fetchSlices( - userId, - dispatch, - filter_value, - sortColumn = 'changed_on', - slices = {}, -) { - const additional_filters = filter_value - ? [{ col: 'slice_name', opr: 'chart_all_text', value: filter_value }] - : []; - - if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { - additional_filters.push({ - col: 'viz_type', - opr: 'neq', - value: 'filter_box', - }); - } - - 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', - 'thumbnail_url', - 'url', - 'viz_type', - ], - filters: [...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 }) => { - const { result } = json; - 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, - thumbnail_url: slice.thumbnail_url, - }; - }); - - 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, - ), - ); - }), - ); -} - -export function fetchAllSlices(userId) { - return (dispatch, getState) => { - const { sliceEntities } = getState(); - if (sliceEntities.lastUpdated === 0) { - dispatch(fetchAllSlicesStarted()); - return fetchSlices(userId, dispatch, undefined); - } - - return dispatch(setAllSlices(sliceEntities.slices)); - }; -} - -export function fetchSortedSlices(userId, order_column) { - return dispatch => { - dispatch(fetchAllSlicesStarted()); - return fetchSlices(userId, dispatch, undefined, order_column); - }; -} - -export function fetchFilteredSlices(userId, filter_value) { - return (dispatch, getState) => { - dispatch(fetchAllSlicesStarted()); - const { sliceEntities } = getState(); - return fetchSlices( - userId, - 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 deleted file mode 100644 index 1f3d0cb440..0000000000 --- a/superset-frontend/src/dashboard/actions/sliceEntities.test.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * 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', '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', '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/actions/sliceEntities.ts b/superset-frontend/src/dashboard/actions/sliceEntities.ts new file mode 100644 index 0000000000..a30470aad1 --- /dev/null +++ b/superset-frontend/src/dashboard/actions/sliceEntities.ts @@ -0,0 +1,178 @@ +/** + * 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 rison from 'rison'; +import { + DatasourceType, + FeatureFlag, + SupersetClient, + t, +} from '@superset-ui/core'; +import { addDangerToast } from 'src/components/MessageToasts/actions'; +import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { isFeatureEnabled } from 'src/featureFlags'; +import { Dispatch } from 'redux'; +import { Slice } from '../types'; + +const FETCH_SLICES_PAGE_SIZE = 200; + +export function getDatasourceParameter( + datasourceId: number, + datasourceType: DatasourceType, +) { + return `${datasourceId}__${datasourceType}`; +} + +export const ADD_SLICES = 'ADD_SLICES'; +function addSlices(slices: { [id: number]: Slice }) { + return { type: ADD_SLICES, payload: { slices } }; +} + +export const SET_SLICES = 'SET_SLICES'; +function setSlices(slices: { [id: number]: Slice }) { + return { type: SET_SLICES, payload: { slices } }; +} + +export const FETCH_ALL_SLICES_STARTED = 'FETCH_ALL_SLICES_STARTED'; +function fetchAllSlicesStarted() { + return { type: FETCH_ALL_SLICES_STARTED }; +} + +export const FETCH_ALL_SLICES_FAILED = 'FETCH_ALL_SLICES_FAILED'; +function fetchAllSlicesFailed(error: string) { + return { type: FETCH_ALL_SLICES_FAILED, payload: { error } }; +} + +const parseResult = (result: any[]) => + result.reduce((slices, slice: any) => { + 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, + }; + return { + ...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, + thumbnail_url: slice.thumbnail_url, + owners: slice.owners, + created_by: slice.created_by, + }, + }; + }, {}); + +export function updateSlices(slices: { [id: number]: Slice }) { + return (dispatch: Dispatch) => { + dispatch(setSlices(slices)); + }; +} + +export function fetchSlices( + userId?: number, + filter_value?: string, + sortColumn = 'changed_on', +) { + return (dispatch: Dispatch) => { + dispatch(fetchAllSlicesStarted()); + + const filters: { + col: string; + opr: string; + value: string | number; + }[] = filter_value + ? [{ col: 'slice_name', opr: 'chart_all_text', value: filter_value }] + : []; + + if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { + filters.push({ + col: 'viz_type', + opr: 'neq', + value: 'filter_box', + }); + } + + if (userId) { + filters.push({ col: 'owners', opr: 'rel_m_m', value: userId }); + } + + 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', + 'thumbnail_url', + 'url', + 'viz_type', + 'owners.id', + 'created_by.id', + ], + 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 }) => { + const { result } = json; + const slices = parseResult(result); + return dispatch(addSlices(slices)); + }) + .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, + ), + ); + }), + ); + }; +} diff --git a/superset-frontend/src/dashboard/components/SliceAdder.jsx b/superset-frontend/src/dashboard/components/SliceAdder.jsx index 95f6c930ab..617fd32e8f 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.jsx @@ -28,6 +28,11 @@ import { Select } from 'src/components'; import Loading from 'src/components/Loading'; import Button from 'src/components/Button'; import Icons from 'src/components/Icons'; +import { + LocalStorageKeys, + getItem, + setItem, +} from 'src/utils/localStorageHelpers'; import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE, @@ -37,18 +42,21 @@ import { NEW_COMPONENTS_SOURCE_ID, } from 'src/dashboard/util/constants'; import { slicePropShape } from 'src/dashboard/util/propShapes'; -import _ from 'lodash'; +import { debounce, pickBy } from 'lodash'; +import Checkbox from 'src/components/Checkbox'; +import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import AddSliceCard from './AddSliceCard'; import AddSliceDragPreview from './dnd/AddSliceDragPreview'; import DragDroppable from './dnd/DragDroppable'; const propTypes = { - fetchAllSlices: PropTypes.func.isRequired, + fetchSlices: PropTypes.func.isRequired, + updateSlices: PropTypes.func.isRequired, isLoading: PropTypes.bool.isRequired, slices: PropTypes.objectOf(slicePropShape).isRequired, lastUpdated: PropTypes.number.isRequired, errorMessage: PropTypes.string, - userId: PropTypes.string.isRequired, + userId: PropTypes.number.isRequired, selectedSliceIds: PropTypes.arrayOf(PropTypes.number), editMode: PropTypes.bool, dashboardId: PropTypes.number, @@ -68,15 +76,20 @@ const KEYS_TO_SORT = { changed_on: t('recent'), }; -const DEFAULT_SORT_KEY = 'changed_on'; +export const DEFAULT_SORT_KEY = 'changed_on'; const DEFAULT_CELL_HEIGHT = 128; const Controls = styled.div` - display: flex; - flex-direction: row; - padding: ${({ theme }) => theme.gridUnit * 3}px; - padding-top: ${({ theme }) => theme.gridUnit * 4}px; + ${({ theme }) => ` + display: flex; + flex-direction: row; + padding: + ${theme.gridUnit * 4}px + ${theme.gridUnit * 3}px + ${theme.gridUnit * 4}px + ${theme.gridUnit * 3}px; + `} `; const StyledSelect = styled(Select)` @@ -133,23 +146,35 @@ class SliceAdder extends React.Component { searchTerm: '', sortBy: DEFAULT_SORT_KEY, selectedSliceIdsSet: new Set(props.selectedSliceIds), + showOnlyMyCharts: getItem( + LocalStorageKeys.dashboard__editor_show_only_my_charts, + true, + ), }; this.rowRenderer = this.rowRenderer.bind(this); this.searchUpdated = this.searchUpdated.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); this.handleSelect = this.handleSelect.bind(this); + this.userIdForFetch = this.userIdForFetch.bind(this); + this.onShowOnlyMyCharts = this.onShowOnlyMyCharts.bind(this); + } + + userIdForFetch() { + return this.state.showOnlyMyCharts ? this.props.userId : undefined; } componentDidMount() { - this.slicesRequest = this.props.fetchAllSlices(this.props.userId); + this.slicesRequest = this.props.fetchSlices(this.userIdForFetch()); } UNSAFE_componentWillReceiveProps(nextProps) { const nextState = {}; if (nextProps.lastUpdated !== this.props.lastUpdated) { - nextState.filteredSlices = Object.values(nextProps.slices) - .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS)) - .sort(SliceAdder.sortByComparator(this.state.sortBy)); + nextState.filteredSlices = this.getFilteredSortedSlices( + nextProps.slices, + this.state.searchTerm, + this.state.sortBy, + this.state.showOnlyMyCharts, + ); } if (nextProps.selectedSliceIds !== this.props.selectedSliceIds) { @@ -162,38 +187,46 @@ class SliceAdder extends React.Component { } componentWillUnmount() { + // Clears the redux store keeping only selected items + const selectedSlices = pickBy(this.props.slices, value => + this.state.selectedSliceIdsSet.has(value.slice_id), + ); + this.props.updateSlices(selectedSlices); if (this.slicesRequest && this.slicesRequest.abort) { this.slicesRequest.abort(); } } - getFilteredSortedSlices(searchTerm, sortBy) { - return Object.values(this.props.slices) + getFilteredSortedSlices(slices, searchTerm, sortBy, showOnlyMyCharts) { + return Object.values(slices) + .filter(slice => + showOnlyMyCharts + ? (slice.owners && + slice.owners.find(owner => owner.id === this.props.userId)) || + (slice.created_by && slice.created_by.id === this.props.userId) + : true, + ) .filter(createFilter(searchTerm, KEYS_TO_FILTERS)) .sort(SliceAdder.sortByComparator(sortBy)); } - handleKeyPress(ev) { - if (ev.key === 'Enter') { - ev.preventDefault(); - - this.searchUpdated(ev.target.value); - } - } - - handleChange = _.debounce(value => { + handleChange = debounce(value => { this.searchUpdated(value); - - const { userId } = this.props; - this.slicesRequest = this.props.fetchFilteredSlices(userId, value); + this.slicesRequest = this.props.fetchSlices( + this.userIdForFetch(), + value, + this.state.sortBy, + ); }, 300); searchUpdated(searchTerm) { this.setState(prevState => ({ searchTerm, filteredSlices: this.getFilteredSortedSlices( + this.props.slices, searchTerm, prevState.sortBy, + prevState.showOnlyMyCharts, ), })); } @@ -202,13 +235,17 @@ class SliceAdder extends React.Component { this.setState(prevState => ({ sortBy, filteredSlices: this.getFilteredSortedSlices( + this.props.slices, prevState.searchTerm, sortBy, + prevState.showOnlyMyCharts, ), })); - - const { userId } = this.props; - this.slicesRequest = this.props.fetchSortedSlices(userId, sortBy); + this.slicesRequest = this.props.fetchSlices( + this.userIdForFetch(), + this.state.searchTerm, + sortBy, + ); } rowRenderer({ key, index, style }) { @@ -258,6 +295,29 @@ class SliceAdder extends React.Component { ); } + onShowOnlyMyCharts(showOnlyMyCharts) { + if (!showOnlyMyCharts) { + this.slicesRequest = this.props.fetchSlices( + undefined, + this.state.searchTerm, + this.state.sortBy, + ); + } + this.setState(prevState => ({ + showOnlyMyCharts, + filteredSlices: this.getFilteredSortedSlices( + this.props.slices, + prevState.searchTerm, + prevState.sortBy, + showOnlyMyCharts, + ), + })); + setItem( + LocalStorageKeys.dashboard__editor_show_only_my_charts, + showOnlyMyCharts, + ); + } + render() { return (
this.handleChange(ev.target.value)} - onKeyPress={this.handleKeyPress} data-test="dashboard-charts-filter-search-input" /> +
css` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: ${theme.gridUnit}px; + padding: 0 ${theme.gridUnit * 3}px ${theme.gridUnit * 4}px + ${theme.gridUnit * 3}px; + `} + > + + {t('Show only my charts')} + +
{this.props.isLoading && } {!this.props.isLoading && this.state.filteredSlices.length > 0 && ( diff --git a/superset-frontend/src/dashboard/components/SliceAdder.test.jsx b/superset-frontend/src/dashboard/components/SliceAdder.test.jsx index 72327d63c7..eda368bcaf 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.test.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.test.jsx @@ -20,24 +20,26 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import SliceAdder, { ChartList } from 'src/dashboard/components/SliceAdder'; +import SliceAdder, { + ChartList, + DEFAULT_SORT_KEY, +} from 'src/dashboard/components/SliceAdder'; import { sliceEntitiesForDashboard as mockSliceEntities } from 'spec/fixtures/mockSliceEntities'; import { styledShallow } from 'spec/helpers/theming'; +jest.mock('lodash/debounce', () => fn => { + // eslint-disable-next-line no-param-reassign + fn.throttle = jest.fn(); + return fn; +}); + describe('SliceAdder', () => { - const mockEvent = { - key: 'Enter', - target: { - value: 'mock event target', - }, - preventDefault: () => {}, - }; const props = { ...mockSliceEntities, - fetchAllSlices: () => {}, - fetchSortedSlices: () => {}, + fetchSlices: jest.fn(), + updateSlices: jest.fn(), selectedSliceIds: [127, 128], - userId: '1', + userId: 1, }; const errorProps = { ...props, @@ -84,16 +86,16 @@ describe('SliceAdder', () => { it('componentDidMount', () => { sinon.spy(SliceAdder.prototype, 'componentDidMount'); - sinon.spy(props, 'fetchAllSlices'); + sinon.spy(props, 'fetchSlices'); shallow(, { lifecycleExperimental: true, }); expect(SliceAdder.prototype.componentDidMount.calledOnce).toBe(true); - expect(props.fetchAllSlices.calledOnce).toBe(true); + expect(props.fetchSlices.calledOnce).toBe(true); SliceAdder.prototype.componentDidMount.restore(); - props.fetchAllSlices.restore(); + props.fetchSlices.restore(); }); describe('UNSAFE_componentWillReceiveProps', () => { @@ -138,32 +140,30 @@ describe('SliceAdder', () => { let wrapper; let spy; beforeEach(() => { - wrapper = shallow(); + spy = props.fetchSlices; + wrapper = shallow(); wrapper.setState({ filteredSlices: Object.values(props.slices) }); - spy = sinon.spy(wrapper.instance(), 'getFilteredSortedSlices'); }); afterEach(() => { - spy.restore(); + spy.mockReset(); }); it('searchUpdated', () => { const newSearchTerm = 'new search term'; - wrapper.instance().searchUpdated(newSearchTerm); - expect(spy.calledOnce).toBe(true); - expect(spy.lastCall.args[0]).toBe(newSearchTerm); + wrapper.instance().handleChange(newSearchTerm); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith( + props.userId, + newSearchTerm, + DEFAULT_SORT_KEY, + ); }); it('handleSelect', () => { const newSortBy = 'viz_type'; wrapper.instance().handleSelect(newSortBy); - expect(spy.calledOnce).toBe(true); - expect(spy.lastCall.args[1]).toBe(newSortBy); - }); - - it('handleKeyPress', () => { - wrapper.instance().handleKeyPress(mockEvent); - expect(spy.calledOnce).toBe(true); - expect(spy.lastCall.args[0]).toBe(mockEvent.target.value); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith(props.userId, '', newSortBy); }); }); }); diff --git a/superset-frontend/src/dashboard/containers/SliceAdder.jsx b/superset-frontend/src/dashboard/containers/SliceAdder.jsx index e3c8e1db87..992098a0da 100644 --- a/superset-frontend/src/dashboard/containers/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/containers/SliceAdder.jsx @@ -18,12 +18,7 @@ */ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; - -import { - fetchAllSlices, - fetchSortedSlices, - fetchFilteredSlices, -} from '../actions/sliceEntities'; +import { fetchSlices, updateSlices } from '../actions/sliceEntities'; import SliceAdder from '../components/SliceAdder'; function mapStateToProps( @@ -32,7 +27,7 @@ function mapStateToProps( ) { return { height: ownProps.height, - userId: dashboardInfo.userId, + userId: +dashboardInfo.userId, dashboardId: dashboardInfo.id, selectedSliceIds: dashboardState.sliceIds, slices: sliceEntities.slices, @@ -46,9 +41,8 @@ function mapStateToProps( function mapDispatchToProps(dispatch) { return bindActionCreators( { - fetchAllSlices, - fetchSortedSlices, - fetchFilteredSlices, + fetchSlices, + updateSlices, }, dispatch, ); diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.js index 70b66db724..1a065b11fa 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.js @@ -21,7 +21,8 @@ import { t } from '@superset-ui/core'; import { FETCH_ALL_SLICES_FAILED, FETCH_ALL_SLICES_STARTED, - SET_ALL_SLICES, + ADD_SLICES, + SET_SLICES, } from '../actions/sliceEntities'; import { HYDRATE_DASHBOARD } from '../actions/hydrate'; @@ -48,7 +49,7 @@ export default function sliceEntitiesReducer( isLoading: true, }; }, - [SET_ALL_SLICES]() { + [ADD_SLICES]() { return { ...state, isLoading: false, @@ -56,6 +57,13 @@ export default function sliceEntitiesReducer( lastUpdated: new Date().getTime(), }; }, + [SET_SLICES]() { + return { + isLoading: false, + slices: { ...action.payload.slices }, + lastUpdated: new Date().getTime(), + }; + }, [FETCH_ALL_SLICES_FAILED]() { return { ...state, diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.test.js b/superset-frontend/src/dashboard/reducers/sliceEntities.test.js index 73b5aebee5..444535d276 100644 --- a/superset-frontend/src/dashboard/reducers/sliceEntities.test.js +++ b/superset-frontend/src/dashboard/reducers/sliceEntities.test.js @@ -19,7 +19,7 @@ import { FETCH_ALL_SLICES_FAILED, FETCH_ALL_SLICES_STARTED, - SET_ALL_SLICES, + ADD_SLICES, } from 'src/dashboard/actions/sliceEntities'; import sliceEntitiesReducer from 'src/dashboard/reducers/sliceEntities'; @@ -41,7 +41,7 @@ describe('sliceEntities reducer', () => { it('should set slices', () => { const result = sliceEntitiesReducer( { slices: { a: {} } }, - { type: SET_ALL_SLICES, payload: { slices: { 1: {}, 2: {} } } }, + { type: ADD_SLICES, payload: { slices: { 1: {}, 2: {} } } }, ); expect(result.slices).toEqual({ diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 56bccf900e..7345f0b1af 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -19,6 +19,7 @@ import { ChartProps, DataMaskStateWithId, + DatasourceType, ExtraFormData, GenericDataType, JsonObject, @@ -193,3 +194,23 @@ export type EmbeddedDashboard = { dashboard_id: string; allowed_domains: string[]; }; + +export type Slice = { + slice_id: number; + slice_name: string; + description: string; + description_markdown: string; + form_data: any; + slice_url: string; + viz_type: string; + thumbnail_url: string; + changed_on: number; + changed_on_humanized: string; + modified: string; + datasource_id: number; + datasource_type: DatasourceType; + datasource_url: string; + datasource_name: string; + owners: { id: number }[]; + created_by: { id: number }; +}; diff --git a/superset-frontend/src/utils/localStorageHelpers.ts b/superset-frontend/src/utils/localStorageHelpers.ts index 3dceaba86e..beb5ed1014 100644 --- a/superset-frontend/src/utils/localStorageHelpers.ts +++ b/superset-frontend/src/utils/localStorageHelpers.ts @@ -54,6 +54,7 @@ export enum LocalStorageKeys { explore__data_table_original_formatted_time_columns = 'explore__data_table_original_formatted_time_columns', dashboard__custom_filter_bar_widths = 'dashboard__custom_filter_bar_widths', dashboard__explore_context = 'dashboard__explore_context', + dashboard__editor_show_only_my_charts = 'dashboard__editor_show_only_my_charts', common__resizable_sidebar_widths = 'common__resizable_sidebar_widths', } @@ -73,6 +74,7 @@ export type LocalStorageValues = { explore__data_table_original_formatted_time_columns: Record; dashboard__custom_filter_bar_widths: Record; dashboard__explore_context: Record; + dashboard__editor_show_only_my_charts: boolean; common__resizable_sidebar_widths: Record; };