Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): Add edit button to dashboard native filters filter cards #22364

Merged
merged 11 commits into from
Dec 9, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { getFilterBarTestId } from '../utils';
export interface FCBProps {
createNewOnOpen?: boolean;
dashboardId?: number;
initialFilterId?: string;
onClick?: () => void;
children?: React.ReactNode;
}

Expand All @@ -37,6 +39,8 @@ const HeaderButton = styled(Button)`
export const FilterConfigurationLink: React.FC<FCBProps> = ({
createNewOnOpen,
dashboardId,
initialFilterId,
onClick,
children,
}) => {
const dispatch = useDispatch();
Expand All @@ -56,7 +60,10 @@ export const FilterConfigurationLink: React.FC<FCBProps> = ({

const handleClick = useCallback(() => {
setOpen(true);
}, [setOpen]);
if (onClick) {
onClick();
}
}, [setOpen, onClick]);

return (
<>
Expand All @@ -73,6 +80,7 @@ export const FilterConfigurationLink: React.FC<FCBProps> = ({
isOpen={isOpen}
onSave={submit}
onCancel={close}
initialFilterId={initialFilterId}
createNewOnOpen={createNewOnOpen}
key={`filters-for-${dashboardId}`}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => {
<RowValue ref={dependenciesRef}>
{dependencies.map((dependency, index) => (
<DependencyValue
key={dependency.id}
dependency={dependency}
hasSeparator={index !== 0}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { SET_FOCUSED_NATIVE_FILTER } from 'src/dashboard/actions/nativeFilters';
import { FilterCardContent } from './FilterCardContent';

const baseInitialState = {
dashboardInfo: {},
nativeFilters: {
filters: {
'NATIVE_FILTER-1': {
Expand Down Expand Up @@ -204,6 +205,13 @@ jest.mock('@superset-ui/core', () => ({
}),
}));

jest.mock(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need that? I think that testing icons didnt require hacks before

Copy link
Member Author

@codyml codyml Dec 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this is what you had to add to avoid act warnings when running tests – without it all of the tests output a bunch of act icon errors. @michael-s-molina I know you worked on this, is this still the best solution to avoiding those test warnings?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that our icons load asynchronously, you can either mock the icons as you did or wait for them to render. We generally mock them when they're not the focus of the tests.

'src/components/Icons/Icon',
() =>
({ fileName }: { fileName: string }) =>
<span role="img" aria-label={fileName.replace('_', '-')} />,
);

// extract text from embedded html tags
// source: https://polvara.me/posts/five-things-you-didnt-know-about-testing-library
const getTextInHTMLTags =
Expand All @@ -217,90 +225,115 @@ const getTextInHTMLTags =
return nodeHasText && childrenDontHaveText;
};

const hidePopover = jest.fn();

const renderContent = (filter = baseFilter, initialState = baseInitialState) =>
render(<FilterCardContent filter={filter} />, {
render(<FilterCardContent filter={filter} hidePopover={hidePopover} />, {
useRedux: true,
initialState,
});

describe('Filter Card', () => {
it('Basic', () => {
renderContent();
expect(screen.getByText('Native filter 1')).toBeVisible();
expect(screen.getByLabelText('filter-small')).toBeVisible();
test('filter card title, type, scope, dependencies', () => {
renderContent();
expect(screen.getByText('Native filter 1')).toBeVisible();
expect(screen.getByLabelText('filter-small')).toBeVisible();

expect(screen.getByText('Filter type')).toBeVisible();
expect(screen.getByText('Select filter')).toBeVisible();
expect(screen.getByText('Filter type')).toBeVisible();
expect(screen.getByText('Select filter')).toBeVisible();

expect(screen.getByText('Scope')).toBeVisible();
expect(screen.getByText('All charts')).toBeVisible();
expect(screen.getByText('Scope')).toBeVisible();
expect(screen.getByText('All charts')).toBeVisible();

expect(screen.queryByText('Dependencies')).not.toBeInTheDocument();
});
expect(screen.queryByText('Dependencies')).not.toBeInTheDocument();
});

test('filter card scope with excluded', () => {
const filter = {
...baseFilter,
scope: { rootPath: [DASHBOARD_ROOT_ID], excluded: [1, 4] },
};
renderContent(filter);
expect(screen.getByText('Scope')).toBeVisible();
expect(
screen.getByText(getTextInHTMLTags('Test chart 2, Test chart 3')),
).toBeVisible();
});

describe('Scope', () => {
it('Scope with excluded', () => {
const filter = {
...baseFilter,
scope: { rootPath: [DASHBOARD_ROOT_ID], excluded: [1, 4] },
};
renderContent(filter);
expect(screen.getByText('Scope')).toBeVisible();
expect(
screen.getByText(getTextInHTMLTags('Test chart 2, Test chart 3')),
).toBeVisible();
});
test('filter card scope with top level tab as root', () => {
const filter = {
...baseFilter,
scope: { rootPath: ['TAB-1', 'TAB-2'], excluded: [1, 2] },
};
renderContent(filter);
expect(screen.getByText('Scope')).toBeVisible();
expect(
screen.getByText(getTextInHTMLTags('Tab 2, Test chart 3')),
).toBeVisible();
});

it('Scope with top level tab as root', () => {
const filter = {
...baseFilter,
scope: { rootPath: ['TAB-1', 'TAB-2'], excluded: [1, 2] },
};
renderContent(filter);
expect(screen.getByText('Scope')).toBeVisible();
expect(
screen.getByText(getTextInHTMLTags('Tab 2, Test chart 3')),
).toBeVisible();
});
test('filter card empty scope', () => {
const filter = {
...baseFilter,
scope: { rootPath: [], excluded: [1, 2, 3, 4] },
};
renderContent(filter);
expect(screen.getByText('Scope')).toBeVisible();
expect(screen.getByText('None')).toBeVisible();
});

it('Empty scope', () => {
const filter = {
...baseFilter,
scope: { rootPath: [], excluded: [1, 2, 3, 4] },
};
renderContent(filter);
expect(screen.getByText('Scope')).toBeVisible();
expect(screen.getByText('None')).toBeVisible();
});
test('filter card with dependency', () => {
const filter = {
...baseFilter,
cascadeParentIds: ['NATIVE_FILTER-2'],
};
renderContent(filter);
expect(screen.getByText('Dependent on')).toBeVisible();
expect(screen.getByText('Native filter 2')).toBeVisible();
});

test('focus filter on filter card dependency click', () => {
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);

const filter = {
...baseFilter,
cascadeParentIds: ['NATIVE_FILTER-2'],
};
renderContent(filter);

userEvent.click(screen.getByText('Native filter 2'));
expect(dummyDispatch).toHaveBeenCalledWith({
type: SET_FOCUSED_NATIVE_FILTER,
id: 'NATIVE_FILTER-2',
});
});

describe('Dependencies', () => {
it('Has dependency', () => {
const filter = {
...baseFilter,
cascadeParentIds: ['NATIVE_FILTER-2'],
};
renderContent(filter);
expect(screen.getByText('Dependent on')).toBeVisible();
expect(screen.getByText('Native filter 2')).toBeVisible();
});
test('edit filter button for dashboard viewer', () => {
renderContent();
expect(
screen.queryByRole('button', { name: /edit/i }),
).not.toBeInTheDocument();
});

it('Focus filter on dependency click', () => {
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
const dummyDispatch = jest.fn();
useDispatchMock.mockReturnValue(dummyDispatch);
test('edit filter button for dashboard editor', () => {
renderContent(baseFilter, {
...baseInitialState,
dashboardInfo: { dash_edit_perm: true },
});

const filter = {
...baseFilter,
cascadeParentIds: ['NATIVE_FILTER-2'],
};
renderContent(filter);
expect(screen.getByRole('button', { name: /edit/i })).toBeVisible();
});

userEvent.click(screen.getByText('Native filter 2'));
expect(dummyDispatch).toHaveBeenCalledWith({
type: SET_FOCUSED_NATIVE_FILTER,
id: 'NATIVE_FILTER-2',
});
});
test('open modal on edit filter button click', async () => {
renderContent(baseFilter, {
...baseInitialState,
dashboardInfo: { dash_edit_perm: true },
});

const editButton = screen.getByRole('button', { name: /edit/i });
userEvent.click(editButton);
expect(
await screen.findByRole('dialog', { name: /add and edit filters/i }),
).toBeVisible();
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ import { DependenciesRow } from './DependenciesRow';
import { NameRow } from './NameRow';
import { TypeRow } from './TypeRow';

export const FilterCardContent = ({ filter }: { filter: Filter }) => (
interface FilterCardContentProps {
filter: Filter;
hidePopover: () => void;
}

export const FilterCardContent = ({
filter,
hidePopover,
}: FilterCardContentProps) => (
<div>
<NameRow filter={filter} />
<NameRow filter={filter} hidePopover={hidePopover} />
<TypeRow filter={filter} />
<ScopeRow filter={filter} />
<DependenciesRow filter={filter} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,61 @@
* under the License.
*/
import React, { useRef } from 'react';
import { css, SupersetTheme } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { css, SupersetTheme, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { useTruncation } from 'src/hooks/useTruncation';
import { Row, FilterName } from './Styles';
import { RootState } from 'src/dashboard/types';
import { Row, FilterName, InternalRow } from './Styles';
import { FilterCardRowProps } from './types';
import { FilterConfigurationLink } from '../FilterBar/FilterConfigurationLink';
import { TooltipWithTruncation } from './TooltipWithTruncation';

export const NameRow = ({ filter }: FilterCardRowProps) => {
export const NameRow = ({
filter,
hidePopover,
}: FilterCardRowProps & { hidePopover: () => void }) => {
const theme = useTheme();
const filterNameRef = useRef<HTMLElement>(null);
const [elementsTruncated] = useTruncation(filterNameRef);
const dashboardId = useSelector<RootState, number>(
({ dashboardInfo }) => dashboardInfo.id,
);

const canEdit = useSelector<RootState, boolean>(
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);

return (
<Row
css={(theme: SupersetTheme) =>
css`
margin-bottom: ${theme.gridUnit * 3}px;
justify-content: space-between;
`
}
>
<Icons.FilterSmall
css={(theme: SupersetTheme) =>
css`
margin-right: ${theme.gridUnit}px;
`
}
/>
<TooltipWithTruncation title={elementsTruncated ? filter.name : null}>
<FilterName ref={filterNameRef}>{filter.name}</FilterName>
</TooltipWithTruncation>
<InternalRow>
<Icons.FilterSmall
css={(theme: SupersetTheme) =>
css`
margin-right: ${theme.gridUnit}px;
`
}
/>
<TooltipWithTruncation title={elementsTruncated ? filter.name : null}>
<FilterName ref={filterNameRef}>{filter.name}</FilterName>
</TooltipWithTruncation>
</InternalRow>
{canEdit && (
<FilterConfigurationLink
dashboardId={dashboardId}
onClick={hidePopover}
initialFilterId={filter.id}
>
<Icons.Edit iconSize="l" iconColor={theme.colors.grayscale.light1} />
</FilterConfigurationLink>
)}
</Row>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@ export const TooltipTrigger = styled.div`
display: inline-flex;
white-space: nowrap;
`;

export const InternalRow = styled.div`
display: flex;
align-items: center;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const FilterCard = ({
}: FilterCardProps) => {
const [internalIsVisible, setInternalIsVisible] = useState(false);

const hidePopover = () => {
setInternalIsVisible(false);
};

useEffect(() => {
if (!externalIsVisible) {
setInternalIsVisible(false);
Expand All @@ -46,9 +50,8 @@ export const FilterCard = ({
setInternalIsVisible(externalIsVisible && visible);
}}
visible={externalIsVisible && internalIsVisible}
content={<FilterCardContent filter={filter} />}
content={<FilterCardContent filter={filter} hidePopover={hidePopover} />}
getPopupContainer={getPopupContainer ?? (() => document.body)}
destroyTooltipOnHide
>
{children}
</Popover>
Expand Down