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.

360 lines
11KB

  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. function parseDefinition(text) {
  12. let definition;
  13. try {
  14. definition = JSON.parse(text);
  15. } catch (_) {
  16. definition = jsyaml.load(text);
  17. }
  18. return definition;
  19. }
  20. function encodeURIComponentIncludingDots(s) {
  21. return encodeURIComponent(s).replace('.', '%2E');
  22. }
  23. function inputSpecInfo(inputSpec) {
  24. const isFile = (inputSpec.type === 'File' || inputSpec.type === 'File[]' ||
  25. (inputSpec.type.type === 'array' && inputSpec.type.items === 'File'));
  26. const isDirectory = (inputSpec.type === 'Directory' || inputSpec.type === 'Directory[]' ||
  27. (inputSpec.type.type === 'array' && inputSpec.type.items === 'Directory'));
  28. const isArray = (inputSpec.type === 'File[]' || inputSpec.type === 'Directory[]' ||
  29. inputSpec.type.type === 'array');
  30. return { isFile, isDirectory, isArray };
  31. }
  32. function uuidsToCwl(obj, isFile) {
  33. if (obj instanceof Array) {
  34. const res = {};
  35. for (let k in obj) {
  36. res[k] = uuidsToCwl(obj[k], isFile);
  37. }
  38. return res;
  39. }
  40. if (typeof(obj) === 'string' &&
  41. (/^[0-9a-z]{5}-[0-9a-z]{5}-[0-9a-z]{15}/.exec(obj) ||
  42. /^[0-9a-f]{32}\+[0-9]+/.exec(obj))) {
  43. return {
  44. 'class': (isFile ? 'File' : 'Directory'),
  45. 'location': 'keep:' + obj
  46. };
  47. }
  48. throw Error('Expected Arvados path or array of paths');
  49. }
  50. class WBPathDisplay extends Component {
  51. fetchData() {
  52. const { app, path } = this.props;
  53. const { arvHost, arvToken } = app.state;
  54. let m;
  55. if (m = /^[0-9a-f]{32}\+[0-9]+/.exec(path));
  56. else if (m = /^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/.exec(path));
  57. else return;
  58. let prom = makeArvadosRequest(arvHost, arvToken,
  59. '/arvados/v1/collections/' + m[0]);
  60. prom = prom.then(xhr => this.setState({
  61. item: xhr.response,
  62. tail: path.substr(m[0].length)
  63. }));
  64. }
  65. componentDidMount() {
  66. this.fetchData();
  67. }
  68. componentWillReceiveProps(nextProps) {
  69. this.props = nextProps;
  70. this.fetchData();
  71. }
  72. render({}, { item, tail }) {
  73. if (!item)
  74. return 'Loading...';
  75. return (
  76. <span>
  77. <a href={ '/collection-browse/' + item.uuid }>
  78. { item.name || item.uuid }
  79. </a><a href={ '/collection-browse/' + item.uuid + '/' + encodeURIComponentIncludingDots(tail) }>
  80. { tail }
  81. </a>
  82. </span>
  83. );
  84. }
  85. }
  86. class WBLaunchWorkflowPage extends Component {
  87. constructor(...args) {
  88. super(...args);
  89. this.browseDialogRef = createRef();
  90. this.state.inputs = {};
  91. this.state.errors = [];
  92. }
  93. componentDidMount() {
  94. let { app, workflowUuid } = this.props;
  95. let { arvHost, arvToken } = app.state;
  96. let prom = makeArvadosRequest(arvHost, arvToken,
  97. '/arvados/v1/workflows/' + workflowUuid);
  98. prom = prom.then(xhr => this.setState({
  99. 'workflow': xhr.response,
  100. 'workflowDefinition': parseDefinition(xhr.response.definition),
  101. 'defaultProcessName': xhr.response.name + ' ' + (new Date().toISOString()),
  102. 'defaultProcessDescription': xhr.response.description
  103. }));
  104. }
  105. renderInput(inputSpec) {
  106. const { app } = this.props;
  107. const { isFile, isDirectory, isArray } = inputSpecInfo(inputSpec);
  108. if (!isFile && !isDirectory)
  109. return (
  110. <div>
  111. <input class="form-control w-100" type="text" placeholder={ inputSpec.label }
  112. value={ this.state.inputs[inputSpec.id] }
  113. onchange={ e => (this.state.inputs[inputSpec.id] = e.target.value) }></input>
  114. <div class="mt-2 text-muted">{ inputSpec.doc }</div>
  115. </div>
  116. );
  117. const button = (
  118. <button class="btn btn-outline-primary"
  119. onclick={ e => {
  120. e.preventDefault();
  121. this.browseDialogRef.current.show(isFile ? 'file' : 'directory', isArray,
  122. v => {
  123. this.state.inputs[inputSpec.id] = JSON.stringify(v);
  124. this.setState({});
  125. });
  126. } }>
  127. Browse...
  128. </button>
  129. );
  130. let value = this.state.inputs[inputSpec.id];
  131. if (value) {
  132. try {
  133. value = jsyaml.load(value);
  134. } catch (_) {}
  135. }
  136. return (
  137. <div>
  138. <div class="input-group">
  139. <input class="form-control w-100" type="text" placeholder={ inputSpec.label }
  140. value={ this.state.inputs[inputSpec.id] }
  141. onchange={ e => (this.state.inputs[inputSpec.id] = e.target.value) }></input>
  142. <div class="input-group-append">
  143. { button }
  144. </div>
  145. </div>
  146. <div class="mt-2 text-muted">{ inputSpec.doc }</div>
  147. { value ?
  148. isArray ? (
  149. <ul class="mb-0">
  150. { value.map(path => (
  151. <li>
  152. <WBPathDisplay app={ app } path={ path } />
  153. </li>
  154. )) }
  155. </ul>
  156. ) : (
  157. <WBPathDisplay app={ app } path={ value } />
  158. ) : null }
  159. </div>
  160. );
  161. }
  162. submit() {
  163. // first see if all inputs are parseable
  164. const inputs = {};
  165. const errors = [];
  166. const { workflowDefinition } = this.state;
  167. const main = workflowDefinition['$graph'].find(a => (a.id === '#main'));
  168. for (let k in this.state.inputs) {
  169. try {
  170. let val = jsyaml.safeLoad(this.state.inputs[k]);
  171. const { isFile } = inputSpecInfo(main.inputs.find(a => (a.id === k)));
  172. val = uuidsToCwl(val, isFile);
  173. k = k.split('/').slice(1).join('/');
  174. inputs[k] = val;
  175. } catch (exc) {
  176. errors.push('Error parsing ' + k + ': ' + exc.message);
  177. }
  178. }
  179. if (errors.length > 0) {
  180. this.setState({ errors });
  181. return;
  182. }
  183. // prepare a request
  184. const { app, workflowUuid } = this.props;
  185. const { processName, processDescription,
  186. defaultProcessName, defaultProcessDescription,
  187. projectUuid } = this.state;
  188. const { arvHost, arvToken, currentUser } = app.state;
  189. const req = {
  190. name: processName || defaultProcessName,
  191. description: processDescription || defaultProcessDescription,
  192. owner_uuid: projectUuid || currentUser.uuid,
  193. container_image: 'arvados/jobs',
  194. properties: {
  195. template_uuid: workflowUuid
  196. },
  197. runtime_constraints: {
  198. API: true,
  199. vcpus: 1,
  200. ram: 1073741824
  201. },
  202. cwd: '/var/spool/cwl',
  203. command: [
  204. 'arvados-cwl-runner',
  205. '--local',
  206. '--api=containers',
  207. '--project-uuid=' + (projectUuid || currentUser.uuid),
  208. '--collection-cache-size=256',
  209. '/var/lib/cwl/workflow.json#main',
  210. '/var/lib/cwl/cwl.input.json'],
  211. output_path: '/var/spool/cwl',
  212. priority: 1,
  213. state: 'Committed',
  214. mounts: {
  215. 'stdout': {
  216. kind: 'file',
  217. path: '/var/spool/cwl/cwl.output.json'
  218. },
  219. '/var/spool/cwl': {
  220. kind: 'collection',
  221. writable: true
  222. },
  223. '/var/lib/cwl/workflow.json': {
  224. kind: 'json',
  225. content: workflowDefinition
  226. },
  227. '/var/lib/cwl/cwl.input.json': {
  228. kind: 'json',
  229. content: inputs
  230. }
  231. }
  232. };
  233. wbDisableControls();
  234. let prom = makeArvadosRequest(arvHost, arvToken,
  235. '/arvados/v1/container_requests',
  236. { method: 'POST', data: JSON.stringify(req) });
  237. prom = prom.then(xhr => {
  238. wbEnableControls();
  239. route('/process/' + xhr.response.uuid);
  240. });
  241. // throw Error('Not implemented');
  242. }
  243. render({ app, workflowUuid },
  244. { workflow, workflowDefinition, projectUuid, processName, processDescription,
  245. defaultProcessName, defaultProcessDescription, errors }) {
  246. return (
  247. <div>
  248. <WBNavbarCommon app={ app } />
  249. <WBBrowseDialog app={ app } ref={ this.browseDialogRef } />
  250. { workflow ?
  251. (<form class="container-fluid">
  252. <h1>Launch Workflow</h1>
  253. <div class="form-group">
  254. <label>Workflow</label>
  255. <WBArvadosCrumbs app={ app } uuid={ workflowUuid } />
  256. </div>
  257. <div class="form-group">
  258. <label for="projectUuid">Project UUID</label>
  259. <div class="input-group mb-3">
  260. <input type="text" class="form-control" id="projectUuid"
  261. placeholder="Enter Project UUID" aria-label="Project UUID"
  262. aria-describedby="button-addon2" value={ projectUuid }
  263. onChange={ linkState(this, 'projectUuid') } />
  264. <div class="input-group-append">
  265. <button class="btn btn-primary" type="button"
  266. id="button-addon2" onclick={ e => { e.preventDefault();
  267. this.browseDialogRef.current.show('owner', false,
  268. projectUuid => this.setState({ projectUuid })); } }>Browse</button>
  269. </div>
  270. </div>
  271. { projectUuid ? (
  272. <WBArvadosCrumbs app={ app } uuid={ projectUuid } />
  273. ) : null }
  274. </div>
  275. <div class="form-group">
  276. <label for="processName">Process Name</label>
  277. <input type="text" class="form-control" id="processName"
  278. placeholder={ defaultProcessName } value={ processName }
  279. onChange={ linkState(this, 'processName') }/>
  280. </div>
  281. <div class="form-group">
  282. <label for="processDescription">Process Description</label>
  283. <input type="text" class="form-control" id="processDescription"
  284. placeholder={ defaultProcessDescription } value={ processDescription }
  285. onChange={ linkState(this, 'processDescription') } />
  286. </div>
  287. <div class="form-group">
  288. <label for="inputs">Inputs</label>
  289. <WBTable columns={ [ 'Name', 'Value'] }
  290. rows={ workflowDefinition.$graph.find(a => (a.id === '#main')).inputs.map(it => [
  291. it.label || it.id,
  292. this.renderInput(it)
  293. ]) } />
  294. </div>
  295. <div class="form-group">
  296. <button class="btn btn-success" onclick={ e => { e.preventDefault(); this.submit(); } }>
  297. Submit
  298. </button>
  299. </div>
  300. { errors.length > 0 ? (
  301. <div class="form-group">
  302. { errors.map(err => (
  303. <div class="alert alert-danger" role="alert">
  304. { err }
  305. </div>
  306. ))}
  307. </div>
  308. ) : null }
  309. </form>) : <div>Loading...</div> }
  310. </div>
  311. );
  312. }
  313. }
  314. export default WBLaunchWorkflowPage;