}
```
+#### 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
"classnames": "2.2.6",
"cwlts": "1.15.29",
"debounce": "1.2.0",
+ "is-image": "2.0.0",
"js-yaml": "3.12.0",
"lodash": "4.17.11",
"react": "16.5.2",
--- /dev/null
+{
+ "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
.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,
})));
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import isImage from 'is-image';
+import { withStyles, WithStyles } from '@material-ui/core';
+import { FileTreeData } from '~/components/file-tree/file-tree-data';
+import { CollectionFileType } from '~/models/collection-file';
+
+export interface FileThumbnailProps {
+ file: FileTreeData;
+}
+
+export const FileThumbnail =
+ ({ file }: FileThumbnailProps) =>
+ file.type === CollectionFileType.FILE && isImage(file.name)
+ ? <ImageFileThumbnail file={file} />
+ : null;
+
+type ImageFileThumbnailCssRules = 'thumbnail';
+
+const imageFileThumbnailStyle = withStyles<ImageFileThumbnailCssRules>(theme => ({
+ thumbnail: {
+ maxWidth: 250,
+ margin: `${theme.spacing.unit}px 0`,
+ }
+}));
+
+const ImageFileThumbnail = imageFileThumbnailStyle(
+ ({ classes, file }: WithStyles<ImageFileThumbnailCssRules> & FileThumbnailProps) =>
+ <img
+ className={classes.thumbnail}
+ alt={file.name}
+ src={file.url} />
+);
export interface FileTreeData {
name: string;
type: string;
+ url: string;
size?: number;
}
import { formatFileSize } from "~/common/formatters";
import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
import { FileTreeData } from "./file-tree-data";
+import { FileThumbnail } from '~/components/file-tree/file-thumbnail';
type CssRules = "root" | "spacer" | "sizeInfo" | "button" | "moreOptions";
class extends React.Component<FileTreeItemProps & WithStyles<CssRules>> {
render() {
const { classes, item } = this.props;
- return <div className={classes.root}>
- <ListItemTextIcon
- icon={getIcon(item)}
- name={item.data.name} />
- <div className={classes.spacer} />
- <Typography
- className={classes.sizeInfo}
- variant="caption">{formatFileSize(item.data.size)}</Typography>
- <Tooltip title="More options" disableFocusListener>
- <IconButton
- className={classes.button}
- onClick={this.handleClick}>
- <MoreOptionsIcon className={classes.moreOptions}/>
- </IconButton>
- </Tooltip>
- </div >;
+ return <>
+ <div className={classes.root}>
+ <ListItemTextIcon
+ icon={getIcon(item)}
+ name={item.data.name} />
+ <div className={classes.spacer} />
+ <Typography
+ className={classes.sizeInfo}
+ variant="caption">{formatFileSize(item.data.size)}</Typography>
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton
+ className={classes.button}
+ onClick={this.handleClick}>
+ <MoreOptionsIcon className={classes.moreOptions} />
+ </IconButton>
+ </Tooltip>
+ </div >
+ <FileThumbnail file={item.data} />
+ </>;
}
handleClick = (event: React.MouseEvent<any>) => {
import { 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()}]`);
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 }) => {
-// 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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from '~/models/resource';
+
+export interface NodeResource extends Resource {
+ slotNumber: number;
+ hostname: string;
+ domain: string;
+ ipAddress: string;
+ jobUuid: string;
+ firstPingAt: string;
+ lastPingAt: string;
+ status: string;
+ info: NodeInfo;
+ properties: NodeProperties;
+}
+
+export interface NodeInfo {
+ lastAction: string;
+ pingSecret: string;
+ ec2InstanceId: string;
+ slurmState?: string;
+}
+
+export interface NodeProperties {
+ cloudNode: CloudNode;
+ totalRamMb: number;
+ totalCpuCores: number;
+ totalScratchMb: number;
+}
+
+interface CloudNode {
+ size: string;
+ price: number;
+}
\ No newline at end of file
CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
LOG = "arvados#log",
+ NODE = "arvados#node",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
REPOSITORY = "arvados#repository",
SSH_KEY = "arvados#authorizedKeys",
+ KEEP_SERVICE = "arvados#keepService",
USER = "arvados#user",
VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
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}';
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;
}
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
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);
};
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);
}
};
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) => {
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
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';
}
public getIsAdmin(): boolean {
- return !!localStorage.getItem(USER_IS_ADMIN);
+ return localStorage.getItem(USER_IS_ADMIN) === 'true';
}
public getUser(): User | undefined {
\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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { NodeResource } from '~/models/node';
+import { ApiActions } from '~/services/api/api-actions';
+
+export class NodeService extends CommonResourceService<NodeResource> {
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "nodes", actions);
+ }
+}
\ No newline at end of file
import { RepositoriesService } from '~/services/repositories-service/repositories-service';
import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service';
import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service';
+import { NodeService } from '~/services/node-service/node-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const keepService = new KeepService(apiClient, actions);
const linkService = new LinkService(apiClient, actions);
const logService = new LogService(apiClient, actions);
+ const nodeService = new NodeService(apiClient, actions);
const permissionService = new PermissionService(apiClient, actions);
const projectService = new ProjectService(apiClient, actions);
const repositoriesService = new RepositoriesService(apiClient, actions);
keepService,
linkService,
logService,
+ nodeService,
permissionService,
projectService,
repositoriesService,
import { 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;
}
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 }));
}
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,
const cliUpdateExample = (uuid: string, resourceKind: string, resource: string | string[], resourceName: string) => {
const CLIUpdateCollectionExample = `arv ${resourceKind} update \\
--uuid ${uuid} \\
- --${resourceKind} '{"${resourceName}":${resource}}'`;
+ --${resourceKind} '{"${resourceName}":${JSON.stringify(resource)}}'`;
return CLIUpdateCollectionExample;
};
https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
<<EOF
{
- "${resourceName}": ${resource}
+ "${resourceName}": ${JSON.stringify(resource, null, 4)}
}
EOF`;
"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
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 } }));
};
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());
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { RootState } from '~/store/store';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { ServiceRepository } from "~/services/services";
+import { NodeResource } from '~/models/node';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { navigateToRootProject } from '~/store/navigation/navigation-action';
+
+export const computeNodesActions = unionize({
+ SET_COMPUTE_NODES: ofType<NodeResource[]>(),
+ REMOVE_COMPUTE_NODE: ofType<string>()
+});
+
+export type ComputeNodesActions = UnionOf<typeof computeNodesActions>;
+
+export const COMPUTE_NODE_REMOVE_DIALOG = 'computeNodeRemoveDialog';
+export const COMPUTE_NODE_ATTRIBUTES_DIALOG = 'computeNodeAttributesDialog';
+
+export const loadComputeNodesPanel = () =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const user = getState().auth.user;
+ if (user && user.isAdmin) {
+ try {
+ dispatch(setBreadcrumbs([{ label: 'Compute Nodes' }]));
+ const response = await services.nodeService.list();
+ dispatch(computeNodesActions.SET_COMPUTE_NODES(response.items));
+ } catch (e) {
+ return;
+ }
+ } else {
+ dispatch(navigateToRootProject);
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 }));
+ }
+ };
+
+export const openComputeNodeAttributesDialog = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const computeNode = getState().computeNodes.find(node => node.uuid === uuid);
+ dispatch(dialogActions.OPEN_DIALOG({ id: COMPUTE_NODE_ATTRIBUTES_DIALOG, data: { computeNode } }));
+ };
+
+export const openComputeNodeRemoveDialog = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(dialogActions.OPEN_DIALOG({
+ id: COMPUTE_NODE_REMOVE_DIALOG,
+ data: {
+ title: 'Remove compute node',
+ text: 'Are you sure you want to remove this compute node?',
+ confirmButtonLabel: 'Remove',
+ uuid
+ }
+ }));
+ };
+
+export const removeComputeNode = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+ try {
+ await services.nodeService.delete(uuid);
+ dispatch(computeNodesActions.REMOVE_COMPUTE_NODE(uuid));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Compute node has been successfully removed.', hideDuration: 2000 }));
+ } catch (e) {
+ return;
+ }
+ };
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { computeNodesActions, ComputeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+import { NodeResource } from '~/models/node';
+
+export type ComputeNodesState = NodeResource[];
+
+const initialState: ComputeNodesState = [];
+
+export const computeNodesReducer = (state: ComputeNodesState = initialState, action: ComputeNodesActions): ComputeNodesState =>
+ computeNodesActions.match(action, {
+ SET_COMPUTE_NODES: nodes => nodes,
+ REMOVE_COMPUTE_NODE: (uuid: string) => state.filter((computeNode) => computeNode.uuid !== uuid),
+ default: () => state
+ });
\ No newline at end of file
import { 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 }>(),
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();
}));
};
-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
}));
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { RootState } from '~/store/store';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { ServiceRepository } from "~/services/services";
+import { 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
--- /dev/null
+// 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
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
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 } }));
};
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());
};
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' &&
appInfo: appInfoReducer,
searchBar: searchBarReducer,
virtualMachines: virtualMachinesReducer,
- repositories: repositoriesReducer
+ repositories: repositoriesReducer,
+ keepServices: keepServicesReducer,
+ computeNodes: computeNodesReducer
});
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());
});
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 = () =>
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 = () =>
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>;
}
itemsAvailable: 0,
items: []
},
- logins: [],
+ logins: {
+ kind: '',
+ items: []
+ },
links: {
kind: '',
offset: 0,
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';
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;
import { Dispatch } from "redux";
import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
import { ContextMenuKind } from "../context-menu/context-menu";
-import { getNode, getNodeChildrenIds, Tree } from "~/models/tree";
-import { CollectionFileType } from "~/models/collection-file";
+import { getNode, getNodeChildrenIds, Tree, TreeNode, initTreeNode } from "~/models/tree";
+import { CollectionFileType, createCollectionDirectory } from "~/models/collection-file";
import { openContextMenu, openCollectionFilesContextMenu } from '~/store/context-menu/context-menu-actions';
import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
import { ResourceKind } from "~/models/resource";
const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
(id: string): TreeItem<FileTreeData> => {
- const node = getNode(id)(tree) || {
+ const node: TreeNode<CollectionPanelDirectory | CollectionPanelFile> = getNode(id)(tree) || initTreeNode({
id: '',
- children: [],
parent: '',
value: {
- name: 'Invalid node',
- type: CollectionFileType.DIRECTORY,
+ ...createCollectionDirectory({ name: 'Invalid file' }),
selected: false,
collapsed: true
}
- };
+ });
return {
active: false,
data: {
name: node.value.name,
size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
- type: node.value.type
+ type: node.value.type,
+ url: node.value.url,
},
id: node.id,
items: getNodeChildrenIds(node.id)(tree)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { compose } from 'redux';
+import {
+ withStyles, Dialog, DialogTitle, DialogContent, DialogActions,
+ Button, StyleRulesCallback, WithStyles, Grid
+} from '@material-ui/core';
+import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog";
+import { COMPUTE_NODE_ATTRIBUTES_DIALOG } from '~/store/compute-nodes/compute-nodes-actions';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { NodeResource, NodeProperties, NodeInfo } from '~/models/node';
+import * as classnames from "classnames";
+
+type CssRules = 'root' | 'grid';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ fontSize: '0.875rem',
+ '& div:nth-child(odd):not(.nestedRoot)': {
+ textAlign: 'right',
+ color: theme.palette.grey["500"]
+ },
+ '& div:nth-child(even)': {
+ overflowWrap: 'break-word'
+ }
+ },
+ grid: {
+ padding: '8px 0 0 0'
+ }
+});
+
+interface AttributesComputeNodeDialogDataProps {
+ computeNode: NodeResource;
+}
+
+export const AttributesComputeNodeDialog = compose(
+ withDialog(COMPUTE_NODE_ATTRIBUTES_DIALOG),
+ withStyles(styles))(
+ ({ open, closeDialog, data, classes }: WithDialogProps<AttributesComputeNodeDialogDataProps> & WithStyles<CssRules>) =>
+ <Dialog open={open} onClose={closeDialog} fullWidth maxWidth='sm'>
+ <DialogTitle>Attributes</DialogTitle>
+ <DialogContent>
+ {data.computeNode && <div>
+ {renderPrimaryInfo(data.computeNode, classes)}
+ {renderInfo(data.computeNode.info, classes)}
+ {renderProperties(data.computeNode.properties, classes)}
+ </div>}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ );
+
+const renderPrimaryInfo = (computeNode: NodeResource, classes: any) => {
+ const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid } = computeNode;
+ return (
+ <Grid container direction="row" spacing={16} className={classes.root}>
+ <Grid item xs={5}>UUID</Grid>
+ <Grid item xs={7}>{uuid}</Grid>
+ <Grid item xs={5}>Owner uuid</Grid>
+ <Grid item xs={7}>{ownerUuid}</Grid>
+ <Grid item xs={5}>Created at</Grid>
+ <Grid item xs={7}>{createdAt}</Grid>
+ <Grid item xs={5}>Modified at</Grid>
+ <Grid item xs={7}>{modifiedAt}</Grid>
+ <Grid item xs={5}>Modified by user uuid</Grid>
+ <Grid item xs={7}>{modifiedByUserUuid}</Grid>
+ <Grid item xs={5}>Modified by client uuid</Grid>
+ <Grid item xs={7}>{modifiedByClientUuid || '(none)'}</Grid>
+ </Grid>
+ );
+};
+
+const renderInfo = (info: NodeInfo, classes: any) => {
+ const { lastAction, pingSecret, ec2InstanceId, slurmState } = info;
+ return (
+ <Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
+ <Grid item xs={5}>Info - Last action</Grid>
+ <Grid item xs={7}>{lastAction || '(none)'}</Grid>
+ <Grid item xs={5}>Info - Ping secret</Grid>
+ <Grid item xs={7}>{pingSecret || '(none)'}</Grid>
+ <Grid item xs={5}>Info - ec2 instance id</Grid>
+ <Grid item xs={7}>{ec2InstanceId || '(none)'}</Grid>
+ <Grid item xs={5}>Info - Slurm state</Grid>
+ <Grid item xs={7}>{slurmState || '(none)'}</Grid>
+ </Grid>
+ );
+};
+
+const renderProperties = (properties: NodeProperties, classes: any) => {
+ const { totalRamMb, totalCpuCores, totalScratchMb, cloudNode } = properties;
+ return (
+ <Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
+ <Grid item xs={5}>Properties - Total ram mb</Grid>
+ <Grid item xs={7}>{totalRamMb || '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Total scratch mb</Grid>
+ <Grid item xs={7}>{totalScratchMb || '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Total cpu cores</Grid>
+ <Grid item xs={7}>{totalCpuCores || '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Cloud node size </Grid>
+ <Grid item xs={7}>{cloudNode ? cloudNode.size : '(none)'}</Grid>
+ <Grid item xs={5}>Properties - Cloud node price</Grid>
+ <Grid item xs={7}>{cloudNode ? cloudNode.price : '(none)'}</Grid>
+ </Grid>
+ );
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { COMPUTE_NODE_REMOVE_DIALOG, removeComputeNode } from '~/store/compute-nodes/compute-nodes-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(removeComputeNode(props.data.uuid));
+ }
+});
+
+export const RemoveComputeNodeDialog = compose(
+ withDialog(COMPUTE_NODE_REMOVE_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { openComputeNodeRemoveDialog, openComputeNodeAttributesDialog } from '~/store/compute-nodes/compute-nodes-actions';
+import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon";
+
+export const computeNodeActionSet: ContextMenuActionSet = [[{
+ name: "Attributes",
+ icon: AttributesIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openComputeNodeAttributesDialog(uuid));
+ }
+}, {
+ name: "Advanced",
+ icon: AdvancedIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openAdvancedTabDialog(uuid));
+ }
+}, {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openComputeNodeRemoveDialog(uuid));
+ }
+}]];
--- /dev/null
+// 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));
+ }
+}]];
export const repositoryActionSet: ContextMenuActionSet = [[{
name: "Attributes",
icon: AttributesIcon,
- execute: (dispatch, { index }) => {
- dispatch<any>(openRepositoryAttributes(index!));
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openRepositoryAttributes(uuid));
}
}, {
name: "Share",
name: "Advanced",
icon: AdvancedIcon,
execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid, resource.index));
+ dispatch<any>(openAdvancedTabDialog(resource.uuid));
}
}, {
name: "Remove",
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",
--- /dev/null
+// 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));
+ }
+}]];
PROCESS_RESOURCE = 'ProcessResource',
PROCESS_LOGS = "ProcessLogs",
REPOSITORY = "Repository",
- SSH_KEY = "SshKey"
+ SSH_KEY = "SshKey",
+ VIRTUAL_MACHINE = "VirtualMachine",
+ KEEP_SERVICE = "KeepService",
+ NODE = "Node"
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { compose } from 'redux';
+import {
+ withStyles, Dialog, DialogTitle, DialogContent, DialogActions,
+ Button, StyleRulesCallback, WithStyles, Grid
+} from '@material-ui/core';
+import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog";
+import { 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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { 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
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 {
<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>
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 {
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) =>
// 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
--- /dev/null
+// 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>
+ );
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { 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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Table,
+ TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton
+} from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { MoreOptionsIcon } from '~/components/icon/icon';
+import { NodeResource } from '~/models/node';
+import { formatDate } from '~/common/formatters';
+
+type CssRules = 'root' | 'tableRow';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ overflow: 'auto'
+ },
+ tableRow: {
+ '& td, th': {
+ whiteSpace: 'nowrap'
+ }
+ }
+});
+
+export interface ComputeNodePanelRootActionProps {
+ openRowOptions: (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) => void;
+}
+
+export interface ComputeNodePanelRootDataProps {
+ computeNodes: NodeResource[];
+ hasComputeNodes: boolean;
+}
+
+type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps & WithStyles<CssRules>;
+
+export const ComputeNodePanelRoot = withStyles(styles)(
+ ({ classes, hasComputeNodes, computeNodes, openRowOptions }: ComputeNodePanelRootProps) =>
+ <Card className={classes.root}>
+ <CardContent>
+ {hasComputeNodes && <Grid container direction="row">
+ <Grid item xs={12}>
+ <Table>
+ <TableHead>
+ <TableRow className={classes.tableRow}>
+ <TableCell>Info</TableCell>
+ <TableCell>UUID</TableCell>
+ <TableCell>Domain</TableCell>
+ <TableCell>First ping at</TableCell>
+ <TableCell>Hostname</TableCell>
+ <TableCell>IP Address</TableCell>
+ <TableCell>Job</TableCell>
+ <TableCell>Last ping at</TableCell>
+ <TableCell />
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {computeNodes.map((computeNode, index) =>
+ <TableRow key={index} className={classes.tableRow}>
+ <TableCell>{computeNode.uuid}</TableCell>
+ <TableCell>{computeNode.uuid}</TableCell>
+ <TableCell>{computeNode.domain}</TableCell>
+ <TableCell>{formatDate(computeNode.firstPingAt) || '(none)'}</TableCell>
+ <TableCell>{computeNode.hostname || '(none)'}</TableCell>
+ <TableCell>{computeNode.ipAddress || '(none)'}</TableCell>
+ <TableCell>{computeNode.jobUuid || '(none)'}</TableCell>
+ <TableCell>{formatDate(computeNode.lastPingAt) || '(none)'}</TableCell>
+ <TableCell>
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton onClick={event => openRowOptions(event, computeNode)}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </Tooltip>
+ </TableCell>
+ </TableRow>)}
+ </TableBody>
+ </Table>
+ </Grid>
+ </Grid>}
+ </CardContent>
+ </Card>
+);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { } from '~/store/compute-nodes/compute-nodes-actions';
+import {
+ ComputeNodePanelRoot,
+ ComputeNodePanelRootDataProps,
+ ComputeNodePanelRootActionProps
+} from '~/views/compute-node-panel/compute-node-panel-root';
+import { openComputeNodeContextMenu } from '~/store/context-menu/context-menu-actions';
+
+const mapStateToProps = (state: RootState): ComputeNodePanelRootDataProps => {
+ return {
+ computeNodes: state.computeNodes,
+ hasComputeNodes: state.computeNodes.length > 0
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ComputeNodePanelRootActionProps => ({
+ openRowOptions: (event, computeNode) => {
+ dispatch<any>(openComputeNodeContextMenu(event, computeNode));
+ }
+});
+
+export const ComputeNodePanel = connect(mapStateToProps, mapDispatchToProps)(ComputeNodePanelRoot);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, 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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import {
+ 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
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())
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;
}
<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>
// 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';
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)
+);
// 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;
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;
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 {
// 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 {
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();
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
+import { memoize } from 'lodash/fp';
import {
isRequiredInput,
FileCommandInputParameter,
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;
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 {
// 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';
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
// 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';
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
// 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';
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
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;
}
</TableCell>
<TableCell>
<Tooltip title="More options" disableFocusListener>
- <IconButton onClick={event => openRowOptions(event, index, sshKey)}>
+ <IconButton onClick={event => openRowOptions(event, sshKey)}>
<MoreOptionsIcon />
</IconButton>
</Tooltip>
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));
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: {
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>;
}
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>
);
}
<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) =>
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';
<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>
<DetailsPanel />
</Grid>
<AdvancedTabDialog />
+ <AttributesComputeNodeDialog />
+ <AttributesKeepServiceDialog />
<AttributesSshKeyDialog />
<ChangeWorkflowDialog />
<ContextMenu />
<ProcessCommandDialog />
<ProcessInputDialog />
<ProjectPropertiesDialog />
+ <RemoveComputeNodeDialog />
+ <RemoveKeepServiceDialog />
<RemoveProcessDialog />
<RemoveRepositoryDialog />
<RemoveSshKeyDialog />
+ <RemoveVirtualMachineDialog />
<RenameFileDialog />
<RepositoryAttributesDialog />
<RepositoriesSampleGitDialog />
<UpdateCollectionDialog />
<UpdateProcessDialog />
<UpdateProjectDialog />
+ <VirtualMachineAttributesDialog />
</Grid>
);
\ No newline at end of file
declare var System: System;
declare module 'react-splitter-layout';
-declare module 'react-rte';
\ No newline at end of file
+declare module 'react-rte';
+
+declare module 'is-image' {
+ export default function isImage(value: string): boolean;
+}
\ No newline at end of file
dependencies:
minimatch "^3.0.4"
+image-extensions@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/image-extensions/-/image-extensions-1.1.0.tgz#b8e6bf6039df0056e333502a00b6637a3105d894"
+ integrity sha1-uOa/YDnfAFbjM1AqALZjejEF2JQ=
+
immutable@^3.8.1:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
dependencies:
is-extglob "^2.1.1"
+is-image@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-image/-/is-image-2.0.0.tgz#454c9569578de31869371fbfaea4958f461b3e0c"
+ integrity sha1-RUyVaVeN4xhpNx+/rqSVj0YbPgw=
+ dependencies:
+ image-extensions "^1.0.1"
+
is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"