16659: Added copy to clipboard button for the api token
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 7 Aug 2020 18:22:08 +0000 (20:22 +0200)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 7 Aug 2020 18:22:08 +0000 (20:22 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

src/components/code-snippet/code-snippet.tsx
src/components/data-table-filters/data-table-filters-popover.tsx
src/components/default-view/default-view.tsx
src/components/list-item-text-icon/list-item-text-icon.tsx
src/components/tree/tree.tsx
src/components/tree/virtual-tree.tsx
src/views-components/compute-nodes-dialog/attributes-dialog.tsx
src/views-components/current-token-dialog/current-token-dialog.test.tsx [new file with mode: 0644]
src/views-components/current-token-dialog/current-token-dialog.tsx
src/views-components/details-panel/details-panel.tsx

index 84271f0ea3be2cbda6e364fd574f28adabd39aab..72d7d92b11224ff537539dbadaf06c32e2de9503 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, Typography, withStyles } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import * as classNames from 'classnames';
+import classNames from 'classnames';
 
 type CssRules = 'root' | 'space';
 
@@ -30,8 +30,8 @@ type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
 
 export const CodeSnippet = withStyles(styles)(
     ({ classes, lines, className, apiResponse }: CodeSnippetProps) =>
-        <Typography 
-        component="div" 
+        <Typography
+        component="div"
         className={classNames(classes.root, className)}>
             {
                 lines.map((line: string, index: number) => {
index 456d237580f2592adfbcc237809e961124fdbde6..80c79c39818e0f48ae4678d837c20c2a1c4a3309 100644 (file)
@@ -18,7 +18,7 @@ import {
     Tooltip,
     IconButton
 } from "@material-ui/core";
-import * as classnames from "classnames";
+import classnames from "classnames";
 import { DefaultTransformOrigin } from "~/components/popover/helpers";
 import { createTree } from '~/models/tree';
 import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
index 036fe1e4be179575309dd47b382d668b751b6721..44db79d1ed2e7dfd00709bd6bba92f0f8262b9f6 100644 (file)
@@ -7,7 +7,7 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st
 import { ArvadosTheme } from '../../common/custom-theme';
 import { Typography } from '@material-ui/core';
 import { IconType } from '../icon/icon';
-import * as classnames from "classnames";
+import classnames from "classnames";
 
 type CssRules = 'root' | 'icon' | 'message';
 
@@ -39,7 +39,7 @@ export const DefaultView = withStyles(styles)(
         <Typography className={classnames([classes.root, classRoot])} component="div">
             <Icon className={classnames([classes.icon, classIcon])} />
             {messages.map((msg: string, index: number) => {
-                return <Typography key={index}  
+                return <Typography key={index}
                     className={classnames([classes.message, classMessage])}>{msg}</Typography>;
             })}
         </Typography>
index 375538d56a2f753203b94f55ad4f1633750c4777..3bea1e1c809334ea0f4dcf15f1937d32434235c9 100644 (file)
@@ -7,7 +7,7 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st
 import { ArvadosTheme } from '~/common/custom-theme';
 import { ListItemIcon, ListItemText, Typography } from '@material-ui/core';
 import { IconType } from '../icon/icon';
-import * as classnames from "classnames";
+import classnames from "classnames";
 
 type CssRules = 'root' | 'listItemText' | 'hasMargin' | 'active';
 
index 76fbf011eb3face1a3bffe266db9b55e7fe1781e..41498fc0525f1aa7f38410f4ca9ace2efa1d874c 100644 (file)
@@ -7,7 +7,7 @@ import { List, ListItem, ListItemIcon, Collapse, Checkbox, Radio } from "@materi
 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
 import { ReactElement } from "react";
 import CircularProgress from '@material-ui/core/CircularProgress';
-import * as classnames from "classnames";
+import classnames from "classnames";
 
 import { ArvadosTheme } from '~/common/custom-theme';
 import { SidePanelRightArrowIcon } from '../icon/icon';
index 59fe34b12a0a46a0d334a97733631a5caa83d09a..6db3d1e2598517e6706ffc787a25b3fa0f3c8f34 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import * as classnames from "classnames";
+import classnames from "classnames";
 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
 import { ReactElement } from "react";
 import { FixedSizeList, ListChildComponentProps } from "react-window";
index 5d9946b4848e01e0bc7d6dd3b8740fca3828b3aa..83a983811c8a72765f441d902813da0378eed80c 100644 (file)
@@ -12,7 +12,7 @@ import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog";
 import { COMPUTE_NODE_ATTRIBUTES_DIALOG } from '~/store/compute-nodes/compute-nodes-actions';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { NodeResource, NodeProperties, NodeInfo } from '~/models/node';
-import * as classnames from "classnames";
+import classnames from "classnames";
 
 type CssRules = 'root' | 'grid';
 
diff --git a/src/views-components/current-token-dialog/current-token-dialog.test.tsx b/src/views-components/current-token-dialog/current-token-dialog.test.tsx
new file mode 100644 (file)
index 0000000..188076d
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { shallow, configure } from "enzyme";
+import * as Adapter from "enzyme-adapter-react-16";
+import * as CopyToClipboard from "react-copy-to-clipboard";
+import { CurrentTokenDialogComponent } from "./current-token-dialog";
+
+configure({ adapter: new Adapter() });
+
+describe("<CurrentTokenDialog />", () => {
+  let props;
+  let wrapper;
+
+  beforeEach(() => {
+    props = {
+      classes: {},
+      data: {
+        currentToken: "123123123123",
+      },
+      dispatch: jest.fn(),
+    };
+  });
+
+  describe("copy to clipboard", () => {
+    beforeEach(() => {
+      wrapper = shallow(<CurrentTokenDialogComponent {...props} />);
+    });
+
+    it("should copy API TOKEN to the clipboard", () => {
+      // when
+      wrapper.find(CopyToClipboard).props().onCopy();
+
+      // then
+      expect(props.dispatch).toHaveBeenCalledWith({
+        payload: {
+          hideDuration: 2000,
+          kind: 1,
+          message: "Token copied to clipboard",
+        },
+        type: "OPEN_SNACKBAR",
+      });
+    });
+  });
+});
index bc0071aff9397055706648aa582ffc7b24e991f3..467fc7c8410344fbe7bd55755ad15367f7c11d52 100644 (file)
@@ -4,14 +4,16 @@
 
 import * as React from 'react';
 import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography } from '@material-ui/core';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { withDialog } from '~/store/dialog/with-dialog';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
-import { connect } from 'react-redux';
+import { connect, DispatchProp } from 'react-redux';
 import { CurrentTokenDialogData, getCurrentTokenDialogData, CURRENT_TOKEN_DIALOG_NAME } from '~/store/current-token-dialog/current-token-dialog-actions';
 import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 
-type CssRules = 'link' | 'paper' | 'button';
+type CssRules = 'link' | 'paper' | 'button' | 'copyButton';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     link: {
@@ -28,54 +30,78 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
         fontSize: '0.8125rem',
         fontWeight: 600
+    },
+    copyButton: {
+        boxShadow: 'none',
+        marginTop: theme.spacing.unit * 2,
+        marginBottom: theme.spacing.unit * 2,
     }
 });
 
-type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyles<CssRules>;
+type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyles<CssRules> & DispatchProp;
 
-export const CurrentTokenDialog =
-    withStyles(styles)(
-    connect(getCurrentTokenDialogData)(
-    withDialog(CURRENT_TOKEN_DIALOG_NAME)(
-    class extends React.Component<CurrentTokenProps> {
-        render() {
-            const { classes, open, closeDialog, ...data } = this.props;
-            return <Dialog
-                open={open}
-                onClose={closeDialog}
-                fullWidth={true}
-                maxWidth='md'>
-                <DialogTitle>Current Token</DialogTitle>
-                <DialogContent>
-                    <Typography  paragraph={true}>
-                        The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
-                                <Typography component='p'>
-                            For more information see
-                                    <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>
-                                Getting an API token.
-                                    </a>
-                        </Typography>
+export class CurrentTokenDialogComponent extends React.Component<CurrentTokenProps> {
+    onCopy = (message: string) => {
+        this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+            message,
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS
+        }));
+    }
+
+    getSnippet = ({ apiHost, currentToken }: CurrentTokenDialogData) =>
+        `HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
+    export ARVADOS_API_TOKEN=${currentToken}
+    export ARVADOS_API_HOST=${apiHost}
+    unset ARVADOS_API_HOST_INSECURE`;
+
+    render() {
+        const { classes, open, closeDialog, ...data } = this.props;
+        return <Dialog
+            open={open}
+            onClose={closeDialog}
+            fullWidth={true}
+            maxWidth='md'>
+            <DialogTitle>Current Token</DialogTitle>
+            <DialogContent>
+                <Typography paragraph={true}>
+                    The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
+                            <Typography component='p'>
+                        For more information see
+                                <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>
+                            Getting an API token.
+                                </a>
                     </Typography>
-                    <Typography  paragraph={true}>
-                        Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
-                            </Typography>
-                    <DefaultCodeSnippet lines={[getSnippet(data)]} />
-                    <Typography >
-                        Arvados
-                                <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
-                        do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
-                            </Typography>
-                </DialogContent>
-                <DialogActions>
-                    <Button onClick={closeDialog} className={classes.button} color="primary">CLOSE</Button>
-                </DialogActions>
-            </Dialog>;
-        }
+                </Typography>
+                <Typography paragraph={true}>
+                    Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
+                        </Typography>
+                <DefaultCodeSnippet lines={[this.getSnippet(data)]} />
+                <CopyToClipboard text={data.currentToken} onCopy={() => this.onCopy('Token copied to clipboard')}>
+                    <Button
+                        color="primary"
+                        size="small"
+                        variant="contained"
+                        className={classes.copyButton}
+                    >
+                        Copy Api Token
+                    </Button>
+                </CopyToClipboard>
+                <Typography >
+                    Arvados
+                            <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
+                    do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
+                        </Typography>
+            </DialogContent>
+            <DialogActions>
+                <Button onClick={closeDialog} className={classes.button} color="primary">CLOSE</Button>
+            </DialogActions>
+        </Dialog>;
     }
-)));
+}
+
+export const CurrentTokenDialog =
+    withStyles(styles)(
+        connect(getCurrentTokenDialogData)(
+            withDialog(CURRENT_TOKEN_DIALOG_NAME)(CurrentTokenDialogComponent)));
 
-const getSnippet = ({ apiHost, currentToken }: CurrentTokenDialogData) =>
-`HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
-export ARVADOS_API_TOKEN=${currentToken}
-export ARVADOS_API_HOST=${apiHost}
-unset ARVADOS_API_HOST_INSECURE`;
index 8244e15f06473f1d472177708830909077db20c8..b6b0cdf10b8078c099f981faa2ae2a22ea9bdfec 100644 (file)
@@ -7,7 +7,7 @@ import { IconButton, Tabs, Tab, Typography, Grid, Tooltip } from '@material-ui/c
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { Transition } from 'react-transition-group';
 import { ArvadosTheme } from '~/common/custom-theme';
-import * as classnames from "classnames";
+import classnames from "classnames";
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
 import { CloseIcon } from '~/components/icon/icon';