Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce hashids permalink keys #19324

Merged
merged 10 commits into from
Mar 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in conf

### Deprecations

- [19078](https://github.com/apache/superset/pull/19078): Creation of old shorturl links has been deprecated in favor of a new permalink feature that solves the long url problem (old shorturls will still work, though!). By default, new permalinks use UUID4 as the key. However, to use serial ids similar to the old shorturls, add the following to your `superset_config.py`: `PERMALINK_KEY_TYPE = "id"`.
- [18960](https://github.com/apache/superset/pull/18960): Persisting URL params in chart metadata is no longer supported. To set a default value for URL params in Jinja code, use the optional second argument: `url_param("my-param", "my-default-value")`.

### Other
Expand Down
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ graphlib-backport==1.0.3
# via apache-superset
gunicorn==20.1.0
# via apache-superset
hashids==1.3.1
# via apache-superset
holidays==0.10.3
# via apache-superset
humanize==3.11.0
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def get_git_sha() -> str:
"geopy",
"graphlib-backport",
"gunicorn>=20.1.0",
"hashids>=1.3.1, <2",
"holidays==0.10.3", # PINNED! https://github.com/dr-prodigy/python-holidays/issues/406
"humanize",
"isodate",
Expand Down
3 changes: 0 additions & 3 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@

from superset.constants import CHANGE_ME_SECRET_KEY
from superset.jinja_context import BaseTemplateProcessor
from superset.key_value.types import KeyType
from superset.stats_logger import DummyStatsLogger
from superset.superset_typing import CacheConfig
from superset.utils.core import is_test, parse_boolean_string
Expand Down Expand Up @@ -600,8 +599,6 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]:
# store cache keys by datasource UID (via CacheKey) for custom processing/invalidation
STORE_CACHE_KEYS_IN_METADATA_DB = False

PERMALINK_KEY_TYPE: KeyType = "uuid"

# CORS Options
ENABLE_CORS = False
CORS_OPTIONS: Dict[Any, Any] = {}
Expand Down
3 changes: 2 additions & 1 deletion superset/dashboards/filter_state/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@

from superset.dashboards.dao import DashboardDAO
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.create import CreateTemporaryCacheCommand
from superset.temporary_cache.commands.entry import Entry
from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.utils import cache_key, random_key
from superset.temporary_cache.utils import cache_key


class CreateFilterStateCommand(CreateTemporaryCacheCommand):
Expand Down
3 changes: 2 additions & 1 deletion superset/dashboards/filter_state/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@

from superset.dashboards.dao import DashboardDAO
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.entry import Entry
from superset.temporary_cache.commands.exceptions import TemporaryCacheAccessDeniedError
from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.commands.update import UpdateTemporaryCacheCommand
from superset.temporary_cache.utils import cache_key, random_key
from superset.temporary_cache.utils import cache_key


class UpdateFilterStateCommand(UpdateTemporaryCacheCommand):
Expand Down
10 changes: 3 additions & 7 deletions superset/dashboards/permalink/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# under the License.
import logging

from flask import current_app, g, request, Response
from flask import g, request, Response
from flask_appbuilder.api import BaseApi, expose, protect, safe
from marshmallow import ValidationError

Expand Down Expand Up @@ -101,11 +101,10 @@ def post(self, pk: str) -> Response:
500:
$ref: '#/components/responses/500'
"""
key_type = current_app.config["PERMALINK_KEY_TYPE"]
try:
state = self.add_model_schema.load(request.json)
key = CreateDashboardPermalinkCommand(
actor=g.user, dashboard_id=pk, state=state, key_type=key_type,
actor=g.user, dashboard_id=pk, state=state,
).run()
http_origin = request.headers.environ.get("HTTP_ORIGIN")
url = f"{http_origin}/superset/dashboard/p/{key}/"
Expand Down Expand Up @@ -158,10 +157,7 @@ def get(self, key: str) -> Response:
$ref: '#/components/responses/500'
"""
try:
key_type = current_app.config["PERMALINK_KEY_TYPE"]
value = GetDashboardPermalinkCommand(
actor=g.user, key=key, key_type=key_type
).run()
value = GetDashboardPermalinkCommand(actor=g.user, key=key).run()
if not value:
return self.response_404()
return self.response(200, **value)
Expand Down
8 changes: 7 additions & 1 deletion superset/dashboards/permalink/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
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, SharedKey


class BaseDashboardPermalinkCommand(BaseCommand, ABC):
resource = "dashboard_permalink"
resource = KeyValueResource.DASHBOARD_PERMALINK

@property
def salt(self) -> str:
return get_permalink_salt(SharedKey.DASHBOARD_PERMALINK_SALT)
17 changes: 5 additions & 12 deletions superset/dashboards/permalink/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,18 @@
from superset.dashboards.permalink.exceptions import DashboardPermalinkCreateFailedError
from superset.dashboards.permalink.types import DashboardPermalinkState
from superset.key_value.commands.create import CreateKeyValueCommand
from superset.key_value.types import KeyType
from superset.key_value.utils import encode_permalink_key

logger = logging.getLogger(__name__)


class CreateDashboardPermalinkCommand(BaseDashboardPermalinkCommand):
def __init__(
self,
actor: User,
dashboard_id: str,
state: DashboardPermalinkState,
key_type: KeyType,
self, actor: User, dashboard_id: str, state: DashboardPermalinkState,
):
self.actor = actor
self.dashboard_id = dashboard_id
self.state = state
self.key_type = key_type

def run(self) -> str:
self.validate()
Expand All @@ -50,12 +45,10 @@ def run(self) -> str:
"dashboardId": self.dashboard_id,
"state": self.state,
}
return CreateKeyValueCommand(
actor=self.actor,
resource=self.resource,
value=value,
key_type=self.key_type,
key = CreateKeyValueCommand(
actor=self.actor, resource=self.resource, value=value,
).run()
return encode_permalink_key(key=key.id, salt=self.salt)
except SQLAlchemyError as ex:
logger.exception("Error running create command")
raise DashboardPermalinkCreateFailedError() from ex
Expand Down
12 changes: 4 additions & 8 deletions superset/dashboards/permalink/commands/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,21 @@
from superset.dashboards.permalink.types import DashboardPermalinkValue
from superset.key_value.commands.get import GetKeyValueCommand
from superset.key_value.exceptions import KeyValueGetFailedError, KeyValueParseKeyError
from superset.key_value.types import KeyType
from superset.key_value.utils import decode_permalink_id

logger = logging.getLogger(__name__)


class GetDashboardPermalinkCommand(BaseDashboardPermalinkCommand):
def __init__(
self, actor: User, key: str, key_type: KeyType,
):
def __init__(self, actor: User, key: str):
self.actor = actor
self.key = key
self.key_type = key_type

def run(self) -> Optional[DashboardPermalinkValue]:
self.validate()
try:
command = GetKeyValueCommand(
resource=self.resource, key=self.key, key_type=self.key_type
)
key = decode_permalink_id(self.key, salt=self.salt)
command = GetKeyValueCommand(resource=self.resource, key=key)
value: Optional[DashboardPermalinkValue] = command.run()
if value:
DashboardDAO.get_by_id_or_slug(value["dashboardId"])
Expand Down
2 changes: 1 addition & 1 deletion superset/dashboards/permalink/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

class DashboardPermalinkPostSchema(Schema):
filterState = fields.Dict(
required=True, allow_none=False, description="Native filter state",
required=False, allow_none=True, description="Native filter state",
)
urlParams = fields.List(
fields.Tuple(
Expand Down
2 changes: 1 addition & 1 deletion superset/dashboards/permalink/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


class DashboardPermalinkState(TypedDict):
filterState: Dict[str, Any]
filterState: Optional[Dict[str, Any]]
hash: Optional[str]
urlParams: Optional[List[Tuple[str, str]]]

Expand Down
3 changes: 2 additions & 1 deletion superset/explore/form_data/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.explore.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.exceptions import TemporaryCacheCreateFailedError
from superset.temporary_cache.utils import cache_key, random_key
from superset.temporary_cache.utils import cache_key
from superset.utils.schema import validate_json

logger = logging.getLogger(__name__)
Expand Down
3 changes: 2 additions & 1 deletion superset/explore/form_data/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.explore.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheUpdateFailedError,
)
from superset.temporary_cache.utils import cache_key, random_key
from superset.temporary_cache.utils import cache_key
from superset.utils.schema import validate_json

logger = logging.getLogger(__name__)
Expand Down
12 changes: 3 additions & 9 deletions superset/explore/permalink/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# under the License.
import logging

from flask import current_app, g, request, Response
from flask import g, request, Response
from flask_appbuilder.api import BaseApi, expose, protect, safe
from marshmallow import ValidationError

Expand Down Expand Up @@ -98,12 +98,9 @@ def post(self) -> Response:
500:
$ref: '#/components/responses/500'
"""
key_type = current_app.config["PERMALINK_KEY_TYPE"]
try:
state = self.add_model_schema.load(request.json)
key = CreateExplorePermalinkCommand(
actor=g.user, state=state, key_type=key_type,
).run()
key = CreateExplorePermalinkCommand(actor=g.user, state=state).run()
http_origin = request.headers.environ.get("HTTP_ORIGIN")
url = f"{http_origin}/superset/explore/p/{key}/"
return self.response(201, key=key, url=url)
Expand Down Expand Up @@ -159,10 +156,7 @@ def get(self, key: str) -> Response:
$ref: '#/components/responses/500'
"""
try:
key_type = current_app.config["PERMALINK_KEY_TYPE"]
value = GetExplorePermalinkCommand(
actor=g.user, key=key, key_type=key_type
).run()
value = GetExplorePermalinkCommand(actor=g.user, key=key).run()
if not value:
return self.response_404()
return self.response(200, **value)
Expand Down
8 changes: 7 additions & 1 deletion superset/explore/permalink/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
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, SharedKey


class BaseExplorePermalinkCommand(BaseCommand, ABC):
resource = "explore_permalink"
resource: KeyValueResource = KeyValueResource.EXPLORE_PERMALINK

@property
def salt(self) -> str:
return get_permalink_salt(SharedKey.EXPLORE_PERMALINK_SALT)
13 changes: 5 additions & 8 deletions superset/explore/permalink/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,17 @@
from superset.explore.permalink.exceptions import ExplorePermalinkCreateFailedError
from superset.explore.utils import check_access
from superset.key_value.commands.create import CreateKeyValueCommand
from superset.key_value.types import KeyType
from superset.key_value.utils import encode_permalink_key

logger = logging.getLogger(__name__)


class CreateExplorePermalinkCommand(BaseExplorePermalinkCommand):
def __init__(self, actor: User, state: Dict[str, Any], key_type: KeyType):
def __init__(self, actor: User, state: Dict[str, Any]):
self.actor = actor
self.chart_id: Optional[int] = state["formData"].get("slice_id")
self.datasource: str = state["formData"]["datasource"]
self.state = state
self.key_type = key_type

def run(self) -> str:
self.validate()
Expand All @@ -49,12 +48,10 @@ def run(self) -> str:
"state": self.state,
}
command = CreateKeyValueCommand(
actor=self.actor,
resource=self.resource,
value=value,
key_type=self.key_type,
actor=self.actor, resource=self.resource, value=value,
)
return command.run()
key = command.run()
return encode_permalink_key(key=key.id, salt=self.salt)
except SQLAlchemyError as ex:
logger.exception("Error running create command")
raise ExplorePermalinkCreateFailedError() from ex
Expand Down
10 changes: 4 additions & 6 deletions superset/explore/permalink/commands/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,22 @@
from superset.explore.utils import check_access
from superset.key_value.commands.get import GetKeyValueCommand
from superset.key_value.exceptions import KeyValueGetFailedError, KeyValueParseKeyError
from superset.key_value.types import KeyType
from superset.key_value.utils import decode_permalink_id

logger = logging.getLogger(__name__)


class GetExplorePermalinkCommand(BaseExplorePermalinkCommand):
def __init__(
self, actor: User, key: str, key_type: KeyType,
):
def __init__(self, actor: User, key: str):
self.actor = actor
self.key = key
self.key_type = key_type

def run(self) -> Optional[ExplorePermalinkValue]:
self.validate()
try:
key = decode_permalink_id(self.key, salt=self.salt)
value: Optional[ExplorePermalinkValue] = GetKeyValueCommand(
resource=self.resource, key=self.key, key_type=self.key_type
resource=self.resource, key=key,
).run()
if value:
chart_id: Optional[int] = value.get("chartId")
Expand Down