|
- import { h, Component, createRef } from 'preact';
- import { route } from 'preact-router';
- import WBNavbarCommon from 'wb-navbar-common';
- import WBArvadosCrumbs from 'wb-arvados-crumbs';
- import WBBrowseDialog from 'wb-browse-dialog';
- import WBTable from 'wb-table';
- import WBNameAndUuid from 'wb-name-and-uuid';
- import makeArvadosRequest from 'make-arvados-request';
- import { wbDisableControls, wbEnableControls } from 'wb-disable-controls';
- import linkState from 'linkstate';
-
- function parseDefinition(text) {
- let definition;
- try {
- definition = JSON.parse(text);
- } catch (_) {
- definition = jsyaml.load(text);
- }
- return definition;
- }
-
- function encodeURIComponentIncludingDots(s) {
- return encodeURIComponent(s).replace('.', '%2E');
- }
-
- function inputSpecInfo(inputSpec) {
- const isFile = (inputSpec.type === 'File' || inputSpec.type === 'File[]' ||
- (inputSpec.type.type === 'array' && [].concat(inputSpec.type.items).indexOf('File') !== -1));
-
- const isDirectory = (inputSpec.type === 'Directory' || inputSpec.type === 'Directory[]' ||
- (inputSpec.type.type === 'array' && [].concat(inputSpec.type.items).indexOf('Directory') !== -1));
-
- const isArray = (inputSpec.type === 'File[]' || inputSpec.type === 'Directory[]' ||
- inputSpec.type.type === 'array');
-
- return { isFile, isDirectory, isArray };
- }
-
- function uuidsToCwl(obj) {
- if (obj instanceof Array) {
- const res = [];
- for (let k in obj) {
- res[k] = uuidsToCwl(obj[k]);
- }
- return res;
- }
-
- if (typeof(obj) === 'string' &&
- (/^[0-9a-z]{5}-[0-9a-z]{5}-[0-9a-z]{15}/.exec(obj) ||
- /^[0-9a-f]{32}\+[0-9]+/.exec(obj))) {
-
- const isDirectory = obj.endsWith('/');
-
- return {
- 'class': (isDirectory ? 'Directory' : 'File'),
- 'location': 'keep:' + (isDirectory ? obj.substr(0, obj.length - 1) : obj)
- };
- }
-
- throw Error('Expected Arvados path or array of paths');
- }
-
- function parseKeepRef(value) {
- if (typeof(value) === 'object' && 'location' in value && value.location.startsWith('keep:'))
- return value.location.substr(5);
- return value;
- }
-
- class WBPathDisplay extends Component {
- fetchData() {
- const { app } = this.props;
- const { arvHost, arvToken } = app.state;
- let { path } = this.props;
- if (path.endsWith('/'))
- path = path.substr(0, path.length - 1);
- let m;
- if (m = /^[0-9a-f]{32}\+[0-9]+/.exec(path));
- else if (m = /^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/.exec(path));
- else return;
- let prom = makeArvadosRequest(arvHost, arvToken,
- '/arvados/v1/collections/' + m[0]);
- prom = prom.then(xhr => this.setState({
- item: xhr.response,
- tail: path.substr(m[0].length)
- }));
- prom = prom.catch(() => this.setState({ 'error': 'Cannot load' }));
- }
-
- componentDidMount() {
- this.fetchData();
- }
-
- componentWillReceiveProps(nextProps) {
- this.props = nextProps;
- this.fetchData();
- }
-
- render({}, { item, tail, error }) {
- if (error)
- return error;
-
- if (!item)
- return 'Loading...';
-
- return (
- <span>
- <a href={ '/collection-browse/' + item.uuid }>
- { item.name || item.uuid }
- </a><a href={ '/collection-browse/' + item.uuid + '/' + encodeURIComponentIncludingDots(tail) }>
- { tail }
- </a>
- </span>
- );
- }
- }
-
- class WBLaunchWorkflowPage extends Component {
- constructor(...args) {
- super(...args);
- this.browseDialogRef = createRef();
- this.state.inputs = {};
- this.state.errors = [];
- }
-
- componentDidMount() {
- let { app, workflowUuid } = this.props;
- let { arvHost, arvToken } = app.state;
-
- let prom = makeArvadosRequest(arvHost, arvToken,
- '/arvados/v1/workflows/' + workflowUuid);
- prom = prom.then(xhr => {
- const def = parseDefinition(xhr.response.definition);
- const inputs = {};
- const main = def['$graph'].find(a => (a.id === '#main'));
- main.inputs.map(a => (inputs[a.id] = JSON.stringify(a.default)));
- this.setState({
- 'workflow': xhr.response,
- 'workflowDefinition': def,
- 'defaultProcessName': xhr.response.name + ' ' + (new Date().toISOString()),
- 'defaultProcessDescription': xhr.response.description,
- inputs
- });
- });
- }
-
- renderInput(inputSpec) {
- const { app } = this.props;
-
- const { isFile, isDirectory, isArray } = inputSpecInfo(inputSpec);
-
- if (!isFile && !isDirectory)
- return (
- <div>
- <input class="form-control w-100" type="text" placeholder={ inputSpec.label }
- value={ this.state.inputs[inputSpec.id] }
- onchange={ e => (this.state.inputs[inputSpec.id] = e.target.value) }></input>
- <div class="mt-2 text-muted">{ inputSpec.doc }</div>
- </div>
- );
-
- const button = (
- <button class="btn btn-outline-primary"
- onclick={ e => {
- e.preventDefault();
- this.browseDialogRef.current.show(
- [].concat(isFile ? 'file' : []).concat(isDirectory ? 'directory' : []),
- isArray,
- v => {
- this.state.inputs[inputSpec.id] = JSON.stringify(v);
- this.setState({});
- });
- } }>
- Browse...
- </button>
- );
-
- let value = this.state.inputs[inputSpec.id];
- if (value) {
- try {
- value = jsyaml.load(value);
- } catch (_) {}
- }
-
- return (
- <div>
- <div class="input-group">
- <input class="form-control w-100" type="text" placeholder={ inputSpec.label }
- value={ this.state.inputs[inputSpec.id] }
- onchange={ e => (this.state.inputs[inputSpec.id] = e.target.value) }></input>
- <div class="input-group-append">
- { button }
- </div>
- </div>
- <div class="mt-2 text-muted">{ inputSpec.doc }</div>
- { value ?
- isArray ? (
- <ul class="mb-0">
- { value.map(path => (
- <li>
- <WBPathDisplay app={ app } path={ parseKeepRef(path) } />
- </li>
- )) }
- </ul>
- ) : (
- <WBPathDisplay app={ app } path={ parseKeepRef(value) } />
- ) : null }
- </div>
- );
- }
-
- submit() {
- // first see if all inputs are parseable
- const inputs = {};
- const errors = [];
-
- const { workflowDefinition } = this.state;
- const main = workflowDefinition['$graph'].find(a => (a.id === '#main'));
-
- for (let k in this.state.inputs) {
- try {
- let val = jsyaml.safeLoad(this.state.inputs[k]);
- val = uuidsToCwl(val);
- k = k.split('/').slice(1).join('/');
- inputs[k] = (val === undefined ? null : val);
- } catch (exc) {
- errors.push('Error parsing ' + k + ': ' + exc.message);
- }
- }
-
- if (errors.length > 0) {
- this.setState({ errors });
- return;
- }
-
- // prepare a request
- const { app, workflowUuid } = this.props;
- const { processName, processDescription,
- defaultProcessName, defaultProcessDescription,
- projectUuid } = this.state;
- const { arvHost, arvToken, currentUser } = app.state;
- const req = {
- name: processName || defaultProcessName,
- description: processDescription || defaultProcessDescription,
- owner_uuid: projectUuid || currentUser.uuid,
- container_image: 'arvados/jobs',
- properties: {
- template_uuid: workflowUuid
- },
- runtime_constraints: {
- API: true,
- vcpus: 1,
- ram: 1073741824
- },
- cwd: '/var/spool/cwl',
- command: [
- 'arvados-cwl-runner',
- '--local',
- '--api=containers',
- '--project-uuid=' + (projectUuid || currentUser.uuid),
- '--collection-cache-size=256',
- '/var/lib/cwl/workflow.json#main',
- '/var/lib/cwl/cwl.input.json'],
- output_path: '/var/spool/cwl',
- priority: 1,
- state: 'Committed',
- mounts: {
- 'stdout': {
- kind: 'file',
- path: '/var/spool/cwl/cwl.output.json'
- },
- '/var/spool/cwl': {
- kind: 'collection',
- writable: true
- },
- '/var/lib/cwl/workflow.json': {
- kind: 'json',
- content: workflowDefinition
- },
- '/var/lib/cwl/cwl.input.json': {
- kind: 'json',
- content: inputs
- }
- }
- };
-
- wbDisableControls();
- let prom = makeArvadosRequest(arvHost, arvToken,
- '/arvados/v1/container_requests',
- { method: 'POST', data: JSON.stringify(req) });
- prom = prom.then(xhr => {
- wbEnableControls();
- route('/process/' + xhr.response.uuid);
- });
-
- // throw Error('Not implemented');
- }
-
- render({ app, workflowUuid },
- { workflow, workflowDefinition, projectUuid, processName, processDescription,
- defaultProcessName, defaultProcessDescription, errors }) {
-
- return (
- <div>
- <WBNavbarCommon app={ app } />
-
- <WBBrowseDialog app={ app } ref={ this.browseDialogRef } />
-
- { workflow ?
- (<form class="container-fluid">
- <h1>Launch Workflow</h1>
-
- <div class="form-group">
- <label>Workflow</label>
- <WBArvadosCrumbs app={ app } uuid={ workflowUuid } />
- </div>
-
- <div class="form-group">
- <label for="projectUuid">Project UUID</label>
-
- <div class="input-group mb-3">
- <input type="text" class="form-control" id="projectUuid"
- placeholder="Enter Project UUID" aria-label="Project UUID"
- aria-describedby="button-addon2" value={ projectUuid }
- onChange={ linkState(this, 'projectUuid') } />
- <div class="input-group-append">
- <button class="btn btn-primary" type="button"
- id="button-addon2" onclick={ e => { e.preventDefault();
- this.browseDialogRef.current.show('owner', false,
- projectUuid => this.setState({ projectUuid })); } }>Browse</button>
- </div>
- </div>
-
- { projectUuid ? (
- <WBArvadosCrumbs app={ app } uuid={ projectUuid } />
- ) : null }
- </div>
-
- <div class="form-group">
- <label for="processName">Process Name</label>
- <input type="text" class="form-control" id="processName"
- placeholder={ defaultProcessName } value={ processName }
- onChange={ linkState(this, 'processName') }/>
- </div>
-
- <div class="form-group">
- <label for="processDescription">Process Description</label>
- <input type="text" class="form-control" id="processDescription"
- placeholder={ defaultProcessDescription } value={ processDescription }
- onChange={ linkState(this, 'processDescription') } />
- </div>
-
- <div class="form-group">
- <label for="inputs">Inputs</label>
- <WBTable columns={ [ 'Name', 'Value'] }
- rows={ workflowDefinition.$graph.find(a => (a.id === '#main')).inputs.map(it => [
- it.label || it.id,
- this.renderInput(it)
- ]) } />
- </div>
-
- <div class="form-group">
- <button class="btn btn-success" onclick={ e => { e.preventDefault(); this.submit(); } }>
- Submit
- </button>
- </div>
-
- { errors.length > 0 ? (
- <div class="form-group">
- { errors.map(err => (
- <div class="alert alert-danger" role="alert">
- { err }
- </div>
- ))}
- </div>
- ) : null }
- </form>) : <div>Loading...</div> }
- </div>
- );
- }
- }
-
- export default WBLaunchWorkflowPage;
|