From 681916d7f3c1c853ee3544d99458667eb9eb918f Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Wed, 10 Aug 2022 14:40:30 -0600 Subject: [PATCH 01/16] Add drill-to-detail modal. --- .../superset-ui-core/src/query/types/Query.ts | 45 ++-- .../src/components/Chart/chartAction.js | 7 +- .../DrillDetailPane/DrillDetailPane.tsx | 239 ++++++++++++++++++ .../DrillDetailPane/TableControls.tsx | 134 ++++++++++ .../components/DrillDetailPane/index.ts | 22 ++ .../components/SliceHeaderControls/index.tsx | 111 +++++++- 6 files changed, 526 insertions(+), 32 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx create mode 100644 superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx create mode 100644 superset-frontend/src/dashboard/components/DrillDetailPane/index.ts 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/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js index 044593eb37461..f45c5f42ccb07 100644 --- a/superset-frontend/src/components/Chart/chartAction.js +++ b/superset-frontend/src/components/Chart/chartAction.js @@ -603,8 +603,13 @@ export const getDatasourceSamples = async ( datasourceId, force, jsonPayload, + pagination, ) => { - const endpoint = `/datasource/samples?force=${force}&datasource_type=${datasourceType}&datasource_id=${datasourceId}`; + let endpoint = `/datasource/samples?force=${force}&datasource_type=${datasourceType}&datasource_id=${datasourceId}`; + if (pagination) { + endpoint += `&page=${pagination.page}&per_page=${pagination.perPage}`; + } + try { const response = await SupersetClient.post({ endpoint, jsonPayload }); return response.json.result; 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..f6b50f9d17d2f --- /dev/null +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -0,0 +1,239 @@ +/** + * 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 { + BinaryQueryObjectFilterClause, + css, + ensureIsArray, + GenericDataType, + t, + useTheme, +} 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'; + +type ResultsPage = { + total: number; + data: Record[]; + colNames: string[]; + colTypes: GenericDataType[]; +}; + +const PAGE_SIZE = 50; +const MAX_CACHED_PAGES = 5; + +export default function DrillDetailPane({ + datasource, + initialFilters, +}: { + datasource: string; + 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>( + new Map(), + ); + + // Get string identifier for dataset + const [datasourceId, datasourceType] = useMemo( + () => datasource.split('__'), + [datasource], + ); + + // Get page of results + const resultsPage = useMemo(() => { + const nextResultsPage = resultsPages.get(pageIndex); + if (nextResultsPage) { + lastPageIndex.current = pageIndex; + return nextResultsPage; + } + + return resultsPages.get(lastPageIndex.current); + }, [pageIndex, resultsPages]); + + // Clear cache and reset page index if filters change + useEffect(() => { + setResultsPages(new Map()); + setPageIndex(0); + }, [filters]); + + // Update cache order if page in cache + useEffect(() => { + if ( + resultsPages.has(pageIndex) && + [...resultsPages.keys()].at(-1) !== pageIndex + ) { + const nextResultsPages = new Map(resultsPages); + nextResultsPages.delete(pageIndex); + setResultsPages( + nextResultsPages.set( + pageIndex, + resultsPages.get(pageIndex) as ResultsPage, + ), + ); + } + }, [pageIndex, resultsPages]); + + // Download page of results & trim cache if page not in cache + useEffect(() => { + if (!resultsPages.has(pageIndex)) { + setIsLoading(true); + getDatasourceSamples( + datasourceType, + datasourceId, + true, + filters.length ? { filters } : null, + { page: pageIndex + 1, perPage: PAGE_SIZE }, + ) + .then(response => { + setResultsPages( + new Map([ + ...[...resultsPages.entries()].slice(-MAX_CACHED_PAGES + 1), + [ + pageIndex, + { + total: response.total_count, + data: response.data, + colNames: ensureIsArray(response.colnames), + colTypes: ensureIsArray(response.coltypes), + }, + ], + ]), + ); + setResponseError(''); + }) + .catch(error => { + setResponseError(`${error.name}: ${error.message}`); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [datasourceId, datasourceType, filters, pageIndex, resultsPages]); + + // this is to preserve the order of the columns, even if there are integer values, + // while also only grabbing the first column's keys + const columns = useTableColumns( + resultsPage?.colNames, + resultsPage?.colTypes, + resultsPage?.data, + datasource, + ); + + const sortDisabledColumns = columns.map(column => ({ + ...column, + disableSortBy: true, + })); + + // Update page index on pagination click + const onServerPagination = useCallback(({ pageIndex }) => { + setPageIndex(pageIndex); + }, []); + + // Clear cache on reload button click + const handleReload = useCallback(() => { + setResultsPages(new Map()); + }, []); + + // Render error if page download failed + if (responseError) { + return ( +
+        {responseError}
+      
+ ); + } + + // Render loading if first page hasn't loaded + if (!resultsPages.size) { + return ( +
+ +
+ ); + } + + // Render empty state if no results are returned for page + if (resultsPage?.total === 0) { + const title = t('No rows were returned for this dataset'); + return ; + } + + // Render chart if at least one page has successfully loaded + return ( +
+ + +
+ ); +} diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx new file mode 100644 index 0000000000000..0be301de46c64 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx @@ -0,0 +1,134 @@ +/** + * 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, useMemo } from 'react'; +import { Tag } from 'antd'; +import { + BinaryQueryObjectFilterClause, + css, + isAdhocColumn, + t, + useTheme, +} from '@superset-ui/core'; +import RowCountLabel from 'src/explore/components/RowCountLabel'; +import Icons from 'src/components/Icons'; + +export default function TableControls({ + filters, + setFilters, + totalCount, + onReload, +}: { + filters: BinaryQueryObjectFilterClause[]; + setFilters: (filters: BinaryQueryObjectFilterClause[]) => void; + totalCount?: number; + loading?: boolean; + onReload: () => void; +}) { + const theme = useTheme(); + const filterMap: Record = useMemo( + () => + Object.assign( + {}, + ...filters.map(filter => ({ + [isAdhocColumn(filter.col) + ? (filter.col.label as string) + : filter.col]: filter, + })), + ), + [filters], + ); + + const removeFilter = useCallback( + colName => { + const updatedFilterMap = { ...filterMap }; + delete updatedFilterMap[colName]; + setFilters([...Object.values(updatedFilterMap)]); + }, + [filterMap, setFilters], + ); + + const filterTags = useMemo( + () => + Object.entries(filterMap) + .map(([colName, { val }]) => ({ colName, val })) + .sort((a, b) => a.colName.localeCompare(b.colName)), + [filterMap], + ); + + return ( +
+
+ {filterTags.map(({ colName, val }) => ( + + + {colName} + + {val} + + ))} +
+
+ + +
+
+ ); +} diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts b/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts new file mode 100644 index 0000000000000..aeaf795770ec3 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts @@ -0,0 +1,22 @@ +/** + * 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 DrillDetailPane from './DrillDetailPane'; + +export default DrillDetailPane; diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 8673a03848518..bb41c82d3ea71 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -16,8 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import React, { MouseEvent, Key } from 'react'; -import { Link, RouteComponentProps, withRouter } from 'react-router-dom'; +import React, { + MouseEvent, + Key, + ReactChild, + useState, + useCallback, +} from 'react'; +import { + Link, + RouteComponentProps, + useHistory, + withRouter, +} from 'react-router-dom'; import moment from 'moment'; import { Behavior, @@ -40,6 +51,8 @@ import ModalTrigger from 'src/components/ModalTrigger'; import Button from 'src/components/Button'; import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal'; import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane'; +import Modal from 'src/components/Modal'; +import DrillDetailPane from 'src/dashboard/components/DrillDetailPane'; const MENU_KEYS = { CROSS_FILTER_SCOPING: 'cross_filter_scoping', @@ -52,6 +65,7 @@ const MENU_KEYS = { TOGGLE_CHART_DESCRIPTION: 'toggle_chart_description', VIEW_QUERY: 'view_query', VIEW_RESULTS: 'view_results', + DRILL_TO_DETAIL: 'drill_to_detail', }; const VerticalDotsContainer = styled.div` @@ -97,6 +111,7 @@ export interface SliceHeaderControlsProps { slice_id: number; slice_description: string; form_data?: { emit_filter?: boolean }; + datasource: string; }; componentId: string; @@ -140,6 +155,68 @@ const dropdownIconsStyles = css` } `; +const DashboardChartModalTrigger = ({ + exploreUrl, + triggerNode, + modalTitle, + modalBody, +}: { + exploreUrl: string; + triggerNode: ReactChild; + modalTitle: ReactChild; + modalBody: ReactChild; +}) => { + const [showModal, setShowModal] = useState(false); + const openModal = useCallback(() => setShowModal(true), []); + const closeModal = useCallback(() => setShowModal(false), []); + const history = useHistory(); + const exploreChart = () => history.push(exploreUrl); + + return ( + <> + + {triggerNode} + + {(() => ( + + + + + } + responsive + resizable + draggable + destroyOnClose + > + {modalBody} + + ))()} + + ); +}; + class SliceHeaderControls extends React.PureComponent< SliceHeaderControlsPropsWithRouter, State @@ -339,7 +416,8 @@ class SliceHeaderControls extends React.PureComponent< {this.props.supersetCanExplore && ( - {t('View as table')} @@ -355,18 +433,23 @@ class SliceHeaderControls extends React.PureComponent< isVisible /> } - modalFooter={ - + /> + + )} + + {this.props.supersetCanExplore && ( + + + {t('Drill to detail')} + + } + modalTitle={t('Drill to detail: %s', slice.slice_name)} + modalBody={ + } - draggable - resizable - responsive /> )} From f3e115912c3a982934386362dd88c5f11768d788 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Thu, 4 Aug 2022 16:52:10 -0400 Subject: [PATCH 02/16] Include additional filters from dashboard context in request. --- .../DrillDetailPane/DrillDetailPane.tsx | 33 ++++++++----- .../components/DrillDetailPane/utils.ts | 46 +++++++++++++++++++ .../components/SliceHeaderControls/index.tsx | 5 +- 3 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index f6b50f9d17d2f..4fb13f9c7f28f 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import React, { useState, useEffect, @@ -31,6 +30,7 @@ import { GenericDataType, t, useTheme, + QueryFormData, } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { EmptyStateMedium } from 'src/components/EmptyState'; @@ -38,6 +38,7 @@ 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; @@ -51,15 +52,17 @@ const MAX_CACHED_PAGES = 5; export default function DrillDetailPane({ datasource, - initialFilters, + queryFormData, + drillFilters, }: { datasource: string; - initialFilters?: BinaryQueryObjectFilterClause[]; + queryFormData?: QueryFormData; + drillFilters?: BinaryQueryObjectFilterClause[]; }) { const theme = useTheme(); const [pageIndex, setPageIndex] = useState(0); const lastPageIndex = useRef(pageIndex); - const [filters, setFilters] = useState(initialFilters || []); + const [filters, setFilters] = useState(drillFilters || []); const [isLoading, setIsLoading] = useState(false); const [responseError, setResponseError] = useState(''); const [resultsPages, setResultsPages] = useState>( @@ -110,13 +113,11 @@ export default function DrillDetailPane({ useEffect(() => { if (!resultsPages.has(pageIndex)) { setIsLoading(true); - getDatasourceSamples( - datasourceType, - datasourceId, - true, - filters.length ? { filters } : null, - { page: pageIndex + 1, perPage: PAGE_SIZE }, - ) + const jsonPayload = getDrillPayload(queryFormData, drillFilters); + getDatasourceSamples(datasourceType, datasourceId, true, jsonPayload, { + page: pageIndex + 1, + perPage: PAGE_SIZE, + }) .then(response => { setResultsPages( new Map([ @@ -141,7 +142,15 @@ export default function DrillDetailPane({ setIsLoading(false); }); } - }, [datasourceId, datasourceType, filters, pageIndex, resultsPages]); + }, [ + datasourceId, + datasourceType, + drillFilters, + filters, + pageIndex, + queryFormData, + resultsPages, + ]); // this is to preserve the order of the columns, even if there are integer values, // while also only grabbing the first column's keys diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts b/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts new file mode 100644 index 0000000000000..e59af45b0de06 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts @@ -0,0 +1,46 @@ +/** + * 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 { omit } from 'lodash'; +import { + ensureIsArray, + QueryFormData, + BinaryQueryObjectFilterClause, + buildQueryObject, +} from '@superset-ui/core'; + +export function getDrillPayload( + queryFormData?: QueryFormData, + drillFilters?: BinaryQueryObjectFilterClause[], +) { + if (!queryFormData) { + return undefined; + } + const queryObject = buildQueryObject(queryFormData); + const extras = omit(queryObject.extras, 'having'); + const filters = [ + ...ensureIsArray(queryObject.filters), + ...ensureIsArray(drillFilters), + ]; + return { + granularity: queryObject.granularity, + time_range: queryObject.time_range, + filters, + extras, + }; +} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index bb41c82d3ea71..77d80199c169e 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -448,7 +448,10 @@ class SliceHeaderControls extends React.PureComponent< } modalTitle={t('Drill to detail: %s', slice.slice_name)} modalBody={ - + } /> From b8444b36e5298336387c8b16f8a243eb28c7a255 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 5 Aug 2022 16:42:10 -0400 Subject: [PATCH 03/16] Set page cache size to be approximately equal to memory usage of Samples pane. --- .../DrillDetailPane/DrillDetailPane.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index 4fb13f9c7f28f..a09494d98a379 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -23,6 +23,7 @@ import React, { useCallback, useRef, } from 'react'; +import { useSelector } from 'react-redux'; import { BinaryQueryObjectFilterClause, css, @@ -31,6 +32,7 @@ import { t, useTheme, QueryFormData, + JsonObject, } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { EmptyStateMedium } from 'src/components/EmptyState'; @@ -48,7 +50,6 @@ type ResultsPage = { }; const PAGE_SIZE = 50; -const MAX_CACHED_PAGES = 5; export default function DrillDetailPane({ datasource, @@ -69,7 +70,12 @@ export default function DrillDetailPane({ new Map(), ); - // Get string identifier for dataset + const SAMPLES_ROW_LIMIT = useSelector( + (state: { common: { conf: JsonObject } }) => + state.common.conf.SAMPLES_ROW_LIMIT, + ); + + // Extract datasource ID/type from string ID const [datasourceId, datasourceType] = useMemo( () => datasource.split('__'), [datasource], @@ -110,6 +116,7 @@ export default function DrillDetailPane({ }, [pageIndex, resultsPages]); // Download page of results & trim cache if page not in cache + const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / PAGE_SIZE); useEffect(() => { if (!resultsPages.has(pageIndex)) { setIsLoading(true); @@ -121,7 +128,7 @@ export default function DrillDetailPane({ .then(response => { setResultsPages( new Map([ - ...[...resultsPages.entries()].slice(-MAX_CACHED_PAGES + 1), + ...[...resultsPages.entries()].slice(-cachePageLimit + 1), [ pageIndex, { @@ -143,10 +150,10 @@ export default function DrillDetailPane({ }); } }, [ + cachePageLimit, datasourceId, datasourceType, drillFilters, - filters, pageIndex, queryFormData, resultsPages, From 802851b0627387bc4dac9cad8ea1ac1857f54903 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 5 Aug 2022 17:13:38 -0400 Subject: [PATCH 04/16] Update getDatasourceSamples signature. --- .../src/components/Chart/chartAction.js | 28 +++++++++++++------ .../DrillDetailPane/DrillDetailPane.tsx | 12 +++++--- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js index f45c5f42ccb07..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,15 +603,27 @@ export const getDatasourceSamples = async ( datasourceId, force, jsonPayload, - pagination, + perPage, + page, ) => { - let endpoint = `/datasource/samples?force=${force}&datasource_type=${datasourceType}&datasource_id=${datasourceId}`; - if (pagination) { - endpoint += `&page=${pagination.page}&per_page=${pagination.perPage}`; - } - 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 index a09494d98a379..94f0ee0749fdb 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -121,10 +121,14 @@ export default function DrillDetailPane({ if (!resultsPages.has(pageIndex)) { setIsLoading(true); const jsonPayload = getDrillPayload(queryFormData, drillFilters); - getDatasourceSamples(datasourceType, datasourceId, true, jsonPayload, { - page: pageIndex + 1, - perPage: PAGE_SIZE, - }) + getDatasourceSamples( + datasourceType, + datasourceId, + true, + jsonPayload, + pageIndex + 1, + PAGE_SIZE, + ) .then(response => { setResultsPages( new Map([ From 25468419cfa3c6a0febcd1bf8a8f396f715c9ee0 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 5 Aug 2022 17:30:48 -0400 Subject: [PATCH 05/16] One-line import/export. --- .../src/dashboard/components/DrillDetailPane/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts b/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts index aeaf795770ec3..7e23e0a55c085 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts @@ -17,6 +17,4 @@ * under the License. */ -import DrillDetailPane from './DrillDetailPane'; - -export default DrillDetailPane; +export { default } from './DrillDetailPane'; From 314fa5141b0f1b79c7e97fcef7f6c2d411d66219 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 5 Aug 2022 17:42:19 -0400 Subject: [PATCH 06/16] Fix incorrect argument order in getDatasourceSamples invocation. --- .../dashboard/components/DrillDetailPane/DrillDetailPane.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index 94f0ee0749fdb..915b636412dab 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -126,8 +126,8 @@ export default function DrillDetailPane({ datasourceId, true, jsonPayload, - pageIndex + 1, PAGE_SIZE, + pageIndex + 1, ) .then(response => { setResultsPages( From cdae1b39710eea5743bda001381e1b3e6f77c92b Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 5 Aug 2022 17:56:48 -0400 Subject: [PATCH 07/16] Fix height of modal. --- .../DrillDetailPane/DrillDetailPane.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index 915b636412dab..aeb330cb16019 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -190,13 +190,19 @@ export default function DrillDetailPane({ // Render error if page download failed if (responseError) { return ( -
-        {responseError}
-      
+
+          {responseError}
+        
+ ); } @@ -205,7 +211,7 @@ export default function DrillDetailPane({ return (
@@ -216,7 +222,15 @@ export default function DrillDetailPane({ // Render empty state if no results are returned for page if (resultsPage?.total === 0) { const title = t('No rows were returned for this dataset'); - return ; + return ( +
+ +
+ ); } // Render chart if at least one page has successfully loaded @@ -225,6 +239,7 @@ export default function DrillDetailPane({ css={css` display: flex; flex-direction: column; + height: ${theme.gridUnit * 128}px; `} >
From cb5c51f3f811e1dd1e5379953fd519f777352553 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 5 Aug 2022 19:35:05 -0400 Subject: [PATCH 08/16] Disable option in chart menu unless feature flag is set. --- .../components/SliceHeaderControls/index.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 77d80199c169e..fa246a8429b8d 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -437,25 +437,26 @@ class SliceHeaderControls extends React.PureComponent< )} - {this.props.supersetCanExplore && ( - - - {t('Drill to detail')} - - } - modalTitle={t('Drill to detail: %s', slice.slice_name)} - modalBody={ - - } - /> - - )} + {isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) && + this.props.supersetCanExplore && ( + + + {t('Drill to detail')} + + } + modalTitle={t('Drill to detail: %s', slice.slice_name)} + modalBody={ + + } + /> + + )} {(slice.description || this.props.supersetCanExplore) && ( From 0229a142322b7c0f8d8ca6a01ede0f388f51bea3 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Tue, 9 Aug 2022 21:19:03 -0600 Subject: [PATCH 09/16] Open modal on right-click. --- .../src/components/Chart/ChartRenderer.jsx | 26 +++-- .../src/components/Chart/DrillDetailModal.tsx | 100 ++++++++++++++++++ .../DrillDetailPane/DrillDetailPane.tsx | 24 ++--- .../components/DrillDetailPane/utils.ts | 2 +- .../components/SliceHeaderControls/index.tsx | 7 +- 5 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 superset-frontend/src/components/Chart/DrillDetailModal.tsx 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 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 + draggable + destroyOnClose + > + + + ); +}; + +export default DrillDetailModal; diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index aeb330cb16019..070c997efa309 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -52,18 +52,16 @@ type ResultsPage = { const PAGE_SIZE = 50; export default function DrillDetailPane({ - datasource, - queryFormData, - drillFilters, + formData, + initialFilters, }: { - datasource: string; - queryFormData?: QueryFormData; - drillFilters?: BinaryQueryObjectFilterClause[]; + formData: QueryFormData; + initialFilters?: BinaryQueryObjectFilterClause[]; }) { const theme = useTheme(); const [pageIndex, setPageIndex] = useState(0); const lastPageIndex = useRef(pageIndex); - const [filters, setFilters] = useState(drillFilters || []); + const [filters, setFilters] = useState(initialFilters || []); const [isLoading, setIsLoading] = useState(false); const [responseError, setResponseError] = useState(''); const [resultsPages, setResultsPages] = useState>( @@ -77,8 +75,8 @@ export default function DrillDetailPane({ // Extract datasource ID/type from string ID const [datasourceId, datasourceType] = useMemo( - () => datasource.split('__'), - [datasource], + () => formData.datasource.split('__'), + [formData.datasource], ); // Get page of results @@ -120,7 +118,7 @@ export default function DrillDetailPane({ useEffect(() => { if (!resultsPages.has(pageIndex)) { setIsLoading(true); - const jsonPayload = getDrillPayload(queryFormData, drillFilters); + const jsonPayload = getDrillPayload(formData, filters); getDatasourceSamples( datasourceType, datasourceId, @@ -157,9 +155,9 @@ export default function DrillDetailPane({ cachePageLimit, datasourceId, datasourceType, - drillFilters, + filters, + formData, pageIndex, - queryFormData, resultsPages, ]); @@ -169,7 +167,7 @@ export default function DrillDetailPane({ resultsPage?.colNames, resultsPage?.colTypes, resultsPage?.data, - datasource, + formData.datasource, ); const sortDisabledColumns = columns.map(column => ({ diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts b/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts index e59af45b0de06..03494024a9855 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts @@ -35,7 +35,7 @@ export function getDrillPayload( const extras = omit(queryObject.extras, 'having'); const filters = [ ...ensureIsArray(queryObject.filters), - ...ensureIsArray(drillFilters), + ...ensureIsArray(drillFilters).map(f => omit(f, 'formattedVal')), ]; return { granularity: queryObject.granularity, diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index fa246a8429b8d..abb59ce359757 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -448,12 +448,7 @@ class SliceHeaderControls extends React.PureComponent< } modalTitle={t('Drill to detail: %s', slice.slice_name)} - modalBody={ - - } + modalBody={} /> )} From dcd69a464bbfdccd960ba8bc1666fbf3d60859f1 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Wed, 10 Aug 2022 14:01:07 -0600 Subject: [PATCH 10/16] Fix double requests on modal open, controls disappearing on filter update. --- .../DrillDetailPane/DrillDetailPane.tsx | 64 ++++++++++--------- .../DrillDetailPane/TableControls.tsx | 5 +- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index 070c997efa309..b92b87a170ae2 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -116,7 +116,7 @@ export default function DrillDetailPane({ // Download page of results & trim cache if page not in cache const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / PAGE_SIZE); useEffect(() => { - if (!resultsPages.has(pageIndex)) { + if (!isLoading && !resultsPages.has(pageIndex)) { setIsLoading(true); const jsonPayload = getDrillPayload(formData, filters); getDatasourceSamples( @@ -157,6 +157,7 @@ export default function DrillDetailPane({ datasourceType, filters, formData, + isLoading, pageIndex, resultsPages, ]); @@ -185,9 +186,11 @@ export default function DrillDetailPane({ setResultsPages(new Map()); }, []); - // Render error if page download failed + let tableContent = null; + if (responseError) { - return ( + // Render error if page download failed + tableContent = (
); - } - - // Render loading if first page hasn't loaded - if (!resultsPages.size) { - return ( + } else if (!resultsPages.size) { + // Render loading if first page hasn't loaded + tableContent = (
); - } - - // Render empty state if no results are returned for page - if (resultsPage?.total === 0) { + } else if (resultsPage?.total === 0) { + // Render empty state if no results are returned for page const title = t('No rows were returned for this dataset'); - return ( + tableContent = (
); - } - - // Render chart if at least one page has successfully loaded - return ( -
- + } else { + // Render table if at least one page has successfully loaded + tableContent = ( + ); + } + + return ( +
+ + {tableContent}
); } diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx index 0be301de46c64..96badcc1578a3 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx @@ -33,12 +33,13 @@ export default function TableControls({ filters, setFilters, totalCount, + loading, onReload, }: { filters: BinaryQueryObjectFilterClause[]; setFilters: (filters: BinaryQueryObjectFilterClause[]) => void; totalCount?: number; - loading?: boolean; + loading: boolean; onReload: () => void; }) { const theme = useTheme(); @@ -120,7 +121,7 @@ export default function TableControls({ height: min-content; `} > - + Date: Thu, 11 Aug 2022 14:39:21 -0600 Subject: [PATCH 11/16] Show formattedVal in clearable filter tag. --- .../dashboard/components/DrillDetailPane/TableControls.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx index 96badcc1578a3..9b0e072379085 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx @@ -68,7 +68,10 @@ export default function TableControls({ const filterTags = useMemo( () => Object.entries(filterMap) - .map(([colName, { val }]) => ({ colName, val })) + .map(([colName, { val, formattedVal }]) => ({ + colName, + val: formattedVal ?? val, + })) .sort((a, b) => a.colName.localeCompare(b.colName)), [filterMap], ); From 9546d12c56f2a386876fef46cd68dd9107f4cc0a Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Thu, 11 Aug 2022 14:41:49 -0600 Subject: [PATCH 12/16] Set force=false for all requests. --- .../dashboard/components/DrillDetailPane/DrillDetailPane.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index b92b87a170ae2..03a3f99f6845f 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -122,7 +122,7 @@ export default function DrillDetailPane({ getDatasourceSamples( datasourceType, datasourceId, - true, + false, jsonPayload, PAGE_SIZE, pageIndex + 1, From d9a6b9104c1c53d7138449398b8ff9ce80ea3bfd Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Thu, 11 Aug 2022 15:00:51 -0600 Subject: [PATCH 13/16] Rearrange/refactor DrillDetailPane. --- .../DrillDetailPane/DrillDetailPane.tsx | 92 ++++++++----------- 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index 03a3f99f6845f..f8ee5f0a9dac6 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -90,6 +90,35 @@ export default function DrillDetailPane({ return resultsPages.get(lastPageIndex.current); }, [pageIndex, resultsPages]); + // this is to preserve the order of the columns, even if there are integer values, + // while also only grabbing the first column's keys + const columns = useTableColumns( + resultsPage?.colNames, + resultsPage?.colTypes, + resultsPage?.data, + formData.datasource, + ); + + // Disable sorting on columns + const sortDisabledColumns = useMemo( + () => + columns.map(column => ({ + ...column, + disableSortBy: true, + })), + [columns], + ); + + // Update page index on pagination click + const onServerPagination = useCallback(({ pageIndex }) => { + setPageIndex(pageIndex); + }, []); + + // Clear cache on reload button click + const handleReload = useCallback(() => { + setResultsPages(new Map()); + }, []); + // Clear cache and reset page index if filters change useEffect(() => { setResultsPages(new Map()); @@ -114,11 +143,11 @@ export default function DrillDetailPane({ }, [pageIndex, resultsPages]); // Download page of results & trim cache if page not in cache - const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / PAGE_SIZE); useEffect(() => { if (!isLoading && !resultsPages.has(pageIndex)) { setIsLoading(true); const jsonPayload = getDrillPayload(formData, filters); + const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / PAGE_SIZE); getDatasourceSamples( datasourceType, datasourceId, @@ -152,7 +181,7 @@ export default function DrillDetailPane({ }); } }, [ - cachePageLimit, + SAMPLES_ROW_LIMIT, datasourceId, datasourceType, filters, @@ -162,72 +191,25 @@ export default function DrillDetailPane({ resultsPages, ]); - // this is to preserve the order of the columns, even if there are integer values, - // while also only grabbing the first column's keys - const columns = useTableColumns( - resultsPage?.colNames, - resultsPage?.colTypes, - resultsPage?.data, - formData.datasource, - ); - - const sortDisabledColumns = columns.map(column => ({ - ...column, - disableSortBy: true, - })); - - // Update page index on pagination click - const onServerPagination = useCallback(({ pageIndex }) => { - setPageIndex(pageIndex); - }, []); - - // Clear cache on reload button click - const handleReload = useCallback(() => { - setResultsPages(new Map()); - }, []); - let tableContent = null; - if (responseError) { // Render error if page download failed tableContent = ( -
-
-          {responseError}
-        
-
+ {responseError} + ); } else if (!resultsPages.size) { // Render loading if first page hasn't loaded - tableContent = ( -
- -
- ); + tableContent = ; } else if (resultsPage?.total === 0) { // Render empty state if no results are returned for page const title = t('No rows were returned for this dataset'); - tableContent = ( -
- -
- ); + tableContent = ; } else { // Render table if at least one page has successfully loaded tableContent = ( From d842f0a196a18d40b9376b9df3d403e49eed60b0 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Thu, 11 Aug 2022 15:02:13 -0600 Subject: [PATCH 14/16] Reset page index on reload. --- .../src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index f8ee5f0a9dac6..0682ae671bc0c 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -117,6 +117,7 @@ export default function DrillDetailPane({ // Clear cache on reload button click const handleReload = useCallback(() => { setResultsPages(new Map()); + setPageIndex(0); }, []); // Clear cache and reset page index if filters change From dc975d034059fc85dd2421a122611cfed857f37a Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Tue, 16 Aug 2022 21:39:54 -0700 Subject: [PATCH 15/16] Fix endless re-requests on request failure. --- .../dashboard/components/DrillDetailPane/DrillDetailPane.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index 0682ae671bc0c..c697ff7f29d58 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -116,12 +116,14 @@ export default function DrillDetailPane({ // Clear cache on reload button click const handleReload = useCallback(() => { + setResponseError(''); setResultsPages(new Map()); setPageIndex(0); }, []); // Clear cache and reset page index if filters change useEffect(() => { + setResponseError(''); setResultsPages(new Map()); setPageIndex(0); }, [filters]); @@ -145,7 +147,7 @@ export default function DrillDetailPane({ // Download page of results & trim cache if page not in cache useEffect(() => { - if (!isLoading && !resultsPages.has(pageIndex)) { + if (!responseError && !isLoading && !resultsPages.has(pageIndex)) { setIsLoading(true); const jsonPayload = getDrillPayload(formData, filters); const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / PAGE_SIZE); @@ -189,6 +191,7 @@ export default function DrillDetailPane({ formData, isLoading, pageIndex, + responseError, resultsPages, ]); From 2310b03e10c89833194e7175e51f5725466b80d9 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Wed, 17 Aug 2022 21:54:14 -0700 Subject: [PATCH 16/16] Fix modal layout issues. --- .../src/components/Chart/DrillDetailModal.tsx | 17 +++++++++++++++++ .../DrillDetailPane/DrillDetailPane.tsx | 16 ++++++---------- .../components/SliceHeaderControls/index.tsx | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/superset-frontend/src/components/Chart/DrillDetailModal.tsx b/superset-frontend/src/components/Chart/DrillDetailModal.tsx index 7180d8922df98..128359741b1f2 100644 --- a/superset-frontend/src/components/Chart/DrillDetailModal.tsx +++ b/superset-frontend/src/components/Chart/DrillDetailModal.tsx @@ -28,8 +28,10 @@ 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'; @@ -46,6 +48,7 @@ const DrillDetailModal: React.FC<{ 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 } }) => @@ -70,6 +73,12 @@ const DrillDetailModal: React.FC<{ return ( diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx index c697ff7f29d58..bf3f1985b7f15 100644 --- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx +++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx @@ -233,21 +233,17 @@ export default function DrillDetailPane({ showRowCount={false} small css={css` - min-height: 0; - overflow: scroll; + overflow: auto; + .table { + margin-bottom: 0; + } `} /> ); } return ( -
+ <> {tableContent} -
+ ); } diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index abb59ce359757..ee99767b310a4 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -37,6 +37,7 @@ import { QueryFormData, styled, t, + useTheme, } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; import { NoAnimationDropdown } from 'src/components/Dropdown'; @@ -171,6 +172,7 @@ const DashboardChartModalTrigger = ({ const closeModal = useCallback(() => setShowModal(false), []); const history = useHistory(); const exploreChart = () => history.push(exploreUrl); + const theme = useTheme(); return ( <> @@ -184,6 +186,12 @@ const DashboardChartModalTrigger = ({ {(() => (