From 49b48eeca484fafa2f1c5ef2520694b1bdf5c522 Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Thu, 13 Oct 2022 14:50:03 +0300 Subject: [PATCH] feat: Cross-referenced Dashboards in Chart list (Column + Filter) (#21760) Co-authored-by: Kamil Gabryjelski --- .../integration/chart_list/filter.test.ts | 14 ++ .../integration/chart_list/list.test.ts | 9 +- .../components/ListView/CrossLinks.test.tsx | 97 ++++++++++++++ .../src/components/ListView/CrossLinks.tsx | 122 ++++++++++++++++++ .../ListView/CrossLinksTooltip.test.tsx | 89 +++++++++++++ .../components/ListView/CrossLinksTooltip.tsx | 73 +++++++++++ .../FilterCard/DependenciesRow.tsx | 12 +- .../nativeFilters/FilterCard/NameRow.tsx | 2 +- .../nativeFilters/FilterCard/ScopeRow.tsx | 12 +- .../useTruncation/index.ts} | 44 +++++-- .../src/views/CRUD/chart/ChartList.tsx | 99 ++++++++++++++ 11 files changed, 550 insertions(+), 23 deletions(-) create mode 100644 superset-frontend/src/components/ListView/CrossLinks.test.tsx create mode 100644 superset-frontend/src/components/ListView/CrossLinks.tsx create mode 100644 superset-frontend/src/components/ListView/CrossLinksTooltip.test.tsx create mode 100644 superset-frontend/src/components/ListView/CrossLinksTooltip.tsx rename superset-frontend/src/{dashboard/components/nativeFilters/FilterCard/useTruncation.ts => hooks/useTruncation/index.ts} (71%) diff --git a/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts index eff415107901e..7bd0891cbf268 100644 --- a/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts @@ -62,6 +62,13 @@ describe('Charts filters', () => { setFilter('Dataset', 'unicode_test'); cy.getBySel('styled-card').should('have.length', 1); }); + + it('should filter by dashboards correctly', () => { + setFilter('Dashboards', 'Unicode Test'); + cy.getBySel('styled-card').should('have.length', 1); + setFilter('Dashboards', 'Tabbed Dashboard'); + cy.getBySel('styled-card').should('have.length', 8); + }); }); describe('list-view', () => { @@ -96,5 +103,12 @@ describe('Charts filters', () => { setFilter('Dataset', 'unicode_test'); cy.getBySel('table-row').should('have.length', 1); }); + + it('should filter by dashboards correctly', () => { + setFilter('Dashboards', 'Unicode Test'); + cy.getBySel('table-row').should('have.length', 1); + setFilter('Dashboards', 'Tabbed Dashboard'); + cy.getBySel('table-row').should('have.length', 8); + }); }); }); diff --git a/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts b/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts index e3837445d9522..6981ead73ab9b 100644 --- a/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts @@ -59,10 +59,11 @@ describe('Charts list', () => { cy.getBySel('sort-header').eq(1).contains('Chart'); cy.getBySel('sort-header').eq(2).contains('Visualization type'); cy.getBySel('sort-header').eq(3).contains('Dataset'); - cy.getBySel('sort-header').eq(4).contains('Modified by'); - cy.getBySel('sort-header').eq(5).contains('Last modified'); - cy.getBySel('sort-header').eq(6).contains('Created by'); - cy.getBySel('sort-header').eq(7).contains('Actions'); + cy.getBySel('sort-header').eq(4).contains('Dashboards added to'); + cy.getBySel('sort-header').eq(5).contains('Modified by'); + cy.getBySel('sort-header').eq(6).contains('Last modified'); + cy.getBySel('sort-header').eq(7).contains('Created by'); + cy.getBySel('sort-header').eq(8).contains('Actions'); }); it('should sort correctly in list mode', () => { diff --git a/superset-frontend/src/components/ListView/CrossLinks.test.tsx b/superset-frontend/src/components/ListView/CrossLinks.test.tsx new file mode 100644 index 0000000000000..ad7eb4e0dd281 --- /dev/null +++ b/superset-frontend/src/components/ListView/CrossLinks.test.tsx @@ -0,0 +1,97 @@ +/** + * 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 } from 'spec/helpers/testing-library'; +import CrossLinks, { CrossLinksProps } from './CrossLinks'; + +const mockedProps = { + crossLinks: [ + { + id: 1, + title: 'Test dashboard', + }, + { + id: 2, + title: 'Test dashboard 2', + }, + { + id: 3, + title: 'Test dashboard 3', + }, + { + id: 4, + title: 'Test dashboard 4', + }, + ], +}; + +function setup(overrideProps: CrossLinksProps | {} = {}) { + return render(, { + useRouter: true, + }); +} + +test('should render', () => { + const { container } = setup(); + expect(container).toBeInTheDocument(); +}); + +test('should not render links', () => { + setup({ + crossLinks: [], + }); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); +}); + +test('should render the link with just one item', () => { + setup({ + crossLinks: [ + { + id: 1, + title: 'Test dashboard', + }, + ], + }); + expect(screen.getByText('Test dashboard')).toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveAttribute( + 'href', + `/superset/dashboard/1`, + ); +}); + +test('should render a custom prefix link', () => { + setup({ + crossLinks: [ + { + id: 1, + title: 'Test dashboard', + }, + ], + linkPrefix: '/custom/dashboard/', + }); + expect(screen.getByRole('link')).toHaveAttribute( + 'href', + `/custom/dashboard/1`, + ); +}); + +test('should render multiple links', () => { + setup(); + expect(screen.getAllByRole('link')).toHaveLength(4); +}); diff --git a/superset-frontend/src/components/ListView/CrossLinks.tsx b/superset-frontend/src/components/ListView/CrossLinks.tsx new file mode 100644 index 0000000000000..3941bcf6caac1 --- /dev/null +++ b/superset-frontend/src/components/ListView/CrossLinks.tsx @@ -0,0 +1,122 @@ +/** + * 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, useRef } from 'react'; +import { styled } from '@superset-ui/core'; +import { Link } from 'react-router-dom'; +import { useTruncation } from 'src/hooks/useTruncation'; +import CrossLinksTooltip from './CrossLinksTooltip'; + +export type CrossLinkProps = { + title: string; + id: number; +}; + +export type CrossLinksProps = { + crossLinks: Array; + maxLinks?: number; + linkPrefix?: string; +}; + +const StyledCrossLinks = styled.div` + ${({ theme }) => ` + & > span { + width: 100%; + display: flex; + + .ant-tooltip-open { + display: inline; + } + + .truncated { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + width: 100%; + vertical-align: bottom; + } + + .count { + cursor: pointer; + color: ${theme.colors.grayscale.base}; + font-weight: ${theme.typography.weights.bold}; + } + } + `} +`; + +export default function CrossLinks({ + crossLinks, + maxLinks = 20, + linkPrefix = '/superset/dashboard/', +}: CrossLinksProps) { + const crossLinksRef = useRef(null); + const plusRef = useRef(null); + const [elementsTruncated, hasHiddenElements] = useTruncation( + crossLinksRef, + plusRef, + ); + const hasMoreItems = useMemo( + () => + crossLinks.length > maxLinks ? crossLinks.length - maxLinks : undefined, + [crossLinks, maxLinks], + ); + const links = useMemo( + () => ( + + {crossLinks.map((link, index) => ( + + {index === 0 ? link.title : `, ${link.title}`} + + ))} + + ), + [crossLinks], + ); + const tooltipLinks = useMemo( + () => + crossLinks.slice(0, maxLinks).map(l => ({ + title: l.title, + to: linkPrefix + l.id, + })), + [crossLinks, maxLinks], + ); + + return ( + + + {links} + {hasHiddenElements && ( + + +{elementsTruncated} + + )} + + + ); +} diff --git a/superset-frontend/src/components/ListView/CrossLinksTooltip.test.tsx b/superset-frontend/src/components/ListView/CrossLinksTooltip.test.tsx new file mode 100644 index 0000000000000..96723e7bf698d --- /dev/null +++ b/superset-frontend/src/components/ListView/CrossLinksTooltip.test.tsx @@ -0,0 +1,89 @@ +/** + * 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 userEvent from '@testing-library/user-event'; +import React from 'react'; +import { render, screen, waitFor } from 'spec/helpers/testing-library'; +import CrossLinksTooltip, { CrossLinksTooltipProps } from './CrossLinksTooltip'; + +const mockedProps = { + crossLinks: [ + { + to: 'somewhere/1', + title: 'Test dashboard', + }, + { + to: 'somewhere/2', + title: 'Test dashboard 2', + }, + { + to: 'somewhere/3', + title: 'Test dashboard 3', + }, + { + to: 'somewhere/4', + title: 'Test dashboard 4', + }, + ], + moreItems: 0, + show: true, +}; + +function setup(overrideProps: CrossLinksTooltipProps | {} = {}) { + return render( + + Hover me + , + { + useRouter: true, + }, + ); +} + +test('should render', () => { + const { container } = setup(); + expect(container).toBeInTheDocument(); +}); + +test('should render multiple links', async () => { + setup(); + userEvent.hover(screen.getByText('Hover me')); + + await waitFor(() => { + expect(screen.getByText('Test dashboard')).toBeInTheDocument(); + expect(screen.getByText('Test dashboard 2')).toBeInTheDocument(); + expect(screen.getByText('Test dashboard 3')).toBeInTheDocument(); + expect(screen.getByText('Test dashboard 4')).toBeInTheDocument(); + expect(screen.getAllByRole('link')).toHaveLength(4); + }); +}); + +test('should not render the "+ {x} more"', () => { + setup(); + userEvent.hover(screen.getByText('Hover me')); + expect(screen.queryByTestId('plus-more')).not.toBeInTheDocument(); +}); + +test('should render the "+ {x} more"', async () => { + setup({ + moreItems: 3, + }); + userEvent.hover(screen.getByText('Hover me')); + expect(await screen.findByTestId('plus-more')).toBeInTheDocument(); + expect(await screen.findByText('+ 3 more')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/ListView/CrossLinksTooltip.tsx b/superset-frontend/src/components/ListView/CrossLinksTooltip.tsx new file mode 100644 index 0000000000000..cc552cd8b4cf9 --- /dev/null +++ b/superset-frontend/src/components/ListView/CrossLinksTooltip.tsx @@ -0,0 +1,73 @@ +/** + * 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 { styled, t } from '@superset-ui/core'; +import { Tooltip } from 'src/components/Tooltip'; +import { Link } from 'react-router-dom'; + +export type CrossLinksTooltipProps = { + children: React.ReactNode; + crossLinks: { to: string; title: string }[]; + moreItems?: number; + show: boolean; +}; + +const StyledLinkedTooltip = styled.div` + .link { + color: ${({ theme }) => theme.colors.grayscale.light5}; + display: block; + text-decoration: underline; + } +`; + +export default function CrossLinksTooltip({ + children, + crossLinks = [], + moreItems = undefined, + show = false, +}: CrossLinksTooltipProps) { + return ( + + {crossLinks.map(link => ( + + {link.title} + + ))} + {moreItems && ( + {t('+ %s more', moreItems)} + )} + + ) + } + > + {children} + + ); +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx index 18a1c257b4ba3..704357c134868 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx @@ -21,6 +21,7 @@ import { useDispatch } from 'react-redux'; import { css, t, useTheme } from '@superset-ui/core'; import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState'; import Icons from 'src/components/Icons'; +import { useTruncation } from 'src/hooks/useTruncation'; import { DependencyItem, Row, @@ -30,7 +31,6 @@ import { TooltipList, } from './Styles'; import { useFilterDependencies } from './useFilterDependencies'; -import { useTruncation } from './useTruncation'; import { DependencyValueProps, FilterCardRowProps } from './types'; import { TooltipWithTruncation } from './TooltipWithTruncation'; @@ -55,7 +55,11 @@ const DependencyValue = ({ export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => { const dependencies = useFilterDependencies(filter); const dependenciesRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation(dependenciesRef); + const plusRef = useRef(null); + const [elementsTruncated, hasHiddenElements] = useTruncation( + dependenciesRef, + plusRef, + ); const theme = useTheme(); const tooltipText = useMemo( @@ -108,7 +112,9 @@ export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => { ))} {hasHiddenElements && ( - +{elementsTruncated} + + +{elementsTruncated} + )} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx index 05cb8119487cd..f6268296efdc6 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx @@ -19,9 +19,9 @@ import React, { useRef } from 'react'; import { css, SupersetTheme } from '@superset-ui/core'; import Icons from 'src/components/Icons'; +import { useTruncation } from 'src/hooks/useTruncation'; import { Row, FilterName } from './Styles'; import { FilterCardRowProps } from './types'; -import { useTruncation } from './useTruncation'; import { TooltipWithTruncation } from './TooltipWithTruncation'; export const NameRow = ({ filter }: FilterCardRowProps) => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx index 66656f0ba514d..8da224c0e706d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx @@ -18,6 +18,7 @@ */ import React, { useMemo, useRef } from 'react'; import { t } from '@superset-ui/core'; +import { useTruncation } from 'src/hooks/useTruncation'; import { useFilterScope } from './useFilterScope'; import { Row, @@ -27,7 +28,6 @@ import { TooltipList, TooltipSectionLabel, } from './Styles'; -import { useTruncation } from './useTruncation'; import { FilterCardRowProps } from './types'; import { TooltipWithTruncation } from './TooltipWithTruncation'; @@ -46,8 +46,12 @@ 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); + const [elementsTruncated, hasHiddenElements] = useTruncation( + scopeRef, + plusRef, + ); const tooltipText = useMemo(() => { if (elementsTruncated === 0 || !scope) { return null; @@ -77,7 +81,9 @@ export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => { : t('None')} {hasHiddenElements > 0 && ( - +{elementsTruncated} + + +{elementsTruncated} + )} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts b/superset-frontend/src/hooks/useTruncation/index.ts similarity index 71% rename from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts rename to superset-frontend/src/hooks/useTruncation/index.ts index a4a893463f616..7f3e1bcadecee 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/useTruncation.ts +++ b/superset-frontend/src/hooks/useTruncation/index.ts @@ -18,17 +18,23 @@ */ import { RefObject, useLayoutEffect, useState, useRef } from 'react'; -export const useTruncation = (elementRef: RefObject) => { +export const useTruncation = ( + elementRef: RefObject, + plusRef?: RefObject, +) => { const [elementsTruncated, setElementsTruncated] = useState(0); const [hasHiddenElements, setHasHiddenElements] = useState(false); const previousEffectInfoRef = useRef({ scrollWidth: 0, parentElementWidth: 0, + plusRefWidth: 0, }); useLayoutEffect(() => { const currentElement = elementRef.current; + const plusRefElement = plusRef?.current; + if (!currentElement) { return; } @@ -45,36 +51,50 @@ export const useTruncation = (elementRef: RefObject) => { // 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.scrollWidth === scrollWidth && + previousEffectInfo.plusRefWidth === plusRefWidth ) { return; } if (scrollWidth > clientWidth) { // "..." is around 6px wide - const maxWidth = clientWidth - 6; + const truncationWidth = 6; + const plusSize = plusRefElement?.offsetWidth || 0; + const maxWidth = clientWidth - truncationWidth; const elementsCount = childNodes.length; + let width = 0; - let i = 0; - while (width < maxWidth) { - width += (childNodes[i] as HTMLElement).offsetWidth; - i += 1; + let hiddenElements = 0; + for (let i = 0; i < elementsCount; i += 1) { + const itemWidth = (childNodes[i] as HTMLElement).offsetWidth; + const remainingWidth = maxWidth - truncationWidth - width - plusSize; + + // assures it shows +{number} only when the item is not visible + if (remainingWidth <= 0) { + hiddenElements += 1; + } + width += itemWidth; } - if (i === elementsCount) { - setElementsTruncated(1); - setHasHiddenElements(false); - } else { - setElementsTruncated(elementsCount - i); + + if (elementsCount > 1 && hiddenElements) { setHasHiddenElements(true); + setElementsTruncated(hiddenElements); + } else { + setHasHiddenElements(false); + setElementsTruncated(1); } } else { + setHasHiddenElements(false); setElementsTruncated(0); } }, [ diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 8fbf37392f870..7600dfbf5d6bf 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -17,7 +17,9 @@ * under the License. */ import { + ensureIsArray, getChartMetadataRegistry, + JsonResponse, styled, SupersetClient, t, @@ -49,6 +51,7 @@ import ListView, { ListViewProps, SelectOption, } from 'src/components/ListView'; +import CrossLinks from 'src/components/ListView/CrossLinks'; import Loading from 'src/components/Loading'; import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers'; import withToasts from 'src/components/MessageToasts/withToasts'; @@ -145,6 +148,11 @@ interface ChartListProps { }; } +type ChartLinkedDashboard = { + id: number; + dashboard_title: string; +}; + const Actions = styled.div` color: ${({ theme }) => theme.colors.grayscale.base}; `; @@ -217,6 +225,7 @@ function ChartList(props: ChartListProps) { const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const enableBroadUserAccess = bootstrapData?.common?.conf?.ENABLE_BROAD_ACTIVITY_ACCESS; + const crossRefEnabled = isFeatureEnabled(FeatureFlag.CROSS_REFERENCES); const handleBulkChartExport = (chartsToExport: Chart[]) => { const ids = chartsToExport.map(({ id }) => id); handleResourceExport('chart', ids, () => { @@ -246,6 +255,80 @@ function ChartList(props: ChartListProps) { ), ); } + const fetchDashboards = async ( + filterValue = '', + page: number, + pageSize: number, + ) => { + // add filters if filterValue + const filters = filterValue + ? { + filters: [ + { + col: 'dashboards', + opr: FilterOperator.relationManyMany, + value: filterValue, + }, + ], + } + : {}; + const queryParams = rison.encode({ + columns: ['dashboard_title', 'id'], + keys: ['none'], + order_column: 'dashboard_title', + order_direction: 'asc', + page, + page_size: pageSize, + ...filters, + }); + const response: void | JsonResponse = await SupersetClient.get({ + endpoint: !filterValue + ? `/api/v1/dashboard/?q=${queryParams}` + : `/api/v1/chart/?q=${queryParams}`, + }).catch(() => + addDangerToast(t('An error occurred while fetching dashboards')), + ); + const dashboards = response?.json?.result?.map( + ({ + dashboard_title: dashboardTitle, + id, + }: { + dashboard_title: string; + id: number; + }) => ({ + label: dashboardTitle, + value: id, + }), + ); + return { + data: uniqBy(dashboards, 'value'), + totalCount: response?.json?.count, + }; + }; + + const dashboardsCol = useMemo( + () => ({ + Cell: ({ + row: { + original: { dashboards }, + }, + }: any) => ( + ({ + title: d.dashboard_title, + id: d.id, + }), + )} + /> + ), + Header: t('Dashboards added to'), + accessor: 'dashboards', + disableSortBy: true, + size: 'xxl', + }), + [], + ); const columns = useMemo( () => [ @@ -324,6 +407,7 @@ function ChartList(props: ChartListProps) { disableSortBy: true, size: 'xl', }, + ...(crossRefEnabled ? [dashboardsCol] : []), { Cell: ({ row: { @@ -490,6 +574,19 @@ function ChartList(props: ChartListProps) { [], ); + const dashboardsFilter: Filter = useMemo( + () => ({ + Header: t('Dashboards'), + id: 'dashboards', + input: 'select', + operator: FilterOperator.relationManyMany, + unfilteredLabel: t('All'), + fetchSelects: fetchDashboards, + paginate: true, + }), + [], + ); + const filters: Filters = useMemo( () => [ { @@ -568,6 +665,7 @@ function ChartList(props: ChartListProps) { fetchSelects: createFetchDatasets, paginate: true, }, + ...(crossRefEnabled ? [dashboardsFilter] : []), ...(userId ? [favoritesFilter] : []), { Header: t('Certified'), @@ -682,6 +780,7 @@ function ChartList(props: ChartListProps) { }); } } + return ( <>