feat: Cross-referenced Dashboards in Chart list (Column + Filter) (#21760)
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
parent
d5b4bdeb72
commit
49b48eeca4
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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(<CrossLinks {...mockedProps} {...overrideProps} />, {
|
||||
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);
|
||||
});
|
||||
|
|
@ -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<CrossLinkProps>;
|
||||
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<HTMLDivElement>(null);
|
||||
const plusRef = useRef<HTMLDivElement>(null);
|
||||
const [elementsTruncated, hasHiddenElements] = useTruncation(
|
||||
crossLinksRef,
|
||||
plusRef,
|
||||
);
|
||||
const hasMoreItems = useMemo(
|
||||
() =>
|
||||
crossLinks.length > maxLinks ? crossLinks.length - maxLinks : undefined,
|
||||
[crossLinks, maxLinks],
|
||||
);
|
||||
const links = useMemo(
|
||||
() => (
|
||||
<span className="truncated" ref={crossLinksRef} data-test="crosslinks">
|
||||
{crossLinks.map((link, index) => (
|
||||
<Link
|
||||
key={link.id}
|
||||
to={linkPrefix + link.id}
|
||||
target="_blank"
|
||||
rel="noreferer noopener"
|
||||
>
|
||||
{index === 0 ? link.title : `, ${link.title}`}
|
||||
</Link>
|
||||
))}
|
||||
</span>
|
||||
),
|
||||
[crossLinks],
|
||||
);
|
||||
const tooltipLinks = useMemo(
|
||||
() =>
|
||||
crossLinks.slice(0, maxLinks).map(l => ({
|
||||
title: l.title,
|
||||
to: linkPrefix + l.id,
|
||||
})),
|
||||
[crossLinks, maxLinks],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledCrossLinks>
|
||||
<CrossLinksTooltip
|
||||
moreItems={hasMoreItems}
|
||||
crossLinks={tooltipLinks}
|
||||
show={!!elementsTruncated}
|
||||
>
|
||||
{links}
|
||||
{hasHiddenElements && (
|
||||
<span ref={plusRef} className="count">
|
||||
+{elementsTruncated}
|
||||
</span>
|
||||
)}
|
||||
</CrossLinksTooltip>
|
||||
</StyledCrossLinks>
|
||||
);
|
||||
}
|
||||
|
|
@ -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(
|
||||
<CrossLinksTooltip {...mockedProps} {...overrideProps}>
|
||||
Hover me
|
||||
</CrossLinksTooltip>,
|
||||
{
|
||||
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();
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
data-test="crosslinks-tooltip"
|
||||
title={
|
||||
show && (
|
||||
<StyledLinkedTooltip>
|
||||
{crossLinks.map(link => (
|
||||
<Link
|
||||
className="link"
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
target="_blank"
|
||||
rel="noreferer noopener"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
{moreItems && (
|
||||
<span data-test="plus-more">{t('+ %s more', moreItems)}</span>
|
||||
)}
|
||||
</StyledLinkedTooltip>
|
||||
)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const [elementsTruncated, hasHiddenElements] = useTruncation(dependenciesRef);
|
||||
const plusRef = useRef<HTMLDivElement>(null);
|
||||
const [elementsTruncated, hasHiddenElements] = useTruncation(
|
||||
dependenciesRef,
|
||||
plusRef,
|
||||
);
|
||||
const theme = useTheme();
|
||||
|
||||
const tooltipText = useMemo(
|
||||
|
|
@ -108,7 +112,9 @@ export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => {
|
|||
))}
|
||||
</RowValue>
|
||||
{hasHiddenElements && (
|
||||
<RowTruncationCount>+{elementsTruncated}</RowTruncationCount>
|
||||
<RowTruncationCount ref={plusRef}>
|
||||
+{elementsTruncated}
|
||||
</RowTruncationCount>
|
||||
)}
|
||||
</TooltipWithTruncation>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const plusRef = useRef<HTMLDivElement>(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')}
|
||||
</RowValue>
|
||||
{hasHiddenElements > 0 && (
|
||||
<RowTruncationCount>+{elementsTruncated}</RowTruncationCount>
|
||||
<RowTruncationCount ref={plusRef}>
|
||||
+{elementsTruncated}
|
||||
</RowTruncationCount>
|
||||
)}
|
||||
</TooltipWithTruncation>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -18,17 +18,23 @@
|
|||
*/
|
||||
import { RefObject, useLayoutEffect, useState, useRef } from 'react';
|
||||
|
||||
export const useTruncation = (elementRef: RefObject<HTMLElement>) => {
|
||||
export const useTruncation = (
|
||||
elementRef: RefObject<HTMLElement>,
|
||||
plusRef?: RefObject<HTMLElement>,
|
||||
) => {
|
||||
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<HTMLElement>) => {
|
|||
// 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);
|
||||
}
|
||||
}, [
|
||||
|
|
@ -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<SelectOption>(dashboards, 'value'),
|
||||
totalCount: response?.json?.count,
|
||||
};
|
||||
};
|
||||
|
||||
const dashboardsCol = useMemo(
|
||||
() => ({
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { dashboards },
|
||||
},
|
||||
}: any) => (
|
||||
<CrossLinks
|
||||
crossLinks={ensureIsArray(dashboards).map(
|
||||
(d: ChartLinkedDashboard) => ({
|
||||
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 (
|
||||
<>
|
||||
<SubMenu name={t('Charts')} buttons={subMenuButtons} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue