edit_design.mako 12 KB

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