Merge branch 'master' into 14452-my-account
authorPawel Kromplewski <pawel.kromplewski@contractors.roche.com>
Wed, 5 Dec 2018 15:13:43 +0000 (16:13 +0100)
committerPawel Kromplewski <pawel.kromplewski@contractors.roche.com>
Wed, 5 Dec 2018 15:13:43 +0000 (16:13 +0100)
refs #14452

Arvados-DCO-1.1-Signed-off-by: Pawel Kromplewski <pawel.kromplewski@contractors.roche.com>

66 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/run-process-panel/inputs/boolean-input.tsx
src/views/run-process-panel/inputs/directory-input.tsx
src/views/run-process-panel/inputs/enum-input.tsx
src/views/run-process-panel/inputs/file-input.tsx
src/views/run-process-panel/inputs/float-input.tsx
src/views/run-process-panel/inputs/int-input.tsx
src/views/run-process-panel/inputs/string-input.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 64a24dcafcef48a5e245f487d214371c209bd636..623b86d3a58d659bae2105ae06aa0629db71b22f 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 400866e3e0183945cbf9656d17f155050377a800..68de3107f1a82bb219fb49e883c27682c486902f 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, matchMyAccountRoute, matchVirtualMachineRoute } from './routes';
-import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadMyAccount, 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,51 +15,57 @@ 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 myAccountMatch = matchMyAccountRoute(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);
+    const myAccountMatch = Routes.matchMyAccountRoute(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);
     } else if (myAccountMatch) {
-        store.dispatch(loadMyAccount);
+        store.dispatch(WorkbenchActions.loadMyAccount);
     }
 };
