mirror of https://github.com/apache/superset.git
feat: Shows user charts by default when editing a dashboard (#23547)
This commit is contained in:
parent
30f210b842
commit
bccd2670cc
|
@ -752,7 +752,7 @@ describe('Dashboard edit', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove added charts', () => {
|
it('should remove added charts', () => {
|
||||||
dragComponent('Pivot Table');
|
dragComponent('Unicode Cloud');
|
||||||
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
|
cy.getBySel('dashboard-component-chart-holder').should('have.length', 1);
|
||||||
cy.getBySel('dashboard-delete-component-button').click();
|
cy.getBySel('dashboard-delete-component-button').click();
|
||||||
cy.getBySel('dashboard-component-chart-holder').should('have.length', 0);
|
cy.getBySel('dashboard-component-chart-holder').should('have.length', 0);
|
||||||
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -28,6 +28,11 @@ import { Select } from 'src/components';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
|
import {
|
||||||
|
LocalStorageKeys,
|
||||||
|
getItem,
|
||||||
|
setItem,
|
||||||
|
} from 'src/utils/localStorageHelpers';
|
||||||
import {
|
import {
|
||||||
CHART_TYPE,
|
CHART_TYPE,
|
||||||
NEW_COMPONENT_SOURCE_TYPE,
|
NEW_COMPONENT_SOURCE_TYPE,
|
||||||
|
@ -37,18 +42,21 @@ import {
|
||||||
NEW_COMPONENTS_SOURCE_ID,
|
NEW_COMPONENTS_SOURCE_ID,
|
||||||
} 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 _ 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 AddSliceCard from './AddSliceCard';
|
||||||
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
||||||
import DragDroppable from './dnd/DragDroppable';
|
import DragDroppable from './dnd/DragDroppable';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
fetchAllSlices: PropTypes.func.isRequired,
|
fetchSlices: PropTypes.func.isRequired,
|
||||||
|
updateSlices: PropTypes.func.isRequired,
|
||||||
isLoading: PropTypes.bool.isRequired,
|
isLoading: PropTypes.bool.isRequired,
|
||||||
slices: PropTypes.objectOf(slicePropShape).isRequired,
|
slices: PropTypes.objectOf(slicePropShape).isRequired,
|
||||||
lastUpdated: PropTypes.number.isRequired,
|
lastUpdated: PropTypes.number.isRequired,
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.number.isRequired,
|
||||||
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
||||||
editMode: PropTypes.bool,
|
editMode: PropTypes.bool,
|
||||||
dashboardId: PropTypes.number,
|
dashboardId: PropTypes.number,
|
||||||
|
@ -68,15 +76,20 @@ const KEYS_TO_SORT = {
|
||||||
changed_on: t('recent'),
|
changed_on: t('recent'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SORT_KEY = 'changed_on';
|
export const DEFAULT_SORT_KEY = 'changed_on';
|
||||||
|
|
||||||
const DEFAULT_CELL_HEIGHT = 128;
|
const DEFAULT_CELL_HEIGHT = 128;
|
||||||
|
|
||||||
const Controls = styled.div`
|
const Controls = styled.div`
|
||||||
display: flex;
|
${({ theme }) => `
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
padding: ${({ theme }) => theme.gridUnit * 3}px;
|
flex-direction: row;
|
||||||
padding-top: ${({ theme }) => theme.gridUnit * 4}px;
|
padding:
|
||||||
|
${theme.gridUnit * 4}px
|
||||||
|
${theme.gridUnit * 3}px
|
||||||
|
${theme.gridUnit * 4}px
|
||||||
|
${theme.gridUnit * 3}px;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSelect = styled(Select)`
|
const StyledSelect = styled(Select)`
|
||||||
|
@ -133,23 +146,35 @@ class SliceAdder extends React.Component {
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
sortBy: DEFAULT_SORT_KEY,
|
sortBy: DEFAULT_SORT_KEY,
|
||||||
selectedSliceIdsSet: new Set(props.selectedSliceIds),
|
selectedSliceIdsSet: new Set(props.selectedSliceIds),
|
||||||
|
showOnlyMyCharts: getItem(
|
||||||
|
LocalStorageKeys.dashboard__editor_show_only_my_charts,
|
||||||
|
true,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
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.handleSelect = this.handleSelect.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() {
|
componentDidMount() {
|
||||||
this.slicesRequest = this.props.fetchAllSlices(this.props.userId);
|
this.slicesRequest = this.props.fetchSlices(this.userIdForFetch());
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
const nextState = {};
|
const nextState = {};
|
||||||
if (nextProps.lastUpdated !== this.props.lastUpdated) {
|
if (nextProps.lastUpdated !== this.props.lastUpdated) {
|
||||||
nextState.filteredSlices = Object.values(nextProps.slices)
|
nextState.filteredSlices = this.getFilteredSortedSlices(
|
||||||
.filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
|
nextProps.slices,
|
||||||
.sort(SliceAdder.sortByComparator(this.state.sortBy));
|
this.state.searchTerm,
|
||||||
|
this.state.sortBy,
|
||||||
|
this.state.showOnlyMyCharts,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextProps.selectedSliceIds !== this.props.selectedSliceIds) {
|
if (nextProps.selectedSliceIds !== this.props.selectedSliceIds) {
|
||||||
|
@ -162,38 +187,46 @@ class SliceAdder extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
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) {
|
if (this.slicesRequest && this.slicesRequest.abort) {
|
||||||
this.slicesRequest.abort();
|
this.slicesRequest.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilteredSortedSlices(searchTerm, sortBy) {
|
getFilteredSortedSlices(slices, searchTerm, sortBy, showOnlyMyCharts) {
|
||||||
return Object.values(this.props.slices)
|
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))
|
.filter(createFilter(searchTerm, KEYS_TO_FILTERS))
|
||||||
.sort(SliceAdder.sortByComparator(sortBy));
|
.sort(SliceAdder.sortByComparator(sortBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress(ev) {
|
handleChange = debounce(value => {
|
||||||
if (ev.key === 'Enter') {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
this.searchUpdated(ev.target.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange = _.debounce(value => {
|
|
||||||
this.searchUpdated(value);
|
this.searchUpdated(value);
|
||||||
|
this.slicesRequest = this.props.fetchSlices(
|
||||||
const { userId } = this.props;
|
this.userIdForFetch(),
|
||||||
this.slicesRequest = this.props.fetchFilteredSlices(userId, value);
|
value,
|
||||||
|
this.state.sortBy,
|
||||||
|
);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
searchUpdated(searchTerm) {
|
searchUpdated(searchTerm) {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
searchTerm,
|
searchTerm,
|
||||||
filteredSlices: this.getFilteredSortedSlices(
|
filteredSlices: this.getFilteredSortedSlices(
|
||||||
|
this.props.slices,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
prevState.sortBy,
|
prevState.sortBy,
|
||||||
|
prevState.showOnlyMyCharts,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -202,13 +235,17 @@ class SliceAdder extends React.Component {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
sortBy,
|
sortBy,
|
||||||
filteredSlices: this.getFilteredSortedSlices(
|
filteredSlices: this.getFilteredSortedSlices(
|
||||||
|
this.props.slices,
|
||||||
prevState.searchTerm,
|
prevState.searchTerm,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
prevState.showOnlyMyCharts,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
this.slicesRequest = this.props.fetchSlices(
|
||||||
const { userId } = this.props;
|
this.userIdForFetch(),
|
||||||
this.slicesRequest = this.props.fetchSortedSlices(userId, sortBy);
|
this.state.searchTerm,
|
||||||
|
sortBy,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
rowRenderer({ key, index, style }) {
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -285,10 +345,13 @@ class SliceAdder extends React.Component {
|
||||||
</NewChartButtonContainer>
|
</NewChartButtonContainer>
|
||||||
<Controls>
|
<Controls>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('Filter your charts')}
|
placeholder={
|
||||||
|
this.state.showOnlyMyCharts
|
||||||
|
? t('Filter your charts')
|
||||||
|
: t('Filter charts')
|
||||||
|
}
|
||||||
className="search-input"
|
className="search-input"
|
||||||
onChange={ev => this.handleChange(ev.target.value)}
|
onChange={ev => this.handleChange(ev.target.value)}
|
||||||
onKeyPress={this.handleKeyPress}
|
|
||||||
data-test="dashboard-charts-filter-search-input"
|
data-test="dashboard-charts-filter-search-input"
|
||||||
/>
|
/>
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
|
@ -302,6 +365,30 @@ class SliceAdder extends React.Component {
|
||||||
placeholder={t('Sort by')}
|
placeholder={t('Sort by')}
|
||||||
/>
|
/>
|
||||||
</Controls>
|
</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 && <Loading />}
|
||||||
{!this.props.isLoading && this.state.filteredSlices.length > 0 && (
|
{!this.props.isLoading && this.state.filteredSlices.length > 0 && (
|
||||||
<ChartList>
|
<ChartList>
|
||||||
|
|
|
@ -20,24 +20,26 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
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 { sliceEntitiesForDashboard as mockSliceEntities } from 'spec/fixtures/mockSliceEntities';
|
||||||
import { styledShallow } from 'spec/helpers/theming';
|
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', () => {
|
describe('SliceAdder', () => {
|
||||||
const mockEvent = {
|
|
||||||
key: 'Enter',
|
|
||||||
target: {
|
|
||||||
value: 'mock event target',
|
|
||||||
},
|
|
||||||
preventDefault: () => {},
|
|
||||||
};
|
|
||||||
const props = {
|
const props = {
|
||||||
...mockSliceEntities,
|
...mockSliceEntities,
|
||||||
fetchAllSlices: () => {},
|
fetchSlices: jest.fn(),
|
||||||
fetchSortedSlices: () => {},
|
updateSlices: jest.fn(),
|
||||||
selectedSliceIds: [127, 128],
|
selectedSliceIds: [127, 128],
|
||||||
userId: '1',
|
userId: 1,
|
||||||
};
|
};
|
||||||
const errorProps = {
|
const errorProps = {
|
||||||
...props,
|
...props,
|
||||||
|
@ -84,16 +86,16 @@ describe('SliceAdder', () => {
|
||||||
|
|
||||||
it('componentDidMount', () => {
|
it('componentDidMount', () => {
|
||||||
sinon.spy(SliceAdder.prototype, 'componentDidMount');
|
sinon.spy(SliceAdder.prototype, 'componentDidMount');
|
||||||
sinon.spy(props, 'fetchAllSlices');
|
sinon.spy(props, 'fetchSlices');
|
||||||
|
|
||||||
shallow(<SliceAdder {...props} />, {
|
shallow(<SliceAdder {...props} />, {
|
||||||
lifecycleExperimental: true,
|
lifecycleExperimental: true,
|
||||||
});
|
});
|
||||||
expect(SliceAdder.prototype.componentDidMount.calledOnce).toBe(true);
|
expect(SliceAdder.prototype.componentDidMount.calledOnce).toBe(true);
|
||||||
expect(props.fetchAllSlices.calledOnce).toBe(true);
|
expect(props.fetchSlices.calledOnce).toBe(true);
|
||||||
|
|
||||||
SliceAdder.prototype.componentDidMount.restore();
|
SliceAdder.prototype.componentDidMount.restore();
|
||||||
props.fetchAllSlices.restore();
|
props.fetchSlices.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UNSAFE_componentWillReceiveProps', () => {
|
describe('UNSAFE_componentWillReceiveProps', () => {
|
||||||
|
@ -138,32 +140,30 @@ describe('SliceAdder', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let spy;
|
let spy;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<SliceAdder {...props} />);
|
spy = props.fetchSlices;
|
||||||
|
wrapper = shallow(<SliceAdder {...props} fetchSlices={spy} />);
|
||||||
wrapper.setState({ filteredSlices: Object.values(props.slices) });
|
wrapper.setState({ filteredSlices: Object.values(props.slices) });
|
||||||
spy = sinon.spy(wrapper.instance(), 'getFilteredSortedSlices');
|
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
spy.restore();
|
spy.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searchUpdated', () => {
|
it('searchUpdated', () => {
|
||||||
const newSearchTerm = 'new search term';
|
const newSearchTerm = 'new search term';
|
||||||
wrapper.instance().searchUpdated(newSearchTerm);
|
wrapper.instance().handleChange(newSearchTerm);
|
||||||
expect(spy.calledOnce).toBe(true);
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(spy.lastCall.args[0]).toBe(newSearchTerm);
|
expect(spy).toHaveBeenCalledWith(
|
||||||
|
props.userId,
|
||||||
|
newSearchTerm,
|
||||||
|
DEFAULT_SORT_KEY,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handleSelect', () => {
|
it('handleSelect', () => {
|
||||||
const newSortBy = 'viz_type';
|
const newSortBy = 'viz_type';
|
||||||
wrapper.instance().handleSelect(newSortBy);
|
wrapper.instance().handleSelect(newSortBy);
|
||||||
expect(spy.calledOnce).toBe(true);
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(spy.lastCall.args[1]).toBe(newSortBy);
|
expect(spy).toHaveBeenCalledWith(props.userId, '', newSortBy);
|
||||||
});
|
|
||||||
|
|
||||||
it('handleKeyPress', () => {
|
|
||||||
wrapper.instance().handleKeyPress(mockEvent);
|
|
||||||
expect(spy.calledOnce).toBe(true);
|
|
||||||
expect(spy.lastCall.args[0]).toBe(mockEvent.target.value);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,12 +18,7 @@
|
||||||
*/
|
*/
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchSlices, updateSlices } from '../actions/sliceEntities';
|
||||||
import {
|
|
||||||
fetchAllSlices,
|
|
||||||
fetchSortedSlices,
|
|
||||||
fetchFilteredSlices,
|
|
||||||
} from '../actions/sliceEntities';
|
|
||||||
import SliceAdder from '../components/SliceAdder';
|
import SliceAdder from '../components/SliceAdder';
|
||||||
|
|
||||||
function mapStateToProps(
|
function mapStateToProps(
|
||||||
|
@ -32,7 +27,7 @@ function mapStateToProps(
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
height: ownProps.height,
|
height: ownProps.height,
|
||||||
userId: dashboardInfo.userId,
|
userId: +dashboardInfo.userId,
|
||||||
dashboardId: dashboardInfo.id,
|
dashboardId: dashboardInfo.id,
|
||||||
selectedSliceIds: dashboardState.sliceIds,
|
selectedSliceIds: dashboardState.sliceIds,
|
||||||
slices: sliceEntities.slices,
|
slices: sliceEntities.slices,
|
||||||
|
@ -46,9 +41,8 @@ function mapStateToProps(
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(
|
return bindActionCreators(
|
||||||
{
|
{
|
||||||
fetchAllSlices,
|
fetchSlices,
|
||||||
fetchSortedSlices,
|
updateSlices,
|
||||||
fetchFilteredSlices,
|
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,8 @@ import { t } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
FETCH_ALL_SLICES_FAILED,
|
FETCH_ALL_SLICES_FAILED,
|
||||||
FETCH_ALL_SLICES_STARTED,
|
FETCH_ALL_SLICES_STARTED,
|
||||||
SET_ALL_SLICES,
|
ADD_SLICES,
|
||||||
|
SET_SLICES,
|
||||||
} from '../actions/sliceEntities';
|
} from '../actions/sliceEntities';
|
||||||
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ export default function sliceEntitiesReducer(
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[SET_ALL_SLICES]() {
|
[ADD_SLICES]() {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
@ -56,6 +57,13 @@ export default function sliceEntitiesReducer(
|
||||||
lastUpdated: new Date().getTime(),
|
lastUpdated: new Date().getTime(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[SET_SLICES]() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
slices: { ...action.payload.slices },
|
||||||
|
lastUpdated: new Date().getTime(),
|
||||||
|
};
|
||||||
|
},
|
||||||
[FETCH_ALL_SLICES_FAILED]() {
|
[FETCH_ALL_SLICES_FAILED]() {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import {
|
import {
|
||||||
FETCH_ALL_SLICES_FAILED,
|
FETCH_ALL_SLICES_FAILED,
|
||||||
FETCH_ALL_SLICES_STARTED,
|
FETCH_ALL_SLICES_STARTED,
|
||||||
SET_ALL_SLICES,
|
ADD_SLICES,
|
||||||
} from 'src/dashboard/actions/sliceEntities';
|
} from 'src/dashboard/actions/sliceEntities';
|
||||||
|
|
||||||
import sliceEntitiesReducer from 'src/dashboard/reducers/sliceEntities';
|
import sliceEntitiesReducer from 'src/dashboard/reducers/sliceEntities';
|
||||||
|
@ -41,7 +41,7 @@ describe('sliceEntities reducer', () => {
|
||||||
it('should set slices', () => {
|
it('should set slices', () => {
|
||||||
const result = sliceEntitiesReducer(
|
const result = sliceEntitiesReducer(
|
||||||
{ slices: { a: {} } },
|
{ slices: { a: {} } },
|
||||||
{ type: SET_ALL_SLICES, payload: { slices: { 1: {}, 2: {} } } },
|
{ type: ADD_SLICES, payload: { slices: { 1: {}, 2: {} } } },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.slices).toEqual({
|
expect(result.slices).toEqual({
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import {
|
import {
|
||||||
ChartProps,
|
ChartProps,
|
||||||
DataMaskStateWithId,
|
DataMaskStateWithId,
|
||||||
|
DatasourceType,
|
||||||
ExtraFormData,
|
ExtraFormData,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
|
@ -193,3 +194,23 @@ export type EmbeddedDashboard = {
|
||||||
dashboard_id: string;
|
dashboard_id: string;
|
||||||
allowed_domains: 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 };
|
||||||
|
};
|
||||||
|
|
|
@ -54,6 +54,7 @@ export enum LocalStorageKeys {
|
||||||
explore__data_table_original_formatted_time_columns = 'explore__data_table_original_formatted_time_columns',
|
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__custom_filter_bar_widths = 'dashboard__custom_filter_bar_widths',
|
||||||
dashboard__explore_context = 'dashboard__explore_context',
|
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',
|
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[]>;
|
explore__data_table_original_formatted_time_columns: Record<string, string[]>;
|
||||||
dashboard__custom_filter_bar_widths: Record<string, number>;
|
dashboard__custom_filter_bar_widths: Record<string, number>;
|
||||||
dashboard__explore_context: Record<string, DashboardContextForExplore>;
|
dashboard__explore_context: Record<string, DashboardContextForExplore>;
|
||||||
|
dashboard__editor_show_only_my_charts: boolean;
|
||||||
common__resizable_sidebar_widths: Record<string, number>;
|
common__resizable_sidebar_widths: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue