From: Pawel Kowalczyk Date: Mon, 3 Dec 2018 12:32:53 +0000 (+0100) Subject: conflicts X-Git-Tag: 1.4.0~97^2~2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/cc72c29b709759a4498ad232e3f0374e857c7a62?hp=60e8ad5f90108900b4c189f88fbe483e7010432e conflicts Feature #14498 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- diff --git a/README.md b/README.md index ea9bc02f..e8d77701 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ Currently this configuration schema is supported: } ``` +#### VOCABULARY_URL +Local path, or any URL that allows cross-origin requests. See +[Vocabulary JSON file example](public/vocabulary-example.json). + ### Licensing Arvados is Free Software. See COPYING for information about Arvados Free diff --git a/public/vocabulary-example.json b/public/vocabulary-example.json new file mode 100644 index 00000000..b227dc23 --- /dev/null +++ b/public/vocabulary-example.json @@ -0,0 +1,32 @@ +{ + "strict": false, + "tags": { + "fruit": { + "values": ["pineapple", "tomato", "orange", "banana", "advocado", "lemon", "apple", "peach", "strawberry"], + "strict": true + }, + "animal": { + "values": ["human", "dog", "elephant", "eagle"], + "strict": false + }, + "color": { + "values": ["yellow", "red", "magenta", "green"], + "strict": false + }, + "text": {}, + "category": { + "values": ["experimental", "development", "production"] + }, + "comments": {}, + "importance": { + "values": ["critical", "important", "low priority"] + }, + "size": { + "values": ["x-small", "small", "medium", "large", "x-large"] + }, + "country": { + "values": ["Afghanistan","Åland Islands","Albania","Algeria","American Samoa","AndorrA","Angola","Anguilla","Antarctica","Antigua and Barbuda","Argentina","Armenia","Aruba","Australia","Austria","Azerbaijan","Bahamas","Bahrain","Bangladesh","Barbados","Belarus","Belgium","Belize","Benin","Bermuda","Bhutan","Bolivia","Bosnia and Herzegovina","Botswana","Bouvet Island","Brazil","British Indian Ocean Territory","Brunei Darussalam","Bulgaria","Burkina Faso","Burundi","Cambodia","Cameroon","Canada","Cape Verde","Cayman Islands","Central African Republic","Chad","Chile","China","Christmas Island","Cocos (Keeling) Islands","Colombia","Comoros","Congo","Congo, The Democratic Republic of the","Cook Islands","Costa Rica","Cote D'Ivoire","Croatia","Cuba","Cyprus","Czech Republic","Denmark","Djibouti","Dominica","Dominican Republic","Ecuador","Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Ethiopia","Falkland Islands (Malvinas)","Faroe Islands","Fiji","Finland","France","French Guiana","French Polynesia","French Southern Territories","Gabon","Gambia","Georgia","Germany","Ghana","Gibraltar","Greece","Greenland","Grenada","Guadeloupe","Guam","Guatemala","Guernsey","Guinea","Guinea-Bissau","Guyana","Haiti","Heard Island and Mcdonald Islands","Holy See (Vatican City State)","Honduras","Hong Kong","Hungary","Iceland","India","Indonesia","Iran, Islamic Republic Of","Iraq","Ireland","Isle of Man","Israel","Italy","Jamaica","Japan","Jersey","Jordan","Kazakhstan","Kenya","Kiribati","Korea, Democratic People'S Republic of","Korea, Republic of","Kuwait","Kyrgyzstan","Lao People'S Democratic Republic","Latvia","Lebanon","Lesotho","Liberia","Libyan Arab Jamahiriya","Liechtenstein","Lithuania","Luxembourg","Macao","Macedonia, The Former Yugoslav Republic of","Madagascar","Malawi","Malaysia","Maldives","Mali","Malta","Marshall Islands","Martinique","Mauritania","Mauritius","Mayotte","Mexico","Micronesia, Federated States of","Moldova, Republic of","Monaco","Mongolia","Montserrat","Morocco","Mozambique","Myanmar","Namibia","Nauru","Nepal","Netherlands","Netherlands Antilles","New Caledonia","New Zealand","Nicaragua","Niger","Nigeria","Niue","Norfolk Island","Northern Mariana Islands","Norway","Oman","Pakistan","Palau","Palestinian Territory, Occupied","Panama","Papua New Guinea","Paraguay","Peru","Philippines","Pitcairn","Poland","Portugal","Puerto Rico","Qatar","Reunion","Romania","Russian Federation","RWANDA","Saint Helena","Saint Kitts and Nevis","Saint Lucia","Saint Pierre and Miquelon","Saint Vincent and the Grenadines","Samoa","San Marino","Sao Tome and Principe","Saudi Arabia","Senegal","Serbia and Montenegro","Seychelles","Sierra Leone","Singapore","Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Georgia and the South Sandwich Islands","Spain","Sri Lanka","Sudan","Suriname","Svalbard and Jan Mayen","Swaziland","Sweden","Switzerland","Syrian Arab Republic","Taiwan, Province of China","Tajikistan","Tanzania, United Republic of","Thailand","Timor-Leste","Togo","Tokelau","Tonga","Trinidad and Tobago","Tunisia","Turkey","Turkmenistan","Turks and Caicos Islands","Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States","United States Minor Outlying Islands","Uruguay","Uzbekistan","Vanuatu","Venezuela","Viet Nam","Virgin Islands, British","Virgin Islands, U.S.","Wallis and Futuna","Western Sahara","Yemen","Zambia","Zimbabwe"], + "strict": true + } + } +} \ No newline at end of file diff --git a/src/common/config.ts b/src/common/config.ts index c74277e4..b7b89bd9 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -60,7 +60,8 @@ export const fetchConfig = () => { .then(config => Axios .get(getDiscoveryURL(config.API_HOST)) .then(response => ({ - config: {...response.data, vocabularyUrl: config.VOCABULARY_URL }, + // TODO: After tests delete `|| '/vocabulary-example.json'` + config: {...response.data, vocabularyUrl: config.VOCABULARY_URL || '/vocabulary-example.json' }, apiHost: config.API_HOST, }))); diff --git a/src/index.tsx b/src/index.tsx index 3ff8088c..d8385967 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,6 +50,7 @@ import HTML5Backend from 'react-dnd-html5-backend'; import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions'; import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set'; import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh-key-action-set'; +import { keepServiceActionSet } from '~/views-components/context-menu/action-sets/keep-service-action-set'; import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions'; import { virtualMachineActionSet } from '~/views-components/context-menu/action-sets/virtual-machine-action-set'; @@ -71,6 +72,7 @@ addMenuActionSet(ContextMenuKind.TRASH, trashActionSet); addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet); addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet); addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet); +addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet); fetchConfig() .then(({ config, apiHost }) => { diff --git a/src/models/keep.ts b/src/models/keep-services.ts similarity index 61% rename from src/models/keep.ts rename to src/models/keep-services.ts index f6b5ef2a..d99943c6 100644 --- a/src/models/keep.ts +++ b/src/models/keep-services.ts @@ -1,12 +1,13 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { Resource } from "./resource"; - -export interface KeepResource extends Resource { - serviceHost: string; - servicePort: number; - serviceSslFlag: boolean; - serviceType: string; -} +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Resource } from '~/models/resource'; + +export interface KeepServiceResource extends Resource { + serviceHost: string; + servicePort: number; + serviceSslFlag: boolean; + serviceType: string; + readOnly: boolean; +} \ No newline at end of file diff --git a/src/models/resource.ts b/src/models/resource.ts index 7e2127b2..ee901749 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -30,6 +30,7 @@ export enum ResourceKind { PROJECT = "arvados#group", REPOSITORY = "arvados#repository", SSH_KEY = "arvados#authorizedKeys", + KEEP_SERVICE = "arvados#keepService", USER = "arvados#user", VIRTUAL_MACHINE = "arvados#virtualMachine", WORKFLOW = "arvados#workflow", @@ -46,7 +47,8 @@ export enum ResourceObjectType { USER = 'tpzed', VIRTUAL_MACHINE = '2x53u', WORKFLOW = '7fd4e', - SSH_KEY = 'fngyi' + SSH_KEY = 'fngyi', + KEEP_SERVICE = 'bi6l4' } export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}'; @@ -85,6 +87,8 @@ export const extractUuidKind = (uuid: string = '') => { return ResourceKind.REPOSITORY; case ResourceObjectType.SSH_KEY: return ResourceKind.SSH_KEY; + case ResourceObjectType.KEEP_SERVICE: + return ResourceKind.KEEP_SERVICE; default: return undefined; } diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index 22d0b7c7..fdc4211f 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -4,10 +4,18 @@ import { History, Location } from 'history'; import { RootStore } from '~/store/store'; -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 { + matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, + matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute, + matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute, + matchKeepServicesRoute +} from './routes'; +import { + loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults, + loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog, + loadSshKeys, loadRepositories, loadVirtualMachines, loadKeepServices +} from '~/store/workbench/workbench-actions'; import { navigateToRootProject } from '~/store/navigation/navigation-action'; -import { loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults } from '~//store/workbench/workbench-actions'; export const addRouteChangeHandlers = (history: History, store: RootStore) => { const handler = handleLocationChange(store); @@ -23,13 +31,14 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { const trashMatch = matchTrashRoute(pathname); const processMatch = matchProcessRoute(pathname); const processLogMatch = matchProcessLogRoute(pathname); - const repositoryMatch = matchRepositoriesRoute(pathname); + const repositoryMatch = matchRepositoriesRoute(pathname); const searchResultsMatch = matchSearchResultsRoute(pathname); const sharedWithMeMatch = matchSharedWithMeRoute(pathname); const runProcessMatch = matchRunProcessRoute(pathname); const virtualMachineMatch = matchVirtualMachineRoute(pathname); const workflowMatch = matchWorkflowRoute(pathname); const sshKeysMatch = matchSshKeysRoute(pathname); + const keepServicesMatch = matchKeepServicesRoute(pathname); if (projectMatch) { store.dispatch(loadProject(projectMatch.params.id)); @@ -59,5 +68,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(loadRepositories); } else if (sshKeysMatch) { store.dispatch(loadSshKeys); + } else if (keepServicesMatch) { + store.dispatch(loadKeepServices); } }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 71cdfdac..5cd3e559 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -22,7 +22,8 @@ export const Routes = { VIRTUAL_MACHINES: '/virtual-machines', WORKFLOWS: '/workflows', SEARCH_RESULTS: '/search-results', - SSH_KEYS: `/ssh-keys` + SSH_KEYS: `/ssh-keys`, + KEEP_SERVICES: `/keep-services` }; export const getResourceUrl = (uuid: string) => { @@ -88,3 +89,6 @@ export const matchRepositoriesRoute = (route: string) => export const matchSshKeysRoute = (route: string) => matchPath(route, { path: Routes.SSH_KEYS }); + +export const matchKeepServicesRoute = (route: string) => + matchPath(route, { path: Routes.KEEP_SERVICES }); diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index b512fb24..98c03215 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -52,7 +52,7 @@ export class AuthService { } public getIsAdmin(): boolean { - return !!localStorage.getItem(USER_IS_ADMIN); + return localStorage.getItem(USER_IS_ADMIN) === 'true'; } public getUser(): User | undefined { diff --git a/src/services/keep-service/keep-service.ts b/src/services/keep-service/keep-service.ts index 17ee522e..5a89ba57 100644 --- a/src/services/keep-service/keep-service.ts +++ b/src/services/keep-service/keep-service.ts @@ -4,11 +4,11 @@ import { CommonResourceService } from "~/services/common-service/common-resource-service"; import { AxiosInstance } from "axios"; -import { KeepResource } from "~/models/keep"; +import { KeepServiceResource } from "~/models/keep-services"; import { ApiActions } from "~/services/api/api-actions"; -export class KeepService extends CommonResourceService { +export class KeepService extends CommonResourceService { constructor(serverApi: AxiosInstance, actions: ApiActions) { super(serverApi, "keep_services", 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 fd68eb4b..d9dabe5c 100644 --- a/src/store/advanced-tab/advanced-tab.ts +++ b/src/store/advanced-tab/advanced-tab.ts @@ -20,6 +20,7 @@ import { VirtualMachinesResource } from '~/models/virtual-machines'; import { UserResource } from '~/models/user'; import { ListResults } from '~/services/common-service/common-resource-service'; import { LinkResource } from '~/models/link'; +import { KeepServiceResource } from '~/models/keep-services'; export const ADVANCED_TAB_DIALOG = 'advancedTabDialog'; @@ -70,10 +71,16 @@ enum VirtualMachineData { enum ResourcePrefix { REPOSITORIES = 'repositories', AUTORIZED_KEYS = 'authorized_keys', - VIRTUAL_MACHINES = 'virtual_machines' + VIRTUAL_MACHINES = 'virtual_machines', + KEEP_SERVICES = 'keep_services' } -type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData; +enum KeepServiceData { + KEEP_SERVICE = 'keep_services', + CREATED_AT = 'created_at' +} + +type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData; type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix; export const openAdvancedTabDialog = (uuid: string) => @@ -170,6 +177,21 @@ export const openAdvancedTabDialog = (uuid: string) => }); dispatch(initAdvancedTabDialog(advanceDataVirtualMachine)); break; + case ResourceKind.KEEP_SERVICE: + const dataKeepService = getState().keepServices.find(it => it.uuid === uuid); + const advanceDataKeepService: AdvancedTabDialogData = advancedTabData({ + uuid, + metadata: '', + user: '', + apiResponseKind: keepServiceApiResponse, + data: dataKeepService, + resourceKind: KeepServiceData.KEEP_SERVICE, + resourcePrefix: ResourcePrefix.KEEP_SERVICES, + resourceKindProperty: KeepServiceData.CREATED_AT, + property: dataKeepService!.createdAt + }); + dispatch(initAdvancedTabDialog(advanceDataKeepService)); + break; default: dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } @@ -379,12 +401,34 @@ const sshKeyApiResponse = (apiResponse: SshKeyResource) => { const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource) => { const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = apiResponse; - const response = `"uuid": "${uuid}", + const response = `"hostname": ${stringify(hostname)}, +"uuid": "${uuid}", "owner_uuid": "${ownerUuid}", "modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, "modified_by_user_uuid": ${stringify(modifiedByUserUuid)}, "modified_at": ${stringify(modifiedAt)}, -"hostname": ${stringify(hostname)}, +"modified_at": ${stringify(modifiedAt)}, "created_at": "${createdAt}"`; + + return response; +}; + +const keepServiceApiResponse = (apiResponse: KeepServiceResource) => { + const { + uuid, readOnly, serviceHost, servicePort, serviceSslFlag, serviceType, + ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid + } = apiResponse; + const response = `"uuid": "${uuid}", +"owner_uuid": "${ownerUuid}", +"modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, +"modified_by_user_uuid": ${stringify(modifiedByUserUuid)}, +"modified_at": ${stringify(modifiedAt)}, +"service_host": "${serviceHost}", +"service_port": "${servicePort}", +"service_ssl_flag": "${stringify(serviceSslFlag)}", +"service_type": "${serviceType}", +"created_at": "${createdAt}", +"read_only": "${stringify(readOnly)}"`; + return response; }; \ No newline at end of file diff --git a/src/store/auth/auth-actions.test.ts b/src/store/auth/auth-actions.test.ts index 3d6913a6..c54438b1 100644 --- a/src/store/auth/auth-actions.test.ts +++ b/src/store/auth/auth-actions.test.ts @@ -43,7 +43,7 @@ describe('auth-actions', () => { localStorage.setItem(USER_LAST_NAME_KEY, "Doe"); localStorage.setItem(USER_UUID_KEY, "uuid"); localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid"); - localStorage.setItem(USER_IS_ADMIN, JSON.stringify(false)); + localStorage.setItem(USER_IS_ADMIN, JSON.stringify("false")); store.dispatch(initAuth()); @@ -56,7 +56,7 @@ describe('auth-actions', () => { lastName: "Doe", uuid: "uuid", ownerUuid: "ownerUuid", - isAdmin: false + isAdmin: true } }); }); diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts index 3dd486b3..a4017db3 100644 --- a/src/store/auth/auth-reducer.test.ts +++ b/src/store/auth/auth-reducer.test.ts @@ -30,7 +30,7 @@ describe('auth-reducer', () => { lastName: "Doe", uuid: "uuid", ownerUuid: "ownerUuid", - isAdmin: true + isAdmin: false }; const state = reducer(initialState, authActions.INIT({ user, token: "token" })); expect(state).toEqual({ @@ -60,7 +60,7 @@ describe('auth-reducer', () => { lastName: "Doe", uuid: "uuid", ownerUuid: "ownerUuid", - isAdmin: true + isAdmin: false }; const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user)); @@ -73,7 +73,7 @@ describe('auth-reducer', () => { lastName: "Doe", uuid: "uuid", ownerUuid: "ownerUuid", - isAdmin: true + isAdmin: false } }); }); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 1412d958..d56a3fb5 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -16,6 +16,7 @@ import { Process } from '~/store/processes/process'; import { RepositoryResource } from '~/models/repositories'; import { SshKeyResource } from '~/models/ssh-key'; import { VirtualMachinesResource } from '~/models/virtual-machines'; +import { KeepServiceResource } from '~/models/keep-services'; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), @@ -34,8 +35,9 @@ export type ContextMenuResource = { isTrashed?: boolean; index?: number }; -export const isKeyboardClick = (event: React.MouseEvent) => - event.nativeEvent.detail === 0; + +export const isKeyboardClick = (event: React.MouseEvent) => event.nativeEvent.detail === 0; + export const openContextMenu = (event: React.MouseEvent, resource: ContextMenuResource) => (dispatch: Dispatch) => { event.preventDefault(); @@ -96,6 +98,17 @@ export const openSshKeyContextMenu = (event: React.MouseEvent, sshK })); }; +export const openKeepServiceContextMenu = (event: React.MouseEvent, keepService: KeepServiceResource) => + (dispatch: Dispatch) => { + dispatch(openContextMenu(event, { + name: '', + uuid: keepService.uuid, + ownerUuid: keepService.ownerUuid, + kind: ResourceKind.KEEP_SERVICE, + menuKind: ContextMenuKind.KEEP_SERVICE + })); + }; + export const openRootProjectContextMenu = (event: React.MouseEvent, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => { const res = getResource(projectUuid)(getState().resources); diff --git a/src/store/keep-services/keep-services-actions.ts b/src/store/keep-services/keep-services-actions.ts new file mode 100644 index 00000000..54a7c3fe --- /dev/null +++ b/src/store/keep-services/keep-services-actions.ts @@ -0,0 +1,71 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from "redux"; +import { unionize, ofType, UnionOf } from "~/common/unionize"; +import { RootState } from '~/store/store'; +import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions'; +import { ServiceRepository } from "~/services/services"; +import { KeepServiceResource } from '~/models/keep-services'; +import { dialogActions } from '~/store/dialog/dialog-actions'; +import { snackbarActions } from '~/store/snackbar/snackbar-actions'; +import { navigateToRootProject } from '~/store/navigation/navigation-action'; + +export const keepServicesActions = unionize({ + SET_KEEP_SERVICES: ofType(), + REMOVE_KEEP_SERVICE: ofType() +}); + +export type KeepServicesActions = UnionOf; + +export const KEEP_SERVICE_REMOVE_DIALOG = 'keepServiceRemoveDialog'; +export const KEEP_SERVICE_ATTRIBUTES_DIALOG = 'keepServiceAttributesDialog'; + +export const loadKeepServicesPanel = () => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const user = getState().auth.user; + if(user && user.isAdmin) { + try { + dispatch(setBreadcrumbs([{ label: 'Keep Services' }])); + const response = await services.keepService.list(); + dispatch(keepServicesActions.SET_KEEP_SERVICES(response.items)); + } catch (e) { + return; + } + } else { + dispatch(navigateToRootProject); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000 })); + } + }; + +export const openKeepServiceAttributesDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + const keepService = getState().keepServices.find(it => it.uuid === uuid); + dispatch(dialogActions.OPEN_DIALOG({ id: KEEP_SERVICE_ATTRIBUTES_DIALOG, data: { keepService } })); + }; + +export const openKeepServiceRemoveDialog = (uuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + dispatch(dialogActions.OPEN_DIALOG({ + id: KEEP_SERVICE_REMOVE_DIALOG, + data: { + title: 'Remove keep service', + text: 'Are you sure you want to remove this keep service?', + confirmButtonLabel: 'Remove', + uuid + } + })); + }; + +export const removeKeepService = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' })); + try { + await services.keepService.delete(uuid); + dispatch(keepServicesActions.REMOVE_KEEP_SERVICE(uuid)); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Keep service has been successfully removed.', hideDuration: 2000 })); + } catch (e) { + return; + } + }; \ No newline at end of file diff --git a/src/store/keep-services/keep-services-reducer.ts b/src/store/keep-services/keep-services-reducer.ts new file mode 100644 index 00000000..043c010a --- /dev/null +++ b/src/store/keep-services/keep-services-reducer.ts @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { keepServicesActions, KeepServicesActions } from '~/store/keep-services/keep-services-actions'; +import { KeepServiceResource } from '~/models/keep-services'; + +export type KeepSericesState = KeepServiceResource[]; + +const initialState: KeepSericesState = []; + +export const keepServicesReducer = (state: KeepSericesState = initialState, action: KeepServicesActions): KeepSericesState => + keepServicesActions.match(action, { + SET_KEEP_SERVICES: items => items, + REMOVE_KEEP_SERVICE: (uuid: string) => state.filter((keepService) => keepService.uuid !== uuid), + default: () => state + }); \ No newline at end of file diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index 2bfd8b99..d452710c 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -67,3 +67,5 @@ export const navigateToVirtualMachines = push(Routes.VIRTUAL_MACHINES); export const navigateToRepositories = push(Routes.REPOSITORIES); export const navigateToSshKeys= push(Routes.SSH_KEYS); + +export const navigateToKeepServices = push(Routes.KEEP_SERVICES); \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts index 4ab0918e..f8bdcc24 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -45,6 +45,7 @@ import { SearchResultsMiddlewareService } from './search-results-panel/search-re import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer"; import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer"; import { repositoriesReducer } from '~/store/repositories/repositories-reducer'; +import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -115,5 +116,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({ appInfo: appInfoReducer, searchBar: searchBarReducer, virtualMachines: virtualMachinesReducer, - repositories: repositoriesReducer + repositories: repositoriesReducer, + keepServices: keepServicesReducer }); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 12dbe7b1..667f1c80 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -56,6 +56,7 @@ import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/searc import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view'; import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions'; import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions'; +import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions'; export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen'; @@ -410,6 +411,11 @@ export const loadSshKeys = handleFirstTimeLoad( await dispatch(loadSshKeysPanel()); }); +export const loadKeepServices = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadKeepServicesPanel()); + }); + const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch) => { const uuid = typeof project === 'string' ? project : project.uuid; diff --git a/src/views-components/context-menu/action-sets/keep-service-action-set.ts b/src/views-components/context-menu/action-sets/keep-service-action-set.ts new file mode 100644 index 00000000..807a3abf --- /dev/null +++ b/src/views-components/context-menu/action-sets/keep-service-action-set.ts @@ -0,0 +1,28 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { openKeepServiceAttributesDialog, openKeepServiceRemoveDialog } from '~/store/keep-services/keep-services-actions'; +import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab'; +import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set"; +import { AdvancedIcon, RemoveIcon, AttributesIcon } from "~/components/icon/icon"; + +export const keepServiceActionSet: ContextMenuActionSet = [[{ + name: "Attributes", + icon: AttributesIcon, + execute: (dispatch, { uuid }) => { + dispatch(openKeepServiceAttributesDialog(uuid)); + } +}, { + name: "Advanced", + icon: AdvancedIcon, + execute: (dispatch, { uuid }) => { + dispatch(openAdvancedTabDialog(uuid)); + } +}, { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, { uuid }) => { + dispatch(openKeepServiceRemoveDialog(uuid)); + } +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index d08798f7..5f321bfe 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -71,5 +71,6 @@ export enum ContextMenuKind { PROCESS_LOGS = "ProcessLogs", REPOSITORY = "Repository", SSH_KEY = "SshKey", - VIRTUAL_MACHINE = "VirtualMachine" + VIRTUAL_MACHINE = "VirtualMachine", + KEEP_SERVICE = "KeepService" } diff --git a/src/views-components/keep-services-dialog/attributes-dialog.tsx b/src/views-components/keep-services-dialog/attributes-dialog.tsx new file mode 100644 index 00000000..113d191b --- /dev/null +++ b/src/views-components/keep-services-dialog/attributes-dialog.tsx @@ -0,0 +1,73 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { compose } from 'redux'; +import { + withStyles, Dialog, DialogTitle, DialogContent, DialogActions, + Button, StyleRulesCallback, WithStyles, Grid +} from '@material-ui/core'; +import { WithDialogProps, withDialog } from "~/store/dialog/with-dialog"; +import { KEEP_SERVICE_ATTRIBUTES_DIALOG } from '~/store/keep-services/keep-services-actions'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { KeepServiceResource } from '~/models/keep-services'; + +type CssRules = 'root'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + fontSize: '0.875rem', + '& div:nth-child(odd)': { + textAlign: 'right', + color: theme.palette.grey["500"] + } + } +}); + +interface AttributesKeepServiceDialogDataProps { + keepService: KeepServiceResource; +} + +export const AttributesKeepServiceDialog = compose( + withDialog(KEEP_SERVICE_ATTRIBUTES_DIALOG), + withStyles(styles))( + ({ open, closeDialog, data, classes }: WithDialogProps & WithStyles) => + + Attributes + + {data.keepService && + UUID + {data.keepService.uuid} + Read only + {JSON.stringify(data.keepService.readOnly)} + Service host + {data.keepService.serviceHost} + Service port + {data.keepService.servicePort} + Service SSL flag + {JSON.stringify(data.keepService.serviceSslFlag)} + Service type + {data.keepService.serviceType} + Owner uuid + {data.keepService.ownerUuid} + Created at + {data.keepService.createdAt} + Modified at + {data.keepService.modifiedAt} + Modified by user uuid + {data.keepService.modifiedByUserUuid} + Modified by client uuid + {data.keepService.modifiedByClientUuid} + } + + + + + + ); \ No newline at end of file diff --git a/src/views-components/keep-services-dialog/remove-dialog.tsx b/src/views-components/keep-services-dialog/remove-dialog.tsx new file mode 100644 index 00000000..7e398509 --- /dev/null +++ b/src/views-components/keep-services-dialog/remove-dialog.tsx @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 +import { Dispatch, compose } from 'redux'; +import { connect } from "react-redux"; +import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog"; +import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog"; +import { KEEP_SERVICE_REMOVE_DIALOG, removeKeepService } from '~/store/keep-services/keep-services-actions'; + +const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps) => ({ + onConfirm: () => { + props.closeDialog(); + dispatch(removeKeepService(props.data.uuid)); + } +}); + +export const RemoveKeepServiceDialog = compose( + withDialog(KEEP_SERVICE_REMOVE_DIALOG), + connect(null, mapDispatchToProps) +)(ConfirmationDialog); \ No newline at end of file diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index ca88021c..075aa69a 100644 --- a/src/views-components/main-app-bar/account-menu.tsx +++ b/src/views-components/main-app-bar/account-menu.tsx @@ -12,7 +12,7 @@ import { logout } from '~/store/auth/auth-action'; import { RootState } from "~/store/store"; import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-token-dialog-actions'; import { openRepositoriesPanel } from "~/store/repositories/repositories-actions"; -import { navigateToSshKeys } from '~/store/navigation/navigation-action'; +import { navigateToSshKeys, navigateToKeepServices } from '~/store/navigation/navigation-action'; import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions"; interface AccountMenuProps { @@ -37,6 +37,7 @@ export const AccountMenu = connect(mapStateToProps)( dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token dispatch(navigateToSshKeys)}>Ssh Keys + { user.isAdmin && dispatch(navigateToKeepServices)}>Keep Services } My account dispatch(logout())}>Logout 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 6b84bde2..66d7cabc 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, matchVirtualMachineRoute } from '~/routes/routes'; +import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute, matchKeepServicesRoute } from '~/routes/routes'; import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; interface MainContentBarProps { @@ -16,32 +16,14 @@ interface MainContentBarProps { buttonVisible: boolean; } -const isWorkflowPath = ({ router }: RootState) => { +const isButtonVisible = ({ router }: RootState) => { const pathname = router.location ? router.location.pathname : ''; - const match = matchWorkflowRoute(pathname); - return !!match; -}; - -const isVirtualMachinePath = ({ router }: RootState) => { - const pathname = router.location ? router.location.pathname : ''; - const match = matchVirtualMachineRoute(pathname); - return !!match; -}; - -const isRepositoriesPath = ({ router }: RootState) => { - const pathname = router.location ? router.location.pathname : ''; - const match = matchRepositoriesRoute(pathname); - return !!match; -}; - -const isSshKeysPath = ({ router }: RootState) => { - const pathname = router.location ? router.location.pathname : ''; - const match = matchSshKeysRoute(pathname); - return !!match; + return !matchWorkflowRoute(pathname) && !matchVirtualMachineRoute(pathname) && + !matchRepositoriesRoute(pathname) && !matchSshKeysRoute(pathname) && !matchKeepServicesRoute(pathname); }; export const MainContentBar = connect((state: RootState) => ({ - buttonVisible: !isWorkflowPath(state) && !isSshKeysPath(state) && !isRepositoriesPath(state) && !isVirtualMachinePath(state) + buttonVisible: isButtonVisible(state) }), { onDetailsPanelToggle: toggleDetailsPanel })((props: MainContentBarProps) => diff --git a/src/views/keep-service-panel/keep-service-panel-root.tsx b/src/views/keep-service-panel/keep-service-panel-root.tsx new file mode 100644 index 00000000..8c266b61 --- /dev/null +++ b/src/views/keep-service-panel/keep-service-panel-root.tsx @@ -0,0 +1,87 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Button, Typography, Grid, Table, TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton, Checkbox } from '@material-ui/core'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { MoreOptionsIcon } from '~/components/icon/icon'; +import { KeepServiceResource } from '~/models/keep-services'; + +type CssRules = 'root' | 'tableRow'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + overflow: 'auto' + }, + tableRow: { + '& td, th': { + whiteSpace: 'nowrap' + } + } +}); + +export interface KeepServicePanelRootActionProps { + openRowOptions: (event: React.MouseEvent, keepService: KeepServiceResource) => void; +} + +export interface KeepServicePanelRootDataProps { + keepServices: KeepServiceResource[]; + hasKeepSerices: boolean; +} + +type KeepServicePanelRootProps = KeepServicePanelRootActionProps & KeepServicePanelRootDataProps & WithStyles; + +export const KeepServicePanelRoot = withStyles(styles)( + ({ classes, hasKeepSerices, keepServices, openRowOptions }: KeepServicePanelRootProps) => + + + {hasKeepSerices && + + + + + UUID + Read only + Service host + Service port + Service SSL flag + Service type + + + + + {keepServices.map((keepService, index) => + + {keepService.uuid} + + + + {keepService.serviceHost} + {keepService.servicePort} + + + + {keepService.serviceType} + + + openRowOptions(event, keepService)}> + + + + + )} + +
+
+
} +
+
+); \ No newline at end of file diff --git a/src/views/keep-service-panel/keep-service-panel.tsx b/src/views/keep-service-panel/keep-service-panel.tsx new file mode 100644 index 00000000..a11cee0b --- /dev/null +++ b/src/views/keep-service-panel/keep-service-panel.tsx @@ -0,0 +1,29 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { RootState } from '~/store/store'; +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { } from '~/store/keep-services/keep-services-actions'; +import { + KeepServicePanelRoot, + KeepServicePanelRootDataProps, + KeepServicePanelRootActionProps +} from '~/views/keep-service-panel/keep-service-panel-root'; +import { openKeepServiceContextMenu } from '~/store/context-menu/context-menu-actions'; + +const mapStateToProps = (state: RootState): KeepServicePanelRootDataProps => { + return { + keepServices: state.keepServices, + hasKeepSerices: state.keepServices.length > 0 + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch): KeepServicePanelRootActionProps => ({ + openRowOptions: (event, keepService) => { + dispatch(openKeepServiceContextMenu(event, keepService)); + } +}); + +export const KeepServicePanel = connect(mapStateToProps, mapDispatchToProps)(KeepServicePanelRoot); \ No newline at end of file diff --git a/src/views/run-process-panel/inputs/boolean-input.tsx b/src/views/run-process-panel/inputs/boolean-input.tsx index 5da54742..6a214e9d 100644 --- a/src/views/run-process-panel/inputs/boolean-input.tsx +++ b/src/views/run-process-panel/inputs/boolean-input.tsx @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; +import { memoize } from 'lodash/fp'; import { BooleanCommandInputParameter } from '~/models/workflow'; import { Field } from 'redux-form'; import { Switch } from '@material-ui/core'; @@ -16,17 +17,23 @@ export const BooleanInput = ({ input }: BooleanInputProps) => name={input.id} commandInput={input} component={BooleanInputComponent} - normalize={(value, prevValue) => !prevValue} + normalize={normalize} />; +const normalize = (_: any, prevValue: boolean) => !prevValue; + const BooleanInputComponent = (props: GenericInputProps) => ; -const Input = (props: GenericInputProps) => +const Input = ({ input, commandInput }: GenericInputProps) => props.input.onChange(props.input.value)} - disabled={props.commandInput.disabled} />; \ No newline at end of file + checked={input.value} + onChange={handleChange(input.onChange, input.value)} + disabled={commandInput.disabled} />; + +const handleChange = memoize( + (onChange: (value: string) => void, value: string) => () => onChange(value) +); diff --git a/src/views/run-process-panel/inputs/directory-input.tsx b/src/views/run-process-panel/inputs/directory-input.tsx index aa25fefc..29ccd6e0 100644 --- a/src/views/run-process-panel/inputs/directory-input.tsx +++ b/src/views/run-process-panel/inputs/directory-input.tsx @@ -3,23 +3,24 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; +import { connect, DispatchProp } from 'react-redux'; +import { memoize } from 'lodash/fp'; +import { Field } from 'redux-form'; +import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core'; import { isRequiredInput, DirectoryCommandInputParameter, CWLType, Directory } from '~/models/workflow'; -import { Field } from 'redux-form'; -import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core'; import { GenericInputProps, GenericInput } from './generic-input'; import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker'; -import { connect, DispatchProp } from 'react-redux'; import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions'; import { TreeItem } from '~/components/tree/tree'; import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker'; import { CollectionResource } from '~/models/collection'; import { ResourceKind } from '~/models/resource'; -import { ERROR_MESSAGE } from '../../../validators/require'; +import { ERROR_MESSAGE } from '~/validators/require'; export interface DirectoryInputProps { input: DirectoryCommandInputParameter; @@ -29,18 +30,25 @@ export const DirectoryInput = ({ input }: DirectoryInputProps) => name={input.id} commandInput={input} component={DirectoryInputComponent} - format={(value?: Directory) => value ? value.basename : ''} - parse={(directory: CollectionResource): Directory => ({ - class: CWLType.DIRECTORY, - location: `keep:${directory.portableDataHash}`, - basename: directory.name, - })} - validate={[ - isRequiredInput(input) - ? (directory?: Directory) => directory ? undefined : ERROR_MESSAGE - : () => undefined, - ]} />; - + format={format} + parse={parse} + validate={getValidation(input)} />; + +const format = (value?: Directory) => value ? value.basename : ''; + +const parse = (directory: CollectionResource): Directory => ({ + class: CWLType.DIRECTORY, + location: `keep:${directory.portableDataHash}`, + basename: directory.name, +}); + +const getValidation = memoize( + (input: DirectoryCommandInputParameter) => ([ + isRequiredInput(input) + ? (directory?: Directory) => directory ? undefined : ERROR_MESSAGE + : () => undefined, + ]) +); interface DirectoryInputComponentState { open: boolean; @@ -78,7 +86,7 @@ const DirectoryInputComponent = connect()( this.props.input.onChange(this.state.directory); } - setDirectory = (event: React.MouseEvent, { data }: TreeItem, pickerId: string) => { + setDirectory = (_: {}, { data }: TreeItem) => { if ('kind' in data && data.kind === ResourceKind.COLLECTION) { this.setState({ directory: data }); } else { diff --git a/src/views/run-process-panel/inputs/enum-input.tsx b/src/views/run-process-panel/inputs/enum-input.tsx index 86ff6fb1..3b0289e7 100644 --- a/src/views/run-process-panel/inputs/enum-input.tsx +++ b/src/views/run-process-panel/inputs/enum-input.tsx @@ -3,9 +3,9 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { EnumCommandInputParameter, CommandInputEnumSchema } from '~/models/workflow'; import { Field } from 'redux-form'; import { Select, MenuItem } from '@material-ui/core'; +import { EnumCommandInputParameter, CommandInputEnumSchema } from '~/models/workflow'; import { GenericInputProps, GenericInput } from './generic-input'; export interface EnumInputProps { @@ -30,8 +30,20 @@ const Input = (props: GenericInputProps) => { onChange={props.input.onChange} disabled={props.commandInput.disabled} > {type.symbols.map(symbol => - - {symbol.split('/').pop()} + + {extractValue(symbol)} )} ; -}; \ No newline at end of file +}; + +/** + * Values in workflow definition have an absolute form, for example: + * + * ```#input_collector.cwl/enum_type/Pathway table``` + * + * We want a value that is in form accepted by backend. + * According to the example above, the correct value is: + * + * ```Pathway table``` + */ +const extractValue = (symbol: string) => symbol.split('/').pop(); diff --git a/src/views/run-process-panel/inputs/file-input.tsx b/src/views/run-process-panel/inputs/file-input.tsx index 7e0925e8..06111007 100644 --- a/src/views/run-process-panel/inputs/file-input.tsx +++ b/src/views/run-process-panel/inputs/file-input.tsx @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; +import { memoize } from 'lodash/fp'; import { isRequiredInput, FileCommandInputParameter, @@ -28,18 +29,24 @@ export const FileInput = ({ input }: FileInputProps) => name={input.id} commandInput={input} component={FileInputComponent} - format={(value?: File) => value ? value.basename : ''} - parse={(file: CollectionFile): File => ({ - class: CWLType.FILE, - location: `keep:${file.id}`, - basename: file.name, - })} - validate={[ - isRequiredInput(input) - ? (file?: File) => file ? undefined : ERROR_MESSAGE - : () => undefined, - ]} />; + format={format} + parse={parse} + validate={getValidation(input)} />; +const format = (value?: File) => value ? value.basename : ''; + +const parse = (file: CollectionFile): File => ({ + class: CWLType.FILE, + location: `keep:${file.id}`, + basename: file.name, +}); + +const getValidation = memoize( + (input: FileCommandInputParameter) => ([ + isRequiredInput(input) + ? (file?: File) => file ? undefined : ERROR_MESSAGE + : () => undefined, + ])); interface FileInputComponentState { open: boolean; @@ -77,7 +84,7 @@ const FileInputComponent = connect()( this.props.input.onChange(this.state.file); } - setFile = (event: React.MouseEvent, { data }: TreeItem, pickerId: string) => { + setFile = (_: {}, { data }: TreeItem) => { if ('type' in data && data.type === CollectionFileType.FILE) { this.setState({ file: data }); } else { diff --git a/src/views/run-process-panel/inputs/float-input.tsx b/src/views/run-process-panel/inputs/float-input.tsx index 56a58012..a5905dc5 100644 --- a/src/views/run-process-panel/inputs/float-input.tsx +++ b/src/views/run-process-panel/inputs/float-input.tsx @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; +import { memoize } from 'lodash/fp'; import { FloatCommandInputParameter, isRequiredInput } from '~/models/workflow'; import { Field } from 'redux-form'; import { isNumber } from '~/validators/is-number'; @@ -17,11 +18,17 @@ export const FloatInput = ({ input }: FloatInputProps) => commandInput={input} component={Input} parse={parseFloat} - format={value => isNaN(value) ? '' : JSON.stringify(value)} - validate={[ - isRequiredInput(input) - ? isNumber - : () => undefined,]} />; + format={format} + validate={getValidation(input)} />; + +const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value); + +const getValidation = memoize( + (input: FloatCommandInputParameter) => ([ + isRequiredInput(input) + ? isNumber + : () => undefined,]) +); const Input = (props: GenericInputProps) => name={input.id} commandInput={input} component={InputComponent} - parse={value => parseInt(value, 10)} - format={value => isNaN(value) ? '' : JSON.stringify(value)} - validate={[ - isRequiredInput(input) - ? isInteger - : () => undefined, - ]} />; + parse={parse} + format={format} + validate={getValidation(input)} />; + +const parse = (value: any) => parseInt(value, 10); + +const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value); + +const getValidation = memoize( + (input: IntCommandInputParameter) => ([ + isRequiredInput(input) + ? isInteger + : () => undefined, + ])); const InputComponent = (props: GenericInputProps) => name={input.id} commandInput={input} component={StringInputComponent} - validate={[ - isRequiredInput(input) - ? require - : () => undefined, - ]} />; + validate={getValidation(input)} />; + +const getValidation = memoize( + (input: StringCommandInputParameter) => ([ + isRequiredInput(input) + ? require + : () => undefined, + ])); const StringInputComponent = (props: GenericInputProps) => + @@ -142,6 +146,7 @@ export const WorkbenchPanel = + @@ -163,6 +168,7 @@ export const WorkbenchPanel = +