IF YOU WOULD LIKE TO GET AN ACCOUNT, please write an email to s dot adaszewski at gmail dot com. User accounts are meant only to report issues and/or generate pull requests. This is a purpose-specific Git hosting for ADARED projects. Thank you for your understanding!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 lines
9.4KB

  1. import { h, Component, createRef } from 'preact';
  2. import { route } from 'preact-router';
  3. import WBNavbarCommon from 'wb-navbar-common';
  4. import WBArvadosCrumbs from 'wb-arvados-crumbs';
  5. import WBBrowseDialog from 'wb-browse-dialog';
  6. import WBTable from 'wb-table';
  7. import WBNameAndUuid from 'wb-name-and-uuid';
  8. import makeArvadosRequest from 'make-arvados-request';
  9. import { wbDisableControls, wbEnableControls } from 'wb-disable-controls';
  10. import linkState from 'linkstate';
  11. import wbParseWorkflowDef from 'wb-parse-workflow-def';
  12. import wbInputSpecInfo from 'wb-input-spec-info';
  13. import wbUuidsToCwl from 'wb-uuids-to-cwl';
  14. import { encodeURIComponentIncludingDots, parseKeepRef } from 'wb-process-misc';
  15. import WBPathDisplay from 'wb-path-display';
  16. class WBLaunchWorkflowPage extends Component {
  17. constructor(...args) {
  18. super(...args);
  19. this.browseDialogRef = createRef();
  20. this.state.inputs = {};
  21. this.state.errors = [];
  22. }
  23. componentDidMount() {
  24. let { app, workflowUuid } = this.props;
  25. let { arvHost, arvToken } = app.state;
  26. let prom = makeArvadosRequest(arvHost, arvToken,
  27. '/arvados/v1/workflows/' + workflowUuid);
  28. prom = prom.then(xhr => {
  29. const def = wbParseWorkflowDef(xhr.response.definition);
  30. const inputs = {};
  31. const main = def['$graph'].find(a => (a.id === '#main'));
  32. main.inputs.map(a => (inputs[a.id] = JSON.stringify(a.default)));
  33. this.setState({
  34. 'workflow': xhr.response,
  35. 'workflowDefinition': def,
  36. 'defaultProcessName': xhr.response.name + ' ' + (new Date().toISOString()),
  37. 'defaultProcessDescription': xhr.response.description,
  38. inputs
  39. });
  40. });
  41. }
  42. renderInput(inputSpec) {
  43. const { app } = this.props;
  44. const { isFile, isDirectory, isArray } = wbInputSpecInfo(inputSpec);
  45. if (!isFile && !isDirectory)
  46. return (
  47. <div>
  48. <input class="form-control w-100" type="text" placeholder={ inputSpec.label }
  49. value={ this.state.inputs[inputSpec.id] }
  50. onchange={ e => (this.state.inputs[inputSpec.id] = e.target.value) }></input>
  51. <div class="mt-2 text-muted">{ inputSpec.doc }</div>
  52. </div>
  53. );
  54. const button = (
  55. <button class="btn btn-outline-primary"
  56. onclick={ e => {
  57. e.preventDefault();
  58. this.browseDialogRef.current.show(
  59. [].concat(isFile ? 'file' : []).concat(isDirectory ? 'directory' : []),
  60. isArray,
  61. v => {
  62. this.state.inputs[inputSpec.id] = JSON.stringify(v);
  63. this.setState({});
  64. });
  65. } }>
  66. Browse...
  67. </button>
  68. );
  69. let value = this.state.inputs[inputSpec.id];
  70. if (value) {
  71. try {
  72. value = jsyaml.load(value);
  73. } catch (_) {}
  74. }
  75. return (
  76. <div>
  77. <div class="input-group">
  78. <input class="form-control w-100" type="text" placeholder={ inputSpec.label }
  79. value={ this.state.inputs[inputSpec.id] }
  80. onchange={ e => (this.state.inputs[inputSpec.id] = e.target.value) }></input>
  81. <div class="input-group-append">
  82. { button }
  83. </div>
  84. </div>
  85. <div class="mt-2 text-muted">{ inputSpec.doc }</div>
  86. { value ?
  87. isArray ? (
  88. <ul class="mb-0">
  89. { value.map(path => (
  90. <li>
  91. <WBPathDisplay app={ app } path={ parseKeepRef(path) } />
  92. </li>
  93. )) }
  94. </ul>
  95. ) : (
  96. <WBPathDisplay app={ app } path={ parseKeepRef(value) } />
  97. ) : null }
  98. </div>
  99. );
  100. }
  101. submit() {
  102. // first see if all inputs are parseable
  103. const inputs = {};
  104. const errors = [];
  105. const { workflowDefinition } = this.state;
  106. const main = workflowDefinition['$graph'].find(a => (a.id === '#main'));
  107. for (let k in this.state.inputs) {
  108. try {
  109. let val = jsyaml.safeLoad(this.state.inputs[k]);
  110. val = wbUuidsToCwl(val);
  111. k = k.split('/').slice(1).join('/');
  112. inputs[k] = (val === undefined ? null : val);
  113. } catch (exc) {
  114. errors.push('Error parsing ' + k + ': ' + exc.message);
  115. }
  116. }
  117. if (errors.length > 0) {
  118. this.setState({ errors });
  119. return;
  120. }
  121. // prepare a request
  122. const { app, workflowUuid } = this.props;
  123. const { processName, processDescription,
  124. defaultProcessName, defaultProcessDescription,
  125. projectUuid } = this.state;
  126. const { arvHost, arvToken, currentUser } = app.state;
  127. const req = {
  128. name: processName || defaultProcessName,
  129. description: processDescription || defaultProcessDescription,
  130. owner_uuid: projectUuid || currentUser.uuid,
  131. container_image: 'arvados/jobs',
  132. properties: {
  133. template_uuid: workflowUuid
  134. },
  135. runtime_constraints: {
  136. API: true,
  137. vcpus: 1,
  138. ram: 1073741824
  139. },
  140. cwd: '/var/spool/cwl',
  141. command: [
  142. 'arvados-cwl-runner',
  143. '--local',
  144. '--api=containers',
  145. '--project-uuid=' + (projectUuid || currentUser.uuid),
  146. '--collection-cache-size=256',
  147. '/var/lib/cwl/workflow.json#main',
  148. '/var/lib/cwl/cwl.input.json'],
  149. output_path: '/var/spool/cwl',
  150. priority: 1,
  151. state: 'Committed',
  152. mounts: {
  153. 'stdout': {
  154. kind: 'file',
  155. path: '/var/spool/cwl/cwl.output.json'
  156. },
  157. '/var/spool/cwl': {
  158. kind: 'collection',
  159. writable: true
  160. },
  161. '/var/lib/cwl/workflow.json': {
  162. kind: 'json',
  163. content: workflowDefinition
  164. },
  165. '/var/lib/cwl/cwl.input.json': {
  166. kind: 'json',
  167. content: inputs
  168. }
  169. }
  170. };
  171. wbDisableControls();
  172. let prom = makeArvadosRequest(arvHost, arvToken,
  173. '/arvados/v1/container_requests',
  174. { method: 'POST', data: JSON.stringify(req) });
  175. prom = prom.then(xhr => {
  176. wbEnableControls();
  177. route('/process/' + xhr.response.uuid);
  178. });
  179. // throw Error('Not implemented');
  180. }
  181. render({ app, workflowUuid },
  182. { workflow, workflowDefinition, projectUuid, processName, processDescription,
  183. defaultProcessName, defaultProcessDescription, errors }) {
  184. return (
  185. <div>
  186. <WBNavbarCommon app={ app } />
  187. <WBBrowseDialog app={ app } ref={ this.browseDialogRef } />
  188. { workflow ?
  189. (<form class="container-fluid">
  190. <h1>Launch Workflow</h1>
  191. <div class="form-group">
  192. <label>Workflow</label>
  193. <WBArvadosCrumbs app={ app } uuid={ workflowUuid } />
  194. </div>
  195. <div class="form-group">
  196. <label for="projectUuid">Project UUID</label>
  197. <div class="input-group mb-3">
  198. <input type="text" class="form-control" id="projectUuid"
  199. placeholder="Enter Project UUID" aria-label="Project UUID"
  200. aria-describedby="button-addon2" value={ projectUuid }
  201. onChange={ linkState(this, 'projectUuid') } />
  202. <div class="input-group-append">
  203. <button class="btn btn-primary" type="button"
  204. id="button-addon2" onclick={ e => { e.preventDefault();
  205. this.browseDialogRef.current.show('owner', false,
  206. projectUuid => this.setState({ projectUuid })); } }>Browse</button>
  207. </div>
  208. </div>
  209. { projectUuid ? (
  210. <WBArvadosCrumbs app={ app } uuid={ projectUuid } />
  211. ) : null }
  212. </div>
  213. <div class="form-group">
  214. <label for="processName">Process Name</label>
  215. <input type="text" class="form-control" id="processName"
  216. placeholder={ defaultProcessName } value={ processName }
  217. onChange={ linkState(this, 'processName') }/>
  218. </div>
  219. <div class="form-group">
  220. <label for="processDescription">Process Description</label>
  221. <input type="text" class="form-control" id="processDescription"
  222. placeholder={ defaultProcessDescription } value={ processDescription }
  223. onChange={ linkState(this, 'processDescription') } />
  224. </div>
  225. <div class="form-group">
  226. <label for="inputs">Inputs</label>
  227. <WBTable columns={ [ 'Name', 'Value'] }
  228. rows={ workflowDefinition.$graph.find(a => (a.id === '#main')).inputs.map(it => [
  229. it.label || it.id,
  230. this.renderInput(it)
  231. ]) } />
  232. </div>
  233. <div class="form-group">
  234. <button class="btn btn-success" onclick={ e => { e.preventDefault(); this.submit(); } }>
  235. Submit
  236. </button>
  237. </div>
  238. { errors.length > 0 ? (
  239. <div class="form-group">
  240. { errors.map(err => (
  241. <div class="alert alert-danger" role="alert">
  242. { err }
  243. </div>
  244. ))}
  245. </div>
  246. ) : null }
  247. </form>) : <div>Loading...</div> }
  248. </div>
  249. );
  250. }
  251. }
  252. export default WBLaunchWorkflowPage;