From 85836abe716b2403f3b7fe75c551e4ace3995157 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Tue, 25 Jan 2022 16:58:12 -0500 Subject: [PATCH] 18284: Resolve bugs in VM listing and add login administration functions Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- src/components/breadcrumbs/breadcrumbs.tsx | 13 ++- src/components/chips-input/chips-input.tsx | 14 ++- src/components/icon/icon.tsx | 1 + src/models/permission.ts | 1 + src/routes/route-change-handlers.ts | 2 +- .../virtual-machines-actions.ts | 105 ++++++++++++++++++ src/store/workbench/workbench-actions.ts | 7 ++ .../data-explorer/renderers.tsx | 31 +++++- .../add-login-dialog.tsx | 56 ++++++++++ .../group-array-input.tsx | 79 +++++++++++++ .../remove-login-dialog.tsx | 22 ++++ .../virtual-machine-admin-panel.tsx | 49 ++++++-- .../virtual-machine-user-panel.tsx | 23 +++- src/views/workbench/workbench.tsx | 6 +- 14 files changed, 383 insertions(+), 26 deletions(-) create mode 100644 src/views-components/virtual-machines-dialog/add-login-dialog.tsx create mode 100644 src/views-components/virtual-machines-dialog/group-array-input.tsx create mode 100644 src/views-components/virtual-machines-dialog/remove-login-dialog.tsx diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index 21211460..3d668856 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -7,12 +7,15 @@ import { Button, Grid, StyleRulesCallback, WithStyles, Typography, Tooltip } fro import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import { withStyles } from '@material-ui/core'; import { IllegalNamingWarning } from '../warning/warning'; +import { IconType } from 'components/icon/icon'; +import grey from '@material-ui/core/colors/grey'; export interface Breadcrumb { label: string; + icon?: IconType; } -type CssRules = "item" | "currentItem" | "label"; +type CssRules = "item" | "currentItem" | "label" | "icon"; const styles: StyleRulesCallback = theme => ({ item: { @@ -23,7 +26,11 @@ const styles: StyleRulesCallback = theme => ({ }, label: { textTransform: "none" - } + }, + icon: { + fontSize: 20, + color: grey["600"] + }, }); export interface BreadcrumbsProps { @@ -39,6 +46,7 @@ export const Breadcrumbs = withStyles(styles)( items.map((item, index) => { const isLastItem = index === items.length - 1; const isFirstItem = index === 0; + const Icon = item.icon || (() => (null)); return ( {isFirstItem ? null : } @@ -54,6 +62,7 @@ export const Breadcrumbs = withStyles(styles)( className={isLastItem ? classes.currentItem : classes.item} onClick={() => onClick(item)} onContextMenu={event => onContextMenu(event, item)}> + { values: Value[]; getLabel?: (value: Value) => string; onChange: (value: Value[]) => void; + handleFocus?: (e: any) => void; + handleBlur?: (e: any) => void; + chipsClassName?: string; createNewValue: (value: string) => Value; inputComponent?: React.ComponentType; inputProps?: InputProps; @@ -52,10 +55,11 @@ export const ChipsInput = withStyles(styles)( this.setState({ text: event.target.value }); } - handleKeyPress = ({ key }: React.KeyboardEvent) => { - if (key === 'Enter') { + handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { this.createNewValue(); - } else if (key === 'Backspace') { + e.preventDefault(); + } else if (e.key === 'Backspace') { this.deleteLastValue(); } } @@ -104,7 +108,7 @@ export const ChipsInput = withStyles(styles)( renderChips() { const { classes, ...props } = this.props; - return
+ return
export const CanReadIcon: IconType = (props) => ; export const CanWriteIcon: IconType = (props) => ; export const CanManageIcon: IconType = (props) => ; +export const AddUserIcon: IconType = (props) => ; diff --git a/src/models/permission.ts b/src/models/permission.ts index f340c502..1d603801 100644 --- a/src/models/permission.ts +++ b/src/models/permission.ts @@ -13,4 +13,5 @@ export enum PermissionLevel { CAN_READ = 'can_read', CAN_WRITE = 'can_write', CAN_MANAGE = 'can_manage', + CAN_LOGIN = 'can_login', } diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index 70f65cb4..044a38bf 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -86,7 +86,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { } else if (virtualMachineUserMatch) { store.dispatch(WorkbenchActions.loadVirtualMachines); } else if (virtualMachineAdminMatch) { - store.dispatch(WorkbenchActions.loadVirtualMachines); + store.dispatch(WorkbenchActions.loadVirtualMachinesAdmin); } else if (repositoryMatch) { store.dispatch(WorkbenchActions.loadRepositories); } else if (sshKeysUserMatch) { diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts index e3b69d8d..08654a44 100644 --- a/src/store/virtual-machines/virtual-machines-actions.ts +++ b/src/store/virtual-machines/virtual-machines-actions.ts @@ -14,6 +14,10 @@ import { FilterBuilder } from "services/api/filter-builder"; import { ListResults } from "services/common-service/common-service"; import { dialogActions } from 'store/dialog/dialog-actions'; import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions'; +import { PermissionLevel } from "models/permission"; +import { deleteResources, updateResources } from 'store/resources/resources-actions'; +import { Participant } from "views-components/sharing-dialog/participant-select"; +import { initialize, reset } from "redux-form"; export const virtualMachinesActions = unionize({ SET_REQUESTED_DATE: ofType(), @@ -27,6 +31,13 @@ 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 VIRTUAL_MACHINE_ADD_LOGIN_DIALOG = 'virtualMachineAddLoginDialog'; +export const VIRTUAL_MACHINE_ADD_LOGIN_FORM = 'virtualMachineAddLoginForm'; +export const VIRTUAL_MACHINE_REMOVE_LOGIN_DIALOG = 'virtualMachineRemoveLoginDialog'; + +export const VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD = 'vmUuid'; +export const VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD = 'user'; +export const VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD = 'groups'; export const openUserVirtualMachines = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { @@ -59,8 +70,29 @@ const loadRequestedDate = () => export const loadVirtualMachinesAdminData = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(loadRequestedDate()); + const virtualMachines = await services.virtualMachineService.list(); + dispatch(updateResources(virtualMachines.items)); dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines)); + + + const logins = await services.permissionService.list({ + filters: new FilterBuilder() + .addIn('head_uuid', virtualMachines.items.map(item => item.uuid)) + .addEqual('name', PermissionLevel.CAN_LOGIN) + .getFilters() + }); + dispatch(updateResources(logins.items)); + dispatch(virtualMachinesActions.SET_LINKS(logins)); + + const users = await services.userService.list({ + filters: new FilterBuilder() + .addIn('uuid', logins.items.map(item => item.tailUuid)) + .getFilters(), + count: "none" + }); + dispatch(updateResources(users.items)); + const getAllLogins = await services.virtualMachineService.getAllLogins(); dispatch(virtualMachinesActions.SET_LOGINS(getAllLogins)); }; @@ -79,6 +111,79 @@ export const loadVirtualMachinesUserData = () => dispatch(virtualMachinesActions.SET_LINKS(links)); }; +export const openAddVirtualMachineLoginDialog = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, {[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: uuid, [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: ['docker']})); + dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {}} )); + } + +export interface AddLoginFormData { + [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: string; + [VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: Participant; + [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: string[]; +} + + +export const addVirtualMachineLogin = ({vmUuid, user, groups}: AddLoginFormData) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + try { + // Get user + const userResource = await services.userService.get(user.uuid); + + const permission = await services.permissionService.create({ + headUuid: vmUuid, + tailUuid: userResource.uuid, + name: PermissionLevel.CAN_LOGIN, + properties: { + username: userResource.username, + groups, + } + }); + dispatch(updateResources([permission])); + + dispatch(reset(VIRTUAL_MACHINE_ADD_LOGIN_FORM)); + dispatch(dialogActions.CLOSE_DIALOG({ id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG })); + dispatch(loadVirtualMachinesAdminData()); + + dispatch(snackbarActions.OPEN_SNACKBAR({ + message: `Permissions updated`, + kind: SnackbarKind.SUCCESS + })); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR })); + } + }; + +export const openRemoveVirtualMachineLoginDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: VIRTUAL_MACHINE_REMOVE_LOGIN_DIALOG, + data: { + title: 'Remove login permission', + text: 'Are you sure you want to remove this permission?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const removeVirtualMachineLogin = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + try { + await services.permissionService.delete(uuid); + dispatch(deleteResources([uuid])); + + dispatch(loadVirtualMachinesAdminData()); + + dispatch(snackbarActions.OPEN_SNACKBAR({ + message: `Login permission removed`, + kind: SnackbarKind.SUCCESS + })); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR })); + } + }; + export const saveRequestedDate = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const date = formatDate((new Date()).toISOString()); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 527d9d74..58d8d5f1 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -100,6 +100,7 @@ import { loadAllProcessesPanel, allProcessesPanelActions } from '../all-processe import { allProcessesPanelColumns } from 'views/all-processes-panel/all-processes-panel'; import { collectionPanelFilesAction } from '../collection-panel/collection-panel-files/collection-panel-files-actions'; import { createTree } from 'models/tree'; +import { AdminMenuIcon } from 'components/icon/icon'; export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen'; @@ -492,6 +493,12 @@ export const loadVirtualMachines = handleFirstTimeLoad( dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }])); }); +export const loadVirtualMachinesAdmin = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadVirtualMachinesPanel()); + dispatch(setBreadcrumbs([{ label: 'Virtual Machines Admin', icon: AdminMenuIcon }])); + }); + export const loadRepositories = handleFirstTimeLoad( async (dispatch: Dispatch) => { await dispatch(loadRepositoriesPanel()); diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 901704d9..ce6c02ca 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -35,6 +35,7 @@ import { formatPermissionLevel } from 'views-components/sharing-dialog/permissio import { PermissionLevel } from 'models/permission'; import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions'; import { getUserUuid } from 'common/getuser'; +import { VirtualMachinesResource } from 'models/virtual-machines'; const renderName = (dispatch: Dispatch, item: GroupContentsResource) => { @@ -221,7 +222,7 @@ const renderIsHidden = (props: { permissionLinkUuid: string, visible: boolean, canManage: boolean, - setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void + setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void }) => { if (props.memberLinkUuid) { return - {item.username}; +const renderUsername = (item: { username: string, uuid: string }) => + {item.username || item.uuid}; export const ResourceUsername = connect( (state: RootState, props: { uuid: string }) => { const resource = getResource(props.uuid)(state.resources); - return resource || { username: '' }; + return resource || { username: '', uuid: props.uuid }; })(renderUsername); +// Virtual machine resource + +const renderHostname = (item: { hostname: string }) => + {item.hostname}; + +export const VirtualMachineHostname = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { hostname: '' }; + })(renderHostname); + +const renderVirtualMachineLogin = (login: {user: string}) => + {login.user} + +export const VirtualMachineLogin = connect( + (state: RootState, props: { linkUuid: string }) => { + const permission = getResource(props.linkUuid)(state.resources); + const user = getResource(permission?.tailUuid || '')(state.resources); + + return {user: user?.username || permission?.tailUuid || ''}; + })(renderVirtualMachineLogin); + // Common methods const renderCommonData = (data: string) => {data}; diff --git a/src/views-components/virtual-machines-dialog/add-login-dialog.tsx b/src/views-components/virtual-machines-dialog/add-login-dialog.tsx new file mode 100644 index 00000000..8d543406 --- /dev/null +++ b/src/views-components/virtual-machines-dialog/add-login-dialog.tsx @@ -0,0 +1,56 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { compose } from "redux"; +import { reduxForm, InjectedFormProps, WrappedFieldProps, Field } from 'redux-form'; +import { withDialog, WithDialogProps } from "store/dialog/with-dialog"; +import { FormDialog } from 'components/form-dialog/form-dialog'; +import { VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, VIRTUAL_MACHINE_ADD_LOGIN_FORM, addVirtualMachineLogin, AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD } from 'store/virtual-machines/virtual-machines-actions'; +import { ParticipantSelect } from 'views-components/sharing-dialog/participant-select'; +import { GroupArrayInput } from 'views-components/virtual-machines-dialog/group-array-input'; + +export const VirtualMachineAddLoginDialog = compose( + withDialog(VIRTUAL_MACHINE_ADD_LOGIN_DIALOG), + reduxForm({ + form: VIRTUAL_MACHINE_ADD_LOGIN_FORM, + onSubmit: (data, dispatch) => { + dispatch(addVirtualMachineLogin(data)); + } + }) +)( + (props: CreateGroupDialogComponentProps) => + +); + +type CreateGroupDialogComponentProps = WithDialogProps<{}> & InjectedFormProps; + +const AddLoginFormFields = () => + <> + + + ; + +const UserField = () => + ; + +const UserSelect = ({ input, meta }: WrappedFieldProps) => + (input.onChange(''))} />; diff --git a/src/views-components/virtual-machines-dialog/group-array-input.tsx b/src/views-components/virtual-machines-dialog/group-array-input.tsx new file mode 100644 index 00000000..12a73019 --- /dev/null +++ b/src/views-components/virtual-machines-dialog/group-array-input.tsx @@ -0,0 +1,79 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { StringArrayCommandInputParameter } from 'models/workflow'; +import { Field } from 'redux-form'; +import { GenericInputProps } from 'views/run-process-panel/inputs/generic-input'; +import { ChipsInput } from 'components/chips-input/chips-input'; +import { identity } from 'lodash'; +import { withStyles, WithStyles, FormGroup, Input, InputLabel, FormControl } from '@material-ui/core'; + +export interface StringArrayInputProps { + name: string; + input: StringArrayCommandInputParameter; + required: boolean; +} + +type CssRules = 'chips'; + +const styles = { + chips: { + marginTop: "16px", + }, +}; + +export const GroupArrayInput = ({name, input}: StringArrayInputProps) => + ; + +const StringArrayInputComponent = (props: GenericInputProps) => { + return + + 0}>{props.commandInput.id} + + + ; + }; + +const StyledInputComponent = withStyles(styles)( + class InputComponent extends React.PureComponent>{ + render() { + const { classes } = this.props; + const { commandInput, input, meta } = this.props; + return ; + } + + handleChange = (values: {}[]) => { + const { input, meta } = this.props; + if (!meta.touched) { + input.onBlur(values); + } + input.onChange(values); + } + + handleBlur = (e: React.FocusEvent) => { + const { input } = this.props; + if (!input.value?.length) { + input.onBlur(e); + } + } + } +); diff --git a/src/views-components/virtual-machines-dialog/remove-login-dialog.tsx b/src/views-components/virtual-machines-dialog/remove-login-dialog.tsx new file mode 100644 index 00000000..60a485f1 --- /dev/null +++ b/src/views-components/virtual-machines-dialog/remove-login-dialog.tsx @@ -0,0 +1,22 @@ +// 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_LOGIN_DIALOG, removeVirtualMachineLogin, loadVirtualMachinesAdminData } from 'store/virtual-machines/virtual-machines-actions'; + +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeVirtualMachineLogin(props.data.uuid)); + dispatch(loadVirtualMachinesAdminData()); + } +}); + +export const RemoveVirtualMachineLoginDialog = compose( + withDialog(VIRTUAL_MACHINE_REMOVE_LOGIN_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); diff --git a/src/views/virtual-machine-panel/virtual-machine-admin-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-admin-panel.tsx index a6ad24a7..0f2c0033 100644 --- a/src/views/virtual-machine-panel/virtual-machine-admin-panel.tsx +++ b/src/views/virtual-machine-panel/virtual-machine-admin-panel.tsx @@ -4,18 +4,19 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Grid, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip, IconButton } from '@material-ui/core'; +import { Grid, Card, Chip, 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 { compose, Dispatch } from 'redux'; -import { loadVirtualMachinesAdminData } from 'store/virtual-machines/virtual-machines-actions'; +import { loadVirtualMachinesAdminData, openAddVirtualMachineLoginDialog, openRemoveVirtualMachineLoginDialog } from 'store/virtual-machines/virtual-machines-actions'; import { RootState } from 'store/store'; import { ListResults } from 'services/common-service/common-service'; -import { MoreOptionsIcon } from 'components/icon/icon'; +import { MoreOptionsIcon, AddUserIcon } from 'components/icon/icon'; import { VirtualMachineLogins, VirtualMachinesResource } from 'models/virtual-machines'; import { openVirtualMachinesContextMenu } from 'store/context-menu/context-menu-actions'; +import { ResourceUuid, VirtualMachineHostname, VirtualMachineLogin } from 'views-components/data-explorer/renderers'; -type CssRules = 'moreOptionsButton' | 'moreOptions'; +type CssRules = 'moreOptionsButton' | 'moreOptions' | 'chipsRoot'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ moreOptionsButton: { @@ -27,6 +28,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ paddingRight: 0 } }, + chipsRoot: { + margin: `0px -${theme.spacing.unit / 2}px`, + }, }); const mapStateToProps = (state: RootState) => { @@ -36,22 +40,31 @@ const mapStateToProps = (state: RootState) => { }; }; -const mapDispatchToProps = (dispatch: Dispatch): Pick => ({ +const mapDispatchToProps = (dispatch: Dispatch): Pick => ({ loadVirtualMachinesData: () => dispatch(loadVirtualMachinesAdminData()), onOptionsMenuOpen: (event, virtualMachine) => { dispatch(openVirtualMachinesContextMenu(event, virtualMachine)); }, + onAddLogin: (uuid: string) => { + dispatch(openAddVirtualMachineLoginDialog(uuid)); + }, + onDeleteLogin: (uuid: string) => { + dispatch(openRemoveVirtualMachineLoginDialog(uuid)); + }, }); interface VirtualMachinesPanelDataProps { virtualMachines: ListResults; logins: VirtualMachineLogins; + links: ListResults; userUuid: string; } interface VirtualMachinesPanelActionProps { loadVirtualMachinesData: () => string; onOptionsMenuOpen: (event: React.MouseEvent, virtualMachine: VirtualMachinesResource) => void; + onAddLogin: (uuid: string) => void; + onDeleteLogin: (uuid: string) => void; } type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles; @@ -92,17 +105,33 @@ const virtualMachinesTable = (props: VirtualMachineProps) => Host name Logins + - {props.logins.items.length > 0 && props.virtualMachines.items.map((it, index) => + {props.logins.items.length > 0 && props.virtualMachines.items.map((machine, index) => - {it.uuid} - {it.hostname} - ["{props.logins.items.map(it => it.userUuid === props.userUuid ? it.username : '')}"] + + + + + {props.links.items.filter((link) => (link.headUuid === machine.uuid)).map((permission, i) => ( + + } onDelete={event => props.onDeleteLogin(permission.uuid)} /> + + ))} + + + + + props.onAddLogin(machine.uuid)} className={props.classes.moreOptionsButton}> + + + + - props.onOptionsMenuOpen(event, it)} className={props.classes.moreOptionsButton}> + props.onOptionsMenuOpen(event, machine)} className={props.classes.moreOptionsButton}> diff --git a/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx index d8725461..091a8198 100644 --- a/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx +++ b/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx @@ -4,7 +4,7 @@ import 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, Chip } from '@material-ui/core'; import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; import { ArvadosTheme } from 'common/custom-theme'; import { compose, Dispatch } from 'redux'; @@ -16,7 +16,7 @@ import { SESSION_STORAGE } from "services/auth-service/auth-service"; // import * as CopyToClipboard from 'react-copy-to-clipboard'; import parse from "parse-duration"; -type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon'; +type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon' | 'chipsRoot'; const EXTRA_TOKEN = "exraToken"; @@ -56,7 +56,10 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ icon: { textAlign: "right", marginTop: theme.spacing.unit - } + }, + chipsRoot: { + margin: `0px -${theme.spacing.unit / 2}px`, + }, }); const mapStateToProps = (state: RootState) => { @@ -174,6 +177,7 @@ const virtualMachinesTable = (props: VirtualMachineProps) => Host name Login name + Groups Command line Web shell @@ -181,7 +185,7 @@ const virtualMachinesTable = (props: VirtualMachineProps) => {props.virtualMachines.items.map(it => props.links.items.map(lk => { - if (lk.tailUuid === props.userUuid) { + if (lk.tailUuid === props.userUuid && lk.headUuid === it.uuid) { const username = lk.properties.username; const command = `ssh ${username}@${it.hostname}${props.hostSuffix}`; let tokenParam = ""; @@ -191,6 +195,17 @@ const virtualMachinesTable = (props: VirtualMachineProps) => return {it.hostname} {username} + + + { + (lk.properties.groups || []).map((group, i) => ( + + + + )) + } + + {command} diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index e7bb048f..49922202 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -68,12 +68,14 @@ import { RemoveApiClientAuthorizationDialog } from 'views-components/api-client- import { RemoveKeepServiceDialog } from 'views-components/keep-services-dialog/remove-dialog'; import { RemoveLinkDialog } from 'views-components/links-dialog/remove-dialog'; import { RemoveSshKeyDialog } from 'views-components/ssh-keys-dialog/remove-dialog'; +import { VirtualMachineAttributesDialog } from 'views-components/virtual-machines-dialog/attributes-dialog'; import { RemoveVirtualMachineDialog } from 'views-components/virtual-machines-dialog/remove-dialog'; +import { RemoveVirtualMachineLoginDialog } from 'views-components/virtual-machines-dialog/remove-login-dialog'; +import { VirtualMachineAddLoginDialog } from 'views-components/virtual-machines-dialog/add-login-dialog'; import { AttributesApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/attributes-dialog'; import { AttributesKeepServiceDialog } from 'views-components/keep-services-dialog/attributes-dialog'; import { AttributesLinkDialog } from 'views-components/links-dialog/attributes-dialog'; import { AttributesSshKeyDialog } from 'views-components/ssh-keys-dialog/attributes-dialog'; -import { VirtualMachineAttributesDialog } from 'views-components/virtual-machines-dialog/attributes-dialog'; import { UserPanel } from 'views/user-panel/user-panel'; import { UserAttributesDialog } from 'views-components/user-dialog/attributes-dialog'; import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog'; @@ -251,6 +253,8 @@ export const WorkbenchPanel = + + -- 2.30.2