[ARVADOS-WORKBENCH2] created: 1.3.0-17-ge032ee6
Git user
git at public.curoverse.com
Wed Dec 5 06:31:24 EST 2018
at e032ee615c409d084d84e35ae6d572999105dd22 (commit)
commit e032ee615c409d084d84e35ae6d572999105dd22
Author: Janicki Artur <artur.janicki at contractors.roche.com>
Date: Wed Dec 5 12:31:06 2018 +0100
add compute nodes with store, service and all dialogs
Feature #14502_admin_compute_nodes
Arvados-DCO-1.1-Signed-off-by: Janicki Artur <artur.janicki at contractors.roche.com>
diff --git a/src/index.tsx b/src/index.tsx
index d838596..fbd6c9a 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -53,6 +53,7 @@ import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh
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()}]`);
@@ -73,6 +74,7 @@ addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet);
addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet);
+addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet);
fetchConfig()
.then(({ config, apiHost }) => {
diff --git a/src/models/node.ts b/src/models/node.ts
new file mode 100644
index 0000000..8723811
--- /dev/null
+++ b/src/models/node.ts
@@ -0,0 +1,37 @@
+// 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
diff --git a/src/models/resource.ts b/src/models/resource.ts
index ee90174..4d2d92e 100644
--- a/src/models/resource.ts
+++ b/src/models/resource.ts
@@ -26,6 +26,7 @@ export enum ResourceKind {
CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
LOG = "arvados#log",
+ NODE = "arvados#node",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
REPOSITORY = "arvados#repository",
@@ -48,7 +49,8 @@ export enum ResourceObjectType {
VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
SSH_KEY = 'fngyi',
- KEEP_SERVICE = 'bi6l4'
+ KEEP_SERVICE = 'bi6l4',
+ NODE = '7ekkf'
}
export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
@@ -89,6 +91,8 @@ export const extractUuidKind = (uuid: string = '') => {
return ResourceKind.SSH_KEY;
case ResourceObjectType.KEEP_SERVICE:
return ResourceKind.KEEP_SERVICE;
+ case ResourceObjectType.NODE:
+ return ResourceKind.NODE;
default:
return undefined;
}
diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index fdc4211..f2304ac 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -4,17 +4,8 @@
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) => {
@@ -24,51 +15,54 @@ 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);
}
};
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 5cd3e55..8f8fa06 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -23,7 +23,8 @@ export const Routes = {
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) => {
@@ -92,3 +93,6 @@ export const matchSshKeysRoute = (route: 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
diff --git a/src/services/node-service/node-service.ts b/src/services/node-service/node-service.ts
new file mode 100644
index 0000000..97f2264
--- /dev/null
+++ b/src/services/node-service/node-service.ts
@@ -0,0 +1,14 @@
+// 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
diff --git a/src/services/services.ts b/src/services/services.ts
index b24b1d9..d524405 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -28,6 +28,7 @@ import { VirtualMachinesService } from "~/services/virtual-machines-service/virt
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>;
@@ -45,6 +46,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
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);
@@ -75,6 +77,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
keepService,
linkService,
logService,
+ nodeService,
permissionService,
projectService,
repositoriesService,
diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts
index 92d14d7..6b20f8b 100644
--- a/src/store/advanced-tab/advanced-tab.ts
+++ b/src/store/advanced-tab/advanced-tab.ts
@@ -21,6 +21,7 @@ import { UserResource } from '~/models/user';
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';
@@ -72,7 +73,8 @@ enum ResourcePrefix {
REPOSITORIES = 'repositories',
AUTORIZED_KEYS = 'authorized_keys',
VIRTUAL_MACHINES = 'virtual_machines',
- KEEP_SERVICES = 'keep_services'
+ KEEP_SERVICES = 'keep_services',
+ COMPUTE_NODES = 'nodes'
}
enum KeepServiceData {
@@ -80,9 +82,14 @@ 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) => {
@@ -193,6 +200,21 @@ export const openAdvancedTabDialog = (uuid: string) =>
});
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 }));
}
@@ -269,7 +291,7 @@ const cliUpdateHeader = (resourceKind: string, resourceName: string) =>
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;
};
@@ -284,7 +306,7 @@ const curlExample = (uuid: string, resourcePrefix: string, resource: string | st
https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
<<EOF
{
- "${resourceName}": ${resource}
+ "${resourceName}": ${JSON.stringify(resource, null, 4)}
}
EOF`;
@@ -441,4 +463,29 @@ const keepServiceApiResponse = (apiResponse: KeepServiceResource) => {
"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
diff --git a/src/store/compute-nodes/compute-nodes-actions.ts b/src/store/compute-nodes/compute-nodes-actions.ts
new file mode 100644
index 0000000..659b1e8
--- /dev/null
+++ b/src/store/compute-nodes/compute-nodes-actions.ts
@@ -0,0 +1,71 @@
+// 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
diff --git a/src/store/compute-nodes/compute-nodes-reducer.ts b/src/store/compute-nodes/compute-nodes-reducer.ts
new file mode 100644
index 0000000..44a3780
--- /dev/null
+++ b/src/store/compute-nodes/compute-nodes-reducer.ts
@@ -0,0 +1,17 @@
+// 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
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index d56a3fb..65ddcff 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -17,6 +17,7 @@ import { RepositoryResource } from '~/models/repositories';
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 }>(),
@@ -109,6 +110,17 @@ export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>,
}));
};
+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);
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index d452710..50cfd88 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -68,4 +68,6 @@ export const navigateToRepositories = push(Routes.REPOSITORIES);
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
diff --git a/src/store/store.ts b/src/store/store.ts
index f8bdcc2..321a19b 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -46,6 +46,7 @@ import { resourcesDataReducer } from "~/store/resources-data/resources-data-redu
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' &&
@@ -117,5 +118,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
searchBar: searchBarReducer,
virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer,
- keepServices: keepServicesReducer
+ keepServices: keepServicesReducer,
+ computeNodes: computeNodesReducer
});
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 667f1c8..e3f96a9 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -57,6 +57,7 @@ import { searchResultsPanelColumns } from '~/views/search-results-panel/search-r
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';
@@ -416,6 +417,11 @@ export const loadKeepServices = handleFirstTimeLoad(
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;
diff --git a/src/views-components/compute-nodes-dialog/attributes-dialog.tsx b/src/views-components/compute-nodes-dialog/attributes-dialog.tsx
new file mode 100644
index 0000000..3959909
--- /dev/null
+++ b/src/views-components/compute-nodes-dialog/attributes-dialog.tsx
@@ -0,0 +1,115 @@
+// 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
diff --git a/src/views-components/compute-nodes-dialog/remove-dialog.tsx b/src/views-components/compute-nodes-dialog/remove-dialog.tsx
new file mode 100644
index 0000000..2233974
--- /dev/null
+++ b/src/views-components/compute-nodes-dialog/remove-dialog.tsx
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { 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
diff --git a/src/views-components/context-menu/action-sets/compute-node-action-set.ts b/src/views-components/context-menu/action-sets/compute-node-action-set.ts
new file mode 100644
index 0000000..cfb90b6
--- /dev/null
+++ b/src/views-components/context-menu/action-sets/compute-node-action-set.ts
@@ -0,0 +1,28 @@
+// 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));
+ }
+}]];
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index 5f321bf..3fa1ab3 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -72,5 +72,6 @@ export enum ContextMenuKind {
REPOSITORY = "Repository",
SSH_KEY = "SshKey",
VIRTUAL_MACHINE = "VirtualMachine",
- KEEP_SERVICE = "KeepService"
+ KEEP_SERVICE = "KeepService",
+ NODE = "Node"
}
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index 075aa69..f4232a1 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -12,7 +12,7 @@ import { logout } from '~/store/auth/auth-action';
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 {
@@ -38,6 +38,7 @@ export const AccountMenu = connect(mapStateToProps)(
<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>
diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx
index 66d7cab..78b79a8 100644
--- a/src/views-components/main-content-bar/main-content-bar.tsx
+++ b/src/views-components/main-content-bar/main-content-bar.tsx
@@ -8,7 +8,7 @@ import { DetailsIcon } from "~/components/icon/icon";
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 {
@@ -18,8 +18,9 @@ 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) => ({
diff --git a/src/views/compute-node-panel/compute-node-panel-root.tsx b/src/views/compute-node-panel/compute-node-panel-root.tsx
new file mode 100644
index 0000000..be3627b
--- /dev/null
+++ b/src/views/compute-node-panel/compute-node-panel-root.tsx
@@ -0,0 +1,85 @@
+// 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
diff --git a/src/views/compute-node-panel/compute-node-panel.tsx b/src/views/compute-node-panel/compute-node-panel.tsx
new file mode 100644
index 0000000..a4f22c8
--- /dev/null
+++ b/src/views/compute-node-panel/compute-node-panel.tsx
@@ -0,0 +1,29 @@
+// 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
diff --git a/src/views/keep-service-panel/keep-service-panel.tsx b/src/views/keep-service-panel/keep-service-panel.tsx
index a11cee0..369b7c2 100644
--- a/src/views/keep-service-panel/keep-service-panel.tsx
+++ b/src/views/keep-service-panel/keep-service-panel.tsx
@@ -5,7 +5,6 @@
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,
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 2d17fad..92c2438 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -52,18 +52,21 @@ import { VirtualMachinePanel } from '~/views/virtual-machine-panel/virtual-machi
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';
@@ -137,6 +140,7 @@ export const WorkbenchPanel =
<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>
@@ -146,6 +150,7 @@ export const WorkbenchPanel =
<DetailsPanel />
</Grid>
<AdvancedTabDialog />
+ <AttributesComputeNodeDialog />
<AttributesKeepServiceDialog />
<AttributesSshKeyDialog />
<ChangeWorkflowDialog />
@@ -168,6 +173,7 @@ export const WorkbenchPanel =
<ProcessCommandDialog />
<ProcessInputDialog />
<ProjectPropertiesDialog />
+ <RemoveComputeNodeDialog />
<RemoveKeepServiceDialog />
<RemoveProcessDialog />
<RemoveRepositoryDialog />
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list