19675: Merge branch '19675-instance-types-panel' from arvados-workbench2.git 19675-test1
authorStephen Smith <stephen@curii.com>
Thu, 7 Dec 2023 20:35:09 +0000 (15:35 -0500)
committerStephen Smith <stephen@curii.com>
Thu, 7 Dec 2023 20:35:09 +0000 (15:35 -0500)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

14 files changed:
services/workbench2/src/common/config.ts
services/workbench2/src/common/formatters.test.ts
services/workbench2/src/common/formatters.ts
services/workbench2/src/components/icon/icon.tsx
services/workbench2/src/routes/route-change-handlers.ts
services/workbench2/src/routes/routes.ts
services/workbench2/src/store/breadcrumbs/breadcrumbs-actions.ts
services/workbench2/src/store/navigation/navigation-action.ts
services/workbench2/src/store/workbench/workbench-actions.ts
services/workbench2/src/views-components/main-app-bar/account-menu.tsx
services/workbench2/src/views/instance-types-panel/instance-types-panel.test.tsx [new file with mode: 0644]
services/workbench2/src/views/instance-types-panel/instance-types-panel.tsx [new file with mode: 0644]
services/workbench2/src/views/process-panel/process-details-attributes.tsx
services/workbench2/src/views/workbench/workbench.tsx

index eff998ae5ea45cff369d753d249acb8c6a510684..ed99e7d9749491491a1a495ee5fcc4f7dfc50021 100644 (file)
@@ -19,6 +19,25 @@ export interface ClusterConfigJSON {
         MaxItemsPerResponse: number
     },
     ClusterID: string;
