feat: Shows related dashboards in Explore (#21685)
This commit is contained in:
parent
0ff1e49e3c
commit
200bed6f7e
|
|
@ -35,7 +35,7 @@ describe('Download Chart > Distribution bar chart', () => {
|
||||||
|
|
||||||
cy.visitChartByParams(formData);
|
cy.visitChartByParams(formData);
|
||||||
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
cy.get('.header-with-actions .ant-dropdown-trigger').click();
|
||||||
cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click();
|
cy.get(':nth-child(3) > .ant-dropdown-menu-submenu-title').click();
|
||||||
cy.get(
|
cy.get(
|
||||||
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',
|
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',
|
||||||
).click();
|
).click();
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,10 @@ export const URL_PARAMS = {
|
||||||
name: 'dashboard_page_id',
|
name: 'dashboard_page_id',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
dashboardFocusedChart: {
|
||||||
|
name: 'focused_chart',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const RESERVED_CHART_URL_PARAMS: string[] = [
|
export const RESERVED_CHART_URL_PARAMS: string[] = [
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
||||||
|
|
||||||
export const hydrateDashboard =
|
export const hydrateDashboard =
|
||||||
({
|
({
|
||||||
|
history,
|
||||||
dashboard,
|
dashboard,
|
||||||
charts,
|
charts,
|
||||||
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||||
|
|
@ -291,8 +292,25 @@ export const hydrateDashboard =
|
||||||
future: [],
|
future: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Searches for a focused_chart parameter in the URL to automatically focus a chart
|
||||||
|
const focusedChartId = getUrlParam(URL_PARAMS.dashboardFocusedChart);
|
||||||
|
let focusedChartLayoutId;
|
||||||
|
if (focusedChartId) {
|
||||||
|
// Converts focused_chart to dashboard layout id
|
||||||
|
const found = Object.values(dashboardLayout.present).find(
|
||||||
|
element => element.meta?.chartId === focusedChartId,
|
||||||
|
);
|
||||||
|
focusedChartLayoutId = found?.id;
|
||||||
|
// Removes the focused_chart parameter from the URL
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.delete(URL_PARAMS.dashboardFocusedChart.name);
|
||||||
|
history.replace({
|
||||||
|
search: params.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// find direct link component and path from root
|
// find direct link component and path from root
|
||||||
const directLinkComponentId = getLocationHash();
|
const directLinkComponentId = focusedChartLayoutId || getLocationHash();
|
||||||
let directPathToChild = dashboardState.directPathToChild || [];
|
let directPathToChild = dashboardState.directPathToChild || [];
|
||||||
if (layout[directLinkComponentId]) {
|
if (layout[directLinkComponentId]) {
|
||||||
directPathToChild = (layout[directLinkComponentId].parents || []).slice();
|
directPathToChild = (layout[directLinkComponentId].parents || []).slice();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
CategoricalColorNamespace,
|
CategoricalColorNamespace,
|
||||||
FeatureFlag,
|
FeatureFlag,
|
||||||
|
|
@ -155,6 +156,7 @@ const useSyncDashboardStateWithLocalStorage = () => {
|
||||||
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const history = useHistory();
|
||||||
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
||||||
state => state.user,
|
state => state.user,
|
||||||
);
|
);
|
||||||
|
|
@ -301,6 +303,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
}
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
hydrateDashboard({
|
hydrateDashboard({
|
||||||
|
history,
|
||||||
dashboard,
|
dashboard,
|
||||||
charts,
|
charts,
|
||||||
activeTabs,
|
activeTabs,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import { Tooltip } from 'src/components/Tooltip';
|
||||||
import {
|
import {
|
||||||
CategoricalColorNamespace,
|
CategoricalColorNamespace,
|
||||||
css,
|
css,
|
||||||
|
FeatureFlag,
|
||||||
|
isFeatureEnabled,
|
||||||
logging,
|
logging,
|
||||||
SupersetClient,
|
SupersetClient,
|
||||||
t,
|
t,
|
||||||
|
|
@ -149,6 +151,7 @@ export const ExploreChartHeader = ({
|
||||||
actions.redirectSQLLab,
|
actions.redirectSQLLab,
|
||||||
openPropertiesModal,
|
openPropertiesModal,
|
||||||
ownState,
|
ownState,
|
||||||
|
metadata?.dashboards,
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadataBar = useMemo(() => {
|
const metadataBar = useMemo(() => {
|
||||||
|
|
@ -162,6 +165,13 @@ export const ExploreChartHeader = ({
|
||||||
metadata.dashboards.length > 0
|
metadata.dashboards.length > 0
|
||||||
? t('Added to %s dashboard(s)', metadata.dashboards.length)
|
? t('Added to %s dashboard(s)', metadata.dashboards.length)
|
||||||
: t('Not added to any dashboard'),
|
: t('Not added to any dashboard'),
|
||||||
|
description:
|
||||||
|
metadata.dashboards.length > 0 &&
|
||||||
|
isFeatureEnabled(FeatureFlag.CROSS_REFERENCES)
|
||||||
|
? t(
|
||||||
|
'You can preview the list of dashboards on the chart settings dropdown.',
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
type: MetadataType.LAST_MODIFIED,
|
type: MetadataType.LAST_MODIFIED,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { Menu } from 'src/components/Menu';
|
||||||
|
import DashboardItems from './DashboardsSubMenu';
|
||||||
|
|
||||||
|
const asyncRender = (numberOfItems: number) =>
|
||||||
|
waitFor(() => {
|
||||||
|
const dashboards = [];
|
||||||
|
for (let i = 1; i <= numberOfItems; i += 1) {
|
||||||
|
dashboards.push({ id: i, dashboard_title: `Dashboard ${i}` });
|
||||||
|
}
|
||||||
|
render(
|
||||||
|
<Menu openKeys={['menu']}>
|
||||||
|
<Menu.SubMenu title="Dashboards added to" key="menu">
|
||||||
|
<DashboardItems key="menu" dashboards={dashboards} />
|
||||||
|
</Menu.SubMenu>
|
||||||
|
</Menu>,
|
||||||
|
{
|
||||||
|
useRouter: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a submenu', async () => {
|
||||||
|
await asyncRender(3);
|
||||||
|
expect(screen.getByText('Dashboard 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Dashboard 2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Dashboard 3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a submenu with search', async () => {
|
||||||
|
await asyncRender(20);
|
||||||
|
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays a searched value', async () => {
|
||||||
|
await asyncRender(20);
|
||||||
|
userEvent.type(screen.getByPlaceholderText('Search'), '2');
|
||||||
|
expect(screen.getByText('Dashboard 2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Dashboard 20')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a "No results found" message when searching', async () => {
|
||||||
|
await asyncRender(20);
|
||||||
|
userEvent.type(screen.getByPlaceholderText('Search'), 'unknown');
|
||||||
|
expect(screen.getByText('No results found')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a submenu with no dashboards', async () => {
|
||||||
|
await asyncRender(0);
|
||||||
|
expect(screen.getByText('None')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows link icon when hovering', async () => {
|
||||||
|
await asyncRender(3);
|
||||||
|
expect(screen.queryByRole('img', { name: 'full' })).not.toBeInTheDocument();
|
||||||
|
userEvent.hover(screen.getByText('Dashboard 1'));
|
||||||
|
expect(screen.getByRole('img', { name: 'full' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
/**
|
||||||
|
* 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 React, { useState } from 'react';
|
||||||
|
import { css, t, useTheme } from '@superset-ui/core';
|
||||||
|
import { Input } from 'src/components/Input';
|
||||||
|
import Icons from 'src/components/Icons';
|
||||||
|
import { Menu } from 'src/components/Menu';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface DashboardsSubMenuProps {
|
||||||
|
chartId?: number;
|
||||||
|
dashboards?: { id: number; dashboard_title: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const WIDTH = 220;
|
||||||
|
const HEIGHT = 300;
|
||||||
|
const SEARCH_THRESHOLD = 10;
|
||||||
|
|
||||||
|
const DashboardsSubMenu = ({
|
||||||
|
chartId,
|
||||||
|
dashboards = [],
|
||||||
|
...menuProps
|
||||||
|
}: DashboardsSubMenuProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [dashboardSearch, setDashboardSearch] = useState<string>();
|
||||||
|
const [hoveredItem, setHoveredItem] = useState<number | null>();
|
||||||
|
const showSearch = dashboards.length > SEARCH_THRESHOLD;
|
||||||
|
const filteredDashboards = dashboards.filter(
|
||||||
|
dashboard =>
|
||||||
|
!dashboardSearch ||
|
||||||
|
dashboard.dashboard_title
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(dashboardSearch.toLowerCase()),
|
||||||
|
);
|
||||||
|
const noResults = dashboards.length === 0;
|
||||||
|
const noResultsFound = dashboardSearch && filteredDashboards.length === 0;
|
||||||
|
const urlQueryString = chartId ? `?focused_chart=${chartId}` : '';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showSearch && (
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
placeholder={t('Search')}
|
||||||
|
prefix={<Icons.Search iconSize="l" />}
|
||||||
|
css={css`
|
||||||
|
width: ${WIDTH}px;
|
||||||
|
margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
|
||||||
|
`}
|
||||||
|
value={dashboardSearch}
|
||||||
|
onChange={e => setDashboardSearch(e.currentTarget.value?.trim())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
max-height: ${HEIGHT}px;
|
||||||
|
overflow: auto;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{filteredDashboards.map(dashboard => (
|
||||||
|
<Menu.Item
|
||||||
|
key={String(dashboard.id)}
|
||||||
|
onMouseEnter={() => setHoveredItem(dashboard.id)}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (hoveredItem === dashboard.id) {
|
||||||
|
setHoveredItem(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...menuProps}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferer noopener"
|
||||||
|
to={`/superset/dashboard/${dashboard.id}${urlQueryString}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
max-width: ${WIDTH}px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
white-space: normal;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{dashboard.dashboard_title}
|
||||||
|
</div>
|
||||||
|
<Icons.Full
|
||||||
|
iconSize="l"
|
||||||
|
iconColor={theme.colors.grayscale.base}
|
||||||
|
css={css`
|
||||||
|
margin-left: ${theme.gridUnit * 2}px;
|
||||||
|
visibility: ${hoveredItem === dashboard.id
|
||||||
|
? 'visible'
|
||||||
|
: 'hidden'};
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
{noResultsFound && (
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
margin-left: ${theme.gridUnit * 3}px;
|
||||||
|
margin-bottom: ${theme.gridUnit}px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{t('No results found')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{noResults && (
|
||||||
|
<Menu.Item
|
||||||
|
disabled
|
||||||
|
css={css`
|
||||||
|
min-width: ${WIDTH}px;
|
||||||
|
`}
|
||||||
|
{...menuProps}
|
||||||
|
>
|
||||||
|
{t('None')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardsSubMenu;
|
||||||
|
|
@ -32,9 +32,11 @@ import HeaderReportDropDown from 'src/components/ReportModal/HeaderReportDropdow
|
||||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||||
import ViewQueryModal from '../controls/ViewQueryModal';
|
import ViewQueryModal from '../controls/ViewQueryModal';
|
||||||
import EmbedCodeContent from '../EmbedCodeContent';
|
import EmbedCodeContent from '../EmbedCodeContent';
|
||||||
|
import DashboardsSubMenu from './DashboardsSubMenu';
|
||||||
|
|
||||||
const MENU_KEYS = {
|
const MENU_KEYS = {
|
||||||
EDIT_PROPERTIES: 'edit_properties',
|
EDIT_PROPERTIES: 'edit_properties',
|
||||||
|
DASHBOARDS_ADDED_TO: 'dashboards_added_to',
|
||||||
DOWNLOAD_SUBMENU: 'download_submenu',
|
DOWNLOAD_SUBMENU: 'download_submenu',
|
||||||
EXPORT_TO_CSV: 'export_to_csv',
|
EXPORT_TO_CSV: 'export_to_csv',
|
||||||
EXPORT_TO_CSV_PIVOTED: 'export_to_csv_pivoted',
|
EXPORT_TO_CSV_PIVOTED: 'export_to_csv_pivoted',
|
||||||
|
|
@ -97,6 +99,7 @@ export const useExploreAdditionalActionsMenu = (
|
||||||
onOpenInEditor,
|
onOpenInEditor,
|
||||||
onOpenPropertiesModal,
|
onOpenPropertiesModal,
|
||||||
ownState,
|
ownState,
|
||||||
|
dashboards,
|
||||||
) => {
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { addDangerToast, addSuccessToast } = useToasts();
|
const { addDangerToast, addSuccessToast } = useToasts();
|
||||||
|
|
@ -246,14 +249,25 @@ export const useExploreAdditionalActionsMenu = (
|
||||||
openKeys={openSubmenus}
|
openKeys={openSubmenus}
|
||||||
onOpenChange={setOpenSubmenus}
|
onOpenChange={setOpenSubmenus}
|
||||||
>
|
>
|
||||||
{slice && (
|
<>
|
||||||
<>
|
{slice && (
|
||||||
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||||
{t('Edit chart properties')}
|
{t('Edit chart properties')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
)}
|
||||||
</>
|
{isFeatureEnabled(FeatureFlag.CROSS_REFERENCES) && (
|
||||||
)}
|
<Menu.SubMenu
|
||||||
|
title={t('Dashboards added to')}
|
||||||
|
key={MENU_KEYS.DASHBOARDS_ADDED_TO}
|
||||||
|
>
|
||||||
|
<DashboardsSubMenu
|
||||||
|
chartId={slice?.slice_id}
|
||||||
|
dashboards={dashboards}
|
||||||
|
/>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
)}
|
||||||
|
<Menu.Divider />
|
||||||
|
</>
|
||||||
<Menu.SubMenu title={t('Download')} key={MENU_KEYS.DOWNLOAD_SUBMENU}>
|
<Menu.SubMenu title={t('Download')} key={MENU_KEYS.DOWNLOAD_SUBMENU}>
|
||||||
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
|
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -369,6 +383,7 @@ export const useExploreAdditionalActionsMenu = (
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
canDownloadCSV,
|
canDownloadCSV,
|
||||||
chart,
|
chart,
|
||||||
|
dashboards,
|
||||||
handleMenuClick,
|
handleMenuClick,
|
||||||
isDropdownVisible,
|
isDropdownVisible,
|
||||||
latestQueryFormData,
|
latestQueryFormData,
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ FEATURE_FLAGS = {
|
||||||
"ALERT_REPORTS": True,
|
"ALERT_REPORTS": True,
|
||||||
"DASHBOARD_NATIVE_FILTERS": True,
|
"DASHBOARD_NATIVE_FILTERS": True,
|
||||||
"DRILL_TO_DETAIL": True,
|
"DRILL_TO_DETAIL": True,
|
||||||
|
"CROSS_REFERENCES": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
WEBDRIVER_BASEURL = "http://0.0.0.0:8081/"
|
WEBDRIVER_BASEURL = "http://0.0.0.0:8081/"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue