/** * 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, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import ButtonGroup from 'src/components/ButtonGroup'; import Alert from 'src/components/Alert'; import Button from 'src/components/Button'; import shortid from 'shortid'; import { QueryResponse, QueryState, styled, t, useTheme, } from '@superset-ui/core'; import { usePrevious } from 'src/hooks/usePrevious'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import { ISaveableDatasource, ISimpleColumn, SaveDatasetModal, } from 'src/SqlLab/components/SaveDatasetModal'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types'; import { mountExploreUrl } from 'src/explore/exploreUtils'; import { postFormData } from 'src/explore/exploreUtils/formData'; import ProgressBar from 'src/components/ProgressBar'; import Loading from 'src/components/Loading'; import FilterableTable, { MAX_COLUMNS_FOR_TABLE, } from 'src/components/FilterableTable'; import CopyToClipboard from 'src/components/CopyToClipboard'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import { prepareCopyToClipboardTabularData } from 'src/utils/common'; import { addQueryEditor, clearQueryResults, CtasEnum, fetchQueryResults, reFetchQueryResults, reRunQuery, } from 'src/SqlLab/actions/sqlLab'; import { URL_PARAMS } from 'src/constants'; import ExploreCtasResultsButton from '../ExploreCtasResultsButton'; import ExploreResultsButton from '../ExploreResultsButton'; import HighlightedSql from '../HighlightedSql'; import QueryStateLabel from '../QueryStateLabel'; enum LIMITING_FACTOR { QUERY = 'QUERY', QUERY_AND_DROPDOWN = 'QUERY_AND_DROPDOWN', DROPDOWN = 'DROPDOWN', NOT_LIMITED = 'NOT_LIMITED', } export interface ResultSetProps { cache?: boolean; csv?: boolean; database?: Record; displayLimit: number; height: number; query: QueryResponse; search?: boolean; showSql?: boolean; visualize?: boolean; user: UserWithPermissionsAndRoles; defaultQueryLimit: number; } const ResultlessStyles = styled.div` position: relative; min-height: ${({ theme }) => theme.gridUnit * 25}px; [role='alert'] { margin-top: ${({ theme }) => theme.gridUnit * 2}px; } .sql-result-track-job { margin-top: ${({ theme }) => theme.gridUnit * 2}px; } `; // Making text render line breaks/tabs as is as monospace, // but wrapping text too so text doesn't overflow const MonospaceDiv = styled.div` font-family: ${({ theme }) => theme.typography.families.monospace}; white-space: pre; word-break: break-word; overflow-x: auto; white-space: pre-wrap; `; const ReturnedRows = styled.div` font-size: ${({ theme }) => theme.typography.sizes.s}px; line-height: ${({ theme }) => theme.gridUnit * 6}px; `; const ResultSetControls = styled.div` display: flex; justify-content: space-between; padding: ${({ theme }) => 2 * theme.gridUnit}px 0; `; const ResultSetButtons = styled.div` display: grid; grid-auto-flow: column; padding-right: ${({ theme }) => 2 * theme.gridUnit}px; `; const LimitMessage = styled.span` color: ${({ theme }) => theme.colors.secondary.light1}; margin-left: ${({ theme }) => theme.gridUnit * 2}px; `; const ResultSet = ({ cache = false, csv = true, database = {}, displayLimit, height, query, search = true, showSql = false, visualize = true, user, defaultQueryLimit, }: ResultSetProps) => { const theme = useTheme(); const [searchText, setSearchText] = useState(''); const [cachedData, setCachedData] = useState[]>([]); const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false); const [alertIsOpen, setAlertIsOpen] = useState(false); const dispatch = useDispatch(); const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => { if ( query.errorMessage && query.errorMessage.indexOf('session timed out') > 0 ) { dispatch(reRunQuery(query)); } }, []); useEffect(() => { // only do this the first time the component is rendered/mounted reRunQueryIfSessionTimeoutErrorOnMount(); }, [reRunQueryIfSessionTimeoutErrorOnMount]); const fetchResults = (query: QueryResponse) => { dispatch(fetchQueryResults(query, displayLimit)); }; const prevQuery = usePrevious(query); useEffect(() => { if (cache && query.cached && query?.results?.data?.length > 0) { setCachedData(query.results.data); dispatch(clearQueryResults(query)); } if (query.resultsKey && query.resultsKey !== prevQuery?.resultsKey) { fetchResults(query); } }, [query, cache]); const calculateAlertRefHeight = (alertElement: HTMLElement | null) => { if (alertElement) { setAlertIsOpen(true); } else { setAlertIsOpen(false); } }; const popSelectStar = (tempSchema: string | null, tempTable: string) => { const qe = { id: shortid.generate(), name: tempTable, autorun: false, dbId: query.dbId, sql: `SELECT * FROM ${tempSchema ? `${tempSchema}.` : ''}${tempTable}`, }; dispatch(addQueryEditor(qe)); }; const changeSearch = (event: React.ChangeEvent) => { setSearchText(event.target.value); }; const createExploreResultsOnClick = async () => { const { results } = query; if (results?.query_id) { const key = await postFormData(results.query_id, 'query', { ...EXPLORE_CHART_DEFAULT, datasource: `${results.query_id}__query`, ...{ all_columns: results.columns.map(column => column.name), }, }); const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key, }); window.open(url, '_blank', 'noreferrer'); } else { addDangerToast(t('Unable to create chart without a query id.')); } }; const renderControls = () => { if (search || visualize || csv) { let { data } = query.results; if (cache && query.cached) { data = cachedData; } const { columns } = query.results; // Added compute logic to stop user from being able to Save & Explore const datasource: ISaveableDatasource = { columns: query.results.columns as ISimpleColumn[], name: query?.tab || 'Untitled', dbId: query?.dbId, sql: query?.sql, templateParams: query?.templateParams, schema: query?.schema, }; return ( setShowSaveDatasetModal(false)} buttonTextOnSave={t('Save & Explore')} buttonTextOnOverwrite={t('Overwrite & Explore')} modalDescription={t( 'Save this query as a virtual dataset to continue exploring', )} datasource={datasource} /> {visualize && database?.allows_virtual_table_explore && ( )} {csv && ( )} {t('Copy to Clipboard')} } hideTooltip /> {search && ( MAX_COLUMNS_FOR_TABLE} placeholder={ columns.length > MAX_COLUMNS_FOR_TABLE ? t('Too many columns to filter') : t('Filter results') } /> )} ); } return
; }; const renderRowsReturned = () => { const { results, rows, queryLimit, limitingFactor } = query; let limitMessage; const limitReached = results?.displayLimitReached; const limit = queryLimit || results.query.limit; const isAdmin = !!user?.roles?.Admin; const rowsCount = Math.min(rows || 0, results?.data?.length || 0); const displayMaxRowsReachedMessage = { withAdmin: t( 'The number of results displayed is limited to %(rows)d by the configuration DISPLAY_MAX_ROWS. ' + 'Please add additional limits/filters or download to csv to see more rows up to ' + 'the %(limit)d limit.', { rows: rowsCount, limit }, ), withoutAdmin: t( 'The number of results displayed is limited to %(rows)d. ' + 'Please add additional limits/filters, download to csv, or contact an admin ' + 'to see more rows up to the %(limit)d limit.', { rows: rowsCount, limit }, ), }; const shouldUseDefaultDropdownAlert = limit === defaultQueryLimit && limitingFactor === LIMITING_FACTOR.DROPDOWN; if (limitingFactor === LIMITING_FACTOR.QUERY && csv) { limitMessage = t( 'The number of rows displayed is limited to %(rows)d by the query', { rows }, ); } else if ( limitingFactor === LIMITING_FACTOR.DROPDOWN && !shouldUseDefaultDropdownAlert ) { limitMessage = t( 'The number of rows displayed is limited to %(rows)d by the limit dropdown.', { rows }, ); } else if (limitingFactor === LIMITING_FACTOR.QUERY_AND_DROPDOWN) { limitMessage = t( 'The number of rows displayed is limited to %(rows)d by the query and limit dropdown.', { rows }, ); } const rowsReturnedMessage = t('%(rows)d rows returned', { rows, }); const tooltipText = `${rowsReturnedMessage}. ${limitMessage}`; return ( {!limitReached && !shouldUseDefaultDropdownAlert && ( {rowsReturnedMessage} {limitMessage} )} {!limitReached && shouldUseDefaultDropdownAlert && (
setAlertIsOpen(false)} description={t( 'The number of rows displayed is limited to %(rows)d by the dropdown.', { rows }, )} />
)} {limitReached && (
setAlertIsOpen(false)} message={t('%(rows)d rows returned', { rows: rowsCount })} description={ isAdmin ? displayMaxRowsReachedMessage.withAdmin : displayMaxRowsReachedMessage.withoutAdmin } />
)}
); }; const limitReached = query?.results?.displayLimitReached; let sql; let exploreDBId = query.dbId; if (database?.explore_database_id) { exploreDBId = database.explore_database_id; } let trackingUrl; if ( query.trackingUrl && query.state !== QueryState.SUCCESS && query.state !== QueryState.FETCHING ) { trackingUrl = ( ); } if (showSql) { sql = ; } if (query.state === QueryState.STOPPED) { return ; } if (query.state === QueryState.FAILED) { return ( {query.errorMessage}} copyText={query.errorMessage || undefined} link={query.link} source="sqllab" /> {trackingUrl} ); } if (query.state === QueryState.SUCCESS && query.ctas) { const { tempSchema, tempTable } = query; let object = 'Table'; if (query.ctas_method === CtasEnum.VIEW) { object = 'View'; } return (
{t(object)} [ {tempSchema ? `${tempSchema}.` : ''} {tempTable} ] {t('was created')}   } />
); } if (query.state === QueryState.SUCCESS && query.results) { const { results } = query; // Accounts for offset needed for height of ResultSetRowsReturned component if !limitReached const rowMessageHeight = !limitReached ? 32 : 0; // Accounts for offset needed for height of Alert if this.state.alertIsOpen const alertContainerHeight = 70; // We need to calculate the height of this.renderRowsReturned() // if we want results panel to be proper height because the // FilterTable component needs an explicit height to render // react-virtualized Table component const rowsHeight = alertIsOpen ? height - alertContainerHeight : height - rowMessageHeight; let data; if (cache && query.cached) { data = cachedData; } else if (results?.data) { ({ data } = results); } if (data && data.length > 0) { const expandedColumns = results.expanded_columns ? results.expanded_columns.map(col => col.name) : []; return ( <> {renderControls()} {renderRowsReturned()} {sql} col.name)} height={rowsHeight} filterText={searchText} expandedColumns={expandedColumns} /> ); } if (data && data.length === 0) { return ; } } if (query.cached || (query.state === QueryState.SUCCESS && !query.results)) { if (query.isDataPreview) { return ( ); } if (query.resultsKey) { return ( ); } } let progressBar; if (query.progress > 0) { progressBar = ( ); } const progressMsg = query?.extra?.progress ?? null; return (
{!progressBar && }
{/* show loading bar whenever progress bar is completed but needs time to render */}
{query.progress === 100 && }
{progressMsg && }
{query.progress !== 100 && progressBar}
{trackingUrl &&
{trackingUrl}
}
); }; export default ResultSet;