This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new d4ba44fce2 fix: Query history view button in SqlLab (#36540)
d4ba44fce2 is described below

commit d4ba44fce200cb21f0e3a2bb11e754e5fb0e0a6f
Author: Geidō <[email protected]>
AuthorDate: Wed Dec 31 15:18:59 2025 +0100

    fix: Query history view button in SqlLab (#36540)
---
 .../components/QueryTable/QueryTable.test.tsx      |  97 +++++++++++++++++-
 .../src/SqlLab/components/QueryTable/index.tsx     | 110 +++++++++++++++------
 .../src/SqlLab/components/QueryTable/styles.ts     |   7 ++
 .../src/SqlLab/components/ResultSet/index.tsx      |  50 ++++++----
 4 files changed, 214 insertions(+), 50 deletions(-)

diff --git 
a/superset-frontend/src/SqlLab/components/QueryTable/QueryTable.test.tsx 
b/superset-frontend/src/SqlLab/components/QueryTable/QueryTable.test.tsx
index eeebc881ed..4d93638a29 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/QueryTable.test.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/QueryTable.test.tsx
@@ -19,9 +19,10 @@
 import { isValidElement } from 'react';
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
+import userEvent from '@testing-library/user-event';
 import QueryTable from 'src/SqlLab/components/QueryTable';
 import { runningQuery, successfulQuery, user } from 'src/SqlLab/fixtures';
-import { render, screen } from 'spec/helpers/testing-library';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
 
 const mockedProps = {
   queries: [runningQuery, successfulQuery],
@@ -29,6 +30,11 @@ const mockedProps = {
   latestQueryId: 'ryhMUZCGb',
 };
 
+const queryWithResults = {
+  ...successfulQuery,
+  resultsKey: 'test-results-key-123',
+};
+
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
 describe('QueryTable', () => {
   test('is valid', () => {
@@ -92,4 +98,93 @@ describe('QueryTable', () => {
       ),
     ).toHaveLength(1);
   });
+
+  test('renders View button when query has resultsKey', () => {
+    const mockStore = configureStore([thunk]);
+    const propsWithResults = {
+      ...mockedProps,
+      columns: ['started', 'duration', 'rows', 'results'],
+      queries: [queryWithResults],
+    };
+    render(<QueryTable {...propsWithResults} />, {
+      store: mockStore({ user, sqlLab: { queries: {} } }),
+    });
+
+    expect(screen.getByRole('button', { name: /view/i })).toBeInTheDocument();
+  });
+
+  test('does not render View button when query has no resultsKey', () => {
+    const mockStore = configureStore([thunk]);
+    const queryWithoutResults = {
+      ...successfulQuery,
+      resultsKey: null,
+    };
+    const propsWithoutResults = {
+      ...mockedProps,
+      columns: ['started', 'duration', 'rows', 'results'],
+      queries: [queryWithoutResults],
+    };
+    render(<QueryTable {...propsWithoutResults} />, {
+      store: mockStore({ user, sqlLab: { queries: {} } }),
+    });
+
+    expect(
+      screen.queryByRole('button', { name: /view/i }),
+    ).not.toBeInTheDocument();
+  });
+
+  test('clicking View button opens data preview modal', async () => {
+    const mockStore = configureStore([thunk]);
+    const propsWithResults = {
+      ...mockedProps,
+      columns: ['started', 'duration', 'rows', 'results'],
+      queries: [queryWithResults],
+    };
+    render(<QueryTable {...propsWithResults} />, {
+      store: mockStore({
+        user,
+        sqlLab: {
+          queries: {
+            [queryWithResults.id]: queryWithResults,
+          },
+        },
+      }),
+    });
+
+    const viewButton = screen.getByRole('button', { name: /view/i });
+    await userEvent.click(viewButton);
+
+    expect(await screen.findByText('Data preview')).toBeInTheDocument();
+  });
+
+  test('modal closes when exiting', async () => {
+    const mockStore = configureStore([thunk]);
+    const propsWithResults = {
+      ...mockedProps,
+      columns: ['started', 'duration', 'rows', 'results'],
+      queries: [queryWithResults],
+    };
+    render(<QueryTable {...propsWithResults} />, {
+      store: mockStore({
+        user,
+        sqlLab: {
+          queries: {
+            [queryWithResults.id]: queryWithResults,
+          },
+        },
+      }),
+    });
+
+    const viewButton = screen.getByRole('button', { name: /view/i });
+    await userEvent.click(viewButton);
+
+    expect(await screen.findByText('Data preview')).toBeInTheDocument();
+
+    const closeButton = screen.getByRole('button', { name: /close/i });
+    await userEvent.click(closeButton);
+
+    await waitFor(() => {
+      expect(screen.queryByText('Data preview')).not.toBeInTheDocument();
+    });
+  });
 });
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx 
b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index 8e05d136d9..344a0cb513 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo, ReactNode } from 'react';
+import { useMemo, ReactNode, useState, useRef } from 'react';
 import {
   Card,
   Button,
@@ -27,9 +27,9 @@ import {
   TableView,
 } from '@superset-ui/core/components';
 import ProgressBar from '@superset-ui/core/components/ProgressBar';
-import { t, QueryResponse } from '@superset-ui/core';
+import { t, QueryResponse, QueryState } from '@superset-ui/core';
 import { useTheme } from '@apache-superset/core/ui';
-import { useDispatch, useSelector } from 'react-redux';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 
 import {
   queryEditorSetSql,
@@ -37,6 +37,7 @@ import {
   fetchQueryResults,
   clearQueryResults,
   removeQuery,
+  startQuery,
 } from 'src/SqlLab/actions/sqlLab';
 import { fDuration, extendedDayjs } from '@superset-ui/core/utils/dates';
 import { SqlLabRootState } from 'src/SqlLab/types';
@@ -44,7 +45,7 @@ import { UserWithPermissionsAndRoles as User } from 
'src/types/bootstrapTypes';
 import { makeUrl } from 'src/utils/pathUtils';
 import ResultSet from '../ResultSet';
 import HighlightedSql from '../HighlightedSql';
-import { StaticPosition, StyledTooltip } from './styles';
+import { StaticPosition, StyledTooltip, ModalResultSetWrapper } from 
'./styles';
 
 interface QueryTableQuery extends Omit<
   QueryResponse,
@@ -82,6 +83,15 @@ const QueryTable = ({
 }: QueryTableProps) => {
   const theme = useTheme();
   const dispatch = useDispatch();
+  const [selectedQuery, setSelectedQuery] = useState<QueryResponse | null>(
+    null,
+  );
+  const selectedQueryRef = useRef<QueryResponse | null>(null);
+  const modalRef = useRef<{
+    close: () => void;
+    open: (e: React.MouseEvent) => void;
+    showModal: boolean;
+  } | null>(null);
 
   const QUERY_HISTORY_TABLE_HEADERS_LOCALIZED = {
     state: t('State'),
@@ -116,6 +126,14 @@ const QueryTable = ({
   );
 
   const user = useSelector<SqlLabRootState, User>(state => state.user);
+  const reduxQueries = useSelector<
+    SqlLabRootState,
+    Record<string, QueryResponse>
+  >(state => state.sqlLab?.queries ?? {}, shallowEqual);
+
+  const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
+    dispatch(fetchQueryResults(query, displayLimit));
+  };
 
   const data = useMemo(() => {
     const restoreSql = (query: QueryResponse) => {
@@ -128,10 +146,6 @@ const QueryTable = ({
       dispatch(cloneQueryToNewTab(query, true));
     };
 
-    const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
-      dispatch(fetchQueryResults(query, displayLimit));
-    };
-
     const statusAttributes = {
       success: {
         config: {
@@ -289,26 +303,17 @@ const QueryTable = ({
         );
         if (q.resultsKey) {
           q.results = (
-            <ModalTrigger
-              className="ResultsModal"
-              triggerNode={
-                <Button buttonSize="xsmall" buttonStyle="secondary">
-                  {t('View')}
-                </Button>
-              }
-              modalTitle={t('Data preview')}
-              beforeOpen={() => openAsyncResults(query, displayLimit)}
-              onExit={() => dispatch(clearQueryResults(query))}
-              modalBody={
-                <ResultSet
-                  showSql
-                  queryId={query.id}
-                  displayLimit={displayLimit}
-                  defaultQueryLimit={1000}
-                />
-              }
-              responsive
-            />
+            <Button
+              buttonSize="xsmall"
+              buttonStyle="secondary"
+              onClick={(e: React.MouseEvent) => {
+                selectedQueryRef.current = query;
+                setSelectedQuery(query);
+                modalRef.current?.open(e);
+              }}
+            >
+              {t('View')}
+            </Button>
           );
         } else {
           q.results = <></>;
@@ -365,6 +370,55 @@ const QueryTable = ({
 
   return (
     <div className="QueryTable">
+      <ModalTrigger
+        ref={modalRef}
+        triggerNode={null}
+        className="ResultsModal"
+        modalTitle={t('Data preview')}
+        beforeOpen={() => {
+          const query = selectedQueryRef.current;
+          if (query) {
+            const existingQuery = reduxQueries[query.id];
+            if (!existingQuery?.sql && query.sql) {
+              dispatch(startQuery({ ...query, sql: query.sql }, false));
+            }
+            openAsyncResults(query, displayLimit);
+          }
+        }}
+        onExit={() => {
+          const query = selectedQueryRef.current;
+          if (query) {
+            dispatch(clearQueryResults(query));
+            selectedQueryRef.current = null;
+            setSelectedQuery(null);
+          }
+        }}
+        modalBody={
+          selectedQuery ? (
+            <ModalResultSetWrapper>
+              {(() => {
+                const height =
+                  reduxQueries[selectedQuery.id]?.state ===
+                    QueryState.Success &&
+                  reduxQueries[selectedQuery.id]?.results
+                    ? Math.floor(window.innerHeight * 0.5)
+                    : undefined;
+                return (
+                  <ResultSet
+                    showSql
+                    queryId={selectedQuery.id}
+                    displayLimit={displayLimit}
+                    defaultQueryLimit={1000}
+                    useFixedHeight
+                    height={height}
+                  />
+                );
+              })()}
+            </ModalResultSetWrapper>
+          ) : null
+        }
+        responsive
+      />
       <TableView
         columns={columnsOfTable}
         data={data}
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/styles.ts 
b/superset-frontend/src/SqlLab/components/QueryTable/styles.ts
index bf6623af3f..2aae4e7c90 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/styles.ts
+++ b/superset-frontend/src/SqlLab/components/QueryTable/styles.ts
@@ -39,3 +39,10 @@ export const StyledTooltip = styled(IconTooltip)`
     }
   }
 `;
+
+export const ModalResultSetWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  max-height: 50vh;
+`;
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx 
b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 8198bd2587..b52a780488 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -104,12 +104,14 @@ export interface ResultSetProps {
   csv?: boolean;
   database?: Record<string, any>;
   displayLimit: number;
+  height?: number;
   queryId: string;
   search?: boolean;
   showSql?: boolean;
   showSqlInline?: boolean;
   visualize?: boolean;
   defaultQueryLimit: number;
+  useFixedHeight?: boolean;
 }
 
 const ResultContainer = styled.div`
@@ -177,12 +179,14 @@ const ResultSet = ({
   csv = true,
   database = {},
   displayLimit,
+  height,
   queryId,
   search = true,
   showSql = false,
   showSqlInline = false,
   visualize = true,
   defaultQueryLimit,
+  useFixedHeight = false,
 }: ResultSetProps) => {
   const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
   const streamingThreshold = useSelector(
@@ -711,6 +715,16 @@ const ResultSet = ({
         LocalStorageKeys.SqllabIsRenderHtmlEnabled,
         true,
       );
+
+      const tableProps = {
+        data,
+        queryId: query.id,
+        orderedColumnKeys: results.columns.map(col => col.column_name),
+        filterText: searchText,
+        expandedColumns,
+        allowHTML,
+      };
+
       return (
         <>
           <ResultContainer>
@@ -753,27 +767,21 @@ const ResultSet = ({
                 {sql}
               </>
             )}
-            <div
-              css={css`
-                flex: 1 1 auto;
-              `}
-            >
-              <AutoSizer disableWidth>
-                {({ height }) => (
-                  <ResultTable
-                    data={data}
-                    queryId={query.id}
-                    orderedColumnKeys={results.columns.map(
-                      col => col.column_name,
-                    )}
-                    height={height}
-                    filterText={searchText}
-                    expandedColumns={expandedColumns}
-                    allowHTML={allowHTML}
-                  />
-                )}
-              </AutoSizer>
-            </div>
+            {useFixedHeight && height !== undefined ? (
+              <ResultTable {...tableProps} height={height} />
+            ) : (
+              <div
+                css={css`
+                  flex: 1 1 auto;
+                `}
+              >
+                <AutoSizer disableWidth>
+                  {({ height: autoHeight }) => (
+                    <ResultTable {...tableProps} height={autoHeight} />
+                  )}
+                </AutoSizer>
+              </div>
+            )}
           </ResultContainer>
           <StreamingExportModal
             visible={showStreamingModal}

Reply via email to