diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json
index 0c267a38d0d..96a3d6a15ba 100644
--- a/openmetadata-ui/src/main/resources/ui/package.json
+++ b/openmetadata-ui/src/main/resources/ui/package.json
@@ -70,7 +70,6 @@
"react-awesome-query-builder": "5.1.2",
"react-codemirror2": "^7.2.1",
"react-context-mutex": "^2.0.0",
- "react-copy-to-clipboard": "^5.0.4",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.2",
"react-dom": "^16.14.0",
@@ -159,7 +158,6 @@
"@types/node": "^15.6.1",
"@types/pako": "^2.0.0",
"@types/react": "^17.0.8",
- "@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-lazylog": "^4.5.1",
"@types/react-router-dom": "^5.1.7",
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg
index b0c0dc9eff9..69ae7ff3acc 100644
--- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/icon-copy.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.test.tsx
index a7ddea958dd..ec3519d59fb 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.test.tsx
@@ -11,23 +11,83 @@
* limitations under the License.
*/
-import { getByTestId, render } from '@testing-library/react';
+import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
import CopyToClipboardButton from './CopyToClipboardButton';
-const mockProps = {
- copyText: 'mock-copy',
+const clipboardWriteTextMock = jest.fn();
+const clipboardMock = {
+ writeText: clipboardWriteTextMock,
};
+const value = 'Test Value';
+const callBack = jest.fn();
+
+Object.defineProperty(window.navigator, 'clipboard', {
+ value: clipboardMock,
+ writable: true,
+});
+
describe('Test CopyToClipboardButton Component', () => {
- it('Should render all child elements', () => {
- const { container } = render(, {
- wrapper: MemoryRouter,
+ it('Should render all child elements', async () => {
+ render();
+
+ expect(screen.getByTestId('copy-icon')).toBeInTheDocument();
+ expect(screen.getByTestId('copy-secret')).toBeInTheDocument();
+ });
+
+ it('Should calls onCopy callback when clicked', async () => {
+ render();
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('copy-secret'));
});
- const copyIcon = getByTestId(container, 'copy-icon');
+ expect(callBack).toHaveBeenCalled();
+ });
- expect(copyIcon).toBeInTheDocument();
+ it('Should show success message and hide after timeout', async () => {
+ jest.useFakeTimers();
+ await act(async () => {
+ render();
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('copy-secret'));
+ });
+
+ expect(screen.getByTestId('copy-success')).toBeInTheDocument();
+
+ jest.advanceTimersByTime(1500);
+
+ // success message should not be in the dom after timeout
+ expect(screen.queryByTestId('copy-success')).not.toBeInTheDocument();
+ });
+
+ it('Should have copied text in clipboard', async () => {
+ render();
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('copy-secret'));
+ });
+
+ // clipboard should have the copied text
+ expect(clipboardWriteTextMock).toHaveBeenCalledWith(value);
+ });
+
+ it('Should handles error when cannot access clipboard API', async () => {
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: undefined,
+ writable: true,
+ });
+
+ render();
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('copy-secret'));
+ });
+
+ // not show the success message if clipboard API has error
+ expect(screen.queryByTestId('copy-success')).not.toBeInTheDocument();
});
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.tsx b/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.tsx
index f2b7312f8cf..cbf198d4cb4 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/buttons/CopyToClipboardButton/CopyToClipboardButton.tsx
@@ -11,13 +11,11 @@
* limitations under the License.
*/
-import { Popover, PopoverProps } from 'antd';
-import React, { FunctionComponent, useState } from 'react';
-import CopyToClipboard from 'react-copy-to-clipboard';
+import { Button, Popover, PopoverProps } from 'antd';
+import { ReactComponent as CopyIcon } from 'assets/svg/icon-copy.svg';
+import { useClipboard } from 'hooks/useClipBoard';
+import React, { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
-import SVGIcons, { Icons } from '../../../utils/SvgUtils';
-
-import { Button } from '../Button/Button';
interface Props {
copyText: string;
@@ -33,44 +31,33 @@ export const CopyToClipboardButton: FunctionComponent = ({
onCopy,
}: Props) => {
const { t } = useTranslation();
- const [copied, setCopied] = useState(false);
-
- const handleCopying = () => {
- setCopied(true);
- onCopy?.();
- setTimeout(() => {
- setCopied(false);
- }, copyTimer);
- };
+ const { hasCopied, onCopyToClipBoard } = useClipboard(
+ copyText,
+ copyTimer,
+ onCopy
+ );
return (
-
+
+ {t('message.copied-to-clipboard')}
+
+ }
+ open={hasCopied}
+ placement={position}
+ trigger="click">
-
+ icon={}
+ type="text"
+ onClick={onCopyToClipBoard}
+ />
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts
new file mode 100644
index 00000000000..b751f5ef203
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.test.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed 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 { act } from '@testing-library/react';
+import { renderHook } from '@testing-library/react-hooks';
+import { useClipboard } from './useClipBoard';
+
+const clipboardWriteTextMock = jest.fn();
+const clipboardMock = {
+ writeText: clipboardWriteTextMock,
+};
+
+Object.defineProperty(window.navigator, 'clipboard', {
+ value: clipboardMock,
+ writable: true,
+});
+
+const value = 'Test Value';
+const callBack = jest.fn();
+const timeout = 1000;
+
+describe('useClipboard hook', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('Should copy to clipboard', async () => {
+ clipboardWriteTextMock.mockResolvedValue(value);
+ const { result } = renderHook(() => useClipboard(value, timeout, callBack));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
+ expect(result.current.hasCopied).toBe(true);
+ expect(callBack).toHaveBeenCalled();
+ });
+
+ it('Should handle error while copying to clipboard', async () => {
+ clipboardWriteTextMock.mockRejectedValue('Error');
+ const { result } = renderHook(() => useClipboard(value));
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(value);
+ expect(result.current.hasCopied).toBe(false);
+ });
+
+ it('Should reset hasCopied after the timeout', async () => {
+ clipboardWriteTextMock.mockResolvedValue(value);
+
+ jest.useFakeTimers();
+
+ const { result, rerender } = renderHook(
+ ({ value, timeout, callBack }) => useClipboard(value, timeout, callBack),
+ { initialProps: { value, timeout, callBack } }
+ );
+
+ await act(async () => {
+ result.current.onCopyToClipBoard();
+ });
+
+ expect(result.current.hasCopied).toBe(true);
+
+ jest.advanceTimersByTime(timeout);
+
+ rerender({ value, timeout, callBack });
+
+ expect(result.current.hasCopied).toBe(false);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts
new file mode 100644
index 00000000000..972023c30fb
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 Collate.
+ * Licensed 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 { toNumber } from 'lodash';
+import { useCallback, useEffect, useState } from 'react';
+
+/**
+ * React hook to copy text to clipboard
+ * @param value the text to copy
+ * @param timeout delay (in ms) to switch back to initial state once copied.
+ * @param callBack execute when content is copied to clipboard
+ */
+export const useClipboard = (
+ value: string,
+ timeout = 1500,
+ callBack?: () => void
+) => {
+ // local state
+ const [hasCopied, setHasCopied] = useState(false);
+ const [valueState, setValueState] = useState(value);
+
+ // handlers
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(valueState);
+ setHasCopied(true);
+ callBack && callBack();
+ } catch (error) {
+ setHasCopied(false);
+ }
+ }, [valueState]);
+
+ // side effects
+ useEffect(() => setValueState(value), [value]);
+
+ useEffect(() => {
+ let timeoutId: number | null = null;
+
+ if (hasCopied) {
+ timeoutId = toNumber(
+ setTimeout(() => {
+ setHasCopied(false);
+ }, timeout)
+ );
+ }
+
+ return () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ };
+ }, [timeout, hasCopied]);
+
+ return {
+ onCopyToClipBoard: handleCopy,
+ hasCopied,
+ };
+};
diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock
index def82461223..e85b5baab56 100644
--- a/openmetadata-ui/src/main/resources/ui/yarn.lock
+++ b/openmetadata-ui/src/main/resources/ui/yarn.lock
@@ -3701,13 +3701,6 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
-"@types/react-copy-to-clipboard@^5.0.2":
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#c29690b472a54edff35916f0d1c6c797ad0fd34b"
- integrity sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==
- dependencies:
- "@types/react" "*"
-
"@types/react-dom@*", "@types/react-dom@>=16.9.0":
version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
@@ -5762,13 +5755,6 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-copy-to-clipboard@^3:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
- integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
- dependencies:
- toggle-selection "^1.0.6"
-
copy-to-clipboard@^3.2.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
@@ -11399,7 +11385,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -12077,14 +12063,6 @@ react-context-mutex@^2.0.0:
resolved "https://registry.yarnpkg.com/react-context-mutex/-/react-context-mutex-2.0.0.tgz#4060bc8fbced6d2680ab093139c727201d402b88"
integrity sha512-Qss0YfJAKQ/+CYdVjHB3w7wRLdeetSrp2IMEEouXEpoTdohaCXIKZtFyNMMKDytLVWqHiiXg0RDA2kj/DQbeMQ==
-react-copy-to-clipboard@^5.0.4:
- version "5.0.4"
- resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#42ec519b03eb9413b118af92d1780c403a5f19bf"
- integrity sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==
- dependencies:
- copy-to-clipboard "^3"
- prop-types "^15.5.8"
-
react-dnd-html5-backend@14.0.2:
version "14.0.2"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c"