Skip to content

Commit

Permalink
feat: Shows related dashboards in Explore (#21685)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-s-molina committed Oct 7, 2022
1 parent 0ff1e49 commit 200bed6
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions superset-frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down
20 changes: 19 additions & 1 deletion superset-frontend/src/dashboard/actions/hydrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';

export const hydrateDashboard =
({
history,
dashboard,
charts,
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions superset-frontend/src/dashboard/containers/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -301,6 +303,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
}
dispatch(
hydrateDashboard({
history,
dashboard,
charts,
activeTabs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { Tooltip } from 'src/components/Tooltip';
import {
CategoricalColorNamespace,
css,
FeatureFlag,
isFeatureEnabled,
logging,
SupersetClient,
t,
Expand Down Expand Up @@ -149,6 +151,7 @@ export const ExploreChartHeader = ({
actions.redirectSQLLab,
openPropertiesModal,
ownState,
metadata?.dashboards,
);

const metadataBar = useMemo(() => {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -97,6 +99,7 @@ export const useExploreAdditionalActionsMenu = (
onOpenInEditor,
onOpenPropertiesModal,
ownState,
dashboards,
) => {
const theme = useTheme();
const { addDangerToast, addSuccessToast } = useToasts();
Expand Down Expand Up @@ -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) ? (
<>
Expand Down Expand Up @@ -369,6 +383,7 @@ export const useExploreAdditionalActionsMenu = (
addDangerToast,
canDownloadCSV,
chart,
dashboards,
handleMenuClick,
isDropdownVisible,
latestQueryFormData,
Expand Down

0 comments on commit 200bed6

Please sign in to comment.