// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import AccessTime from '@material-ui/icons/AccessTime';
import Add from '@material-ui/icons/Add';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
export const ProjectIcon: IconType = (props) => <Folder {...props} />;
export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
-export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
export const RemoveIcon: IconType = (props) => <Delete {...props} />;
export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
export const RenameIcon: IconType = (props) => <Edit {...props} />;
});
};
+// force build comment #1
// SPDX-License-Identifier: AGPL-3.0
import { Resource } from "./resource";
+import { TagProperty } from "~/models/tag";
export interface LinkResource extends Resource {
headUuid: string;
tailUuid: string;
linkClass: string;
name: string;
- properties: {};
+ properties: TagProperty;
}
export enum LinkClass {
REPOSITORY = "arvados#repository",
SSH_KEY = "arvados#authorizedKeys",
USER = "arvados#user",
+ VIRTUAL_MACHINE = "arvados#virtualMachine",
WORKFLOW = "arvados#workflow",
NONE = "arvados#none"
}
LOG = '57u5n',
REPOSITORY = 's0uqq',
USER = 'tpzed',
+ VIRTUAL_MACHINE = '2x53u',
WORKFLOW = '7fd4e',
+ SSH_KEY = 'fngyi'
}
export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
return ResourceKind.LOG;
case ResourceObjectType.WORKFLOW:
return ResourceKind.WORKFLOW;
+ case ResourceObjectType.VIRTUAL_MACHINE:
+ return ResourceKind.VIRTUAL_MACHINE;
case ResourceObjectType.REPOSITORY:
return ResourceKind.REPOSITORY;
+ case ResourceObjectType.SSH_KEY:
+ return ResourceKind.SSH_KEY;
default:
return undefined;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "~/models/resource";
+
+export interface VirtualMachinesResource extends Resource {
+ hostname: string;
+}
+
+export interface VirtualMachinesLoginsResource {
+ hostname: string;
+ username: string;
+ public_key: string;
+ user_uuid: string;
+ virtual_machine_uuid: string;
+ authorized_key_uuid: string;
+}
\ 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 } from './routes';
-import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories } from '~/store/workbench/workbench-actions';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from './routes';
+import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, loadSshKeys, loadRepositories, loadVirtualMachines } from '~/store/workbench/workbench-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions';
const searchResultsMatch = matchSearchResultsRoute(pathname);
const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
const runProcessMatch = matchRunProcessRoute(pathname);
+ const virtualMachineMatch = matchVirtualMachineRoute(pathname);
const workflowMatch = matchWorkflowRoute(pathname);
const sshKeysMatch = matchSshKeysRoute(pathname);
store.dispatch(loadWorkflow);
} else if (searchResultsMatch) {
store.dispatch(loadSearchResults);
+ } else if (virtualMachineMatch) {
+ store.dispatch(loadVirtualMachines);
} else if(repositoryMatch) {
store.dispatch(loadRepositories);
} else if (sshKeysMatch) {
REPOSITORIES: '/repositories',
SHARED_WITH_ME: '/shared-with-me',
RUN_PROCESS: '/run-process',
+ VIRTUAL_MACHINES: '/virtual-machines',
WORKFLOWS: '/workflows',
SEARCH_RESULTS: '/search-results',
SSH_KEYS: `/ssh-keys`
export const matchSearchResultsRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.SEARCH_RESULTS });
+export const matchVirtualMachineRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.VIRTUAL_MACHINES });
+
export const matchRepositoriesRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.REPOSITORIES });
import { WorkflowService } from "~/services/workflow-service/workflow-service";
import { SearchService } from '~/services/search-service/search-service';
import { PermissionService } from "~/services/permission-service/permission-service";
+import { VirtualMachinesService } from "~/services/virtual-machines-service/virtual-machines-service";
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';
const projectService = new ProjectService(apiClient, actions);
const repositoriesService = new RepositoriesService(apiClient, actions);
const userService = new UserService(apiClient, actions);
+ const virtualMachineService = new VirtualMachinesService(apiClient, actions);
const workflowService = new WorkflowService(apiClient, actions);
const ancestorsService = new AncestorService(groupsService, userService);
searchService,
tagService,
userService,
+ virtualMachineService,
webdavClient,
workflowService,
vocabularyService,
--- /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 { VirtualMachinesResource } from '~/models/virtual-machines';
+import { ApiActions } from '~/services/api/api-actions';
+
+export class VirtualMachinesService extends CommonResourceService<VirtualMachinesResource> {
+ constructor(serverApi: AxiosInstance, actions: ApiActions) {
+ super(serverApi, "virtual_machines", actions);
+ }
+
+ getRequestedDate(): string {
+ return localStorage.getItem('requestedDate') || '';
+ }
+
+ saveRequestedDate(date: string) {
+ localStorage.setItem('requestedDate', date);
+ }
+
+ logins(uuid: string) {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .get(`virtual_machines/${uuid}/logins`),
+ this.actions
+ );
+ }
+
+ getAllLogins() {
+ return CommonResourceService.defaultResponse(
+ this.serverApi
+ .get('virtual_machines/get_all_logins'),
+ this.actions
+ );
+ }
+}
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
+import { Dispatch } from 'redux';
import { dialogActions } from '~/store/dialog/dialog-actions';
import { RootState } from '~/store/store';
-import { Dispatch } from 'redux';
import { ResourceKind, extractUuidKind } from '~/models/resource';
import { getResource } from '~/store/resources/resources';
import { GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
import { ServiceRepository } from '~/services/services';
import { FilterBuilder } from '~/services/api/filter-builder';
import { RepositoryResource } from '~/models/repositories';
+import { SshKeyResource } from '~/models/ssh-key';
export const ADVANCED_TAB_DIALOG = 'advancedTabDialog';
-export interface AdvancedTabDialogData {
+interface AdvancedTabDialogData {
apiResponse: any;
metadata: any;
user: string;
CREATED_AT = 'created_at'
}
+enum SshKeyData {
+ SSH_KEY = 'authorized_keys',
+ CREATED_AT = 'created_at'
+}
+
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData;
+type AdvanceResourcePrefix = GroupContentsResourcePrefix | 'repositories' | 'authorized_keys';
+
export const openAdvancedTabDialog = (uuid: string, index?: number) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const { resources } = getState();
const kind = extractUuidKind(uuid);
- const data = getResource<any>(uuid)(resources);
- const repositoryData = getState().repositories.items[index!];
- if (data || repositoryData) {
- if (data) {
- const metadata = await services.linkService.list({
- filters: new FilterBuilder()
- .addEqual('headUuid', uuid)
- .getFilters()
- });
- const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid);
- if (kind === ResourceKind.COLLECTION) {
- const dataCollection: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, collectionApiResponse, data, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, data.storageClassesConfirmed);
- dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataCollection }));
- } else if (kind === ResourceKind.PROCESS) {
- const dataProcess: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, containerRequestApiResponse, data, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, data.outputName);
- dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProcess }));
- } else if (kind === ResourceKind.PROJECT) {
- const dataProject: AdvancedTabDialogData = advancedTabData(uuid, metadata, user, groupRequestApiResponse, data, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, data.deleteAt);
- dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataProject }));
- }
- } else if (kind === ResourceKind.REPOSITORY) {
- const dataRepository: AdvancedTabDialogData = advancedTabData(uuid, '', '', repositoryApiResponse, repositoryData, RepositoryData.REPOSITORY, 'repositories', RepositoryData.CREATED_AT, repositoryData.createdAt);
- dispatch(dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data: dataRepository }));
- }
- } else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ 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);
+ 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);
+ 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);
+ 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);
+ 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);
+ dispatch<any>(initAdvancedTabDialog(advanceDataSshKey));
+ break;
+ default:
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
};
-const advancedTabData = (uuid: string, metadata: any, user: any, apiResponseKind: any, data: any, resourceKind: CollectionData | ProcessData | ProjectData | RepositoryData, resourcePrefix: GroupContentsResourcePrefix | 'repositories', resourceKindProperty: CollectionData | ProcessData | ProjectData | RepositoryData, property: any) => {
+const getDataForAdvancedTab = (uuid: string) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<any>(uuid)(resources);
+ const metadata = await services.linkService.list({
+ filters: new FilterBuilder()
+ .addEqual('headUuid', uuid)
+ .getFilters()
+ });
+ const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid || '');
+ return { data, metadata, user };
+ };
+
+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) => {
return {
uuid,
user,
const pythonExample = (uuid: string, resourcePrefix: string) => {
const pythonExample = `import arvados
- x = arvados.api().${resourcePrefix}().get(uuid='${uuid}').execute()`;
+x = arvados.api().${resourcePrefix}().get(uuid='${uuid}').execute()`;
return pythonExample;
};
const cliGetExample = (uuid: string, resourceKind: string) => {
const cliGetExample = `arv ${resourceKind} get \\
- --uuid ${uuid}`;
+ --uuid ${uuid}`;
return cliGetExample;
};
`An example arv command to update the "${resourceName}" attribute for the current ${resourceKind}:`;
const cliUpdateExample = (uuid: string, resourceKind: string, resource: string | string[], resourceName: string) => {
- const CLIUpdateCollectionExample = `arv ${resourceKind} update \\
- --uuid ${uuid} \\
- --${resourceKind} '{"${resourceName}":${resource}}'`;
+ const CLIUpdateCollectionExample = `arv ${resourceKind} update \\
+ --uuid ${uuid} \\
+ --${resourceKind} '{"${resourceName}":${resource}}'`;
return CLIUpdateCollectionExample;
};
const curlExample = (uuid: string, resourcePrefix: string, resource: string | string[], resourceKind: string, resourceName: string) => {
const curlExample = `curl -X PUT \\
- -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \\
- --data-urlencode ${resourceKind}@/dev/stdin \\
- https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
- <<EOF
+ -H "Authorization: OAuth2 $ARVADOS_API_TOKEN" \\
+ --data-urlencode ${resourceKind}@/dev/stdin \\
+ https://$ARVADOS_API_HOST/arvados/v1/${resourcePrefix}/${uuid} \\
+ <<EOF
{
"${resourceName}": ${resource}
}
"name": ${stringify(name)},
"created_at": "${createdAt}"`;
+ return response;
+};
+
+const sshKeyApiResponse = (apiResponse: SshKeyResource) => {
+ const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, authorizedUserUuid, expiresAt } = apiResponse;
+ const response = `"uuid": "${uuid}",
+"owner_uuid": "${ownerUuid}",
+"authorized_user_uuid": "${authorizedUserUuid}",
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
+"modified_at": ${stringify(modifiedAt)},
+"name": ${stringify(name)},
+"created_at": "${createdAt}",
+"expires_at": "${expiresAt}"`;
return response;
};
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { dialogActions } from "~/store/dialog/dialog-actions";
-import { getProperty } from '../properties/properties';
+import { getProperty } from '~/store/properties/properties';
import { propertiesActions } from '~/store/properties/properties-actions';
import { RootState } from '~/store/store';
export const navigateToSearchResults = push(Routes.SEARCH_RESULTS);
+export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES);
+
export const navigateToRepositories = push(Routes.REPOSITORIES);
export const navigateToSshKeys= push(Routes.SSH_KEYS);
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
export const SEARCH_RESULTS_PANEL_ID = "searchResultsPanel";
export const searchResultsPanelActions = bindDataExplorerActions(SEARCH_RESULTS_PANEL_ID);
export const loadSearchResultsPanel = () =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(setBreadcrumbs([{ label: 'Search results' }]));
dispatch(searchResultsPanelActions.REQUEST_ITEMS());
};
\ No newline at end of file
const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
const dialog = getDialog<string>(getState().dialog, SHARING_DIALOG_NAME);
-
if (dialog) {
dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
- const { items } = await permissionService.listResourcePermissions(dialog.data);
- dispatch<any>(initializePublicAccessForm(items));
- await dispatch<any>(initializeManagementForm(items));
- dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ try {
+ const { items } = await permissionService.listResourcePermissions(dialog.data);
+ dispatch<any>(initializePublicAccessForm(items));
+ await dispatch<any>(initializeManagementForm(items));
+ dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You do not have access to share this item', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
+ dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ }
}
};
PROJECTS = 'Projects',
SHARED_WITH_ME = 'Shared with me',
WORKFLOWS = 'Workflows',
- RECENT_OPEN = 'Recently open',
FAVORITES = 'Favorites',
TRASH = 'Trash'
}
const SIDE_PANEL_CATEGORIES = [
SidePanelTreeCategory.WORKFLOWS,
- SidePanelTreeCategory.RECENT_OPEN,
SidePanelTreeCategory.FAVORITES,
SidePanelTreeCategory.TRASH,
];
import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
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';
const composeEnhancers =
runProcessPanel: runProcessPanelReducer,
appInfo: appInfoReducer,
searchBar: searchBarReducer,
+ virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer
});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from '~/store/store';
+import { ServiceRepository } from "~/services/services";
+import { navigateToVirtualMachines } from "../navigation/navigation-action";
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { formatDate } from "~/common/formatters";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { VirtualMachinesLoginsResource } from '~/models/virtual-machines';
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { ListResults } from "~/services/common-service/common-resource-service";
+
+export const virtualMachinesActions = unionize({
+ SET_REQUESTED_DATE: ofType<string>(),
+ SET_VIRTUAL_MACHINES: ofType<ListResults<any>>(),
+ SET_LOGINS: ofType<VirtualMachinesLoginsResource[]>(),
+ SET_LINKS: ofType<ListResults<any>>()
+});
+
+export type VirtualMachineActions = UnionOf<typeof virtualMachinesActions>;
+
+export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel';
+
+export const openVirtualMachines = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(navigateToVirtualMachines);
+ };
+
+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());
+ const virtualMachines = await services.virtualMachineService.list();
+ const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid);
+ const links = await services.linkService.list({
+ filters: new FilterBuilder()
+ .addIn("headUuid", virtualMachinesUuids)
+ .getFilters()
+ });
+ dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
+ dispatch(virtualMachinesActions.SET_LINKS(links));
+ };
+
+export const saveRequestedDate = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const date = formatDate((new Date).toISOString());
+ services.virtualMachineService.saveRequestedDate(date);
+ dispatch<any>(loadRequestedDate());
+ };
+
+const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL);
+
+export const loadVirtualMachinesPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(virtualMachinesBindedActions.REQUEST_ITEMS());
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+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';
+
+interface VirtualMachines {
+ date: string;
+ virtualMachines: ListResults<any>;
+ logins: VirtualMachinesLoginsResource[];
+ links: ListResults<any>;
+}
+
+const initialState: VirtualMachines = {
+ date: '',
+ virtualMachines: {
+ kind: '',
+ offset: 0,
+ limit: 0,
+ itemsAvailable: 0,
+ items: []
+ },
+ logins: [],
+ links: {
+ kind: '',
+ offset: 0,
+ limit: 0,
+ itemsAvailable: 0,
+ items: []
+ }
+};
+
+export const virtualMachinesReducer = (state = initialState, action: VirtualMachineActions): VirtualMachines =>
+ virtualMachinesActions.match(action, {
+ SET_REQUESTED_DATE: date => ({ ...state, date }),
+ SET_VIRTUAL_MACHINES: virtualMachines => ({ ...state, virtualMachines }),
+ SET_LOGINS: logins => ({ ...state, logins }),
+ SET_LINKS: links => ({ ...state, links }),
+ default: () => state
+ });
import { CollectionResource } from "~/models/collection";
import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
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';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
await dispatch(loadSearchResultsPanel());
});
+export const loadVirtualMachines = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
+ });
+
export const loadRepositories = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadRepositoriesPanel());
import { propertiesActions } from '~/store/properties/properties-actions';
import { getResource } from '../resources/resources';
import { getProperty } from '~/store/properties/properties';
-import { WorkflowResource } from '../../models/workflow';
+import { WorkflowResource } from '~/models/workflow';
+import { navigateToRunProcess } from '~/store/navigation/navigation-action';
+import { goToStep, runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
export const WORKFLOW_PANEL_ID = "workflowPanel";
const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
export const workflowPanelActions = bindDataExplorerActions(WORKFLOW_PANEL_ID);
export const loadWorkflowPanel = () =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(workflowPanelActions.REQUEST_ITEMS());
+ const response = await services.workflowService.list();
+ dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items));
};
export const setUuidPrefix = (uuidPrefix: string) =>
return state.properties.uuidPrefix;
};
+export const openRunProcess = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const workflows = getState().runProcessPanel.searchWorkflows;
+ const workflow = workflows.find(workflow => workflow.uuid === uuid);
+ dispatch<any>(navigateToRunProcess);
+ dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
+ dispatch<any>(goToStep(1));
+ dispatch(runProcessPanelActions.SET_STEP_CHANGED(true));
+ dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow!));
+ };
+
export const getPublicUserUuid = (state: RootState) => {
const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
return `${prefix}-tpzed-anonymouspublic`;
dispatch<any>(openMoveProjectDialog(resource));
}
},
- {
- icon: CopyIcon,
- name: "Copy to project",
- execute: (dispatch, resource) => {
- // add code
- }
- },
+ // {
+ // icon: CopyIcon,
+ // name: "Copy to project",
+ // execute: (dispatch, resource) => {
+ // // add code
+ // }
+ // },
{
icon: DetailsIcon,
name: "View details",
import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon";
import { openSshKeyRemoveDialog, openSshKeyAttributesDialog } from '~/store/auth/auth-action';
+import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
export const sshKeyActionSet: ContextMenuActionSet = [[{
name: "Attributes",
name: "Advanced",
icon: AdvancedIcon,
execute: (dispatch, { uuid, index }) => {
- // ToDo
+ dispatch<any>(openAdvancedTabDialog(uuid, index));
}
}, {
name: "Remove",
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } from '@material-ui/core';
+import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
import { withDialog } from '~/store/dialog/with-dialog';
import { WithDialogProps } from '~/store/dialog/with-dialog';
import { connect } from 'react-redux';
-import { CurrentTokenDialogData, getCurrentTokenDialogData } from '~/store/current-token-dialog/current-token-dialog-actions';
+import { CurrentTokenDialogData, getCurrentTokenDialogData, CURRENT_TOKEN_DIALOG_NAME } from '~/store/current-token-dialog/current-token-dialog-actions';
import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
type CssRules = 'link' | 'paper' | 'button';
export const CurrentTokenDialog =
withStyles(styles)(
connect(getCurrentTokenDialogData)(
- withDialog('currentTokenDialog')(
+ withDialog(CURRENT_TOKEN_DIALOG_NAME)(
class extends React.Component<CurrentTokenProps> {
render() {
const { classes, open, closeDialog, ...data } = this.props;
import { compose, Dispatch } from 'redux';
import { WorkflowResource } from '~/models/workflow';
import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
-import { getUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
-import { CollectionResource } from "~/models/collection";
+import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
import { getResourceData } from "~/store/resources-data/resources-data";
import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
return `${uuidPrefix}-tpzed-anonymouspublic`;
};
-// ToDo: share onClick
export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
return (
<div>
- { isPublic && uuid &&
+ {!isPublic && uuid &&
<Tooltip title="Share">
<IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
<ShareIcon />
})((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
+export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
+ return (
+ <div>
+ {uuid &&
+ <Tooltip title="Run process">
+ <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
+ <ProcessIcon />
+ </IconButton>
+ </Tooltip>}
+ </div>
+ );
+};
+
+export const ResourceRunProcess = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
+ return {
+ uuid: resource ? resource.uuid : ''
+ };
+ })((props: { uuid: string } & DispatchProp<any>) =>
+ resourceRunProcess(props.dispatch, props.uuid));
+
export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
if (ownerUuid === getPublicUuid(uuidPrefix)) {
return renderStatus(ResourceStatus.PUBLIC);
import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions';
import { openRepositoriesPanel } from "~/store/repositories/repositories-actions";
import { navigateToSshKeys } from '~/store/navigation/navigation-action';
+import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
interface AccountMenuProps {
user?: User;
<MenuItem>
{getUserFullname(user)}
</MenuItem>
+ <MenuItem onClick={() => dispatch(openVirtualMachines())}>Virtual Machines</MenuItem>
<MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
<MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
<MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
-import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute } from '~/routes/routes';
+import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from '~/routes/routes';
import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
interface MainContentBarProps {
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);
};
export const MainContentBar = connect((state: RootState) => ({
- buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state)
+ buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) && !isVirtualMachinePath(state)
}), {
onDetailsPanelToggle: toggleDetailsPanel
})((props: MainContentBarProps) =>
import { ProjectResource } from "~/models/project";
import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon';
-import { RecentIcon, WorkflowIcon } from '~/components/icon/icon';
+import { WorkflowIcon } from '~/components/icon/icon';
import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
import { noop } from 'lodash';
return FavoriteIcon;
case SidePanelTreeCategory.PROJECTS:
return ProjectsIcon;
- case SidePanelTreeCategory.RECENT_OPEN:
- return RecentIcon;
case SidePanelTreeCategory.SHARED_WITH_ME:
return ShareMeIcon;
case SidePanelTreeCategory.TRASH:
import { connect } from 'react-redux';
import { RootState } from '~/store/store';
import { RunProcessPanelRootDataProps, RunProcessPanelRootActionProps, RunProcessPanelRoot } from '~/views/run-process-panel/run-process-panel-root';
-import { goToStep, setWorkflow, runProcess, searchWorkflows, openSetWorkflowDialog } from '~/store/run-process-panel/run-process-panel-actions';
+import { goToStep, runProcess, searchWorkflows, openSetWorkflowDialog } from '~/store/run-process-panel/run-process-panel-actions';
import { WorkflowResource } from '~/models/workflow';
const mapStateToProps = ({ runProcessPanel }: RootState): RunProcessPanelRootDataProps => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+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 { 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 { 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 { Routes } from '~/routes/routes';
+
+type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ button: {
+ marginTop: theme.spacing.unit,
+ marginBottom: theme.spacing.unit
+ },
+ codeSnippet: {
+ borderRadius: theme.spacing.unit * 0.5,
+ border: '1px solid',
+ borderColor: theme.palette.grey["400"],
+ },
+ link: {
+ textDecoration: 'none',
+ color: theme.palette.primary.main,
+ "&:hover": {
+ color: theme.palette.primary.dark,
+ transition: 'all 0.5s ease'
+ }
+ },
+ linkIcon: {
+ textDecoration: 'none',
+ color: theme.palette.grey["500"],
+ textAlign: 'right',
+ "&:hover": {
+ color: theme.palette.common.black,
+ transition: 'all 0.5s ease'
+ }
+ },
+ rightAlign: {
+ textAlign: "right"
+ },
+ cardWithoutMachines: {
+ display: 'flex'
+ },
+ icon: {
+ textAlign: "right",
+ marginTop: theme.spacing.unit
+ }
+});
+
+const mapStateToProps = ({ virtualMachines }: RootState) => {
+ return {
+ requestedDate: virtualMachines.date,
+ ...virtualMachines
+ };
+};
+
+const mapDispatchToProps = {
+ saveRequestedDate,
+ loadVirtualMachinesData
+};
+
+interface VirtualMachinesPanelDataProps {
+ requestedDate: string;
+ virtualMachines: ListResults<any>;
+ logins: VirtualMachinesLoginsResource[];
+ links: ListResults<any>;
+}
+
+interface VirtualMachinesPanelActionProps {
+ saveRequestedDate: () => void;
+ loadVirtualMachinesData: () => string;
+}
+
+type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles<CssRules>;
+
+export const VirtualMachinePanel = compose(
+ withStyles(styles),
+ connect(mapStateToProps, mapDispatchToProps))(
+ class extends React.Component<VirtualMachineProps> {
+ componentDidMount() {
+ this.props.loadVirtualMachinesData();
+ }
+
+ render() {
+ const { virtualMachines, links } = this.props;
+ return (
+ <Grid container spacing={16}>
+ {virtualMachines.itemsAvailable === 0 && <CardContentWithNoVirtualMachines {...this.props} />}
+ {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && <CardContentWithVirtualMachines {...this.props} />}
+ {<CardSSHSection {...this.props} />}
+ </Grid>
+ );
+ }
+ }
+ );
+
+const CardContentWithNoVirtualMachines = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent className={props.classes.cardWithoutMachines}>
+ <Grid item xs={6}>
+ <Typography variant="body2">
+ You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell.
+ </Typography>
+ </Grid>
+ <Grid item xs={6} 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>}
+ </Grid>
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const CardContentWithVirtualMachines = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <div className={props.classes.rightAlign}>
+ <Button variant="contained" color="primary" className={props.classes.button} onClick={props.saveRequestedDate}>
+ SEND REQUEST FOR SHELL ACCESS
+ </Button>
+ {props.requestedDate &&
+ <Typography variant="body1">
+ A request for shell access was sent on {props.requestedDate}
+ </Typography>}
+ </div>
+ <div className={props.classes.icon}>
+ <a href="https://doc.arvados.org/user/getting_started/vm-login-with-webshell.html" target="_blank" className={props.classes.linkIcon}>
+ <Tooltip title="Access VM using webshell">
+ <HelpIcon />
+ </Tooltip>
+ </a>
+ </div>
+ <Table>
+ <TableHead>
+ <TableRow>
+ <TableCell>Host name</TableCell>
+ <TableCell>Login name</TableCell>
+ <TableCell>Command line</TableCell>
+ <TableCell>Web shell</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {props.virtualMachines.items.map((it, index) =>
+ <TableRow key={index}>
+ <TableCell>{it.hostname}</TableCell>
+ <TableCell>{getUsername(props.links, it)}</TableCell>
+ <TableCell>ssh {getUsername(props.links, it)}@shell.arvados</TableCell>
+ <TableCell>
+ <a href={`https://workbench.c97qk.arvadosapi.com${it.href}/webshell/${getUsername(props.links, it)}`} target="_blank" className={props.classes.link}>
+ Log in as {getUsername(props.links, it)}
+ </a>
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </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 CardSSHSection = (props: VirtualMachineProps) =>
+ <Grid item xs={12}>
+ <Card>
+ <CardContent>
+ <Typography variant="body2">
+ In order to access virtual machines using SSH, <Link to={Routes.SSH_KEYS} className={props.classes.link}>add an SSH key to your account</Link> and add a section like this to your SSH configuration file ( ~/.ssh/config):
+ </Typography>
+ <DefaultCodeSnippet
+ className={props.classes.codeSnippet}
+ lines={[textSSH]} />
+ </CardContent>
+ </Card>
+ </Grid>;
+
+const textSSH = `Host *.arvados
+ TCPKeepAlive yes
+ ServerAliveInterval 60
+ ProxyCommand ssh -p2222 turnout@switchyard.api.ardev.roche.com -x -a $SSH_PROXY_FLAGS %h`;
\ No newline at end of file
import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog';
import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog';
import { ProcessInputDialog } from '~/views-components/process-input-dialog/process-input-dialog';
+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 { RepositoriesSampleGitDialog } from '~/views-components/repositories-sample-git-dialog/repositories-sample-git-dialog';
<Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
<Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
<Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
+ <Route path={Routes.VIRTUAL_MACHINES} component={VirtualMachinePanel} />
<Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
<Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
</Switch>
ResourceLastModifiedDate,
RosurceWorkflowName,
ResourceWorkflowStatus,
- ResourceShare
+ ResourceShare,
+ ResourceRunProcess
} from "~/views-components/data-explorer/renderers";
import { SortDirection } from '~/components/data-table/data-column';
import { DataColumns } from '~/components/data-table/data-table';
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
import { Grid, Paper } from '@material-ui/core';
import { WorkflowDetailsCard } from './workflow-description-card';
-import { WorkflowResource } from '../../models/workflow';
+import { WorkflowResource } from '~/models/workflow';
import { createTree } from '~/models/tree';
export enum WorkflowPanelColumnNames {
configurable: false,
filters: createTree(),
render: (uuid: string) => <ResourceShare uuid={uuid} />
+ },
+ {
+ name: '',
+ selected: true,
+ configurable: false,
+ filters: createTree(),
+ render: (uuid: string) => <ResourceRunProcess uuid={uuid} />
}
];
export const WorkflowPanelView = (props: WorkflowPanelProps) => {
- return <Grid container spacing={16} style={{minHeight: '500px'}}>
+ return <Grid container spacing={16} style={{ minHeight: '500px' }}>
<Grid item xs={6}>
<DataExplorer
id={WORKFLOW_PANEL_ID}
});
const mapDispatchToProps = (dispatch: Dispatch): WorfklowPanelActionProps => ({
-
handleRowDoubleClick: (uuid: string) => {
dispatch<any>(navigateTo(uuid));
},