refs #14280 Merge branch 'origin/14280-query-language'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Thu, 6 Dec 2018 07:47:00 +0000 (08:47 +0100)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Thu, 6 Dec 2018 07:47:21 +0000 (08:47 +0100)
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

59 files changed:
README.md
package.json
public/vocabulary-example.json [new file with mode: 0644]
src/common/config.ts
src/components/file-tree/file-thumbnail.tsx [new file with mode: 0644]
src/components/file-tree/file-tree-data.ts
src/components/file-tree/file-tree-item.tsx
src/index.tsx
src/models/keep-services.ts [moved from src/models/keep.ts with 61% similarity]
src/models/node.ts [new file with mode: 0644]
src/models/resource.ts
src/models/virtual-machines.ts
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/auth-service/auth-service.ts
src/services/keep-service/keep-service.ts
src/services/node-service/node-service.ts [new file with mode: 0644]
src/services/services.ts
src/store/advanced-tab/advanced-tab.ts
src/store/auth/auth-action.ts
src/store/auth/auth-actions.test.ts
src/store/compute-nodes/compute-nodes-actions.ts [new file with mode: 0644]
src/store/compute-nodes/compute-nodes-reducer.ts [new file with mode: 0644]
src/store/context-menu/context-menu-actions.ts
src/store/keep-services/keep-services-actions.ts [new file with mode: 0644]
src/store/keep-services/keep-services-reducer.ts [new file with mode: 0644]
src/store/navigation/navigation-action.ts
src/store/repositories/repositories-actions.ts
src/store/store.ts
src/store/virtual-machines/virtual-machines-actions.ts
src/store/virtual-machines/virtual-machines-reducer.ts
src/store/workbench/workbench-actions.ts
src/views-components/collection-panel-files/collection-panel-files.ts
src/views-components/compute-nodes-dialog/attributes-dialog.tsx [new file with mode: 0644]
src/views-components/compute-nodes-dialog/remove-dialog.tsx [new file with mode: 0644]
src/views-components/context-menu/action-sets/compute-node-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/keep-service-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/repository-action-set.ts
src/views-components/context-menu/action-sets/ssh-key-action-set.ts
src/views-components/context-menu/action-sets/virtual-machine-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/keep-services-dialog/attributes-dialog.tsx [new file with mode: 0644]
src/views-components/keep-services-dialog/remove-dialog.tsx [new file with mode: 0644]
src/views-components/main-app-bar/account-menu.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views-components/repository-remove-dialog/repository-remove-dialog.ts
src/views-components/virtual-machines-dialog/attributes-dialog.tsx [new file with mode: 0644]
src/views-components/virtual-machines-dialog/remove-dialog.tsx [new file with mode: 0644]
src/views/compute-node-panel/compute-node-panel-root.tsx [new file with mode: 0644]
src/views/compute-node-panel/compute-node-panel.tsx [new file with mode: 0644]
src/views/keep-service-panel/keep-service-panel-root.tsx [new file with mode: 0644]
src/views/keep-service-panel/keep-service-panel.tsx [new file with mode: 0644]
src/views/repositories-panel/repositories-panel.tsx
src/views/ssh-key-panel/ssh-key-panel-root.tsx
src/views/ssh-key-panel/ssh-key-panel.tsx
src/views/virtual-machine-panel/virtual-machine-panel.tsx
src/views/workbench/workbench.tsx
typings/global.d.ts
yarn.lock

index ea9bc02fc1f0f038b016f5fd8f5ade2ab38ad2f9..e8d77701107179a6ef88fcfb68cd47d857a321a9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -45,6 +45,10 @@ Currently this configuration schema is supported:
 }
 ```
 
+#### VOCABULARY_URL
+Local path, or any URL that allows cross-origin requests. See 
+[Vocabulary JSON file example](public/vocabulary-example.json).
+
 ### Licensing
 
 Arvados is Free Software. See COPYING for information about Arvados Free
index 1c066db8c1776fcf3238bf0a8e06190851b9e224..1332630471dccf4c6baf093905c74cd525880bfc 100644 (file)
@@ -19,6 +19,7 @@
     "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",
diff --git a/public/vocabulary-example.json b/public/vocabulary-example.json
new file mode 100644 (file)
index 0000000..b227dc2
--- /dev/null
@@ -0,0 +1,32 @@
+{
+    "strict": false,
+    "tags": {
+        "fruit": {
+            "values": ["pineapple", "tomato", "orange", "banana", "advocado", "lemon", "apple", "peach", "strawberry"],
+            "strict": true
+        },
+        "animal": {
+            "values": ["human", "dog", "elephant", "eagle"],
+            "strict": false
+        },
+        "color": {
+            "values": ["yellow", "red", "magenta", "green"],
+            "strict": false
+        },
+        "text": {},
+        "category": {
+            "values": ["experimental", "development", "production"]
+        },
+        "comments": {},
+        "importance": {
+            "values": ["critical", "important", "low priority"]
+        },
+        "size": {
+            "values": ["x-small", "small", "medium", "large", "x-large"]
+        },
+        "country": {
+            "values": ["Afghanistan","Ă…land Islands","Albania","Algeria","American Samoa","AndorrA","Angola","Anguilla","Antarctica","Antigua and Barbuda","Argentina","Armenia","Aruba","Australia","Austria","Azerbaijan","Bahamas","Bahrain","Bangladesh","Barbados","Belarus","Belgium","Belize","Benin","Bermuda","Bhutan","Bolivia","Bosnia and Herzegovina","Botswana","Bouvet Island","Brazil","British Indian Ocean Territory","Brunei Darussalam","Bulgaria","Burkina Faso","Burundi","Cambodia","Cameroon","Canada","Cape Verde","Cayman Islands","Central African Republic","Chad","Chile","China","Christmas Island","Cocos (Keeling) Islands","Colombia","Comoros","Congo","Congo, The Democratic Republic of the","Cook Islands","Costa Rica","Cote D'Ivoire","Croatia","Cuba","Cyprus","Czech Republic","Denmark","Djibouti","Dominica","Dominican Republic","Ecuador","Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Ethiopia","Falkland Islands (Malvinas)","Faroe Islands","Fiji","Finland","France","French Guiana","French Polynesia","French Southern Territories","Gabon","Gambia","Georgia","Germany","Ghana","Gibraltar","Greece","Greenland","Grenada","Guadeloupe","Guam","Guatemala","Guernsey","Guinea","Guinea-Bissau","Guyana","Haiti","Heard Island and Mcdonald Islands","Holy See (Vatican City State)","Honduras","Hong Kong","Hungary","Iceland","India","Indonesia","Iran, Islamic Republic Of","Iraq","Ireland","Isle of Man","Israel","Italy","Jamaica","Japan","Jersey","Jordan","Kazakhstan","Kenya","Kiribati","Korea, Democratic People'S Republic of","Korea, Republic of","Kuwait","Kyrgyzstan","Lao People'S Democratic Republic","Latvia","Lebanon","Lesotho","Liberia","Libyan Arab Jamahiriya","Liechtenstein","Lithuania","Luxembourg","Macao","Macedonia, The Former Yugoslav Republic of","Madagascar","Malawi","Malaysia","Maldives","Mali","Malta","Marshall Islands","Martinique","Mauritania","Mauritius","Mayotte","Mexico","Micronesia, Federated States of","Moldova, Republic of","Monaco","Mongolia","Montserrat","Morocco","Mozambique","Myanmar","Namibia","Nauru","Nepal","Netherlands","Netherlands Antilles","New Caledonia","New Zealand","Nicaragua","Niger","Nigeria","Niue","Norfolk Island","Northern Mariana Islands","Norway","Oman","Pakistan","Palau","Palestinian Territory, Occupied","Panama","Papua New Guinea","Paraguay","Peru","Philippines","Pitcairn","Poland","Portugal","Puerto Rico","Qatar","Reunion","Romania","Russian Federation","RWANDA","Saint Helena","Saint Kitts and Nevis","Saint Lucia","Saint Pierre and Miquelon","Saint Vincent and the Grenadines","Samoa","San Marino","Sao Tome and Principe","Saudi Arabia","Senegal","Serbia and Montenegro","Seychelles","Sierra Leone","Singapore","Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Georgia and the South Sandwich Islands","Spain","Sri Lanka","Sudan","Suriname","Svalbard and Jan Mayen","Swaziland","Sweden","Switzerland","Syrian Arab Republic","Taiwan, Province of China","Tajikistan","Tanzania, United Republic of","Thailand","Timor-Leste","Togo","Tokelau","Tonga","Trinidad and Tobago","Tunisia","Turkey","Turkmenistan","Turks and Caicos Islands","Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States","United States Minor Outlying Islands","Uruguay","Uzbekistan","Vanuatu","Venezuela","Viet Nam","Virgin Islands, British","Virgin Islands, U.S.","Wallis and Futuna","Western Sahara","Yemen","Zambia","Zimbabwe"],
+            "strict": true
+        }
+    }
+}
\ No newline at end of file
index c74277e42cc1ff78a9499e688f9f3bc171ca2835..b7b89bd9e4930100188725b7f117f67abf076587 100644 (file)
@@ -60,7 +60,8 @@ export const fetchConfig = () => {
         .then(config => Axios
             .get<Config>(getDiscoveryURL(config.API_HOST))
             .then(response => ({ 
-                config: {...response.data, vocabularyUrl: config.VOCABULARY_URL }, 
+                // TODO: After tests delete `|| '/vocabulary-example.json'`
+                config: {...response.data, vocabularyUrl: config.VOCABULARY_URL || '/vocabulary-example.json' }, 
                 apiHost: config.API_HOST, 
             })));
 
diff --git a/src/components/file-tree/file-thumbnail.tsx b/src/components/file-tree/file-thumbnail.tsx
new file mode 100644 (file)
index 0000000..e1a0d5e
--- /dev/null
@@ -0,0 +1,36 @@
+// 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} />
+);
index 4be4ace854ec08efe52561b2780708042ca514a2..4154611355566862de617abd65e562d4275f18d3 100644 (file)
@@ -5,5 +5,6 @@
 export interface FileTreeData {
     name: string;
     type: string;
+    url: string;
     size?: number;
 }
index 89bf43c66d84d3c6f9f157adb3e84812866ddde2..0e8c92e2da6c2343f589696fa999d7e7f9fce49a 100644 (file)
@@ -9,6 +9,7 @@ import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles, Too
 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";
 
@@ -42,22 +43,25 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
     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>) => {
index 801a56a1d382a3ac589af3bb732a085190567b1c..fbd6c9a88e1bb8ac18d66bd3ff4c681f38ab89ee 100644 (file)
@@ -50,7 +50,10 @@ 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';
+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()}]`);
 
