[ARVADOS-WORKBENCH2] created: 2.3.0-143-g85836abe
Git user
git at public.arvados.org
Tue Jan 25 21:58:26 UTC 2022
at 85836abe716b2403f3b7fe75c551e4ace3995157 (commit)
commit 85836abe716b2403f3b7fe75c551e4ace3995157
Author: Stephen Smith <stephen at curii.com>
Date: Tue Jan 25 16:58:12 2022 -0500
18284: Resolve bugs in VM listing and add login administration functions
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
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<CssRules> = theme => ({
item: {
@@ -23,7 +26,11 @@ const styles: StyleRulesCallback<CssRules> = 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 (
<React.Fragment key={index}>
{isFirstItem ? null : <IllegalNamingWarning name={item.label} />}
@@ -54,6 +62,7 @@ export const Breadcrumbs = withStyles(styles)(
className={isLastItem ? classes.currentItem : classes.item}
onClick={() => onClick(item)}
onContextMenu={event => onContextMenu(event, item)}>
+ <Icon className={classes.icon} />
<Typography
noWrap
color="inherit"
diff --git a/src/components/chips-input/chips-input.tsx b/src/components/chips-input/chips-input.tsx
index 32077fbe..31e12333 100644
--- a/src/components/chips-input/chips-input.tsx
+++ b/src/components/chips-input/chips-input.tsx
@@ -12,6 +12,9 @@ interface ChipsInputProps<Value> {
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?: InputProps;
@@ -52,10 +55,11 @@ export const ChipsInput = withStyles(styles)(
this.setState({ text: event.target.value });
}
- handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
- if (key === 'Enter') {
+ handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ 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 <div className={classes.chips}>
+ return <div className={[classes.chips, this.props.chipsClassName].join(' ')}>
<Chips
{...props}
clickable={!props.disabled}
@@ -121,6 +125,8 @@ export const ChipsInput = withStyles(styles)(
onChange={this.setText}
disabled={this.props.disabled}
onKeyDown={this.handleKeyPress}
+ onFocus={this.props.handleFocus}
+ onBlur={this.props.handleBlur}
inputProps={{
...(InputProps && InputProps.inputProps),
className: classes.input,
diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 4f7305f7..54b7bee6 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -172,3 +172,4 @@ export const FolderSharedIcon: IconType = (props) => <FolderShared {...props} />
export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
export const CanManageIcon: IconType = (props) => <Computer {...props} />;
+export const AddUserIcon: IconType = (props) => <PersonAdd {...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<string>(),
@@ -27,6 +31,13 @@ export type VirtualMachineActions = UnionOf<typeof virtualMachinesActions>;
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<any>(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<any>(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<any>(deleteResources([uuid]));
+
+ dispatch<any>(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<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setBreadcrumbs([{ label: 'Virtual Machines Admin', icon: AdminMenuIcon }]));
+ });
+
export const loadRepositories = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
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 <Checkbox
@@ -271,15 +272,37 @@ export const ResourceIsAdmin = connect(
}, { toggleIsAdmin }
)(renderIsAdmin);
-const renderUsername = (item: { username: string }) =>
- <Typography noWrap>{item.username}</Typography>;
+const renderUsername = (item: { username: string, uuid: string }) =>
+ <Typography noWrap>{item.username || item.uuid}</Typography>;
export const ResourceUsername = connect(
(state: RootState, props: { uuid: string }) => {
const resource = getResource<UserResource>(props.uuid)(state.resources);
- return resource || { username: '' };
+ return resource || { username: '', uuid: props.uuid };
})(renderUsername);
+// Virtual machine resource
+
+const renderHostname = (item: { hostname: string }) =>
+ <Typography noWrap>{item.hostname}</Typography>;
+
+export const VirtualMachineHostname = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
+ return resource || { hostname: '' };
+ })(renderHostname);
+
+const renderVirtualMachineLogin = (login: {user: string}) =>
+ <Typography noWrap>{login.user}</Typography>
+
+export const VirtualMachineLogin = connect(
+ (state: RootState, props: { linkUuid: string }) => {
+ const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
+ const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
+
+ return {user: user?.username || permission?.tailUuid || ''};
+ })(renderVirtualMachineLogin);
+
// Common methods
const renderCommonData = (data: string) =>
<Typography noWrap>{data}</Typography>;
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<AddLoginFormData>({
+ form: VIRTUAL_MACHINE_ADD_LOGIN_FORM,
+ onSubmit: (data, dispatch) => {
+ dispatch(addVirtualMachineLogin(data));
+ }
+ })
+)(
+ (props: CreateGroupDialogComponentProps) =>
+ <FormDialog
+ dialogTitle='Add login permissions'
+ formFields={AddLoginFormFields}
+ submitLabel='Add'
+ {...props}
+ />
+);
+
+type CreateGroupDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<AddLoginFormData>;
+
+const AddLoginFormFields = () =>
+ <>
+ <UserField />
+ <GroupArrayInput
+ name={VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD}
+ input={{id:"Add groups to VM login", disabled:false}}
+ required={false}
+ />
+ </>;
+
+const UserField = () =>
+ <Field
+ name={VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}
+ component={UserSelect}
+ />;
+
+const UserSelect = ({ input, meta }: WrappedFieldProps) =>
+ <ParticipantSelect
+ onlyPeople
+ label='Search for users to grant login permission'
+ items={input.value ? [input.value] : []}
+ onSelect={input.onChange}
+ onDelete={() => (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) =>
+ <Field
+ name={name}
+ commandInput={input}
+ component={StringArrayInputComponent as any}
+ />;
+
+const StringArrayInputComponent = (props: GenericInputProps) => {
+ return <FormGroup>
+ <FormControl fullWidth error={props.meta.error}>
+ <InputLabel shrink={props.meta.active || props.input.value.length > 0}>{props.commandInput.id}</InputLabel>
+ <StyledInputComponent {...props} />
+ </FormControl>
+ </FormGroup>;
+ };
+
+const StyledInputComponent = withStyles(styles)(
+ class InputComponent extends React.PureComponent<GenericInputProps & WithStyles<CssRules>>{
+ render() {
+ const { classes } = this.props;
+ const { commandInput, input, meta } = this.props;
+ return <ChipsInput
+ deletable={!commandInput.disabled}
+ orderable={!commandInput.disabled}
+ disabled={commandInput.disabled}
+ values={input.value}
+ onChange={this.handleChange}
+ handleFocus={input.onFocus}
+ handleBlur={this.handleBlur}
+ createNewValue={identity}
+ inputComponent={Input}
+ chipsClassName={classes.chips}
+ inputProps={{
+ error: meta.error,
+ }} />;
+ }
+
+ handleChange = (values: {}[]) => {
+ const { input, meta } = this.props;
+ if (!meta.touched) {
+ input.onBlur(values);
+ }
+ input.onChange(values);
+ }
+
+ handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
+ 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<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(removeVirtualMachineLogin(props.data.uuid));
+ dispatch<any>(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<CssRules> = (theme: ArvadosTheme) => ({
moreOptionsButton: {
@@ -27,6 +28,9 @@ const styles: StyleRulesCallback<CssRules> = (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<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'onOptionsMenuOpen'> => ({
+const mapDispatchToProps = (dispatch: Dispatch): Pick<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'onOptionsMenuOpen' | 'onAddLogin' | 'onDeleteLogin'> => ({
loadVirtualMachinesData: () => dispatch<any>(loadVirtualMachinesAdminData()),
onOptionsMenuOpen: (event, virtualMachine) => {
dispatch<any>(openVirtualMachinesContextMenu(event, virtualMachine));
},
+ onAddLogin: (uuid: string) => {
+ dispatch<any>(openAddVirtualMachineLoginDialog(uuid));
+ },
+ onDeleteLogin: (uuid: string) => {
+ dispatch<any>(openRemoveVirtualMachineLoginDialog(uuid));
+ },
});
interface VirtualMachinesPanelDataProps {
virtualMachines: ListResults<any>;
logins: VirtualMachineLogins;
+ links: ListResults<any>;
userUuid: string;
}
interface VirtualMachinesPanelActionProps {
loadVirtualMachinesData: () => string;
onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, virtualMachine: VirtualMachinesResource) => void;
+ onAddLogin: (uuid: string) => void;
+ onDeleteLogin: (uuid: string) => void;
}
type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
@@ -92,17 +105,33 @@ const virtualMachinesTable = (props: VirtualMachineProps) =>
<TableCell>Host name</TableCell>
<TableCell>Logins</TableCell>
<TableCell />
+ <TableCell />
</TableRow>
</TableHead>
<TableBody>
- {props.logins.items.length > 0 && props.virtualMachines.items.map((it, index) =>
+ {props.logins.items.length > 0 && props.virtualMachines.items.map((machine, index) =>
<TableRow key={index}>
- <TableCell>{it.uuid}</TableCell>
- <TableCell>{it.hostname}</TableCell>
- <TableCell>["{props.logins.items.map(it => it.userUuid === props.userUuid ? it.username : '')}"]</TableCell>
+ <TableCell><ResourceUuid uuid={machine.uuid} /></TableCell>
+ <TableCell><VirtualMachineHostname uuid={machine.uuid} /></TableCell>
+ <TableCell>
+ <Grid container spacing={8} className={props.classes.chipsRoot}>
+ {props.links.items.filter((link) => (link.headUuid === machine.uuid)).map((permission, i) => (
+ <Grid item key={i}>
+ <Chip label={<VirtualMachineLogin linkUuid={permission.uuid} />} onDelete={event => props.onDeleteLogin(permission.uuid)} />
+ </Grid>
+ ))}
+ </Grid>
+ </TableCell>
+ <TableCell>
+ <Tooltip title="Add Login Permission" disableFocusListener>
+ <IconButton onClick={event => props.onAddLogin(machine.uuid)} className={props.classes.moreOptionsButton}>
+ <AddUserIcon />
+ </IconButton>
+ </Tooltip>
+ </TableCell>
<TableCell className={props.classes.moreOptions}>
<Tooltip title="More options" disableFocusListener>
- <IconButton onClick={event => props.onOptionsMenuOpen(event, it)} className={props.classes.moreOptionsButton}>
+ <IconButton onClick={event => props.onOptionsMenuOpen(event, machine)} className={props.classes.moreOptionsButton}>
<MoreOptionsIcon />
</IconButton>
</Tooltip>
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<CssRules> = (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) =>
<TableRow>
<TableCell>Host name</TableCell>
<TableCell>Login name</TableCell>
+ <TableCell>Groups</TableCell>
<TableCell>Command line</TableCell>
<TableCell>Web shell</TableCell>
</TableRow>
@@ -181,7 +185,7 @@ const virtualMachinesTable = (props: VirtualMachineProps) =>
<TableBody>
{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 <TableRow key={lk.uuid}>
<TableCell>{it.hostname}</TableCell>
<TableCell>{username}</TableCell>
+ <TableCell>
+ <Grid container spacing={8} className={props.classes.chipsRoot}>
+ {
+ (lk.properties.groups || []).map((group, i) => (
+ <Grid item key={i}>
+ <Chip label={group} />
+ </Grid>
+ ))
+ }
+ </Grid>
+ </TableCell>
<TableCell>
{command}
</TableCell>
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 =
<RemoveRepositoryDialog />
<RemoveSshKeyDialog />
<RemoveVirtualMachineDialog />
+ <RemoveVirtualMachineLoginDialog />
+ <VirtualMachineAddLoginDialog />
<RenameFileDialog />
<RepositoryAttributesDialog />
<RepositoriesSampleGitDialog />
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list