From 42e1329fd218aca657db2c964275e440cac95d6a Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Tue, 9 Apr 2024 12:30:57 +0200 Subject: [PATCH] fix: useTruncation infinite loop, reenable dashboard cross links on ChartList (#27701) (cherry picked from commit ae0f2ce3c11aaeef9d8f3ee17ab68d4a4219ae81) --- .../cypress/e2e/chart_list/list.test.ts | 9 +- .../cypress/e2e/explore/chart.test.js | 4 +- .../useChildElementTruncation.test.ts | 196 ++++++++++++++---- .../useChildElementTruncation.ts | 122 +++++------ .../src/components/ListView/CrossLinks.tsx | 25 +-- .../ListView/DashboardCrossLinks.tsx | 37 ++++ .../src/components/TruncatedList/index.tsx | 10 +- .../FilterCard/DependenciesRow.tsx | 10 +- .../nativeFilters/FilterCard/NameRow.tsx | 5 +- .../nativeFilters/FilterCard/ScopeRow.tsx | 12 +- .../DashboardsSubMenu.test.tsx | 2 +- .../useExploreAdditionalActionsMenu/index.jsx | 2 +- .../src/pages/ChartList/index.tsx | 19 +- 13 files changed, 278 insertions(+), 175 deletions(-) create mode 100644 superset-frontend/src/components/ListView/DashboardCrossLinks.tsx diff --git a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts index 3cd1f91b49003..4e1dc17410ba7 100644 --- a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts @@ -54,7 +54,7 @@ function visitChartList() { } describe('Charts list', () => { - describe.skip('Cross-referenced dashboards', () => { + describe('Cross-referenced dashboards', () => { beforeEach(() => { cy.createSampleDashboards([0, 1, 2, 3]); cy.createSampleCharts([0]); @@ -112,9 +112,10 @@ describe('Charts list', () => { cy.getBySel('sort-header').eq(1).contains('Name'); cy.getBySel('sort-header').eq(2).contains('Type'); cy.getBySel('sort-header').eq(3).contains('Dataset'); - cy.getBySel('sort-header').eq(4).contains('Owners'); - cy.getBySel('sort-header').eq(5).contains('Last modified'); - cy.getBySel('sort-header').eq(6).contains('Actions'); + cy.getBySel('sort-header').eq(4).contains('On dashboards'); + cy.getBySel('sort-header').eq(5).contains('Owners'); + cy.getBySel('sort-header').eq(6).contains('Last modified'); + cy.getBySel('sort-header').eq(7).contains('Actions'); }); it('should sort correctly in list mode', () => { diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js index d198672ef3edb..14c386e0ea62f 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js +++ b/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js @@ -31,13 +31,13 @@ const SAMPLE_DASHBOARDS_INDEXES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; function openDashboardsAddedTo() { cy.getBySel('actions-trigger').click(); cy.get('.ant-dropdown-menu-submenu-title') - .contains('Dashboards added to') + .contains('On dashboards') .trigger('mouseover', { force: true }); } function closeDashboardsAddedTo() { cy.get('.ant-dropdown-menu-submenu-title') - .contains('Dashboards added to') + .contains('On dashboards') .trigger('mouseout', { force: true }); cy.getBySel('actions-trigger').click(); } diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts index ee3e95139f153..7441c259874c5 100644 --- a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts +++ b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts @@ -20,6 +20,10 @@ import { renderHook } from '@testing-library/react-hooks'; import { RefObject } from 'react'; import useChildElementTruncation from './useChildElementTruncation'; +let observeMock: jest.Mock; +let disconnectMock: jest.Mock; +let originalResizeObserver: typeof ResizeObserver; + const genElements = ( scrollWidth: number, clientWidth: number, @@ -34,26 +38,87 @@ const genElements = ( }; return [elementRef, plusRef]; }; -const useTruncation = (elementRef: any, plusRef: any) => - useChildElementTruncation( - elementRef as RefObject, - plusRef as RefObject, + +const testTruncationHookWithInitialValues = ( + [scrollWidth, clientWidth, offsetWidth, childNodes = []]: [ + number, + number, + number | undefined, + any?, + ], + expectedElementsTruncated: number, + shouldHaveHiddenElements: boolean, +) => { + const [elementRef, plusRef] = genElements( + scrollWidth, + clientWidth, + offsetWidth, + childNodes, ); + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: elementRef.current, + }); + Object.defineProperty(result.current[1], 'current', { + value: plusRef.current, + }); + + rerender(); + + expect(result.current).toEqual([ + elementRef, + plusRef, + expectedElementsTruncated, + shouldHaveHiddenElements, + ]); +}; + +beforeAll(() => { + // Store the original ResizeObserver + originalResizeObserver = window.ResizeObserver; + + // Mock ResizeObserver + observeMock = jest.fn(); + disconnectMock = jest.fn(); + window.ResizeObserver = jest.fn(() => ({ + observe: observeMock, + disconnect: disconnectMock, + })) as unknown as typeof ResizeObserver; +}); + +afterAll(() => { + // Restore original ResizeObserver after all tests are done + window.ResizeObserver = originalResizeObserver; +}); + +afterEach(() => { + observeMock.mockClear(); + disconnectMock.mockClear(); +}); test('should return [0, false] when elementRef.current is not defined', () => { - const { result } = renderHook(() => - useTruncation({ current: undefined }, { current: undefined }), - ); + const { result } = renderHook(() => useChildElementTruncation()); + expect(result.current).toEqual([ + { current: null }, + { current: null }, + 0, + false, + ]); - expect(result.current).toEqual([0, false]); + expect(observeMock).not.toHaveBeenCalled(); }); test('should not recompute when previousEffectInfo is the same as previous', () => { - const elementRef = { current: document.createElement('div') }; - const plusRef = { current: document.createElement('div') }; - const { result, rerender } = renderHook(() => - useTruncation(elementRef, plusRef), - ); + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: document.createElement('div'), + }); + Object.defineProperty(result.current[1], 'current', { + value: document.createElement('div'), + }); + const previousEffectInfo = result.current; rerender(); @@ -62,41 +127,96 @@ test('should not recompute when previousEffectInfo is the same as previous', () }); test('should return [0, false] when there are no truncated/hidden elements', () => { - const [elementRef, plusRef] = genElements(100, 100, 10); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([0, false]); + testTruncationHookWithInitialValues([100, 100, 10], 0, false); }); test('should return [1, false] when there is only one truncated element', () => { - const [elementRef, plusRef] = genElements(150, 100, 10); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([1, false]); + testTruncationHookWithInitialValues([150, 100, 10], 1, false); }); test('should return [1, true] with one truncated and hidden elements', () => { - const [elementRef, plusRef] = genElements(150, 100, 10, [ - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - ]); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([1, true]); + testTruncationHookWithInitialValues( + [ + 150, + 100, + 10, + [ + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + ], + ], + 1, + true, + ); }); test('should return [2, true] with 2 truncated and hidden elements', () => { - const [elementRef, plusRef] = genElements(150, 100, 10, [ - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - ]); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([2, true]); + testTruncationHookWithInitialValues( + [ + 150, + 100, + 10, + [ + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + ], + ], + 2, + true, + ); }); test('should return [1, true] with plusSize offsetWidth undefined', () => { - const [elementRef, plusRef] = genElements(150, 100, undefined, [ - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - ]); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([1, true]); + testTruncationHookWithInitialValues( + [ + 150, + 100, + undefined, + [ + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + ], + ], + 1, + true, + ); +}); + +test('should call ResizeObserver.observe on element parent', () => { + const elementRef = { current: document.createElement('div') }; + Object.defineProperty(elementRef.current, 'parentElement', { + value: document.createElement('div'), + }); + const plusRef = { current: document.createElement('div') }; + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: elementRef.current, + }); + Object.defineProperty(result.current[1], 'current', { + value: plusRef.current, + }); + + rerender(); + + expect(observeMock).toHaveBeenCalled(); + expect(observeMock).toHaveBeenCalledWith(elementRef.current.parentElement); +}); + +test('should not call ResizeObserver.observe if element parent is undefined', () => { + const elementRef = { current: document.createElement('div') }; + const plusRef = { current: document.createElement('div') }; + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: elementRef.current, + }); + Object.defineProperty(result.current[1], 'current', { + value: plusRef.current, + }); + + rerender(); + + expect(observeMock).not.toHaveBeenCalled(); }); diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts index 4f6b628642ab4..2c95aa98b0553 100644 --- a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts +++ b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { RefObject, useLayoutEffect, useState, useRef } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; /** * This hook encapsulates logic to support truncation of child HTML @@ -27,92 +27,68 @@ import { RefObject, useLayoutEffect, useState, useRef } from 'react'; * (including those completely hidden) and whether any elements * are completely hidden. */ -const useChildElementTruncation = ( - elementRef: RefObject, - plusRef?: RefObject, -) => { +const useChildElementTruncation = () => { const [elementsTruncated, setElementsTruncated] = useState(0); const [hasHiddenElements, setHasHiddenElements] = useState(false); - - const previousEffectInfoRef = useRef({ - scrollWidth: 0, - parentElementWidth: 0, - plusRefWidth: 0, - }); + const elementRef = useRef(null); + const plusRef = useRef(null); useLayoutEffect(() => { - const currentElement = elementRef.current; - const plusRefElement = plusRef?.current; - - if (!currentElement) { - return; - } - - const { scrollWidth, clientWidth, childNodes } = currentElement; - - // By using the result of this effect to truncate content - // we're effectively changing it's size. - // That will trigger another pass at this effect. - // Depending on the content elements width, that second rerender could - // yield a different truncate count, thus potentially leading to a - // rendering loop. - // There's only a need to recompute if the parent width or the width of - // the child nodes changes. - const previousEffectInfo = previousEffectInfoRef.current; - const parentElementWidth = currentElement.parentElement?.clientWidth || 0; - const plusRefWidth = plusRefElement?.offsetWidth || 0; - previousEffectInfoRef.current = { - scrollWidth, - parentElementWidth, - plusRefWidth, - }; - - if ( - previousEffectInfo.parentElementWidth === parentElementWidth && - previousEffectInfo.scrollWidth === scrollWidth && - previousEffectInfo.plusRefWidth === plusRefWidth - ) { - return; - } + const onResize = () => { + const currentElement = elementRef.current; + if (!currentElement) { + return; + } + const plusRefElement = plusRef.current; + const { scrollWidth, clientWidth, childNodes } = currentElement; - if (scrollWidth > clientWidth) { - // "..." is around 6px wide - const truncationWidth = 6; - const plusSize = plusRefElement?.offsetWidth || 0; - const maxWidth = clientWidth - truncationWidth; - const elementsCount = childNodes.length; + if (scrollWidth > clientWidth) { + // "..." is around 6px wide + const truncationWidth = 6; + const plusSize = plusRefElement?.offsetWidth || 0; + const maxWidth = clientWidth - truncationWidth; + const elementsCount = childNodes.length; - let width = 0; - let hiddenElements = 0; - for (let i = 0; i < elementsCount; i += 1) { - const itemWidth = (childNodes[i] as HTMLElement).offsetWidth; - const remainingWidth = maxWidth - truncationWidth - width - plusSize; + let width = 0; + let hiddenElements = 0; + for (let i = 0; i < elementsCount; i += 1) { + const itemWidth = (childNodes[i] as HTMLElement).offsetWidth; + const remainingWidth = maxWidth - width - plusSize; - // assures it shows +{number} only when the item is not visible - if (remainingWidth <= 0) { - hiddenElements += 1; + // assures it shows +{number} only when the item is not visible + if (remainingWidth <= 0) { + hiddenElements += 1; + } + width += itemWidth; } - width += itemWidth; - } - if (elementsCount > 1 && hiddenElements) { - setHasHiddenElements(true); - setElementsTruncated(hiddenElements); + if (elementsCount > 1 && hiddenElements) { + setHasHiddenElements(true); + setElementsTruncated(hiddenElements); + } else { + setHasHiddenElements(false); + setElementsTruncated(1); + } } else { setHasHiddenElements(false); - setElementsTruncated(1); + setElementsTruncated(0); } - } else { - setHasHiddenElements(false); - setElementsTruncated(0); + }; + const obs = new ResizeObserver(onResize); + + const element = elementRef.current?.parentElement; + if (element) { + obs.observe(element); } - }, [ - elementRef.current?.offsetWidth, - elementRef.current?.clientWidth, - elementRef, - ]); - return [elementsTruncated, hasHiddenElements]; + onResize(); + + return () => { + obs.disconnect(); + }; + }, [plusRef.current]); // plus is rendered dynamically - the component rerenders the hook when plus appears, this makes sure that useLayoutEffect is rerun + + return [elementRef, plusRef, elementsTruncated, hasHiddenElements] as const; }; export default useChildElementTruncation; diff --git a/superset-frontend/src/components/ListView/CrossLinks.tsx b/superset-frontend/src/components/ListView/CrossLinks.tsx index e3157506742bc..6b3eb5e4b1505 100644 --- a/superset-frontend/src/components/ListView/CrossLinks.tsx +++ b/superset-frontend/src/components/ListView/CrossLinks.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo, useRef } from 'react'; +import React, { useMemo } from 'react'; import { styled, useTruncation } from '@superset-ui/core'; import { Link } from 'react-router-dom'; import CrossLinksTooltip from './CrossLinksTooltip'; @@ -60,17 +60,13 @@ const StyledCrossLinks = styled.div` `} `; -export default function CrossLinks({ +function CrossLinks({ crossLinks, maxLinks = 20, linkPrefix = '/superset/dashboard/', }: CrossLinksProps) { - const crossLinksRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - crossLinksRef, - plusRef, - ); + const [crossLinksRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const hasMoreItems = useMemo( () => crossLinks.length > maxLinks ? crossLinks.length - maxLinks : undefined, @@ -80,18 +76,13 @@ export default function CrossLinks({ () => ( {crossLinks.map((link, index) => ( - + {index === 0 ? link.title : `, ${link.title}`} ))} ), - [crossLinks], + [crossLinks, crossLinksRef, linkPrefix], ); const tooltipLinks = useMemo( () => @@ -99,7 +90,7 @@ export default function CrossLinks({ title: l.title, to: linkPrefix + l.id, })), - [crossLinks, maxLinks], + [crossLinks, linkPrefix, maxLinks], ); return ( @@ -119,3 +110,5 @@ export default function CrossLinks({ ); } + +export default React.memo(CrossLinks); diff --git a/superset-frontend/src/components/ListView/DashboardCrossLinks.tsx b/superset-frontend/src/components/ListView/DashboardCrossLinks.tsx new file mode 100644 index 0000000000000..409f24bfb72f5 --- /dev/null +++ b/superset-frontend/src/components/ListView/DashboardCrossLinks.tsx @@ -0,0 +1,37 @@ +/** + * 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, { useMemo } from 'react'; +import { ensureIsArray } from '@superset-ui/core'; +import { ChartLinkedDashboard } from 'src/types/Chart'; +import CrossLinks from './CrossLinks'; + +export const DashboardCrossLinks = React.memo( + ({ dashboards }: { dashboards: ChartLinkedDashboard[] }) => { + const crossLinks = useMemo( + () => + ensureIsArray(dashboards).map((d: ChartLinkedDashboard) => ({ + title: d.dashboard_title, + id: d.id, + })), + [dashboards], + ); + return ; + }, +); diff --git a/superset-frontend/src/components/TruncatedList/index.tsx b/superset-frontend/src/components/TruncatedList/index.tsx index 00e0acc0c37c2..883a5ad61e7a1 100644 --- a/superset-frontend/src/components/TruncatedList/index.tsx +++ b/superset-frontend/src/components/TruncatedList/index.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { ReactNode, useMemo, useRef } from 'react'; +import React, { ReactNode, useMemo } from 'react'; import { styled, t, useTruncation } from '@superset-ui/core'; import { Tooltip } from '../Tooltip'; @@ -99,12 +99,8 @@ export default function TruncatedList({ getKey = item => item as unknown as React.Key, maxLinks = 20, }: TruncatedListProps) { - const itemsNotInTooltipRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - itemsNotInTooltipRef, - plusRef, - ) as [number, boolean]; + const [itemsNotInTooltipRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const nMoreItems = useMemo( () => (items.length > maxLinks ? items.length - maxLinks : undefined), diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx index 253ce4649d988..3ac76882ba0f9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useCallback, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { css, t, useTheme, useTruncation } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -53,12 +53,8 @@ const DependencyValue = ({ export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => { const dependencies = useFilterDependencies(filter); - const dependenciesRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - dependenciesRef, - plusRef, - ); + const [dependenciesRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const theme = useTheme(); const tooltipText = useMemo( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx index 37f18eda29e12..58e9969b91d12 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useRef } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { css, SupersetTheme, useTheme, useTruncation } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -31,8 +31,7 @@ export const NameRow = ({ hidePopover, }: FilterCardRowProps & { hidePopover: () => void }) => { const theme = useTheme(); - const filterNameRef = useRef(null); - const [elementsTruncated] = useTruncation(filterNameRef); + const [filterNameRef, , elementsTruncated] = useTruncation(); const dashboardId = useSelector( ({ dashboardInfo }) => dashboardInfo.id, ); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx index ff5c1142a5387..910fb99aaa615 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo, useRef } from 'react'; +import React, { useMemo } from 'react'; import { t, useTruncation } from '@superset-ui/core'; import { useFilterScope } from './useFilterScope'; import { @@ -44,13 +44,9 @@ const getTooltipSection = (items: string[] | undefined, label: string) => export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => { const scope = useFilterScope(filter); - const scopeRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - scopeRef, - plusRef, - ); + const [scopeRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const tooltipText = useMemo(() => { if (elementsTruncated === 0 || !scope) { return null; @@ -81,7 +77,7 @@ export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => { )) : t('None')} - {hasHiddenElements > 0 && ( + {hasHiddenElements && ( +{elementsTruncated} diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx index aea0b4a8e598e..4b14d5600cb3c 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/DashboardsSubMenu.test.tsx @@ -30,7 +30,7 @@ const asyncRender = (numberOfItems: number) => } render( - + , diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx index 0d30e5735595e..6ec0cf62d2a88 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx @@ -310,7 +310,7 @@ export const useExploreAdditionalActionsMenu = ( )} ( - ({ - title: d.dashboard_title, - id: d.id, - }), - )} - /> - ), - Header: t('Dashboards added to'), + }: any) => , + Header: t('On dashboards'), accessor: 'dashboards', disableSortBy: true, size: 'xxl', - hidden: true, }, { Cell: ({