From: Michal Klobukowski Date: Thu, 29 Nov 2018 09:42:35 +0000 (+0100) Subject: Merge branch 'master' into 14393-vocabulary X-Git-Tag: 1.3.0~5^2^2^2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/b7de31f185fccd2f9b276c1e89754d288e7facfe?hp=9a6108f929e78dd1b68dcf3cc27fd934d1fde9d6 Merge branch 'master' into 14393-vocabulary refs #14393 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index a0f58be4..8049686f 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -3,7 +3,6 @@ // 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'; @@ -90,7 +89,6 @@ export const ProcessIcon: IconType = (props) => ; export const ProjectIcon: IconType = (props) => ; export const ProjectsIcon: IconType = (props) => ; export const ProvenanceGraphIcon: IconType = (props) => ; -export const RecentIcon: IconType = (props) => ; export const RemoveIcon: IconType = (props) => ; export const RemoveFavoriteIcon: IconType = (props) => ; export const RenameIcon: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index 62db7f0f..801a56a1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -306,3 +306,4 @@ const createSampleProcess = ({ containerRequestService }: ServiceRepository) => }); }; +// force build comment #1 diff --git a/src/models/link.ts b/src/models/link.ts index 9d1711d8..baaff658 100644 --- a/src/models/link.ts +++ b/src/models/link.ts @@ -3,13 +3,14 @@ // 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 { diff --git a/src/models/resource.ts b/src/models/resource.ts index 5fa61797..7e2127b2 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -31,6 +31,7 @@ export enum ResourceKind { REPOSITORY = "arvados#repository", SSH_KEY = "arvados#authorizedKeys", USER = "arvados#user", + VIRTUAL_MACHINE = "arvados#virtualMachine", WORKFLOW = "arvados#workflow", NONE = "arvados#none" } @@ -43,7 +44,9 @@ export enum ResourceObjectType { LOG = '57u5n', REPOSITORY = 's0uqq', USER = 'tpzed', + VIRTUAL_MACHINE = '2x53u', WORKFLOW = '7fd4e', + SSH_KEY = 'fngyi' } export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}'; @@ -76,8 +79,12 @@ export const extractUuidKind = (uuid: string = '') => { 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; } diff --git a/src/models/virtual-machines.ts b/src/models/virtual-machines.ts new file mode 100644 index 00000000..0652c350 --- /dev/null +++ b/src/models/virtual-machines.ts @@ -0,0 +1,18 @@ +// 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 diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index c7f3555b..22d0b7c7 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -4,8 +4,8 @@ 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'; @@ -27,6 +27,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { const searchResultsMatch = matchSearchResultsRoute(pathname); const sharedWithMeMatch = matchSharedWithMeRoute(pathname); const runProcessMatch = matchRunProcessRoute(pathname); + const virtualMachineMatch = matchVirtualMachineRoute(pathname); const workflowMatch = matchWorkflowRoute(pathname); const sshKeysMatch = matchSshKeysRoute(pathname); @@ -52,6 +53,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(loadWorkflow); } else if (searchResultsMatch) { store.dispatch(loadSearchResults); + } else if (virtualMachineMatch) { + store.dispatch(loadVirtualMachines); } else if(repositoryMatch) { store.dispatch(loadRepositories); } else if (sshKeysMatch) { diff --git a/src/routes/routes.ts b/src/routes/routes.ts index c9c2ae20..71cdfdac 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -19,6 +19,7 @@ export const Routes = { 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` @@ -79,6 +80,9 @@ export const matchWorkflowRoute = (route: string) => export const matchSearchResultsRoute = (route: string) => matchPath(route, { path: Routes.SEARCH_RESULTS }); +export const matchVirtualMachineRoute = (route: string) => + matchPath(route, { path: Routes.VIRTUAL_MACHINES }); + export const matchRepositoriesRoute = (route: string) => matchPath(route, { path: Routes.REPOSITORIES }); diff --git a/src/services/services.ts b/src/services/services.ts index b0d3ba67..b24b1d99 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -24,6 +24,7 @@ import { ApiActions } from "~/services/api/api-actions"; 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'; @@ -48,6 +49,7 @@ export const createServices = (config: Config, actions: ApiActions) => { 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); @@ -79,6 +81,7 @@ export const createServices = (config: Config, actions: ApiActions) => { searchService, tagService, userService, + virtualMachineService, webdavClient, workflowService, vocabularyService, diff --git a/src/services/virtual-machines-service/virtual-machines-service.ts b/src/services/virtual-machines-service/virtual-machines-service.ts new file mode 100644 index 00000000..c54eff47 --- /dev/null +++ b/src/services/virtual-machines-service/virtual-machines-service.ts @@ -0,0 +1,38 @@ +// 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 { + 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 diff --git a/src/store/advanced-tab/advanced-tab.ts b/src/store/advanced-tab/advanced-tab.ts index c5f600d4..b3c5164c 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -2,9 +2,9 @@ // // 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'; @@ -15,10 +15,11 @@ import { ProjectResource } from '~/models/project'; 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; @@ -52,40 +53,65 @@ enum RepositoryData { 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, getState: () => RootState, services: ServiceRepository) => { - const { resources } = getState(); const kind = extractUuidKind(uuid); - const data = getResource(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(getDataForAdvancedTab(uuid)); + const advanceDataCollection: AdvancedTabDialogData = advancedTabData(uuid, metaCollection, userCollection, collectionApiResponse, dataCollection, CollectionData.COLLECTION, GroupContentsResourcePrefix.COLLECTION, CollectionData.STORAGE_CLASSES_CONFIRMED, dataCollection.storageClassesConfirmed); + dispatch(initAdvancedTabDialog(advanceDataCollection)); + break; + case ResourceKind.PROCESS: + const { data: dataProcess, metadata: metaProcess, user: userProcess } = await dispatch(getDataForAdvancedTab(uuid)); + const advancedDataProcess: AdvancedTabDialogData = advancedTabData(uuid, metaProcess, userProcess, containerRequestApiResponse, dataProcess, ProcessData.CONTAINER_REQUEST, GroupContentsResourcePrefix.PROCESS, ProcessData.OUTPUT_NAME, dataProcess.outputName); + dispatch(initAdvancedTabDialog(advancedDataProcess)); + break; + case ResourceKind.PROJECT: + const { data: dataProject, metadata: metaProject, user: userProject } = await dispatch(getDataForAdvancedTab(uuid)); + const advanceDataProject: AdvancedTabDialogData = advancedTabData(uuid, metaProject, userProject, groupRequestApiResponse, dataProject, ProjectData.GROUP, GroupContentsResourcePrefix.PROJECT, ProjectData.DELETE_AT, dataProject.deleteAt); + dispatch(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(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(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, getState: () => RootState, services: ServiceRepository) => { + const { resources } = getState(); + const data = getResource(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, @@ -108,7 +134,7 @@ const pythonHeader = (resourceKind: string) => 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; }; @@ -118,7 +144,7 @@ const cliGetHeader = (resourceKind: string) => const cliGetExample = (uuid: string, resourceKind: string) => { const cliGetExample = `arv ${resourceKind} get \\ - --uuid ${uuid}`; + --uuid ${uuid}`; return cliGetExample; }; @@ -127,9 +153,9 @@ const cliUpdateHeader = (resourceKind: string, resourceName: string) => `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; }; @@ -139,10 +165,10 @@ const curlHeader = (resourceKind: string, resource: string) => 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} \\ - < { "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 diff --git a/src/store/current-token-dialog/current-token-dialog-actions.tsx b/src/store/current-token-dialog/current-token-dialog-actions.tsx index 030b18e2..fe8186b7 100644 --- a/src/store/current-token-dialog/current-token-dialog-actions.tsx +++ b/src/store/current-token-dialog/current-token-dialog-actions.tsx @@ -3,7 +3,7 @@ // 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'; diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index fc08f3ac..2bfd8b99 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -62,6 +62,8 @@ export const navigateToRunProcess = push(Routes.RUN_PROCESS); 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); diff --git a/src/store/search-results-panel/search-results-panel-actions.ts b/src/store/search-results-panel/search-results-panel-actions.ts index 05da5b3e..f7dc5d45 100644 --- a/src/store/search-results-panel/search-results-panel-actions.ts +++ b/src/store/search-results-panel/search-results-panel-actions.ts @@ -6,11 +6,13 @@ import { Dispatch } from 'redux'; 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 diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts index 0e3c76b2..37de6f8c 100644 --- a/src/store/sharing-dialog/sharing-dialog-actions.ts +++ b/src/store/sharing-dialog/sharing-dialog-actions.ts @@ -56,13 +56,18 @@ export const sendSharingInvitations = async (dispatch: Dispatch) => { const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => { const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); - if (dialog) { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); - const { items } = await permissionService.listResourcePermissions(dialog.data); - dispatch(initializePublicAccessForm(items)); - await dispatch(initializeManagementForm(items)); - dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); + try { + const { items } = await permissionService.listResourcePermissions(dialog.data); + dispatch(initializePublicAccessForm(items)); + await dispatch(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)); + } } }; diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts index 562f7096..09009930 100644 --- a/src/store/side-panel-tree/side-panel-tree-actions.ts +++ b/src/store/side-panel-tree/side-panel-tree-actions.ts @@ -20,7 +20,6 @@ export enum SidePanelTreeCategory { PROJECTS = 'Projects', SHARED_WITH_ME = 'Shared with me', WORKFLOWS = 'Workflows', - RECENT_OPEN = 'Recently open', FAVORITES = 'Favorites', TRASH = 'Trash' } @@ -44,7 +43,6 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker) const SIDE_PANEL_CATEGORIES = [ SidePanelTreeCategory.WORKFLOWS, - SidePanelTreeCategory.RECENT_OPEN, SidePanelTreeCategory.FAVORITES, SidePanelTreeCategory.TRASH, ]; diff --git a/src/store/store.ts b/src/store/store.ts index 5e648c99..4ab0918e 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -43,6 +43,7 @@ import { searchBarReducer } from './search-bar/search-bar-reducer'; 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 = @@ -113,5 +114,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ runProcessPanel: runProcessPanelReducer, appInfo: appInfoReducer, searchBar: searchBarReducer, + virtualMachines: virtualMachinesReducer, repositories: repositoriesReducer }); diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts new file mode 100644 index 00000000..9bd79884 --- /dev/null +++ b/src/store/virtual-machines/virtual-machines-actions.ts @@ -0,0 +1,65 @@ +// 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(), + SET_VIRTUAL_MACHINES: ofType>(), + SET_LOGINS: ofType(), + SET_LINKS: ofType>() +}); + +export type VirtualMachineActions = UnionOf; + +export const VIRTUAL_MACHINES_PANEL = 'virtualMachinesPanel'; + +export const openVirtualMachines = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(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(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(loadRequestedDate()); + }; + +const virtualMachinesBindedActions = bindDataExplorerActions(VIRTUAL_MACHINES_PANEL); + +export const loadVirtualMachinesPanel = () => + (dispatch: Dispatch) => { + dispatch(virtualMachinesBindedActions.REQUEST_ITEMS()); + }; diff --git a/src/store/virtual-machines/virtual-machines-reducer.ts b/src/store/virtual-machines/virtual-machines-reducer.ts new file mode 100644 index 00000000..fa28417e --- /dev/null +++ b/src/store/virtual-machines/virtual-machines-reducer.ts @@ -0,0 +1,42 @@ +// 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; + logins: VirtualMachinesLoginsResource[]; + links: ListResults; +} + +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 + }); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 5e33661c..12dbe7b1 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -54,6 +54,7 @@ import { collectionPanelActions } from "~/store/collection-panel/collection-pane 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'; @@ -392,6 +393,12 @@ export const loadSearchResults = handleFirstTimeLoad( await dispatch(loadSearchResultsPanel()); }); +export const loadVirtualMachines = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadVirtualMachinesPanel()); + dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }])); + }); + export const loadRepositories = handleFirstTimeLoad( async (dispatch: Dispatch) => { await dispatch(loadRepositoriesPanel()); diff --git a/src/store/workflow-panel/workflow-panel-actions.ts b/src/store/workflow-panel/workflow-panel-actions.ts index da0da54d..3d51cbb8 100644 --- a/src/store/workflow-panel/workflow-panel-actions.ts +++ b/src/store/workflow-panel/workflow-panel-actions.ts @@ -9,7 +9,9 @@ import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-act 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'; @@ -17,8 +19,10 @@ const WORKFLOW_PANEL_DETAILS_UUID = 'workflowPanelDetailsUuid'; 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) => @@ -28,6 +32,17 @@ export const getUuidPrefix = (state: RootState) => { 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(navigateToRunProcess); + dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL()); + dispatch(goToStep(1)); + dispatch(runProcessPanelActions.SET_STEP_CHANGED(true)); + dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow!)); + }; + export const getPublicUserUuid = (state: RootState) => { const prefix = getProperty(UUID_PREFIX_PROPERTY_NAME)(state.properties); return `${prefix}-tpzed-anonymouspublic`; diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts index 5e1182bb..8c81e3bd 100644 --- a/src/views-components/context-menu/action-sets/project-action-set.ts +++ b/src/views-components/context-menu/action-sets/project-action-set.ts @@ -60,13 +60,13 @@ export const projectActionSet: ContextMenuActionSet = [[ dispatch(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", diff --git a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts index 3fa2f16f..6e86b2bc 100644 --- a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts +++ b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts @@ -5,6 +5,7 @@ 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", @@ -16,7 +17,7 @@ export const sshKeyActionSet: ContextMenuActionSet = [[{ name: "Advanced", icon: AdvancedIcon, execute: (dispatch, { uuid, index }) => { - // ToDo + dispatch(openAdvancedTabDialog(uuid, index)); } }, { name: "Remove", diff --git a/src/views-components/current-token-dialog/current-token-dialog.tsx b/src/views-components/current-token-dialog/current-token-dialog.tsx index 503206a6..934be54d 100644 --- a/src/views-components/current-token-dialog/current-token-dialog.tsx +++ b/src/views-components/current-token-dialog/current-token-dialog.tsx @@ -3,12 +3,12 @@ // 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'; @@ -36,7 +36,7 @@ type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyl export const CurrentTokenDialog = withStyles(styles)( connect(getCurrentTokenDialogData)( - withDialog('currentTokenDialog')( + withDialog(CURRENT_TOKEN_DIALOG_NAME)( class extends React.Component { render() { const { classes, open, closeDialog, ...data } = this.props; diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 87ba73ff..a032b3ed 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -18,8 +18,7 @@ import { ArvadosTheme } from '~/common/custom-theme'; 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'; @@ -87,12 +86,11 @@ const getPublicUuid = (uuidPrefix: string) => { return `${uuidPrefix}-tpzed-anonymouspublic`; }; -// ToDo: share onClick export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => { const isPublic = ownerUuid === getPublicUuid(uuidPrefix); return (
- { isPublic && uuid && + {!isPublic && uuid && dispatch(openSharingDialog(uuid))}> @@ -115,6 +113,28 @@ export const ResourceShare = connect( })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp) => resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)); +export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { + return ( +
+ {uuid && + + dispatch(openRunProcess(uuid))}> + + + } +
+ ); +}; + +export const ResourceRunProcess = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return { + uuid: resource ? resource.uuid : '' + }; + })((props: { uuid: string } & DispatchProp) => + resourceRunProcess(props.dispatch, props.uuid)); + export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => { if (ownerUuid === getPublicUuid(uuidPrefix)) { return renderStatus(ResourceStatus.PUBLIC); diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index f00c678e..ca88021c 100644 --- a/src/views-components/main-app-bar/account-menu.tsx +++ b/src/views-components/main-app-bar/account-menu.tsx @@ -13,6 +13,7 @@ import { RootState } from "~/store/store"; import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions'; import { openRepositoriesPanel } from "~/store/repositories/repositories-actions"; import { navigateToSshKeys } from '~/store/navigation/navigation-action'; +import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions"; interface AccountMenuProps { user?: User; @@ -32,6 +33,7 @@ export const AccountMenu = connect(mapStateToProps)( {getUserFullname(user)} + dispatch(openVirtualMachines())}>Virtual Machines dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token dispatch(navigateToSshKeys)}>Ssh Keys diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx index 741a7e00..6b84bde2 100644 --- a/src/views-components/main-content-bar/main-content-bar.tsx +++ b/src/views-components/main-content-bar/main-content-bar.tsx @@ -8,7 +8,7 @@ import { DetailsIcon } from "~/components/icon/icon"; import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs"; import { connect } from 'react-redux'; import { RootState } from '~/store/store'; -import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute } from '~/routes/routes'; +import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute } from '~/routes/routes'; import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; interface MainContentBarProps { @@ -22,6 +22,12 @@ const isWorkflowPath = ({ router }: RootState) => { 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); @@ -35,7 +41,7 @@ const isSshKeysPath = ({ router }: RootState) => { }; 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) => diff --git a/src/views-components/side-panel-tree/side-panel-tree.tsx b/src/views-components/side-panel-tree/side-panel-tree.tsx index 33ee97f9..dd5005c3 100644 --- a/src/views-components/side-panel-tree/side-panel-tree.tsx +++ b/src/views-components/side-panel-tree/side-panel-tree.tsx @@ -10,7 +10,7 @@ import { TreeItem } from "~/components/tree/tree"; 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'; @@ -59,8 +59,6 @@ const getSidePanelIcon = (category: string) => { return FavoriteIcon; case SidePanelTreeCategory.PROJECTS: return ProjectsIcon; - case SidePanelTreeCategory.RECENT_OPEN: - return RecentIcon; case SidePanelTreeCategory.SHARED_WITH_ME: return ShareMeIcon; case SidePanelTreeCategory.TRASH: diff --git a/src/views/run-process-panel/run-process-panel.tsx b/src/views/run-process-panel/run-process-panel.tsx index c8411ad7..c5b95c3b 100644 --- a/src/views/run-process-panel/run-process-panel.tsx +++ b/src/views/run-process-panel/run-process-panel.tsx @@ -6,7 +6,7 @@ import { Dispatch } from 'redux'; 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 => { diff --git a/src/views/virtual-machine-panel/virtual-machine-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-panel.tsx new file mode 100644 index 00000000..c94c3a74 --- /dev/null +++ b/src/views/virtual-machine-panel/virtual-machine-panel.tsx @@ -0,0 +1,200 @@ +// 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 = (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; + logins: VirtualMachinesLoginsResource[]; + links: ListResults; +} + +interface VirtualMachinesPanelActionProps { + saveRequestedDate: () => void; + loadVirtualMachinesData: () => string; +} + +type VirtualMachineProps = VirtualMachinesPanelActionProps & VirtualMachinesPanelDataProps & WithStyles; + +export const VirtualMachinePanel = compose( + withStyles(styles), + connect(mapStateToProps, mapDispatchToProps))( + class extends React.Component { + componentDidMount() { + this.props.loadVirtualMachinesData(); + } + + render() { + const { virtualMachines, links } = this.props; + return ( + + {virtualMachines.itemsAvailable === 0 && } + {virtualMachines.itemsAvailable > 0 && links.itemsAvailable > 0 && } + {} + + ); + } + } + ); + +const CardContentWithNoVirtualMachines = (props: VirtualMachineProps) => + + + + + + 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. + + + + + {props.requestedDate && + + A request for shell access was sent on {props.requestedDate} + } + + + + ; + +const CardContentWithVirtualMachines = (props: VirtualMachineProps) => + + + +
+ + {props.requestedDate && + + A request for shell access was sent on {props.requestedDate} + } +
+ + + + + Host name + Login name + Command line + Web shell + + + + {props.virtualMachines.items.map((it, index) => + + {it.hostname} + {getUsername(props.links, it)} + ssh {getUsername(props.links, it)}@shell.arvados + + + Log in as {getUsername(props.links, it)} + + + + )} + +
+
+
+
; + +const getUsername = (links: ListResults, virtualMachine: VirtualMachinesResource) => { + const link = links.items.find((item: any) => item.headUuid === virtualMachine.uuid); + return link.properties.username || undefined; +}; + +const CardSSHSection = (props: VirtualMachineProps) => + + + + + In order to access virtual machines using SSH, add an SSH key to your account and add a section like this to your SSH configuration file ( ~/.ssh/config): + + + + + ; + +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 diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 84c8e24c..3914f646 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -48,6 +48,7 @@ import { SshKeyPanel } from '~/views/ssh-key-panel/ssh-key-panel'; 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'; @@ -127,6 +128,7 @@ export const WorkbenchPanel = + diff --git a/src/views/workflow-panel/workflow-panel-view.tsx b/src/views/workflow-panel/workflow-panel-view.tsx index b8e0e436..da8a0c4b 100644 --- a/src/views/workflow-panel/workflow-panel-view.tsx +++ b/src/views/workflow-panel/workflow-panel-view.tsx @@ -11,14 +11,15 @@ import { 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 { @@ -110,11 +111,18 @@ export const workflowPanelColumns: DataColumns = [ configurable: false, filters: createTree(), render: (uuid: string) => + }, + { + name: '', + selected: true, + configurable: false, + filters: createTree(), + render: (uuid: string) => } ]; export const WorkflowPanelView = (props: WorkflowPanelProps) => { - return + return ({ }); const mapDispatchToProps = (dispatch: Dispatch): WorfklowPanelActionProps => ({ - handleRowDoubleClick: (uuid: string) => { dispatch(navigateTo(uuid)); },