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 }>;
export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
+export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
export const MailIcon: IconType = (props) => <Mail {...props} />;
export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
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()}]`);
addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
+addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
fetchConfig()
.then(({ config, apiHost }) => {
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
REPOSITORY = "arvados#repository",
+ SSH_KEY = "arvados#authorizedKeys",
USER = "arvados#user",
VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
export interface AdvancedTabDialogData {
apiResponse: any;
metadata: any;
- uuid: string;
+ user: string;
pythonHeader: string;
pythonExample: string;
cliGetHeader: string;
const repositoryData = getState().repositories.items[index!];
if (data || repositoryData) {
if (data) {
- const user = await services.userService.get(data.ownerUuid);
const metadata = await services.linkService.list({
filters: new FilterBuilder()
.addEqual('headUuid', uuid)
.getFilters()
});
+ const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid);
if (kind === ResourceKind.COLLECTION) {
const dataCollection: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, collectionApiResponse, data, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, data.storageClassesConfirmed);
dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataCollection }));
const dataProject: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, groupRequestApiResponse, data, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, data.deleteAt);
dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProject }));
}
-
} else if (kind === ResourceKind.REPOSITORY) {
const dataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, repositoryData, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, repositoryData.createdAt);
dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataRepository }));
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';
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>(),
SET_SSH_KEYS: ofType<SshKeyResource[]>(),
- ADD_SSH_KEY: ofType<SshKeyResource>()
+ ADD_SSH_KEY: ofType<SshKeyResource>(),
+ REMOVE_SSH_KEY: ofType<string>()
});
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;
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
expect(store.getState().auth).toEqual({
apiToken: "token",
+ sshKeys: [],
user: {
email: "test@test.com",
firstName: "John",
const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
expect(state).toEqual({
apiToken: "token",
- user
+ user,
+ sshKeys: []
});
});
export interface AuthState {
user?: User;
apiToken?: string;
- sshKeys?: SshKeyResource[];
+ sshKeys: SshKeyResource[];
}
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;
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
});
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 }>(),
}));
};
+export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, index: number, sshKey: SshKeyResource) =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: sshKey.uuid,
+ ownerUuid: sshKey.ownerUuid,
+ kind: ResourceKind.SSH_KEY,
+ menuKind: ContextMenuKind.SSH_KEY,
+ index
+ }));
+ };
+
export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
const res = getResource<UserResource>(projectUuid)(getState().resources);
(dispatch: Dispatch, getState: () => RootState) => {
const resource = {
uuid: process.containerRequest.uuid,
- ownerUuid: '',
+ ownerUuid: process.containerRequest.ownerUuid,
kind: ResourceKind.PROCESS,
- name: '',
- description: '',
+ name: process.containerRequest.name,
+ description: process.containerRequest.description,
menuKind: ContextMenuKind.PROCESS
};
dispatch<any>(openContextMenu(event, resource));
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
-import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process';
+import { getProcess } from '~/store/processes/process';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
if (process) {
- const processStatus = getProcessStatus(process);
- if (processStatus === ProcessStatus.DRAFT) {
- dispatch<any>(resetPickerProjectTree());
- dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
- const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
- dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
- dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
- } else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can copy only draft processes.', hideDuration: 2000 }));
- }
+ dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
+ const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
+ dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
} else {
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
}
dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
try {
const process = await services.containerRequestService.get(resource.uuid);
- const uuidKey = 'uuid';
- delete process[uuidKey];
- await services.containerRequestService.create({ ...process, ownerUuid: resource.ownerUuid, name: resource.name });
+ const { kind, containerImage, outputPath, outputName, containerCountMax, command, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters } = process;
+ await services.containerRequestService.create({ command, containerImage, outputPath, ownerUuid: resource.ownerUuid, name: resource.name, kind, outputName, containerCountMax, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters });
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
return process;
} catch (e) {
import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
import { projectPanelActions } from '~/store/project-panel/project-panel-action';
-import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process';
+import { getProcess } from '~/store/processes/process';
import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
if (process) {
- const processStatus = getProcessStatus(process);
- if (processStatus === ProcessStatus.DRAFT) {
- dispatch<any>(resetPickerProjectTree());
- dispatch<any>(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME));
- dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
- dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
- } else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can move only draft processes.', hideDuration: 2000 }));
- }
+ dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME));
+ dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
} else {
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
}
dispatch(startSubmit(PROCESS_MOVE_FORM_NAME));
try {
const process = await services.containerRequestService.get(resource.uuid);
- await services.containerRequestService.update(resource.uuid, { ...process, ownerUuid: resource.ownerUuid });
+ await services.containerRequestService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
return process;
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' }));
- } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
- dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'You can move only draft processes.' }));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the process.', hideDuration: 2000 }));
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME));
try {
- const process = await services.containerRequestService.get(resource.uuid);
- const updatedProcess = await services.containerRequestService.update(resource.uuid, { ...process, name: resource.name });
+ const updatedProcess = await services.containerRequestService.update(resource.uuid, { name: resource.name });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
return updatedProcess;
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' }));
- } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
- dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'You cannot modified in "Final" state.' }));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000 }));
import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from '~/views/run-process-panel/run-process-advanced-form';
import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
import { dialogActions } from '~/store/dialog/dialog-actions';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
export const runProcessPanelActions = unionize({
SET_PROCESS_OWNER_UUID: ofType<string>(),
export const loadRunProcessPanel = () =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
try {
+ dispatch(setBreadcrumbs([{ label: 'Run Process' }]));
dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
const response = await services.workflowService.list();
dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items));
message: 'Resource has been shared',
kind: SnackbarKind.SUCCESS,
}));
+ dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
};
const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
<TableCell className={props.classes.cell}>{it.uuid}</TableCell>
<TableCell className={props.classes.cell}>{it.linkClass}</TableCell>
<TableCell className={props.classes.cell}>{it.name}</TableCell>
- <TableCell className={props.classes.cell}>{props.user ? `User: ${props.user.firstName} ${props.user.lastName}` : it.tailUuid}</TableCell>
+ <TableCell className={props.classes.cell}>{props.user && `User: ${props.user.firstName} ${props.user.lastName}`}</TableCell>
<TableCell className={props.classes.cell}>{it.headUuid === props.uuid ? 'this' : it.headUuid}</TableCell>
<TableCell className={props.classes.cell}>{JSON.stringify(it.properties)}</TableCell>
</TableRow>
import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleProjectTrashed } from "~/store/trash/trash-actions";
-import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
import { ShareIcon } from '~/components/icon/icon';
import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
}, {
name: "Advanced",
icon: AdvancedIcon,
- execute: (dispatch, { uuid, index }) => {
- dispatch<any>(openAdvancedTabDialog(uuid, index));
+ execute: (dispatch, resource) => {
+ dispatch<any>(openAdvancedTabDialog(resource.uuid, resource.index));
}
}, {
name: "Remove",
--- /dev/null
+// 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<any>(openSshKeyAttributesDialog(index!));
+ }
+}, {
+ name: "Advanced",
+ icon: AdvancedIcon,
+ execute: (dispatch, { uuid, index }) => {
+ // ToDo
+ }
+}, {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSshKeyRemoveDialog(uuid));
+ }
+}]];
import { ContextMenuActionSet } from "../context-menu-action-set";
import { DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RestoreFromTrashIcon } from '~/components/icon/icon';
import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
PROCESS = "Process",
PROCESS_RESOURCE = 'ProcessResource',
PROCESS_LOGS = "ProcessLogs",
- REPOSITORY = "Repository"
+ REPOSITORY = "Repository",
+ SSH_KEY = "SshKey"
}
import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize } from '~/common/formatters';
import { resourceLabel } from '~/common/labels';
-import { connect } from 'react-redux';
+import { connect, DispatchProp } from 'react-redux';
import { RootState } from '~/store/store';
import { getResource } from '~/store/resources/resources';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
import { getProcess, Process, getProcessStatus, getProcessStatusColor } from '~/store/processes/process';
import { ArvadosTheme } from '~/common/custom-theme';
-import { compose } from 'redux';
+import { compose, Dispatch } from 'redux';
import { WorkflowResource } from '~/models/workflow';
import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
import { getUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
import { CollectionResource } from "~/models/collection";
import { getResourceData } from "~/store/resources-data/resources-data";
+import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
export const renderName = (item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
return `${uuidPrefix}-tpzed-anonymouspublic`;
};
-// do share onClick
-export const resourceShare = (uuidPrefix: string, ownerUuid?: string) => {
- return <Tooltip title="Share">
- <IconButton onClick={() => undefined}>
- {ownerUuid === getPublicUuid(uuidPrefix) ? <ShareIcon /> : null}
- </IconButton>
- </Tooltip>;
+// ToDo: share onClick
+export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
+ const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
+ return (
+ <div>
+ { isPublic && uuid &&
+ <Tooltip title="Share">
+ <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
+ <ShareIcon />
+ </IconButton>
+ </Tooltip>
+ }
+ </div>
+ );
};
export const ResourceShare = connect(
const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
const uuidPrefix = getUuidPrefix(state);
return {
+ uuid: resource ? resource.uuid : '',
ownerUuid: resource ? resource.ownerUuid : '',
uuidPrefix
};
- })((props: { ownerUuid?: string, uuidPrefix: string }) => resourceShare(props.uuidPrefix, props.ownerUuid));
+ })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
+ resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
if (ownerUuid === getPublicUuid(uuidPrefix)) {
--- /dev/null
+// 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<CssRules> = (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<AttributesSshKeyDialogDataProps> & WithStyles<CssRules>) =>
+ <Dialog open={open}
+ onClose={closeDialog}
+ fullWidth
+ maxWidth='sm'>
+ <DialogTitle>Attributes</DialogTitle>
+ <DialogContent>
+ {data.sshKey && <Grid container direction="row" spacing={16} className={classes.root}>
+ <Grid item xs={5}>Name</Grid>
+ <Grid item xs={7}>{data.sshKey.name}</Grid>
+ <Grid item xs={5}>uuid</Grid>
+ <Grid item xs={7}>{data.sshKey.uuid}</Grid>
+ <Grid item xs={5}>Owner uuid</Grid>
+ <Grid item xs={7}>{data.sshKey.ownerUuid}</Grid>
+ <Grid item xs={5}>Authorized user uuid</Grid>
+ <Grid item xs={7}>{data.sshKey.authorizedUserUuid}</Grid>
+ <Grid item xs={5}>Created at</Grid>
+ <Grid item xs={7}>{data.sshKey.createdAt}</Grid>
+ <Grid item xs={5}>Modified at</Grid>
+ <Grid item xs={7}>{data.sshKey.modifiedAt}</Grid>
+ <Grid item xs={5}>Expires at</Grid>
+ <Grid item xs={7}>{data.sshKey.expiresAt}</Grid>
+ <Grid item xs={5}>Modified by user uuid</Grid>
+ <Grid item xs={7}>{data.sshKey.modifiedByUserUuid}</Grid>
+ <Grid item xs={5}>Modified by client uuid</Grid>
+ <Grid item xs={7}>{data.sshKey.modifiedByClientUuid}</Grid>
+ </Grid>}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ );
\ No newline at end of file
--- /dev/null
+// 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<CssRules> = (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<PublicKeyDialogDataProps> & WithStyles<CssRules>) =>
+ <Dialog open={open}
+ onClose={closeDialog}
+ fullWidth
+ maxWidth='sm'>
+ <DialogTitle>{data.name} - SSH Key</DialogTitle>
+ <DialogContent>
+ {data && data.publicKey && <DefaultCodeSnippet
+ className={classes.codeSnippet}
+ lines={data.publicKey.split(' ')} />}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ );
\ No newline at end of file
--- /dev/null
+// 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<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(removeSshKey(props.data.uuid));
+ }
+});
+
+export const RemoveSshKeyDialog = compose(
+ withDialog(SSH_KEY_REMOVE_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
// 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<CssRules> = (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<HTMLElement>, 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<CssRules>;
export const SshKeyPanelRoot = withStyles(styles)(
- ({ classes, sshKeys, onClick }: SshKeyPanelRootProps) =>
+ ({ classes, sshKeys, openSshKeyCreateDialog, openPublicKeyDialog, hasKeys, openRowOptions }: SshKeyPanelRootProps) =>
<Card className={classes.root}>
<CardContent>
- <Typography variant='body1' paragraph={true}>
- You have not yet set up an SSH public key for use with Arvados.
- <a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html' target='blank' className={classes.link}>
- Learn more.
- </a>
- </Typography>
- <Typography variant='body1' paragraph={true}>
- When you have an SSH key you would like to use, add it using button below.
- </Typography>
- <Button
- onClick={onClick}
- color="primary"
- variant="contained">
- Add New Ssh Key
- </Button>
+ <Grid container direction="row">
+ <Grid item xs={8}>
+ { !hasKeys && <Typography variant='body1' paragraph={true} >
+ You have not yet set up an SSH public key for use with Arvados.
+ <a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html'
+ target='blank' className={classes.link}>
+ Learn more.
+ </a>
+ </Typography>}
+ { !hasKeys && <Typography variant='body1' paragraph={true}>
+ When you have an SSH key you would like to use, add it using button below.
+ </Typography> }
+ </Grid>
+ <Grid item xs={4} className={classes.buttonContainer}>
+ <Button onClick={openSshKeyCreateDialog} color="primary" variant="contained">
+ <AddIcon /> Add New Ssh Key
+ </Button>
+ </Grid>
+ </Grid>
+ <Grid item xs={12}>
+ {hasKeys && <Table className={classes.table}>
+ <TableHead>
+ <TableRow className={classes.tableRow}>
+ <TableCell>Name</TableCell>
+ <TableCell>UUID</TableCell>
+ <TableCell>Authorized user</TableCell>
+ <TableCell>Expires at</TableCell>
+ <TableCell>Key type</TableCell>
+ <TableCell>Public Key</TableCell>
+ <TableCell />
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {sshKeys.map((sshKey, index) =>
+ <TableRow key={index} className={classes.tableRow}>
+ <TableCell>{sshKey.name}</TableCell>
+ <TableCell>{sshKey.uuid}</TableCell>
+ <TableCell>{sshKey.authorizedUserUuid}</TableCell>
+ <TableCell>{sshKey.expiresAt || '(none)'}</TableCell>
+ <TableCell>{sshKey.keyType}</TableCell>
+ <TableCell>
+ <Tooltip title="Public Key" disableFocusListener>
+ <IconButton onClick={() => openPublicKeyDialog(sshKey.name, sshKey.publicKey)}>
+ <KeyIcon className={classes.keyIcon} />
+ </IconButton>
+ </Tooltip>
+ </TableCell>
+ <TableCell>
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton onClick={event => openRowOptions(event, index, sshKey)}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </Tooltip>
+ </TableCell>
+ </TableRow>)}
+ </TableBody>
+ </Table>}
+ </Grid>
</CardContent>
</Card>
);
\ No newline at end of file
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<any>(openSshKeyCreateDialog());
+ },
+ openRowOptions: (event, index, sshKey) => {
+ dispatch<any>(openSshKeyContextMenu(event, index, sshKey));
+ },
+ openPublicKeyDialog: (name: string, publicKey: string) => {
+ dispatch<any>(openPublicKeyDialog(name, publicKey));
}
});
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';
<DetailsPanel />
</Grid>
<AdvancedTabDialog />
+ <AttributesSshKeyDialog />
<ChangeWorkflowDialog />
<ContextMenu />
<CopyCollectionDialog />
<MoveProcessDialog />
<MoveProjectDialog />
<MultipleFilesRemoveDialog />
+ <PublicKeyDialog />
<PartialCopyCollectionDialog />
<ProcessCommandDialog />
<ProcessInputDialog />
<ProjectPropertiesDialog />
<RemoveProcessDialog />
<RemoveRepositoryDialog />
+ <RemoveSshKeyDialog />
<RenameFileDialog />
<RepositoryAttributesDialog />
<RepositoriesSampleGitDialog />