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 65c4d39c31 refactor(Shared_url_query): Fix shared  query URL access 
for SQL Lab users. (#31421)
65c4d39c31 is described below

commit 65c4d39c3112bac65e5677bb5f2e4f37ba6edd2d
Author: Levis Mbote <[email protected]>
AuthorDate: Fri Jan 24 21:34:55 2025 +0300

    refactor(Shared_url_query): Fix shared  query URL access for SQL Lab users. 
(#31421)
---
 .../pages/SavedQueryList/SavedQueryList.test.jsx   | 65 ++++++++++++++++++++++
 .../src/pages/SavedQueryList/index.tsx             | 36 ++++++++----
 superset-frontend/src/views/CRUD/types.ts          |  1 +
 superset/sqllab/permalink/schemas.py               |  5 ++
 superset/sqllab/permalink/types.py                 |  1 +
 5 files changed, 97 insertions(+), 11 deletions(-)

diff --git a/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.jsx 
b/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.jsx
index 952b234e21..30442e8730 100644
--- a/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.jsx
+++ b/superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.jsx
@@ -18,6 +18,7 @@
  */
 import thunk from 'redux-thunk';
 import * as reactRedux from 'react-redux';
+import { Provider } from 'react-redux';
 import { BrowserRouter } from 'react-router-dom';
 import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
@@ -242,6 +243,55 @@ describe('SavedQueryList', () => {
     expect(fetchMock.calls(/saved_query\/0/, 'DELETE')).toHaveLength(1);
   });
 
+  it('copies a query link when the API succeeds', async () => {
+    Object.assign(navigator, {
+      clipboard: {
+        writeText: jest.fn(),
+      },
+    });
+
+    fetchMock.get('glob:*/api/v1/saved_query', {
+      result: [
+        {
+          id: 1,
+          label: 'Test Query',
+          db_id: 1,
+          schema: 'public',
+          sql: 'SELECT * FROM table',
+        },
+      ],
+      count: 1,
+    });
+    fetchMock.post('glob:*/api/v1/sqllab/permalink', {
+      body: { url: 'http://example.com/permalink' },
+      status: 200,
+    });
+
+    render(
+      <Provider store={store}>
+        <BrowserRouter>
+          <QueryParamProvider>
+            <SavedQueryList />
+          </QueryParamProvider>
+        </BrowserRouter>
+      </Provider>,
+    );
+
+    const copyActionButton = await waitFor(
+      () => screen.getAllByTestId('copy-action')[0],
+    );
+    userEvent.hover(copyActionButton);
+
+    userEvent.click(copyActionButton);
+    await waitFor(() => {
+      expect(fetchMock.calls('glob:*/api/v1/sqllab/permalink').length).toBe(1);
+    });
+
+    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
+      'http://example.com/permalink',
+    );
+  });
+
   it('shows/hides bulk actions when bulk actions is clicked', async () => {
     const button = wrapper.find(Button).at(0);
     act(() => {
@@ -331,6 +381,21 @@ describe('RTL', () => {
     expect(exportTooltip).toBeInTheDocument();
   });
 
+  it('renders a copy button in the actions bar', async () => {
+    // Grab copy action button and mock mouse hovering over it
+    const copyActionButton = screen.getAllByTestId('copy-action')[0];
+    userEvent.hover(copyActionButton);
+
+    // Wait for the tooltip to pop up
+    await screen.findByRole('tooltip');
+
+    // Grab and assert that "Copy query URl" tooltip is in the document
+    const copyTooltip = screen.getByRole('tooltip', {
+      name: /Copy query URL/i,
+    });
+    expect(copyTooltip).toBeInTheDocument();
+  });
+
   it('renders an import button in the submenu', async () => {
     // Grab and assert that import saved query button is visible
     const importButton = await screen.findByTestId('import-button');
diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx 
b/superset-frontend/src/pages/SavedQueryList/index.tsx
index a65e6bcdf0..052815f635 100644
--- a/superset-frontend/src/pages/SavedQueryList/index.tsx
+++ b/superset-frontend/src/pages/SavedQueryList/index.tsx
@@ -51,7 +51,6 @@ import { TagsList } from 'src/components/Tags';
 import { Tooltip } from 'src/components/Tooltip';
 import { commonMenuData } from 'src/features/home/commonMenuData';
 import { QueryObjectColumns, SavedQueryObject } from 'src/views/CRUD/types';
-import copyTextToClipboard from 'src/utils/copy';
 import Tag from 'src/types/TagType';
 import ImportModelsModal from 'src/components/ImportModal/index';
 import { ModifiedInfo } from 'src/components/AuditInfo';
@@ -233,16 +232,31 @@ function SavedQueryList({
   };
 
   const copyQueryLink = useCallback(
-    (id: number) => {
-      copyTextToClipboard(() =>
-        Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
-      )
-        .then(() => {
-          addSuccessToast(t('Link Copied!'));
-        })
-        .catch(() => {
-          addDangerToast(t('Sorry, your browser does not support copying.'));
+    async (savedQuery: SavedQueryObject) => {
+      try {
+        const payload = {
+          dbId: savedQuery.db_id,
+          name: savedQuery.label,
+          schema: savedQuery.schema,
+          catalog: savedQuery.catalog,
+          sql: savedQuery.sql,
+          autorun: false,
+          templateParams: null,
+        };
+
+        const response = await SupersetClient.post({
+          endpoint: '/api/v1/sqllab/permalink',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(payload),
         });
+
+        const { url: permalink } = response.json;
+
+        await navigator.clipboard.writeText(permalink);
+        addSuccessToast(t('Link Copied!'));
+      } catch (error) {
+        addDangerToast(t('There was an error generating the permalink.'));
+      }
     },
     [addDangerToast, addSuccessToast],
   );
@@ -393,7 +407,7 @@ function SavedQueryList({
           };
           const handleEdit = ({ metaKey }: MouseEvent) =>
             openInSqlLab(original.id, Boolean(metaKey));
-          const handleCopy = () => copyQueryLink(original.id);
+          const handleCopy = () => copyQueryLink(original);
           const handleExport = () => handleBulkSavedQueryExport([original]);
           const handleDelete = () => setQueryCurrentlyDeleting(original);
 
diff --git a/superset-frontend/src/views/CRUD/types.ts 
b/superset-frontend/src/views/CRUD/types.ts
index fd93713555..0228711366 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -70,6 +70,7 @@ export interface Dashboard {
 
 export type SavedQueryObject = {
   id: number;
+  catalog: string | null;
   changed_on: string;
   changed_on_delta_humanized: string;
   database: {
diff --git a/superset/sqllab/permalink/schemas.py 
b/superset/sqllab/permalink/schemas.py
index d89c2b5796..9fbbf88666 100644
--- a/superset/sqllab/permalink/schemas.py
+++ b/superset/sqllab/permalink/schemas.py
@@ -34,6 +34,11 @@ class SqlLabPermalinkSchema(Schema):
         allow_none=True,
         metadata={"description": "The schema name of the query"},
     )
+    catalog = fields.String(
+        required=False,
+        allow_none=True,
+        metadata={"description": "The catalog name of the query"},
+    )
     sql = fields.String(
         required=True,
         allow_none=False,
diff --git a/superset/sqllab/permalink/types.py 
b/superset/sqllab/permalink/types.py
index adc127da11..0e0aa48ab0 100644
--- a/superset/sqllab/permalink/types.py
+++ b/superset/sqllab/permalink/types.py
@@ -18,6 +18,7 @@ from typing import Optional, TypedDict
 
 
 class SqlLabPermalinkValue(TypedDict):
+    catalog: Optional[str]
     dbId: int
     name: str
     schema: Optional[str]

Reply via email to