mirror of https://github.com/apache/superset.git
refactor(sqllab): migrate share queries via kv by permalink
This commit is contained in:
parent
fc9bc175e6
commit
836a60ccc2
|
@ -1185,6 +1185,26 @@ export function persistEditorHeight(queryEditor, northPercent, southPercent) {
|
|||
};
|
||||
}
|
||||
|
||||
export function popPermalink(key) {
|
||||
return function (dispatch) {
|
||||
return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/${key}` })
|
||||
.then(({ json }) =>
|
||||
dispatch(
|
||||
addQueryEditor({
|
||||
name: json.name ? json.name : t('Shared query'),
|
||||
dbId: json.dbId ? parseInt(json.dbId, 10) : null,
|
||||
catalog: json.catalog ? json.catalog : null,
|
||||
schema: json.schema ? json.schema : null,
|
||||
autorun: json.autorun ? json.autorun : false,
|
||||
sql: json.sql ? json.sql : 'SELECT ...',
|
||||
templateParams: json.templateParams,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
|
||||
};
|
||||
}
|
||||
|
||||
export function popStoredQuery(urlId) {
|
||||
return function (dispatch) {
|
||||
return SupersetClient.get({ endpoint: `/kv/${urlId}` })
|
||||
|
|
|
@ -23,7 +23,7 @@ import fetchMock from 'fetch-mock';
|
|||
import * as uiCore from '@superset-ui/core';
|
||||
import { Provider } from 'react-redux';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import { render, screen, act, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as utils from 'src/utils/common';
|
||||
|
@ -92,20 +92,24 @@ const standardProviderWithUnsaved: FC = ({ children }) => (
|
|||
);
|
||||
|
||||
describe('ShareSqlLabQuery', () => {
|
||||
const storeQueryUrl = 'glob:*/kv/store/';
|
||||
const storeQueryMockId = '123';
|
||||
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
|
||||
const storeQueryMockId = 'ci39c3';
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.post(storeQueryUrl, () => ({ id: storeQueryMockId }), {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.post(
|
||||
storeQueryUrl,
|
||||
() => ({ key: storeQueryMockId, url: `/p/${storeQueryMockId}` }),
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.resetHistory();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
describe('via /kv/store', () => {
|
||||
describe('via permalink api', () => {
|
||||
beforeAll(() => {
|
||||
isFeatureEnabledMock = jest
|
||||
.spyOn(uiCore, 'isFeatureEnabled')
|
||||
|
@ -124,11 +128,13 @@ describe('ShareSqlLabQuery', () => {
|
|||
});
|
||||
const button = screen.getByRole('button');
|
||||
const { id, remoteId, ...expected } = mockQueryEditor;
|
||||
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
|
||||
userEvent.click(button);
|
||||
expect(storeQuerySpy.mock.calls).toHaveLength(1);
|
||||
expect(storeQuerySpy).toBeCalledWith(expected);
|
||||
storeQuerySpy.mockRestore();
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
|
||||
);
|
||||
expect(
|
||||
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it('calls storeQuery() with unsaved changes', async () => {
|
||||
|
@ -139,11 +145,13 @@ describe('ShareSqlLabQuery', () => {
|
|||
});
|
||||
const button = screen.getByRole('button');
|
||||
const { id, ...expected } = unsavedQueryEditor;
|
||||
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
|
||||
userEvent.click(button);
|
||||
expect(storeQuerySpy.mock.calls).toHaveLength(1);
|
||||
expect(storeQuerySpy).toBeCalledWith(expected);
|
||||
storeQuerySpy.mockRestore();
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
|
||||
);
|
||||
expect(
|
||||
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -23,12 +23,12 @@ import {
|
|||
useTheme,
|
||||
isFeatureEnabled,
|
||||
getClientErrorObject,
|
||||
SupersetClient,
|
||||
} from '@superset-ui/core';
|
||||
import Button from 'src/components/Button';
|
||||
import Icons from 'src/components/Icons';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import { storeQuery } from 'src/utils/common';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
|
||||
interface ShareSqlLabQueryProps {
|
||||
|
@ -63,12 +63,16 @@ const ShareSqlLabQuery = ({
|
|||
'templateParams',
|
||||
]);
|
||||
|
||||
const getCopyUrlForKvStore = (callback: Function) => {
|
||||
const getCopyUrlForPermalink = (callback: Function) => {
|
||||
const sharedQuery = { dbId, name, schema, autorun, sql, templateParams };
|
||||
|
||||
return storeQuery(sharedQuery)
|
||||
.then(shortUrl => {
|
||||
callback(shortUrl);
|
||||
return SupersetClient.post({
|
||||
endpoint: '/api/v1/sqllab/permalink',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(sharedQuery),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
callback(json.url);
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(() => {
|
||||
|
@ -92,7 +96,7 @@ const ShareSqlLabQuery = ({
|
|||
};
|
||||
const getCopyUrl = (callback: Function) => {
|
||||
if (isFeatureEnabled(FeatureFlag.ShareQueriesViaKvStore)) {
|
||||
return getCopyUrlForKvStore(callback);
|
||||
return getCopyUrlForPermalink(callback);
|
||||
}
|
||||
return getCopyUrlForSavedQuery(callback);
|
||||
};
|
||||
|
|
|
@ -71,6 +71,27 @@ describe('componentDidMount', () => {
|
|||
'/sqllab',
|
||||
);
|
||||
});
|
||||
test('should handle permalink', async () => {
|
||||
const key = '9sadkfl';
|
||||
fetchMock.get(`glob:*/api/v1/sqllab/permalink/${key}`, {
|
||||
label: 'test permalink',
|
||||
sql: 'SELECT * FROM test_table',
|
||||
dbId: 1,
|
||||
});
|
||||
uriStub.mockReturnValue({ p: key });
|
||||
setup(store);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
|
||||
).toHaveLength(1),
|
||||
);
|
||||
expect(replaceState).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'/sqllab',
|
||||
);
|
||||
fetchMock.reset();
|
||||
});
|
||||
test('should handle savedQueryId', () => {
|
||||
uriStub.mockReturnValue({ savedQueryId: 1 });
|
||||
setup(store);
|
||||
|
|
|
@ -79,6 +79,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
p,
|
||||
sql,
|
||||
savedQueryId,
|
||||
datasourceKey,
|
||||
|
@ -97,8 +98,10 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
|
|||
} as Record<string, string>;
|
||||
|
||||
// Popping a new tab based on the querystring
|
||||
if (id || sql || savedQueryId || datasourceKey || queryId) {
|
||||
if (id) {
|
||||
if (p || id || sql || savedQueryId || datasourceKey || queryId) {
|
||||
if (p) {
|
||||
this.props.actions.popPermalink(p);
|
||||
} else if (id) {
|
||||
this.props.actions.popStoredQuery(id);
|
||||
} else if (savedQueryId) {
|
||||
this.props.actions.popSavedQuery(savedQueryId);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1,35 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from abc import ABC
|
||||
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.key_value.shared_entries import get_permalink_salt
|
||||
from superset.key_value.types import (
|
||||
KeyValueResource,
|
||||
MarshmallowKeyValueCodec,
|
||||
SharedKey,
|
||||
)
|
||||
from superset.sqllab.permalink.schemas import SqlLabPermalinkSchema
|
||||
|
||||
|
||||
class BaseSqlLabPermalinkCommand(BaseCommand, ABC):
|
||||
resource: KeyValueResource = KeyValueResource.SQLLAB_PERMALINK
|
||||
codec = MarshmallowKeyValueCodec(SqlLabPermalinkSchema())
|
||||
|
||||
@property
|
||||
def salt(self) -> str:
|
||||
return get_permalink_salt(SharedKey.SQLLAB_PERMALINK_SALT)
|
|
@ -0,0 +1,49 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from superset.commands.key_value.create import CreateKeyValueCommand
|
||||
from superset.commands.sql_lab.permalink.base import BaseSqlLabPermalinkCommand
|
||||
from superset.key_value.exceptions import KeyValueCodecEncodeException
|
||||
from superset.key_value.utils import encode_permalink_key
|
||||
from superset.sqllab.permalink.exceptions import SqlLabPermalinkCreateFailedError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateSqlLabPermalinkCommand(BaseSqlLabPermalinkCommand):
|
||||
def __init__(self, state: dict[str, Any]):
|
||||
self._properties = state.copy()
|
||||
|
||||
def run(self) -> str:
|
||||
self.validate()
|
||||
try:
|
||||
command = CreateKeyValueCommand(
|
||||
resource=self.resource,
|
||||
value=self._properties,
|
||||
codec=self.codec,
|
||||
)
|
||||
key = command.run()
|
||||
if key.id is None:
|
||||
raise SqlLabPermalinkCreateFailedError("Unexpected missing key id")
|
||||
return encode_permalink_key(key=key.id, salt=self.salt)
|
||||
except KeyValueCodecEncodeException as ex:
|
||||
raise SqlLabPermalinkCreateFailedError(str(ex)) from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
pass
|
|
@ -0,0 +1,58 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from superset.commands.key_value.get import GetKeyValueCommand
|
||||
from superset.commands.sql_lab.permalink.base import BaseSqlLabPermalinkCommand
|
||||
from superset.key_value.exceptions import (
|
||||
KeyValueCodecDecodeException,
|
||||
KeyValueGetFailedError,
|
||||
KeyValueParseKeyError,
|
||||
)
|
||||
from superset.key_value.utils import decode_permalink_id
|
||||
from superset.sqllab.permalink.exceptions import SqlLabPermalinkGetFailedError
|
||||
from superset.sqllab.permalink.types import SqlLabPermalinkValue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GetSqlLabPermalinkCommand(BaseSqlLabPermalinkCommand):
|
||||
def __init__(self, key: str):
|
||||
self.key = key
|
||||
|
||||
def run(self) -> Optional[SqlLabPermalinkValue]:
|
||||
self.validate()
|
||||
try:
|
||||
key = decode_permalink_id(self.key, salt=self.salt)
|
||||
value: Optional[SqlLabPermalinkValue] = GetKeyValueCommand(
|
||||
resource=self.resource,
|
||||
key=key,
|
||||
codec=self.codec,
|
||||
).run()
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
except (
|
||||
KeyValueCodecDecodeException,
|
||||
KeyValueGetFailedError,
|
||||
KeyValueParseKeyError,
|
||||
) as ex:
|
||||
raise SqlLabPermalinkGetFailedError(message=ex.message) from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
pass
|
|
@ -154,6 +154,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||
from superset.row_level_security.api import RLSRestApi
|
||||
from superset.security.api import SecurityRestApi
|
||||
from superset.sqllab.api import SqlLabRestApi
|
||||
from superset.sqllab.permalink.api import SqlLabPermalinkRestApi
|
||||
from superset.tags.api import TagRestApi
|
||||
from superset.views.alerts import AlertView, ReportView
|
||||
from superset.views.all_entities import TaggedObjectsModelView
|
||||
|
@ -221,6 +222,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||
appbuilder.add_api(SavedQueryRestApi)
|
||||
appbuilder.add_api(TagRestApi)
|
||||
appbuilder.add_api(SqlLabRestApi)
|
||||
appbuilder.add_api(SqlLabPermalinkRestApi)
|
||||
#
|
||||
# Setup regular views
|
||||
#
|
||||
|
|
|
@ -50,11 +50,13 @@ class KeyValueResource(StrEnum):
|
|||
EXPLORE_PERMALINK = "explore_permalink"
|
||||
METASTORE_CACHE = "superset_metastore_cache"
|
||||
LOCK = "lock"
|
||||
SQLLAB_PERMALINK = "sqllab_permalink"
|
||||
|
||||
|
||||
class SharedKey(StrEnum):
|
||||
DASHBOARD_PERMALINK_SALT = "dashboard_permalink_salt"
|
||||
EXPLORE_PERMALINK_SALT = "explore_permalink_salt"
|
||||
SQLLAB_PERMALINK_SALT = "sqllab_permalink_salt"
|
||||
|
||||
|
||||
class KeyValueCodec(ABC):
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1,144 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
|
||||
from flask import request, Response
|
||||
from flask_appbuilder.api import expose, protect, safe
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from superset.commands.sql_lab.permalink.create import CreateSqlLabPermalinkCommand
|
||||
from superset.commands.sql_lab.permalink.get import GetSqlLabPermalinkCommand
|
||||
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||
from superset.extensions import event_logger
|
||||
from superset.key_value.exceptions import KeyValueAccessDeniedError
|
||||
from superset.sqllab.permalink.exceptions import SqlLabPermalinkInvalidStateError
|
||||
from superset.sqllab.permalink.schemas import SqlLabPermalinkSchema
|
||||
from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SqlLabPermalinkRestApi(BaseSupersetApi):
|
||||
add_model_schema = SqlLabPermalinkSchema()
|
||||
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||
allow_browser_login = True
|
||||
class_permission_name = "SqlLabPermalinkRestApi"
|
||||
resource_name = "sqllab"
|
||||
openapi_spec_tag = "SQL Lab Permanent Link"
|
||||
openapi_spec_component_schemas = (SqlLabPermalinkSchema,)
|
||||
|
||||
@expose("/permalink", methods=("POST",))
|
||||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(
|
||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
|
||||
log_to_statsd=False,
|
||||
)
|
||||
@requires_json
|
||||
def post(self) -> Response:
|
||||
"""Create a new permanent link for SQL Lab editor
|
||||
---
|
||||
post:
|
||||
summary: Create a new permanent link
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExplorePermalinkStateSchema'
|
||||
responses:
|
||||
201:
|
||||
description: The permanent link was stored successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: The key to retrieve the permanent link data.
|
||||
url:
|
||||
type: string
|
||||
description: permanent link.
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
422:
|
||||
$ref: '#/components/responses/422'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
state = self.add_model_schema.load(request.json)
|
||||
key = CreateSqlLabPermalinkCommand(state=state).run()
|
||||
http_origin = request.headers.environ.get("HTTP_ORIGIN")
|
||||
url = f"{http_origin}/sqllab?p={key}"
|
||||
return self.response(201, key=key, url=url)
|
||||
except ValidationError as ex:
|
||||
return self.response(400, message=ex.messages)
|
||||
except KeyValueAccessDeniedError as ex:
|
||||
return self.response(403, message=str(ex))
|
||||
|
||||
@expose("/permalink/<string:key>", methods=("GET",))
|
||||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(
|
||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
|
||||
log_to_statsd=False,
|
||||
)
|
||||
def get(self, key: str) -> Response:
|
||||
"""Get chart's permanent link state.
|
||||
---
|
||||
get:
|
||||
summary: Get chart's permanent link state
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
type: string
|
||||
name: key
|
||||
responses:
|
||||
200:
|
||||
description: Returns the stored form_data.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
type: object
|
||||
description: The stored state
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
404:
|
||||
$ref: '#/components/responses/404'
|
||||
422:
|
||||
$ref: '#/components/responses/422'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
value = GetSqlLabPermalinkCommand(key=key).run()
|
||||
if not value:
|
||||
return self.response_404()
|
||||
return self.response(200, **value)
|
||||
except SqlLabPermalinkInvalidStateError as ex:
|
||||
return self.response(400, message=str(ex))
|
|
@ -0,0 +1,31 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
from superset.commands.exceptions import CommandException, CreateFailedError
|
||||
|
||||
|
||||
class SqlLabPermalinkInvalidStateError(CreateFailedError):
|
||||
message = _("Invalid state.")
|
||||
|
||||
|
||||
class SqlLabPermalinkCreateFailedError(CreateFailedError):
|
||||
message = _("An error occurred while creating the copy link.")
|
||||
|
||||
|
||||
class SqlLabPermalinkGetFailedError(CommandException):
|
||||
message = _("An error occurred while accessing the copy link.")
|
|
@ -0,0 +1,46 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from marshmallow import fields, Schema
|
||||
|
||||
|
||||
class SqlLabPermalinkSchema(Schema):
|
||||
autorun = fields.Boolean()
|
||||
dbId = fields.Integer(
|
||||
required=True,
|
||||
allow_none=False,
|
||||
metadata={"description": "The id of the database"},
|
||||
)
|
||||
name = fields.String(
|
||||
required=True,
|
||||
allow_none=False,
|
||||
metadata={"description": "The label of the editor tab"},
|
||||
)
|
||||
schema = fields.String(
|
||||
required=False,
|
||||
allow_none=True,
|
||||
metadata={"description": "The schema name of the query"},
|
||||
)
|
||||
sql = fields.String(
|
||||
required=True,
|
||||
allow_none=False,
|
||||
metadata={"description": "SQL query text"},
|
||||
)
|
||||
templateParams = fields.String(
|
||||
required=False,
|
||||
allow_none=True,
|
||||
metadata={"description": "stringfied JSON string for template parameters"},
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from typing import Optional, TypedDict
|
||||
|
||||
|
||||
class SqlLabPermalinkValue(TypedDict):
|
||||
dbId: int
|
||||
name: str
|
||||
schema: Optional[str]
|
||||
sql: str
|
||||
autorun: bool
|
||||
templateParams: Optional[str]
|
|
@ -0,0 +1,16 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1,69 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import json
|
||||
|
||||
from superset import db
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
from tests.integration_tests.base_tests import SupersetTestCase
|
||||
from tests.integration_tests.constants import (
|
||||
GAMMA_SQLLAB_USERNAME,
|
||||
)
|
||||
|
||||
|
||||
class TestSqlLabPermalinkApi(SupersetTestCase):
|
||||
data = {
|
||||
"dbId": 1,
|
||||
"name": "Untitled Query 1",
|
||||
"schema": "main",
|
||||
"sql": "SELECT 'foo' as bar",
|
||||
"autorun": False,
|
||||
"templateParams": '{"param1": "value1"}',
|
||||
}
|
||||
|
||||
def test_post(self):
|
||||
self.login(GAMMA_SQLLAB_USERNAME)
|
||||
resp = self.client.post("api/v1/sqllab/permalink", json=self.data)
|
||||
assert resp.status_code == 201
|
||||
data = resp.json
|
||||
key = data["key"]
|
||||
url = data["url"]
|
||||
assert key in url
|
||||
db.session.query(KeyValueEntry).filter_by(id=key).delete()
|
||||
db.session.commit()
|
||||
|
||||
def test_post_access_denied(self):
|
||||
resp = self.client.post("api/v1/sqllab/permalink", json=self.data)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_post_invalid_schema(self):
|
||||
self.login(GAMMA_SQLLAB_USERNAME)
|
||||
resp = self.client.post(
|
||||
"api/v1/sqllab/permalink", json={"name": "Untitled Query 1", "sql": "Test"}
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_get(self):
|
||||
self.login(GAMMA_SQLLAB_USERNAME)
|
||||
resp = self.client.post("api/v1/sqllab/permalink", json=self.data)
|
||||
data = resp.json
|
||||
key = data["key"]
|
||||
resp = self.client.get(f"api/v1/sqllab/permalink/{key}")
|
||||
assert resp.status_code == 200
|
||||
result = json.loads(resp.data.decode("utf-8"))
|
||||
assert result == self.data
|
||||
db.session.query(KeyValueEntry).filter_by(id=key).delete()
|
||||
db.session.commit()
|
Loading…
Reference in New Issue