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.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(
|
||||
'.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)',
|
||||
).click();
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ export const URL_PARAMS = {
|
|||
name: 'dashboard_page_id',
|
||||
type: 'string',
|
||||
},
|
||||
dashboardFocusedChart: {
|
||||
name: 'focused_chart',
|
||||
type: 'number',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const RESERVED_CHART_URL_PARAMS: string[] = [
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
|||
|
||||
export const hydrateDashboard =
|
||||
({
|
||||
history,
|
||||
dashboard,
|
||||
charts,
|
||||
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
|
|
@ -291,8 +292,25 @@ export const hydrateDashboard =
|
|||
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
|
||||
const directLinkComponentId = getLocationHash();
|
||||
const directLinkComponentId = focusedChartLayoutId || getLocationHash();
|
||||
let directPathToChild = dashboardState.directPathToChild || [];
|
||||
if (layout[directLinkComponentId]) {
|
||||
directPathToChild = (layout[directLinkComponentId].parents || []).slice();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
FeatureFlag,
|
||||
|
|
@ -155,6 +156,7 @@ const useSyncDashboardStateWithLocalStorage = () => {
|
|||
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
||||
state => state.user,
|
||||
);
|
||||
|
|
@ -301,6 +303,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
|||
}
|
||||
dispatch(
|
||||
hydrateDashboard({
|
||||
history,
|
||||
dashboard,
|
||||
charts,
|
||||
activeTabs,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import { Tooltip } from 'src/components/Tooltip';
|
|||
import {
|
||||
CategoricalColorNamespace,
|
||||
css,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
logging,
|
||||
SupersetClient,
|
||||
t,
|
||||
|
|
@ -149,6 +151,7 @@ export const ExploreChartHeader = ({
|
|||
actions.redirectSQLLab,
|
||||
openPropertiesModal,
|
||||
ownState,
|
||||
metadata?.dashboards,
|
||||
);
|
||||
|
||||
const metadataBar = useMemo(() => {
|
||||
|
|
@ -162,6 +165,13 @@ export const ExploreChartHeader = ({
|
|||
metadata.dashboards.length > 0
|
||||
? t('Added to %s dashboard(s)', metadata.dashboards.length)
|
||||
: 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({
|
||||
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 ViewQueryModal from '../controls/ViewQueryModal';
|
||||
import EmbedCodeContent from '../EmbedCodeContent';
|
||||
import DashboardsSubMenu from './DashboardsSubMenu';
|
||||
|
||||
const MENU_KEYS = {
|
||||
EDIT_PROPERTIES: 'edit_properties',
|
||||
DASHBOARDS_ADDED_TO: 'dashboards_added_to',
|
||||
DOWNLOAD_SUBMENU: 'download_submenu',
|
||||
EXPORT_TO_CSV: 'export_to_csv',
|
||||
EXPORT_TO_CSV_PIVOTED: 'export_to_csv_pivoted',
|
||||
|
|
@ -97,6 +99,7 @@ export const useExploreAdditionalActionsMenu = (
|
|||
onOpenInEditor,
|
||||
onOpenPropertiesModal,
|
||||
ownState,
|
||||
dashboards,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const { addDangerToast, addSuccessToast } = useToasts();
|
||||
|
|
@ -246,14 +249,25 @@ export const useExploreAdditionalActionsMenu = (
|
|||
openKeys={openSubmenus}
|
||||
onOpenChange={setOpenSubmenus}
|
||||
>
|
||||
{slice && (
|
||||
<>
|
||||
<>
|
||||
{slice && (
|
||||
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||
{t('Edit chart properties')}
|
||||
</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}>
|
||||
{VIZ_TYPES_PIVOTABLE.includes(latestQueryFormData.viz_type) ? (
|
||||
<>
|
||||
|
|
@ -369,6 +383,7 @@ export const useExploreAdditionalActionsMenu = (
|
|||
addDangerToast,
|
||||
canDownloadCSV,
|
||||
chart,
|
||||
dashboards,
|
||||
handleMenuClick,
|
||||
isDropdownVisible,
|
||||
latestQueryFormData,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ FEATURE_FLAGS = {
|
|||
"ALERT_REPORTS": True,
|
||||
"DASHBOARD_NATIVE_FILTERS": True,
|
||||
"DRILL_TO_DETAIL": True,
|
||||
"CROSS_REFERENCES": True,
|
||||
}
|
||||
|
||||
WEBDRIVER_BASEURL = "http://0.0.0.0:8081/"
|
||||
|
|
|
|||
Loading…
Reference in New Issue