mirror of
https://github.com/apache/superset.git
synced 2026-07-05 14:25:32 +00:00
Compare commits
3 Commits
master
...
fix/saniti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4df5a15e1 | ||
|
|
74f815d09c | ||
|
|
d250eacaee |
@@ -19,7 +19,6 @@
|
||||
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { TIMEOUT } from '../utils/constants';
|
||||
import { AgGrid } from '../components/core/AgGrid';
|
||||
|
||||
/**
|
||||
* Explore Page object
|
||||
@@ -31,11 +30,6 @@ export class ExplorePage {
|
||||
DATASOURCE_CONTROL: '[data-test="datasource-control"]',
|
||||
VIZ_SWITCHER: '[data-test="fast-viz-switcher"]',
|
||||
CHART_CONTAINER: '[data-test="chart-container"]',
|
||||
// The bottom data panel (DataTablesPane / SouthPane) with Results/Samples tabs
|
||||
SOUTH_PANE: '[data-test="some-purposeful-instance"]',
|
||||
EXPAND_DATA_PANEL: '[aria-label="Expand data panel"]',
|
||||
RESULTS_TAB: '[data-node-key="results"]',
|
||||
ACTIVE_TABPANE: '.ant-tabs-tabpane-active',
|
||||
} as const;
|
||||
|
||||
constructor(page: Page) {
|
||||
@@ -112,44 +106,4 @@ export class ExplorePage {
|
||||
getVizSwitcher(): Locator {
|
||||
return this.page.locator(ExplorePage.SELECTORS.VIZ_SWITCHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the bottom data panel if it is currently collapsed.
|
||||
* Safe to call when already expanded (no-op).
|
||||
*/
|
||||
async expandDataPanel(): Promise<void> {
|
||||
const expandButton = this.page.locator(
|
||||
ExplorePage.SELECTORS.EXPAND_DATA_PANEL,
|
||||
);
|
||||
if (await expandButton.isVisible().catch(() => false)) {
|
||||
await expandButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the bottom data panel and activates the "Results" tab. Clicking the
|
||||
* already-active tab collapses the panel, so the click is guarded.
|
||||
*/
|
||||
async openResultsTab(): Promise<void> {
|
||||
await this.expandDataPanel();
|
||||
const resultsTab = this.page
|
||||
.locator(ExplorePage.SELECTORS.SOUTH_PANE)
|
||||
.locator(ExplorePage.SELECTORS.RESULTS_TAB);
|
||||
const className = (await resultsTab.getAttribute('class')) ?? '';
|
||||
if (!className.includes('ant-tabs-tab-active')) {
|
||||
await resultsTab.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AgGrid wrapper around the currently active Results tab grid.
|
||||
*/
|
||||
getResultsGrid(): AgGrid {
|
||||
const grid = this.page
|
||||
.locator(ExplorePage.SELECTORS.SOUTH_PANE)
|
||||
.locator(ExplorePage.SELECTORS.ACTIVE_TABPANE)
|
||||
.locator('[role="grid"]')
|
||||
.first();
|
||||
return new AgGrid(this.page, grid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Regression coverage for #38152 (PR #38165): the Explore "Results" tab reuses
|
||||
* the chart's already-fetched query data instead of firing a second, identical
|
||||
* request to /api/v1/chart/data. The duplicate only exists across the real
|
||||
* backend round-trip; the reuse-vs-fallback and row-limit slicing logic is
|
||||
* unit-tested in useResultsPane.test.tsx, so this E2E asserts only the
|
||||
* end-to-end win: opening Results issues no extra chart/data request.
|
||||
*
|
||||
* Lives under tests/experimental/ until proven stable in CI; run with:
|
||||
* INCLUDE_EXPERIMENTAL=true npm run playwright:test \
|
||||
* tests/experimental/explore/results-tab-reuse.spec.ts -- --headed
|
||||
*/
|
||||
import { testWithAssets, expect } from '../../../helpers/fixtures';
|
||||
import { apiPostChart } from '../../../helpers/api/chart';
|
||||
import { getDatasetByName } from '../../../helpers/api/dataset';
|
||||
import { ExplorePage } from '../../../pages/ExplorePage';
|
||||
import { TIMEOUT } from '../../../utils/constants';
|
||||
|
||||
const DATASET_NAME = 'birth_names';
|
||||
const CHART_DATA_PATH = '/api/v1/chart/data';
|
||||
|
||||
testWithAssets(
|
||||
'Results tab reuses chart data without a duplicate query (#38165)',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
|
||||
let chartDataRequests = 0;
|
||||
page.on('request', request => {
|
||||
if (
|
||||
request.method() === 'POST' &&
|
||||
request.url().includes(CHART_DATA_PATH)
|
||||
) {
|
||||
chartDataRequests += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const dataset = await getDatasetByName(page, DATASET_NAME);
|
||||
if (!dataset) {
|
||||
throw new Error(`Dataset ${DATASET_NAME} not found`);
|
||||
}
|
||||
const datasetId = dataset.id;
|
||||
|
||||
const params = {
|
||||
datasource: `${datasetId}__table`,
|
||||
viz_type: 'table',
|
||||
query_mode: 'raw',
|
||||
all_columns: ['name', 'gender', 'num'],
|
||||
adhoc_filters: [],
|
||||
order_by_cols: [],
|
||||
row_limit: 1000,
|
||||
server_pagination: false,
|
||||
};
|
||||
const chartResp = await apiPostChart(page, {
|
||||
slice_name: `results_tab_reuse_${Date.now()}`,
|
||||
viz_type: 'table',
|
||||
datasource_id: datasetId,
|
||||
datasource_type: 'table',
|
||||
params: JSON.stringify(params),
|
||||
});
|
||||
expect(chartResp.ok()).toBe(true);
|
||||
const chartBody = await chartResp.json();
|
||||
const chartId: number = chartBody.result?.id ?? chartBody.id;
|
||||
expect(chartId, 'chart creation should return an id').toBeTruthy();
|
||||
testAssets.trackChart(chartId);
|
||||
|
||||
// Wait for the chart query to finish so its result is in Redux; opening
|
||||
// Results before that races and falls back to its own request.
|
||||
const explorePage = new ExplorePage(page);
|
||||
const chartQueryFinished = page.waitForResponse(
|
||||
response =>
|
||||
response.request().method() === 'POST' &&
|
||||
response.url().includes(CHART_DATA_PATH),
|
||||
{ timeout: TIMEOUT.API_RESPONSE },
|
||||
);
|
||||
await explorePage.goto(chartId);
|
||||
await chartQueryFinished;
|
||||
|
||||
const chartContainer = explorePage.getChartContainer();
|
||||
await chartContainer.waitFor({
|
||||
state: 'visible',
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
await chartContainer
|
||||
.getByRole('status', { name: 'Loading' })
|
||||
.waitFor({ state: 'hidden', timeout: TIMEOUT.API_RESPONSE })
|
||||
.catch(() => {}); // spinner may have already cleared
|
||||
|
||||
expect(chartDataRequests).toBeGreaterThanOrEqual(1);
|
||||
const requestsAfterChartLoad = chartDataRequests;
|
||||
|
||||
await explorePage.openResultsTab();
|
||||
const grid = explorePage.getResultsGrid();
|
||||
await grid.waitForRows({ timeout: TIMEOUT.API_RESPONSE });
|
||||
expect(await grid.getRowCount()).toBeGreaterThan(0);
|
||||
|
||||
// Allow time for an unwanted duplicate request, then assert none fired.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1500);
|
||||
expect(
|
||||
chartDataRequests,
|
||||
'opening Results must not trigger a second chart/data query',
|
||||
).toBe(requestsAfterChartLoad);
|
||||
},
|
||||
);
|
||||
@@ -38,7 +38,6 @@ import { css, styled } from '@apache-superset/core/theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { type ItemType } from '@superset-ui/core/components/Menu';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { NULL_STRING } from 'src/utils/common';
|
||||
import { getSubmenuYOffset } from '../utils';
|
||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||
import { TruncatedMenuLabel } from '../MenuItemWithTruncation';
|
||||
@@ -220,40 +219,33 @@ export const useDrillDetailMenuItems = ({
|
||||
popupOffset: [0, submenuYOffset],
|
||||
popupClassName: 'chart-context-submenu',
|
||||
children: [
|
||||
...filters.map((filter, i) => {
|
||||
const isNullVal =
|
||||
filter.val === null || filter.formattedVal === NULL_STRING;
|
||||
const formattedVal = isNullVal
|
||||
? NULL_STRING
|
||||
: filter.formattedVal;
|
||||
return {
|
||||
key: `drill-detail-filter-${i}`,
|
||||
onClick: openModal.bind(null, [filter]),
|
||||
label: (
|
||||
<div
|
||||
css={css`
|
||||
max-width: 200px;
|
||||
`}
|
||||
...filters.map((filter, i) => ({
|
||||
key: `drill-detail-filter-${i}`,
|
||||
onClick: openModal.bind(null, [filter]),
|
||||
label: (
|
||||
<div
|
||||
css={css`
|
||||
max-width: 200px;
|
||||
`}
|
||||
>
|
||||
<TruncatedMenuLabel
|
||||
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
|
||||
aria-label={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
|
||||
>
|
||||
<TruncatedMenuLabel
|
||||
tooltipText={`${DRILL_TO_DETAIL_BY} ${formattedVal}`}
|
||||
aria-label={`${DRILL_TO_DETAIL_BY} ${formattedVal}`}
|
||||
<span
|
||||
css={css`
|
||||
display: inline;
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={css`
|
||||
display: inline;
|
||||
`}
|
||||
>
|
||||
{DRILL_TO_DETAIL_BY}{' '}
|
||||
<StyledFilter stripHTML={!isNullVal}>
|
||||
{formattedVal}
|
||||
</StyledFilter>
|
||||
</span>
|
||||
</TruncatedMenuLabel>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
{DRILL_TO_DETAIL_BY}{' '}
|
||||
<StyledFilter stripHTML>
|
||||
{filter.formattedVal}
|
||||
</StyledFilter>
|
||||
</span>
|
||||
</TruncatedMenuLabel>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
...(filters.length > 1
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -74,13 +74,6 @@ const filterB: BinaryQueryObjectFilterClause = {
|
||||
formattedVal: 'Two days ago',
|
||||
};
|
||||
|
||||
const filterNull: BinaryQueryObjectFilterClause = {
|
||||
col: 'sample_column_3',
|
||||
op: '==',
|
||||
val: null as unknown as string,
|
||||
formattedVal: '<NULL>',
|
||||
};
|
||||
|
||||
const MockRenderChart = ({
|
||||
chartId,
|
||||
formData,
|
||||
@@ -433,14 +426,3 @@ test.skip('context menu for supported chart, dimensions, all filters', async ()
|
||||
await setupMenu(filters);
|
||||
await expectDrillToDetailByAll(filters);
|
||||
});
|
||||
|
||||
test('context menu renders <NULL> for null dimension values', async () => {
|
||||
renderMenu({
|
||||
formData: defaultFormData,
|
||||
isContextMenu: true,
|
||||
filters: [filterNull],
|
||||
});
|
||||
|
||||
await expectDrillToDetailByEnabled();
|
||||
await expectDrillToDetailByDimension(filterNull);
|
||||
});
|
||||
|
||||
@@ -1,90 +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 {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import type { MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { KebabMenuButton } from '.';
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Edit',
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
},
|
||||
];
|
||||
|
||||
test('opens the menu on click', async () => {
|
||||
render(<KebabMenuButton menuItems={menuItems} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'More Options' }));
|
||||
|
||||
expect(await screen.findByText('Edit')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delete')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not open the menu on hover', async () => {
|
||||
render(<KebabMenuButton menuItems={menuItems} />);
|
||||
|
||||
await userEvent.hover(screen.getByRole('button', { name: 'More Options' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('sets accessibility and test attributes on the trigger button', () => {
|
||||
render(
|
||||
<KebabMenuButton
|
||||
menuItems={menuItems}
|
||||
ariaLabel="Open actions"
|
||||
buttonId="actions-menu"
|
||||
dataTest="actions-menu-trigger"
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Open actions' });
|
||||
expect(button).toHaveAttribute('id', 'actions-menu');
|
||||
expect(button).toHaveAttribute('aria-haspopup', 'true');
|
||||
expect(button).toHaveAttribute('data-test', 'actions-menu-trigger');
|
||||
});
|
||||
|
||||
test('renders a custom icon', () => {
|
||||
render(
|
||||
<KebabMenuButton
|
||||
menuItems={menuItems}
|
||||
icon={<span data-test="custom-menu-icon">Actions</span>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('custom-menu-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders a rotated vertical icon when requested', () => {
|
||||
render(<KebabMenuButton menuItems={menuItems} iconOrientation="vertical" />);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'More Options' });
|
||||
expect(button.querySelector('[style*="rotate(90deg)"]')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,83 +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 { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Icons,
|
||||
type ButtonProps,
|
||||
type DropdownProps,
|
||||
} from '@superset-ui/core/components';
|
||||
import type { MenuItem } from '@superset-ui/core/components/Menu';
|
||||
|
||||
export interface KebabMenuButtonProps {
|
||||
menuItems: MenuItem[];
|
||||
icon?: ReactNode;
|
||||
iconOrientation?: 'horizontal' | 'vertical';
|
||||
ariaLabel?: string;
|
||||
dataTest?: string;
|
||||
buttonId?: string;
|
||||
buttonSize?: ButtonProps['buttonSize'];
|
||||
buttonStyle?: ButtonProps['buttonStyle'];
|
||||
placement?: DropdownProps['placement'];
|
||||
overlayStyle?: DropdownProps['overlayStyle'];
|
||||
}
|
||||
|
||||
export function KebabMenuButton({
|
||||
menuItems,
|
||||
icon,
|
||||
iconOrientation = 'horizontal',
|
||||
ariaLabel = 'More Options',
|
||||
dataTest,
|
||||
buttonId,
|
||||
buttonSize = 'xsmall',
|
||||
buttonStyle = 'link',
|
||||
placement,
|
||||
overlayStyle,
|
||||
}: KebabMenuButtonProps) {
|
||||
const defaultIcon =
|
||||
iconOrientation === 'vertical' ? (
|
||||
<Icons.EllipsisOutlined
|
||||
iconSize="xl"
|
||||
style={{ transform: 'rotate(90deg)' }}
|
||||
/>
|
||||
) : (
|
||||
<Icons.MoreOutlined iconSize="xl" />
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['click']}
|
||||
placement={placement}
|
||||
overlayStyle={overlayStyle}
|
||||
>
|
||||
<Button
|
||||
id={buttonId}
|
||||
buttonSize={buttonSize}
|
||||
buttonStyle={buttonStyle}
|
||||
aria-label={ariaLabel}
|
||||
aria-haspopup="true"
|
||||
data-test={dataTest}
|
||||
>
|
||||
{icon || defaultIcon}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -49,4 +49,3 @@ export {
|
||||
type PluginContextType,
|
||||
} from './DynamicPlugins';
|
||||
export * from './FacePile';
|
||||
export { KebabMenuButton, type KebabMenuButtonProps } from './KebabMenuButton';
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
within,
|
||||
screen,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { addAlpha, FeatureFlag } from '@superset-ui/core';
|
||||
import { FeatureFlag } from '@superset-ui/core';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import {
|
||||
OPEN_FILTER_BAR_WIDTH,
|
||||
@@ -588,50 +588,6 @@ test('should apply min-height to the top-level tab drop target so tabs can be dr
|
||||
);
|
||||
});
|
||||
|
||||
test('should render chart tiles with a theme-driven border at rest, see https://github.com/apache/superset/issues/41618', () => {
|
||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||
100,
|
||||
jest.fn(),
|
||||
]);
|
||||
(fetchFaveStar as jest.Mock).mockReturnValue({ type: 'mock-action' });
|
||||
(setActiveTab as jest.Mock).mockReturnValue({ type: 'mock-action' });
|
||||
|
||||
const { container } = render(<DashboardBuilder />, {
|
||||
useRedux: true,
|
||||
store: storeWithState({
|
||||
...mockState,
|
||||
dashboardLayout: undoableDashboardLayout,
|
||||
}),
|
||||
useDnd: true,
|
||||
useTheme: true,
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
// StyledDashboardContent (className "dashboard-content") owns the nested
|
||||
// `.dashboard-component-chart-holder` CSS, so it's the element to assert
|
||||
// style rules against, not the individual chart holder nodes it renders.
|
||||
const dashboardContent = container.querySelector('.dashboard-content');
|
||||
|
||||
expect(dashboardContent).toHaveStyleRule(
|
||||
'border',
|
||||
`1px solid ${supersetTheme.colorBorder}`,
|
||||
{ target: '.dashboard-component-chart-holder' },
|
||||
);
|
||||
expect(dashboardContent).toHaveStyleRule(
|
||||
'border-radius',
|
||||
`${supersetTheme.borderRadius}px`,
|
||||
{ target: '.dashboard-component-chart-holder' },
|
||||
);
|
||||
|
||||
// .fade-out no longer re-declares border/border-radius (it inherits the
|
||||
// base rule above); it should still layer its own hairline box-shadow.
|
||||
expect(dashboardContent).toHaveStyleRule(
|
||||
'box-shadow',
|
||||
`0 0 0 1px ${addAlpha(supersetTheme.colorBorder, 0.5)}`,
|
||||
{ target: '.dashboard-component-chart-holder.fade-out' },
|
||||
);
|
||||
});
|
||||
|
||||
test('should maintain layout when switching between tabs', async () => {
|
||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||
100,
|
||||
|
||||
@@ -317,8 +317,6 @@ const StyledDashboardContent = styled.div<{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: ${theme.dashboardTileBg ?? theme.colorBgContainer};
|
||||
border: ${theme.dashboardTileBorder ?? `1px solid ${theme.colorBorder}`};
|
||||
border-radius: ${theme.dashboardTileBorderRadius ?? theme.borderRadius}px;
|
||||
position: relative;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
box-sizing: border-box;
|
||||
@@ -331,12 +329,16 @@ const StyledDashboardContent = styled.div<{
|
||||
box-shadow ${theme.motionDurationMid} ease-in-out;
|
||||
|
||||
&.fade-in {
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
box-shadow:
|
||||
inset 0 0 0 2px ${theme.colorPrimary},
|
||||
0 0 0 3px ${addAlpha(theme.colorPrimary, 0.1)};
|
||||
}
|
||||
|
||||
&.fade-out {
|
||||
border: ${theme.dashboardTileBorder ?? 'none'};
|
||||
border-radius: ${theme.dashboardTileBorderRadius ??
|
||||
theme.borderRadius}px;
|
||||
box-shadow: ${theme.dashboardTileBoxShadow ??
|
||||
`0 0 0 1px ${addAlpha(theme.colorBorder, 0.5)}`};
|
||||
}
|
||||
|
||||
@@ -82,18 +82,22 @@ const RefreshTooltip = styled.div`
|
||||
const getScreenshotNodeSelector = (chartId: string | number) =>
|
||||
`.dashboard-chart-id-${chartId}`;
|
||||
|
||||
const VerticalDotsTrigger = () => (
|
||||
<Icons.EllipsisOutlined
|
||||
css={css`
|
||||
transform: rotate(90deg);
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`}
|
||||
iconSize="xl"
|
||||
className="dot"
|
||||
/>
|
||||
);
|
||||
const VerticalDotsTrigger = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Icons.EllipsisOutlined
|
||||
css={css`
|
||||
transform: rotate(90deg);
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`}
|
||||
iconSize="xl"
|
||||
iconColor={theme.colorTextLabel}
|
||||
className="dot"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface SliceHeaderControlsProps {
|
||||
chartHolderRef?: RefObject<HTMLDivElement>;
|
||||
|
||||
@@ -100,7 +100,7 @@ export function getAppliedFilterValues(
|
||||
// use cached data if possible
|
||||
if (!(chartId in appliedFilterValuesByChart)) {
|
||||
const applicableFilters = Object.entries(filters || activeFilters).filter(
|
||||
([, activeFilter]) => activeFilter?.scope?.includes(chartId),
|
||||
([, { scope: chartIds }]) => chartIds.includes(chartId),
|
||||
);
|
||||
appliedFilterValuesByChart[chartId] = flow(
|
||||
keyBy(
|
||||
|
||||
@@ -459,7 +459,7 @@ export default function getFormDataWithExtraFilters({
|
||||
|
||||
let extraData: JsonObject = {};
|
||||
const filterIdsAppliedOnChart = Object.entries(activeFilters)
|
||||
.filter(([, activeFilter]) => activeFilter?.scope?.includes(chart.id))
|
||||
.filter(([, activeFilter]) => activeFilter.scope.includes(chart.id))
|
||||
.map(([filterId]) => filterId);
|
||||
|
||||
if (filterIdsAppliedOnChart.length) {
|
||||
@@ -474,7 +474,7 @@ export default function getFormDataWithExtraFilters({
|
||||
const isDeckMultiChart = chart.form_data?.viz_type === 'deck_multi';
|
||||
const hasLayerScopeInActiveFilters =
|
||||
passedActiveFilters &&
|
||||
Object.values(passedActiveFilters).some(filter => filter?.layerScope);
|
||||
Object.values(passedActiveFilters).some(filter => filter.layerScope);
|
||||
|
||||
if (isDeckMultiChart || hasLayerScopeInActiveFilters) {
|
||||
const filterDataMapping = createFilterDataMapping(
|
||||
|
||||
@@ -1,66 +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 { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
|
||||
jest.mock('src/public-path', () => ({}));
|
||||
|
||||
jest.mock('query-string', () => ({}));
|
||||
|
||||
jest.mock('@superset-ui/core/components/ThemedAgGridReact', () => ({
|
||||
setupAGGridModules: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('src/preamble', () => jest.fn().mockResolvedValue(true));
|
||||
|
||||
jest.mock('src/setup/setupPlugins', () => jest.fn(), { virtual: true });
|
||||
|
||||
jest.mock('src/setup/setupCodeOverrides', () => jest.fn(), { virtual: true });
|
||||
|
||||
jest.mock('src/utils/getBootstrapData', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
embedded: { dashboard_id: '123' },
|
||||
common: {
|
||||
application_root: '/',
|
||||
static_assets_prefix: '/',
|
||||
conf: {
|
||||
SQLLAB_DEFAULT_DBID: 1,
|
||||
DEFAULT_SQLLAB_LIMIT: 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
applicationRoot: () => '/',
|
||||
staticAssetsPrefix: () => '/',
|
||||
}));
|
||||
|
||||
describe('embedded/index.tsx', () => {
|
||||
beforeAll(() => {
|
||||
document.body.innerHTML = '<div id="app"></div>';
|
||||
});
|
||||
|
||||
test('initializes AG Grid modules on bootstrap', async () => {
|
||||
require('./index');
|
||||
|
||||
// index.tsx uses initPreamble().then(...) to initialize plugins and AG grid
|
||||
// Wait for the promise chain to resolve
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(setupAGGridModules).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
import Switchboard from '@superset-ui/switchboard';
|
||||
import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
|
||||
import initPreamble from 'src/preamble';
|
||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import setupClient from 'src/setup/setupClient';
|
||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||
import { store, USER_LOADED } from 'src/views/store';
|
||||
@@ -70,7 +69,6 @@ const pluginsReady = initPreamble()
|
||||
]);
|
||||
setupPlugins();
|
||||
setupCodeOverrides({ embedded: true });
|
||||
setupAGGridModules();
|
||||
});
|
||||
|
||||
const debugMode = process.env.WEBPACK_MODE === 'development';
|
||||
|
||||
@@ -103,7 +103,6 @@ export const DataTablesPane = ({
|
||||
errorMessage,
|
||||
setForceQuery,
|
||||
canDownload,
|
||||
queriesResponse,
|
||||
}: DataTablesPaneProps) => {
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>(ResultTypes.Results);
|
||||
const [isRequest, setIsRequest] = useState<Record<ResultTypes, boolean>>({
|
||||
@@ -129,10 +128,6 @@ export const DataTablesPane = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (panelOpen && chartStatus === 'loading') {
|
||||
setIsRequest(prev => ({ ...prev, results: false }));
|
||||
}
|
||||
|
||||
if (
|
||||
panelOpen &&
|
||||
activeTabKey.startsWith(ResultTypes.Results) &&
|
||||
@@ -211,7 +206,6 @@ export const DataTablesPane = ({
|
||||
isRequest: isRequest.results,
|
||||
setForceQuery,
|
||||
canDownload,
|
||||
queriesResponse,
|
||||
}).map((pane, idx) => {
|
||||
const tabKey =
|
||||
idx === 0 ? ResultTypes.Results : `${ResultTypes.Results} ${idx + 1}`;
|
||||
|
||||
@@ -56,7 +56,6 @@ export const ResultsPaneOnDashboard = ({
|
||||
dataSize = 50,
|
||||
canDownload,
|
||||
columnDisplayNames,
|
||||
queriesResponse,
|
||||
}: ResultsPaneProps) => {
|
||||
const resultsPanes = useResultsPane({
|
||||
errorMessage,
|
||||
@@ -69,7 +68,6 @@ export const ResultsPaneOnDashboard = ({
|
||||
isVisible,
|
||||
canDownload,
|
||||
columnDisplayNames,
|
||||
queriesResponse,
|
||||
});
|
||||
|
||||
if (resultsPanes.length === 1) {
|
||||
|
||||
@@ -20,11 +20,9 @@ import { useState, useEffect, useMemo, ReactElement, useCallback } from 'react';
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
ChartDataResponseResult,
|
||||
ensureIsArray,
|
||||
getChartMetadataRegistry,
|
||||
getClientErrorObject,
|
||||
QueryData,
|
||||
} from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
@@ -47,13 +45,6 @@ const StyledDiv = styled.div`
|
||||
|
||||
const cache = new WeakMap();
|
||||
|
||||
// `queriesResponse` is the loose `QueryData`; only reuse it when every entry is
|
||||
// a full v1 result (colnames/coltypes/data arrays), else fall back to the API.
|
||||
const isV1QueryResult = (query: QueryData): query is ChartDataResponseResult =>
|
||||
Array.isArray((query as ChartDataResponseResult).colnames) &&
|
||||
Array.isArray((query as ChartDataResponseResult).coltypes) &&
|
||||
Array.isArray((query as ChartDataResponseResult).data);
|
||||
|
||||
export const useResultsPane = ({
|
||||
isRequest,
|
||||
queryFormData,
|
||||
@@ -64,7 +55,6 @@ export const useResultsPane = ({
|
||||
isVisible,
|
||||
canDownload,
|
||||
columnDisplayNames,
|
||||
queriesResponse,
|
||||
}: ResultsPaneProps): ReactElement[] => {
|
||||
const metadata = getChartMetadataRegistry().get(
|
||||
queryFormData?.viz_type || queryFormData?.vizType,
|
||||
@@ -99,38 +89,7 @@ export const useResultsPane = ({
|
||||
useEffect(() => {
|
||||
// it's an invalid formData when gets a errorMessage
|
||||
if (errorMessage) return;
|
||||
if (!isRequest) return;
|
||||
|
||||
// The chart query and the results query produce identical SQL, so reuse the
|
||||
// chart's data instead of a second request. The chart always ran with a
|
||||
// row_limit >= effectiveRowLimit, so its first `effectiveRowLimit` rows
|
||||
// match what a dedicated results query would return — slice locally to keep
|
||||
// the row-limit dropdown working without a duplicate query.
|
||||
if (queriesResponse?.length && queriesResponse.every(isV1QueryResult)) {
|
||||
const mapped = queriesResponse.map(q => {
|
||||
const result = q as ChartDataResponseResult;
|
||||
const limitedData = ensureIsArray(result.data).slice(
|
||||
0,
|
||||
effectiveRowLimit,
|
||||
);
|
||||
return {
|
||||
colnames: result.colnames,
|
||||
coltypes: result.coltypes,
|
||||
data: limitedData,
|
||||
rowcount: limitedData.length,
|
||||
};
|
||||
}) as unknown as QueryResultInterface[];
|
||||
setResultResp(mapped);
|
||||
setResponseError('');
|
||||
if (queryForce) {
|
||||
setForceQuery?.(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: use cached data
|
||||
if (cache.has(cappedFormData)) {
|
||||
if (isRequest && cache.has(cappedFormData)) {
|
||||
setResultResp(
|
||||
ensureIsArray(cache.get(cappedFormData)) as QueryResultInterface[],
|
||||
);
|
||||
@@ -139,35 +98,34 @@ export const useResultsPane = ({
|
||||
setForceQuery?.(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: fetch from API (legacy charts without queriesResponse)
|
||||
setIsLoading(true);
|
||||
getChartDataRequest({
|
||||
formData: cappedFormData,
|
||||
force: queryForce,
|
||||
resultFormat: 'json',
|
||||
resultType: 'results',
|
||||
ownState,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setResultResp(ensureIsArray(json.result) as QueryResultInterface[]);
|
||||
setResponseError('');
|
||||
cache.set(cappedFormData, json.result);
|
||||
if (queryForce) {
|
||||
setForceQuery?.(false);
|
||||
}
|
||||
if (isRequest && !cache.has(cappedFormData)) {
|
||||
setIsLoading(true);
|
||||
getChartDataRequest({
|
||||
formData: cappedFormData,
|
||||
force: queryForce,
|
||||
resultFormat: 'json',
|
||||
resultType: 'results',
|
||||
ownState,
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(({ error, message }) => {
|
||||
setResponseError(error || message || t('Sorry, an error occurred'));
|
||||
.then(({ json }) => {
|
||||
setResultResp(ensureIsArray(json.result) as QueryResultInterface[]);
|
||||
setResponseError('');
|
||||
cache.set(cappedFormData, json.result);
|
||||
if (queryForce) {
|
||||
setForceQuery?.(false);
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(({ error, message }) => {
|
||||
setResponseError(error || message || t('Sorry, an error occurred'));
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [cappedFormData, isRequest, queriesResponse, effectiveRowLimit]);
|
||||
}
|
||||
}, [cappedFormData, isRequest]);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorMessage) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
import { DatasourceType, QueryData, VizType } from '@superset-ui/core';
|
||||
import { DatasourceType, VizType } from '@superset-ui/core';
|
||||
import { ChartStatus } from 'src/explore/types';
|
||||
import {
|
||||
DataTablesPaneProps,
|
||||
@@ -106,16 +106,12 @@ export const createResultsPaneOnDashboardProps = ({
|
||||
vizType = VizType.Table,
|
||||
queryForce = false,
|
||||
isRequest = true,
|
||||
queriesResponse,
|
||||
rowLimit,
|
||||
}: {
|
||||
sliceId: number;
|
||||
vizType?: string;
|
||||
errorMessage?: ReactElement;
|
||||
queryForce?: boolean;
|
||||
isRequest?: boolean;
|
||||
queriesResponse?: QueryData[] | null;
|
||||
rowLimit?: number;
|
||||
}) =>
|
||||
({
|
||||
isRequest,
|
||||
@@ -123,12 +119,10 @@ export const createResultsPaneOnDashboardProps = ({
|
||||
...queryFormData,
|
||||
slice_id: sliceId,
|
||||
viz_type: vizType,
|
||||
...(rowLimit !== undefined ? { row_limit: rowLimit } : {}),
|
||||
},
|
||||
queryForce,
|
||||
isVisible: true,
|
||||
setForceQuery: jest.fn(),
|
||||
errorMessage,
|
||||
canDownload: true,
|
||||
queriesResponse,
|
||||
}) as ResultsPaneProps;
|
||||
|
||||
@@ -1,201 +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 { screen, render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import { ResultsPaneOnDashboard } from '../components';
|
||||
import { createResultsPaneOnDashboardProps } from './fixture';
|
||||
|
||||
// Mocked so each test can assert whether the results branch hit the network.
|
||||
jest.mock('src/components/Chart/chartAction', () => ({
|
||||
getChartDataRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedGetChartDataRequest = getChartDataRequest as jest.Mock;
|
||||
|
||||
beforeAll(() => {
|
||||
setupAGGridModules();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('useResultsPane query data reuse', () => {
|
||||
beforeEach(() => {
|
||||
mockedGetChartDataRequest.mockReset();
|
||||
});
|
||||
|
||||
test('reuses queriesResponse from Redux and skips the results API call (v1 charts)', async () => {
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 201,
|
||||
queriesResponse: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: [{ genre: 'Action' }, { genre: 'Horror' }],
|
||||
rowcount: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
expect(await screen.findByText('Action')).toBeVisible();
|
||||
expect(screen.getByText('Horror')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('falls back to the results API for legacy charts without a typed queriesResponse', async () => {
|
||||
mockedGetChartDataRequest.mockResolvedValue({
|
||||
json: {
|
||||
result: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: [{ genre: 'Drama' }],
|
||||
rowcount: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const props = createResultsPaneOnDashboardProps({ sliceId: 202 });
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
expect(await screen.findByText('Drama')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('falls back to the results API when queriesResponse has no colnames', async () => {
|
||||
mockedGetChartDataRequest.mockResolvedValue({
|
||||
json: {
|
||||
result: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: [{ genre: 'Thriller' }],
|
||||
rowcount: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 203,
|
||||
queriesResponse: [{ data: [{ genre: 'ignored' }] }],
|
||||
});
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
expect(await screen.findByText('Thriller')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('falls back to the API when queriesResponse has colnames but not the full v1 shape', async () => {
|
||||
mockedGetChartDataRequest.mockResolvedValue({
|
||||
json: {
|
||||
result: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: [{ genre: 'Indie' }],
|
||||
rowcount: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 207,
|
||||
// colnames present but coltypes/data missing: not a real v1 result
|
||||
queriesResponse: [{ colnames: ['genre'] }],
|
||||
});
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
expect(await screen.findByText('Indie')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('reuse path honors the effective row limit (slices Redux data, no extra query)', async () => {
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 204,
|
||||
rowLimit: 2,
|
||||
queriesResponse: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: [
|
||||
{ genre: 'Action' },
|
||||
{ genre: 'Horror' },
|
||||
{ genre: 'Comedy' },
|
||||
{ genre: 'Drama' },
|
||||
{ genre: 'Sci-Fi' },
|
||||
],
|
||||
rowcount: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
expect(await screen.findByText('Action')).toBeVisible();
|
||||
expect(screen.getByText('Horror')).toBeVisible();
|
||||
expect(screen.queryByText('Comedy')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Sci-Fi')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('2 rows')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders an empty (0 rows) result from reused data without an API call', async () => {
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 205,
|
||||
queriesResponse: [
|
||||
{ colnames: ['genre'], coltypes: [1], data: [], rowcount: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
expect(await screen.findByText('0 rows')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('clears the force-query flag when reusing Redux data', async () => {
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 206,
|
||||
queryForce: true,
|
||||
queriesResponse: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: [{ genre: 'Action' }],
|
||||
rowcount: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const setForceQuery = jest.fn();
|
||||
|
||||
render(
|
||||
<ResultsPaneOnDashboard {...props} setForceQuery={setForceQuery} />,
|
||||
{
|
||||
useRedux: true,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => expect(setForceQuery).toHaveBeenCalledWith(false));
|
||||
expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { JsonObject, LatestQueryFormData, QueryData } from '@superset-ui/core';
|
||||
import { JsonObject, LatestQueryFormData } from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import type { ChartStatus, Datasource } from 'src/explore/types';
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface DataTablesPaneProps {
|
||||
errorMessage?: React.ReactNode;
|
||||
setForceQuery: SetForceQueryAction;
|
||||
canDownload: boolean;
|
||||
queriesResponse?: QueryData[] | null;
|
||||
}
|
||||
|
||||
export interface ResultsPaneProps {
|
||||
@@ -52,7 +51,6 @@ export interface ResultsPaneProps {
|
||||
canDownload: boolean;
|
||||
// Optional map of column/metric name -> verbose label
|
||||
columnDisplayNames?: Record<string, string>;
|
||||
queriesResponse?: QueryData[] | null;
|
||||
}
|
||||
|
||||
export interface SamplesPaneProps {
|
||||
|
||||
@@ -560,7 +560,6 @@ const ExploreChartPanel = ({
|
||||
errorMessage={errorMessage}
|
||||
setForceQuery={actions.setForceQuery}
|
||||
canDownload={canDownload}
|
||||
queriesResponse={chart.queriesResponse}
|
||||
/>
|
||||
</Split>
|
||||
{showDatasetModal && (
|
||||
|
||||
@@ -22,14 +22,16 @@ import { css } from '@apache-superset/core/theme';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
ConfirmStatusChange,
|
||||
Button,
|
||||
Dropdown,
|
||||
FaveStar,
|
||||
Icons,
|
||||
Label,
|
||||
ListViewCard,
|
||||
Icons,
|
||||
MenuItem,
|
||||
} from '@superset-ui/core/components';
|
||||
import Chart from 'src/types/Chart';
|
||||
import { FacePile, KebabMenuButton } from 'src/components';
|
||||
import { FacePile } from 'src/components';
|
||||
import { handleChartDelete, CardStyles } from 'src/views/CRUD/utils';
|
||||
import { assetUrl } from 'src/utils/assetUrl';
|
||||
import type { ListViewFetchDataConfig as FetchDataConfig } from 'src/components';
|
||||
@@ -205,7 +207,11 @@ export default function ChartCard({
|
||||
isStarred={favoriteStatus}
|
||||
/>
|
||||
)}
|
||||
<KebabMenuButton menuItems={menuItems} dataTest="chart-card-menu" />
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click', 'hover']}>
|
||||
<Button buttonSize="xsmall" type="link" buttonStyle="link">
|
||||
<Icons.MoreOutlined iconSize="xl" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -21,15 +21,17 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||
import { CardStyles } from 'src/views/CRUD/utils';
|
||||
import {
|
||||
Dropdown,
|
||||
Button,
|
||||
FaveStar,
|
||||
Icons,
|
||||
PublishedLabel,
|
||||
ListViewCard,
|
||||
} from '@superset-ui/core/components';
|
||||
import { MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Dashboard } from 'src/views/CRUD/types';
|
||||
import { assetUrl } from 'src/utils/assetUrl';
|
||||
import { FacePile, KebabMenuButton } from 'src/components';
|
||||
import { FacePile } from 'src/components';
|
||||
|
||||
interface DashboardCardProps {
|
||||
isChart?: boolean;
|
||||
@@ -162,10 +164,11 @@ function DashboardCard({
|
||||
isStarred={favoriteStatus}
|
||||
/>
|
||||
)}
|
||||
<KebabMenuButton
|
||||
menuItems={menuItems}
|
||||
dataTest="dashboard-card-menu"
|
||||
/>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['hover', 'click']}>
|
||||
<Button buttonSize="xsmall" buttonStyle="link">
|
||||
<Icons.MoreOutlined iconSize="xl" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -27,11 +27,16 @@ import CodeSyntaxHighlighter, {
|
||||
import { LoadingCards } from 'src/pages/Home';
|
||||
import { TableTab } from 'src/views/CRUD/types';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { DeleteModal, Icons, ListViewCard } from '@superset-ui/core/components';
|
||||
import {
|
||||
Dropdown,
|
||||
DeleteModal,
|
||||
Button,
|
||||
ListViewCard,
|
||||
} from '@superset-ui/core/components';
|
||||
import { MenuItem } from '@superset-ui/core/components/Menu';
|
||||
import { copyQueryLink, useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { User } from 'src/types/bootstrapTypes';
|
||||
import { KebabMenuButton } from 'src/components';
|
||||
import {
|
||||
CardContainer,
|
||||
createErrorHandler,
|
||||
@@ -339,10 +344,14 @@ export const SavedQueries = ({
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<KebabMenuButton
|
||||
menuItems={menuItems(q)}
|
||||
dataTest="saved-query-card-menu"
|
||||
/>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems(q) }}
|
||||
trigger={['click', 'hover']}
|
||||
>
|
||||
<Button buttonSize="xsmall" buttonStyle="link">
|
||||
<Icons.MoreOutlined iconSize="xl" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SupersetClient,
|
||||
getClientErrorObject,
|
||||
ClientErrorObject,
|
||||
sanitizeHtml,
|
||||
} from '@superset-ui/core';
|
||||
import setupErrorMessages from 'src/setup/setupErrorMessages';
|
||||
|
||||
@@ -41,7 +42,7 @@ function showApiMessage(resp: ClientErrorObject) {
|
||||
const severity = resp.severity || 'info';
|
||||
$(template)
|
||||
.addClass(`alert-${severity}`)
|
||||
.append(resp.message || '')
|
||||
.append(sanitizeHtml(resp.message || ''))
|
||||
.appendTo($('#alert-container'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user