mirror of
https://github.com/apache/superset.git
synced 2026-08-25 17:41:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
271cdd4564 | ||
|
|
2ebddb2e98 |
@@ -259,21 +259,6 @@ describe('isUserEditorOrAdmin', () => {
|
||||
test('returns false when editors is omitted', () => {
|
||||
expect(isUserEditorOrAdmin(outsiderUser)).toEqual(false);
|
||||
});
|
||||
|
||||
test('returns true when the user is granted editorship only through extra_editors', () => {
|
||||
expect(isUserEditorOrAdmin(editorUser, [], [10])).toEqual(true);
|
||||
});
|
||||
|
||||
test('unions editors and extra_editors rather than preferring one', () => {
|
||||
const nonMatchingSubject: Subject = { id: 999, label: 'Other', type: 1 };
|
||||
expect(isUserEditorOrAdmin(editorUser, [nonMatchingSubject], [10])).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false when extra_editors names other subjects', () => {
|
||||
expect(isUserEditorOrAdmin(editorUser, [], [999])).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
|
||||
@@ -55,6 +55,9 @@ export const isUserInSubjects = (
|
||||
);
|
||||
};
|
||||
|
||||
const isUserInEditors = (editors: Subject[] = []): boolean =>
|
||||
isUserInSubjects(editors);
|
||||
|
||||
export const isUserAdmin = (
|
||||
user?: UserWithPermissionsAndRoles | UndefinedUser,
|
||||
) =>
|
||||
@@ -63,12 +66,10 @@ export const isUserAdmin = (
|
||||
role => role.toLowerCase() === ADMIN_ROLE_NAME.toLowerCase(),
|
||||
);
|
||||
|
||||
/** `extraEditors` is editorship granted via a deployment's EXTRA_EDITORS_RESOLVER. */
|
||||
export const isUserEditorOrAdmin = (
|
||||
user?: UserWithPermissionsAndRoles | UndefinedUser,
|
||||
editors: Subject[] = [],
|
||||
extraEditors?: SubjectRef[] | null,
|
||||
): boolean => isUserInSubjects(editors, extraEditors) || isUserAdmin(user);
|
||||
): boolean => isUserInEditors(editors) || isUserAdmin(user);
|
||||
|
||||
/**
|
||||
* Editorship of *dashboard*, matching the server's `is_editor`: the explicit
|
||||
|
||||
@@ -29,7 +29,22 @@ import {
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import { SamplesPane, useResultsPane } from './components';
|
||||
import { DataTablesPaneProps, ResultTypes } from './types';
|
||||
import { getStaleResultsTabFallback } from './utils';
|
||||
|
||||
/**
|
||||
* A mixed chart can be reconfigured to return fewer result panes than before
|
||||
* (e.g. dropping a query), which removes the corresponding results tab. If the
|
||||
* selected tab was one of those, the active key goes stale and the data panel
|
||||
* renders blank until the user reselects a valid tab. Returns the first
|
||||
* results tab to fall back to in that case, otherwise undefined.
|
||||
*/
|
||||
export const getStaleResultsTabFallback = (
|
||||
activeTabKey: string,
|
||||
resultsTabKeys: string[],
|
||||
): string | undefined =>
|
||||
activeTabKey.startsWith(ResultTypes.Results) &&
|
||||
!resultsTabKeys.includes(activeTabKey)
|
||||
? ResultTypes.Results
|
||||
: undefined;
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
${() => `
|
||||
|
||||
+10
-19
@@ -20,15 +20,22 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import Tabs from '@superset-ui/core/components/Tabs';
|
||||
import { ResultTypes, ResultsPaneProps } from '../types';
|
||||
import { getStaleResultsTabFallback } from '../utils';
|
||||
import { useResultsPane } from './useResultsPane';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -79,25 +86,9 @@ export const ResultsPaneOnDashboard = ({
|
||||
};
|
||||
});
|
||||
|
||||
const resultsTabFallback = getStaleResultsTabFallback(
|
||||
activeTabKey,
|
||||
items.map(({ key }) => key),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (resultsTabFallback) {
|
||||
setActiveTabKey(resultsTabFallback);
|
||||
}
|
||||
}, [resultsTabFallback]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Tabs
|
||||
fullHeight
|
||||
activeKey={activeTabKey}
|
||||
onChange={setActiveTabKey}
|
||||
items={items}
|
||||
/>
|
||||
<Tabs activeKey={activeTabKey} onChange={setActiveTabKey} items={items} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
-56
@@ -20,34 +20,14 @@ import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
screen,
|
||||
render,
|
||||
act,
|
||||
waitForElementToBeRemoved,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { ChartMetadata, ChartPlugin, VizType } from '@superset-ui/core';
|
||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import Tabs from '@superset-ui/core/components/Tabs';
|
||||
import { ResultsPaneOnDashboard } from '../components';
|
||||
import { useResultsPane } from '../components/useResultsPane';
|
||||
import { createResultsPaneOnDashboardProps } from './fixture';
|
||||
|
||||
// `fullHeight`'s CSS isn't testable under jsdom (no `importSource` for the
|
||||
// `css` prop in jest's babel config), so spy on call args instead.
|
||||
jest.mock('@superset-ui/core/components/Tabs', () => {
|
||||
const actual = jest.requireActual('@superset-ui/core/components/Tabs');
|
||||
return { __esModule: true, ...actual, default: jest.fn(actual.default) };
|
||||
});
|
||||
|
||||
// Wraps the real hook; only overridden below to avoid mounting a second
|
||||
// real AG Grid instance, which jsdom doesn't support.
|
||||
jest.mock('../components/useResultsPane', () => {
|
||||
const actual = jest.requireActual('../components/useResultsPane');
|
||||
return {
|
||||
__esModule: true,
|
||||
useResultsPane: jest.fn(actual.useResultsPane),
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
setupAGGridModules();
|
||||
});
|
||||
@@ -126,10 +106,6 @@ describe('ResultsPaneOnDashboard', () => {
|
||||
expect(
|
||||
await findByText('No results were returned for this query'),
|
||||
).toBeVisible();
|
||||
expect(Tabs).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ fullHeight: true }),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
test('render errorMessage', async () => {
|
||||
@@ -243,36 +219,4 @@ describe('ResultsPaneOnDashboard', () => {
|
||||
expect(tab2).toBeVisible();
|
||||
expect(tab3).toBeNull();
|
||||
});
|
||||
|
||||
test('falls back to the first results tab when the active one disappears', async () => {
|
||||
const mockedUseResultsPane = useResultsPane as jest.Mock;
|
||||
mockedUseResultsPane.mockReturnValue([<div key="a" />, <div key="b" />]);
|
||||
|
||||
const props = createResultsPaneOnDashboardProps({ sliceId: 999 });
|
||||
const { rerender } = render(<ResultsPaneOnDashboard {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
const latestTabsProps = () => {
|
||||
const { calls } = (Tabs as unknown as jest.Mock).mock;
|
||||
return calls[calls.length - 1][0];
|
||||
};
|
||||
expect(latestTabsProps().items.map((i: { key: string }) => i.key)).toEqual([
|
||||
'results',
|
||||
'results 2',
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
latestTabsProps().onChange('results 2');
|
||||
});
|
||||
expect(latestTabsProps().activeKey).toBe('results 2');
|
||||
|
||||
// A mixed chart dropped from two query results to one, removing "results 2"
|
||||
mockedUseResultsPane.mockReturnValue([<div key="a" />]);
|
||||
rerender(<ResultsPaneOnDashboard {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(latestTabsProps().activeKey).toBe('results');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { getStaleResultsTabFallback } from '../utils';
|
||||
import { getStaleResultsTabFallback } from '../DataTablesPane';
|
||||
import { ResultTypes } from '../types';
|
||||
|
||||
test('keeps the active tab when it still exists', () => {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* 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 { ResultTypes } from './types';
|
||||
|
||||
/**
|
||||
* A mixed chart can be reconfigured to return fewer result panes than before
|
||||
* (e.g. dropping a query), which removes the corresponding results tab. If the
|
||||
* selected tab was one of those, the active key goes stale and the data panel
|
||||
* renders blank until the user reselects a valid tab. Returns the first
|
||||
* results tab to fall back to in that case, otherwise undefined.
|
||||
*/
|
||||
export const getStaleResultsTabFallback = (
|
||||
activeTabKey: string,
|
||||
resultsTabKeys: string[],
|
||||
): string | undefined =>
|
||||
activeTabKey.startsWith(ResultTypes.Results) &&
|
||||
!resultsTabKeys.includes(activeTabKey)
|
||||
? ResultTypes.Results
|
||||
: undefined;
|
||||
@@ -96,11 +96,7 @@ export default function ChartCard({
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
const canExport = hasPerm('can_export');
|
||||
const allowEdit = isUserEditorOrAdmin(
|
||||
user,
|
||||
chart.editors,
|
||||
chart.extra_editors,
|
||||
);
|
||||
const allowEdit = isUserEditorOrAdmin(user, chart.editors);
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (canEdit) {
|
||||
|
||||
@@ -83,11 +83,7 @@ function DashboardCard({
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
const canExport = hasPerm('can_export');
|
||||
const allowEdit = isUserEditorOrAdmin(
|
||||
user,
|
||||
dashboard.editors,
|
||||
dashboard.extra_editors,
|
||||
);
|
||||
const allowEdit = isUserEditorOrAdmin(user, dashboard.editors);
|
||||
const digest = dashboard.changed_on_utc || dashboard.changed_on;
|
||||
const thumbnailUrl =
|
||||
isFeatureEnabled(FeatureFlag.Thumbnails) && dashboard.id && digest
|
||||
|
||||
@@ -650,11 +650,7 @@ function ChartList(props: ChartListProps) {
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: CellProps<Chart>) => {
|
||||
const allowEdit = isUserEditorOrAdmin(
|
||||
user,
|
||||
original.editors,
|
||||
original.extra_editors,
|
||||
);
|
||||
const allowEdit = isUserEditorOrAdmin(user, original.editors);
|
||||
const openEditModal = () => openChartEditModal(original);
|
||||
const handleExport = () => handleBulkChartExport([original]);
|
||||
if (!canEdit && !canDelete && !canExport) {
|
||||
|
||||
@@ -122,8 +122,6 @@ export interface Dashboard {
|
||||
description?: string;
|
||||
thumbnail_url?: string | null;
|
||||
editors?: Subject[];
|
||||
// Bare subject ids from a deployment's EXTRA_EDITORS_RESOLVER.
|
||||
extra_editors?: number[];
|
||||
viewers?: Subject[];
|
||||
tags: TagType[];
|
||||
created_by: object;
|
||||
@@ -507,11 +505,7 @@ function DashboardList(props: DashboardListProps) {
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: CellProps<Dashboard>) => {
|
||||
const allowEdit = isUserEditorOrAdmin(
|
||||
user,
|
||||
original.editors,
|
||||
original.extra_editors,
|
||||
);
|
||||
const allowEdit = isUserEditorOrAdmin(user, original.editors);
|
||||
const handleDelete = () =>
|
||||
handleDashboardDelete(
|
||||
original,
|
||||
|
||||
@@ -45,8 +45,6 @@ export interface Chart {
|
||||
cache_timeout: number | null;
|
||||
thumbnail_url?: string;
|
||||
editors?: Subject[];
|
||||
// Bare subject ids from a deployment's EXTRA_EDITORS_RESOLVER.
|
||||
extra_editors?: number[];
|
||||
viewers?: Subject[];
|
||||
tags?: TagType[];
|
||||
last_saved_at?: string;
|
||||
|
||||
@@ -67,8 +67,6 @@ export interface Dashboard {
|
||||
url: string;
|
||||
thumbnail_url?: string | null;
|
||||
editors?: Subject[];
|
||||
// Bare subject ids from a deployment's EXTRA_EDITORS_RESOLVER.
|
||||
extra_editors?: number[];
|
||||
viewers?: Subject[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
+1
-13
@@ -92,10 +92,7 @@ from superset.exceptions import (
|
||||
)
|
||||
from superset.extensions import event_logger, security_manager
|
||||
from superset.models.slice import Slice
|
||||
from superset.security.manager import (
|
||||
get_extra_editor_subject_ids,
|
||||
get_extra_editors_by_pk,
|
||||
)
|
||||
from superset.security.manager import get_extra_editor_subject_ids
|
||||
from superset.subjects.filters import (
|
||||
FilterRelatedSubjects,
|
||||
subject_type_filter,
|
||||
@@ -413,15 +410,6 @@ class ChartRestApi(SoftDeleteApiMixin, BaseSupersetModelRestApi):
|
||||
except ChartNotFoundError:
|
||||
return self.response_404()
|
||||
|
||||
def pre_get_list(self, data: dict[str, Any]) -> None:
|
||||
"""Attach ``extra_editors`` to each row, matching the single-object GET."""
|
||||
super().pre_get_list(data)
|
||||
ids = data.get("ids", [])
|
||||
extra_editors_by_id = get_extra_editors_by_pk(Slice, ids)
|
||||
for row, row_id in zip(data.get("result", []), ids, strict=False):
|
||||
if row_id in extra_editors_by_id:
|
||||
row["extra_editors"] = extra_editors_by_id[row_id]
|
||||
|
||||
@expose("/<pk>/deck_layers/", methods=("GET",))
|
||||
@protect()
|
||||
@safe
|
||||
|
||||
@@ -142,10 +142,7 @@ from superset.extensions import event_logger, security_manager
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.embedded_dashboard import EmbeddedDashboard
|
||||
from superset.security.guest_token import GuestUser
|
||||
from superset.security.manager import (
|
||||
get_extra_editor_subject_ids,
|
||||
get_extra_editors_by_pk,
|
||||
)
|
||||
from superset.security.manager import get_extra_editor_subject_ids
|
||||
from superset.subjects.filters import (
|
||||
FilterRelatedSubjects,
|
||||
subject_type_filter,
|
||||
@@ -436,15 +433,6 @@ class DashboardRestApi(
|
||||
"""
|
||||
return super().get_list(**kwargs)
|
||||
|
||||
def pre_get_list(self, data: dict[str, Any]) -> None:
|
||||
"""Attach ``extra_editors`` to each row, matching the single-object GET."""
|
||||
super().pre_get_list(data)
|
||||
ids = data.get("ids", [])
|
||||
extra_editors_by_id = get_extra_editors_by_pk(Dashboard, ids)
|
||||
for row, row_id in zip(data.get("result", []), ids, strict=False):
|
||||
if row_id in extra_editors_by_id:
|
||||
row["extra_editors"] = extra_editors_by_id[row_id]
|
||||
|
||||
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
|
||||
order_columns = [
|
||||
"changed_by.first_name",
|
||||
|
||||
@@ -176,7 +176,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
"compare_lag": "10",
|
||||
"compare_suffix": "o10Y",
|
||||
"limit": "25",
|
||||
"granularity_sqla": "ds",
|
||||
"granularity": "ds",
|
||||
"groupby": [],
|
||||
"row_limit": current_app.config["ROW_LIMIT"],
|
||||
"time_range": "100 years ago : now",
|
||||
@@ -213,7 +213,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="big_number",
|
||||
granularity_sqla="ds",
|
||||
granularity="ds",
|
||||
compare_lag="5",
|
||||
compare_suffix="over 5Y",
|
||||
metric=metric,
|
||||
@@ -237,7 +237,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
defaults,
|
||||
viz_type="echarts_timeseries_line",
|
||||
groupby=["name"],
|
||||
granularity_sqla="ds",
|
||||
granularity="ds",
|
||||
rich_tooltip=True,
|
||||
show_legend=True,
|
||||
metrics=metrics,
|
||||
@@ -420,7 +420,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
}
|
||||
],
|
||||
metrics_b=["sum__num"],
|
||||
granularity_sqla="ds",
|
||||
granularity="ds",
|
||||
yAxisIndex=0,
|
||||
yAxisIndexB=1,
|
||||
),
|
||||
@@ -474,7 +474,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
"label": "SUM(num_california)",
|
||||
},
|
||||
viz_type="big_number_total",
|
||||
granularity_sqla="ds",
|
||||
granularity="ds",
|
||||
),
|
||||
editors=[],
|
||||
),
|
||||
@@ -496,7 +496,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
}
|
||||
],
|
||||
viz_type="echarts_timeseries_line",
|
||||
granularity_sqla="ds",
|
||||
granularity="ds",
|
||||
groupby=["name"],
|
||||
series_limit_metric={
|
||||
"expressionType": "SIMPLE",
|
||||
@@ -542,7 +542,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
|
||||
defaults,
|
||||
metric=metric,
|
||||
viz_type="big_number_total",
|
||||
granularity_sqla="ds",
|
||||
granularity="ds",
|
||||
adhoc_filters=[gen_filter("gender", "girl")],
|
||||
subheader="total female participants",
|
||||
),
|
||||
|
||||
@@ -28,7 +28,7 @@ params:
|
||||
subject: gender
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby: []
|
||||
limit: '100'
|
||||
markup_type: markdown
|
||||
|
||||
@@ -28,7 +28,7 @@ params:
|
||||
subject: gender
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- name
|
||||
limit: '25'
|
||||
|
||||
@@ -22,7 +22,7 @@ description: null
|
||||
params:
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- gender
|
||||
limit: '25'
|
||||
|
||||
@@ -30,7 +30,7 @@ params:
|
||||
subject: state
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- state
|
||||
limit: '25'
|
||||
|
||||
@@ -28,7 +28,7 @@ params:
|
||||
subject: gender
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby: []
|
||||
limit: '100'
|
||||
markup_type: markdown
|
||||
|
||||
@@ -28,7 +28,7 @@ params:
|
||||
subject: gender
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- name
|
||||
limit: '25'
|
||||
|
||||
@@ -22,7 +22,7 @@ description: null
|
||||
params:
|
||||
compare_lag: '5'
|
||||
compare_suffix: over 5Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby: []
|
||||
limit: '25'
|
||||
markup_type: markdown
|
||||
|
||||
@@ -22,7 +22,7 @@ description: null
|
||||
params:
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby: []
|
||||
groupbyColumns:
|
||||
- state
|
||||
|
||||
@@ -29,7 +29,7 @@ params:
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
comparison_type: values
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- name
|
||||
limit: 10
|
||||
|
||||
@@ -29,7 +29,7 @@ params:
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
comparison_type: values
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- name
|
||||
limit: 10
|
||||
|
||||
@@ -22,7 +22,7 @@ description: null
|
||||
params:
|
||||
compare_lag: '10'
|
||||
compare_suffix: o10Y
|
||||
granularity_sqla: ds
|
||||
granularity: ds
|
||||
groupby:
|
||||
- name
|
||||
limit: '25'
|
||||
|
||||
@@ -154,7 +154,7 @@ def create_slices(tbl: BaseDatasource) -> list[Slice]:
|
||||
"compare_lag": "10",
|
||||
"compare_suffix": "o10Y",
|
||||
"limit": "25",
|
||||
"granularity_sqla": "year",
|
||||
"granularity": "year",
|
||||
"groupby": [],
|
||||
"row_limit": current_app.config["ROW_LIMIT"],
|
||||
"since": "2014-01-01",
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby:
|
||||
- region
|
||||
limit: '25'
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby:
|
||||
- country_name
|
||||
limit: '25'
|
||||
|
||||
@@ -43,7 +43,7 @@ params:
|
||||
subject: country_code
|
||||
color_scheme: supersetColors
|
||||
entity: country_name
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
legendOrientation: top
|
||||
legendType: scroll
|
||||
max_bubble_size: '50'
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby:
|
||||
- country_name
|
||||
limit: '25'
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby: []
|
||||
limit: '25'
|
||||
markup_type: markdown
|
||||
|
||||
@@ -27,7 +27,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby: []
|
||||
limit: '25'
|
||||
markup_type: markdown
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby:
|
||||
- region
|
||||
- country_code
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: o10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby:
|
||||
- region
|
||||
limit: '25'
|
||||
|
||||
@@ -24,7 +24,7 @@ params:
|
||||
compare_suffix: over 10Y
|
||||
country_fieldtype: cca3
|
||||
entity: country_code
|
||||
granularity_sqla: year
|
||||
granularity: year
|
||||
groupby: []
|
||||
limit: '25'
|
||||
markup_type: markdown
|
||||
|
||||
@@ -170,36 +170,6 @@ def get_extra_editor_subject_ids(resource: Model) -> list[int]:
|
||||
return subject_ids
|
||||
|
||||
|
||||
def get_extra_editors_by_pk(
|
||||
model_cls: type[Model], primary_keys: list[Any]
|
||||
) -> dict[Any, list[int]]:
|
||||
"""
|
||||
Resolve extra editor subject IDs for a batch of resources, keyed by
|
||||
primary key. List responses only have serialized rows, not model
|
||||
instances, so this re-queries the page's rows in one batched query.
|
||||
"""
|
||||
if not primary_keys or not (
|
||||
has_app_context() and current_app.config.get("EXTRA_EDITORS_RESOLVER")
|
||||
):
|
||||
return {}
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from superset import db
|
||||
from superset.models.helpers import SKIP_VISIBILITY_FILTER_CLASSES
|
||||
|
||||
pk_col = inspect(model_cls).primary_key[0]
|
||||
resources = (
|
||||
db.session.query(model_cls)
|
||||
.execution_options(**{SKIP_VISIBILITY_FILTER_CLASSES: {model_cls}})
|
||||
.filter(pk_col.in_(primary_keys))
|
||||
.all()
|
||||
)
|
||||
return {
|
||||
getattr(resource, pk_col.name): get_extra_editor_subject_ids(resource)
|
||||
for resource in resources
|
||||
}
|
||||
|
||||
|
||||
def _render_permission_instructions_link(
|
||||
*,
|
||||
datasource_id: str = "",
|
||||
|
||||
@@ -922,55 +922,6 @@ class TestDashboardApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCa
|
||||
db.session.delete(dashboard)
|
||||
db.session.commit()
|
||||
|
||||
def test_get_dashboards_list_omits_extra_editors_by_default(self):
|
||||
"""No EXTRA_EDITORS_RESOLVER configured: list rows omit extra_editors."""
|
||||
admin = self.get_user("admin")
|
||||
dashboard = self.insert_dashboard(
|
||||
"no_extra_editors_list_dashboard",
|
||||
"no-extra-editors-list-dashboard",
|
||||
[admin.id],
|
||||
)
|
||||
try:
|
||||
self.login(ADMIN_USERNAME)
|
||||
rv = self.client.get("api/v1/dashboard/")
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
row = next(
|
||||
d
|
||||
for d in data["result"]
|
||||
if d["dashboard_title"] == dashboard.dashboard_title
|
||||
)
|
||||
assert "extra_editors" not in row
|
||||
finally:
|
||||
db.session.delete(dashboard)
|
||||
db.session.commit()
|
||||
|
||||
@with_config({"EXTRA_EDITORS_RESOLVER": lambda resource: [123]})
|
||||
def test_get_dashboards_list_includes_extra_editors_when_resolver_configured(
|
||||
self,
|
||||
):
|
||||
"""List rows get extra_editors too, mirroring the single-object GET."""
|
||||
admin = self.get_user("admin")
|
||||
dashboard = self.insert_dashboard(
|
||||
"extra_editors_list_dashboard",
|
||||
"extra-editors-list-dashboard",
|
||||
[admin.id],
|
||||
)
|
||||
try:
|
||||
self.login(ADMIN_USERNAME)
|
||||
rv = self.client.get("api/v1/dashboard/")
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
row = next(
|
||||
d
|
||||
for d in data["result"]
|
||||
if d["dashboard_title"] == dashboard.dashboard_title
|
||||
)
|
||||
assert row["extra_editors"] == [123]
|
||||
finally:
|
||||
db.session.delete(dashboard)
|
||||
db.session.commit()
|
||||
|
||||
def test_get_charts_admin_sees_existing_charts(self):
|
||||
"""Regression for #25890: GET /api/v1/chart/ as an Admin user should
|
||||
return existing charts, not an empty list."""
|
||||
@@ -993,41 +944,6 @@ class TestDashboardApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCa
|
||||
db.session.delete(chart)
|
||||
db.session.commit()
|
||||
|
||||
def test_get_charts_list_omits_extra_editors_by_default(self):
|
||||
"""No EXTRA_EDITORS_RESOLVER configured: list rows omit extra_editors."""
|
||||
admin = self.get_user("admin")
|
||||
chart = self.insert_chart(
|
||||
"no_extra_editors_list_chart", [admin.id], 1, params="{}"
|
||||
)
|
||||
try:
|
||||
self.login(ADMIN_USERNAME)
|
||||
rv = self.client.get("api/v1/chart/")
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
row = next(c for c in data["result"] if c["slice_name"] == chart.slice_name)
|
||||
assert "extra_editors" not in row
|
||||
finally:
|
||||
db.session.delete(chart)
|
||||
db.session.commit()
|
||||
|
||||
@with_config({"EXTRA_EDITORS_RESOLVER": lambda resource: [123]})
|
||||
def test_get_charts_list_includes_extra_editors_when_resolver_configured(self):
|
||||
"""List rows get extra_editors too, mirroring the single-object GET."""
|
||||
admin = self.get_user("admin")
|
||||
chart = self.insert_chart(
|
||||
"extra_editors_list_chart", [admin.id], 1, params="{}"
|
||||
)
|
||||
try:
|
||||
self.login(ADMIN_USERNAME)
|
||||
rv = self.client.get("api/v1/chart/")
|
||||
assert rv.status_code == 200
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
row = next(c for c in data["result"] if c["slice_name"] == chart.slice_name)
|
||||
assert row["extra_editors"] == [123]
|
||||
finally:
|
||||
db.session.delete(chart)
|
||||
db.session.commit()
|
||||
|
||||
def test_get_dashboards_filter(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboards filter
|
||||
|
||||
Reference in New Issue
Block a user