index a27b4274c707002a57b5a358d5aecbb9e03c6385..71d920ab3e4d984dcf126a5aaf6b439431dde9d7 100644 (file)
@@ -23,7 +23,9 @@ export const Routes = {
     WORKFLOWS: '/workflows',
     SEARCH_RESULTS: '/search-results',
     SSH_KEYS: `/ssh-keys`,
-    MY_ACCOUNT: '/my-account'
+    MY_ACCOUNT: '/my-account',
+    KEEP_SERVICES: `/keep-services`,
+    COMPUTE_NODES: `/nodes`
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -92,3 +94,9 @@ export const matchSshKeysRoute = (route: string) =>
 
 export const matchMyAccountRoute = (route: string) =>
     matchPath(route, { path: Routes.MY_ACCOUNT });
+
+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 6faaf99ee7a37109a93021cd9cafeb31c4fa57a3..8c2ad5caf3065c7d1779c8eae13b01b934b95581 100644 (file)
@@ -4,7 +4,7 @@
 
 import { User, userPrefs } 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';
@@ -56,7 +56,7 @@ export class AuthService {
     }
 
     public getIsAdmin(): boolean {
-        return !!localStorage.getItem(USER_IS_ADMIN);
+        return localStorage.getItem(USER_IS_ADMIN) === 'true';
     }
 
     public getUser(): User | undefined {
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 a2046f33732350fabb9775f246d3b39553e8e560..0ec39ebc8d7cf475e04099176d733a37589f859d 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 3d6913a67d9a3f4514725af7fc371739028e8911..aeee2b3b7c8f66d90b4e26c6bb4c09a3c10337e2 100644 (file)
@@ -43,7 +43,7 @@ describe('auth-actions', () => {
         localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
         localStorage.setItem(USER_UUID_KEY, "uuid");
         localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
-        localStorage.setItem(USER_IS_ADMIN, JSON.stringify(false));
+        localStorage.setItem(USER_IS_ADMIN, JSON.stringify("false"));
 
         store.dispatch(initAuth());
 
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 80a7f2136eee707a5c87e320d9b74722a4e1ff20..a3652726f1d2dfda5b51d8c1f5200ebf1bbd6b71 100644 (file)
@@ -69,3 +69,7 @@ export const navigateToRepositories = push(Routes.REPOSITORIES);
 export const navigateToSshKeys= push(Routes.SSH_KEYS);
 
 export const navigateToMyAccount = push(Routes.MY_ACCOUNT);
+
+export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
+
+export const navigateToComputeNodes = push(Routes.COMPUTE_NODES);
\ No newline at end of file
index a672738fd2edf18c36237f3e20c490a1100fdf73..61caa769f1ea32746608f3a4f570a86ddbb4db38 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 091a8ccc7c9583c41d97bd4067816eadb491528e..9d0140f33ca8eb736cea4d798d970b7b2c7181b8 100644 (file)
@@ -57,6 +57,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';
 
@@ -416,6 +418,16 @@ export const loadMyAccount = handleFirstTimeLoad(
         await dispatch(loadMyAccountPanel());
     });
 
+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 0aedee9001c4bde86a49950502e3622feae5b624..ee726f3d75ec5a603acbf80c0708be9085b59316 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, navigateToMyAccount } from '~/store/navigation/navigation-action';
+import { navigateToSshKeys, navigateToKeepServices, navigateToComputeNodes, navigateToMyAccount } 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 onClick={() => dispatch(navigateToMyAccount)}>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 5da547423bb94155cf825645f52d057ec0c93449..6a214e9dd846aa0a6907bbefbdfd6cc47e7680e2 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { memoize } from 'lodash/fp';
 import { BooleanCommandInputParameter } from '~/models/workflow';
 import { Field } from 'redux-form';
 import { Switch } from '@material-ui/core';
@@ -16,17 +17,23 @@ export const BooleanInput = ({ input }: BooleanInputProps) =>
         name={input.id}
         commandInput={input}
         component={BooleanInputComponent}
-        normalize={(value, prevValue) => !prevValue}
+        normalize={normalize}
     />;
 
+const normalize = (_: any, prevValue: boolean) => !prevValue;
+
 const BooleanInputComponent = (props: GenericInputProps) =>
     <GenericInput
         component={Input}
         {...props} />;
 
-const Input = (props: GenericInputProps) =>
+const Input = ({ input, commandInput }: GenericInputProps) =>
     <Switch
         color='primary'
-        checked={props.input.value}
-        onChange={() => props.input.onChange(props.input.value)} 
-        disabled={props.commandInput.disabled} />;
\ No newline at end of file
+        checked={input.value}
+        onChange={handleChange(input.onChange, input.value)}
+        disabled={commandInput.disabled} />;
+
+const handleChange = memoize(
+    (onChange: (value: string) => void, value: string) => () => onChange(value)
+);
index aa25fefc0bfbe9b45213ce220e8a855c8f22b68f..29ccd6e0ddf7d544855840fd2420fcba78e90135 100644 (file)
@@ -3,23 +3,24 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { memoize } from 'lodash/fp';
+import { Field } from 'redux-form';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
 import {
     isRequiredInput,
     DirectoryCommandInputParameter,
     CWLType,
     Directory
 } from '~/models/workflow';
-import { Field } from 'redux-form';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
 import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
-import { connect, DispatchProp } from 'react-redux';
 import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 import { TreeItem } from '~/components/tree/tree';
 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
 import { CollectionResource } from '~/models/collection';
 import { ResourceKind } from '~/models/resource';
-import { ERROR_MESSAGE } from '../../../validators/require';
+import { ERROR_MESSAGE } from '~/validators/require';
 
 export interface DirectoryInputProps {
     input: DirectoryCommandInputParameter;
@@ -29,18 +30,25 @@ export const DirectoryInput = ({ input }: DirectoryInputProps) =>
         name={input.id}
         commandInput={input}
         component={DirectoryInputComponent}
-        format={(value?: Directory) => value ? value.basename : ''}
-        parse={(directory: CollectionResource): Directory => ({
-            class: CWLType.DIRECTORY,
-            location: `keep:${directory.portableDataHash}`,
-            basename: directory.name,
-        })}
-        validate={[
-            isRequiredInput(input)
-                ? (directory?: Directory) => directory ? undefined : ERROR_MESSAGE
-                : () => undefined,
-        ]} />;
-
+        format={format}
+        parse={parse}
+        validate={getValidation(input)} />;
+
+const format = (value?: Directory) => value ? value.basename : '';
+
+const parse = (directory: CollectionResource): Directory => ({
+    class: CWLType.DIRECTORY,
+    location: `keep:${directory.portableDataHash}`,
+    basename: directory.name,
+});
+
+const getValidation = memoize(
+    (input: DirectoryCommandInputParameter) => ([
+        isRequiredInput(input)
+            ? (directory?: Directory) => directory ? undefined : ERROR_MESSAGE
+            : () => undefined,
+    ])
+);
 
 interface DirectoryInputComponentState {
     open: boolean;
@@ -78,7 +86,7 @@ const DirectoryInputComponent = connect()(
             this.props.input.onChange(this.state.directory);
         }
 
-        setDirectory = (event: React.MouseEvent<HTMLElement>, { data }: TreeItem<ProjectsTreePickerItem>, pickerId: string) => {
+        setDirectory = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
             if ('kind' in data && data.kind === ResourceKind.COLLECTION) {
                 this.setState({ directory: data });
             } else {
index 86ff6fb14c2e733b21ed792e5e2ef8fb0dd9f64d..3b0289e79f4a8fee08ecde6546f99f7cf853b5d0 100644 (file)
@@ -3,9 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { EnumCommandInputParameter, CommandInputEnumSchema } from '~/models/workflow';
 import { Field } from 'redux-form';
 import { Select, MenuItem } from '@material-ui/core';
+import { EnumCommandInputParameter, CommandInputEnumSchema } from '~/models/workflow';
 import { GenericInputProps, GenericInput } from './generic-input';
 
 export interface EnumInputProps {
@@ -30,8 +30,20 @@ const Input = (props: GenericInputProps) => {
         onChange={props.input.onChange}
         disabled={props.commandInput.disabled} >
         {type.symbols.map(symbol =>
-            <MenuItem key={symbol} value={symbol.split('/').pop()}>
-                {symbol.split('/').pop()}
+            <MenuItem key={symbol} value={extractValue(symbol)}>
+                {extractValue(symbol)}
             </MenuItem>)}
     </Select>;
-};
\ No newline at end of file
+};
+
+/**
+ * Values in workflow definition have an absolute form, for example: 
+ * 
+ * ```#input_collector.cwl/enum_type/Pathway table```
+ * 
+ * We want a value that is in form accepted by backend.
+ * According to the example above, the correct value is:
+ * 
+ * ```Pathway table```
+ */
+const extractValue = (symbol: string) => symbol.split('/').pop();
index 7e0925e8e9e175481887c1c9988be0eee329f82e..0611100722a2e30512a675dc770b933d34cd88b6 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { memoize } from 'lodash/fp';
 import {
     isRequiredInput,
     FileCommandInputParameter,
@@ -28,18 +29,24 @@ export const FileInput = ({ input }: FileInputProps) =>
         name={input.id}
         commandInput={input}
         component={FileInputComponent}
-        format={(value?: File) => value ? value.basename : ''}
-        parse={(file: CollectionFile): File => ({
-            class: CWLType.FILE,
-            location: `keep:${file.id}`,
-            basename: file.name,
-        })}
-        validate={[
-            isRequiredInput(input)
-                ? (file?: File) => file ? undefined : ERROR_MESSAGE
-                : () => undefined,
-        ]} />;
+        format={format}
+        parse={parse}
+        validate={getValidation(input)} />;
 
+const format = (value?: File) => value ? value.basename : '';
+
+const parse = (file: CollectionFile): File => ({
+    class: CWLType.FILE,
+    location: `keep:${file.id}`,
+    basename: file.name,
+});
+
+const getValidation = memoize(
+    (input: FileCommandInputParameter) => ([
+        isRequiredInput(input)
+            ? (file?: File) => file ? undefined : ERROR_MESSAGE
+            : () => undefined,
+    ]));
 
 interface FileInputComponentState {
     open: boolean;
@@ -77,7 +84,7 @@ const FileInputComponent = connect()(
             this.props.input.onChange(this.state.file);
         }
 
-        setFile = (event: React.MouseEvent<HTMLElement>, { data }: TreeItem<ProjectsTreePickerItem>, pickerId: string) => {
+        setFile = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
             if ('type' in data && data.type === CollectionFileType.FILE) {
                 this.setState({ file: data });
             } else {
index 56a58012b260bc2b6492671bc760d12f0479e4eb..a5905dc586d16bb684bfa3b9c443062c5a4e0ebc 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { memoize } from 'lodash/fp';
 import { FloatCommandInputParameter, isRequiredInput } from '~/models/workflow';
 import { Field } from 'redux-form';
 import { isNumber } from '~/validators/is-number';
@@ -17,11 +18,17 @@ export const FloatInput = ({ input }: FloatInputProps) =>
         commandInput={input}
         component={Input}
         parse={parseFloat}
-        format={value => isNaN(value) ? '' : JSON.stringify(value)}
-        validate={[
-            isRequiredInput(input)
-                ? isNumber
-                : () => undefined,]} />;
+        format={format}
+        validate={getValidation(input)} />;
+
+const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
+
+const getValidation = memoize(
+    (input: FloatCommandInputParameter) => ([
+        isRequiredInput(input)
+            ? isNumber
+            : () => undefined,])
+);
 
 const Input = (props: GenericInputProps) =>
     <GenericInput
index 413ee49c7abc0eee00961fd66f941e2b71ef19ad..32ebeb75c27bc8ee6be8ed186b5051a5dcff0ab0 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { memoize } from 'lodash/fp';
 import { IntCommandInputParameter, isRequiredInput } from '~/models/workflow';
 import { Field } from 'redux-form';
 import { isInteger } from '~/validators/is-integer';
@@ -17,13 +18,20 @@ export const IntInput = ({ input }: IntInputProps) =>
         name={input.id}
         commandInput={input}
         component={InputComponent}
-        parse={value => parseInt(value, 10)}
-        format={value => isNaN(value) ? '' : JSON.stringify(value)}
-        validate={[
-            isRequiredInput(input)
-                ? isInteger
-                : () => undefined,
-        ]} />;
+        parse={parse}
+        format={format}
+        validate={getValidation(input)} />;
+
+const parse = (value: any) => parseInt(value, 10);
+
+const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
+
+const getValidation = memoize(
+    (input: IntCommandInputParameter) => ([
+        isRequiredInput(input)
+            ? isInteger
+            : () => undefined,
+    ]));
 
 const InputComponent = (props: GenericInputProps) =>
     <GenericInput
index f6b50a7c47dd5bb417e75e503520f0be6ee691d9..7fc74315e98d6f3739381c10da74c3ea300438eb 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { memoize } from 'lodash/fp';
 import { isRequiredInput, StringCommandInputParameter } from '~/models/workflow';
 import { Field } from 'redux-form';
 import { require } from '~/validators/require';
@@ -17,11 +18,14 @@ export const StringInput = ({ input }: StringInputProps) =>
         name={input.id}
         commandInput={input}
         component={StringInputComponent}
-        validate={[
-            isRequiredInput(input)
-                ? require
-                : () => undefined,
-        ]} />;
+        validate={getValidation(input)} />;
+
+const getValidation = memoize(
+    (input: StringCommandInputParameter) => ([
+        isRequiredInput(input)
+            ? require
+            : () => undefined,
+    ]));
 
 const StringInputComponent = (props: GenericInputProps) =>
     <GenericInput
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 2cff4317e5576470c44e22ff910a043302a4aa3f..5efffa19ac26b705233cd24c3fb4e08578a6c32f 100644 (file)
@@ -52,14 +52,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';
 
@@ -132,6 +140,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} />
                                 <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
                             </Switch>
                         </Grid>
@@ -142,6 +152,8 @@ export const WorkbenchPanel =
                 <DetailsPanel />
             </Grid>
             <AdvancedTabDialog />
+            <AttributesComputeNodeDialog />
+            <AttributesKeepServiceDialog />
             <AttributesSshKeyDialog />
             <ChangeWorkflowDialog />
             <ContextMenu />
@@ -163,9 +175,12 @@ export const WorkbenchPanel =
             <ProcessCommandDialog />
             <ProcessInputDialog />
             <ProjectPropertiesDialog />
+            <RemoveComputeNodeDialog />
+            <RemoveKeepServiceDialog />
             <RemoveProcessDialog />
             <RemoveRepositoryDialog />
             <RemoveSshKeyDialog />
+            <RemoveVirtualMachineDialog />
             <RenameFileDialog />
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />
@@ -175,5 +190,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 1eaa15f871ffe38692a88a660cb4b20300da648b..f08c370f5d3fb8121d01411f0519fda30590e7ba 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"