From: Janicki Artur Date: Mon, 3 Dec 2018 12:05:41 +0000 (+0100) Subject: Merge branch 'master' of git.curoverse.com:arvados-workbench2 into 14503_keep_service... X-Git-Tag: 1.4.0~99^2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/785a62a8934dc439cbd201d9011775ccbcbb2c24?hp=f320a6e774ee53dbefb8dafdc40a4f42ab7454ed Merge branch 'master' of git.curoverse.com:arvados-workbench2 into 14503_keep_services_panel refs #2 14503 Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- 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/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/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) =>