+    Containers: {
+        ReserveExtraRAM: number;
+    },
+    InstanceTypes?: {
+        [key: string]: {
+            AddedScratch: number;
+            CUDA?: {
+                DeviceCount: number;
+                DriverVersion: string;
+                HardwareCapability: string;
+            };
+            IncludedScratch: number;
+            Preemptible: boolean;
+            Price: number;
+            ProviderType: string;
+            RAM: number;
+            VCPUs: number;
+        };
+    };
     RemoteClusters: {
         [key: string]: {
             ActivateUsers: boolean
@@ -276,6 +295,9 @@ export const mockClusterConfigJSON = (
         MaxItemsPerResponse: 1000,
     },
     ClusterID: '',
+    Containers: {
+        ReserveExtraRAM: 576716800,
+    },
     RemoteClusters: {},
     Services: {
         Controller: { ExternalURL: '' },
index 048779727e4865724e1bdcd67e862d3012e6a361..7f9ffa0c35618007f32cf3b2de0b9623a22d00fd 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { formatUploadSpeed, formatContainerCost } from "./formatters";
+import { formatUploadSpeed, formatCost } from "./formatters";
 
 describe('formatUploadSpeed', () => {
     it('should show speed less than 1MB/s', () => {
@@ -30,16 +30,16 @@ describe('formatUploadSpeed', () => {
 
 describe('formatContainerCost', () => {
     it('should correctly round to tenth of a cent', () => {
-        expect(formatContainerCost(0.0)).toBe('$0');
-        expect(formatContainerCost(0.125)).toBe('$0.125');
-        expect(formatContainerCost(0.1254)).toBe('$0.125');
-        expect(formatContainerCost(0.1255)).toBe('$0.126');
+        expect(formatCost(0.0)).toBe('$0');
+        expect(formatCost(0.125)).toBe('$0.125');
+        expect(formatCost(0.1254)).toBe('$0.125');
+        expect(formatCost(0.1255)).toBe('$0.126');
     });
 
     it('should round up any smaller value to 0.001', () => {
-        expect(formatContainerCost(0.0)).toBe('$0');
-        expect(formatContainerCost(0.001)).toBe('$0.001');
-        expect(formatContainerCost(0.0001)).toBe('$0.001');
-        expect(formatContainerCost(0.00001)).toBe('$0.001');
+        expect(formatCost(0.0)).toBe('$0');
+        expect(formatCost(0.001)).toBe('$0.001');
+        expect(formatCost(0.0001)).toBe('$0.001');
+        expect(formatCost(0.00001)).toBe('$0.001');
     });
 });
index a38609a678661c420d5b88a23197540feb859dca..3366af0d561e233d644dbe51fdb1ef88a530e4a2 100644 (file)
@@ -116,7 +116,7 @@ export const formatPropertyValue = (
     return '';
 };
 
-export const formatContainerCost = (cost: number): string => {
+export const formatCost = (cost: number): string => {
     const decimalPlaces = 3;
 
     const factor = Math.pow(10, decimalPlaces);
index 998cd8059aabe97f1d56e0e9d6c699dea876e06b..69ebffd7ed9ce4af67bd57fe8a7625991ffb4bec 100644 (file)
@@ -77,6 +77,7 @@ import NotInterested from "@material-ui/icons/NotInterested";
 import Image from "@material-ui/icons/Image";
 import Stop from "@material-ui/icons/Stop";
 import FileCopy from "@material-ui/icons/FileCopy";
+import Storage from "@material-ui/icons/Storage";
 
 // Import FontAwesome icons
 import { library } from "@fortawesome/fontawesome-svg-core";
@@ -267,3 +268,4 @@ export const StartIcon: IconType = props => <PlayArrow {...props} />;
 export const StopIcon: IconType = props => <Stop {...props} />;
 export const SelectAllIcon: IconType = props => <CheckboxMultipleOutline {...props} />;
 export const SelectNoneIcon: IconType = props => <CheckboxMultipleBlankOutline {...props} />;
+export const InstanceTypeIcon: IconType = props => <Storage {...props} />;
index bdc1ddc06325bb473dc7493595df0f42fd56c5a7..58887452533d0df58575b1c6cadf8d610ea889b5 100644 (file)
@@ -36,6 +36,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const virtualMachineAdminMatch = Routes.matchAdminVirtualMachineRoute(pathname);
     const sshKeysUserMatch = Routes.matchSshKeysUserRoute(pathname);
     const sshKeysAdminMatch = Routes.matchSshKeysAdminRoute(pathname);
+    const instanceTypesMatch = Routes.matchInstanceTypesRoute(pathname);
     const siteManagerMatch = Routes.matchSiteManagerRoute(pathname);
     const keepServicesMatch = Routes.matchKeepServicesRoute(pathname);
     const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
@@ -92,6 +93,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(WorkbenchActions.loadSshKeys);
     } else if (sshKeysAdminMatch) {
         store.dispatch(WorkbenchActions.loadSshKeys);
+    } else if (instanceTypesMatch) {
+        store.dispatch(WorkbenchActions.loadInstanceTypes);
     } else if (siteManagerMatch) {
         store.dispatch(WorkbenchActions.loadSiteManager);
     } else if (keepServicesMatch) {
index 4dfd998e8dadcc08450fc727b4389c03d199c5c0..cbb09a500d9117d7556bcc5db657199b88994116 100644 (file)
@@ -35,6 +35,7 @@ export const Routes = {
     SEARCH_RESULTS: '/search-results',
     SSH_KEYS_ADMIN: `/ssh-keys-admin`,
     SSH_KEYS_USER: `/ssh-keys-user`,
+    INSTANCE_TYPES: `/instance-types`,
     SITE_MANAGER: `/site-manager`,
     MY_ACCOUNT: '/my-account',
     LINK_ACCOUNT: '/link_account',
@@ -161,9 +162,12 @@ export const matchRepositoriesRoute = (route: string) =>
 export const matchSshKeysUserRoute = (route: string) =>
     matchPath(route, { path: Routes.SSH_KEYS_USER });
 
-export const matchSshKeysAdminRoute = (route: string) =>
+    export const matchSshKeysAdminRoute = (route: string) =>
     matchPath(route, { path: Routes.SSH_KEYS_ADMIN });
 
+export const matchInstanceTypesRoute = (route: string) =>
+    matchPath(route, { path: Routes.INSTANCE_TYPES });
+
 export const matchSiteManagerRoute = (route: string) =>
     matchPath(route, { path: Routes.SITE_MANAGER });
 
index 9aebeb904c64115e574624163718d2fea43bcb82..80348f3791a183f02b28d91a73b8204310e7eb8e 100644 (file)
@@ -20,7 +20,7 @@ import { ProcessResource } from 'models/process';
 import { OrderBuilder } from 'services/api/order-builder';
 import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
 import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
-import { CollectionIcon, IconType, ProcessIcon, ProjectIcon, WorkflowIcon } from 'components/icon/icon';
+import { AdminMenuIcon, CollectionIcon, IconType, InstanceTypeIcon, ProcessIcon, ProjectIcon, WorkflowIcon } from 'components/icon/icon';
 import { CollectionResource } from 'models/collection';
 import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree';
 import { WorkflowResource } from 'models/workflow';
@@ -290,3 +290,39 @@ export const setMyAccountBreadcrumbs = () =>
             { label: MY_ACCOUNT_PANEL_LABEL, uuid: MY_ACCOUNT_PANEL_LABEL },
         ]));
     };
+
+export const INSTANCE_TYPES_PANEL_LABEL = 'Instance Types';
+
+export const setInstanceTypesBreadcrumbs = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([
+            { label: INSTANCE_TYPES_PANEL_LABEL, uuid: INSTANCE_TYPES_PANEL_LABEL, icon: InstanceTypeIcon },
+        ]));
+    };
+
+export const VIRTUAL_MACHINES_USER_PANEL_LABEL = 'Virtual Machines';
+
+export const setVirtualMachinesBreadcrumbs = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([
+            { label: VIRTUAL_MACHINES_USER_PANEL_LABEL, uuid: VIRTUAL_MACHINES_USER_PANEL_LABEL },
+        ]));
+    };
+
+export const VIRTUAL_MACHINES_ADMIN_PANEL_LABEL = 'Virtual Machines Admin';
+
+export const setVirtualMachinesAdminBreadcrumbs = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([
+            { label: VIRTUAL_MACHINES_ADMIN_PANEL_LABEL, uuid: VIRTUAL_MACHINES_ADMIN_PANEL_LABEL, icon: AdminMenuIcon },
+        ]));
+    };
+
+export const REPOSITORIES_PANEL_LABEL = 'Repositories';
+
+export const setRepositoriesBreadcrumbs = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([
+            { label: REPOSITORIES_PANEL_LABEL, uuid: REPOSITORIES_PANEL_LABEL, icon: AdminMenuIcon },
+        ]));
+    };
index 55112fb0ae44ab153e89ce41058205d5fb0d5ca6..3ab5cd9570b1d43a30adf9cb3bd6f9f94cf5ef2b 100644 (file)
@@ -11,7 +11,7 @@ import { RootState } from "store/store";
 import { ServiceRepository } from "services/services";
 import { pluginConfig } from "plugins";
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from "store/breadcrumbs/breadcrumbs-actions";
+import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL, INSTANCE_TYPES_PANEL_LABEL, VIRTUAL_MACHINES_USER_PANEL_LABEL, VIRTUAL_MACHINES_ADMIN_PANEL_LABEL, REPOSITORIES_PANEL_LABEL } from "store/breadcrumbs/breadcrumbs-actions";
 
 export const navigationNotAvailable = (id: string) =>
     snackbarActions.OPEN_SNACKBAR({
@@ -78,6 +78,18 @@ export const navigateTo = (uuid: string) => async (dispatch: Dispatch, getState:
         case MY_ACCOUNT_PANEL_LABEL:
             dispatch(navigateToMyAccount);
             return;
+        case INSTANCE_TYPES_PANEL_LABEL:
+            dispatch(navigateToInstanceTypes);
+            return;
+        case VIRTUAL_MACHINES_USER_PANEL_LABEL:
+            dispatch(navigateToUserVirtualMachines);
+            return;
+        case VIRTUAL_MACHINES_ADMIN_PANEL_LABEL:
+            dispatch(navigateToAdminVirtualMachines);
+            return;
+        case REPOSITORIES_PANEL_LABEL:
+            dispatch(navigateToRepositories);
+            return;
     }
 
     dispatch(navigationNotAvailable(uuid));
@@ -132,6 +144,8 @@ export const navigateToSshKeysAdmin = push(Routes.SSH_KEYS_ADMIN);
 
 export const navigateToSshKeysUser = push(Routes.SSH_KEYS_USER);
 
+export const navigateToInstanceTypes = push(Routes.INSTANCE_TYPES);
+
 export const navigateToSiteManager = push(Routes.SITE_MANAGER);
 
 export const navigateToMyAccount = push(Routes.MY_ACCOUNT);
index e89a95e039df04e19abbcf47e00237643b24998a..b12f52b58f2c643325356a5880e67651fb36a6de 100644 (file)
@@ -21,7 +21,6 @@ import { projectPanelColumns } from "views/project-panel/project-panel";
 import { favoritePanelColumns } from "views/favorite-panel/favorite-panel";
 import { matchRootRoute } from "routes/routes";
 import {
-    setBreadcrumbs,
     setGroupDetailsBreadcrumbs,
     setGroupsBreadcrumbs,
     setProcessBreadcrumbs,
@@ -31,6 +30,10 @@ import {
     setUsersBreadcrumbs,
     setMyAccountBreadcrumbs,
     setUserProfileBreadcrumbs,
+    setInstanceTypesBreadcrumbs,
+    setVirtualMachinesBreadcrumbs,
+    setVirtualMachinesAdminBreadcrumbs,
+    setRepositoriesBreadcrumbs,
 } from "store/breadcrumbs/breadcrumbs-actions";
 import { navigateTo, navigateToRootProject } from "store/navigation/navigation-action";
 import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
@@ -95,7 +98,6 @@ import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-
 import { subprocessPanelColumns } from "views/subprocess-panel/subprocess-panel-root";
 import { loadAllProcessesPanel, allProcessesPanelActions } from "../all-processes-panel/all-processes-panel-action";
 import { allProcessesPanelColumns } from "views/all-processes-panel/all-processes-panel";
-import { AdminMenuIcon } from "components/icon/icon";
 import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
 import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
 import { multiselectActions } from "store/multiselect/multiselect-actions";
@@ -742,23 +744,27 @@ export const loadLinks = handleFirstTimeLoad(async (dispatch: Dispatch<any>) =>
 
 export const loadVirtualMachines = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
     await dispatch(loadVirtualMachinesPanel());
-    dispatch(setBreadcrumbs([{ label: "Virtual Machines" }]));
+    dispatch(setVirtualMachinesBreadcrumbs());
 });
 
 export const loadVirtualMachinesAdmin = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
     await dispatch(loadVirtualMachinesPanel());
-    dispatch(setBreadcrumbs([{ label: "Virtual Machines Admin", icon: AdminMenuIcon }]));
+    dispatch(setVirtualMachinesAdminBreadcrumbs());
 });
 
 export const loadRepositories = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
     await dispatch(loadRepositoriesPanel());
