refactor(Shared_url_query): Fix shared query URL access for SQL Lab users. (#31421)
This commit is contained in:
parent
4b0e907c3d
commit
65c4d39c31
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import * as reactRedux from 'react-redux';
|
import * as reactRedux from 'react-redux';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
@ -242,6 +243,55 @@ describe('SavedQueryList', () => {
|
||||||
expect(fetchMock.calls(/saved_query\/0/, 'DELETE')).toHaveLength(1);
|
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 () => {
|
it('shows/hides bulk actions when bulk actions is clicked', async () => {
|
||||||
const button = wrapper.find(Button).at(0);
|
const button = wrapper.find(Button).at(0);
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|
@ -331,6 +381,21 @@ describe('RTL', () => {
|
||||||
expect(exportTooltip).toBeInTheDocument();
|
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 () => {
|
it('renders an import button in the submenu', async () => {
|
||||||
// Grab and assert that import saved query button is visible
|
// Grab and assert that import saved query button is visible
|
||||||
const importButton = await screen.findByTestId('import-button');
|
const importButton = await screen.findByTestId('import-button');
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ import { TagsList } from 'src/components/Tags';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import { commonMenuData } from 'src/features/home/commonMenuData';
|
import { commonMenuData } from 'src/features/home/commonMenuData';
|
||||||
import { QueryObjectColumns, SavedQueryObject } from 'src/views/CRUD/types';
|
import { QueryObjectColumns, SavedQueryObject } from 'src/views/CRUD/types';
|
||||||
import copyTextToClipboard from 'src/utils/copy';
|
|
||||||
import Tag from 'src/types/TagType';
|
import Tag from 'src/types/TagType';
|
||||||
import ImportModelsModal from 'src/components/ImportModal/index';
|
import ImportModelsModal from 'src/components/ImportModal/index';
|
||||||
import { ModifiedInfo } from 'src/components/AuditInfo';
|
import { ModifiedInfo } from 'src/components/AuditInfo';
|
||||||
|
|
@ -233,16 +232,31 @@ function SavedQueryList({
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyQueryLink = useCallback(
|
const copyQueryLink = useCallback(
|
||||||
(id: number) => {
|
async (savedQuery: SavedQueryObject) => {
|
||||||
copyTextToClipboard(() =>
|
try {
|
||||||
Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
|
const payload = {
|
||||||
)
|
dbId: savedQuery.db_id,
|
||||||
.then(() => {
|
name: savedQuery.label,
|
||||||
addSuccessToast(t('Link Copied!'));
|
schema: savedQuery.schema,
|
||||||
})
|
catalog: savedQuery.catalog,
|
||||||
.catch(() => {
|
sql: savedQuery.sql,
|
||||||
addDangerToast(t('Sorry, your browser does not support copying.'));
|
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],
|
[addDangerToast, addSuccessToast],
|
||||||
);
|
);
|
||||||
|
|
@ -393,7 +407,7 @@ function SavedQueryList({
|
||||||
};
|
};
|
||||||
const handleEdit = ({ metaKey }: MouseEvent) =>
|
const handleEdit = ({ metaKey }: MouseEvent) =>
|
||||||
openInSqlLab(original.id, Boolean(metaKey));
|
openInSqlLab(original.id, Boolean(metaKey));
|
||||||
const handleCopy = () => copyQueryLink(original.id);
|
const handleCopy = () => copyQueryLink(original);
|
||||||
const handleExport = () => handleBulkSavedQueryExport([original]);
|
const handleExport = () => handleBulkSavedQueryExport([original]);
|
||||||
const handleDelete = () => setQueryCurrentlyDeleting(original);
|
const handleDelete = () => setQueryCurrentlyDeleting(original);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export interface Dashboard {
|
||||||
|
|
||||||
export type SavedQueryObject = {
|
export type SavedQueryObject = {
|
||||||
id: number;
|
id: number;
|
||||||
|
catalog: string | null;
|
||||||
changed_on: string;
|
changed_on: string;
|
||||||
changed_on_delta_humanized: string;
|
changed_on_delta_humanized: string;
|
||||||
database: {
|
database: {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ class SqlLabPermalinkSchema(Schema):
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
metadata={"description": "The schema name of the query"},
|
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(
|
sql = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
allow_none=False,
|
allow_none=False,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from typing import Optional, TypedDict
|
||||||
|
|
||||||
|
|
||||||
class SqlLabPermalinkValue(TypedDict):
|
class SqlLabPermalinkValue(TypedDict):
|
||||||
|
catalog: Optional[str]
|
||||||
dbId: int
|
dbId: int
|
||||||
name: str
|
name: str
|
||||||
schema: Optional[str]
|
schema: Optional[str]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue