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.

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