-    dispatch(setBreadcrumbs([{ label: "Repositories" }]));
+    dispatch(setRepositoriesBreadcrumbs());
 });
 
 export const loadSshKeys = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
     await dispatch(loadSshKeysPanel());
 });
 
+export const loadInstanceTypes = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+    dispatch(setInstanceTypesBreadcrumbs());
+});
+
 export const loadSiteManager = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
     await dispatch(loadSiteManagerPanel());
 });
index c2cc0e2a47c772502d35f46add124bd62273fbf6..b1a71ef25ebcdde71e6b78bc673debc31fe38f53 100644 (file)
@@ -17,7 +17,8 @@ import {
     navigateToSiteManager,
     navigateToSshKeysUser,
     navigateToMyAccount,
-    navigateToLinkAccount
+    navigateToLinkAccount,
+    navigateToInstanceTypes
 } from 'store/navigation/navigation-action';
 import { openUserVirtualMachines } from "store/virtual-machines/virtual-machines-actions";
 import { pluginConfig } from 'plugins';
@@ -58,6 +59,7 @@ export const AccountMenuComponent =
                 dispatch(openTokenDialog);
             }}>Get API token</MenuItem>
             <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
+            <MenuItem onClick={() => dispatch(navigateToInstanceTypes)}>Instance Types</MenuItem>
             <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
             <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>
             <MenuItem onClick={() => dispatch(navigateToLinkAccount)}>Link account</MenuItem>
