mirror of
https://github.com/apache/superset.git
synced 2026-06-28 10:55:36 +00:00
Compare commits
1 Commits
fix/scarf-
...
fix-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57b93055b6 |
@@ -223,9 +223,8 @@ compose based installation, edit the `x-superset-image:` line in your `docker-co
|
||||
`docker-compose-non-dev.yml` files, replacing `apachesuperset.docker.scarf.sh/apache/superset` with
|
||||
`apache/superset` to pull the image directly from Docker Hub.
|
||||
|
||||
To disable the Scarf telemetry pixel, set the `SCARF_ANALYTICS` environment variable to `false` in
|
||||
your `docker/.env` file. This is read at runtime, so it disables the pixel on the pre-built image
|
||||
without rebuilding the frontend.
|
||||
To disable the Scarf telemetry pixel, set the `SCARF_ANALYTICS` environment variable to `False` in
|
||||
your terminal and/or in your `docker/.env` file.
|
||||
:::
|
||||
|
||||
## 3. Log in to Superset
|
||||
|
||||
@@ -136,17 +136,7 @@ init:
|
||||
:::note
|
||||
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
|
||||
|
||||
There are two independent telemetry channels:
|
||||
|
||||
- **Image pulls** (Scarf Gateway): to opt out, edit the `repository:` line in your `helm/superset/values.yaml` file, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub.
|
||||
- **The analytics pixel** rendered in the UI: to opt out, set the `SCARF_ANALYTICS` environment variable to `false` on the Superset containers via `extraEnv` in your `values.yaml`:
|
||||
|
||||
```yaml
|
||||
extraEnv:
|
||||
SCARF_ANALYTICS: "false"
|
||||
```
|
||||
|
||||
This is read at runtime, so it takes effect on the pre-built images without rebuilding the frontend.
|
||||
To opt-out of this data collection in your Helm-based installation, edit the `repository:` line in your `helm/superset/values.yaml` file, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub.
|
||||
:::
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -321,8 +321,8 @@ This can be used, for example, to convert UTC time to local time.
|
||||
Superset uses [Scarf](https://about.scarf.sh/) by default to collect basic telemetry data upon installing and/or running Superset. This data helps the maintainers of Superset better understand which versions of Superset are being used, in order to prioritize patch/minor releases and security fixes.
|
||||
We use the [Scarf Gateway](https://docs.scarf.sh/gateway/) to sit in front of container registries, the [scarf-js](https://about.scarf.sh/package-sdks) package to track `npm` installations, and a Scarf pixel to gather anonymous analytics on Superset page views.
|
||||
Scarf purges PII and provides aggregated statistics. Superset users can easily opt out of analytics in various ways documented [here](https://docs.scarf.sh/gateway/#do-not-track) and [here](https://docs.scarf.sh/package-analytics/#as-a-user-of-a-package-using-scarf-js-how-can-i-opt-out-of-analytics).
|
||||
You can also opt out of the analytics pixel by setting the `SCARF_ANALYTICS` environment variable to `false`. This is read at runtime, so setting it on the Superset container (for example via `extraEnv` in the Helm chart, or `docker/.env` for Docker Compose) disables the pixel on the pre-built images without rebuilding the frontend.
|
||||
Additional opt-out instructions are available on the [Docker Compose](/admin-docs/installation/docker-compose) and [Kubernetes](/admin-docs/installation/kubernetes) installation pages.
|
||||
Superset maintainers can also opt out of telemetry data collection by setting the `SCARF_ANALYTICS` environment variable to `false` in the Superset container (or anywhere Superset/webpack are run).
|
||||
Additional opt-out instructions for Docker users are available on the [Docker Installation](/admin-docs/installation/docker-compose) page.
|
||||
|
||||
## Does Superset have an archive panel or trash bin from which a user can recover deleted assets?
|
||||
|
||||
|
||||
@@ -46,19 +46,3 @@ test('should NOT render the pixel link when FF is off', () => {
|
||||
const image = document.querySelector('img[src*="scarf.sh"]');
|
||||
expect(image).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render the pixel link when disabled at runtime', () => {
|
||||
process.env.SCARF_ANALYTICS = 'true';
|
||||
render(<TelemetryPixel enabled={false} />);
|
||||
|
||||
const image = document.querySelector('img[src*="scarf.sh"]');
|
||||
expect(image).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the pixel link when enabled at runtime', () => {
|
||||
process.env.SCARF_ANALYTICS = 'true';
|
||||
render(<TelemetryPixel enabled />);
|
||||
|
||||
const image = document.querySelector('img[src*="scarf.sh"]');
|
||||
expect(image).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -23,25 +23,17 @@ interface TelemetryPixelProps {
|
||||
version?: string;
|
||||
sha?: string;
|
||||
build?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a telemetry pixel component to capture anonymous, aggregated telemetry via Scarf.
|
||||
*
|
||||
* Telemetry can be disabled in two ways:
|
||||
* - At build time, by setting the SCARF_ANALYTICS environment variable to `false`
|
||||
* (inlined by webpack; only effective when building the frontend yourself).
|
||||
* - At runtime, by passing `enabled={false}`, which the app derives from the
|
||||
* `SCARF_ANALYTICS` backend config exposed via the bootstrap payload. This is
|
||||
* what allows opting out in pre-built images, where the build-time flag is fixed.
|
||||
* This can be disabled by setting the SCARF_ANALYTICS environment variable to false.
|
||||
*
|
||||
* @component
|
||||
* @param {TelemetryPixelProps} props - The props for the TelemetryPixel component.
|
||||
* @param {string} props.version - The version of Superset that's currently in use.
|
||||
* @param {string} props.sha - The SHA of Superset that's currently in use.
|
||||
* @param {string} props.build - The build of Superset that's currently in use.
|
||||
* @param {boolean} props.enabled - Runtime opt-out switch; when false the pixel is not rendered.
|
||||
* @returns {JSX.Element | null} The rendered TelemetryPixel component.
|
||||
*/
|
||||
|
||||
@@ -51,11 +43,9 @@ export const TelemetryPixel = ({
|
||||
version = 'unknownVersion',
|
||||
sha = 'unknownSHA',
|
||||
build = 'unknownBuild',
|
||||
enabled = true,
|
||||
}: TelemetryPixelProps): ReactElement | null => {
|
||||
const pixelPath = `https://apachesuperset.gateway.scarf.sh/pixel/${PIXEL_ID}/${version}/${sha}/${build}`;
|
||||
const disabled = !enabled || process.env.SCARF_ANALYTICS === 'false';
|
||||
return disabled ? null : (
|
||||
return process.env.SCARF_ANALYTICS === 'false' ? null : (
|
||||
<img
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src={pixelPath}
|
||||
|
||||
@@ -227,7 +227,10 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
formData?.result_type === 'results');
|
||||
|
||||
if (isDownloadQuery) {
|
||||
moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
|
||||
moreProps.row_limit =
|
||||
formDataCopy.row_limit != null
|
||||
? Number(formDataCopy.row_limit)
|
||||
: undefined;
|
||||
moreProps.row_offset = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ const fakeTableApiResult = {
|
||||
result: [
|
||||
{
|
||||
id: 1,
|
||||
value: 'fake_api_result1',
|
||||
value: 'fake api result1',
|
||||
label: 'fake api label1',
|
||||
type: 'table',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
value: 'fake_api_result2',
|
||||
value: 'fake api result2',
|
||||
label: 'fake api label2',
|
||||
type: 'table',
|
||||
},
|
||||
@@ -152,64 +152,6 @@ test('returns keywords including fetched function_names data', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('quotes table identifiers that require quoting in the inserted value', async () => {
|
||||
const dbFunctionNamesApiRoute = `glob:*/api/v1/database/${expectDbId}/function_names/`;
|
||||
fetchMock.get(dbFunctionNamesApiRoute, fakeFunctionNamesApiResult);
|
||||
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
tableApiUtil.upsertQueryData(
|
||||
'tables',
|
||||
{ dbId: expectDbId, schema: expectSchema },
|
||||
{
|
||||
options: [
|
||||
{ value: 'COVID Vaccines', label: 'COVID Vaccines', type: 'table' },
|
||||
{ value: 'simple_table', label: 'simple_table', type: 'table' },
|
||||
],
|
||||
hasMore: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useKeywords({
|
||||
queryEditorId: 'testqueryid',
|
||||
dbId: expectDbId,
|
||||
schema: expectSchema,
|
||||
}),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
useRedux: true,
|
||||
store,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1),
|
||||
);
|
||||
|
||||
// A name that needs quoting is inserted as a double-quoted identifier,
|
||||
// while its display name stays human-readable.
|
||||
expect(result.current).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: 'COVID Vaccines',
|
||||
value: '"COVID Vaccines"',
|
||||
meta: 'table',
|
||||
}),
|
||||
);
|
||||
// A simple identifier is inserted as-is, without quotes.
|
||||
expect(result.current).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: 'simple_table',
|
||||
value: 'simple_table',
|
||||
meta: 'table',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('skip fetching if autocomplete skipped', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
|
||||
@@ -53,14 +53,6 @@ const getHelperText = (value: string) =>
|
||||
detail: value,
|
||||
};
|
||||
|
||||
// Names that aren't simple identifiers (spaces, punctuation, leading digits)
|
||||
// must be double-quoted to be valid SQL, with embedded quotes doubled.
|
||||
const SIMPLE_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
const quoteIdentifier = (identifier: string) =>
|
||||
SIMPLE_IDENTIFIER_RE.test(identifier)
|
||||
? identifier
|
||||
: `"${identifier.replace(/"/g, '""')}"`;
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
export function useKeywords(
|
||||
@@ -205,7 +197,7 @@ export function useKeywords(
|
||||
() =>
|
||||
allCachedTables.map(({ value, label, schema: tableSchema }) => ({
|
||||
name: label,
|
||||
value: quoteIdentifier(value),
|
||||
value,
|
||||
schema: tableSchema,
|
||||
score: TABLE_AUTOCOMPLETE_SCORE,
|
||||
meta: 'table',
|
||||
|
||||
@@ -562,7 +562,6 @@ const Chart = (props: ChartProps) => {
|
||||
exportFormData as unknown as import('@superset-ui/core').QueryFormData,
|
||||
resultType,
|
||||
resultFormat: format,
|
||||
force: true,
|
||||
ownState: exportOwnState,
|
||||
onStartStreamingExport: shouldUseStreaming
|
||||
? (exportParams: JsonObject) => {
|
||||
|
||||
@@ -134,7 +134,6 @@ const RightMenu = ({
|
||||
EXCEL_EXTENSIONS,
|
||||
ALLOWED_EXTENSIONS,
|
||||
HAS_GSHEETS_INSTALLED,
|
||||
SCARF_ANALYTICS,
|
||||
} = useSelector<any, ExtensionConfigs>(state => state.common.conf);
|
||||
const [showDatabaseModal, setShowDatabaseModal] = useState<boolean>(false);
|
||||
const [showCSVUploadModal, setShowCSVUploadModal] = useState<boolean>(false);
|
||||
@@ -782,7 +781,6 @@ const RightMenu = ({
|
||||
// segments in the Scarf pixel URL.
|
||||
sha={navbarRight.version_sha || undefined}
|
||||
build={navbarRight.build_number || undefined}
|
||||
enabled={SCARF_ANALYTICS !== false}
|
||||
/>
|
||||
</StyledDiv>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface ExtensionConfigs {
|
||||
COLUMNAR_EXTENSIONS: Array<any>;
|
||||
EXCEL_EXTENSIONS: Array<any>;
|
||||
HAS_GSHEETS_INSTALLED: boolean;
|
||||
SCARF_ANALYTICS?: boolean;
|
||||
}
|
||||
export interface RightMenuProps {
|
||||
align: 'flex-start' | 'flex-end';
|
||||
|
||||
@@ -100,10 +100,7 @@ class CacheRestApi(BaseSupersetModelRestApi):
|
||||
)
|
||||
cache_keys = [c.cache_key for c in cache_key_objs]
|
||||
if cache_key_objs:
|
||||
# Chart query results live in ``data_cache``, not the default
|
||||
# ``cache`` — using the wrong backend silently misses the Redis
|
||||
# keys when ``CACHE_KEY_PREFIX`` differs between the two configs.
|
||||
all_keys_deleted = cache_manager.data_cache.delete_many(*cache_keys)
|
||||
all_keys_deleted = cache_manager.cache.delete_many(*cache_keys)
|
||||
|
||||
if not all_keys_deleted:
|
||||
# expected behavior as keys may expire and cache is not a
|
||||
|
||||
@@ -2336,13 +2336,6 @@ DATABASE_OAUTH2_TIMEOUT = timedelta(seconds=30)
|
||||
# Enable/disable CSP warning
|
||||
CONTENT_SECURITY_POLICY_WARNING = True
|
||||
|
||||
# Superset uses Scarf (https://about.scarf.sh/) to collect anonymous, aggregated
|
||||
# telemetry via a pixel rendered in the UI. Set the SCARF_ANALYTICS environment
|
||||
# variable to "false" to opt out. This value is exposed to the frontend through
|
||||
# the bootstrap payload so it takes effect at runtime, including in pre-built
|
||||
# images where the webpack build-time flag of the same name cannot be changed.
|
||||
SCARF_ANALYTICS = utils.cast_to_boolean(os.environ.get("SCARF_ANALYTICS", True))
|
||||
|
||||
# Do you want Talisman enabled?
|
||||
TALISMAN_ENABLED = utils.cast_to_boolean(os.environ.get("TALISMAN_ENABLED", True))
|
||||
|
||||
|
||||
@@ -303,7 +303,6 @@ DEFAULT_GET_DASHBOARD_INFO_COLUMNS: List[str] = [
|
||||
"created_on",
|
||||
"changed_on",
|
||||
"uuid",
|
||||
"embedded_uuid",
|
||||
"url",
|
||||
"created_on_humanized",
|
||||
"changed_on_humanized",
|
||||
@@ -428,18 +427,6 @@ class DashboardInfo(BaseModel):
|
||||
created_on: str | datetime | None = None
|
||||
changed_on: str | datetime | None = None
|
||||
uuid: str | None = None
|
||||
embedded_uuid: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Embedded UUID for this dashboard. This is the UUID required when "
|
||||
"generating guest tokens for embedded dashboards "
|
||||
"(resources[].id in the guest token payload). "
|
||||
"Only present when the dashboard has been configured for embedding "
|
||||
"via the Embed Dashboard UI. Distinct from `uuid` (the internal "
|
||||
"dashboard UUID) — using the wrong one causes 403 errors in guest "
|
||||
"token validation."
|
||||
),
|
||||
)
|
||||
url: str | None = None
|
||||
created_on_humanized: str | None = None
|
||||
changed_on_humanized: str | None = None
|
||||
@@ -1365,9 +1352,6 @@ def dashboard_serializer(dashboard: "Dashboard") -> DashboardInfo:
|
||||
created_on=dashboard.created_on,
|
||||
changed_on=dashboard.changed_on,
|
||||
uuid=str(dashboard.uuid) if dashboard.uuid else None,
|
||||
embedded_uuid=str(dashboard.embedded[0].uuid)
|
||||
if dashboard.embedded
|
||||
else None,
|
||||
url=absolute_url,
|
||||
created_on_humanized=dashboard.created_on_humanized,
|
||||
changed_on_humanized=dashboard.changed_on_humanized,
|
||||
|
||||
@@ -155,11 +155,10 @@ async def get_dashboard_info(
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
|
||||
# Eager load slices, tags, and embedded to avoid N+1 queries.
|
||||
# Eager load slices and tags to avoid N+1 queries during serialization.
|
||||
eager_options = [
|
||||
subqueryload(Dashboard.slices).subqueryload(Slice.tags),
|
||||
subqueryload(Dashboard.tags),
|
||||
subqueryload(Dashboard.embedded),
|
||||
]
|
||||
|
||||
with event_logger.log_context(action="mcp.get_dashboard_info.lookup"):
|
||||
|
||||
@@ -125,7 +125,6 @@ FRONTEND_CONF_KEYS = (
|
||||
"MAPBOX_API_KEY",
|
||||
"DEFAULT_MAP_RENDERER",
|
||||
"CSV_STREAMING_ROW_THRESHOLD",
|
||||
"SCARF_ANALYTICS",
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"""Unit tests for Superset"""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -53,43 +52,17 @@ def test_invalidate_cache(invalidate):
|
||||
def test_invalidate_existing_cache(invalidate):
|
||||
db.session.add(CacheKey(cache_key="cache_key", datasource_uid="3__table"))
|
||||
db.session.commit()
|
||||
cache_manager.data_cache.set("cache_key", "value")
|
||||
cache_manager.cache.set("cache_key", "value")
|
||||
|
||||
rv = invalidate({"datasource_uids": ["3__table"]})
|
||||
|
||||
assert rv.status_code == 201
|
||||
assert cache_manager.data_cache.get("cache_key") is None # noqa: E711
|
||||
assert cache_manager.cache.get("cache_key") is None # noqa: E711
|
||||
assert (
|
||||
not db.session.query(CacheKey).filter(CacheKey.cache_key == "cache_key").first()
|
||||
)
|
||||
|
||||
|
||||
def test_invalidate_uses_data_cache_not_default_cache(invalidate):
|
||||
"""Regression test for #40489.
|
||||
|
||||
Chart query results are written through ``cache_manager.data_cache``
|
||||
(``DATA_CACHE_CONFIG``). When ``CACHE_CONFIG`` and ``DATA_CACHE_CONFIG``
|
||||
use distinct ``CACHE_KEY_PREFIX`` values, deleting via the default
|
||||
``cache_manager.cache`` silently misses the underlying Redis keys
|
||||
because flask-caching prepends the wrong prefix to the DEL call.
|
||||
"""
|
||||
db.session.add(CacheKey(cache_key="cache_key", datasource_uid="3__table"))
|
||||
db.session.commit()
|
||||
|
||||
with (
|
||||
patch.object(cache_manager.data_cache, "delete_many") as data_delete,
|
||||
patch.object(cache_manager.cache, "delete_many") as default_delete,
|
||||
):
|
||||
data_delete.return_value = True
|
||||
rv = invalidate({"datasource_uids": ["3__table"]})
|
||||
|
||||
assert rv.status_code == 201
|
||||
# Chart-data cache backend (the one that wrote the keys) must be hit.
|
||||
data_delete.assert_called_once_with("cache_key")
|
||||
# The default cache must NOT be touched — that's the #40489 regression.
|
||||
default_delete.assert_not_called()
|
||||
|
||||
|
||||
def test_invalidate_cache_empty_input(invalidate):
|
||||
rv = invalidate({"datasource_uids": []})
|
||||
assert rv.status_code == 201
|
||||
@@ -138,10 +111,10 @@ def test_invalidate_existing_caches(invalidate):
|
||||
db.session.add(CacheKey(cache_key="cache_keyX", datasource_uid="X__table"))
|
||||
db.session.commit()
|
||||
|
||||
cache_manager.data_cache.set("cache_key1", "value")
|
||||
cache_manager.data_cache.set("cache_key2", "value")
|
||||
cache_manager.data_cache.set("cache_key4", "value")
|
||||
cache_manager.data_cache.set("cache_keyX", "value")
|
||||
cache_manager.cache.set("cache_key1", "value")
|
||||
cache_manager.cache.set("cache_key2", "value")
|
||||
cache_manager.cache.set("cache_key4", "value")
|
||||
cache_manager.cache.set("cache_keyX", "value")
|
||||
|
||||
rv = invalidate(
|
||||
{
|
||||
@@ -182,10 +155,10 @@ def test_invalidate_existing_caches(invalidate):
|
||||
)
|
||||
|
||||
assert rv.status_code == 201
|
||||
assert cache_manager.data_cache.get("cache_key1") is None
|
||||
assert cache_manager.data_cache.get("cache_key2") is None
|
||||
assert cache_manager.data_cache.get("cache_key4") is None
|
||||
assert cache_manager.data_cache.get("cache_keyX") == "value"
|
||||
assert cache_manager.cache.get("cache_key1") is None
|
||||
assert cache_manager.cache.get("cache_key2") is None
|
||||
assert cache_manager.cache.get("cache_key4") is None
|
||||
assert cache_manager.cache.get("cache_keyX") == "value"
|
||||
assert (
|
||||
not db.session.query(CacheKey)
|
||||
.filter(CacheKey.cache_key.in_({"cache_key1", "cache_key2", "cache_key4"}))
|
||||
|
||||
@@ -96,7 +96,6 @@ async def test_list_dashboards_basic(mock_list, mcp_server):
|
||||
dashboard.uuid = "test-dashboard-uuid-1"
|
||||
dashboard.thumbnail_url = None
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
dashboard._mapping = {
|
||||
"id": dashboard.id,
|
||||
@@ -163,7 +162,6 @@ async def test_list_dashboards_with_filters(mock_list, mcp_server):
|
||||
dashboard.uuid = None
|
||||
dashboard.thumbnail_url = None
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
dashboard._mapping = {
|
||||
"id": dashboard.id,
|
||||
@@ -259,7 +257,6 @@ async def test_list_dashboards_with_search(mock_list, mcp_server):
|
||||
dashboard.uuid = None
|
||||
dashboard.thumbnail_url = None
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
dashboard._mapping = {
|
||||
"id": dashboard.id,
|
||||
@@ -354,7 +351,6 @@ async def test_get_dashboard_info_success(
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
dashboard._mapping = {
|
||||
"id": dashboard.id,
|
||||
@@ -433,7 +429,6 @@ async def test_get_dashboard_info_permalink_does_not_double_sanitize(
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
mock_info.return_value = dashboard
|
||||
permalink_value = {
|
||||
@@ -526,7 +521,6 @@ async def test_get_dashboard_info_permalink_key_includes_filter_state(
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
mock_info.return_value = dashboard
|
||||
|
||||
@@ -773,7 +767,6 @@ async def test_get_dashboard_info_does_not_expose_access_list_or_roles(
|
||||
dashboard.owners = [owner]
|
||||
dashboard.tags = []
|
||||
dashboard.roles = [dashboard_role]
|
||||
dashboard.embedded = []
|
||||
|
||||
mock_info.return_value = dashboard
|
||||
|
||||
@@ -845,7 +838,6 @@ async def test_get_dashboard_info_restricted_user_redacts_data_model_metadata(
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
|
||||
mock_info.return_value = dashboard
|
||||
|
||||
@@ -898,7 +890,6 @@ async def test_get_dashboard_info_restricted_user_redacts_permalink_filter_state
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
|
||||
mock_info.return_value = dashboard
|
||||
|
||||
@@ -1021,88 +1012,6 @@ async def test_list_dashboards_omits_requested_user_directory_fields(
|
||||
assert field not in data["columns_available"]
|
||||
|
||||
|
||||
@patch("superset.mcp_service.mcp_core.ModelGetInfoCore._find_object")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dashboard_info_includes_embedded_uuid(mock_find_object, mcp_server):
|
||||
"""Test that get_dashboard_info returns embedded_uuid when set."""
|
||||
from superset.models.embedded_dashboard import EmbeddedDashboard
|
||||
|
||||
dashboard = Mock()
|
||||
dashboard.id = 1
|
||||
dashboard.dashboard_title = "Embedded Dashboard"
|
||||
dashboard.slug = ""
|
||||
dashboard.description = None
|
||||
dashboard.css = None
|
||||
dashboard.certified_by = None
|
||||
dashboard.certification_details = None
|
||||
dashboard.json_metadata = "{}"
|
||||
dashboard.published = True
|
||||
dashboard.is_managed_externally = False
|
||||
dashboard.external_url = None
|
||||
dashboard.created_on = None
|
||||
dashboard.changed_on = None
|
||||
dashboard.created_on_humanized = None
|
||||
dashboard.changed_on_humanized = None
|
||||
dashboard.uuid = "94b826a5-dbd5-473d-ab58-1af676ee07e4"
|
||||
dashboard.url = "/dashboard/1"
|
||||
dashboard.slices = []
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
|
||||
embedded = Mock(spec=EmbeddedDashboard)
|
||||
embedded.uuid = "37c56048-d3f1-452d-b3ae-0879802dcb1f"
|
||||
dashboard.embedded = [embedded]
|
||||
|
||||
mock_find_object.return_value = dashboard
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"get_dashboard_info", {"request": {"identifier": 1}}
|
||||
)
|
||||
assert result.data["uuid"] == "94b826a5-dbd5-473d-ab58-1af676ee07e4"
|
||||
assert result.data["embedded_uuid"] == "37c56048-d3f1-452d-b3ae-0879802dcb1f"
|
||||
|
||||
|
||||
@patch("superset.mcp_service.mcp_core.ModelGetInfoCore._find_object")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dashboard_info_embedded_uuid_none_when_not_embedded(
|
||||
mock_find_object, mcp_server
|
||||
):
|
||||
"""Test that embedded_uuid is None when the dashboard has not been configured
|
||||
for embedding."""
|
||||
dashboard = Mock()
|
||||
dashboard.id = 2
|
||||
dashboard.dashboard_title = "Non-Embedded Dashboard"
|
||||
dashboard.slug = ""
|
||||
dashboard.description = None
|
||||
dashboard.css = None
|
||||
dashboard.certified_by = None
|
||||
dashboard.certification_details = None
|
||||
dashboard.json_metadata = "{}"
|
||||
dashboard.published = True
|
||||
dashboard.is_managed_externally = False
|
||||
dashboard.external_url = None
|
||||
dashboard.created_on = None
|
||||
dashboard.changed_on = None
|
||||
dashboard.created_on_humanized = None
|
||||
dashboard.changed_on_humanized = None
|
||||
dashboard.uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
dashboard.url = "/dashboard/2"
|
||||
dashboard.slices = []
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
|
||||
mock_find_object.return_value = dashboard
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"get_dashboard_info", {"request": {"identifier": 2}}
|
||||
)
|
||||
assert result.data.get("embedded_uuid") is None
|
||||
|
||||
|
||||
# TODO (Phase 3+): Add tests for get_dashboard_available_filters tool
|
||||
|
||||
|
||||
@@ -1135,7 +1044,6 @@ async def test_get_dashboard_info_by_uuid(mock_find_object, mcp_server):
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
|
||||
mock_find_object.return_value = dashboard
|
||||
async with Client(mcp_server) as client:
|
||||
@@ -1175,7 +1083,6 @@ async def test_get_dashboard_info_by_slug(mock_find_object, mcp_server):
|
||||
dashboard.owners = []
|
||||
dashboard.tags = []
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
|
||||
mock_find_object.return_value = dashboard
|
||||
async with Client(mcp_server) as client:
|
||||
@@ -1215,7 +1122,6 @@ async def test_list_dashboards_custom_uuid_slug_columns(mock_list, mcp_server):
|
||||
dashboard.external_url = None
|
||||
dashboard.thumbnail_url = None
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
dashboard._mapping = {
|
||||
"id": dashboard.id,
|
||||
@@ -1297,7 +1203,6 @@ async def test_list_dashboards_sanitizes_dashboard_descriptions_and_filter_text(
|
||||
dashboard.external_url = None
|
||||
dashboard.thumbnail_url = None
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
dashboard._mapping = {
|
||||
"id": dashboard.id,
|
||||
@@ -1438,7 +1343,6 @@ class TestDashboardDefaultColumnFiltering:
|
||||
dashboard.external_url = None
|
||||
dashboard.thumbnail_url = None
|
||||
dashboard.roles = []
|
||||
dashboard.embedded = []
|
||||
dashboard.charts = []
|
||||
mock_list.return_value = ([dashboard], 1)
|
||||
|
||||
|
||||
@@ -76,15 +76,6 @@ def test_default_map_renderer_is_exposed_to_frontend_config() -> None:
|
||||
assert "DEFAULT_MAP_RENDERER" in FRONTEND_CONF_KEYS
|
||||
|
||||
|
||||
def test_scarf_analytics_is_exposed_to_frontend_config() -> None:
|
||||
"""SCARF_ANALYTICS must reach the frontend config so pre-built images can opt
|
||||
out at runtime via the config/env var (the webpack build-time flag cannot be
|
||||
changed there)."""
|
||||
from superset.views.base import FRONTEND_CONF_KEYS
|
||||
|
||||
assert "SCARF_ANALYTICS" in FRONTEND_CONF_KEYS
|
||||
|
||||
|
||||
_FULL_METADATA = {
|
||||
"version_string": "4.0.0",
|
||||
"version_sha": "abcdef12",
|
||||
|
||||
Reference in New Issue
Block a user