From 6063faa0b0fcc7067215f42cd8ae5c60cd0bd235 Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Wed, 23 Oct 2024 11:24:53 -0400
Subject: [PATCH] feat(dashboards): Allow disabling `WidgetFrame` actions
(#79592)
Used in a few places in the UI, like in Dashboard Preview mode.
---
.../widgets/common/widgetFrame.spec.tsx | 60 ++++++++++++
.../widgets/common/widgetFrame.stories.tsx | 49 +++++++++-
.../dashboards/widgets/common/widgetFrame.tsx | 93 +++++++++++++------
3 files changed, 172 insertions(+), 30 deletions(-)
diff --git a/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx b/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx
index d3255405af00ca..077609547462ca 100644
--- a/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx
+++ b/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx
@@ -70,6 +70,36 @@ describe('WidgetFrame', () => {
expect(onAction).toHaveBeenCalledTimes(1);
});
+ it('Allows disabling a single action', async () => {
+ const onAction = jest.fn();
+
+ render(
+
+ );
+
+ 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();
@@ -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(
+
+ );
+
+ 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', () => {
diff --git a/static/app/views/dashboards/widgets/common/widgetFrame.stories.tsx b/static/app/views/dashboards/widgets/common/widgetFrame.stories.tsx
index 6cd5f3ecb63b97..fa054d1671eef7 100644
--- a/static/app/views/dashboards/widgets/common/widgetFrame.stories.tsx
+++ b/static/app/views/dashboards/widgets/common/widgetFrame.stories.tsx
@@ -106,7 +106,9 @@ export default storyBook(WidgetFrame, story => {
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 actionsDisabled
prop,
+ and supplemented with an optional actionsMessage
prop that adds a
+ tooltip.
@@ -127,6 +129,25 @@ export default storyBook(WidgetFrame, story => {
/>
+
+ {
+ // eslint-disable-next-line no-console
+ console.log('See more!');
+ },
+ },
+ ]}
+ />
+
+
{
]}
/>
+
+
+ {
+ // 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!');
+ },
+ },
+ ]}
+ />
+
);
diff --git a/static/app/views/dashboards/widgets/common/widgetFrame.tsx b/static/app/views/dashboards/widgets/common/widgetFrame.tsx
index 9dd7a318d2c4d7..0385532811c581 100644
--- a/static/app/views/dashboards/widgets/common/widgetFrame.tsx
+++ b/static/app/views/dashboards/widgets/common/widgetFrame.tsx
@@ -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;
@@ -63,36 +65,51 @@ export function WidgetFrame(props: WidgetFrameProps) {
{(props.description ||
props.onFullScreenViewClick ||
(actions && actions.length > 0)) && (
-
+
{props.description && (
)}
- {actions.length === 1 ? (
- actions[0].to ? (
-
- {actions[0].label}
-
- ) : (
-
- )
- ) : null}
-
- {actions.length > 1 ? (
- ,
- }}
- position="bottom-end"
- />
- ) : null}
+
+ {actions.length === 1 ? (
+ actions[0].to ? (
+
+ {actions[0].label}
+
+ ) : (
+
+ )
+ ) : null}
+
+ {actions.length > 1 ? (
+ ,
+ }}
+ position="bottom-end"
+ />
+ ) : null}
+
{props.onFullScreenViewClick && (
)}
-
+
)}
@@ -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)};
@@ -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 (
+
+ {children}
+
+ );
+}
+
const Frame = styled('div')`
position: relative;
display: flex;
@@ -153,7 +188,7 @@ const Frame = styled('div')`
}
&:not(:hover):not(:focus-within) {
- ${TitleActions} {
+ ${TitleHoverItems} {
opacity: 0;
${p => p.theme.visuallyHidden}
}