feat: Shows user charts by default when editing a dashboard (#23547)

This commit is contained in:
Michael S. Molina 2023-04-06 10:10:37 -03:00 committed by GitHub
parent 30f210b842
commit bccd2670cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 365 additions and 350 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<div
@ -285,10 +345,13 @@ class SliceAdder extends React.Component {
</NewChartButtonContainer>
<Controls>
<Input
placeholder={t('Filter your charts')}
placeholder={
this.state.showOnlyMyCharts
? t('Filter your charts')
: t('Filter charts')
}
className="search-input"
onChange={ev => this.handleChange(ev.target.value)}
onKeyPress={this.handleKeyPress}
data-test="dashboard-charts-filter-search-input"
/>
<StyledSelect
@ -302,6 +365,30 @@ class SliceAdder extends React.Component {
placeholder={t('Sort by')}
/>
</Controls>
<div
css={theme => 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;
`}
>
<Checkbox
onChange={this.onShowOnlyMyCharts}
checked={this.state.showOnlyMyCharts}
/>
{t('Show only my charts')}
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
`You can choose to display all charts that you have access to or only the ones you own.
Your filter selection will be saved and remain active until you choose to change it.`,
)}
/>
</div>
{this.props.isLoading && <Loading />}
{!this.props.isLoading && this.state.filteredSlices.length > 0 && (
<ChartList>

View File

@ -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(<SliceAdder {...props} />, {
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(<SliceAdder {...props} />);
spy = props.fetchSlices;
wrapper = shallow(<SliceAdder {...props} fetchSlices={spy} />);
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);
});
});
});

View File

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

View File

@ -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,

View File

@ -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({

View File

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

View File

@ -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<string, string[]>;
dashboard__custom_filter_bar_widths: Record<string, number>;
dashboard__explore_context: Record<string, DashboardContextForExplore>;
dashboard__editor_show_only_my_charts: boolean;
common__resizable_sidebar_widths: Record<string, number>;
};