From 06338407778005ff0a35a54bc011f22a57c2c90c Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Wed, 20 Jul 2022 06:33:46 -0300 Subject: [PATCH] GrafanaUI: Add success state to ClipboardButton (#52069) * User Experience: apply the same pattern feedback for all copy to clipboard buttons * add copy icon to all ClipboardButton use cases * Change primary color for copy to clipboard in create token * Add success button variant * Remove copy confirmation from TableCellInspectModal because it's in the base component now * Design tweaks to copy confirmation - Only change the icon to tick to avoid the button changing size - Change button to success green - Only show copy confirmation state for 2 seconds * revert TabelCellInspectModal text button back * revert accidental change to ShareLink Co-authored-by: joshhunt --- .../src/components/Button/Button.tsx | 5 ++- .../ClipboardButton/ClipboardButton.tsx | 40 +++++++++++++++++-- .../Table/TableCellInspectModal.tsx | 31 ++------------ .../app/features/alerting/TestRuleResult.tsx | 8 +--- .../rules/RuleDetailsActionButtons.tsx | 4 +- .../features/api-keys/ApiKeysAddedModal.tsx | 14 ++----- .../forms/SaveProvisionedDashboardForm.tsx | 8 +--- .../components/ShareModal/ShareEmbed.tsx | 9 +---- .../components/ShareModal/ShareLink.tsx | 11 ++--- .../ShareModal/SharePublicDashboard.tsx | 7 ---- .../components/ShareModal/ShareSnapshot.tsx | 32 +++++++-------- .../components/ShareModal/ViewJsonModal.tsx | 9 +---- .../app/features/inspector/QueryInspector.tsx | 8 +--- public/app/features/invites/InviteeRow.tsx | 2 +- public/app/features/playlist/ShareModal.tsx | 13 ++---- .../components/CreateTokenModal.tsx | 2 +- 16 files changed, 78 insertions(+), 125 deletions(-) diff --git a/packages/grafana-ui/src/components/Button/Button.tsx b/packages/grafana-ui/src/components/Button/Button.tsx index bce87b5a739..4813543f9c6 100644 --- a/packages/grafana-ui/src/components/Button/Button.tsx +++ b/packages/grafana-ui/src/components/Button/Button.tsx @@ -11,7 +11,7 @@ import { getPropertiesForButtonSize } from '../Forms/commonStyles'; import { Icon } from '../Icon/Icon'; import { PopoverContent, Tooltip, TooltipPlacement } from '../Tooltip'; -export type ButtonVariant = 'primary' | 'secondary' | 'destructive'; +export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'success'; export const allButtonVariants: ButtonVariant[] = ['primary', 'secondary', 'destructive']; export type ButtonFill = 'solid' | 'outline' | 'text'; export const allButtonFills: ButtonFill[] = ['solid', 'outline', 'text']; @@ -294,6 +294,9 @@ export function getPropertiesForVariant(theme: GrafanaTheme2, variant: ButtonVar case 'destructive': return getButtonVariantStyles(theme, theme.colors.error, fill); + case 'success': + return getButtonVariantStyles(theme, theme.colors.success, fill); + case 'primary': default: return getButtonVariantStyles(theme, theme.colors.primary, fill); diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx index 67007913992..b5eabd32d70 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useRef, useState, useEffect } from 'react'; import { Button, ButtonProps } from '../Button'; @@ -11,13 +11,40 @@ export interface Props extends ButtonProps { onClipboardError?(copiedText: string, error: unknown): void; } -export function ClipboardButton({ onClipboardCopy, onClipboardError, children, getText, ...buttonProps }: Props) { +const SHOW_SUCCESS_DURATION = 2 * 1000; + +export function ClipboardButton({ + onClipboardCopy, + onClipboardError, + children, + getText, + icon, + variant, + ...buttonProps +}: Props) { + const [showCopySuccess, setShowCopySuccess] = useState(false); + + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + if (showCopySuccess) { + timeoutId = setTimeout(() => { + setShowCopySuccess(false); + }, SHOW_SUCCESS_DURATION); + } + + return () => { + window.clearTimeout(timeoutId); + }; + }, [showCopySuccess]); + const buttonRef = useRef(null); const copyTextCallback = useCallback(async () => { const textToCopy = getText(); try { await copyText(textToCopy, buttonRef); + setShowCopySuccess(true); onClipboardCopy?.(textToCopy); } catch (e) { onClipboardError?.(textToCopy, e); @@ -25,7 +52,14 @@ export function ClipboardButton({ onClipboardCopy, onClipboardError, children, g }, [getText, onClipboardCopy, onClipboardError]); return ( - ); diff --git a/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx b/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx index 2feb934a8c0..0f6611f844f 100644 --- a/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx @@ -1,8 +1,7 @@ import { isString } from 'lodash'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { ClipboardButton } from '../ClipboardButton/ClipboardButton'; -import { Icon } from '../Icon/Icon'; import { Modal } from '../Modal/Modal'; import { CodeEditor } from '../Monaco/CodeEditor'; @@ -13,23 +12,6 @@ interface TableCellInspectModalProps { } export function TableCellInspectModal({ value, onDismiss, mode }: TableCellInspectModalProps) { - const [isInClipboard, setIsInClipboard] = useState(false); - const timeoutRef = React.useRef(); - - useEffect(() => { - if (isInClipboard) { - timeoutRef.current = window.setTimeout(() => { - setIsInClipboard(false); - }, 2000); - } - - return () => { - if (timeoutRef.current) { - window.clearTimeout(timeoutRef.current); - } - }; - }, [isInClipboard]); - let displayValue = value; if (isString(value)) { try { @@ -60,15 +42,8 @@ export function TableCellInspectModal({ value, onDismiss, mode }: TableCellInspe
{text}
)} - text} onClipboardCopy={() => setIsInClipboard(true)}> - {!isInClipboard ? ( - 'Copy to Clipboard' - ) : ( - <> - - Copied to clipboard - - )} + text}> + Copy to Clipboard diff --git a/public/app/features/alerting/TestRuleResult.tsx b/public/app/features/alerting/TestRuleResult.tsx index bbed594e7d3..3de19747775 100644 --- a/public/app/features/alerting/TestRuleResult.tsx +++ b/public/app/features/alerting/TestRuleResult.tsx @@ -1,9 +1,7 @@ import React, { PureComponent } from 'react'; -import { AppEvents } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { LoadingPlaceholder, JSONFormatter, Icon, HorizontalGroup, ClipboardButton } from '@grafana/ui'; -import appEvents from 'app/core/app_events'; import { DashboardModel, PanelModel } from '../dashboard/state'; @@ -58,10 +56,6 @@ export class TestRuleResult extends PureComponent { return JSON.stringify(this.formattedJson, null, 2); }; - onClipboardSuccess = () => { - appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']); - }; - onToggleExpand = () => { this.setState((prevState) => ({ ...prevState, @@ -108,7 +102,7 @@ export class TestRuleResult extends PureComponent {
{this.renderExpandCollapse()}
- + Copy to Clipboard
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index ce59459125f..c657ab760ea 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -200,9 +200,7 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { rightButtons.push( { - notifyApp.success('URL copied!'); - }} + icon="copy" onClipboardError={(copiedText) => { notifyApp.error('Error while copying URL', copiedText); }} diff --git a/public/app/features/api-keys/ApiKeysAddedModal.tsx b/public/app/features/api-keys/ApiKeysAddedModal.tsx index 26bfe2b8e26..ce04d4bd7ef 100644 --- a/public/app/features/api-keys/ApiKeysAddedModal.tsx +++ b/public/app/features/api-keys/ApiKeysAddedModal.tsx @@ -2,11 +2,7 @@ import React, { useCallback } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Field, Modal, useStyles2, Input, Icon, ClipboardButton } from '@grafana/ui'; - -import { notifyApp } from '../../core/actions'; -import { createSuccessNotification } from '../../core/copy/appNotification'; -import { dispatch } from '../../store/store'; +import { Alert, Field, Modal, useStyles2, Input, ClipboardButton } from '@grafana/ui'; export interface Props { onDismiss: () => void; @@ -17,9 +13,7 @@ export interface Props { export function ApiKeysAddedModal({ onDismiss, apiKey, rootPath }: Props): JSX.Element { const styles = useStyles2(getStyles); const getClipboardText = useCallback(() => apiKey, [apiKey]); - const onClipboardCopy = () => { - dispatch(notifyApp(createSuccessNotification('Content copied to clipboard'))); - }; + return ( @@ -28,8 +22,8 @@ export function ApiKeysAddedModal({ onDismiss, apiKey, rootPath }: Props): JSX.E value={apiKey} readOnly addonAfter={ - - Copy + + Copy } /> diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx index 8fdb65546d6..335cbd53c16 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx @@ -5,13 +5,11 @@ import React, { useCallback, useState } from 'react'; import { GrafanaTheme } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { Button, ClipboardButton, HorizontalGroup, stylesFactory, TextArea, useTheme } from '@grafana/ui'; -import { useAppNotification } from 'app/core/copy/appNotification'; import { SaveDashboardFormProps } from '../types'; export const SaveProvisionedDashboardForm: React.FC = ({ dashboard, onCancel }) => { const theme = useTheme(); - const notifyApp = useAppNotification(); const [dashboardJSON, setDashboardJson] = useState(() => { const clone = dashboard.getSaveModelClone(); delete clone.id; @@ -25,10 +23,6 @@ export const SaveProvisionedDashboardForm: React.FC = ({ saveAs(blob, dashboard.title + '-' + new Date().getTime() + '.json'); }, [dashboard.title, dashboardJSON]); - const onCopyToClipboardSuccess = useCallback(() => { - notifyApp.success('Dashboard JSON copied to clipboard'); - }, [notifyApp]); - const styles = getStyles(theme); return ( <> @@ -64,7 +58,7 @@ export const SaveProvisionedDashboardForm: React.FC = ({ - dashboardJSON} onClipboardCopy={onCopyToClipboardSuccess}> + dashboardJSON}> Copy JSON to clipboard