diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index a3cfa886181d6..94c48d432f129 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -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 */
@@ -402,4 +409,8 @@ export enum ContributionType {
Column = 'column',
}
+export type DatasourceSamplesQuery = {
+ filters?: QueryObjectFilterClause[];
+};
+
export default {};
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx
index 4c11cfc085d38..d1584441e35f8 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.jsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx
@@ -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,
@@ -83,6 +84,7 @@ class ChartRenderer extends React.Component {
super(props);
this.state = {
inContextMenu: false,
+ drillDetailFilters: null,
};
this.hasQueryResponseChange = false;
@@ -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() {
@@ -289,12 +288,19 @@ class ChartRenderer extends React.Component {
return (
{this.props.source === 'dashboard' && (
-
+ <>
+
+
+ >
)}
= ({ 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 } }) =>
+ 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 (
+
+
+
+ >
+ }
+ responsive
+ resizable
+ resizableConfig={{
+ minHeight: theme.gridUnit * 128,
+ minWidth: theme.gridUnit * 128,
+ defaultSize: {
+ width: 'auto',
+ height: '75vh',
+ },
+ }}
+ draggable
+ destroyOnClose
+ >
+
+
+ );
+};
+
+export default DrillDetailModal;
diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js
index 044593eb37461..dea41497b82c6 100644
--- a/superset-frontend/src/components/Chart/chartAction.js
+++ b/superset-frontend/src/components/Chart/chartAction.js
@@ -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 {
@@ -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);
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
new file mode 100644
index 0000000000000..bf3f1985b7f15
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
@@ -0,0 +1,257 @@
+/**
+ * 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, {
+ useState,
+ useEffect,
+ useMemo,
+ useCallback,
+ useRef,
+} from 'react';
+import { useSelector } from 'react-redux';
+import {
+ BinaryQueryObjectFilterClause,
+ css,
+ ensureIsArray,
+ GenericDataType,
+ t,
+ useTheme,
+ QueryFormData,
+ JsonObject,
+} from '@superset-ui/core';
+import Loading from 'src/components/Loading';
+import { EmptyStateMedium } from 'src/components/EmptyState';
+import TableView, { EmptyWrapperType } from 'src/components/TableView';
+import { useTableColumns } from 'src/explore/components/DataTableControl';
+import { getDatasourceSamples } from 'src/components/Chart/chartAction';
+import TableControls from './TableControls';
+import { getDrillPayload } from './utils';
+
+type ResultsPage = {
+ total: number;
+ data: Record[];
+ colNames: string[];
+ colTypes: GenericDataType[];
+};
+
+const PAGE_SIZE = 50;
+
+export default function DrillDetailPane({
+ formData,
+ initialFilters,
+}: {
+ formData: QueryFormData;
+ initialFilters?: BinaryQueryObjectFilterClause[];
+}) {
+ const theme = useTheme();
+ const [pageIndex, setPageIndex] = useState(0);
+ const lastPageIndex = useRef(pageIndex);
+ const [filters, setFilters] = useState(initialFilters || []);
+ const [isLoading, setIsLoading] = useState(false);
+ const [responseError, setResponseError] = useState('');
+ const [resultsPages, setResultsPages] = useState