@@ -69,6 +72,9 @@ addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 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 }) => {
similarity index 61%
rename from src/models/keep.ts
rename to src/models/keep-services.ts
index f6b5ef2a9b42c82209b2a8562d6da2a15bc71f99..d99943c6555b8e1d4eaa5680b598a40e8e3cf7c1 100644 (file)
@@ -1,12 +1,13 @@
-// Copyright (C) The Arvados Authors. All rights reserved.\r
-//\r
-// SPDX-License-Identifier: AGPL-3.0\r
-\r
-import { Resource } from "./resource";\r
-\r
-export interface KeepResource extends Resource {\r
-    serviceHost: string;\r
-    servicePort: number;\r
-    serviceSslFlag: boolean;\r
-    serviceType: string;\r
-}\r
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from '~/models/resource';
+
+export interface KeepServiceResource extends Resource {
+    serviceHost: string;
+    servicePort: number;
+    serviceSslFlag: boolean;
+    serviceType: string;
+    readOnly: boolean;
+}
\ No newline at end of file
diff --git a/src/models/node.ts b/src/models/node.ts
new file mode 100644 (file)
index 0000000..8723811
--- /dev/null
@@ -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
index 7e2127b2813dad8076881b8fc69a03bb82184633..4d2d92e0155b183763b1445865420ffe84a558bf 100644 (file)
@@ -26,10 +26,12 @@ 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",
     SSH_KEY = "arvados#authorizedKeys",
+    KEEP_SERVICE = "arvados#keepService",
     USER = "arvados#user",
     VIRTUAL_MACHINE = "arvados#virtualMachine",
     WORKFLOW = "arvados#workflow",
@@ -46,7 +48,9 @@ export enum ResourceObjectType {
     USER = 'tpzed',
     VIRTUAL_MACHINE = '2x53u',
     WORKFLOW = '7fd4e',
-    SSH_KEY = 'fngyi'
+    SSH_KEY = 'fngyi',
+    KEEP_SERVICE = 'bi6l4',
+    NODE = '7ekkf'
 }
 
 export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
@@ -85,6 +89,10 @@ export const extractUuidKind = (uuid: string = '') => {
             return ResourceKind.REPOSITORY;
         case ResourceObjectType.SSH_KEY:
             return ResourceKind.SSH_KEY;
+        case ResourceObjectType.KEEP_SERVICE:
+            return ResourceKind.KEEP_SERVICE;
+        case ResourceObjectType.NODE:
+            return ResourceKind.NODE;
         default:
             return undefined;
     }
index 0652c35017e4626289b08569b3979cf795cc6ee1..85d0a565c07bf18ab34b67386008357be07d3a91 100644 (file)
@@ -8,11 +8,16 @@ export interface VirtualMachinesResource extends Resource {
     hostname: string;
 }
 
-export interface VirtualMachinesLoginsResource {
+export interface VirtualMachinesLoginsItems {
     hostname: string;
     username: string;
     public_key: string;
-    user_uuid: string;
-    virtual_machine_uuid: string;
-    authorized_key_uuid: string;
+    userUuid: string;
+    virtualMachineUuid: string;
+    authorizedKeyUuid: string;
+}
+
+export interface VirtualMachineLogins {
+    kind: string;
+    items: VirtualMachinesLoginsItems[];
 }
\ No newline at end of file
index 22d0b7c711364d2a466b8b0af5e2b7d465cb73aa..f2304acaa77d2b8d0b69789cafa93626446b47e2 100644 (file)
@@ -4,10 +4,9 @@
 
 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 } from './routes';
-import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadVirtualMachines } 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';
-import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -16,48 +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 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(WorkbenchActions.loadKeepServices);
+    } else if (computeNodesMatch) {
+        store.dispatch(WorkbenchActions.loadComputeNodes);
     }
 };
