"classnames": "2.2.6",
"cwlts": "1.15.29",
"debounce": "1.2.0",
+ "is-image": "2.0.0",
"js-yaml": "3.12.0",
"lodash": "4.17.11",
"react": "16.5.2",
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import isImage from 'is-image';
+import { withStyles, WithStyles } from '@material-ui/core';
+import { FileTreeData } from '~/components/file-tree/file-tree-data';
+import { CollectionFileType } from '~/models/collection-file';
+
+export interface FileThumbnailProps {
+ file: FileTreeData;
+}
+
+export const FileThumbnail =
+ ({ file }: FileThumbnailProps) =>
+ file.type === CollectionFileType.FILE && isImage(file.name)
+ ? <ImageFileThumbnail file={file} />
+ : null;
+
+type ImageFileThumbnailCssRules = 'thumbnail';
+
+const imageFileThumbnailStyle = withStyles<ImageFileThumbnailCssRules>(theme => ({
+ thumbnail: {
+ maxWidth: 250,
+ margin: `${theme.spacing.unit}px 0`,
+ }
+}));
+
+const ImageFileThumbnail = imageFileThumbnailStyle(
+ ({ classes, file }: WithStyles<ImageFileThumbnailCssRules> & FileThumbnailProps) =>
+ <img
+ className={classes.thumbnail}
+ alt={file.name}
+ src={file.url} />
+);
export interface FileTreeData {
name: string;
type: string;
+ url: string;
size?: number;
}
import { formatFileSize } from "~/common/formatters";
import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
import { FileTreeData } from "./file-tree-data";
+import { FileThumbnail } from '~/components/file-tree/file-thumbnail';
type CssRules = "root" | "spacer" | "sizeInfo" | "button" | "moreOptions";
class extends React.Component<FileTreeItemProps & WithStyles<CssRules>> {
render() {
const { classes, item } = this.props;
- return <div className={classes.root}>
- <ListItemTextIcon
- icon={getIcon(item)}
- name={item.data.name} />
- <div className={classes.spacer} />
- <Typography
- className={classes.sizeInfo}
- variant="caption">{formatFileSize(item.data.size)}</Typography>
- <Tooltip title="More options" disableFocusListener>
- <IconButton
- className={classes.button}
- onClick={this.handleClick}>
- <MoreOptionsIcon className={classes.moreOptions}/>
- </IconButton>
- </Tooltip>
- </div >;
+ return <>
+ <div className={classes.root}>
+ <ListItemTextIcon
+ icon={getIcon(item)}
+ name={item.data.name} />
+ <div className={classes.spacer} />
+ <Typography
+ className={classes.sizeInfo}
+ variant="caption">{formatFileSize(item.data.size)}</Typography>
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton
+ className={classes.button}
+ onClick={this.handleClick}>
+ <MoreOptionsIcon className={classes.moreOptions} />
+ </IconButton>
+ </Tooltip>
+ </div >
+ <FileThumbnail file={item.data} />
+ </>;
}
handleClick = (event: React.MouseEvent<any>) => {
import { keepServiceActionSet } from '~/views-components/context-menu/action-sets/keep-service-action-set';
import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions';
import { virtualMachineActionSet } from '~/views-components/context-menu/action-sets/virtual-machine-action-set';
+import { computeNodeActionSet } from '~/views-components/context-menu/action-sets/compute-node-action-set';
console.log(`Starting arvados [${getBuildInfo()}]`);
addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet);
addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet);
+addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet);
fetchConfig()
.then(({ config, apiHost }) => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from '~/models/resource';
+
+export interface NodeResource extends Resource {
+ slotNumber: number;
+ hostname: string;
+ domain: string;
+ ipAddress: string;
+ jobUuid: string;
+ firstPingAt: string;
+ lastPingAt: string;
+ status: string;
+ info: NodeInfo;
+ properties: NodeProperties;
+}
+
+export interface NodeInfo {
+ lastAction: string;
+ pingSecret: string;
+ ec2InstanceId: string;
+ slurmState?: string;
+}
+
+export interface NodeProperties {
+ cloudNode: CloudNode;
+ totalRamMb: number;
+ totalCpuCores: number;
+ totalScratchMb: number;
+}
+
+interface CloudNode {
+ size: string;
+ price: number;
+}
\ No newline at end of file
CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
LOG = "arvados#log",
+ NODE = "arvados#node",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
REPOSITORY = "arvados#repository",
VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
SSH_KEY = 'fngyi',
- KEEP_SERVICE = 'bi6l4'
+ KEEP_SERVICE = 'bi6l4',
+ NODE = '7ekkf'
}
export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
return ResourceKind.SSH_KEY;
case ResourceObjectType.KEEP_SERVICE:
return ResourceKind.KEEP_SERVICE;
+ case ResourceObjectType.NODE:
+ return ResourceKind.NODE;
default:
return undefined;
}
import { History, Location } from 'history';
import { RootStore } from '~/store/store';
-import {
- matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute,
- matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute,
- matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute,
- matchKeepServicesRoute
-} from './routes';
-import {
- loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults,
- loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog,
- loadSshKeys, loadRepositories, loadVirtualMachines, loadKeepServices
-} from '~/store/workbench/workbench-actions';
+import * as Routes from '~/routes/routes';
+import * as WorkbenchActions from '~/store/workbench/workbench-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
export const addRouteChangeHandlers = (history: History, store: RootStore) => {
};
const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
- const rootMatch = matchRootRoute(pathname);
- const projectMatch = matchProjectRoute(pathname);
- const collectionMatch = matchCollectionRoute(pathname);
- const favoriteMatch = matchFavoritesRoute(pathname);
- const trashMatch = matchTrashRoute(pathname);
- const processMatch = matchProcessRoute(pathname);
- const processLogMatch = matchProcessLogRoute(pathname);
- const repositoryMatch = matchRepositoriesRoute(pathname);
- const searchResultsMatch = matchSearchResultsRoute(pathname);
- const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
- const runProcessMatch = matchRunProcessRoute(pathname);
- const virtualMachineMatch = matchVirtualMachineRoute(pathname);
- const workflowMatch = matchWorkflowRoute(pathname);
- const sshKeysMatch = matchSshKeysRoute(pathname);
- const keepServicesMatch = matchKeepServicesRoute(pathname);
+ const rootMatch = Routes.matchRootRoute(pathname);
+ const projectMatch = Routes.matchProjectRoute(pathname);
+ const collectionMatch = Routes.matchCollectionRoute(pathname);
+ const favoriteMatch = Routes.matchFavoritesRoute(pathname);
+ const trashMatch = Routes.matchTrashRoute(pathname);
+ const processMatch = Routes.matchProcessRoute(pathname);
+ const processLogMatch = Routes.matchProcessLogRoute(pathname);
+ const repositoryMatch = Routes.matchRepositoriesRoute(pathname);
+ const searchResultsMatch = Routes.matchSearchResultsRoute(pathname);
+ const sharedWithMeMatch = Routes.matchSharedWithMeRoute(pathname);
+ const runProcessMatch = Routes.matchRunProcessRoute(pathname);
+ const virtualMachineMatch = Routes.matchVirtualMachineRoute(pathname);
+ const workflowMatch = Routes.matchWorkflowRoute(pathname);
+ const sshKeysMatch = Routes.matchSshKeysRoute(pathname);
+ const keepServicesMatch = Routes.matchKeepServicesRoute(pathname);
+ const computeNodesMatch = Routes.matchComputeNodesRoute(pathname);
if (projectMatch) {
- store.dispatch(loadProject(projectMatch.params.id));
+ store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
} else if (collectionMatch) {
- store.dispatch(loadCollection(collectionMatch.params.id));
+ store.dispatch(WorkbenchActions.loadCollection(collectionMatch.params.id));
} else if (favoriteMatch) {
- store.dispatch(loadFavorites());
+ store.dispatch(WorkbenchActions.loadFavorites());
} else if (trashMatch) {
- store.dispatch(loadTrash());
+ store.dispatch(WorkbenchActions.loadTrash());
} else if (processMatch) {
- store.dispatch(loadProcess(processMatch.params.id));
+ store.dispatch(WorkbenchActions.loadProcess(processMatch.params.id));
} else if (processLogMatch) {
- store.dispatch(loadProcessLog(processLogMatch.params.id));
+ store.dispatch(WorkbenchActions.loadProcessLog(processLogMatch.params.id));
} else if (rootMatch) {
store.dispatch(navigateToRootProject);
} else if (sharedWithMeMatch) {
- store.dispatch(loadSharedWithMe);
+ store.dispatch(WorkbenchActions.loadSharedWithMe);
} else if (runProcessMatch) {
- store.dispatch(loadRunProcess);
+ store.dispatch(WorkbenchActions.loadRunProcess);
} else if (workflowMatch) {
- store.dispatch(loadWorkflow);
+ store.dispatch(WorkbenchActions.loadWorkflow);
} else if (searchResultsMatch) {
- store.dispatch(loadSearchResults);
+ store.dispatch(WorkbenchActions.loadSearchResults);
} else if (virtualMachineMatch) {
- store.dispatch(loadVirtualMachines);
+ store.dispatch(WorkbenchActions.loadVirtualMachines);
} else if(repositoryMatch) {
- store.dispatch(loadRepositories);
+ store.dispatch(WorkbenchActions.loadRepositories);
} else if (sshKeysMatch) {
- store.dispatch(loadSshKeys);
+ store.dispatch(WorkbenchActions.loadSshKeys);
} else if (keepServicesMatch) {
- store.dispatch(loadKeepServices);
+ store.dispatch(WorkbenchActions.loadKeepServices);
+ } else if (computeNodesMatch) {
+ store.dispatch(WorkbenchActions.loadComputeNodes);
}
};
WORKFLOWS: '/workflows',
SEARCH_RESULTS: '/search-results',
SSH_KEYS: `/ssh-keys`,
- KEEP_SERVICES: `/keep-services`
+ KEEP_SERVICES: `/keep-services`,
+ COMPUTE_NODES: `/nodes`
};
export const getResourceUrl = (uuid: string) => {
export const matchKeepServicesRoute = (route: string) =>
matchPath(route, { path: Routes.KEEP_SERVICES });
+
+export const matchComputeNodesRoute = (route: string) =>
+ matchPath(route, { path: Routes.COMPUTE_NODES });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { NodeResource } from '~/models/node';
+import { ApiActions } from '~/services/api/api-actions';
+
+export class NodeService extends CommonResourceService<NodeResource> {
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "nodes", actions);
+ }
+}
\ No newline at end of file
import { RepositoriesService } from '~/services/repositories-service/repositories-service';
import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service';
import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service';
+import { NodeService } from '~/services/node-service/node-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const keepService = new KeepService(apiClient, actions);
const linkService = new LinkService(apiClient, actions);
const logService = new LogService(apiClient, actions);
+ const nodeService = new NodeService(apiClient, actions);
const permissionService = new PermissionService(apiClient, actions);
const projectService = new ProjectService(apiClient, actions);
const repositoriesService = new RepositoriesService(apiClient, actions);
keepService,
linkService,
logService,
+ nodeService,
permissionService,
projectService,
repositoriesService,
import { ListResults } from '~/services/common-service/common-resource-service';
import { LinkResource } from '~/models/link';
import { KeepServiceResource } from '~/models/keep-services';
+import { NodeResource } from '~/models/node';
export const ADVANCED_TAB_DIALOG = 'advancedTabDialog';
REPOSITORIES = 'repositories',
AUTORIZED_KEYS = 'authorized_keys',
VIRTUAL_MACHINES = 'virtual_machines',
- KEEP_SERVICES = 'keep_services'
+ KEEP_SERVICES = 'keep_services',
+ COMPUTE_NODES = 'nodes'
}
enum KeepServiceData {
CREATED_AT = 'created_at'
}
-type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData;
+enum ComputeNodeData {
+ COMPUTE_NODE = 'node',
+ PROPERTIES = 'properties'
+}
+
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData;
type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix;
-type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | undefined;
+type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | undefined;
export const openAdvancedTabDialog = (uuid: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
});
dispatch<any>(initAdvancedTabDialog(advanceDataKeepService));
break;
+ case ResourceKind.NODE:
+ const dataComputeNode = getState().computeNodes.find(node => node.uuid === uuid);
+ const advanceDataComputeNode = advancedTabData({
+ uuid,
+ metadata: '',
+ user: '',
+ apiResponseKind: computeNodeApiResponse,
+ data: dataComputeNode,
+ resourceKind: ComputeNodeData.COMPUTE_NODE,
+ resourcePrefix: ResourcePrefix.COMPUTE_NODES,
+ resourceKindProperty: ComputeNodeData.PROPERTIES,
+ property: dataComputeNode!.properties
+ });
+ dispatch<any>(initAdvancedTabDialog(advanceDataComputeNode));
+ break;
default:
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
const cliUpdateExample = (uuid: string, resourceKind: string, resource: string | string[], resourceName: string) => {
const CLIUpdateCollectionExample = `arv ${resourceKind} update \\
--uuid ${uuid} \\
- --${resourceKind} '{"${resourceName}":${resource}}'`;
+ --${resourceKind} '{"${resourceName}":${JSON.stringify(resource)}}'`;
return CLIUpdateCollectionExample;
};
https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
<<EOF
{
- "${resourceName}": ${resource}
+ "${resourceName}": ${JSON.stringify(resource, null, 4)}
}
EOF`;
"created_at": "${createdAt}",
"read_only": "${stringify(readOnly)}"`;
+ return response;
+};
+
+const computeNodeApiResponse = (apiResponse: NodeResource) => {
+ const {
+ uuid, slotNumber, hostname, domain, ipAddress, firstPingAt, lastPingAt, jobUuid,
+ ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
+ properties, info
+ } = apiResponse;
+ const response = `"uuid": "${uuid}",
+"owner_uuid": "${ownerUuid}",
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
+"modified_at": ${stringify(modifiedAt)},
+"created_at": "${createdAt}",
+"slot_number": "${stringify(slotNumber)}",
+"hostname": "${stringify(hostname)}",
+"domain": "${stringify(domain)}",
+"ip_address": "${stringify(ipAddress)}",
+"first_ping_at": "${stringify(firstPingAt)}",
+"last_ping_at": "${stringify(lastPingAt)}",
+"job_uuid": "${stringify(jobUuid)}",
+"properties": "${JSON.stringify(properties, null, 4)}",
+"info": "${JSON.stringify(info, null, 4)}"`;
+
return response;
};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { RootState } from '~/store/store';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { ServiceRepository } from "~/services/services";
+import { NodeResource } from '~/models/node';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { navigateToRootProject } from '~/store/navigation/navigation-action';
+
+export const computeNodesActions = unionize({
+ SET_COMPUTE_NODES: ofType<NodeResource[]>(),
+ REMOVE_COMPUTE_NODE: ofType<string>()
+});
+
+export type ComputeNodesActions = UnionOf<typeof computeNodesActions>;
+
+export const COMPUTE_NODE_REMOVE_DIALOG = 'computeNodeRemoveDialog';
+export const COMPUTE_NODE_ATTRIBUTES_DIALOG = 'computeNodeAttributesDialog';
+
+export const loadComputeNodesPanel = () =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const user = getState().auth.user;
+ if (user && user.isAdmin) {
+ try {
+ dispatch(setBreadcrumbs([{ label: 'Compute Nodes' }]));
+ const response = await services.nodeService.list();
+ dispatch(computeNodesActions.SET_COMPUTE_NODES(response.items));
+ } catch (e) {
+ return;
+ }
+ } else {
+ dispatch(navigateToRootProject);
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 }));
+ }
+ };
+
+export const openComputeNodeAttributesDialog = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const computeNode = getState().computeNodes.find(node => node.uuid === uuid);
+ dispatch(dialogActions.OPEN_DIALOG({ id: COMPUTE_NODE_ATTRIBUTES_DIALOG, data: { computeNode } }));
+ };
+
+export const openComputeNodeRemoveDialog = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(dialogActions.OPEN_DIALOG({
+ id: COMPUTE_NODE_REMOVE_DIALOG,
+ data: {
+ title: 'Remove compute node',
+ text: 'Are you sure you want to remove this compute node?',
+ confirmButtonLabel: 'Remove',
+ uuid
+ }
+ }));
+ };
+
+export const removeComputeNode = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+ try {
+ await services.nodeService.delete(uuid);
+ dispatch(computeNodesActions.REMOVE_COMPUTE_NODE(uuid));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Compute node has been successfully removed.', hideDuration: 2000 }));
+ } catch (e) {
+ return;
+ }
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { computeNodesActions, ComputeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+import { NodeResource } from '~/models/node';
+
+export type ComputeNodesState = NodeResource[];
+
+const initialState: ComputeNodesState = [];
+
+export const computeNodesReducer = (state: ComputeNodesState = initialState, action: ComputeNodesActions): ComputeNodesState =>
+ computeNodesActions.match(action, {
+ SET_COMPUTE_NODES: nodes => nodes,
+ REMOVE_COMPUTE_NODE: (uuid: string) => state.filter((computeNode) => computeNode.uuid !== uuid),
+ default: () => state
+ });
\ No newline at end of file
import { SshKeyResource } from '~/models/ssh-key';
import { VirtualMachinesResource } from '~/models/virtual-machines';
import { KeepServiceResource } from '~/models/keep-services';
+import { NodeResource } from '~/models/node';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
}));
};
+export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) =>
+ (dispatch: Dispatch) => {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: computeNode.uuid,
+ ownerUuid: computeNode.ownerUuid,
+ kind: ResourceKind.NODE,
+ menuKind: ContextMenuKind.NODE
+ }));
+ };
+
export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
const res = getResource<UserResource>(projectUuid)(getState().resources);
export const navigateToSshKeys= push(Routes.SSH_KEYS);
-export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
\ No newline at end of file
+export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
+
+export const navigateToComputeNodes = push(Routes.COMPUTE_NODES);
\ No newline at end of file
import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer";
import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
+import { computeNodesReducer } from '~/store/compute-nodes/compute-nodes-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
searchBar: searchBarReducer,
virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer,
- keepServices: keepServicesReducer
+ keepServices: keepServicesReducer,
+ computeNodes: computeNodesReducer
});
import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
+import { loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
await dispatch(loadKeepServicesPanel());
});
+export const loadComputeNodes = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadComputeNodesPanel());
+ });
+
const finishLoadingProject = (project: GroupContentsResource | string) =>
async (dispatch: Dispatch<any>) => {
const uuid = typeof project === 'string' ? project : project.uuid;
import { Dispatch } from "redux";
import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
import { ContextMenuKind } from "../context-menu/context-menu";
-import { getNode, getNodeChildrenIds, Tree } from "~/models/tree";
-import { CollectionFileType } from "~/models/collection-file";
+import { getNode, getNodeChildrenIds, Tree, TreeNode, initTreeNode } from "~/models/tree";
+import { CollectionFileType, createCollectionDirectory } from "~/models/collection-file";
import { openContextMenu, openCollectionFilesContextMenu } from '~/store/context-menu/context-menu-actions';
import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
import { ResourceKind } from "~/models/resource";
const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
(id: string): TreeItem<FileTreeData> => {
- const node = getNode(id)(tree) || {
+ const node: TreeNode<CollectionPanelDirectory | CollectionPanelFile> = getNode(id)(tree) || initTreeNode({
id: '',
- children: [],
parent: '',
value: {
- name: 'Invalid node',
- type: CollectionFileType.DIRECTORY,
+ ...createCollectionDirectory({ name: 'Invalid file' }),
selected: false,
collapsed: true
}
- };
+ });
return {
active: false,
data: {
name: node.value.name,
size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
- type: node.value.type
+ type: node.value.type,
+ url: node.value.url,
},
id: node.id,
items: getNodeChildrenIds(node.id)(tree)
--- /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, Grid
+} from '@material-ui/core';
+import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog";
+import { COMPUTE_NODE_ATTRIBUTES_DIALOG } from '~/store/compute-nodes/compute-nodes-actions';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { NodeResource, NodeProperties, NodeInfo } from '~/models/node';
+import * as classnames from "classnames";
+
+type CssRules = 'root' | 'grid';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ fontSize: '0.875rem',
+ '& div:nth-child(odd):not(.nestedRoot)': {
+ textAlign: 'right',
+ color: theme.palette.grey["500"]
+ },
+ '& div:nth-child(even)': {
+ overflowWrap: 'break-word'
+ }
+ },
+ grid: {
+ padding: '8px 0 0 0'
+ }
+});
+
+interface AttributesComputeNodeDialogDataProps {
+ computeNode: NodeResource;
+}
+
+export const AttributesComputeNodeDialog = compose(
+ withDialog(COMPUTE_NODE_ATTRIBUTES_DIALOG),
+ withStyles(styles))(
+ ({ open, closeDialog, data, classes }: WithDialogProps<AttributesComputeNodeDialogDataProps> & WithStyles<CssRules>) =>
+ <Dialog open={open} onClose={closeDialog} fullWidth maxWidth='sm'>
+ <DialogTitle>Attributes</DialogTitle>
+ <DialogContent>
+ {data.computeNode && <div>
+ {renderPrimaryInfo(data.computeNode, classes)}
+ {renderInfo(data.computeNode.info, classes)}
+ {renderProperties(data.computeNode.properties, classes)}
+ </div>}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ );
+
+const renderPrimaryInfo = (computeNode: NodeResource, classes: any) => {
+ const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid } = computeNode;
+ return (
+ <Grid container direction="row" spacing={16} className={classes.root}>
+ <Grid item xs={5}>UUID</Grid>
+ <Grid item xs={7}>{uuid}</Grid>
+ <Grid item xs={5}>Owner uuid</Grid>
+ <Grid item xs={7}>{ownerUuid}</Grid>
+ <Grid item xs={5}>Created at</Grid>
+ <Grid item xs={7}>{createdAt}</Grid>
+ <Grid item xs={5}>Modified at</Grid>
+ <Grid item xs={7}>{modifiedAt}</Grid>
+ <Grid item xs={5}>Modified by user uuid</Grid>
+ <Grid item xs={7}>{modifiedByUserUuid}</Grid>
+ <Grid item xs={5}>Modified by client uuid</Grid>
+ <Grid item xs={7}>{modifiedByClientUuid || '(none)'}</Grid>
+ </Grid>
+ );
+};
+
+const renderInfo = (info: NodeInfo, classes: any) => {
+ const { lastAction, pingSecret, ec2InstanceId, slurmState } = info;
+ return (
+ <Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
+ <Grid item xs={5}>Info - Last action</Grid>
+ <Grid item xs={7}>{lastAction || '(none)'}</Grid>
+ <Grid item xs={5}>Info - Ping secret</Grid>
+ <Grid item xs={7}>{pingSecret || '(none)'}</Grid>
+ <Grid item xs={5}>Info - ec2 instance id</Grid>
+ <Grid item xs={7}>{ec2InstanceId || '(none)'}</Grid>
+ <Grid item xs={5}>Info - Slurm state</Grid>
+ <Grid item xs={7}>{slurmState || '(none)'}</Grid>
+ </Grid>
+ );
+};
+
+const renderProperties = (properties: NodeProperties, classes: any) => {
+ const { totalRamMb, totalCpuCores, totalScratchMb, cloudNode } = properties;
+ return (
+ <Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
+ <Grid item xs={5}>Properties - Total ram mb</Grid>
+ <Grid item xs={7}>{totalRamMb || '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Total scratch mb</Grid>
+ <Grid item xs={7}>{totalScratchMb || '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Total cpu cores</Grid>
+ <Grid item xs={7}>{totalCpuCores || '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Cloud node size </Grid>
+ <Grid item xs={7}>{cloudNode ? cloudNode.size : '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Cloud node price</Grid>
+ <Grid item xs={7}>{cloudNode ? cloudNode.price : '(none)'}</Grid>
+ </Grid>
+ );
+};
\ 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 { COMPUTE_NODE_REMOVE_DIALOG, removeComputeNode } from '~/store/compute-nodes/compute-nodes-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(removeComputeNode(props.data.uuid));
+ }
+});
+
+export const RemoveComputeNodeDialog = compose(
+ withDialog(COMPUTE_NODE_REMOVE_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { openComputeNodeRemoveDialog, openComputeNodeAttributesDialog } from '~/store/compute-nodes/compute-nodes-actions';
+import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon";
+
+export const computeNodeActionSet: ContextMenuActionSet = [[{
+ name: "Attributes",
+ icon: AttributesIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openComputeNodeAttributesDialog(uuid));
+ }
+}, {
+ name: "Advanced",
+ icon: AdvancedIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openAdvancedTabDialog(uuid));
+ }
+}, {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openComputeNodeRemoveDialog(uuid));
+ }
+}]];
REPOSITORY = "Repository",
SSH_KEY = "SshKey",
VIRTUAL_MACHINE = "VirtualMachine",
- KEEP_SERVICE = "KeepService"
+ KEEP_SERVICE = "KeepService",
+ NODE = "Node"
}
import { RootState } from "~/store/store";
import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions';
import { openRepositoriesPanel } from "~/store/repositories/repositories-actions";
-import { navigateToSshKeys, navigateToKeepServices } from '~/store/navigation/navigation-action';
+import { navigateToSshKeys, navigateToKeepServices, navigateToComputeNodes } from '~/store/navigation/navigation-action';
import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
interface AccountMenuProps {
<MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
<MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
{ user.isAdmin && <MenuItem onClick={() => dispatch(navigateToKeepServices)}>Keep Services</MenuItem> }
+ { user.isAdmin && <MenuItem onClick={() => dispatch(navigateToComputeNodes)}>Compute Nodes</MenuItem> }
<MenuItem>My account</MenuItem>
<MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
</DropdownMenu>
import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
-import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute, matchKeepServicesRoute } from '~/routes/routes';
+import * as Routes from '~/routes/routes';
import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
interface MainContentBarProps {
const isButtonVisible = ({ router }: RootState) => {
const pathname = router.location ? router.location.pathname : '';
- return !matchWorkflowRoute(pathname) && !matchVirtualMachineRoute(pathname) &&
- !matchRepositoriesRoute(pathname) && !matchSshKeysRoute(pathname) && !matchKeepServicesRoute(pathname);
+ return !Routes.matchWorkflowRoute(pathname) && !Routes.matchVirtualMachineRoute(pathname) &&
+ !Routes.matchRepositoriesRoute(pathname) && !Routes.matchSshKeysRoute(pathname) &&
+ !Routes.matchKeepServicesRoute(pathname) && !Routes.matchComputeNodesRoute(pathname);
};
export const MainContentBar = connect((state: RootState) => ({
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Table,
+ TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton
+} from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { MoreOptionsIcon } from '~/components/icon/icon';
+import { NodeResource } from '~/models/node';
+import { formatDate } from '~/common/formatters';
+
+type CssRules = 'root' | 'tableRow';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ overflow: 'auto'
+ },
+ tableRow: {
+ '& td, th': {
+ whiteSpace: 'nowrap'
+ }
+ }
+});
+
+export interface ComputeNodePanelRootActionProps {
+ openRowOptions: (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) => void;
+}
+
+export interface ComputeNodePanelRootDataProps {
+ computeNodes: NodeResource[];
+ hasComputeNodes: boolean;
+}
+
+type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps & WithStyles<CssRules>;
+
+export const ComputeNodePanelRoot = withStyles(styles)(
+ ({ classes, hasComputeNodes, computeNodes, openRowOptions }: ComputeNodePanelRootProps) =>
+ <Card className={classes.root}>
+ <CardContent>
+ {hasComputeNodes && <Grid container direction="row">
+ <Grid item xs={12}>
+ <Table>
+ <TableHead>
+ <TableRow className={classes.tableRow}>
+ <TableCell>Info</TableCell>
+ <TableCell>UUID</TableCell>
+ <TableCell>Domain</TableCell>
+ <TableCell>First ping at</TableCell>
+ <TableCell>Hostname</TableCell>
+ <TableCell>IP Address</TableCell>
+ <TableCell>Job</TableCell>
+ <TableCell>Last ping at</TableCell>
+ <TableCell />
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {computeNodes.map((computeNode, index) =>
+ <TableRow key={index} className={classes.tableRow}>
+ <TableCell>{computeNode.uuid}</TableCell>
+ <TableCell>{computeNode.uuid}</TableCell>
+ <TableCell>{computeNode.domain}</TableCell>
+ <TableCell>{formatDate(computeNode.firstPingAt) || '(none)'}</TableCell>
+ <TableCell>{computeNode.hostname || '(none)'}</TableCell>
+ <TableCell>{computeNode.ipAddress || '(none)'}</TableCell>
+ <TableCell>{computeNode.jobUuid || '(none)'}</TableCell>
+ <TableCell>{formatDate(computeNode.lastPingAt) || '(none)'}</TableCell>
+ <TableCell>
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton onClick={event => openRowOptions(event, computeNode)}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </Tooltip>
+ </TableCell>
+ </TableRow>)}
+ </TableBody>
+ </Table>
+ </Grid>
+ </Grid>}
+ </CardContent>
+ </Card>
+);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { } from '~/store/compute-nodes/compute-nodes-actions';
+import {
+ ComputeNodePanelRoot,
+ ComputeNodePanelRootDataProps,
+ ComputeNodePanelRootActionProps
+} from '~/views/compute-node-panel/compute-node-panel-root';
+import { openComputeNodeContextMenu } from '~/store/context-menu/context-menu-actions';
+
+const mapStateToProps = (state: RootState): ComputeNodePanelRootDataProps => {
+ return {
+ computeNodes: state.computeNodes,
+ hasComputeNodes: state.computeNodes.length > 0
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ComputeNodePanelRootActionProps => ({
+ openRowOptions: (event, computeNode) => {
+ dispatch<any>(openComputeNodeContextMenu(event, computeNode));
+ }
+});
+
+export const ComputeNodePanel = connect(mapStateToProps, mapDispatchToProps)(ComputeNodePanelRoot);
\ No newline at end of file
import { RootState } from '~/store/store';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
-import { } from '~/store/keep-services/keep-services-actions';
import {
KeepServicePanelRoot,
KeepServicePanelRootDataProps,
import { ProjectPropertiesDialog } from '~/views-components/project-properties-dialog/project-properties-dialog';
import { RepositoriesPanel } from '~/views/repositories-panel/repositories-panel';
import { KeepServicePanel } from '~/views/keep-service-panel/keep-service-panel';
+import { ComputeNodePanel } from '~/views/compute-node-panel/compute-node-panel';
import { RepositoriesSampleGitDialog } from '~/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog';
import { RepositoryAttributesDialog } from '~/views-components/repository-attributes-dialog/repository-attributes-dialog';
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 { RemoveComputeNodeDialog } from '~/views-components/compute-nodes-dialog/remove-dialog';
import { RemoveKeepServiceDialog } from '~/views-components/keep-services-dialog/remove-dialog';
import { RemoveSshKeyDialog } from '~/views-components/ssh-keys-dialog/remove-dialog';
+import { RemoveVirtualMachineDialog } from '~/views-components/virtual-machines-dialog/remove-dialog';
+import { AttributesComputeNodeDialog } from '~/views-components/compute-nodes-dialog/attributes-dialog';
import { AttributesKeepServiceDialog } from '~/views-components/keep-services-dialog/attributes-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';
<Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
<Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
<Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
+ <Route path={Routes.COMPUTE_NODES} component={ComputeNodePanel} />
</Switch>
</Grid>
</Grid>
<DetailsPanel />
</Grid>
<AdvancedTabDialog />
+ <AttributesComputeNodeDialog />
<AttributesKeepServiceDialog />
<AttributesSshKeyDialog />
<ChangeWorkflowDialog />
<ProcessCommandDialog />
<ProcessInputDialog />
<ProjectPropertiesDialog />
+ <RemoveComputeNodeDialog />
<RemoveKeepServiceDialog />
<RemoveProcessDialog />
<RemoveRepositoryDialog />
declare var System: System;
declare module 'react-splitter-layout';
-declare module 'react-rte';
\ No newline at end of file
+declare module 'react-rte';
+
+declare module 'is-image' {
+ export default function isImage(value: string): boolean;
+}
\ No newline at end of file
dependencies:
minimatch "^3.0.4"
+image-extensions@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/image-extensions/-/image-extensions-1.1.0.tgz#b8e6bf6039df0056e333502a00b6637a3105d894"
+ integrity sha1-uOa/YDnfAFbjM1AqALZjejEF2JQ=
+
immutable@^3.8.1:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
dependencies:
is-extglob "^2.1.1"
+is-image@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-image/-/is-image-2.0.0.tgz#454c9569578de31869371fbfaea4958f461b3e0c"
+ integrity sha1-RUyVaVeN4xhpNx+/rqSVj0YbPgw=
+ dependencies:
+ image-extensions "^1.0.1"
+
is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"