diff --git a/frontend/src/metabase/components/ViewFooterButton/ViewFooterButton.tsx b/frontend/src/metabase/components/ViewFooterButton/ViewFooterButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8cf3d1ec09fb29f761988ff63c29c8d056819c8f --- /dev/null +++ b/frontend/src/metabase/components/ViewFooterButton/ViewFooterButton.tsx @@ -0,0 +1,31 @@ +import { type Ref, forwardRef, type HTMLAttributes } from "react"; + +import { + ActionIcon, + Center, + Icon, + Tooltip, + type ActionIconProps, + type IconName, +} from "metabase/ui"; + +export type ViewFooterButtonProps = { + icon: IconName; + tooltipLabel?: string | null; +} & ActionIconProps & + HTMLAttributes<HTMLButtonElement>; + +export const ViewFooterButton = forwardRef(function _ViewFooterButton( + { icon, tooltipLabel, ...actionIconProps }: ViewFooterButtonProps, + ref: Ref<HTMLButtonElement>, +) { + return ( + <Tooltip label={tooltipLabel}> + <Center> + <ActionIcon ref={ref} variant="viewFooter" {...actionIconProps}> + <Icon size={18} name={icon} /> + </ActionIcon> + </Center> + </Tooltip> + ); +}); diff --git a/frontend/src/metabase/components/ViewFooterButton/index.ts b/frontend/src/metabase/components/ViewFooterButton/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f4cd5838b00d06c6e842ed2b2984436c334f5fb --- /dev/null +++ b/frontend/src/metabase/components/ViewFooterButton/index.ts @@ -0,0 +1 @@ +export * from "./ViewFooterButton"; diff --git a/frontend/src/metabase/dashboard/components/EmbedMenu/AdminEmbedMenu.tsx b/frontend/src/metabase/dashboard/components/EmbedMenu/AdminEmbedMenu.tsx index 56279baa0c171aabf29ac8ba275b2e3a0aa6cf68..a7f13c49175339786fd074b8d28ad191862cd9ef 100644 --- a/frontend/src/metabase/dashboard/components/EmbedMenu/AdminEmbedMenu.tsx +++ b/frontend/src/metabase/dashboard/components/EmbedMenu/AdminEmbedMenu.tsx @@ -11,6 +11,7 @@ import { } from "metabase/dashboard/components/PublicLinkPopover"; import { useSelector } from "metabase/lib/redux"; import { ResourceEmbedButton } from "metabase/public/components/ResourceEmbedButton"; +import { ViewFooterSharingButton } from "metabase/query_builder/components/view/ViewFooterSharingButton"; import { getSetting } from "metabase/selectors/settings"; import { Menu, Title, Text, Stack, Center, Icon } from "metabase/ui"; @@ -31,9 +32,12 @@ export const AdminEmbedMenu = ({ getSetting(state, "enable-embedding"), ); - const target = ( - <ResourceEmbedButton hasBackground={resourceType === "dashboard"} /> - ); + const target = + resourceType === "dashboard" ? ( + <ResourceEmbedButton hasBackground={true} /> + ) : ( + <ViewFooterSharingButton /> + ); if (menuMode === "public-link-popover") { return resourceType === "dashboard" ? ( diff --git a/frontend/src/metabase/dashboard/components/EmbedMenu/NonAdminEmbedMenu.tsx b/frontend/src/metabase/dashboard/components/EmbedMenu/NonAdminEmbedMenu.tsx index 096fd2e2d7403415756eb52e5d28ed09ee6c74d0..05591ce7a7942d5a9585bd29fecab0aed34d0b0d 100644 --- a/frontend/src/metabase/dashboard/components/EmbedMenu/NonAdminEmbedMenu.tsx +++ b/frontend/src/metabase/dashboard/components/EmbedMenu/NonAdminEmbedMenu.tsx @@ -8,6 +8,7 @@ import { } from "metabase/dashboard/components/PublicLinkPopover"; import { useSelector } from "metabase/lib/redux"; import { ResourceEmbedButton } from "metabase/public/components/ResourceEmbedButton"; +import { ViewFooterSharingButton } from "metabase/query_builder/components/view/ViewFooterSharingButton"; import { getSetting } from "metabase/selectors/settings"; export const NonAdminEmbedMenu = ({ @@ -27,14 +28,21 @@ export const NonAdminEmbedMenu = ({ const isDisabled = !isPublicSharingEnabled || !hasPublicLink; - const target = ( - <ResourceEmbedButton - hasBackground={resourceType === "dashboard"} - onClick={() => setIsOpen(!isOpen)} - disabled={isDisabled} - tooltip={isDisabled ? tooltipLabel : null} - /> - ); + const target = + resourceType === "dashboard" ? ( + <ResourceEmbedButton + hasBackground={true} + onClick={() => setIsOpen(!isOpen)} + disabled={isDisabled} + tooltip={isDisabled ? tooltipLabel : null} + /> + ) : ( + <ViewFooterSharingButton + onClick={() => setIsOpen(!isOpen)} + disabled={isDisabled} + tooltipLabel={isDisabled ? tooltipLabel : null} + /> + ); return resourceType === "dashboard" ? ( <DashboardPublicLinkPopover diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget/QueryDownloadWidget.tsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget/QueryDownloadWidget.tsx index 160af54b02454df56b0bcdcca90453791ba81e12..cf435eedae44e02c58871ea47caf5dfc4739c354 100644 --- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget/QueryDownloadWidget.tsx +++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget/QueryDownloadWidget.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; import { t } from "ttag"; +import { ViewFooterButton } from "metabase/components/ViewFooterButton"; import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins"; -import { Flex, Popover, Tooltip } from "metabase/ui"; +import { Flex, Popover } from "metabase/ui"; import type Question from "metabase-lib/v1/Question"; import type { DashboardId, @@ -14,8 +15,6 @@ import type { import { QueryDownloadPopover } from "../QueryDownloadPopover"; import { useDownloadData } from "../QueryDownloadPopover/use-download-data"; -import { DownloadIcon } from "./QueryDownloadWidget.styled"; - interface QueryDownloadWidgetProps { className?: string; question: Question; @@ -53,14 +52,12 @@ const QueryDownloadWidget = ({ <Popover opened={isPopoverOpen} onClose={() => setIsPopoverOpen(false)}> <Popover.Target> <Flex className={className}> - <Tooltip label={t`Download full results`}> - <DownloadIcon - onClick={() => setIsPopoverOpen(!isPopoverOpen)} - name="download" - size={20} - data-testid="download-button" - /> - </Tooltip> + <ViewFooterButton + icon="download" + data-testid="download-button" + tooltipLabel={t`Download full results`} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + /> </Flex> </Popover.Target> <Popover.Dropdown p="0.75rem"> diff --git a/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx b/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx index 359cebf9d4508226b4e05413d999a748ea6c28c0..8711683f89f678d60e505bd13f05712ed6d3f8c7 100644 --- a/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx +++ b/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx @@ -4,13 +4,12 @@ import { createRef, Component } from "react"; import { t } from "ttag"; import Popover from "metabase/components/Popover"; +import { ViewFooterButton } from "metabase/components/ViewFooterButton"; import CS from "metabase/css/core/index.css"; import { Icon } from "metabase/ui"; import { AlertListPopoverContent } from "../AlertListPopoverContent"; -import { AlertIcon } from "./QuestionAlertWidget.styled"; - export default class QuestionAlertWidget extends Component { state = { isOpen: false, @@ -69,12 +68,11 @@ export default class QuestionAlertWidget extends Component { ); } else { return ( - <AlertIcon - name="bell" - tooltip={t`Get alerts`} - size={20} - className={className} + <ViewFooterButton + icon="bell" + tooltipLabel={t`Get alerts`} onClick={onCreateAlert} + className={className} /> ); } diff --git a/frontend/src/metabase/query_builder/components/view/QuestionTimelineWidget/QuestionTimelineWidget.tsx b/frontend/src/metabase/query_builder/components/view/QuestionTimelineWidget/QuestionTimelineWidget.tsx index 52c2c0357e36824dffa7ddb1f058d87fafd9afbf..3327979abccf24b8724149ef3c3344ae4ea2dbf5 100644 --- a/frontend/src/metabase/query_builder/components/view/QuestionTimelineWidget/QuestionTimelineWidget.tsx +++ b/frontend/src/metabase/query_builder/components/view/QuestionTimelineWidget/QuestionTimelineWidget.tsx @@ -1,6 +1,6 @@ import { t } from "ttag"; -import { TimelineIcon } from "./QuestionTimelineWidget.styled"; +import { ViewFooterButton } from "metabase/components/ViewFooterButton"; export interface QuestionTimelineWidgetProps { className?: string; @@ -16,11 +16,11 @@ const QuestionTimelineWidget = ({ onCloseTimelines, }: QuestionTimelineWidgetProps): JSX.Element => { return ( - <TimelineIcon - className={className} - name="calendar" - tooltip={t`Events`} + <ViewFooterButton + icon="calendar" + tooltipLabel={t`Events`} onClick={isShowingTimelineSidebar ? onCloseTimelines : onOpenTimelines} + className={className} /> ); }; diff --git a/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx b/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx index c6e9e073003f0c97e9cd655e809994ce4b17a2d6..c9c428b328e6f0e6f3d94db01e13112a0a9b484a 100644 --- a/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx +++ b/frontend/src/metabase/query_builder/components/view/ViewFooter.jsx @@ -5,9 +5,10 @@ import { t } from "ttag"; import ButtonBar from "metabase/components/ButtonBar"; import CS from "metabase/css/core/index.css"; import { EmbedMenu } from "metabase/dashboard/components/EmbedMenu"; -import { ResourceEmbedButton } from "metabase/public/components/ResourceEmbedButton"; import QueryDownloadWidget from "metabase/query_builder/components/QueryDownloadWidget"; +import { ViewFooterSharingButton } from "metabase/query_builder/components/view/ViewFooterSharingButton"; import { MODAL_TYPES } from "metabase/query_builder/constants"; +import { Group } from "metabase/ui"; import * as Lib from "metabase-lib"; import { ExecutionTime } from "./ExecutionTime"; @@ -112,68 +113,64 @@ const ViewFooter = ({ ExecutionTime.shouldRender({ result }) && ( <ExecutionTime key="execution_time" time={result.running_time} /> ), - QuestionLastUpdated.shouldRender({ result }) && ( - <QuestionLastUpdated - key="last-updated" - className={cx(CS.hide, CS.smShow)} - result={result} - /> - ), - QueryDownloadWidget.shouldRender({ result }) && ( - <QueryDownloadWidget - key="download" - className={cx(CS.hide, CS.smShow)} - question={question} - result={result} - visualizationSettings={visualizationSettings} - dashcardId={question.card().dashcardId} - dashboardId={question.card().dashboardId} - /> - ), - QuestionAlertWidget.shouldRender({ - question, - visualizationSettings, - }) && ( - <QuestionAlertWidget - key="alerts" - className={cx(CS.hide, CS.smShow)} - canManageSubscriptions={canManageSubscriptions} - question={question} - questionAlerts={questionAlerts} - onCreateAlert={() => - question.isSaved() - ? onOpenModal("create-alert") - : onOpenModal("save-question-before-alert") - } - /> - ), - type === "question" && - !question.isArchived() && - (question.isSaved() ? ( - <EmbedMenu - key="embed" - resource={question} - resourceType="question" - hasPublicLink={!!question.publicUUID()} - onModalOpen={() => onOpenModal(MODAL_TYPES.EMBED)} + <Group key="button-group" spacing="sm" noWrap> + {QuestionLastUpdated.shouldRender({ result }) && ( + <QuestionLastUpdated + className={cx(CS.hide, CS.smShow)} + result={result} + /> + )} + {QueryDownloadWidget.shouldRender({ result }) && ( + <QueryDownloadWidget + className={cx(CS.hide, CS.smShow)} + question={question} + result={result} + visualizationSettings={visualizationSettings} + dashcardId={question.card().dashcardId} + dashboardId={question.card().dashboardId} /> - ) : ( - <ResourceEmbedButton - hasBackground={false} - onClick={() => - onOpenModal(MODAL_TYPES.SAVE_QUESTION_BEFORE_EMBED) + )} + {QuestionAlertWidget.shouldRender({ + question, + visualizationSettings, + }) && ( + <QuestionAlertWidget + className={cx(CS.hide, CS.smShow)} + canManageSubscriptions={canManageSubscriptions} + question={question} + questionAlerts={questionAlerts} + onCreateAlert={() => + question.isSaved() + ? onOpenModal(MODAL_TYPES.CREATE_ALERT) + : onOpenModal(MODAL_TYPES.SAVE_QUESTION_BEFORE_ALERT) } /> - )), - QuestionTimelineWidget.shouldRender({ isTimeseries }) && ( - <QuestionTimelineWidget - key="timelines" - className={cx(CS.hide, CS.smShow)} - isShowingTimelineSidebar={isShowingTimelineSidebar} - onOpenTimelines={onOpenTimelines} - onCloseTimelines={onCloseTimelines} - /> - ), + )} + {type === "question" && + !question.isArchived() && + (question.isSaved() ? ( + <EmbedMenu + resource={question} + resourceType="question" + hasPublicLink={!!question.publicUUID()} + onModalOpen={() => onOpenModal(MODAL_TYPES.EMBED)} + /> + ) : ( + <ViewFooterSharingButton + onClick={() => + onOpenModal(MODAL_TYPES.SAVE_QUESTION_BEFORE_EMBED) + } + /> + ))} + {QuestionTimelineWidget.shouldRender({ isTimeseries }) && ( + <QuestionTimelineWidget + className={cx(CS.hide, CS.smShow)} + isShowingTimelineSidebar={isShowingTimelineSidebar} + onOpenTimelines={onOpenTimelines} + onCloseTimelines={onCloseTimelines} + /> + )} + </Group>, ]} /> </ViewFooterRoot> diff --git a/frontend/src/metabase/query_builder/components/view/ViewFooterSharingButton.tsx b/frontend/src/metabase/query_builder/components/view/ViewFooterSharingButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2ae429de236b1e67934302805bb24700ddc7c627 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/view/ViewFooterSharingButton.tsx @@ -0,0 +1,29 @@ +import { t } from "ttag"; + +import { + ViewFooterButton, + type ViewFooterButtonProps, +} from "metabase/components/ViewFooterButton"; +import { useSelector } from "metabase/lib/redux"; +import { getSetting } from "metabase/selectors/settings"; + +export const ViewFooterSharingButton = ( + viewFooterButtonProps: Omit<ViewFooterButtonProps, "icon" | "data-testid">, +) => { + const isPublicSharingEnabled = useSelector(state => + getSetting(state, "enable-public-sharing"), + ); + + const tooltipLabel = + viewFooterButtonProps.tooltipLabel ?? + (isPublicSharingEnabled ? t`Sharing` : t`Embedding`); + + return ( + <ViewFooterButton + icon="share" + data-testid="resource-embed-button" + tooltipLabel={tooltipLabel} + {...viewFooterButtonProps} + /> + ); +}; diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/ViewTitleHeaderRightSide/ViewTitleHeaderRightSide.tsx b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/ViewTitleHeaderRightSide/ViewTitleHeaderRightSide.tsx index bc4ec8a0524715f6f13dc1137a352144572f9f0d..49cf45c5da510bcf1e1e0c37d7f13b96b56748fc 100644 --- a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/ViewTitleHeaderRightSide/ViewTitleHeaderRightSide.tsx +++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/ViewTitleHeaderRightSide/ViewTitleHeaderRightSide.tsx @@ -23,6 +23,7 @@ import { } from "metabase/query_builder/components/view/ViewHeader/components"; import { canExploreResults } from "metabase/query_builder/components/view/ViewHeader/utils"; import type { QueryModalType } from "metabase/query_builder/constants"; +import { MODAL_TYPES } from "metabase/query_builder/constants"; import { Tooltip } from "metabase/ui"; import * as Lib from "metabase-lib"; import type Question from "metabase-lib/v1/Question"; @@ -235,7 +236,7 @@ export function ViewTitleHeaderRightSide({ onClick={event => { event.preventDefault(); if (!isSaveDisabled) { - onOpenModal("save"); + onOpenModal(MODAL_TYPES.SAVE); } }} > diff --git a/frontend/src/metabase/ui/components/buttons/ActionIcon/ActionIcon.styled.tsx b/frontend/src/metabase/ui/components/buttons/ActionIcon/ActionIcon.styled.tsx index 8e829dee5d1c46aadfc0c209a678a6357cc35a8c..94c68e84ea64f77632cbf55f449ff86ab6d7d2ca 100644 --- a/frontend/src/metabase/ui/components/buttons/ActionIcon/ActionIcon.styled.tsx +++ b/frontend/src/metabase/ui/components/buttons/ActionIcon/ActionIcon.styled.tsx @@ -44,6 +44,18 @@ export const getActionIconOverrides = }, }, }), + viewFooter: theme => ({ + root: { + color: theme.fn.themeColor("text-medium"), + "&:hover": { + color: theme.fn.themeColor("brand"), + }, + "&:disabled, &[data-disabled]": { + color: theme.fn.themeColor("text-light"), + backgroundColor: "transparent", + }, + }, + }), }, }, });