index 71cdfdacad218da9ea51cf636ffacd2cf95916ad..8f8fa06bd0379232d6da87ea6a47dad5673b811d 100644 (file)
@@ -22,7 +22,9 @@ export const Routes = {
     VIRTUAL_MACHINES: '/virtual-machines',
     WORKFLOWS: '/workflows',
     SEARCH_RESULTS: '/search-results',
-    SSH_KEYS: `/ssh-keys`
+    SSH_KEYS: `/ssh-keys`,
+    KEEP_SERVICES: `/keep-services`,
+    COMPUTE_NODES: `/nodes`
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -88,3 +90,9 @@ export const matchRepositoriesRoute = (route: string) =>
     
 export const matchSshKeysRoute = (route: string) =>
     matchPath(route, { path: Routes.SSH_KEYS });
+
+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
index edc6e24fce752b628a7573899f21af2e6063073b..98c0321598090b11002375823fba7ffa1a374aee 100644 (file)
@@ -4,7 +4,7 @@
 
 import { User } from "~/models/user";
 import { AxiosInstance } from "axios";
-import { ApiActions, ProgressFn } from "~/services/api/api-actions";
+import { ApiActions } from "~/services/api/api-actions";
 import * as uuid from "uuid/v4";
 
 export const API_TOKEN_KEY = 'apiToken';
@@ -52,7 +52,7 @@ export class AuthService {
     }
 
     public getIsAdmin(): boolean {
-        return !!localStorage.getItem(USER_IS_ADMIN);
+        return localStorage.getItem(USER_IS_ADMIN) === 'true';
     }
 
     public getUser(): User | undefined {
@@ -61,7 +61,7 @@ export class AuthService {
         const lastName = localStorage.getItem(USER_LAST_NAME_KEY);
         const uuid = this.getUuid();
         const ownerUuid = this.getOwnerUuid();
-        const isAdmin = this.getIsAdmin();
+        const isAdmin = this.getIsAdmin();   
 
         return email && firstName && lastName && uuid && ownerUuid
             ? { email, firstName, lastName, uuid, ownerUuid, isAdmin }
index 17ee522e4dea00d7c6ecea813a3111a43f64fb4a..5a89ba57b49652a26d086620d4ca8e8bbac74ec5 100644 (file)
@@ -4,11 +4,11 @@
 \r
 import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
 import { AxiosInstance } from "axios";\r
-import { KeepResource } from "~/models/keep";\r
+import { KeepServiceResource } from "~/models/keep-services";\r
 import { ApiActions } from "~/services/api/api-actions";\r
 \r
-export class KeepService extends CommonResourceService<KeepResource> {\r
+export class KeepService extends CommonResourceService<KeepServiceResource> {\r
     constructor(serverApi: AxiosInstance, actions: ApiActions) {\r
         super(serverApi, "keep_services", actions);\r
     }\r
-}\r
+}
\ 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 (file)
index 0000000..97f2264
--- /dev/null
@@ -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
index b24b1d99a181a8086e9da06bd41684cd5969a907..d524405fe62000a889d980520e00ef0b9e771e79 100644 (file)
@@ -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,
index b3c5164c5561e8d3e8104b6a4d700e9c904a21df..6b20f8b328a2ac555552031d5732834a6912fdc8 100644 (file)
@@ -16,13 +16,19 @@ import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from '~/services/api/filter-builder';
 import { RepositoryResource } from '~/models/repositories';
 import { SshKeyResource } from '~/models/ssh-key';
+import { VirtualMachinesResource } from '~/models/virtual-machines';
+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';
 
 interface AdvancedTabDialogData {
     apiResponse: any;
-    metadata: any;
-    user: string;
+    metadata: ListResults<LinkResource> | string;
+    user: UserResource | string;
     pythonHeader: string;
     pythonExample: string;
     cliGetHeader: string;
@@ -54,42 +60,161 @@ enum RepositoryData {
 }
 
 enum SshKeyData {
-    SSH_KEY = 'authorized_keys',
+    SSH_KEY = 'authorized_key',
     CREATED_AT = 'created_at'
 }
 
-type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData;
-type AdvanceResourcePrefix = GroupContentsResourcePrefix | 'repositories' | 'authorized_keys';
+enum VirtualMachineData {
+    VIRTUAL_MACHINE = 'virtual_machine',
+    CREATED_AT = 'created_at'
+}
+
+enum ResourcePrefix {
+    REPOSITORIES = 'repositories',
+    AUTORIZED_KEYS = 'authorized_keys',
+    VIRTUAL_MACHINES = 'virtual_machines',
+    KEEP_SERVICES = 'keep_services',
+    COMPUTE_NODES = 'nodes'
+}
+
+enum KeepServiceData {
+    KEEP_SERVICE = 'keep_services',
+    CREATED_AT = 'created_at'
+}
+
+enum ComputeNodeData {
+    COMPUTE_NODE = 'node',
+    PROPERTIES = 'properties'
+}
 
-export const openAdvancedTabDialog = (uuid: string, index?: number) =>
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData;
+type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix;
+type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | undefined;
+
+export const openAdvancedTabDialog = (uuid: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const kind = extractUuidKind(uuid);
         switch (kind) {
             case ResourceKind.COLLECTION:
                 const { data: dataCollection, metadata: metaCollection, user: userCollection } = await dispatch<any>(getDataForAdvancedTab(uuid));
-                const advanceDataCollection: AdvancedTabDialogData = advancedTabData(uuid, metaCollection, userCollection, collectionApiResponse, dataCollection, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, dataCollection.storageClassesConfirmed);
+                const advanceDataCollection = advancedTabData({
+                    uuid,
+                    metadata: metaCollection,
+                    user: userCollection,
+                    apiResponseKind: collectionApiResponse,
+                    data: dataCollection,
+                    resourceKind: CollectionData.COLLECTION,
+                    resourcePrefix: GroupContentsResourcePrefix.COLLECTION,
+                    resourceKindProperty: CollectionData.STORAGE_CLASSES_CONFIRMED,
+                    property: dataCollection.storageClassesConfirmed
+                });
                 dispatch<any>(initAdvancedTabDialog(advanceDataCollection));
                 break;
             case ResourceKind.PROCESS:
                 const { data: dataProcess, metadata: metaProcess, user: userProcess } = await dispatch<any>(getDataForAdvancedTab(uuid));
-                const advancedDataProcess: AdvancedTabDialogData = advancedTabData(uuid, metaProcess, userProcess, containerRequestApiResponse, dataProcess, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, dataProcess.outputName);
+                const advancedDataProcess = advancedTabData({
+                    uuid,
+                    metadata: metaProcess,
+                    user: userProcess,
+                    apiResponseKind: containerRequestApiResponse,
+                    data: dataProcess,
+                    resourceKind: ProcessData.CONTAINER_REQUEST,
+                    resourcePrefix: GroupContentsResourcePrefix.PROCESS,
+                    resourceKindProperty: ProcessData.OUTPUT_NAME,
+                    property: dataProcess.outputName
+                });
                 dispatch<any>(initAdvancedTabDialog(advancedDataProcess));
                 break;
             case ResourceKind.PROJECT:
                 const { data: dataProject, metadata: metaProject, user: userProject } = await dispatch<any>(getDataForAdvancedTab(uuid));
-                const advanceDataProject: AdvancedTabDialogData = advancedTabData(uuid, metaProject, userProject, groupRequestApiResponse, dataProject, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, dataProject.deleteAt);
+                const advanceDataProject = advancedTabData({
+                    uuid,
+                    metadata: metaProject,
+                    user: userProject,
+                    apiResponseKind: groupRequestApiResponse,
+                    data: dataProject,
+                    resourceKind: ProjectData.GROUP,
+                    resourcePrefix: GroupContentsResourcePrefix.PROJECT,
+                    resourceKindProperty: ProjectData.DELETE_AT,
+                    property: dataProject.deleteAt
+                });
                 dispatch<any>(initAdvancedTabDialog(advanceDataProject));
                 break;
             case ResourceKind.REPOSITORY:
-                const dataRepository = getState().repositories.items[index!];
-                const advanceDataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, dataRepository, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, dataRepository.createdAt);
+                const dataRepository = getState().repositories.items.find(it => it.uuid === uuid);
+                const advanceDataRepository = advancedTabData({
+                    uuid,
+                    metadata: '',
+                    user: '',
+                    apiResponseKind: repositoryApiResponse,
+                    data: dataRepository,
+                    resourceKind: RepositoryData.REPOSITORY,
+                    resourcePrefix: ResourcePrefix.REPOSITORIES,
+                    resourceKindProperty: RepositoryData.CREATED_AT,
+                    property: dataRepository!.createdAt
+                });
                 dispatch<any>(initAdvancedTabDialog(advanceDataRepository));
                 break;
             case ResourceKind.SSH_KEY:
-                const dataSshKey = getState().auth.sshKeys[index!];
-                const advanceDataSshKey: AdvancedTabDialogData = advancedTabData(uuid, '', '', sshKeyApiResponse, dataSshKey, SshKeyData.SSH_KEY, 'authorized_keys', SshKeyData.CREATED_AT, dataSshKey.createdAt);
+                const dataSshKey = getState().auth.sshKeys.find(it => it.uuid === uuid);
+                const advanceDataSshKey = advancedTabData({
+                    uuid,
+                    metadata: '',
+                    user: '',
+                    apiResponseKind: sshKeyApiResponse,
+                    data: dataSshKey,
+                    resourceKind: SshKeyData.SSH_KEY,
+                    resourcePrefix: ResourcePrefix.AUTORIZED_KEYS,
+                    resourceKindProperty: SshKeyData.CREATED_AT,
+                    property: dataSshKey!.createdAt
+                });
                 dispatch<any>(initAdvancedTabDialog(advanceDataSshKey));
                 break;
+            case ResourceKind.VIRTUAL_MACHINE:
+                const dataVirtualMachine = getState().virtualMachines.virtualMachines.items.find(it => it.uuid === uuid);
+                const advanceDataVirtualMachine = advancedTabData({
+                    uuid,
+                    metadata: '',
+                    user: '',
+                    apiResponseKind: virtualMachineApiResponse,
+                    data: dataVirtualMachine,
+                    resourceKind: VirtualMachineData.VIRTUAL_MACHINE,
+                    resourcePrefix: ResourcePrefix.VIRTUAL_MACHINES,
+                    resourceKindProperty: VirtualMachineData.CREATED_AT,
+                    property: dataVirtualMachine.createdAt
+                });
+                dispatch<any>(initAdvancedTabDialog(advanceDataVirtualMachine));
+                break;
+            case ResourceKind.KEEP_SERVICE:
+                const dataKeepService = getState().keepServices.find(it => it.uuid === uuid);
+                const advanceDataKeepService = advancedTabData({
+                    uuid,
+                    metadata: '',
+                    user: '',
+                    apiResponseKind: keepServiceApiResponse,
+                    data: dataKeepService,
+                    resourceKind: KeepServiceData.KEEP_SERVICE,
+                    resourcePrefix: ResourcePrefix.KEEP_SERVICES,
+                    resourceKindProperty: KeepServiceData.CREATED_AT,
+                    property: dataKeepService!.createdAt
+                });
+                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 }));
         }
@@ -110,8 +235,19 @@ const getDataForAdvancedTab = (uuid: string) =>
 
 const initAdvancedTabDialog = (data: AdvancedTabDialogData) => dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data });
 
