Skip to content

Commit 2874096

Browse files
authored
refactor(sqllab): migrate share queries via kv by permalink (#29163)
1 parent b86572b commit 2874096

File tree

27 files changed

+682
-229
lines changed

27 files changed

+682
-229
lines changed

RESOURCES/FEATURE_FLAGS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ These features flags are **safe for production**. They have been tested and will
6262
[//]: # "PLEASE KEEP THESE LISTS SORTED ALPHABETICALLY"
6363

6464
### Flags on the path to feature launch and flag deprecation/removal
65+
6566
- DASHBOARD_VIRTUALIZATION
6667
- DRILL_BY
6768
- DISABLE_LEGACY_DATASOURCE_EDITOR

UPDATING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This file documents any backwards-incompatible changes in Superset and
2323
assists people when migrating to a new version.
2424

2525
## Next
26+
2627
- [31894](https://github.com/apache/superset/pull/31894) Domain sharding is deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration will be removed in the next major version (6.0)
2728
- [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling.
2829
- [31582](https://github.com/apache/superset/pull/31582) Removed the legacy Area, Bar, Event Flow, Heatmap, Histogram, Line, Sankey, and Sankey Loop charts. They were all automatically migrated to their ECharts counterparts with the exception of the Event Flow and Sankey Loop charts which were removed as they were not actively maintained and not widely used. If you were using the Event Flow or Sankey Loop charts, you will need to find an alternative solution.
@@ -33,8 +34,9 @@ assists people when migrating to a new version.
3334
- [31262](https://github.com/apache/superset/pull/31262) NOTE: deprecated `pylint` in favor of `ruff` as our only python linter. Only affect development workflows positively (not the release itself). It should cover most important rules, be much faster, but some things linting rules that were enforced before may not be enforce in the exact same way as before.
3435
- [31173](https://github.com/apache/superset/pull/31173) Modified `fetch_csrf_token` to align with HTTP standards, particularly regarding how cookies are handled. If you encounter any issues related to CSRF functionality, please report them as a new issue and reference this PR for context.
3536
- [31385](https://github.com/apache/superset/pull/31385) Significant docker refactor, reducing access levels for the `superset` user, streamlining layer building, ...
36-
- [31503](https://github.com/apache/superset/pull/31503) Deprecating python 3.9.x support, 3.11 is now the recommended version and 3.10 is still supported over the Superset 5.0 lifecycle.
37+
- [31503](https://github.com/apache/superset/pull/31503) Deprecating python 3.9.x support, 3.11 is now the recommended version and 3.10 is still supported over the Superset 5.0 lifecycle.
3738
- [29121](https://github.com/apache/superset/pull/29121) Removed the `css`, `position_json`, and `json_metadata` from the payload of the dashboard list endpoint (`GET api/v1/dashboard`) for performance reasons.
39+
- [29163](https://github.com/apache/superset/pull/29163) Removed the `SHARE_QUERIES_VIA_KV_STORE` and `KV_STORE` feature flags and changed the way Superset shares SQL Lab queries to use permalinks. The legacy `/kv` API was removed but we still support legacy links in 5.0. In 6.0, only permalinks will be supported.
3840

3941
### Potential Downtime
4042

superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ export enum FeatureFlag {
5252
HorizontalFilterBar = 'HORIZONTAL_FILTER_BAR',
5353
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
5454
ScheduledQueries = 'SCHEDULED_QUERIES',
55-
ShareQueriesViaKvStore = 'SHARE_QUERIES_VIA_KV_STORE',
5655
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
5756
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
5857
SshTunneling = 'SSH_TUNNELING',

superset-frontend/src/SqlLab/actions/sqlLab.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1167,9 +1167,31 @@ export function persistEditorHeight(queryEditor, northPercent, southPercent) {
11671167
};
11681168
}
11691169

1170+
export function popPermalink(key) {
1171+
return function (dispatch) {
1172+
return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/${key}` })
1173+
.then(({ json }) =>
1174+
dispatch(
1175+
addQueryEditor({
1176+
name: json.name ? json.name : t('Shared query'),
1177+
dbId: json.dbId ? parseInt(json.dbId, 10) : null,
1178+
catalog: json.catalog ? json.catalog : null,
1179+
schema: json.schema ? json.schema : null,
1180+
autorun: json.autorun ? json.autorun : false,
1181+
sql: json.sql ? json.sql : 'SELECT ...',
1182+
templateParams: json.templateParams,
1183+
}),
1184+
),
1185+
)
1186+
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
1187+
};
1188+
}
1189+
11701190
export function popStoredQuery(urlId) {
11711191
return function (dispatch) {
1172-
return SupersetClient.get({ endpoint: `/kv/${urlId}` })
1192+
return SupersetClient.get({
1193+
endpoint: `/api/v1/sqllab/permalink/kv:${urlId}`,
1194+
})
11731195
.then(({ json }) =>
11741196
dispatch(
11751197
addQueryEditor({

superset-frontend/src/SqlLab/components/ShareSqlLabQuery/ShareSqlLabQuery.test.tsx

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ import fetchMock from 'fetch-mock';
2323
import * as uiCore from '@superset-ui/core';
2424
import { Provider } from 'react-redux';
2525
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
26-
import { render, screen, act } from '@testing-library/react';
26+
import { render, screen, act, waitFor } from '@testing-library/react';
2727
import '@testing-library/jest-dom';
2828
import userEvent from '@testing-library/user-event';
29-
import * as utils from 'src/utils/common';
3029
import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery';
3130
import { initialState } from 'src/SqlLab/fixtures';
3231

@@ -92,20 +91,24 @@ const standardProviderWithUnsaved: FC = ({ children }) => (
9291
);
9392

9493
describe('ShareSqlLabQuery', () => {
95-
const storeQueryUrl = 'glob:*/kv/store/';
96-
const storeQueryMockId = '123';
94+
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
95+
const storeQueryMockId = 'ci39c3';
9796

9897
beforeEach(async () => {
99-
fetchMock.post(storeQueryUrl, () => ({ id: storeQueryMockId }), {
100-
overwriteRoutes: true,
101-
});
98+
fetchMock.post(
99+
storeQueryUrl,
100+
() => ({ key: storeQueryMockId, url: `/p/${storeQueryMockId}` }),
101+
{
102+
overwriteRoutes: true,
103+
},
104+
);
102105
fetchMock.resetHistory();
103106
jest.clearAllMocks();
104107
});
105108

106109
afterAll(fetchMock.reset);
107110

108-
describe('via /kv/store', () => {
111+
describe('via permalink api', () => {
109112
beforeAll(() => {
110113
isFeatureEnabledMock = jest
111114
.spyOn(uiCore, 'isFeatureEnabled')
@@ -124,11 +127,13 @@ describe('ShareSqlLabQuery', () => {
124127
});
125128
const button = screen.getByRole('button');
126129
const { id, remoteId, ...expected } = mockQueryEditor;
127-
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
128130
userEvent.click(button);
129-
expect(storeQuerySpy.mock.calls).toHaveLength(1);
130-
expect(storeQuerySpy).toHaveBeenCalledWith(expected);
131-
storeQuerySpy.mockRestore();
131+
await waitFor(() =>
132+
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
133+
);
134+
expect(
135+
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
136+
).toEqual(expected);
132137
});
133138

134139
it('calls storeQuery() with unsaved changes', async () => {
@@ -139,48 +144,13 @@ describe('ShareSqlLabQuery', () => {
139144
});
140145
const button = screen.getByRole('button');
141146
const { id, ...expected } = unsavedQueryEditor;
142-
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
143-
userEvent.click(button);
144-
expect(storeQuerySpy.mock.calls).toHaveLength(1);
145-
expect(storeQuerySpy).toHaveBeenCalledWith(expected);
146-
storeQuerySpy.mockRestore();
147-
});
148-
});
149-
150-
describe('via saved query', () => {
151-
beforeAll(() => {
152-
isFeatureEnabledMock = jest
153-
.spyOn(uiCore, 'isFeatureEnabled')
154-
.mockImplementation(() => false);
155-
});
156-
157-
afterAll(() => {
158-
isFeatureEnabledMock.mockReset();
159-
});
160-
161-
it('does not call storeQuery() with the query when getCopyUrl() is called and feature is not enabled', async () => {
162-
await act(async () => {
163-
render(<ShareSqlLabQuery {...defaultProps} />, {
164-
wrapper: standardProvider,
165-
});
166-
});
167-
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
168-
const button = screen.getByRole('button');
169147
userEvent.click(button);
170-
expect(storeQuerySpy.mock.calls).toHaveLength(0);
171-
storeQuerySpy.mockRestore();
172-
});
173-
174-
it('button is disabled and there is a request to save the query', async () => {
175-
const updatedProps = {
176-
queryEditorId: disabled.id,
177-
};
178-
179-
render(<ShareSqlLabQuery {...updatedProps} />, {
180-
wrapper: standardProvider,
181-
});
182-
const button = await screen.findByRole('button', { name: /copy link/i });
183-
expect(button).toBeDisabled();
148+
await waitFor(() =>
149+
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
150+
);
151+
expect(
152+
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
153+
).toEqual(expected);
184154
});
185155
});
186156
});

superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx

Lines changed: 23 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,16 @@
1717
* under the License.
1818
*/
1919
import {
20-
FeatureFlag,
2120
styled,
2221
t,
2322
useTheme,
24-
isFeatureEnabled,
2523
getClientErrorObject,
24+
SupersetClient,
2625
} from '@superset-ui/core';
2726
import Button from 'src/components/Button';
2827
import Icons from 'src/components/Icons';
2928
import withToasts from 'src/components/MessageToasts/withToasts';
3029
import CopyToClipboard from 'src/components/CopyToClipboard';
31-
import { storeQuery } from 'src/utils/common';
3230
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
3331
import { LOG_ACTIONS_SQLLAB_COPY_LINK } from 'src/logger/LogUtils';
3432
import useLogAction from 'src/logger/useLogAction';
@@ -54,23 +52,21 @@ const ShareSqlLabQuery = ({
5452
}: ShareSqlLabQueryProps) => {
5553
const theme = useTheme();
5654
const logAction = useLogAction({ queryEditorId });
57-
const { dbId, name, schema, autorun, sql, remoteId, templateParams } =
58-
useQueryEditor(queryEditorId, [
59-
'dbId',
60-
'name',
61-
'schema',
62-
'autorun',
63-
'sql',
64-
'remoteId',
65-
'templateParams',
66-
]);
55+
const { dbId, name, schema, autorun, sql, templateParams } = useQueryEditor(
56+
queryEditorId,
57+
['dbId', 'name', 'schema', 'autorun', 'sql', 'templateParams'],
58+
);
6759

68-
const getCopyUrlForKvStore = (callback: Function) => {
60+
const getCopyUrlForPermalink = (callback: Function) => {
6961
const sharedQuery = { dbId, name, schema, autorun, sql, templateParams };
7062

71-
return storeQuery(sharedQuery)
72-
.then(shortUrl => {
73-
callback(shortUrl);
63+
return SupersetClient.post({
64+
endpoint: '/api/v1/sqllab/permalink',
65+
headers: { 'Content-Type': 'application/json' },
66+
body: JSON.stringify(sharedQuery),
67+
})
68+
.then(({ json }) => {
69+
callback(json.url);
7470
})
7571
.catch(response => {
7672
getClientErrorObject(response).then(() => {
@@ -79,61 +75,29 @@ const ShareSqlLabQuery = ({
7975
});
8076
};
8177

82-
const getCopyUrlForSavedQuery = (callback: Function) => {
83-
let savedQueryToastContent;
84-
85-
if (remoteId) {
86-
savedQueryToastContent = `${
87-
window.location.origin + window.location.pathname
88-
}?savedQueryId=${remoteId}`;
89-
callback(savedQueryToastContent);
90-
} else {
91-
savedQueryToastContent = t('Please save the query to enable sharing');
92-
callback(savedQueryToastContent);
93-
}
94-
};
9578
const getCopyUrl = (callback: Function) => {
9679
logAction(LOG_ACTIONS_SQLLAB_COPY_LINK, {
9780
shortcut: false,
9881
});
99-
if (isFeatureEnabled(FeatureFlag.ShareQueriesViaKvStore)) {
100-
return getCopyUrlForKvStore(callback);
101-
}
102-
return getCopyUrlForSavedQuery(callback);
82+
return getCopyUrlForPermalink(callback);
10383
};
10484

105-
const buildButton = (canShare: boolean) => {
106-
const tooltip = canShare
107-
? t('Copy query link to your clipboard')
108-
: t('Save the query to enable this feature');
85+
const buildButton = () => {
86+
const tooltip = t('Copy query link to your clipboard');
10987
return (
110-
<Button buttonSize="small" tooltip={tooltip} disabled={!canShare}>
111-
<StyledIcon
112-
iconColor={
113-
canShare ? theme.colors.primary.base : theme.colors.grayscale.base
114-
}
115-
iconSize="xl"
116-
/>
88+
<Button buttonSize="small" tooltip={tooltip}>
89+
<StyledIcon iconColor={theme.colors.primary.base} iconSize="xl" />
11790
{t('Copy link')}
11891
</Button>
11992
);
12093
};
12194

122-
const canShare =
123-
!!remoteId || isFeatureEnabled(FeatureFlag.ShareQueriesViaKvStore);
124-
12595
return (
126-
<>
127-
{canShare ? (
128-
<CopyToClipboard
129-
getText={getCopyUrl}
130-
wrapped={false}
131-
copyNode={buildButton(canShare)}
132-
/>
133-
) : (
134-
buildButton(canShare)
135-
)}
136-
</>
96+
<CopyToClipboard
97+
getText={getCopyUrl}
98+
wrapped={false}
99+
copyNode={buildButton()}
100+
/>
137101
);
138102
};
139103

superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ import { Store } from 'redux';
2828
import { RootState } from 'src/views/store';
2929
import { SET_ACTIVE_QUERY_EDITOR } from 'src/SqlLab/actions/sqlLab';
3030

31-
fetchMock.get('glob:*/api/v1/database/*', {});
32-
fetchMock.get('glob:*/api/v1/saved_query/*', {});
33-
fetchMock.get('glob:*/kv/*', {});
34-
3531
jest.mock('src/SqlLab/components/SqlEditor', () => () => (
3632
<div data-test="mock-sql-editor" />
3733
));
@@ -46,11 +42,20 @@ const setup = (overridesStore?: Store, initialState?: RootState) =>
4642
initialState,
4743
...(overridesStore && { store: overridesStore }),
4844
});
45+
let pathStub = jest.spyOn(URI.prototype, 'path');
4946

5047
beforeEach(() => {
48+
fetchMock.get('glob:*/api/v1/database/*', {});
49+
fetchMock.get('glob:*/api/v1/saved_query/*', {});
50+
pathStub = jest.spyOn(URI.prototype, 'path').mockReturnValue(`/sqllab/`);
5151
store.clearActions();
5252
});
5353

54+
afterEach(() => {
55+
fetchMock.reset();
56+
pathStub.mockReset();
57+
});
58+
5459
describe('componentDidMount', () => {
5560
let uriStub = jest.spyOn(URI.prototype, 'search');
5661
let replaceState = jest.spyOn(window.history, 'replaceState');
@@ -62,14 +67,47 @@ describe('componentDidMount', () => {
6267
replaceState.mockReset();
6368
uriStub.mockReset();
6469
});
65-
test('should handle id', () => {
70+
test('should handle id', async () => {
71+
const id = 1;
72+
fetchMock.get(`glob:*/api/v1/sqllab/permalink/kv:${id}`, {
73+
label: 'test permalink',
74+
sql: 'SELECT * FROM test_table',
75+
dbId: 1,
76+
});
6677
uriStub.mockReturnValue({ id: 1 });
6778
setup(store);
6879
expect(replaceState).toHaveBeenCalledWith(
6980
expect.anything(),
7081
expect.anything(),
7182
'/sqllab',
7283
);
84+
await waitFor(() =>
85+
expect(
86+
fetchMock.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`),
87+
).toHaveLength(1),
88+
);
89+
fetchMock.reset();
90+
});
91+
test('should handle permalink', async () => {
92+
const key = '9sadkfl';
93+
fetchMock.get(`glob:*/api/v1/sqllab/permalink/${key}`, {
94+
label: 'test permalink',
95+
sql: 'SELECT * FROM test_table',
96+
dbId: 1,
97+
});
98+
pathStub.mockReturnValue(`/sqllab/p/${key}`);
99+
setup(store);
100+
await waitFor(() =>
101+
expect(
102+
fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
103+
).toHaveLength(1),
104+
);
105+
expect(replaceState).toHaveBeenCalledWith(
106+
expect.anything(),
107+
expect.anything(),
108+
'/sqllab',
109+
);
110+
fetchMock.reset();
73111
});
74112
test('should handle savedQueryId', () => {
75113
uriStub.mockReturnValue({ savedQueryId: 1 });

0 commit comments

Comments
 (0)