Skip to content

Commit

Permalink
feat(dashboard): Add Drill to Detail modal w/ chart menu + right-clic…
Browse files Browse the repository at this point in the history
…k support (#20728)

* Add drill-to-detail modal.

* Include additional filters from dashboard context in request.

* Set page cache size to be approximately equal to memory usage of Samples pane.

* Update getDatasourceSamples signature.

* One-line import/export.

* Fix incorrect argument order in getDatasourceSamples invocation.

* Fix height of modal.

* Disable option in chart menu unless feature flag is set.

* Open modal on right-click.

* Fix double requests on modal open, controls disappearing on filter update.

* Show formattedVal in clearable filter tag.

* Set force=false for all requests.

* Rearrange/refactor DrillDetailPane.

* Reset page index on reload.

* Fix endless re-requests on request failure.

* Fix modal layout issues.
  • Loading branch information
codyml authored Aug 22, 2022
1 parent ca98fd8 commit 52648ec
Show file tree
Hide file tree
Showing 9 changed files with 755 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,33 @@ import { PostProcessingRule } from './PostProcessing';
import { JsonObject } from '../../connection';
import { TimeGranularity } from '../../time-format';

export type QueryObjectFilterClause = {
export type BaseQueryObjectFilterClause = {
col: QueryFormColumn;
grain?: TimeGranularity;
isExtra?: boolean;
} & (
| {
op: BinaryOperator;
val: string | number | boolean | null | Date;
formattedVal?: string;
}
| {
op: SetOperator;
val: (string | number | boolean | null | Date)[];
formattedVal?: string[];
}
| {
op: UnaryOperator;
formattedVal?: string;
}
);
};

export type BinaryQueryObjectFilterClause = BaseQueryObjectFilterClause & {
op: BinaryOperator;
val: string | number | boolean | null | Date;
formattedVal?: string;
};

export type SetQueryObjectFilterClause = BaseQueryObjectFilterClause & {
op: SetOperator;
val: (string | number | boolean | null | Date)[];
formattedVal?: string[];
};

export type UnaryQueryObjectFilterClause = BaseQueryObjectFilterClause & {
op: UnaryOperator;
formattedVal?: string;
};

export type QueryObjectFilterClause =
| BinaryQueryObjectFilterClause
| SetQueryObjectFilterClause
| UnaryQueryObjectFilterClause;

export type QueryObjectExtras = Partial<{
/** HAVING condition for Druid */
Expand Down Expand Up @@ -402,4 +409,8 @@ export enum ContributionType {
Column = 'column',
}

export type DatasourceSamplesQuery = {
filters?: QueryObjectFilterClause[];
};

export default {};
26 changes: 16 additions & 10 deletions superset-frontend/src/components/Chart/ChartRenderer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { EmptyStateBig, EmptyStateSmall } from 'src/components/EmptyState';
import ChartContextMenu from './ChartContextMenu';
import DrillDetailModal from './DrillDetailModal';

const propTypes = {
annotationData: PropTypes.object,
Expand Down Expand Up @@ -83,6 +84,7 @@ class ChartRenderer extends React.Component {
super(props);
this.state = {
inContextMenu: false,
drillDetailFilters: null,
};
this.hasQueryResponseChange = false;

Expand Down Expand Up @@ -202,10 +204,7 @@ class ChartRenderer extends React.Component {
}

handleContextMenuSelected(filters) {
const extraFilters = this.props.formData.extra_form_data?.filters || [];
// eslint-disable-next-line no-alert
alert(JSON.stringify(filters.concat(extraFilters)));
this.setState({ inContextMenu: false });
this.setState({ inContextMenu: false, drillDetailFilters: filters });
}

handleContextMenuClosed() {
Expand Down Expand Up @@ -289,12 +288,19 @@ class ChartRenderer extends React.Component {
return (
<div>
{this.props.source === 'dashboard' && (
<ChartContextMenu
ref={this.contextMenuRef}
id={chartId}
onSelection={this.handleContextMenuSelected}
onClose={this.handleContextMenuClosed}
/>
<>
<ChartContextMenu
ref={this.contextMenuRef}
id={chartId}
onSelection={this.handleContextMenuSelected}
onClose={this.handleContextMenuClosed}
/>
<DrillDetailModal
chartId={chartId}
initialFilters={this.state.drillDetailFilters}
formData={currentFormData}
/>
</>
)}
<SuperChart
disableErrorBoundary
Expand Down
117 changes: 117 additions & 0 deletions superset-frontend/src/components/Chart/DrillDetailModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* 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 React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import {
BinaryQueryObjectFilterClause,
css,
QueryFormData,
t,
useTheme,
} from '@superset-ui/core';
import DrillDetailPane from 'src/dashboard/components/DrillDetailPane';
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
import { Slice } from 'src/types/Chart';
import Modal from '../Modal';
import Button from '../Button';

const DrillDetailModal: React.FC<{
chartId: number;
initialFilters?: BinaryQueryObjectFilterClause[];
formData: QueryFormData;
}> = ({ chartId, initialFilters, formData }) => {
const [showModal, setShowModal] = useState(false);
const openModal = useCallback(() => setShowModal(true), []);
const closeModal = useCallback(() => setShowModal(false), []);
const history = useHistory();
const theme = useTheme();
const dashboardPageId = useContext(DashboardPageIdContext);
const { slice_name: chartName } = useSelector(
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
state.sliceEntities.slices[chartId],
);

const exploreUrl = useMemo(
() => `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${chartId}`,
[chartId, dashboardPageId],
);

const exploreChart = useCallback(() => {
history.push(exploreUrl);
}, [exploreUrl, history]);

// Trigger modal open when initial filters change
useEffect(() => {
if (initialFilters) {
openModal();
}
}, [initialFilters, openModal]);

return (
<Modal
css={css`
.ant-modal-body {
display: flex;
flex-direction: column;
}
`}
show={showModal}
onHide={closeModal}
title={t('Drill to detail: %s', chartName)}
footer={
<>
<Button
buttonStyle="secondary"
buttonSize="small"
onClick={exploreChart}
>
{t('Edit chart')}
</Button>
<Button buttonStyle="primary" buttonSize="small" onClick={closeModal}>
{t('Close')}
</Button>
</>
}
responsive
resizable
resizableConfig={{
minHeight: theme.gridUnit * 128,
minWidth: theme.gridUnit * 128,
defaultSize: {
width: 'auto',
height: '75vh',
},
}}
draggable
destroyOnClose
>
<DrillDetailPane formData={formData} initialFilters={initialFilters} />
</Modal>
);
};

export default DrillDetailModal;
23 changes: 20 additions & 3 deletions superset-frontend/src/components/Chart/chartAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/* eslint no-undef: 'error' */
/* eslint no-param-reassign: ["error", { "props": false }] */
import moment from 'moment';
import { t, SupersetClient } from '@superset-ui/core';
import { t, SupersetClient, isDefined } from '@superset-ui/core';
import { getControlsState } from 'src/explore/store';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import {
Expand Down Expand Up @@ -603,10 +603,27 @@ export const getDatasourceSamples = async (
datasourceId,
force,
jsonPayload,
perPage,
page,
) => {
const endpoint = `/datasource/samples?force=${force}&datasource_type=${datasourceType}&datasource_id=${datasourceId}`;
try {
const response = await SupersetClient.post({ endpoint, jsonPayload });
const searchParams = {
force,
datasource_type: datasourceType,
datasource_id: datasourceId,
};

if (isDefined(perPage) && isDefined(page)) {
searchParams.per_page = perPage;
searchParams.page = page;
}

const response = await SupersetClient.post({
endpoint: '/datasource/samples',
jsonPayload,
searchParams,
});

return response.json.result;
} catch (err) {
const clientError = await getClientErrorObject(err);
Expand Down
Loading

0 comments on commit 52648ec

Please sign in to comment.