-const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: AdvanceResourceKind, 
-    resourcePrefix: AdvanceResourcePrefix, resourceKindProperty: AdvanceResourceKind, property: any) => {
+interface AdvancedTabData {
+    uuid: string;
+    metadata: ListResults<LinkResource> | string;
+    user: UserResource | string;
+    apiResponseKind: (apiResponse: AdvanceResponseData) => string;
+    data: AdvanceResponseData;
+    resourceKind: AdvanceResourceKind;
+    resourcePrefix: AdvanceResourcePrefix;
+    resourceKindProperty: AdvanceResourceKind;
+    property: any;
+}
+
+const advancedTabData = ({ uuid, user, metadata, apiResponseKind, data, resourceKind, resourcePrefix, resourceKindProperty, property }: AdvancedTabData) => {
     return {
         uuid,
         user,
@@ -155,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;
 };
@@ -170,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`;
 
@@ -292,5 +428,64 @@ const sshKeyApiResponse = (apiResponse: SshKeyResource) => {
 "name": ${stringify(name)},
 "created_at": "${createdAt}",
 "expires_at": "${expiresAt}"`;
+    return response;
+};
+
+const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource) => {
+    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = apiResponse;
+    const response = `"hostname": ${stringify(hostname)},
+"uuid": "${uuid}",
+"owner_uuid": "${ownerUuid}",
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
+"modified_at": ${stringify(modifiedAt)},
+"modified_at": ${stringify(modifiedAt)},
+"created_at": "${createdAt}"`;
+
+    return response;
+};
+
+const keepServiceApiResponse = (apiResponse: KeepServiceResource) => {
+    const {
+        uuid, readOnly, serviceHost, servicePort, serviceSslFlag, serviceType,
+        ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid
+    } = apiResponse;
+    const response = `"uuid": "${uuid}",
+"owner_uuid": "${ownerUuid}",
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
+"modified_at": ${stringify(modifiedAt)},
+"service_host": "${serviceHost}",
+"service_port": "${servicePort}",
+"service_ssl_flag": "${stringify(serviceSslFlag)}",
+"service_type": "${serviceType}",
+"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
index 9aadd5b949b2fe325d38d75bf2a03f4bb60e524b..4ed348751faa6a516eb07bcf3cb2c18688534e01 100644 (file)
@@ -94,9 +94,9 @@ export const openSshKeyCreateDialog = () => dialogActions.OPEN_DIALOG({ id: SSH_
 export const openPublicKeyDialog = (name: string, publicKey: string) =>
     dialogActions.OPEN_DIALOG({ id: SSH_KEY_PUBLIC_KEY_DIALOG, data: { name, publicKey } });
 
-export const openSshKeyAttributesDialog = (index: number) =>
+export const openSshKeyAttributesDialog = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const sshKey = getState().auth.sshKeys[index];
+        const sshKey = getState().auth.sshKeys.find(it => it.uuid === uuid);
         dispatch(dialogActions.OPEN_DIALOG({ id: SSH_KEY_ATTRIBUTES_DIALOG, data: { sshKey } }));
     };
 
index c54438b170c5b1a27c945444644c6f17bfb066fb..aeee2b3b7c8f66d90b4e26c6bb4c09a3c10337e2 100644 (file)
@@ -56,7 +56,7 @@ describe('auth-actions', () => {
                 lastName: "Doe",
                 uuid: "uuid",
                 ownerUuid: "ownerUuid",
-                isAdmin: true
+                isAdmin: false
             }
         });
     });
diff --git a/src/store/compute-nodes/compute-nodes-actions.ts b/src/store/compute-nodes/compute-nodes-actions.ts
new file mode 100644 (file)
index 0000000..659b1e8
--- /dev/null
@@ -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 (file)
index 0000000..44a3780
--- /dev/null
@@ -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
index 5631a5e85cf9880b99fde0afd22f339a55ee734f..65ddcff2c7f1b360baa0e4ea98587cdb9bbbc179 100644 (file)
@@ -15,6 +15,9 @@ import { extractUuidKind, ResourceKind } from '~/models/resource';
 import { Process } from '~/store/processes/process';
 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 }>(),
@@ -33,8 +36,9 @@ export type ContextMenuResource = {
     isTrashed?: boolean;
     index?: number
 };
-export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) =>
-    event.nativeEvent.detail === 0;
+
+export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
+
 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
     (dispatch: Dispatch) => {
         event.preventDefault();
@@ -62,27 +66,58 @@ export const openCollectionFilesContextMenu = (event: React.MouseEvent<HTMLEleme
         }));
     };
 
-export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>, index: number, repository: RepositoryResource) =>
+export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-            dispatch<any>(openContextMenu(event, {
-                name: '',
-                uuid: repository.uuid,
-                ownerUuid: repository.ownerUuid,
-                kind: ResourceKind.REPOSITORY,
-                menuKind: ContextMenuKind.REPOSITORY,
-                index
-            }));
+        dispatch<any>(openContextMenu(event, {
+            name: '',
+            uuid: repository.uuid,
+            ownerUuid: repository.ownerUuid,
+            kind: ResourceKind.REPOSITORY,
+            menuKind: ContextMenuKind.REPOSITORY
+        }));
+    };
+
+export const openVirtualMachinesContextMenu = (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch<any>(openContextMenu(event, {
+            name: '',
+            uuid: repository.uuid,
+            ownerUuid: repository.ownerUuid,
+            kind: ResourceKind.VIRTUAL_MACHINE,
+            menuKind: ContextMenuKind.VIRTUAL_MACHINE
+        }));
     };
 
-export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, index: number, sshKey: SshKeyResource) =>
+export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) =>
     (dispatch: Dispatch) => {
         dispatch<any>(openContextMenu(event, {
             name: '',
             uuid: sshKey.uuid,
             ownerUuid: sshKey.ownerUuid,
             kind: ResourceKind.SSH_KEY,
-            menuKind: ContextMenuKind.SSH_KEY,
-            index
+            menuKind: ContextMenuKind.SSH_KEY
+        }));
+    };
+
+export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(openContextMenu(event, {
+            name: '',
+            uuid: keepService.uuid,
+            ownerUuid: keepService.ownerUuid,
+            kind: ResourceKind.KEEP_SERVICE,
+            menuKind: ContextMenuKind.KEEP_SERVICE
+        }));
+    };
+
+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
         }));
     };
 
diff --git a/src/store/keep-services/keep-services-actions.ts b/src/store/keep-services/keep-services-actions.ts
new file mode 100644 (file)
index 0000000..54a7c3f
--- /dev/null
@@ -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 { KeepServiceResource } from '~/models/keep-services';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { navigateToRootProject } from '~/store/navigation/navigation-action';
+
+export const keepServicesActions = unionize({
+    SET_KEEP_SERVICES: ofType<KeepServiceResource[]>(),
+    REMOVE_KEEP_SERVICE: ofType<string>()
+});
+
+export type KeepServicesActions = UnionOf<typeof keepServicesActions>;
+
+export const KEEP_SERVICE_REMOVE_DIALOG = 'keepServiceRemoveDialog';
+export const KEEP_SERVICE_ATTRIBUTES_DIALOG = 'keepServiceAttributesDialog';
+
+export const loadKeepServicesPanel = () =>
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const user = getState().auth.user;
+        if(user && user.isAdmin) {
+            try {
+                dispatch(setBreadcrumbs([{ label: 'Keep Services' }]));
+                const response = await services.keepService.list();
+                dispatch(keepServicesActions.SET_KEEP_SERVICES(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 openKeepServiceAttributesDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const keepService = getState().keepServices.find(it => it.uuid === uuid);
+        dispatch(dialogActions.OPEN_DIALOG({ id: KEEP_SERVICE_ATTRIBUTES_DIALOG, data: { keepService } }));
+    };
+
+export const openKeepServiceRemoveDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: KEEP_SERVICE_REMOVE_DIALOG,
+            data: {
+                title: 'Remove keep service',
+                text: 'Are you sure you want to remove this keep service?',
+                confirmButtonLabel: 'Remove',
+                uuid
+            }
+        }));
+    };
+
+export const removeKeepService = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+        try {
+            await services.keepService.delete(uuid);
+            dispatch(keepServicesActions.REMOVE_KEEP_SERVICE(uuid));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Keep service has been successfully removed.', hideDuration: 2000 }));
+        } catch (e) {
+            return;
+        }
+    };
\ No newline at end of file
diff --git a/src/store/keep-services/keep-services-reducer.ts b/src/store/keep-services/keep-services-reducer.ts
new file mode 100644 (file)
index 0000000..043c010
--- /dev/null
@@ -0,0 +1,17 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { keepServicesActions, KeepServicesActions } from '~/store/keep-services/keep-services-actions';
+import { KeepServiceResource } from '~/models/keep-services';
+
+export type KeepSericesState = KeepServiceResource[];
+
+const initialState: KeepSericesState = [];
+
+export const keepServicesReducer = (state: KeepSericesState = initialState, action: KeepServicesActions): KeepSericesState =>
+    keepServicesActions.match(action, {
+        SET_KEEP_SERVICES: items => items,
+        REMOVE_KEEP_SERVICE: (uuid: string) => state.filter((keepService) => keepService.uuid !== uuid),
+        default: () => state
+    });
\ No newline at end of file
index 2bfd8b9944ec75e4911ab2bbd08c19676cab62c5..50cfd88d326e8fe97a4610b6b27b6ab842a8a092 100644 (file)
@@ -67,3 +67,7 @@ export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES);
 export const navigateToRepositories = push(Routes.REPOSITORIES);
 
 export const navigateToSshKeys= push(Routes.SSH_KEYS);
+
+export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
+
+export const navigateToComputeNodes = push(Routes.COMPUTE_NODES);
\ No newline at end of file
index dd80f8d79fd117851fa4f86eec41319ff43d9187..ea64bfc90ee2bb3cd109c1944f8992fdbeb3f087 100644 (file)
@@ -32,9 +32,9 @@ export const openRepositoriesSampleGitDialog = () =>
         dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORIES_SAMPLE_GIT_DIALOG, data: { uuidPrefix } }));
     };
 
-export const openRepositoryAttributes = (index: number) =>
+export const openRepositoryAttributes = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const repositoryData = getState().repositories.items[index];
+        const repositoryData = getState().repositories.items.find(it => it.uuid === uuid);
         dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORY_ATTRIBUTES_DIALOG, data: { repositoryData } }));
     };
 
@@ -84,7 +84,7 @@ export const removeRepository = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
         await services.repositoriesService.delete(uuid);
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
         dispatch<any>(loadRepositoriesData());
     };
 
index 4ab0918e60fb0ac6a636d03e4f95d0d9d3c6e468..321a19b6f25182072d405e29e8b308457b4162df 100644 (file)
@@ -45,6 +45,8 @@ import { SearchResultsMiddlewareService } from './search-results-panel/search-re
 import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer";
 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' &&
@@ -115,5 +117,7 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     appInfo: appInfoReducer,
     searchBar: searchBarReducer,
     virtualMachines: virtualMachinesReducer,
-    repositories: repositoriesReducer
+    repositories: repositoriesReducer,
+    keepServices: keepServicesReducer,
+    computeNodes: computeNodesReducer
 });
index 9bd79884ff470e86e4bb9d08c79d53c5867989c3..c95277b3a4c412149d4d2f3606a21f0bf3fc699f 100644 (file)
@@ -9,33 +9,42 @@ import { navigateToVirtualMachines } from "../navigation/navigation-action";
 import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
 import { formatDate } from "~/common/formatters";
 import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { VirtualMachinesLoginsResource } from '~/models/virtual-machines';
+import { VirtualMachineLogins } from '~/models/virtual-machines';
 import { FilterBuilder } from "~/services/api/filter-builder";
 import { ListResults } from "~/services/common-service/common-resource-service";
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 
 export const virtualMachinesActions = unionize({
     SET_REQUESTED_DATE: ofType<string>(),
     SET_VIRTUAL_MACHINES: ofType<ListResults<any>>(),
-    SET_LOGINS: ofType<VirtualMachinesLoginsResource[]>(),
+    SET_LOGINS: ofType<VirtualMachineLogins>(),
     SET_LINKS: ofType<ListResults<any>>()
 });
 
 export type VirtualMachineActions = UnionOf<typeof virtualMachinesActions>;
 
 export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel';
+export const VIRTUAL_MACHINE_ATTRIBUTES_DIALOG = 'virtualMachineAttributesDialog';
+export const VIRTUAL_MACHINE_REMOVE_DIALOG = 'virtualMachineRemoveDialog';
 
 export const openVirtualMachines = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch<any>(navigateToVirtualMachines);
     };
 
+export const openVirtualMachineAttributes = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const virtualMachineData = getState().virtualMachines.virtualMachines.items.find(it => it.uuid === uuid);
+        dispatch(dialogActions.OPEN_DIALOG({ id: VIRTUAL_MACHINE_ATTRIBUTES_DIALOG, data: { virtualMachineData } }));
+    };
+
 const loadRequestedDate = () =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const date = services.virtualMachineService.getRequestedDate();
         dispatch(virtualMachinesActions.SET_REQUESTED_DATE(date));
     };
 
-
 export const loadVirtualMachinesData = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch<any>(loadRequestedDate());
@@ -48,6 +57,8 @@ export const loadVirtualMachinesData = () =>
         });
         dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
         dispatch(virtualMachinesActions.SET_LINKS(links));
+        const getAllLogins = await services.virtualMachineService.getAllLogins();
+        dispatch(virtualMachinesActions.SET_LOGINS(getAllLogins));
     };
 
 export const saveRequestedDate = () =>
@@ -57,6 +68,27 @@ export const saveRequestedDate = () =>
         dispatch<any>(loadRequestedDate());
     };
 
+export const openRemoveVirtualMachineDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: VIRTUAL_MACHINE_REMOVE_DIALOG,
+            data: {
+                title: 'Remove virtual machine',
+                text: 'Are you sure you want to remove this virtual machine?',
+                confirmButtonLabel: 'Remove',
+                uuid
+            }
+        }));
+    };
+
+export const removeVirtualMachine = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+        await services.virtualMachineService.delete(uuid);
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+        dispatch<any>(loadVirtualMachinesData());
+    };
+
 const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL);
 
 export const loadVirtualMachinesPanel = () =>
index fa28417efec42c0d6a021bb4257ede748aed564d..475ad7523896eeb2f63bd96efe92bfa3ce44ea8a 100644 (file)
@@ -4,12 +4,12 @@
 
 import { virtualMachinesActions, VirtualMachineActions } from '~/store/virtual-machines/virtual-machines-actions';
 import { ListResults } from '~/services/common-service/common-resource-service';
-import { VirtualMachinesLoginsResource } from '~/models/virtual-machines';
+import { VirtualMachineLogins } from '~/models/virtual-machines';
 
 interface VirtualMachines {
     date: string;
     virtualMachines: ListResults<any>;
-    logins: VirtualMachinesLoginsResource[];
+    logins: VirtualMachineLogins;
     links: ListResults<any>;
 }
 
@@ -22,7 +22,10 @@ const initialState: VirtualMachines = {
         itemsAvailable: 0,
         items: []
     },
-    logins: [],
+    logins: {
+        kind: '',
+        items: []
+    },
     links: {
         kind: '',
         offset: 0,
index 12dbe7b1a8a2d71ebfcd35eb4c2fb27cd8c6fdec..e3f96a9c07de620385640dd43445ca566c276f8b 100644 (file)
@@ -56,6 +56,8 @@ import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/searc
 import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
 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';
 
@@ -410,6 +412,16 @@ export const loadSshKeys = handleFirstTimeLoad(
         await dispatch(loadSshKeysPanel());
     });
 
+export const loadKeepServices = handleFirstTimeLoad(
+    async (dispatch: Dispatch<any>) => {
+        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;
index 294bd6d5d2858c667b3979d6876fb0d3c3e3746e..d912ac1302c9b066e059c9a31dddc1e0602e4a5e 100644 (file)
@@ -18,8 +18,8 @@ import { FileTreeData } from "~/components/file-tree/file-tree-data";
 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";
@@ -63,23 +63,22 @@ export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispat
 
 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)
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 (file)
index 0000000..3959909
--- /dev/null
@@ -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 (file)
index 0000000..2233974
--- /dev/null
@@ -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 (file)
index 0000000..cfb90b6
--- /dev/null
@@ -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/action-sets/keep-service-action-set.ts b/src/views-components/context-menu/action-sets/keep-service-action-set.ts
new file mode 100644 (file)
index 0000000..807a3ab
--- /dev/null
@@ -0,0 +1,28 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { openKeepServiceAttributesDialog, openKeepServiceRemoveDialog } from '~/store/keep-services/keep-services-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 keepServiceActionSet: ContextMenuActionSet = [[{
+    name: "Attributes",
+    icon: AttributesIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openKeepServiceAttributesDialog(uuid));
+    }
+}, {
+    name: "Advanced",
+    icon: AdvancedIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openAdvancedTabDialog(uuid));
+    }
+}, {
+    name: "Remove",
+    icon: RemoveIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openKeepServiceRemoveDialog(uuid));
+    }
+}]];
index 22f6bee135c71f6b11fb6faf21b4af1fe27ed4f6..82c2f2b8e04909b861d561ff59557c89ae973a1c 100644 (file)
@@ -11,8 +11,8 @@ import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions
 export const repositoryActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
     icon: AttributesIcon,
-    execute: (dispatch, { index }) => {
-        dispatch<any>(openRepositoryAttributes(index!));
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openRepositoryAttributes(uuid));
     }
 }, {
     name: "Share",
@@ -24,7 +24,7 @@ export const repositoryActionSet: ContextMenuActionSet = [[{
     name: "Advanced",
     icon: AdvancedIcon,
     execute: (dispatch, resource) => {
-        dispatch<any>(openAdvancedTabDialog(resource.uuid, resource.index));
+        dispatch<any>(openAdvancedTabDialog(resource.uuid));
     }
 }, {
     name: "Remove",
index 6e86b2bc17a2044d05e3ab3eb320e0cbc8fb0e0c..0ce0c43118549b9107a17f8e9c3bf515b53f50a3 100644 (file)
@@ -10,14 +10,14 @@ import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
 export const sshKeyActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
     icon: AttributesIcon,
-    execute: (dispatch, { index }) => {
-        dispatch<any>(openSshKeyAttributesDialog(index!));
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openSshKeyAttributesDialog(uuid));
     }
 }, {
     name: "Advanced",
     icon: AdvancedIcon,
-    execute: (dispatch, { uuid, index }) => {
-        dispatch<any>(openAdvancedTabDialog(uuid, index));
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openAdvancedTabDialog(uuid));
     }
 }, {
     name: "Remove",
diff --git a/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts b/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts
new file mode 100644 (file)
index 0000000..ea274af
--- /dev/null
@@ -0,0 +1,28 @@
+// 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 { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { openVirtualMachineAttributes, openRemoveVirtualMachineDialog } from "~/store/virtual-machines/virtual-machines-actions";
+
+export const virtualMachineActionSet: ContextMenuActionSet = [[{
+    name: "Attributes",
+    icon: AttributesIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openVirtualMachineAttributes(uuid));
+    }
+}, {
+    name: "Advanced",
+    icon: AdvancedIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openAdvancedTabDialog(uuid));
+    }
+}, {
+    name: "Remove",
+    icon: RemoveIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openRemoveVirtualMachineDialog(uuid));
+    }
+}]];
index af5aaa929c43f14623c9f8c6e85d43ddeb4ebf7f..3fa1ab30d83d5d994e7b94bbe49b356deee74009 100644 (file)
@@ -70,5 +70,8 @@ export enum ContextMenuKind {
     PROCESS_RESOURCE = 'ProcessResource',
     PROCESS_LOGS = "ProcessLogs",
     REPOSITORY = "Repository",
-    SSH_KEY = "SshKey"
+    SSH_KEY = "SshKey",
+    VIRTUAL_MACHINE = "VirtualMachine",
+    KEEP_SERVICE = "KeepService",
+    NODE = "Node"
 }
diff --git a/src/views-components/keep-services-dialog/attributes-dialog.tsx b/src/views-components/keep-services-dialog/attributes-dialog.tsx
new file mode 100644 (file)
index 0000000..113d191
--- /dev/null
@@ -0,0 +1,73 @@
+// 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 { KEEP_SERVICE_ATTRIBUTES_DIALOG } from '~/store/keep-services/keep-services-actions';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { KeepServiceResource } from '~/models/keep-services';
+
+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 AttributesKeepServiceDialogDataProps {
+    keepService: KeepServiceResource;
+}
+
+export const AttributesKeepServiceDialog = compose(
+    withDialog(KEEP_SERVICE_ATTRIBUTES_DIALOG),
+    withStyles(styles))(
+        ({ open, closeDialog, data, classes }: WithDialogProps<AttributesKeepServiceDialogDataProps> & WithStyles<CssRules>) =>
+            <Dialog open={open} onClose={closeDialog} fullWidth maxWidth='sm'>
+                <DialogTitle>Attributes</DialogTitle>
+                <DialogContent>
+                    {data.keepService && <Grid container direction="row" spacing={16} className={classes.root}>
+                        <Grid item xs={5}>UUID</Grid>
+                        <Grid item xs={7}>{data.keepService.uuid}</Grid>
+                        <Grid item xs={5}>Read only</Grid>
+                        <Grid item xs={7}>{JSON.stringify(data.keepService.readOnly)}</Grid>
+                        <Grid item xs={5}>Service host</Grid>
+                        <Grid item xs={7}>{data.keepService.serviceHost}</Grid>
+                        <Grid item xs={5}>Service port</Grid>
+                        <Grid item xs={7}>{data.keepService.servicePort}</Grid>
+                        <Grid item xs={5}>Service SSL flag</Grid>
+                        <Grid item xs={7}>{JSON.stringify(data.keepService.serviceSslFlag)}</Grid>
+                        <Grid item xs={5}>Service type</Grid>
+                        <Grid item xs={7}>{data.keepService.serviceType}</Grid>
+                        <Grid item xs={5}>Owner uuid</Grid>
+                        <Grid item xs={7}>{data.keepService.ownerUuid}</Grid>
+                        <Grid item xs={5}>Created at</Grid>
+                        <Grid item xs={7}>{data.keepService.createdAt}</Grid>
+                        <Grid item xs={5}>Modified at</Grid>
+                        <Grid item xs={7}>{data.keepService.modifiedAt}</Grid>
+                        <Grid item xs={5}>Modified by user uuid</Grid>
+                        <Grid item xs={7}>{data.keepService.modifiedByUserUuid}</Grid>
+                        <Grid item xs={5}>Modified by client uuid</Grid>
+                        <Grid item xs={7}>{data.keepService.modifiedByClientUuid}</Grid>
+                    </Grid>}
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={closeDialog}>
+                        Close
+                    </Button>
+                </DialogActions>
+            </Dialog>
+    );
\ No newline at end of file
diff --git a/src/views-components/keep-services-dialog/remove-dialog.tsx b/src/views-components/keep-services-dialog/remove-dialog.tsx
new file mode 100644 (file)
index 0000000..7e39850
--- /dev/null
@@ -0,0 +1,20 @@
+// 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 { KEEP_SERVICE_REMOVE_DIALOG, removeKeepService } from '~/store/keep-services/keep-services-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(removeKeepService(props.data.uuid));
+    }
+});
+
+export const RemoveKeepServiceDialog = compose(
+    withDialog(KEEP_SERVICE_REMOVE_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
index ca88021ccdb04c6491ec5389f94dade7ccd35424..f4232a129dd3c07b11d64d88f661eab40b79ab9c 100644 (file)
@@ -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 } 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 {
@@ -37,6 +37,8 @@ export const AccountMenu = connect(mapStateToProps)(
                 <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
                 <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>
index 6b84bde2b6143a6e00abd80e6a17068eb27be66c..78b79a8ed46bf0f70737fa412178d421fa0562fb 100644 (file)
@@ -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 } from '~/routes/routes';
+import * as Routes from '~/routes/routes';
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 interface MainContentBarProps {
@@ -16,32 +16,15 @@ interface MainContentBarProps {
     buttonVisible: boolean;
 }
 
-const isWorkflowPath = ({ router }: RootState) => {
+const isButtonVisible = ({ router }: RootState) => {
     const pathname = router.location ? router.location.pathname : '';
-    const match = matchWorkflowRoute(pathname);
-    return !!match;
-};
-
-const isVirtualMachinePath = ({ router }: RootState) => {
-    const pathname = router.location ? router.location.pathname : '';
-    const match = matchVirtualMachineRoute(pathname);
-    return !!match;
-};
-
-const isRepositoriesPath = ({ router }: RootState) => {
-    const pathname = router.location ? router.location.pathname : '';
-    const match = matchRepositoriesRoute(pathname);
-    return !!match;
-};
-
-const isSshKeysPath = ({ router }: RootState) => {
-    const pathname = router.location ? router.location.pathname : '';
-    const match = matchSshKeysRoute(pathname);
-    return !!match;
+    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) => ({
-    buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) && !isVirtualMachinePath(state)
+    buttonVisible: isButtonVisible(state)
 }), {
         onDetailsPanelToggle: toggleDetailsPanel
     })((props: MainContentBarProps) =>
index 148e78bdf361034688537032c200cc57ef27c8df..ca51c8495fb841d6a1cf98a713a748db5f61dd52 100644 (file)
@@ -1,20 +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 { removeRepository, REPOSITORY_REMOVE_DIALOG } from '~/store/repositories/repositories-actions';
 
- const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
     onConfirm: () => {
         props.closeDialog();
         dispatch<any>(removeRepository(props.data.uuid));
     }
 });
 
- export const RemoveRepositoryDialog = compose(
+export const RemoveRepositoryDialog = compose(
     withDialog(REPOSITORY_REMOVE_DIALOG),
     connect(null, mapDispatchToProps)
 )(ConfirmationDialog);
\ No newline at end of file
diff --git a/src/views-components/virtual-machines-dialog/attributes-dialog.tsx b/src/views-components/virtual-machines-dialog/attributes-dialog.tsx
new file mode 100644 (file)
index 0000000..05c1b0a
--- /dev/null
@@ -0,0 +1,89 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Grid } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { VIRTUAL_MACHINE_ATTRIBUTES_DIALOG } from "~/store/virtual-machines/virtual-machines-actions";
+import { WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { compose } from "redux";
+import { VirtualMachinesResource } from "~/models/virtual-machines";
+
+type CssRules = 'rightContainer' | 'leftContainer' | 'spacing';
+
+const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
+    rightContainer: {
+        textAlign: 'right',
+        paddingRight: theme.spacing.unit * 2,
+        color: theme.palette.grey["500"]
+    },
+    leftContainer: {
+        textAlign: 'left',
+        paddingLeft: theme.spacing.unit * 2
+    },
+    spacing: {
+        paddingTop: theme.spacing.unit * 2
+    },
+}));
+
+interface VirtualMachineAttributesDataProps {
+    virtualMachineData: VirtualMachinesResource;
+}
+
+type VirtualMachineAttributesProps = VirtualMachineAttributesDataProps & WithStyles<CssRules>;
+
+export const VirtualMachineAttributesDialog = compose(
+    withDialog(VIRTUAL_MACHINE_ATTRIBUTES_DIALOG),
+    styles)(
+        (props: WithDialogProps<VirtualMachineAttributesProps> & VirtualMachineAttributesProps) =>
+            <Dialog open={props.open}
+                onClose={props.closeDialog}
+                fullWidth
+                maxWidth="sm">
+                <DialogTitle>Attributes</DialogTitle>
+                <DialogContent>
+                    <Typography variant="body2" className={props.classes.spacing}>
+                        {props.data.virtualMachineData && attributes(props.data.virtualMachineData, props.classes)}
+                    </Typography>
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={props.closeDialog}>
+                        Close
+                </Button>
+                </DialogActions>
+            </Dialog>
+    );
+
+const attributes = (virtualMachine: VirtualMachinesResource, classes: any) => {
+    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = virtualMachine;
+    return (
+        <span>
+            <Grid container direction="row">
+                <Grid item xs={5} className={classes.rightContainer}>
+                    <Grid item>Hostname</Grid>
+                    <Grid item>Owner uuid</Grid>
+                    <Grid item>Created at</Grid>
+                    <Grid item>Modified at</Grid>
+                    <Grid item>Modified by user uuid</Grid>
+                    <Grid item>Modified by client uuid</Grid>
+                    <Grid item>uuid</Grid>
+                </Grid>
+                <Grid item xs={7} className={classes.leftContainer}>
+                    <Grid item>{hostname}</Grid>
+                    <Grid item>{ownerUuid}</Grid>
+                    <Grid item>{createdAt}</Grid>
+                    <Grid item>{modifiedAt}</Grid>
+                    <Grid item>{modifiedByUserUuid}</Grid>
+                    <Grid item>{modifiedByClientUuid}</Grid>
+                    <Grid item>{uuid}</Grid>
+                </Grid>
+            </Grid>
+        </span>
+    );
+};
diff --git a/src/views-components/virtual-machines-dialog/remove-dialog.tsx b/src/views-components/virtual-machines-dialog/remove-dialog.tsx
new file mode 100644 (file)
index 0000000..11ab9c4
--- /dev/null
@@ -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 { VIRTUAL_MACHINE_REMOVE_DIALOG, removeVirtualMachine } from '~/store/virtual-machines/virtual-machines-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(removeVirtualMachine(props.data.uuid));
+    }
+});
+
+export const RemoveVirtualMachineDialog = compose(
+    withDialog(VIRTUAL_MACHINE_REMOVE_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
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 (file)
index 0000000..be3627b
--- /dev/null
@@ -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 (file)
index 0000000..a4f22c8
--- /dev/null
@@ -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-root.tsx b/src/views/keep-service-panel/keep-service-panel-root.tsx
new file mode 100644 (file)
index 0000000..8c266b6
--- /dev/null
@@ -0,0 +1,87 @@
+// 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, Button, Typography, Grid, Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton, Checkbox } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { MoreOptionsIcon } from '~/components/icon/icon';
+import { KeepServiceResource } from '~/models/keep-services';
+
+type CssRules = 'root' | 'tableRow';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+        overflow: 'auto'
+    },
+    tableRow: {
+        '& td, th': {
+            whiteSpace: 'nowrap'
+        }
+    }
+});
+
+export interface KeepServicePanelRootActionProps {
+    openRowOptions: (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => void;
+}
+
+export interface KeepServicePanelRootDataProps {
+    keepServices: KeepServiceResource[];
+    hasKeepSerices: boolean;
+}
+
+type KeepServicePanelRootProps = KeepServicePanelRootActionProps & KeepServicePanelRootDataProps & WithStyles<CssRules>;
+
+export const KeepServicePanelRoot = withStyles(styles)(
+    ({ classes, hasKeepSerices, keepServices, openRowOptions }: KeepServicePanelRootProps) => 
+        <Card className={classes.root}>
+            <CardContent>
+                {hasKeepSerices && <Grid container direction="row">
+                    <Grid item xs={12}>
+                        <Table>
+                            <TableHead>
+                                <TableRow className={classes.tableRow}>
+                                    <TableCell>UUID</TableCell>
+                                    <TableCell>Read only</TableCell>
+                                    <TableCell>Service host</TableCell>
+                                    <TableCell>Service port</TableCell>
+                                    <TableCell>Service SSL flag</TableCell>
+                                    <TableCell>Service type</TableCell>
+                                    <TableCell />
+                                </TableRow>
+                            </TableHead>
+                            <TableBody>
+                                {keepServices.map((keepService, index) =>
+                                    <TableRow key={index} className={classes.tableRow}>
+                                        <TableCell>{keepService.uuid}</TableCell>
+                                        <TableCell>
+                                            <Checkbox
+                                                disableRipple
+                                                color="primary"
+                                                checked={keepService.readOnly} />
+                                        </TableCell>
+                                        <TableCell>{keepService.serviceHost}</TableCell>
+                                        <TableCell>{keepService.servicePort}</TableCell>
+                                        <TableCell>
+                                            <Checkbox
+                                                disableRipple
+                                                color="primary"
+                                                checked={keepService.serviceSslFlag} />
+                                        </TableCell>
+                                        <TableCell>{keepService.serviceType}</TableCell>
+                                        <TableCell>
+                                            <Tooltip title="More options" disableFocusListener>
+                                                <IconButton onClick={event => openRowOptions(event, keepService)}>
+                                                    <MoreOptionsIcon />
+                                                </IconButton>
+                                            </Tooltip>
+                                        </TableCell>
+                                    </TableRow>)}
+                            </TableBody>
+                        </Table>
+                    </Grid>
+                </Grid>}
+            </CardContent>
+        </Card>
+);
\ 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
new file mode 100644 (file)
index 0000000..369b7c2
--- /dev/null
@@ -0,0 +1,28 @@
+// 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 { 
+    KeepServicePanelRoot, 
+    KeepServicePanelRootDataProps, 
+    KeepServicePanelRootActionProps 
+} from '~/views/keep-service-panel/keep-service-panel-root';
+import { openKeepServiceContextMenu } from '~/store/context-menu/context-menu-actions';
+
+const mapStateToProps = (state: RootState): KeepServicePanelRootDataProps => {
+    return {
+        keepServices: state.keepServices,
+        hasKeepSerices: state.keepServices.length > 0
+    };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): KeepServicePanelRootActionProps => ({
+    openRowOptions: (event, keepService) => {
+        dispatch<any>(openKeepServiceContextMenu(event, keepService));
+    }
+});
+
+export const KeepServicePanel = connect(mapStateToProps, mapDispatchToProps)(KeepServicePanelRoot);
\ No newline at end of file
index cfe59f0d26d772c0ddf9f227e2b5a6cc60d8ea2d..c7016f6298848355be16a206b7c4ce66ac9a029e 100644 (file)
@@ -66,8 +66,8 @@ const mapStateToProps = (state: RootState) => {
 
 const mapDispatchToProps = (dispatch: Dispatch): Pick<RepositoriesActionProps, 'onOptionsMenuOpen' | 'loadRepositories' | 'openRepositoriesSampleGitDialog' | 'openRepositoryCreateDialog'> => ({
     loadRepositories: () => dispatch<any>(loadRepositoriesData()),
-    onOptionsMenuOpen: (event, index, repository) => {
-        dispatch<any>(openRepositoryContextMenu(event, index, repository));
+    onOptionsMenuOpen: (event, repository) => {
+        dispatch<any>(openRepositoryContextMenu(event, repository));
     },
     openRepositoriesSampleGitDialog: () => dispatch<any>(openRepositoriesSampleGitDialog()),
     openRepositoryCreateDialog: () => dispatch<any>(openRepositoryCreateDialog())
@@ -75,7 +75,7 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<RepositoriesActionProps, '
 
 interface RepositoriesActionProps {
     loadRepositories: () => void;
-    onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, index: number, repository: RepositoryResource) => void;
+    onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => void;
     openRepositoriesSampleGitDialog: () => void;
     openRepositoryCreateDialog: () => void;
 }
@@ -137,7 +137,7 @@ export const RepositoriesPanel = compose(
                                                 <TableCell className={classes.cloneUrls}>{repository.cloneUrls.join("\n")}</TableCell>
                                                 <TableCell className={classes.moreOptions}>
                                                     <Tooltip title="More options" disableFocusListener>
-                                                        <IconButton onClick={event => onOptionsMenuOpen(event, index, repository)} className={classes.moreOptionsButton}>
+                                                        <IconButton onClick={event => onOptionsMenuOpen(event, repository)} className={classes.moreOptionsButton}>
                                                             <MoreOptionsIcon />
                                                         </IconButton>
                                                     </Tooltip>
index 869662dd0150f07e8fe07e11a6f0f2fcebe4c71e..2cdad07dd14fb774ca9260d87b1c4a2285c40e52 100644 (file)
@@ -38,7 +38,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 export interface SshKeyPanelRootActionProps {
     openSshKeyCreateDialog: () => void;
-    openRowOptions: (event: React.MouseEvent<HTMLElement>, index: number, sshKey: SshKeyResource) => void;
+    openRowOptions: (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => void;
     openPublicKeyDialog: (name: string, publicKey: string) => void;
 }
 
@@ -102,7 +102,7 @@ export const SshKeyPanelRoot = withStyles(styles)(
                                     </TableCell>
                                     <TableCell>
                                         <Tooltip title="More options" disableFocusListener>
-                                            <IconButton onClick={event => openRowOptions(event, index, sshKey)}>
+                                            <IconButton onClick={event => openRowOptions(event, sshKey)}>
                                                 <MoreOptionsIcon />
                                             </IconButton>
                                         </Tooltip>
index c7e3516e0ab9cf2bede1b08df4256ab8c2821d55..4e800296d076fb3f368ac9d98959d3d00520a18b 100644 (file)
@@ -20,8 +20,8 @@ const mapDispatchToProps = (dispatch: Dispatch): SshKeyPanelRootActionProps => (
     openSshKeyCreateDialog: () => {
         dispatch<any>(openSshKeyCreateDialog());
     },
-    openRowOptions: (event, index, sshKey) => {
-        dispatch<any>(openSshKeyContextMenu(event, index, sshKey));
+    openRowOptions: (event, sshKey) => {
+        dispatch<any>(openSshKeyContextMenu(event, sshKey));
     },
     openPublicKeyDialog: (name: string, publicKey: string) => {
         dispatch<any>(openPublicKeyDialog(name, publicKey));
index c94c3a74bb956a2506be60cfb762f1ad6f354571..5dbd3f0965aa6336d2893fd7faf48ecba75e43e7 100644 (file)
@@ -4,20 +4,21 @@
 
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip } from '@material-ui/core';
+import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip, IconButton } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
 import { Link } from 'react-router-dom';
-import { Dispatch, compose } from 'redux';
+import { compose, Dispatch } from 'redux';
 import { saveRequestedDate, loadVirtualMachinesData } from '~/store/virtual-machines/virtual-machines-actions';
 import { RootState } from '~/store/store';
 import { ListResults } from '~/services/common-service/common-resource-service';
-import { HelpIcon } from '~/components/icon/icon';
-import { VirtualMachinesLoginsResource, VirtualMachinesResource } from '~/models/virtual-machines';
+import { HelpIcon, MoreOptionsIcon } from '~/components/icon/icon';
+import { VirtualMachineLogins, VirtualMachinesResource } from '~/models/virtual-machines';
 import { Routes } from '~/routes/routes';
+import { openVirtualMachinesContextMenu } from '~/store/context-menu/context-menu-actions';
 
-type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
+type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon' | 'moreOptionsButton' | 'moreOptions';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
@@ -55,31 +56,47 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     icon: {
         textAlign: "right",
         marginTop: theme.spacing.unit
-    }
+    },
+    moreOptionsButton: {
+        padding: 0
+    },
+    moreOptions: {
+        textAlign: 'right',
+        '&:last-child': {
+            paddingRight: 0
+        }
+    },
 });
 
-const mapStateToProps = ({ virtualMachines }: RootState) => {
+const mapStateToProps = ({ virtualMachines, auth }: RootState) => {
     return {
         requestedDate: virtualMachines.date,
+        isAdmin: auth.user!.isAdmin,
+        logins: virtualMachines.logins,
         ...virtualMachines
     };
 };
 
-const mapDispatchToProps = {
-    saveRequestedDate,
-    loadVirtualMachinesData
-};
+const mapDispatchToProps = (dispatch: Dispatch): Pick<VirtualMachinesPanelActionProps, 'loadVirtualMachinesData' | 'saveRequestedDate' | 'onOptionsMenuOpen'> => ({
+    saveRequestedDate: () => dispatch<any>(saveRequestedDate()),
+    loadVirtualMachinesData: () => dispatch<any>(loadVirtualMachinesData()),
+    onOptionsMenuOpen: (event, virtualMachine) => {
+        dispatch<any>(openVirtualMachinesContextMenu(event, virtualMachine));
+    },
+});
 
 interface VirtualMachinesPanelDataProps {
     requestedDate: string;
     virtualMachines: ListResults<any>;
-    logins: VirtualMachinesLoginsResource[];
+    logins: VirtualMachineLogins;
     links: ListResults<any>;
+    isAdmin: boolean;
 }
 
 interface VirtualMachinesPanelActionProps {
     saveRequestedDate: () => void;
     loadVirtualMachinesData: () => string;
+    onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>, virtualMachine: VirtualMachinesResource) => void;
 }
 
 type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
@@ -93,12 +110,12 @@ export const VirtualMachinePanel = compose(
             }
 
             render() {
-                const { virtualMachines, links } = this.props;
+                const { virtualMachines, links, isAdmin } = this.props;
                 return (
                     <Grid container spacing={16}>
-                        {virtualMachines.itemsAvailable === 0 && <CardContentWithNoVirtualMachines {...this.props} />}
+                        {!isAdmin && virtualMachines.itemsAvailable > 0 && <CardContentWithNoVirtualMachines {...this.props} />}
                         {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
-                        {<CardSSHSection {...this.props} />}
+                        {!isAdmin && <CardSSHSection {...this.props} />}
                     </Grid>
                 );
             }
@@ -131,53 +148,87 @@ const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
     <Grid item xs={12}>
         <Card>
             <CardContent>
-                <div className={props.classes.rightAlign}>
-                    <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
-                        SEND REQUEST FOR SHELL ACCESS
-                    </Button>
-                    {props.requestedDate &&
-                        <Typography variant="body1">
-                            A request for shell access was sent on {props.requestedDate}
-                        </Typography>}
-                </div>
-                <div className={props.classes.icon}>
-                    <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" className={props.classes.linkIcon}>
-                        <Tooltip title="Access VM using webshell">
-                            <HelpIcon />
-                        </Tooltip>
-                    </a>
-                </div>
-                <Table>
-                    <TableHead>
-                        <TableRow>
-                            <TableCell>Host name</TableCell>
-                            <TableCell>Login name</TableCell>
-                            <TableCell>Command line</TableCell>
-                            <TableCell>Web shell</TableCell>
-                        </TableRow>
-                    </TableHead>
-                    <TableBody>
-                        {props.virtualMachines.items.map((it, index) =>
-                            <TableRow key={index}>
-                                <TableCell>{it.hostname}</TableCell>
-                                <TableCell>{getUsername(props.links, it)}</TableCell>
-                                <TableCell>ssh {getUsername(props.links, it)}@shell.arvados</TableCell>
-                                <TableCell>
-                                    <a href={`https://workbench.c97qk.arvadosapi.com${it.href}/webshell/${getUsername(props.links, it)}`} target="_blank" className={props.classes.link}>
-                                        Log in as {getUsername(props.links, it)}
-                                    </a>
-                                </TableCell>
-                            </TableRow>
-                        )}
-                    </TableBody>
-                </Table>
+                {props.isAdmin ? <span>{adminVirtualMachinesTable(props)}</span>
+                    : <span>
+                        <div className={props.classes.rightAlign}>
+                            <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
+                                SEND REQUEST FOR SHELL ACCESS
+                                </Button>
+                            {props.requestedDate &&
+                                <Typography variant="body1">
+                                    A request for shell access was sent on {props.requestedDate}
+                                </Typography>}
+                        </div>
+                        <div className={props.classes.icon}>
+                            <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" className={props.classes.linkIcon}>
+                                <Tooltip title="Access VM using webshell">
+                                    <HelpIcon />
+                                </Tooltip>
+                            </a>
+                        </div>
+                        {userVirtualMachinesTable(props)}
+                    </span>
+                }
             </CardContent>
         </Card>
     </Grid>;
 
