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 (
<>