From: Janicki Artur Date: Sat, 24 Nov 2018 21:24:54 +0000 (+0100) Subject: add table view, actions and dialogs X-Git-Tag: 1.3.0~15^2~1 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/3a3de86b86ef60fc86f1190d42bc8a2471ab5276 add table view, actions and dialogs Feature #14528_table_view_and_actions Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index b46195de..a0f58be4 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -50,6 +50,7 @@ import SettingsEthernet from '@material-ui/icons/SettingsEthernet'; import Star from '@material-ui/icons/Star'; import StarBorder from '@material-ui/icons/StarBorder'; import Warning from '@material-ui/icons/Warning'; +import VpnKey from '@material-ui/icons/VpnKey'; export type IconType = React.SFC<{ className?: string, style?: object }>; @@ -74,6 +75,7 @@ export const HelpIcon: IconType = (props) => ; export const HelpOutlineIcon: IconType = (props) => ; export const ImportContactsIcon: IconType = (props) => ; export const InputIcon: IconType = (props) => ; +export const KeyIcon: IconType = (props) => ; export const LogIcon: IconType = (props) => ; export const MailIcon: IconType = (props) => ; export const MoreOptionsIcon: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index 922720a4..88fd2298 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -49,6 +49,7 @@ import { DragDropContextProvider } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions'; import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set'; +import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh-key-action-set'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -66,6 +67,7 @@ addMenuActionSet(ContextMenuKind.PROCESS, processActionSet); addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet); addMenuActionSet(ContextMenuKind.TRASH, trashActionSet); addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet); +addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet); fetchConfig() .then(({ config, apiHost }) => { diff --git a/src/models/resource.ts b/src/models/resource.ts index 520520f7..5fa61797 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -29,6 +29,7 @@ export enum ResourceKind { PROCESS = "arvados#containerRequest", PROJECT = "arvados#group", REPOSITORY = "arvados#repository", + SSH_KEY = "arvados#authorizedKeys", USER = "arvados#user", WORKFLOW = "arvados#workflow", NONE = "arvados#none" diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts index 3658c589..28559b1a 100644 --- a/src/store/auth/auth-action.ts +++ b/src/store/auth/auth-action.ts @@ -4,7 +4,7 @@ import { ofType, unionize, UnionOf } from '~/common/unionize'; import { Dispatch } from "redux"; -import { reset, stopSubmit } from 'redux-form'; +import { reset, stopSubmit, startSubmit } from 'redux-form'; import { AxiosInstance } from "axios"; import { RootState } from "../store"; import { snackbarActions } from '~/store/snackbar/snackbar-actions'; @@ -23,10 +23,14 @@ export const authActions = unionize({ USER_DETAILS_REQUEST: {}, USER_DETAILS_SUCCESS: ofType(), SET_SSH_KEYS: ofType(), - ADD_SSH_KEY: ofType() + ADD_SSH_KEY: ofType(), + REMOVE_SSH_KEY: ofType() }); export const SSH_KEY_CREATE_FORM_NAME = 'sshKeyCreateFormName'; +export const SSH_KEY_PUBLIC_KEY_DIALOG = 'sshKeyPublicKeyDialog'; +export const SSH_KEY_REMOVE_DIALOG = 'sshKeyRemoveDialog'; +export const SSH_KEY_ATTRIBUTES_DIALOG = 'sshKeyAttributesDialog'; export interface SshKeyCreateFormDialogData { publicKey: string; @@ -87,20 +91,51 @@ export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootSta export const openSshKeyCreateDialog = () => dialogActions.OPEN_DIALOG({ id: SSH_KEY_CREATE_FORM_NAME, data: {} }); +export const openPublicKeyDialog = (name: string, publicKey: string) => + dialogActions.OPEN_DIALOG({ id: SSH_KEY_PUBLIC_KEY_DIALOG, data: { name, publicKey } }); + +export const openSshKeyAttributesDialog = (index: number) => + (dispatch: Dispatch, getState: () => RootState) => { + const sshKey = getState().auth.sshKeys[index]; + dispatch(dialogActions.OPEN_DIALOG({ id: SSH_KEY_ATTRIBUTES_DIALOG, data: { sshKey } })); + }; + +export const openSshKeyRemoveDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: SSH_KEY_REMOVE_DIALOG, + data: { + title: 'Remove public key', + text: 'Are you sure you want to remove this public key?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const removeSshKey = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); + await services.authorizedKeysService.delete(uuid); + dispatch(authActions.REMOVE_SSH_KEY(uuid)); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Public Key has been successfully removed.', hideDuration: 2000 })); + }; + export const createSshKey = (data: SshKeyCreateFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const userUuid = getState().auth.user!.uuid; + const { name, publicKey } = data; + dispatch(startSubmit(SSH_KEY_CREATE_FORM_NAME)); try { - const userUuid = getState().auth.user!.uuid; - const { name, publicKey } = data; const newSshKey = await services.authorizedKeysService.create({ - name, + name, publicKey, keyType: KeyType.SSH, authorizedUserUuid: userUuid }); + dispatch(authActions.ADD_SSH_KEY(newSshKey)); dispatch(dialogActions.CLOSE_DIALOG({ id: SSH_KEY_CREATE_FORM_NAME })); dispatch(reset(SSH_KEY_CREATE_FORM_NAME)); - dispatch(authActions.ADD_SSH_KEY(newSshKey)); dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Public key has been successfully created.", hideDuration: 2000 diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts index 8f234dad..a8e4340a 100644 --- a/src/store/auth/auth-reducer.ts +++ b/src/store/auth/auth-reducer.ts @@ -10,7 +10,7 @@ import { SshKeyResource } from '~/models/ssh-key'; export interface AuthState { user?: User; apiToken?: string; - sshKeys?: SshKeyResource[]; + sshKeys: SshKeyResource[]; } const initialState: AuthState = { @@ -19,13 +19,13 @@ const initialState: AuthState = { sshKeys: [] }; -export const authReducer = (services: ServiceRepository) => (state: AuthState = initialState, action: AuthAction) => { +export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => { return authActions.match(action, { SAVE_API_TOKEN: (token: string) => { return {...state, apiToken: token}; }, INIT: ({ user, token }) => { - return { user, apiToken: token }; + return { ...state, user, apiToken: token }; }, LOGIN: () => { return state; @@ -40,7 +40,10 @@ export const authReducer = (services: ServiceRepository) => (state: AuthState = return {...state, sshKeys}; }, ADD_SSH_KEY: (sshKey: SshKeyResource) => { - return { ...state, sshKeys: state.sshKeys!.concat(sshKey) }; + return { ...state, sshKeys: state.sshKeys.concat(sshKey) }; + }, + REMOVE_SSH_KEY: (uuid: string) => { + return { ...state, sshKeys: state.sshKeys.filter((sshKey) => sshKey.uuid !== uuid )}; }, default: () => state }); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 596ac87b..0a6b5a82 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -14,6 +14,7 @@ import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree import { extractUuidKind, ResourceKind } from '~/models/resource'; import { Process } from '~/store/processes/process'; import { RepositoryResource } from '~/models/repositories'; +import { SshKeyResource } from '~/models/ssh-key'; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), @@ -73,6 +74,18 @@ export const openRepositoryContextMenu = (event: React.MouseEvent, })); }; +export const openSshKeyContextMenu = (event: React.MouseEvent, index: number, sshKey: SshKeyResource) => + (dispatch: Dispatch) => { + dispatch(openContextMenu(event, { + name: '', + uuid: sshKey.uuid, + ownerUuid: sshKey.ownerUuid, + kind: ResourceKind.SSH_KEY, + menuKind: ContextMenuKind.SSH_KEY, + index + })); + }; + export const openRootProjectContextMenu = (event: React.MouseEvent, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => { const res = getResource(projectUuid)(getState().resources); diff --git a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts new file mode 100644 index 00000000..3fa2f16f --- /dev/null +++ b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts @@ -0,0 +1,27 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set"; +import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon"; +import { openSshKeyRemoveDialog, openSshKeyAttributesDialog } from '~/store/auth/auth-action'; + +export const sshKeyActionSet: ContextMenuActionSet = [[{ + name: "Attributes", + icon: AttributesIcon, + execute: (dispatch, { index }) => { + dispatch(openSshKeyAttributesDialog(index!)); + } +}, { + name: "Advanced", + icon: AdvancedIcon, + execute: (dispatch, { uuid, index }) => { + // ToDo + } +}, { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, { uuid }) => { + dispatch(openSshKeyRemoveDialog(uuid)); + } +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index 30ecc981..af5aaa92 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -69,5 +69,6 @@ export enum ContextMenuKind { PROCESS = "Process", PROCESS_RESOURCE = 'ProcessResource', PROCESS_LOGS = "ProcessLogs", - REPOSITORY = "Repository" + REPOSITORY = "Repository", + SSH_KEY = "SshKey" } diff --git a/src/views-components/ssh-keys-dialog/attributes-dialog.tsx b/src/views-components/ssh-keys-dialog/attributes-dialog.tsx new file mode 100644 index 00000000..0a2a6814 --- /dev/null +++ b/src/views-components/ssh-keys-dialog/attributes-dialog.tsx @@ -0,0 +1,65 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { compose } from 'redux'; +import { withStyles, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, WithStyles, Typography, Grid } from '@material-ui/core'; +import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog"; +import { SSH_KEY_ATTRIBUTES_DIALOG } from '~/store/auth/auth-action'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { SshKeyResource } from "~/models/ssh-key"; + +type CssRules = 'root'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + fontSize: '0.875rem', + '& div:nth-child(odd)': { + textAlign: 'right', + color: theme.palette.grey["500"] + } + } +}); + +interface AttributesSshKeyDialogDataProps { + sshKey: SshKeyResource; +} + +export const AttributesSshKeyDialog = compose( + withDialog(SSH_KEY_ATTRIBUTES_DIALOG), + withStyles(styles))( + ({ open, closeDialog, data, classes }: WithDialogProps & WithStyles) => + + Attributes + + {data.sshKey && + Name + {data.sshKey.name} + Owner uuid + {data.sshKey.ownerUuid} + Created at + {data.sshKey.createdAt} + Modified at + {data.sshKey.modifiedAt} + Modified by user uuid + {data.sshKey.modifiedByUserUuid} + Modified by client uuid + {data.sshKey.modifiedByClientUuid} + uuid + {data.sshKey.uuid} + } + + + + + + ); \ No newline at end of file diff --git a/src/views-components/ssh-keys-dialog/public-key-dialog.tsx b/src/views-components/ssh-keys-dialog/public-key-dialog.tsx new file mode 100644 index 00000000..77c6cfde --- /dev/null +++ b/src/views-components/ssh-keys-dialog/public-key-dialog.tsx @@ -0,0 +1,55 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { compose } from 'redux'; +import { withStyles, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, WithStyles } from '@material-ui/core'; +import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog"; +import { SSH_KEY_PUBLIC_KEY_DIALOG } from '~/store/auth/auth-action'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet'; + +type CssRules = 'codeSnippet'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + codeSnippet: { + borderRadius: theme.spacing.unit * 0.5, + border: '1px solid', + borderColor: theme.palette.grey["400"], + '& pre': { + wordWrap: 'break-word', + whiteSpace: 'pre-wrap' + } + }, +}); + +interface PublicKeyDialogDataProps { + name: string; + publicKey: string; +} + +export const PublicKeyDialog = compose( + withDialog(SSH_KEY_PUBLIC_KEY_DIALOG), + withStyles(styles))( + ({ open, closeDialog, data, classes }: WithDialogProps & WithStyles) => + + {data.name} - SSH Key + + {data && data.publicKey && } + + + + + + ); \ No newline at end of file diff --git a/src/views-components/ssh-keys-dialog/remove-dialog.tsx b/src/views-components/ssh-keys-dialog/remove-dialog.tsx new file mode 100644 index 00000000..8077f21b --- /dev/null +++ b/src/views-components/ssh-keys-dialog/remove-dialog.tsx @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 +import { Dispatch, compose } from 'redux'; +import { connect } from "react-redux"; +import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog"; +import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog"; +import { SSH_KEY_REMOVE_DIALOG, removeSshKey } from '~/store/auth/auth-action'; + +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeSshKey(props.data.uuid)); + } +}); + +export const RemoveSshKeyDialog = compose( + withDialog(SSH_KEY_REMOVE_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); \ No newline at end of file diff --git a/src/views/ssh-key-panel/ssh-key-panel-root.tsx b/src/views/ssh-key-panel/ssh-key-panel-root.tsx index f752228f..869662dd 100644 --- a/src/views/ssh-key-panel/ssh-key-panel-root.tsx +++ b/src/views/ssh-key-panel/ssh-key-panel-root.tsx @@ -3,53 +3,114 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography } from '@material-ui/core'; +import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography, Grid, Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton } from '@material-ui/core'; import { ArvadosTheme } from '~/common/custom-theme'; import { SshKeyResource } from '~/models/ssh-key'; +import { AddIcon, MoreOptionsIcon, KeyIcon } from '~/components/icon/icon'; - -type CssRules = 'root' | 'link'; +type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' | 'keyIcon'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { - width: '100%' + width: '100%', + overflow: 'auto' }, link: { color: theme.palette.primary.main, textDecoration: 'none', margin: '0px 4px' + }, + buttonContainer: { + textAlign: 'right' + }, + table: { + marginTop: theme.spacing.unit + }, + tableRow: { + '& td, th': { + whiteSpace: 'nowrap' + } + }, + keyIcon: { + color: theme.palette.primary.main } }); export interface SshKeyPanelRootActionProps { - onClick: () => void; + openSshKeyCreateDialog: () => void; + openRowOptions: (event: React.MouseEvent, index: number, sshKey: SshKeyResource) => void; + openPublicKeyDialog: (name: string, publicKey: string) => void; } export interface SshKeyPanelRootDataProps { - sshKeys?: SshKeyResource[]; + sshKeys: SshKeyResource[]; + hasKeys: boolean; } type SshKeyPanelRootProps = SshKeyPanelRootDataProps & SshKeyPanelRootActionProps & WithStyles; export const SshKeyPanelRoot = withStyles(styles)( - ({ classes, sshKeys, onClick }: SshKeyPanelRootProps) => + ({ classes, sshKeys, openSshKeyCreateDialog, openPublicKeyDialog, hasKeys, openRowOptions }: SshKeyPanelRootProps) => - - You have not yet set up an SSH public key for use with Arvados. - - Learn more. - - - - When you have an SSH key you would like to use, add it using button below. - - + + + { !hasKeys && + You have not yet set up an SSH public key for use with Arvados. + + Learn more. + + } + { !hasKeys && + When you have an SSH key you would like to use, add it using button below. + } + + + + + + + {hasKeys && + + + Name + UUID + Authorized user + Expires at + Key type + Public Key + + + + + {sshKeys.map((sshKey, index) => + + {sshKey.name} + {sshKey.uuid} + {sshKey.authorizedUserUuid} + {sshKey.expiresAt || '(none)'} + {sshKey.keyType} + + + openPublicKeyDialog(sshKey.name, sshKey.publicKey)}> + + + + + + + openRowOptions(event, index, sshKey)}> + + + + + )} + +
} +
); \ No newline at end of file diff --git a/src/views/ssh-key-panel/ssh-key-panel.tsx b/src/views/ssh-key-panel/ssh-key-panel.tsx index f600677d..c7e3516e 100644 --- a/src/views/ssh-key-panel/ssh-key-panel.tsx +++ b/src/views/ssh-key-panel/ssh-key-panel.tsx @@ -5,18 +5,26 @@ import { RootState } from '~/store/store'; import { Dispatch } from 'redux'; import { connect } from 'react-redux'; +import { openSshKeyCreateDialog, openPublicKeyDialog } from '~/store/auth/auth-action'; +import { openSshKeyContextMenu } from '~/store/context-menu/context-menu-actions'; import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps } from '~/views/ssh-key-panel/ssh-key-panel-root'; -import { openSshKeyCreateDialog } from '~/store/auth/auth-action'; const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => { return { - sshKeys: state.auth.sshKeys + sshKeys: state.auth.sshKeys, + hasKeys: state.auth.sshKeys!.length > 0 }; }; const mapDispatchToProps = (dispatch: Dispatch): SshKeyPanelRootActionProps => ({ - onClick: () => { - dispatch(openSshKeyCreateDialog()); + openSshKeyCreateDialog: () => { + dispatch(openSshKeyCreateDialog()); + }, + openRowOptions: (event, index, sshKey) => { + dispatch(openSshKeyContextMenu(event, index, sshKey)); + }, + openPublicKeyDialog: (name: string, publicKey: string) => { + dispatch(openPublicKeyDialog(name, publicKey)); } }); diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index ebdf57c3..84c8e24c 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -55,6 +55,9 @@ import { RepositoryAttributesDialog } from '~/views-components/repository-attrib import { CreateRepositoryDialog } from '~/views-components/dialog-forms/create-repository-dialog'; import { RemoveRepositoryDialog } from '~/views-components/repository-remove-dialog/repository-remove-dialog'; import { CreateSshKeyDialog } from '~/views-components/dialog-forms/create-ssh-key-dialog'; +import { PublicKeyDialog } from '~/views-components/ssh-keys-dialog/public-key-dialog'; +import { RemoveSshKeyDialog } from '~/views-components/ssh-keys-dialog/remove-dialog'; +import { AttributesSshKeyDialog } from '~/views-components/ssh-keys-dialog/attributes-dialog'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -135,6 +138,7 @@ export const WorkbenchPanel = + @@ -150,12 +154,14 @@ export const WorkbenchPanel = + +