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]