diff --git a/services/workbench2/src/views/instance-types-panel/instance-types-panel.test.tsx b/services/workbench2/src/views/instance-types-panel/instance-types-panel.test.tsx
new file mode 100644 (file)
index 0000000..ca27051
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { configure, mount } from "enzyme";
+import { InstanceTypesPanel } from './instance-types-panel';
+import Adapter from "enzyme-adapter-react-16";
+import { combineReducers, createStore } from "redux";
+import { Provider } from "react-redux";
+import { formatFileSize } from 'common/formatters';
+
+configure({ adapter: new Adapter() });
+
+describe('<InstanceTypesPanel />', () => {
+
+    // let props;
+    let store;
+
+    const initialAuthState = {
+        config: {
+            clusterConfig: {
+                InstanceTypes: {
+                    "normalType" : {
+                        ProviderType: "provider",
+                        Price: 0.123,
+                        VCPUs: 6,
+                        Preemptible: false,
+                        IncludedScratch: 1000,
+                        RAM: 5000,
+                    },
+                    "gpuType" : {
+                        ProviderType: "gpuProvider",
+                        Price: 0.456,
+                        VCPUs: 8,
+                        Preemptible: true,
+                        IncludedScratch: 500,
+                        RAM: 6000,
+                        CUDA: {
+                            DeviceCount: 1,
+                            HardwareCapability: '8.6',
+                            DriverVersion: '11.4',
+                        },
+                    },
+                },
+                Containers: {
+                    ReserveExtraRAM: 1000,
+                }
+            }
+        }
+    }
+
+    beforeEach(() => {
+
+        store = createStore(combineReducers({
+            auth: (state: any = initialAuthState, action: any) => state,
+        }));
+    });
+
+    it('renders instance types', () => {
+        // when
+        const panel = mount(
+            <Provider store={store}>
+                <InstanceTypesPanel />
+            </Provider>);
+
+        // then
+        Object.keys(initialAuthState.config.clusterConfig.InstanceTypes).forEach((instanceKey) => {
+            const instanceType = initialAuthState.config.clusterConfig.InstanceTypes[instanceKey];
+            const item = panel.find(`Grid[data-cy="${instanceKey}"]`)
+
+            expect(item.find('h6').text()).toContain(instanceKey);
+            expect(item.text()).toContain(`Provider type: ${instanceType.ProviderType}`);
+            expect(item.text()).toContain(`Price: $${instanceType.Price}`);
+            expect(item.text()).toContain(`Cores: ${instanceType.VCPUs}`);
+            expect(item.text()).toContain(`Preemptible: ${instanceType.Preemptible.toString()}`);
+            expect(item.text()).toContain(`Max disk request: ${formatFileSize(instanceType.IncludedScratch)}`);
+            expect(item.text()).toContain(`Max ram request: ${formatFileSize(instanceType.RAM - initialAuthState.config.clusterConfig.Containers.ReserveExtraRAM)}`);
+            if (instanceType.CUDA && instanceType.CUDA.DeviceCount > 0) {
+                expect(item.text()).toContain(`CUDA GPUs: ${instanceType.CUDA.DeviceCount}`);
+                expect(item.text()).toContain(`Hardware capability: ${instanceType.CUDA.HardwareCapability}`);
+                expect(item.text()).toContain(`Driver version: ${instanceType.CUDA.DriverVersion}`);
+            }
+        });
+    });
+});
diff --git a/services/workbench2/src/views/instance-types-panel/instance-types-panel.tsx b/services/workbench2/src/views/instance-types-panel/instance-types-panel.tsx
new file mode 100644 (file)
index 0000000..a18f5d2
--- /dev/null
@@ -0,0 +1,97 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Typography, Grid } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { InstanceTypeIcon } from 'components/icon/icon';
+import { RootState } from 'store/store';
+import { connect } from 'react-redux';
+import { ClusterConfigJSON } from 'common/config';
+import { NotFoundView } from 'views/not-found-panel/not-found-panel';
+import { formatCost, formatFileSize } from 'common/formatters';
+
+type CssRules = 'root' | 'instanceType';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+       width: '100%',
+       overflow: 'auto'
+    },
+    instanceType: {
+        padding: "10px",
+    },
+});
+
+type InstanceTypesPanelConnectedProps = {config: ClusterConfigJSON};
+
+type InstanceTypesPanelRootProps = InstanceTypesPanelConnectedProps & WithStyles<CssRules>;
+
+const mapStateToProps = ({auth}: RootState): InstanceTypesPanelConnectedProps => ({
+    config: auth.config.clusterConfig,
+});
+
+export const InstanceTypesPanel = withStyles(styles)(connect(mapStateToProps)(
+    ({ config, classes }: InstanceTypesPanelRootProps) => {
+
+        const instances = config.InstanceTypes || {};
+
+        return <Card className={classes.root}>
+            <CardContent>
+                <Grid container direction="row">
+                    {Object.keys(instances).length > 0 ?
+                        Object.keys(instances).map((instanceKey) => {
+                            const instanceType = instances[instanceKey];
+
+                            return <Grid data-cy={instanceKey} className={classes.instanceType} item sm={6} xs={12} key={instanceKey}>
+                                <Card>
+                                    <CardContent>
+                                        <Typography variant="h6">
+                                            {instanceKey}
+                                        </Typography>
+                                        <Typography>
+                                            Provider type: {instanceType.ProviderType}
+                                        </Typography>
+                                        <Typography>
+                                            Price: {formatCost(instanceType.Price)}
+                                        </Typography>
+                                        <Typography>
+                                            Cores: {instanceType.VCPUs}
+                                        </Typography>
+                                        <Typography>
+                                            Preemptible: {instanceType.Preemptible.toString()}
+                                        </Typography>
+                                        <Typography>
+                                            Max disk request: {formatFileSize(instanceType.IncludedScratch)}
+                                        </Typography>
+                                        <Typography>
+                                            Max ram request: {formatFileSize(instanceType.RAM - config.Containers.ReserveExtraRAM)}
+                                        </Typography>
+                                        {instanceType.CUDA && instanceType.CUDA.DeviceCount > 0 ?
+                                            <>
+                                                <Typography>
+                                                    CUDA GPUs: {instanceType.CUDA.DeviceCount}
+                                                </Typography>
+                                                <Typography>
+                                                    Hardware capability: {instanceType.CUDA.HardwareCapability}
+                                                </Typography>
+                                                <Typography>
+                                                    Driver version: {instanceType.CUDA.DriverVersion}
+                                                </Typography>
+                                            </> : <></>
+                                        }
+                                    </CardContent>
+                                </Card>
+                            </Grid>
+                        }) :
+                        <NotFoundView
+                            icon={InstanceTypeIcon}
+                            messages={["No instances found"]}
+                        />
+                    }
+                </Grid>
+            </CardContent>
+        </Card>
+    }
+));
index ffacd967f4d8e289edf6f9bf66bf31a3c59d2ea1..4e5c038386f9ce400f0943e9ae80701af29513b4 100644 (file)
@@ -5,7 +5,7 @@
 import React from "react";
 import { Grid, StyleRulesCallback, withStyles } from "@material-ui/core";
 import { Dispatch } from 'redux';
