edit_design.mako 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. ## Licensed to Cloudera, Inc. under one
  2. ## or more contributor license agreements. See the NOTICE file
  3. ## distributed with this work for additional information
  4. ## regarding copyright ownership. Cloudera, Inc. licenses this file
  5. ## to you under the Apache License, Version 2.0 (the
  6. ## "License"); you may not use this file except in compliance
  7. ## with the License. You may obtain a copy of the License at
  8. ##
  9. ## http://www.apache.org/licenses/LICENSE-2.0
  10. ##
  11. ## Unless required by applicable law or agreed to in writing, software
  12. ## distributed under the License is distributed on an "AS IS" BASIS,
  13. ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. ## See the License for the specific language governing permissions and
  15. ## limitations under the License.
  16. <%!
  17. import urllib
  18. from desktop.views import commonheader, commonfooter
  19. from desktop.lib.django_util import extract_field_data
  20. from django.utils.translation import ugettext as _
  21. %>
  22. <%namespace name="layout" file="layout.mako" />
  23. ${commonheader(_('Job Designer'), "jobsub", "100px")}
  24. ${layout.menubar(section='designs')}
  25. <link rel="stylesheet" href="/static/ext/css/jquery-ui-autocomplete-1.8.18.css" type="text/css" media="screen" title="no title" charset="utf-8" />
  26. <script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
  27. <script src="/static/ext/js/jquery/plugins/jquery-ui-autocomplete-1.8.18.min.js" type="text/javascript" charset="utf-8"></script>
  28. <%def name="render_field(field)">
  29. %if not field.is_hidden:
  30. <% group_class = len(field.errors) and "error" or "" %>
  31. <div class="control-group ${group_class}">
  32. <label class="control-label">${field.label | n}</label>
  33. <div class="controls">
  34. <input name="${field.html_name | n}"
  35. class="span5 ${field.field.widget.attrs.get('class', '')}"
  36. value="${extract_field_data(field) or ''}" />
  37. % if len(field.errors):
  38. <span class="help-inline">${unicode(field.errors) | n}</span>
  39. % endif
  40. </div>
  41. </div>
  42. %endif
  43. </%def>
  44. <div class="container-fluid">
  45. <h1>${_('Job Design (%(type)s type)') % dict(type=action_type)}</h1>
  46. <form class="form-horizontal" id="workflowForm" action="${urllib.quote(action)}" method="POST">
  47. <fieldset>
  48. % for field in form.wf:
  49. ${render_field(field)}
  50. % endfor
  51. <hr/>
  52. <div class="control-group">
  53. <p class="alert alert-info">
  54. ${_('You can parameterize the values, using')} <code>$myVar</code> ${_('or')}
  55. <code>${"${"}myVar}</code> .
  56. ${_('When the design is submitted, you will be prompted for the actual value of ')}<code>myVar</code> .
  57. </p>
  58. </div>
  59. % for field in form.action:
  60. ${render_field(field)}
  61. % endfor
  62. <div class="control-group">
  63. <label class="control-label">${_('Job Properties')}</label>
  64. <div class="controls">
  65. ## Data bind for job properties
  66. <table class="table-condensed designTable" data-bind="visible: properties().length > 0">
  67. <thead>
  68. <tr>
  69. <th>${_('Property name')}</th>
  70. <th>${_('Value')}</th>
  71. <th />
  72. </tr>
  73. </thead>
  74. <tbody data-bind="foreach: properties">
  75. <tr>
  76. <td><input class="span3 required propKey" data-bind="value: name, uniqueName: false" /></td>
  77. <td><input class="span4 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" /></td>
  78. <td><a class="btn btn-small" href="#" data-bind="click: $root.removeProp">${_('Delete')}</a></td>
  79. </tr>
  80. </tbody>
  81. </table>
  82. % if len(form.action["job_properties"].errors):
  83. <div class="row">
  84. <div class="alert alert-error">
  85. ${unicode(form.action["job_properties"].errors) | n}
  86. </div>
  87. </div>
  88. % endif
  89. <button class="btn" data-bind="click: addProp">${_('Add Property')}</button>
  90. </div>
  91. </div>
  92. <div class="control-group">
  93. <label class="control-label">${_('Files')}</label>
  94. <div class="controls">
  95. ## Data bind for files (distributed cache)
  96. <table class="table-condensed designTable" data-bind="visible: files().length > 0">
  97. <tbody data-bind="foreach: files">
  98. <tr>
  99. <td><input class="input span5 required pathChooserKo"
  100. data-bind="fileChooser: $data, value: name, uniqueName: false" /></td>
  101. <td><a class="btn" href="#" data-bind="click: $root.removeFile">${_('Delete')}</a></td>
  102. </tr>
  103. </tbody>
  104. </table>
  105. % if len(form.action["files"].errors):
  106. <div class="alert alert-error">
  107. ${unicode(form.action["files"].errors) | n}
  108. </div>
  109. % endif
  110. <button class="btn" data-bind="click: addFile">${_('Add File')}</button>
  111. </div>
  112. </div>
  113. <div class="control-group">
  114. <label class="control-label">${_('Archives')}</label>
  115. <div class="controls">
  116. ## Data bind for archives (distributed cache)
  117. <table class="table-condensed designTable" data-bind="visible: archives().length > 0">
  118. <tbody data-bind="foreach: archives">
  119. <tr>
  120. <td><input class="input span5 required pathChooserKo"
  121. data-bind="fileChooser: $data, value: name, uniqueName: false" /></td>
  122. <td><a class="btn" href="#" data-bind="click: $root.removeArchive">${_('Delete')}</a></td>
  123. </tr>
  124. </tbody>
  125. </table>
  126. % if len(form.action["archives"].errors):
  127. <div class="alert alert-error">
  128. ${unicode(form.action["archives"].errors) | n}
  129. </div>
  130. % endif
  131. <button class="btn" data-bind="click: addArchive">${_('Add Archive')}</button>
  132. </div>
  133. </div>
  134. </fieldset>
  135. ## Submit
  136. <div class="form-actions">
  137. <button data-bind="click: submit" class="btn btn-primary">${_('Save')}</button>
  138. <a href="/jobsub" class="btn">${_('Cancel')}</a>
  139. </div>
  140. </form>
  141. </div>
  142. <div id="chooseFile" class="modal hide fade">
  143. <div class="modal-header">
  144. <a href="#" class="close" data-dismiss="modal">&times;</a>
  145. <h3>${_('Choose a file')}</h3>
  146. </div>
  147. <div class="modal-body">
  148. <div id="fileChooserModal">
  149. </div>
  150. </div>
  151. <div class="modal-footer">
  152. </div>
  153. </div>
  154. <style>
  155. .pathChooser, .pathChooserKo {
  156. border-radius: 3px 0 0 3px;
  157. border-right:0;
  158. }
  159. .fileChooserBtn {
  160. border-radius: 0 3px 3px 0;
  161. }
  162. #fileChooserModal {
  163. padding:14px;
  164. height:270px;
  165. }
  166. #fileChooserModal > ul.unstyled {
  167. height:180px;
  168. overflow-y:auto;
  169. }
  170. .designTable {
  171. margin-left:0;
  172. }
  173. .designTable th, .designTable td {
  174. padding-left: 0;
  175. }
  176. .designTable th {
  177. text-align:left;
  178. }
  179. </style>
  180. <script type="text/javascript" charset="utf-8">
  181. $(document).ready(function(){
  182. var propertiesHint = ${properties_hint};
  183. // The files and archives are dictionaries in the model, because we
  184. // can add and remove it the same way we add/remove properties.
  185. // But the server expects them to be arrays. So we transform the
  186. // two representations back and forth.
  187. var arrayToDictArray = function(arr) {
  188. var res = [ ];
  189. for (var i in arr) {
  190. res.push( { name: arr[i], dummy: "" } );
  191. }
  192. return res;
  193. };
  194. var dictArrayToArray = function(dictArray) {
  195. var res = [ ];
  196. for (var i in dictArray) {
  197. res.push(dictArray[i]["name"]);
  198. }
  199. return res;
  200. };
  201. // Handles adding autocomplete to job properties.
  202. // We need to propagate the selected value to knockoutjs.
  203. var addAutoComplete = function(i, elem) {
  204. $(elem).autocomplete({
  205. source: propertiesHint,
  206. select: function(event, ui) {
  207. var context = ko.contextFor(this);
  208. context.$data.name = ui.item.value;
  209. }
  210. });
  211. };
  212. var ViewModel = function(properties, files, archives) {
  213. var self = this;
  214. self.properties = ko.observableArray(properties);
  215. self.files = ko.observableArray(files);
  216. self.archives = ko.observableArray(archives);
  217. self.myVar = ko.observable();
  218. self.addProp = function() {
  219. self.properties.push({ name: "", value: "" });
  220. $(".propKey:last").each(addAutoComplete);
  221. };
  222. self.removeProp = function(val) {
  223. self.properties.remove(val);
  224. };
  225. self.addFile = function() {
  226. self.files.push({ name: "", dummy: "" });
  227. };
  228. self.removeFile = function(val) {
  229. self.files.remove(val);
  230. };
  231. self.addArchive = function() {
  232. self.archives.push({ name: "", dummy: "" });
  233. };
  234. self.removeArchive = function(val) {
  235. self.archives.remove(val);
  236. };
  237. self.submit = function(form) {
  238. var form = $("#workflowForm");
  239. var files_arr = dictArrayToArray(ko.toJS(self.files));
  240. var archives_arr = dictArrayToArray(ko.toJS(self.archives));
  241. $("<input>").attr("type", "hidden")
  242. .attr("name", "action-job_properties")
  243. .attr("value", ko.utils.stringifyJson(self.properties))
  244. .appendTo(form);
  245. $("<input>").attr("type", "hidden")
  246. .attr("name", "action-files")
  247. .attr("value", JSON.stringify(files_arr))
  248. .appendTo(form);
  249. $("<input>").attr("type", "hidden")
  250. .attr("name", "action-archives")
  251. .attr("value", JSON.stringify(archives_arr))
  252. .appendTo(form);
  253. form.submit();
  254. };
  255. };
  256. var viewModel = new ViewModel(${properties},
  257. arrayToDictArray(${files}),
  258. arrayToDictArray(${archives}));
  259. ko.bindingHandlers.fileChooser = {
  260. init: function(element, valueAccessor, allBindings, model) {
  261. var self = $(element);
  262. self.after(getFileBrowseButton(self));
  263. }
  264. };
  265. ko.applyBindings(viewModel);
  266. $(".pathChooser").each(function(){
  267. var self = $(this);
  268. self.after(getFileBrowseButton(self));
  269. });
  270. function getFileBrowseButton(inputElement) {
  271. return $("<button>").addClass("btn").addClass("fileChooserBtn").text("..").click(function(e){
  272. e.preventDefault();
  273. $("#fileChooserModal").jHueFileChooser({
  274. initialPath: inputElement.val(),
  275. onFileChoose: function(filePath) {
  276. inputElement.val(filePath);
  277. $("#chooseFile").modal("hide");
  278. },
  279. createFolder: false
  280. });
  281. $("#chooseFile").modal("show");
  282. })
  283. }
  284. $(".propKey").each(addAutoComplete);
  285. });
  286. </script>
  287. ${commonfooter(messages)}