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!
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

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