-const getUsername = (links: ListResults<any>, virtualMachine: VirtualMachinesResource) => {
-    const link = links.items.find((item: any) => item.headUuid === virtualMachine.uuid);
-    return link.properties.username || undefined;
+const userVirtualMachinesTable = (props: VirtualMachineProps) =>
+    <Table>
+        <TableHead>
+            <TableRow>
+                <TableCell>Host name</TableCell>
+                <TableCell>Login name</TableCell>
+                <TableCell>Command line</TableCell>
+                <TableCell>Web shell</TableCell>
+            </TableRow>
+        </TableHead>
+        <TableBody>
+            {props.virtualMachines.items.map((it, index) =>
+                <TableRow key={index}>
+                    <TableCell>{it.hostname}</TableCell>
+                    <TableCell>{getUsername(props.links)}</TableCell>
+                    <TableCell>ssh {getUsername(props.links)}@{it.hostname}.arvados</TableCell>
+                    <TableCell>
+                        <a href={`https://workbench.c97qk.arvadosapi.com${it.href}/webshell/${getUsername(props.links)}`} target="_blank" className={props.classes.link}>
+                            Log in as {getUsername(props.links)}
+                        </a>
+                    </TableCell>
+                </TableRow>
+            )}
+        </TableBody>
+    </Table>;
+
+const adminVirtualMachinesTable = (props: VirtualMachineProps) =>
+    <Table>
+        <TableHead>
+            <TableRow>
+                <TableCell>Uuid</TableCell>
+                <TableCell>Host name</TableCell>
+                <TableCell>Logins</TableCell>
+                <TableCell />
+            </TableRow>
+        </TableHead>
+        <TableBody>
+            {props.logins.items.length > 0 && props.virtualMachines.items.map((it, index) =>
+                <TableRow key={index}>
+                    <TableCell>{it.uuid}</TableCell>
+                    <TableCell>{it.hostname}</TableCell>
+                    <TableCell>["{props.logins.items[0].username}"]</TableCell>
+                    <TableCell className={props.classes.moreOptions}>
+                        <Tooltip title="More options" disableFocusListener>
+                            <IconButton onClick={event => props.onOptionsMenuOpen(event, it)} className={props.classes.moreOptionsButton}>
+                                <MoreOptionsIcon />
+                            </IconButton>
+                        </Tooltip>
+                    </TableCell>
+                </TableRow>
+            )}
+        </TableBody>
+    </Table>;
+
+const getUsername = (links: ListResults<any>) => {
+    return links.items[0].properties.username;
 };
 
 const CardSSHSection = (props: VirtualMachineProps) =>
index 3914f64632e716bfe0aa4aca8265ae42a6a6bc93..92c2438b49f92c3d16bcf741713d76e6972d8643 100644 (file)
@@ -51,14 +51,22 @@ import { ProcessInputDialog } from '~/views-components/process-input-dialog/proc
 import { VirtualMachinePanel } from '~/views/virtual-machine-panel/virtual-machine-panel';
 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';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -131,6 +139,8 @@ export const WorkbenchPanel =
                                 <Route path={Routes.VIRTUAL_MACHINES} component={VirtualMachinePanel} />
                                 <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>
@@ -140,6 +150,8 @@ export const WorkbenchPanel =
                 <DetailsPanel />
             </Grid>
             <AdvancedTabDialog />
+            <AttributesComputeNodeDialog />
+            <AttributesKeepServiceDialog />
             <AttributesSshKeyDialog />
             <ChangeWorkflowDialog />
             <ContextMenu />
@@ -161,9 +173,12 @@ export const WorkbenchPanel =
             <ProcessCommandDialog />
             <ProcessInputDialog />
             <ProjectPropertiesDialog />
+            <RemoveComputeNodeDialog />
+            <RemoveKeepServiceDialog />
             <RemoveProcessDialog />
             <RemoveRepositoryDialog />
             <RemoveSshKeyDialog />
+            <RemoveVirtualMachineDialog />
             <RenameFileDialog />
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />
@@ -173,5 +188,6 @@ export const WorkbenchPanel =
             <UpdateCollectionDialog />
             <UpdateProcessDialog />
             <UpdateProjectDialog />
+            <VirtualMachineAttributesDialog />
         </Grid>
     );
\ No newline at end of file
index b9f1cc627770e4061f08cccbf10db29e40fefbac..93aa3cf108e5788f1c1265b01571db4de7706277 100644 (file)
@@ -13,4 +13,8 @@ declare interface System {
 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
index e4d50aae5aa5cce20ac28cfddf3024c457f2f5ab..d3d6396d479953c17c56aa7ebbfca727d5cc2661 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -4329,6 +4329,11 @@ ignore-walk@^3.0.1:
   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"
@@ -4644,6 +4649,13 @@ is-glob@^4.0.0:
   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"