This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch chore/consolidate-copy-to-clipboard in repository https://gitbox.apache.org/repos/asf/superset.git
commit 806dbb4aa5d2390edb09cc2c0a410ed0d0b16abd Author: Evan Rusackas <[email protected]> AuthorDate: Fri Feb 6 14:26:35 2026 -0800 chore: consolidate CopyToClipboard and clipboard utilities to @superset-ui/core This change: - Creates a generic CopyToClipboard component in @superset-ui/core - Uses onSuccess/onError callbacks (not tied to any toast system) - Includes tooltip, text display, and async getText support - Creates clipboard utilities in @superset-ui/core - copyTextToClipboard with Safari/Chrome compatibility - isSafari browser detection utility - Creates ErrorBoundary in @superset-ui/core (removes react-error-boundary dep) - Updates src/components/CopyToClipboard to be a thin wrapper that: - Integrates with Superset's toast notification system - Delegates to the core component for actual functionality - Updates src/utils/copy.ts to re-export from core for backward compatibility Developers can now: - Use the generic CopyToClipboard from @superset-ui/core/components directly - Use the toast-integrated version from src/components for Superset-specific use Co-Authored-By: Claude Opus 4.5 <[email protected]> --- superset-frontend/package-lock.json | 10 -- .../packages/superset-ui-core/package.json | 1 - .../src/chart/components/ErrorBoundary.tsx | 83 +++++++++++ .../src/chart/components/SuperChart.tsx | 6 +- .../packages/superset-ui-core/src/chart/index.ts | 5 + .../src/components/CopyToClipboard/index.tsx | 55 +++++--- .../superset-ui-core/src/components/index.ts | 1 + .../superset-ui-core/src/utils/clipboard.ts} | 39 ++++-- .../packages/superset-ui-core/src/utils/index.ts | 1 + .../test/chart/components/SuperChart.test.tsx | 2 +- .../src/components/CopyToClipboard/index.tsx | 156 +++++++-------------- .../src/components/CopyToClipboard/types.ts | 32 ----- superset-frontend/src/utils/copy.ts | 84 ++--------- 13 files changed, 219 insertions(+), 256 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 1a97f933ab2..d8d810d8c9b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -49999,15 +49999,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-error-boundary": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.0.tgz", - "integrity": "sha512-02k9WQ/mUhdbXir0tC1NiMesGzRPaCsJEWU/4bcFrbY1YMZOtHShtZP6zw0SJrBWA/31H0KT9/FgdL8+sPKgHA==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, "node_modules/react-google-recaptcha": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", @@ -63712,7 +63703,6 @@ "re-resizable": "^6.11.2", "react-ace": "^14.0.1", "react-draggable": "^4.5.0", - "react-error-boundary": "^6.1.0", "react-js-cron": "^5.2.0", "react-markdown": "^8.0.7", "react-resize-detector": "^7.1.2", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index c8bc4627b1d..f02d4cbc2e9 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -55,7 +55,6 @@ "react-resize-detector": "^7.1.2", "react-syntax-highlighter": "^16.1.0", "react-ultimate-pagination": "^1.3.2", - "react-error-boundary": "^6.1.0", "react-markdown": "^8.0.7", "regenerator-runtime": "^0.14.1", "rehype-raw": "^7.0.0", diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/ErrorBoundary.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/ErrorBoundary.tsx new file mode 100644 index 00000000000..39c6031a76d --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/ErrorBoundary.tsx @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Component, ComponentType, ErrorInfo, ReactNode } from 'react'; + +export interface FallbackProps { + error: Error; + resetErrorBoundary: () => void; +} + +export interface ErrorBoundaryProps { + children: ReactNode; + FallbackComponent?: ComponentType<FallbackProps>; + fallbackRender?: (props: FallbackProps) => ReactNode; + onError?: (error: Error, info: ErrorInfo) => void; +} + +interface ErrorBoundaryState { + error: Error | null; +} + +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + this.props.onError?.(error, info); + } + + resetErrorBoundary = (): void => { + this.setState({ error: null }); + }; + + render() { + const { error } = this.state; + const { children, FallbackComponent, fallbackRender } = this.props; + + if (error) { + const fallbackProps: FallbackProps = { + error, + resetErrorBoundary: this.resetErrorBoundary, + }; + + if (fallbackRender) { + return fallbackRender(fallbackProps); + } + + if (FallbackComponent) { + return <FallbackComponent {...fallbackProps} />; + } + + return null; + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChart.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChart.tsx index 553a98f568a..f4b40649c66 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChart.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/SuperChart.tsx @@ -27,9 +27,9 @@ import { import { ErrorBoundary, - ErrorBoundaryProps, - FallbackProps, -} from 'react-error-boundary'; + type ErrorBoundaryProps, + type FallbackProps, +} from './ErrorBoundary'; import { ParentSize } from '@visx/responsive'; import { createSelector } from 'reselect'; import { withTheme } from '@emotion/react'; diff --git a/superset-frontend/packages/superset-ui-core/src/chart/index.ts b/superset-frontend/packages/superset-ui-core/src/chart/index.ts index 69081f9740d..edfd1e57f39 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/index.ts @@ -26,6 +26,11 @@ export { ChartProps }; export type { ChartPropsConfig }; export { default as createLoadableRenderer } from './components/createLoadableRenderer'; +export { + ErrorBoundary, + type ErrorBoundaryProps, + type FallbackProps, +} from './components/ErrorBoundary'; export { default as reactify } from './components/reactify'; export { default as SuperChart } from './components/SuperChart'; diff --git a/superset-frontend/src/components/CopyToClipboard/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/CopyToClipboard/index.tsx similarity index 69% copy from superset-frontend/src/components/CopyToClipboard/index.tsx copy to superset-frontend/packages/superset-ui-core/src/components/CopyToClipboard/index.tsx index 976f0059c30..490bc747193 100644 --- a/superset-frontend/src/components/CopyToClipboard/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/CopyToClipboard/index.tsx @@ -16,13 +16,39 @@ * specific language governing permissions and limitations * under the License. */ -import { Component, cloneElement, ReactElement } from 'react'; +import { + Component, + cloneElement, + type ReactElement, + type ReactNode, +} from 'react'; +import { css, type SupersetTheme } from '@apache-superset/core/ui'; import { t } from '@apache-superset/core'; -import { css, SupersetTheme } from '@apache-superset/core/ui'; -import copyTextToClipboard from 'src/utils/copy'; -import { Tooltip } from '@superset-ui/core/components'; -import withToasts from '../MessageToasts/withToasts'; -import type { CopyToClipboardProps } from './types'; +import { copyTextToClipboard } from '../../utils/clipboard'; +import { Tooltip } from '../Tooltip'; + +export interface CopyToClipboardProps { + /** The clickable element that triggers the copy action */ + copyNode?: ReactNode; + /** Async function to get text to copy (alternative to `text` prop) */ + getText?: (callback: (data: string) => void) => void; + /** Called after copy attempt completes (success or failure) */ + onCopyEnd?: () => void; + /** Called when copy succeeds */ + onSuccess?: () => void; + /** Called when copy fails */ + onError?: (error: Error) => void; + /** Whether to show the text alongside the copy button */ + shouldShowText?: boolean; + /** Static text to copy (alternative to `getText` prop) */ + text?: string; + /** Whether to wrap in a span with text display */ + wrapped?: boolean; + /** Tooltip text shown on hover */ + tooltipText?: string; + /** Hide the tooltip entirely */ + hideTooltip?: boolean; +} const defaultProps: Partial<CopyToClipboardProps> = { copyNode: <span>{t('Copy')}</span>, @@ -33,7 +59,7 @@ const defaultProps: Partial<CopyToClipboardProps> = { hideTooltip: false, }; -class CopyToClip extends Component<CopyToClipboardProps> { +export class CopyToClipboard extends Component<CopyToClipboardProps> { static defaultProps = defaultProps; constructor(props: CopyToClipboardProps) { @@ -62,17 +88,13 @@ class CopyToClip extends Component<CopyToClipboardProps> { copyToClipboard(textToCopy: Promise<string>) { copyTextToClipboard(() => textToCopy) .then(() => { - this.props.addSuccessToast(t('Copied to clipboard!')); + this.props.onSuccess?.(); }) - .catch(() => { - this.props.addDangerToast( - t( - 'Sorry, your browser does not support copying. Use Ctrl / Cmd + C!', - ), - ); + .catch((error: Error) => { + this.props.onError?.(error); }) .finally(() => { - if (this.props.onCopyEnd) this.props.onCopyEnd(); + this.props.onCopyEnd?.(); }); } @@ -128,5 +150,4 @@ class CopyToClip extends Component<CopyToClipboardProps> { } } -export const CopyToClipboard = withToasts(CopyToClip); -export type { CopyToClipboardProps }; +export default CopyToClipboard; diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts index 30bad84f4d8..d01ec8e4478 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts @@ -210,3 +210,4 @@ export { type CodeEditorTheme, } from './CodeEditor'; export { ActionButton, type ActionProps } from './ActionButton'; +export { CopyToClipboard, type CopyToClipboardProps } from './CopyToClipboard'; diff --git a/superset-frontend/src/utils/copy.ts b/superset-frontend/packages/superset-ui-core/src/utils/clipboard.ts similarity index 73% copy from superset-frontend/src/utils/copy.ts copy to superset-frontend/packages/superset-ui-core/src/utils/clipboard.ts index 0980f2ab170..6de402aeb1d 100644 --- a/superset-frontend/src/utils/copy.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/clipboard.ts @@ -17,10 +17,24 @@ * under the License. */ -import { isSafari } from './common'; +/** + * Detects if the browser is Safari (WebKit without Chrome). + */ +export const isSafari = (): boolean => { + const { userAgent } = navigator; + return Boolean(userAgent && /^((?!chrome|android).)*safari/i.test(userAgent)); +}; -// Use the new Clipboard API if the browser supports it -const copyTextWithClipboardApi = async (getText: () => Promise<string>) => { +/** + * Copy text to clipboard using the modern Clipboard API with fallback + * for older browsers. + * + * @param getText - Function that returns a Promise resolving to the text to copy + * @returns Promise that resolves when copy succeeds, rejects on failure + */ +const copyTextWithClipboardApi = async ( + getText: () => Promise<string>, +): Promise<void> => { // Safari (WebKit) does not support delayed generation of clipboard. // This means that writing to the clipboard, from the moment the user // interacts with the app, must be instantaneous. @@ -49,7 +63,18 @@ const copyTextWithClipboardApi = async (getText: () => Promise<string>) => { } }; -const copyTextToClipboard = (getText: () => Promise<string>) => +/** + * Copy text to clipboard with automatic fallback for older browsers. + * + * Uses the modern Clipboard API when available, falling back to + * document.execCommand('copy') for legacy browser support. + * + * @param getText - Function that returns a Promise resolving to the text to copy + * @returns Promise that resolves when copy succeeds, rejects on failure + */ +export const copyTextToClipboard = ( + getText: () => Promise<string>, +): Promise<void> => copyTextWithClipboardApi(getText) // If the Clipboard API is not supported, fallback to the older method. .catch(() => @@ -73,10 +98,10 @@ const copyTextToClipboard = (getText: () => Promise<string>) => try { if (!document.execCommand('copy')) { - reject(); + reject(new Error('execCommand copy failed')); } } catch (err) { - reject(); + reject(err); } document.body.removeChild(span); @@ -91,5 +116,3 @@ const copyTextToClipboard = (getText: () => Promise<string>) => }), ), ); - -export default copyTextToClipboard; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/index.ts b/superset-frontend/packages/superset-ui-core/src/utils/index.ts index 4d6e869cd0c..5f717de0c32 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/index.ts @@ -34,3 +34,4 @@ export * from './typedMemo'; export * from './html'; export * from './tooltip'; export * from './merge'; +export * from './clipboard'; diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx index a739748d2ee..9f647320588 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx @@ -21,7 +21,7 @@ import '@testing-library/jest-dom'; import { render, screen } from '@superset-ui/core/spec'; import mockConsole, { RestoreConsole } from 'jest-mock-console'; import { triggerResizeObserver } from 'resize-observer-polyfill'; -import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorBoundary } from '../../../src/chart/components/ErrorBoundary'; import { promiseTimeout, SuperChart } from '@superset-ui/core'; import { WrapperProps } from '../../../src/chart/components/SuperChart'; diff --git a/superset-frontend/src/components/CopyToClipboard/index.tsx b/superset-frontend/src/components/CopyToClipboard/index.tsx index 976f0059c30..58f61bd3738 100644 --- a/superset-frontend/src/components/CopyToClipboard/index.tsx +++ b/superset-frontend/src/components/CopyToClipboard/index.tsx @@ -16,117 +16,57 @@ * specific language governing permissions and limitations * under the License. */ -import { Component, cloneElement, ReactElement } from 'react'; -import { t } from '@apache-superset/core'; -import { css, SupersetTheme } from '@apache-superset/core/ui'; -import copyTextToClipboard from 'src/utils/copy'; -import { Tooltip } from '@superset-ui/core/components'; -import withToasts from '../MessageToasts/withToasts'; -import type { CopyToClipboardProps } from './types'; - -const defaultProps: Partial<CopyToClipboardProps> = { - copyNode: <span>{t('Copy')}</span>, - onCopyEnd: () => {}, - shouldShowText: true, - wrapped: true, - tooltipText: t('Copy to clipboard'), - hideTooltip: false, -}; - -class CopyToClip extends Component<CopyToClipboardProps> { - static defaultProps = defaultProps; - - constructor(props: CopyToClipboardProps) { - super(props); - this.copyToClipboard = this.copyToClipboard.bind(this); - this.onClick = this.onClick.bind(this); - } - - onClick() { - if (this.props.getText) { - this.props.getText((d: string) => { - this.copyToClipboard(Promise.resolve(d)); - }); - } else { - this.copyToClipboard(Promise.resolve(this.props.text || '')); - } - } - - getDecoratedCopyNode() { - return cloneElement(this.props.copyNode as ReactElement, { - style: { cursor: 'pointer' }, - onClick: this.onClick, - }); - } - copyToClipboard(textToCopy: Promise<string>) { - copyTextToClipboard(() => textToCopy) - .then(() => { - this.props.addSuccessToast(t('Copied to clipboard!')); - }) - .catch(() => { - this.props.addDangerToast( - t( - 'Sorry, your browser does not support copying. Use Ctrl / Cmd + C!', - ), - ); - }) - .finally(() => { - if (this.props.onCopyEnd) this.props.onCopyEnd(); - }); - } - - renderTooltip(cursor: string) { - return ( - <> - {!this.props.hideTooltip ? ( - <Tooltip - id="copy-to-clipboard-tooltip" - placement="topRight" - style={{ cursor }} - title={this.props.tooltipText || ''} - trigger={['hover']} - arrow={{ pointAtCenter: true }} - > - {this.getDecoratedCopyNode()} - </Tooltip> - ) : ( - this.getDecoratedCopyNode() - )} - </> - ); - } +/** + * This is a Superset-specific wrapper around the generic CopyToClipboard + * component from @superset-ui/core. It integrates with Superset's toast + * notification system to show success/error messages. + * + * For the generic component without toast integration, import directly from: + * import { CopyToClipboard } from '@superset-ui/core/components'; + */ +import { t } from '@apache-superset/core'; +import { + CopyToClipboard as BaseCopyToClipboard, + type CopyToClipboardProps as BaseCopyToClipboardProps, +} from '@superset-ui/core/components'; +import withToasts, { type ToastProps } from '../MessageToasts/withToasts'; - renderNotWrapped() { - return this.renderTooltip('pointer'); - } +export interface CopyToClipboardProps extends Omit< + BaseCopyToClipboardProps, + 'onSuccess' | 'onError' +> { + /** Custom success message (defaults to "Copied to clipboard!") */ + successMessage?: string; + /** Custom error message (defaults to browser not supporting copying message) */ + errorMessage?: string; +} - renderLink() { - return ( - <span css={{ display: 'inline-flex', alignItems: 'center' }}> - {this.props.shouldShowText && this.props.text && ( - <span - data-test="short-url" - css={(theme: SupersetTheme) => css` - margin-right: ${theme.sizeUnit}px; - `} - > - {this.props.text} - </span> - )} - {this.renderTooltip('pointer')} - </span> - ); - } +type CopyToClipboardWithToastsProps = CopyToClipboardProps & ToastProps; - render() { - const { wrapped } = this.props; - if (!wrapped) { - return this.renderNotWrapped(); - } - return this.renderLink(); - } +function CopyToClipboardWithToasts({ + addSuccessToast, + addDangerToast, + successMessage, + errorMessage, + ...props +}: CopyToClipboardWithToastsProps) { + return ( + <BaseCopyToClipboard + {...props} + onSuccess={() => + addSuccessToast(successMessage || t('Copied to clipboard!')) + } + onError={() => + addDangerToast( + errorMessage || + t( + 'Sorry, your browser does not support copying. Use Ctrl / Cmd + C!', + ), + ) + } + /> + ); } -export const CopyToClipboard = withToasts(CopyToClip); -export type { CopyToClipboardProps }; +export const CopyToClipboard = withToasts(CopyToClipboardWithToasts); diff --git a/superset-frontend/src/components/CopyToClipboard/types.ts b/superset-frontend/src/components/CopyToClipboard/types.ts deleted file mode 100644 index 10d87e8decf..00000000000 --- a/superset-frontend/src/components/CopyToClipboard/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import type { ReactNode } from 'react'; - -export interface CopyToClipboardProps { - copyNode?: ReactNode; - getText?: (callback: (data: string) => void) => void; - onCopyEnd?: () => void; - shouldShowText?: boolean; - text?: string; - wrapped?: boolean; - tooltipText?: string; - addDangerToast: (msg: string) => void; - addSuccessToast: (msg: string) => void; - hideTooltip?: boolean; -} diff --git a/superset-frontend/src/utils/copy.ts b/superset-frontend/src/utils/copy.ts index 0980f2ab170..041e1a93898 100644 --- a/superset-frontend/src/utils/copy.ts +++ b/superset-frontend/src/utils/copy.ts @@ -17,79 +17,11 @@ * under the License. */ -import { isSafari } from './common'; - -// Use the new Clipboard API if the browser supports it -const copyTextWithClipboardApi = async (getText: () => Promise<string>) => { - // Safari (WebKit) does not support delayed generation of clipboard. - // This means that writing to the clipboard, from the moment the user - // interacts with the app, must be instantaneous. - // However, neither writeText nor write accepts a Promise, so - // we need to create a ClipboardItem that accepts said Promise to - // delay the text generation, as needed. - // Source: https://bugs.webkit.org/show_bug.cgi?id=222262P - if (isSafari()) { - try { - const clipboardItem = new ClipboardItem({ - 'text/plain': getText(), - }); - await navigator.clipboard.write([clipboardItem]); - } catch { - // Fallback to default clipboard API implementation - const text = await getText(); - await navigator.clipboard.writeText(text); - } - } else { - // For Blink, the above method won't work, but we can use the - // default (intended) API, since the delayed generation of the - // clipboard is now supported. - // Source: https://bugs.chromium.org/p/chromium/issues/detail?id=1014310 - const text = await getText(); - await navigator.clipboard.writeText(text); - } -}; - -const copyTextToClipboard = (getText: () => Promise<string>) => - copyTextWithClipboardApi(getText) - // If the Clipboard API is not supported, fallback to the older method. - .catch(() => - getText().then( - text => - new Promise<void>((resolve, reject) => { - const selection: Selection | null = document.getSelection(); - if (selection) { - selection.removeAllRanges(); - const range = document.createRange(); - const span = document.createElement('span'); - span.textContent = text; - span.style.position = 'fixed'; - span.style.top = '0'; - span.style.clip = 'rect(0, 0, 0, 0)'; - span.style.whiteSpace = 'pre'; - - document.body.appendChild(span); - range.selectNode(span); - selection.addRange(range); - - try { - if (!document.execCommand('copy')) { - reject(); - } - } catch (err) { - reject(); - } - - document.body.removeChild(span); - if (selection.removeRange) { - selection.removeRange(range); - } else { - selection.removeAllRanges(); - } - } - - resolve(); - }), - ), - ); - -export default copyTextToClipboard; +/** + * Re-export clipboard utilities from @superset-ui/core for backward compatibility. + * + * For new code, prefer importing directly from: + * import { copyTextToClipboard, isSafari } from '@superset-ui/core'; + */ +export { copyTextToClipboard, isSafari } from '@superset-ui/core'; +export { copyTextToClipboard as default } from '@superset-ui/core';
