From 12d3f564d3e26a928133d2edfb0186accaab1892 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Wed, 20 Jul 2022 12:59:31 -0400 Subject: [PATCH] Add server-paginated results in drill-to-detail modal. --- .../superset-ui-core/src/query/types/Query.ts | 4 + .../src/components/Chart/chartAction.js | 7 +- .../DatasourceFilterBar.tsx | 30 +++ .../DatasourceResultsPane/index.tsx | 172 ++++++++++++++++++ .../components/SliceHeader/index.tsx | 2 + .../components/SliceHeaderControls/index.tsx | 104 +++++++++-- .../components/gridComponents/Chart.jsx | 1 + 7 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/DatasourceResultsPane/DatasourceFilterBar.tsx create mode 100644 superset-frontend/src/dashboard/components/DatasourceResultsPane/index.tsx 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 9105e5b9c386d..61d98d2569833 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 @@ -399,4 +399,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/DatasourceResultsPane/DatasourceFilterBar.tsx b/superset-frontend/src/dashboard/components/DatasourceResultsPane/DatasourceFilterBar.tsx new file mode 100644 index 0000000000000..b060bc9f4eb26 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DatasourceResultsPane/DatasourceFilterBar.tsx @@ -0,0 +1,30 @@ +/** + * 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 { QueryObjectFilterClause } from '@superset-ui/core'; + +export default function DatasourceFilterBar({ + filters, + setFilters, +}: { + filters: QueryObjectFilterClause[]; + setFilters: (filters: QueryObjectFilterClause[]) => void; +}) { + return null; +} diff --git a/superset-frontend/src/dashboard/components/DatasourceResultsPane/index.tsx b/superset-frontend/src/dashboard/components/DatasourceResultsPane/index.tsx new file mode 100644 index 0000000000000..f93544c7e2960 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DatasourceResultsPane/index.tsx @@ -0,0 +1,172 @@ +/** + * 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 { + css, + Datasource, + ensureIsArray, + GenericDataType, + QueryObjectFilterClause, + styled, + 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 DatasourceFilterBar from './DatasourceFilterBar'; + +const Error = styled.pre` + margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`}; +`; + +const PAGE_SIZE = 50; + +export default function DatasourceResultsPane({ + datasource, + initialFilters, +}: { + datasource: Datasource; + initialFilters?: QueryObjectFilterClause[]; +}) { + const theme = useTheme(); + const pageResponses = useRef({}); + const [results, setResults] = useState<{ + total: number; + dataPage: Record[]; + } | null>(); + + const [colnames, setColnames] = useState([]); + const [coltypes, setColtypes] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [responseError, setResponseError] = useState(''); + const [filters, setFilters] = useState(initialFilters || []); + const [page, setPage] = useState(0); + const datasourceId = useMemo( + () => `${datasource.id}__${datasource.type}`, + [datasource], + ); + + useEffect(() => { + pageResponses.current = {}; + }, [datasource, filters]); + + useEffect(() => { + const getPageData = async () => { + try { + setIsLoading(true); + let pageResponse = pageResponses.current[page]; + if (!pageResponse) { + pageResponse = await getDatasourceSamples( + datasource.type, + datasource.id, + true, + filters.length ? filters : null, + { page: page + 1, perPage: PAGE_SIZE }, + ); + + pageResponses.current[page] = pageResponse; + } + + setResults({ + total: pageResponse.total_count, + dataPage: pageResponse.data, + }); + + setColnames(ensureIsArray(pageResponse.colnames)); + setColtypes(ensureIsArray(pageResponse.coltypes)); + setResponseError(''); + } catch (error) { + setResults(null); + setColnames([]); + setColtypes([]); + setResponseError(`${error.name}: ${error.message}`); + } finally { + setIsLoading(false); + } + }; + + getPageData(); + }, [page, datasource, filters]); + + // 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( + colnames, + coltypes, + results?.dataPage, + datasourceId, + ); + + const onServerPagination = useCallback(({ pageIndex }) => { + setPage(pageIndex); + }, []); + + if (isLoading && !results) { + return ( +
+ +
+ ); + } + + if (responseError) { + return {responseError}; + } + + if (!results || results.total === 0) { + const title = t('No rows were returned for this dataset'); + return ; + } + + return ( + <> + + + + ); +} diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx index d4b5166691a50..e0f566c64f63f 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx @@ -94,6 +94,7 @@ const SliceHeader: FC = ({ formData, width, height, + datasource, }) => { const dispatch = useDispatch(); const uiConfig = useUiConfig(); @@ -222,6 +223,7 @@ const SliceHeader: FC = ({ isDescriptionExpanded={isExpanded} chartStatus={chartStatus} formData={formData} + datasource={datasource} /> )} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index ba748fc9cb1c9..ded87f0e14370 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -16,11 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import React, { MouseEvent, Key } from 'react'; +import React, { + MouseEvent, + Key, + ReactChild, + useState, + useCallback, +} from 'react'; import moment from 'moment'; import { Behavior, css, + Datasource, getChartMetadataRegistry, QueryFormData, styled, @@ -39,6 +46,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 DatasourceResultsPane from '../DatasourceResultsPane'; const MENU_KEYS = { CROSS_FILTER_SCOPING: 'cross_filter_scoping', @@ -51,6 +60,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` @@ -124,6 +134,8 @@ export interface SliceHeaderControlsProps { supersetCanShare?: boolean; supersetCanCSV?: boolean; sliceCanEdit?: boolean; + + datasource: Datasource; } interface State { showControls: boolean; @@ -137,6 +149,66 @@ const dropdownIconsStyles = css` } `; +const DashboardChartModalTrigger = ({ + onExploreChart, + triggerNode, + modalTitle, + modalBody, +}: { + onExploreChart: (event: MouseEvent) => void; + triggerNode: ReactChild; + modalTitle: ReactChild; + modalBody: ReactChild; +}) => { + const [showModal, setShowModal] = useState(false); + const openModal = useCallback(() => setShowModal(true), []); + const closeModal = useCallback(() => setShowModal(false), []); + + return ( + <> + + {triggerNode} + + {(() => ( + + + + + } + responsive + resizable + draggable + destroyOnClose + > + {modalBody} + + ))()} + + ); +}; + class SliceHeaderControls extends React.PureComponent< SliceHeaderControlsProps, State @@ -335,7 +407,8 @@ class SliceHeaderControls extends React.PureComponent< {this.props.supersetCanExplore && ( - {t('View as table')} @@ -351,18 +424,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 /> )} diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 314dd6d1cb0ea..eb86660d77134 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -451,6 +451,7 @@ class Chart extends React.Component { formData={formData} width={width} height={this.getHeaderHeight()} + datasource={datasource} /> {/*