-import { formatContainerCost, formatDate } from "common/formatters";
+import { formatCost, formatDate } from "common/formatters";
 import { resourceLabel } from "common/labels";
 import { DetailsAttribute } from "components/details-attribute/details-attribute";
 import { ResourceKind } from "models/resource";
@@ -162,7 +162,7 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                 </Grid>
                 {container && <Grid item xs={12} md={mdSize}>
                     <DetailsAttribute label='Cost' value={
-                        `${hasTotalCost ? formatContainerCost(containerRequest.cumulativeCost) + ' total, ' : (totalCostNotReady ? 'total pending completion, ' : '')}${container.cost > 0 ? formatContainerCost(container.cost) : 'not available'} for this container`
+                        `${hasTotalCost ? formatCost(containerRequest.cumulativeCost) + ' total, ' : (totalCostNotReady ? 'total pending completion, ' : '')}${container.cost > 0 ? formatCost(container.cost) : 'not available'} for this container`
                     } />
 
                     {container && workflowCollection && <Grid item xs={12} md={mdSize}>
index 4a2cd7009804335bfb5534946cc7cbbf529d46e9..05ea215dd9a2de0716b17a2778eec722ac789c18 100644 (file)
@@ -106,6 +106,7 @@ import { pluginConfig } from "plugins";
 import { ElementListReducer } from "common/plugintypes";
 import { COLLAPSE_ICON_SIZE } from "views-components/side-panel-toggle/side-panel-toggle";
 import { Banner } from "views-components/baner/banner";
+import { InstanceTypesPanel } from "views/instance-types-panel/instance-types-panel";
 
 type CssRules = "root" | "container" | "splitter" | "asidePanel" | "contentWrapper" | "content";
 
@@ -228,6 +229,10 @@ let routes = (
             path={Routes.SSH_KEYS_ADMIN}
             component={SshKeyAdminPanel}
         />
+        <Route
+            path={Routes.INSTANCE_TYPES}
+            component={InstanceTypesPanel}
+        />
         <Route
             path={Routes.SITE_MANAGER}
             component={SiteManagerPanel}