From 93cb6b20b9f7893c588d9679c7b904ee35238f1b Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Fri, 30 Nov 2018 11:19:36 +0100 Subject: [PATCH] context-menu-for-VMs Feature #14498 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- src/index.tsx | 2 + src/models/virtual-machines.ts | 7 +- src/services/auth-service/auth-service.ts | 3 +- src/store/advanced-tab/advanced-tab.ts | 31 ++++- .../context-menu/context-menu-actions.ts | 29 +++-- .../repositories/repositories-actions.ts | 2 +- .../virtual-machines-actions.ts | 40 ++++++- .../virtual-machines-reducer.ts | 9 +- .../action-sets/virtual-machine-action-set.ts | 28 +++++ .../context-menu/context-menu.tsx | 3 +- .../repository-remove-dialog.ts | 5 +- .../attributes-dialog.tsx | 89 +++++++++++++++ .../virtual-machines-dialog/remove-dialog.tsx | 21 ++++ .../virtual-machine-panel.tsx | 106 +++++++++++------- src/views/workbench/workbench.tsx | 4 + 15 files changed, 313 insertions(+), 66 deletions(-) create mode 100644 src/views-components/context-menu/action-sets/virtual-machine-action-set.ts create mode 100644 src/views-components/virtual-machines-dialog/attributes-dialog.tsx create mode 100644 src/views-components/virtual-machines-dialog/remove-dialog.tsx diff --git a/src/index.tsx b/src/index.tsx index 801a56a1d3..3ff8088cc4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -51,6 +51,7 @@ import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actio 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'; import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions'; +import { virtualMachineActionSet } from '~/views-components/context-menu/action-sets/virtual-machine-action-set'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -69,6 +70,7 @@ addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet); addMenuActionSet(ContextMenuKind.TRASH, trashActionSet); addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet); addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet); +addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet); fetchConfig() .then(({ config, apiHost }) => { diff --git a/src/models/virtual-machines.ts b/src/models/virtual-machines.ts index 0652c35017..acc084a3ea 100644 --- a/src/models/virtual-machines.ts +++ b/src/models/virtual-machines.ts @@ -8,11 +8,16 @@ export interface VirtualMachinesResource extends Resource { hostname: string; } -export interface VirtualMachinesLoginsResource { +export interface VirtualMachinesLoginsItems { hostname: string; username: string; public_key: string; user_uuid: string; virtual_machine_uuid: string; authorized_key_uuid: string; +} + +export interface VirtualMachineLogins { + kind: string; + items: VirtualMachinesLoginsItems[]; } \ No newline at end of file diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index 2c0c83d47d..b512fb24c3 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -61,8 +61,7 @@ export class AuthService { const lastName = localStorage.getItem(USER_LAST_NAME_KEY); const uuid = this.getUuid(); const ownerUuid = this.getOwnerUuid(); - const isAdmin = this.getIsAdmin(); - console.log(isAdmin); + const isAdmin = this.getIsAdmin(); return email && firstName && lastName && uuid && ownerUuid ? { email, firstName, lastName, uuid, ownerUuid, isAdmin } diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts index b3c5164c55..2b7e883da9 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -16,6 +16,7 @@ import { ServiceRepository } from '~/services/services'; import { FilterBuilder } from '~/services/api/filter-builder'; import { RepositoryResource } from '~/models/repositories'; import { SshKeyResource } from '~/models/ssh-key'; +import { VirtualMachinesResource } from '~/models/virtual-machines'; export const ADVANCED_TAB_DIALOG = 'advancedTabDialog'; @@ -54,12 +55,17 @@ enum RepositoryData { } enum SshKeyData { - SSH_KEY = 'authorized_keys', + SSH_KEY = 'authorized_key', CREATED_AT = 'created_at' } -type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData; -type AdvanceResourcePrefix = GroupContentsResourcePrefix | 'repositories' | 'authorized_keys'; +enum VirtualMachineData { + VIRTUAL_MACHINE = 'virtual_machine', + CREATED_AT = 'created_at' +} + +type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData; +type AdvanceResourcePrefix = GroupContentsResourcePrefix | 'repositories' | 'authorized_keys' | 'virtual_machines'; export const openAdvancedTabDialog = (uuid: string, index?: number) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { @@ -90,6 +96,11 @@ export const openAdvancedTabDialog = (uuid: string, index?: number) => const advanceDataSshKey: AdvancedTabDialogData = advancedTabData(uuid, '', '', sshKeyApiResponse, dataSshKey, SshKeyData.SSH_KEY, 'authorized_keys', SshKeyData.CREATED_AT, dataSshKey.createdAt); dispatch(initAdvancedTabDialog(advanceDataSshKey)); break; + case ResourceKind.VIRTUAL_MACHINE: + const dataVirtualMachine = getState().virtualMachines.virtualMachines.items[index!]; + const advanceDataVirtualMachine: AdvancedTabDialogData = advancedTabData(uuid, '', '', virtualMachineApiResponse, dataVirtualMachine, VirtualMachineData.VIRTUAL_MACHINE, 'virtual_machines', VirtualMachineData.CREATED_AT, dataVirtualMachine.createdAt); + dispatch(initAdvancedTabDialog(advanceDataVirtualMachine)); + break; default: dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } @@ -110,7 +121,7 @@ const getDataForAdvancedTab = (uuid: string) => const initAdvancedTabDialog = (data: AdvancedTabDialogData) => dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data }); -const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: AdvanceResourceKind, +const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: AdvanceResourceKind, resourcePrefix: AdvanceResourcePrefix, resourceKindProperty: AdvanceResourceKind, property: any) => { return { uuid, @@ -293,4 +304,16 @@ const sshKeyApiResponse = (apiResponse: SshKeyResource) => { "created_at": "${createdAt}", "expires_at": "${expiresAt}"`; return response; +}; + +const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource) => { + const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = apiResponse; + const response = `"uuid": "${uuid}", +"owner_uuid": "${ownerUuid}", +"modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, +"modified_by_user_uuid": ${stringify(modifiedByUserUuid)}, +"modified_at": ${stringify(modifiedAt)}, +"hostname": ${stringify(hostname)}, +"created_at": "${createdAt}"`; + return response; }; \ No newline at end of file diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 5631a5e85c..0dbae18b99 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -15,6 +15,7 @@ import { extractUuidKind, ResourceKind } from '~/models/resource'; import { Process } from '~/store/processes/process'; import { RepositoryResource } from '~/models/repositories'; import { SshKeyResource } from '~/models/ssh-key'; +import { VirtualMachinesResource } from '~/models/virtual-machines'; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), @@ -64,14 +65,26 @@ export const openCollectionFilesContextMenu = (event: React.MouseEvent, index: number, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => { - dispatch(openContextMenu(event, { - name: '', - uuid: repository.uuid, - ownerUuid: repository.ownerUuid, - kind: ResourceKind.REPOSITORY, - menuKind: ContextMenuKind.REPOSITORY, - index - })); + dispatch(openContextMenu(event, { + name: '', + uuid: repository.uuid, + ownerUuid: repository.ownerUuid, + kind: ResourceKind.REPOSITORY, + menuKind: ContextMenuKind.REPOSITORY, + index + })); + }; + +export const openVirtualMachinesContextMenu = (event: React.MouseEvent, index: number, repository: VirtualMachinesResource) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(openContextMenu(event, { + name: '', + uuid: repository.uuid, + ownerUuid: repository.ownerUuid, + kind: ResourceKind.VIRTUAL_MACHINE, + menuKind: ContextMenuKind.VIRTUAL_MACHINE, + index + })); }; export const openSshKeyContextMenu = (event: React.MouseEvent, index: number, sshKey: SshKeyResource) => diff --git a/src/store/repositories/repositories-actions.ts b/src/store/repositories/repositories-actions.ts index a672738fd2..d617289993 100644 --- a/src/store/repositories/repositories-actions.ts +++ b/src/store/repositories/repositories-actions.ts @@ -84,7 +84,7 @@ export const removeRepository = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); await services.repositoriesService.delete(uuid); - dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 })); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS })); dispatch(loadRepositoriesData()); }; diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts index 6f59c9109e..bb942bb6dc 100644 --- a/src/store/virtual-machines/virtual-machines-actions.ts +++ b/src/store/virtual-machines/virtual-machines-actions.ts @@ -9,26 +9,36 @@ import { navigateToVirtualMachines } from "../navigation/navigation-action"; import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action'; import { formatDate } from "~/common/formatters"; import { unionize, ofType, UnionOf } from "~/common/unionize"; -import { VirtualMachinesLoginsResource } from '~/models/virtual-machines'; +import { VirtualMachineLogins } from '~/models/virtual-machines'; import { FilterBuilder } from "~/services/api/filter-builder"; import { ListResults } from "~/services/common-service/common-resource-service"; +import { dialogActions } from '~/store/dialog/dialog-actions'; +import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; export const virtualMachinesActions = unionize({ SET_REQUESTED_DATE: ofType(), SET_VIRTUAL_MACHINES: ofType>(), - SET_LOGINS: ofType(), + SET_LOGINS: ofType(), SET_LINKS: ofType>() }); export type VirtualMachineActions = UnionOf; export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel'; +export const VIRTUAL_MACHINE_ATTRIBUTES_DIALOG = 'virtualMachineAttributesDialog'; +export const VIRTUAL_MACHINE_REMOVE_DIALOG = 'virtualMachineRemoveDialog'; export const openVirtualMachines = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(navigateToVirtualMachines); }; +export const openVirtualMachineAttributes = (index: number) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const virtualMachineData = getState().virtualMachines.virtualMachines.items[index]; + dispatch(dialogActions.OPEN_DIALOG({ id: VIRTUAL_MACHINE_ATTRIBUTES_DIALOG, data: { virtualMachineData } })); + }; + const loadRequestedDate = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const date = services.virtualMachineService.getRequestedDate(); @@ -47,9 +57,8 @@ export const loadVirtualMachinesData = () => }); dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines)); dispatch(virtualMachinesActions.SET_LINKS(links)); - const getAllLogins = await services.virtualMachineService.getAllLogins(); - console.log(getAllLogins); - dispatch(virtualMachinesActions.SET_LOGINS(getAllLogins)); + const logins = await services.virtualMachineService.logins(virtualMachinesUuids[0]); + dispatch(virtualMachinesActions.SET_LOGINS(logins)); }; export const saveRequestedDate = () => @@ -59,6 +68,27 @@ export const saveRequestedDate = () => dispatch(loadRequestedDate()); }; +export const openRemoveVirtualMachineDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: VIRTUAL_MACHINE_REMOVE_DIALOG, + data: { + title: 'Remove virtual machine', + text: 'Are you sure you want to remove this virtual machine?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const removeVirtualMachine = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); + await services.virtualMachineService.delete(uuid); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS })); + dispatch(loadVirtualMachinesData()); + }; + const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL); export const loadVirtualMachinesPanel = () => diff --git a/src/store/virtual-machines/virtual-machines-reducer.ts b/src/store/virtual-machines/virtual-machines-reducer.ts index fa28417efe..475ad75238 100644 --- a/src/store/virtual-machines/virtual-machines-reducer.ts +++ b/src/store/virtual-machines/virtual-machines-reducer.ts @@ -4,12 +4,12 @@ import { virtualMachinesActions, VirtualMachineActions } from '~/store/virtual-machines/virtual-machines-actions'; import { ListResults } from '~/services/common-service/common-resource-service'; -import { VirtualMachinesLoginsResource } from '~/models/virtual-machines'; +import { VirtualMachineLogins } from '~/models/virtual-machines'; interface VirtualMachines { date: string; virtualMachines: ListResults; - logins: VirtualMachinesLoginsResource[]; + logins: VirtualMachineLogins; links: ListResults; } @@ -22,7 +22,10 @@ const initialState: VirtualMachines = { itemsAvailable: 0, items: [] }, - logins: [], + logins: { + kind: '', + items: [] + }, links: { kind: '', offset: 0, diff --git a/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts b/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts new file mode 100644 index 0000000000..bb04b8c6f7 --- /dev/null +++ b/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts @@ -0,0 +1,28 @@ +// 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 { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab'; +import { openVirtualMachineAttributes, openRemoveVirtualMachineDialog } from "~/store/virtual-machines/virtual-machines-actions"; + +export const virtualMachineActionSet: ContextMenuActionSet = [[{ + name: "Attributes", + icon: AttributesIcon, + execute: (dispatch, { index }) => { + dispatch(openVirtualMachineAttributes(index!)); + } +}, { + name: "Advanced", + icon: AdvancedIcon, + execute: (dispatch, { uuid, index }) => { + dispatch(openAdvancedTabDialog(uuid, index)); + } +}, { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, { uuid }) => { + dispatch(openRemoveVirtualMachineDialog(uuid)); + } +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index af5aaa929c..d08798f773 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -70,5 +70,6 @@ export enum ContextMenuKind { PROCESS_RESOURCE = 'ProcessResource', PROCESS_LOGS = "ProcessLogs", REPOSITORY = "Repository", - SSH_KEY = "SshKey" + SSH_KEY = "SshKey", + VIRTUAL_MACHINE = "VirtualMachine" } diff --git a/src/views-components/repository-remove-dialog/repository-remove-dialog.ts b/src/views-components/repository-remove-dialog/repository-remove-dialog.ts index 148e78bdf3..ca51c8495f 100644 --- a/src/views-components/repository-remove-dialog/repository-remove-dialog.ts +++ b/src/views-components/repository-remove-dialog/repository-remove-dialog.ts @@ -1,20 +1,21 @@ // 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 { removeRepository, REPOSITORY_REMOVE_DIALOG } from '~/store/repositories/repositories-actions'; - const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ onConfirm: () => { props.closeDialog(); dispatch(removeRepository(props.data.uuid)); } }); - export const RemoveRepositoryDialog = compose( +export const RemoveRepositoryDialog = compose( withDialog(REPOSITORY_REMOVE_DIALOG), connect(null, mapDispatchToProps) )(ConfirmationDialog); \ No newline at end of file diff --git a/src/views-components/virtual-machines-dialog/attributes-dialog.tsx b/src/views-components/virtual-machines-dialog/attributes-dialog.tsx new file mode 100644 index 0000000000..78b58da5cf --- /dev/null +++ b/src/views-components/virtual-machines-dialog/attributes-dialog.tsx @@ -0,0 +1,89 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Grid } from "@material-ui/core"; +import { WithDialogProps } from "~/store/dialog/with-dialog"; +import { withDialog } from '~/store/dialog/with-dialog'; +import { VIRTUAL_MACHINE_ATTRIBUTES_DIALOG } from "~/store/virtual-machines/virtual-machines-actions"; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { compose } from "redux"; +import { VirtualMachinesResource } from "~/models/virtual-machines"; + +type CssRules = 'rightContainer' | 'leftContainer' | 'spacing'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + rightContainer: { + textAlign: 'right', + paddingRight: theme.spacing.unit * 2, + color: theme.palette.grey["500"] + }, + leftContainer: { + textAlign: 'left', + paddingLeft: theme.spacing.unit * 2 + }, + spacing: { + paddingTop: theme.spacing.unit * 2 + }, +}); + +interface VirtualMachineAttributesDataProps { + virtualMachineData: VirtualMachinesResource; +} + +type VirtualMachineAttributesProps = VirtualMachineAttributesDataProps & WithStyles; + +export const VirtualMachineAttributesDialog = compose( + withDialog(VIRTUAL_MACHINE_ATTRIBUTES_DIALOG), + withStyles(styles))( + (props: WithDialogProps & VirtualMachineAttributesProps) => + + Attributes + + + {props.data.virtualMachineData && attributes(props.data.virtualMachineData, props.classes)} + + + + + + + ); + +const attributes = (virtualMachine: VirtualMachinesResource, classes: any) => { + const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = virtualMachine; + return ( + + + + Hostname + Owner uuid + Created at + Modified at + Modified by user uuid + Modified by client uuid + uuid + + + {hostname} + {ownerUuid} + {createdAt} + {modifiedAt} + {modifiedByUserUuid} + {modifiedByClientUuid} + {uuid} + + + + ); +}; diff --git a/src/views-components/virtual-machines-dialog/remove-dialog.tsx b/src/views-components/virtual-machines-dialog/remove-dialog.tsx new file mode 100644 index 0000000000..11ab9c43a1 --- /dev/null +++ b/src/views-components/virtual-machines-dialog/remove-dialog.tsx @@ -0,0 +1,21 @@ +// 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 { VIRTUAL_MACHINE_REMOVE_DIALOG, removeVirtualMachine } from '~/store/virtual-machines/virtual-machines-actions'; + +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeVirtualMachine(props.data.uuid)); + } +}); + +export const RemoveVirtualMachineDialog = compose( + withDialog(VIRTUAL_MACHINE_REMOVE_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); \ No newline at end of file diff --git a/src/views/virtual-machine-panel/virtual-machine-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-panel.tsx index 504910e0f7..6c8955448c 100644 --- a/src/views/virtual-machine-panel/virtual-machine-panel.tsx +++ b/src/views/virtual-machine-panel/virtual-machine-panel.tsx @@ -4,20 +4,21 @@ import * as React from 'react'; import { connect } from 'react-redux'; -import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip } from '@material-ui/core'; +import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip, IconButton } from '@material-ui/core'; import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; import { ArvadosTheme } from '~/common/custom-theme'; import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet'; import { Link } from 'react-router-dom'; -import { compose } from 'redux'; +import { compose, Dispatch } from 'redux'; import { saveRequestedDate, loadVirtualMachinesData } from '~/store/virtual-machines/virtual-machines-actions'; import { RootState } from '~/store/store'; import { ListResults } from '~/services/common-service/common-resource-service'; -import { HelpIcon } from '~/components/icon/icon'; -import { VirtualMachinesLoginsResource, VirtualMachinesResource } from '~/models/virtual-machines'; +import { HelpIcon, MoreOptionsIcon, AddIcon } from '~/components/icon/icon'; +import { VirtualMachineLogins, VirtualMachinesResource } from '~/models/virtual-machines'; import { Routes } from '~/routes/routes'; +import { openVirtualMachinesContextMenu } from '~/store/context-menu/context-menu-actions'; -type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon'; +type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon' | 'moreOptionsButton' | 'moreOptions'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ button: { @@ -55,26 +56,39 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ icon: { textAlign: "right", marginTop: theme.spacing.unit - } + }, + moreOptionsButton: { + padding: 0 + }, + moreOptions: { + textAlign: 'right', + '&:last-child': { + paddingRight: 0 + } + }, }); const mapStateToProps = ({ virtualMachines, auth }: RootState) => { return { requestedDate: virtualMachines.date, isAdmin: auth.user!.isAdmin, + logins: virtualMachines.logins, ...virtualMachines }; }; -const mapDispatchToProps = { - saveRequestedDate, - loadVirtualMachinesData -}; +const mapDispatchToProps = (dispatch: Dispatch): Pick => ({ + saveRequestedDate: () => dispatch(saveRequestedDate()), + loadVirtualMachinesData: () => dispatch(loadVirtualMachinesData()), + onOptionsMenuOpen: (event, index, virtualMachine) => { + dispatch(openVirtualMachinesContextMenu(event, index, virtualMachine)); + }, +}); interface VirtualMachinesPanelDataProps { requestedDate: string; virtualMachines: ListResults; - logins: VirtualMachinesLoginsResource[]; + logins: VirtualMachineLogins; links: ListResults; isAdmin: boolean; } @@ -82,6 +96,7 @@ interface VirtualMachinesPanelDataProps { interface VirtualMachinesPanelActionProps { saveRequestedDate: () => void; loadVirtualMachinesData: () => string; + onOptionsMenuOpen: (event: React.MouseEvent, index: number, virtualMachine: VirtualMachinesResource) => void; } type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles; @@ -95,12 +110,12 @@ export const VirtualMachinePanel = compose( } render() { - const { virtualMachines, links } = this.props; + const { virtualMachines, links, isAdmin } = this.props; return ( {virtualMachines.itemsAvailable === 0 && } {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && } - {} + {!isAdmin && } ); } @@ -133,24 +148,35 @@ const CardContentWithVirtualMachines = (props: VirtualMachineProps) => -
- - {props.requestedDate && - - A request for shell access was sent on {props.requestedDate} - } -
- - {console.log(props.isAdmin)} - {props.isAdmin ? adminVirtualMachinesTable(props) : userVirtualMachinesTable(props)} + {props.isAdmin ? + +
+ +
+ {adminVirtualMachinesTable(props)} +
: + +
+ + {props.requestedDate && + + A request for shell access was sent on {props.requestedDate} + } +
+ + {userVirtualMachinesTable(props)} +
+ }
; @@ -188,19 +214,21 @@ const adminVirtualMachinesTable = (props: VirtualMachineProps) => Uuid Host name Logins - + - {props.virtualMachines.items.map((it, index) => + {props.logins.items.length > 0 && props.virtualMachines.items.map((it, index) => {it.uuid} - shell - ssh {getUsername(props.links, it)}@shell.arvados - - - Log in as {getUsername(props.links, it)} - + {props.logins.items[0].hostname} + ["{props.logins.items[0].username}"] + + + props.onOptionsMenuOpen(event, index, it)} className={props.classes.moreOptionsButton}> + + + )} diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 3914f64632..3fc514af87 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -59,6 +59,8 @@ import { CreateSshKeyDialog } from '~/views-components/dialog-forms/create-ssh-k 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'; +import { VirtualMachineAttributesDialog } from '~/views-components/virtual-machines-dialog/attributes-dialog'; +import { RemoveVirtualMachineDialog } from '~/views-components/virtual-machines-dialog/remove-dialog'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -164,6 +166,7 @@ export const WorkbenchPanel = + @@ -173,5 +176,6 @@ export const WorkbenchPanel = + ); \ No newline at end of file -- 2.39.5