Skip to content

Commit

Permalink
feat(dashboards): Allow disabling WidgetFrame actions (#79592)
Browse files Browse the repository at this point in the history
Used in a few places in the UI, like in Dashboard Preview mode.
  • Loading branch information
gggritso authored Oct 23, 2024
1 parent 20de747 commit 6063faa
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 30 deletions.
60 changes: 60 additions & 0 deletions static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,36 @@ describe('WidgetFrame', () => {
expect(onAction).toHaveBeenCalledTimes(1);
});

it('Allows disabling a single action', async () => {
const onAction = jest.fn();

render(
<WidgetFrame
title="EPS"
description="Number of events per second"
actionsDisabled
actionsMessage="Actions are not supported"
actions={[
{
key: 'hello',
label: 'Make Go',
onAction,
},
]}
/>
);

const $button = screen.getByRole('button', {name: 'Make Go'});
expect($button).toBeInTheDocument();
expect($button).toBeDisabled();

await userEvent.click($button);
expect(onAction).not.toHaveBeenCalled();

await userEvent.hover($button);
expect(await screen.findByText('Actions are not supported')).toBeInTheDocument();
});

it('Renders multiple actions in a dropdown menu', async () => {
const onAction1 = jest.fn();
const onAction2 = jest.fn();
Expand Down Expand Up @@ -101,6 +131,36 @@ describe('WidgetFrame', () => {
await userEvent.click(screen.getByRole('menuitemradio', {name: 'Two'}));
expect(onAction2).toHaveBeenCalledTimes(1);
});

it('Allows disabling multiple actions', async () => {
render(
<WidgetFrame
title="EPS"
description="Number of events per second"
actionsDisabled
actionsMessage="Actions are not supported"
actions={[
{
key: 'one',
label: 'One',
},
{
key: 'two',
label: 'Two',
},
]}
/>
);

const $trigger = screen.getByRole('button', {name: 'Actions'});
await userEvent.click($trigger);

expect(screen.queryByRole('menuitemradio', {name: 'One'})).not.toBeInTheDocument();
expect(screen.queryByRole('menuitemradio', {name: 'Two'})).not.toBeInTheDocument();

await userEvent.hover($trigger);
expect(await screen.findByText('Actions are not supported')).toBeInTheDocument();
});
});

describe('Full Screen View Button', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export default storyBook(WidgetFrame, story => {
<JSXNode name="WidgetFrame" /> supports an action menu. If only one action is
passed, the single action is rendered as a small button. If multiple actions are
passed, they are grouped into a dropdown menu. Menu actions appear on hover or
keyboard focus.
keyboard focus. They can be disabled with the <code>actionsDisabled</code> prop,
and supplemented with an optional <code>actionsMessage</code> prop that adds a
tooltip.
</p>

<SideBySide>
Expand All @@ -127,6 +129,25 @@ export default storyBook(WidgetFrame, story => {
/>
</NormalWidget>

<NormalWidget>
<WidgetFrame
title="Count"
actionsDisabled
actionsMessage="Not possible here"
description="This counts up the amount of something that happens."
actions={[
{
key: 'see-more',
label: t('See More'),
onAction: () => {
// eslint-disable-next-line no-console
console.log('See more!');
},
},
]}
/>
</NormalWidget>

<NormalWidget>
<WidgetFrame
title="Count"
Expand All @@ -151,6 +172,32 @@ export default storyBook(WidgetFrame, story => {
]}
/>
</NormalWidget>

<NormalWidget>
<WidgetFrame
title="Count"
actionsDisabled
actionsMessage="Not available in this context"
actions={[
{
key: 'see-more',
label: t('See More'),
onAction: () => {
// eslint-disable-next-line no-console
console.log('See more!');
},
},
{
key: 'see-less',
label: t('See Less'),
onAction: () => {
// eslint-disable-next-line no-console
console.log('See less!');
},
},
]}
/>
</NormalWidget>
</SideBySide>
</Fragment>
);
Expand Down
93 changes: 64 additions & 29 deletions static/app/views/dashboards/widgets/common/widgetFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {WarningsList} from './warningsList';

export interface WidgetFrameProps extends StateProps {
actions?: MenuItemProps[];
actionsDisabled?: boolean;
actionsMessage?: string;
badgeProps?: BadgeProps;
children?: React.ReactNode;
description?: string;
Expand Down Expand Up @@ -63,36 +65,51 @@ export function WidgetFrame(props: WidgetFrameProps) {
{(props.description ||
props.onFullScreenViewClick ||
(actions && actions.length > 0)) && (
<TitleActions>
<TitleHoverItems>
{props.description && (
<QuestionTooltip title={props.description} size="sm" icon="info" />
)}

{actions.length === 1 ? (
actions[0].to ? (
<LinkButton size="xs" onClick={actions[0].onAction} to={actions[0].to}>
{actions[0].label}
</LinkButton>
) : (
<Button size="xs" onClick={actions[0].onAction}>
{actions[0].label}
</Button>
)
) : null}

{actions.length > 1 ? (
<DropdownMenu
items={actions}
triggerProps={{
'aria-label': t('Actions'),
size: 'xs',
borderless: true,
showChevron: false,
icon: <IconEllipsis direction="down" size="sm" />,
}}
position="bottom-end"
/>
) : null}
<TitleActionsWrapper
disabled={Boolean(props.actionsDisabled)}
disabledMessage={props.actionsMessage ?? ''}
>
{actions.length === 1 ? (
actions[0].to ? (
<LinkButton
size="xs"
disabled={props.actionsDisabled}
onClick={actions[0].onAction}
to={actions[0].to}
>
{actions[0].label}
</LinkButton>
) : (
<Button
size="xs"
disabled={props.actionsDisabled}
onClick={actions[0].onAction}
>
{actions[0].label}
</Button>
)
) : null}

{actions.length > 1 ? (
<DropdownMenu
items={actions}
isDisabled={props.actionsDisabled}
triggerProps={{
'aria-label': t('Actions'),
size: 'xs',
borderless: true,
showChevron: false,
icon: <IconEllipsis direction="down" size="sm" />,
}}
position="bottom-end"
/>
) : null}
</TitleActionsWrapper>

{props.onFullScreenViewClick && (
<Button
Expand All @@ -105,7 +122,7 @@ export function WidgetFrame(props: WidgetFrameProps) {
}}
/>
)}
</TitleActions>
</TitleHoverItems>
)}
</Header>

Expand All @@ -116,7 +133,7 @@ export function WidgetFrame(props: WidgetFrameProps) {
);
}

const TitleActions = styled('div')`
const TitleHoverItems = styled('div')`
display: flex;
align-items: center;
gap: ${space(0.5)};
Expand All @@ -126,6 +143,24 @@ const TitleActions = styled('div')`
transition: opacity 0.1s;
`;

interface TitleActionsProps {
children: React.ReactNode;
disabled: boolean;
disabledMessage: string;
}

function TitleActionsWrapper({disabled, disabledMessage, children}: TitleActionsProps) {
if (!disabled || !disabledMessage) {
return children;
}

return (
<Tooltip title={disabledMessage} isHoverable>
{children}
</Tooltip>
);
}

const Frame = styled('div')`
position: relative;
display: flex;
Expand Down Expand Up @@ -153,7 +188,7 @@ const Frame = styled('div')`
}
&:not(:hover):not(:focus-within) {
${TitleActions} {
${TitleHoverItems} {
opacity: 0;
${p => p.theme.visuallyHidden}
}
Expand Down

0 comments on commit 6063faa

Please sign in to comment.