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.

383 lines
12KB

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