Browse Source

WIP: #190 JSON export (#236)

* Implement functionality for copying and downloading data

* Test
Alexander Krivonosov 4 years ago
parent
commit
7c86adbd67

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessageItem.tsx

@@ -21,7 +21,7 @@ const MessageItem: React.FC<MessageItemProp> = ({
     <td style={{ width: 150 }}>{offset}</td>
     <td style={{ width: 100 }}>{partition}</td>
     <td style={{ wordBreak: 'break-word' }}>
-      {content && <JSONViewer data={content as { [key: string]: string }} />}
+      {content && <JSONViewer data={content as Record<string, string>} />}
     </td>
   </tr>
 );

+ 44 - 0
kafka-ui-react-app/src/components/common/JSONViewer/DynamicButton.tsx

@@ -0,0 +1,44 @@
+import React from 'react';
+
+interface ButtonProps {
+  callback: () => void;
+  classes?: string;
+  title: string;
+  style?: { [key: string]: string | number };
+  text: {
+    default: string;
+    dynamic: string;
+  };
+}
+
+const DynamicButton: React.FC<ButtonProps> = ({
+  callback,
+  classes,
+  title,
+  style,
+  text,
+  children,
+}) => {
+  const [buttonText, setButtonText] = React.useState(text.default);
+  let timeout: number;
+  const clickHandler = () => {
+    callback();
+    setButtonText(text.dynamic);
+    timeout = window.setTimeout(() => setButtonText(text.default), 3000);
+  };
+  React.useEffect(() => () => window.clearTimeout(timeout), [callback]);
+  return (
+    <button
+      className={classes}
+      title={title}
+      type="button"
+      style={style}
+      onClick={clickHandler}
+    >
+      {children}
+      <span>{buttonText}</span>
+    </button>
+  );
+};
+
+export default DynamicButton;

+ 42 - 3
kafka-ui-react-app/src/components/common/JSONViewer/JSONViewer.tsx

@@ -1,6 +1,8 @@
 import React from 'react';
 import JSONTree from 'react-json-tree';
+import useDataSaver from 'lib/hooks/useDataSaver';
 import theme from './themes/google';
+import DynamicButton from './DynamicButton';
 
 interface JSONViewerProps {
   data: {
@@ -8,8 +10,45 @@ interface JSONViewerProps {
   };
 }
 
-const JSONViewer: React.FC<JSONViewerProps> = ({ data }) => (
-  <JSONTree data={data} theme={theme} shouldExpandNode={() => true} hideRoot />
-);
+const JSONViewer: React.FC<JSONViewerProps> = ({ data }) => {
+  const { copyToClipboard, saveFile } = useDataSaver();
+  const copyButtonHandler = () => {
+    copyToClipboard(JSON.stringify(data));
+  };
+  const buttonClasses = 'button is-link is-outlined is-small is-centered';
+  return (
+    <div>
+      <JSONTree
+        data={data}
+        theme={theme}
+        shouldExpandNode={() => true}
+        hideRoot
+      />
+      <div className="field has-addons is-justify-content-flex-end">
+        <DynamicButton
+          callback={copyButtonHandler}
+          classes={`${buttonClasses} mr-1`}
+          title="Copy the message to the clipboard"
+          text={{ default: 'Copy', dynamic: 'Copied!' }}
+        >
+          <span className="icon">
+            <i className="far fa-clipboard" />
+          </span>
+        </DynamicButton>
+        <button
+          className={buttonClasses}
+          title="Download the message as a .json/.txt file"
+          type="button"
+          onClick={() => saveFile(JSON.stringify(data), `topic-message`)}
+        >
+          <span className="icon">
+            <i className="fas fa-file-download" />
+          </span>
+          <span>Save</span>
+        </button>
+      </div>
+    </div>
+  );
+};
 
 export default JSONViewer;

+ 24 - 0
kafka-ui-react-app/src/components/common/JSONViewer/__tests__/DynamicButton.spec.tsx

@@ -0,0 +1,24 @@
+import { mount, shallow } from 'enzyme';
+import React from 'react';
+import DynamicButton from '../DynamicButton';
+
+describe('DynamicButton', () => {
+  const mockCallback = jest.fn();
+  const text = { default: 'DefaultText', dynamic: 'DynamicText' };
+  it('exectutes callback', () => {
+    const component = shallow(
+      <DynamicButton callback={mockCallback} title="title" text={text} />
+    );
+    component.simulate('click');
+    expect(mockCallback).toBeCalled();
+  });
+
+  it('changes the text', () => {
+    const component = mount(
+      <DynamicButton callback={mockCallback} title="title" text={text} />
+    );
+    expect(component.text()).toEqual(text.default);
+    component.simulate('click');
+    expect(component.text()).toEqual(text.dynamic);
+  });
+});

+ 40 - 0
kafka-ui-react-app/src/lib/hooks/__tests__/useDataSaver.spec.tsx

@@ -0,0 +1,40 @@
+import useDataSaver from '../useDataSaver';
+
+describe('useDataSaver hook', () => {
+  const content = {
+    title: 'title',
+  };
+  it('downloads the file', () => {
+    const link: HTMLAnchorElement = document.createElement('a');
+    link.click = jest.fn();
+    const mockCreate = jest
+      .spyOn(document, 'createElement')
+      .mockImplementation(() => link);
+    const { saveFile } = useDataSaver();
+    saveFile(JSON.stringify(content), 'fileName');
+
+    expect(mockCreate).toHaveBeenCalledTimes(1);
+    expect(link.download).toEqual('fileName.json');
+    expect(link.href).toEqual(
+      `data:text/json;charset=utf-8,${encodeURIComponent(
+        JSON.stringify(content)
+      )}`
+    );
+    expect(link.click).toHaveBeenCalledTimes(1);
+  });
+
+  it('copies the data to the clipboard', () => {
+    Object.assign(navigator, {
+      clipboard: {
+        writeText: jest.fn(),
+      },
+    });
+    jest.spyOn(navigator.clipboard, 'writeText');
+    const { copyToClipboard } = useDataSaver();
+    copyToClipboard(JSON.stringify(content));
+
+    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
+      JSON.stringify(content)
+    );
+  });
+});

+ 27 - 0
kafka-ui-react-app/src/lib/hooks/useDataSaver.tsx

@@ -0,0 +1,27 @@
+const useDataSaver = () => {
+  const copyToClipboard = (content: string) => {
+    if (navigator.clipboard) navigator.clipboard.writeText(content);
+  };
+
+  const saveFile = (content: string, fileName: string) => {
+    let extension = 'json';
+    try {
+      JSON.parse(content);
+    } catch (e) {
+      extension = 'txt';
+    }
+    const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
+      content
+    )}`;
+    const downloadAnchorNode = document.createElement('a');
+    downloadAnchorNode.setAttribute('href', dataStr);
+    downloadAnchorNode.setAttribute('download', `${fileName}.${extension}`);
+    document.body.appendChild(downloadAnchorNode);
+    downloadAnchorNode.click();
+    downloadAnchorNode.remove();
+  };
+
+  return { copyToClipboard, saveFile };
+};
+
+export default useDataSaver;