Hue.Beeswax.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  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. ---
  18. description: Beeswax (the Hive UI)
  19. provides: [Hue.Beeswax]
  20. requires: [JFrame/JFrame.Browser, clientcide/TabSwapper, More/Form.Validator.Inline, hue-shared/DynamicTextarea, hue-shared/Hue.JFrame.Chooser]
  21. script: Hue.Beeswax.js
  22. ...
  23. */
  24. ART.Sheet.define('window.beeswax.browser', {
  25. 'min-width': 740
  26. });
  27. ART.Sheet.define('splitview.bw-editor', {
  28. 'right-background-color': '#333'
  29. });
  30. (function(){
  31. Hue.Beeswax = new Class({
  32. Extends: Hue.JBrowser,
  33. options: {
  34. displayHistory: false,
  35. className: 'art beeswax browser logo_header',
  36. height: 350,
  37. jframeOptions: {
  38. clickRelays: 'a, .relays'
  39. }
  40. },
  41. initialize: function(path, options){
  42. this.parent(path || '/beeswax/', options);
  43. this.jframe.addShortcuts({
  44. 'Open Selected': {
  45. keys: 'enter',
  46. shortcut: 'enter',
  47. handler: function(e){
  48. var table = $(this).getElement('[data-filters*=HtmlTable]');
  49. if (!table) return;
  50. hTable = table.retrieve('HtmlTable');
  51. var selected = hTable.selectedRows[0];
  52. if (!selected) return;
  53. Hue.JFrame.doubleClickHandler(this.jframe, e, selected);
  54. }.bind(this),
  55. description: 'Open the selected item.'
  56. }
  57. });
  58. this.jframe.addBehaviors({
  59. // breadcrumb pattern; has to set z-index programatically for layout
  60. 'Breadcrumb': function(element, methods) {
  61. var items = element.getElements('li');
  62. items.reverse().each(function(item, i){
  63. item.setStyle('z-index', i+1);
  64. });
  65. },
  66. //breadcrumb form; displays a breadcrumb that grows as you fill out the form
  67. 'BreadcrumbForm': function(element, methods) {
  68. /*
  69. The element is a UL with LI items for each breadcrumb; looks like this:
  70. <ul data-filters="Breadcrumb, BreadcrumbForm" class="clearfix" data-bc-sections=".hue-bc-section" data-bc-form="form">
  71. <li><a href="#step1">One</a></li>
  72. <li><a href="#step2">Two</a></li>
  73. <li><a href="#step3">Ect</a></li>
  74. </ul>
  75. it has a data-bc-sections definition which is a selector to select the sections that correlate to the bc links
  76. it has a data-bc-form selector that correlates to the form for this bc form.
  77. */
  78. container = methods.getContentElement();
  79. var items = element.getElements('li');
  80. var form = container.getElement(element.get('data', 'bc-form')) || container.getElement('form');
  81. var sections = container.getElements(element.get('data', 'bc-sections'));
  82. //extend tab swapper; we need to manage things like OverText and FormValidator when we change sections
  83. var TS = new Class({
  84. Extends: TabSwapper,
  85. showSection: function(index) {
  86. //we validate the form when the user pages forward and not on the first display when this.now is still undefined
  87. //(where this.now is the current index of the TabSwapper; it's undefined on init)
  88. var validate = index > this.now && this.now != undefined;
  89. if (!validate) {
  90. //reset the error state
  91. form.get('validator').reset();
  92. if (this.now != undefined) items[this.now].removeClass('hue-bc-error');
  93. }
  94. if (!validate || getValidator(form).validate()) {
  95. //show the section if we're valid
  96. this.parent(index);
  97. //show the overtext that might have been hidden
  98. OverText.update();
  99. }
  100. return this;
  101. }
  102. });
  103. //get the form validator and add an event for when elements are validated
  104. //if the element fails, get the nav item and breadcrumb section and add an error class for styling
  105. getValidator(form).addEvents({
  106. elementValidate: function(valid, field){
  107. var section = field.getParent(element.get('data', 'bc-sections'));
  108. var nav = items[sections.indexOf(section)];
  109. if (valid) nav.removeClass('hue-bc-error');
  110. else nav.addClass('hue-bc-error');
  111. }
  112. });
  113. //create our modified tabswapper
  114. var tabs = new TS({
  115. tabs: items,
  116. sections: sections,
  117. smooth: true
  118. });
  119. //find any sections with an element with the class .hue-multipart-next and make that
  120. //next button change tabs; note that this is the only way the user can progress forward (for now)
  121. //todo: add some keyboard love; enter, tab, arrows, etc.
  122. sections.each(function(section, i) {
  123. section.getElements('.hue-multipart-next').addEvent('click', function(e){
  124. e.stop();
  125. if (getValidator(form).validate()) tabs.show(i+1);
  126. });
  127. });
  128. }
  129. });
  130. this.jframe.addBehaviorPlugin('SelectWithOther', 'SelectWithOtherValidation', function(element, methods) {
  131. //adds validation upon hiding the 'other' field
  132. var select = element.getElement('select');
  133. select.addEvent('change', function() {
  134. if(!select.getSelected()[0].get('value') == '__other__') {
  135. getValidator(element.getParent('form')).validate();
  136. }
  137. });
  138. });
  139. this.jframe.addFilters({
  140. visible: function(container){
  141. if(!container.get('html').contains('jframe-visible')) return;
  142. container.getElements('.jframe-visible').setStyle("display", "");
  143. }
  144. });
  145. this.addEvents({
  146. load: this.setup.bind(this)
  147. });
  148. this.jframe.addEvents({
  149. beforeRenderer: this.stripColumnTemplates.bind(this)
  150. });
  151. },
  152. setup: function(view) {
  153. switch(view) {
  154. case 'table-setup':
  155. this.setupTableForm();
  156. break;
  157. case 'execute':
  158. this.setupEditor();
  159. break;
  160. case 'choose-file':
  161. this.setupChooseFile();
  162. break;
  163. case 'choose-delimiter':
  164. this.setupChooseDelimiter();
  165. break;
  166. case 'define-columns':
  167. this.setupDefineColumns();
  168. break;
  169. }
  170. },
  171. setupChooseFile: function(){
  172. var importData = $(this).getElement('.bw-import_data');
  173. var impLabel = importData.getElement('label');
  174. var impInput = importData.getElement('input');
  175. var checkImportData = function(e){
  176. if (impInput.get('checked')) {
  177. impLabel.removeClass('hue-bw-selected');
  178. impInput.setProperty('checked', 'false');
  179. } else {
  180. impLabel.addClass('hue-bw-selected');
  181. impInput.setProperty('checked');
  182. }
  183. };
  184. impLabel.addEvent('click', checkImportData);
  185. if(impInput.get('checked')) impLabel.addClass('hue-bw-selected');
  186. },
  187. setupDefineColumns: function(){
  188. var showHive = $(this).getElement('.bw-show_hive');
  189. var hiveLabel = showHive.getElement('label');
  190. var hiveInput = showHive.getElement('input');
  191. var checkShowHive = function(e){
  192. if (hiveInput.get('checked')) {
  193. hiveLabel.addClass('hue-bw-selected');
  194. hiveInput.setProperty('checked', 'false');
  195. } else {
  196. hiveLabel.removeClass('hue-bw-selected');
  197. hiveInput.setProperty('checked');
  198. }
  199. };
  200. hiveLabel.addEvent('click', checkShowHive);
  201. },
  202. setupChooseDelimiter: function(){
  203. var confirmDelim = $(this).getElement('.bw-confirm_delim');
  204. var selectDelim = $(this).getElement('.bw-select_delim');
  205. if(confirmDelim) {
  206. var yes = confirmDelim.getElement('input[name=Yes]');
  207. var no = confirmDelim.getElement('input[name=No]');
  208. confirmDelim.show();
  209. selectDelim.hide();
  210. no.addEvent('click', function(){
  211. confirmDelim.hide();
  212. selectDelim.show();
  213. });
  214. } else {
  215. selectDelim.show();
  216. }
  217. var otherInput = selectDelim.getElement('input[name=delimiter_1]');
  218. var delimiters = selectDelim.getElement('select[name=delimiter_0]');
  219. delimiters.addEvent('change', function(e){
  220. if(delimiters.getProperty('value') !== '__other__')
  221. {
  222. var form = delimiters.getParent('form');
  223. form.retrieve('form.request').setOptions({
  224. extraData:
  225. {'submit_preview': 'Preview'}
  226. }).send();
  227. }
  228. });
  229. },
  230. setupTableForm: function(){
  231. /* format inputs */
  232. var formatSection = $(this).getElement('.bw-format');
  233. //all the inputs in the format section have custom click behavior
  234. //to add a css class for styling purposes based on their checked state
  235. var checkFormSectionInput = function(input, validate) {
  236. if (input.get('checked')) {
  237. formatSection.getElements('dt').removeClass('hue-bw-selected');
  238. input.getParent('dt').addClass('hue-bw-selected');
  239. if (validate) $(this).getElement('form').validate();
  240. }
  241. }.bind(this);
  242. //when the user clicks, check the input and set up classes, validate the form, etc.
  243. formatSection.getElements('input').each(function(input) {
  244. input.addEvent('click', function(){
  245. checkFormSectionInput(input, true);
  246. });
  247. //do this on startup in case the input has a default value
  248. checkFormSectionInput(input);
  249. //if the input is checked, we have to invoke the data-group-toggle manually
  250. //otherwise the next slide shows both the delimited and serde options
  251. if (input.get('checked')) input.getParent('[data-group-toggle]').fireEvent('click');
  252. }, this);
  253. /* file formats */
  254. var fileSection = $(this).getElement('.bw-file_formats');
  255. var labels = fileSection.getElements('label');
  256. var inputFormatDetails = $(this).getElement('.bw-io_formats');
  257. //fix layout issues; django forces all choice elements to be in a ul
  258. fileSection.getElement('ul').addClass('clearfix');
  259. //private method to retrieve an overtext for an input or create one if there isn't one yet
  260. var getOT = function(input) {
  261. var ot = input.retrieve('OverText');
  262. if (!ot) ot = new OverText(input);
  263. return ot;
  264. };
  265. //get the inputs for the file section
  266. fileSection.getElements('input').each(function(input) {
  267. //whent he user chooses one, add some selected state for styling
  268. input.addEvent('click', function(){
  269. labels.removeClass('hue-bw-selected');
  270. input.getParent('label').addClass('hue-bw-selected');
  271. //if the user chooses InputFormat, show the details for that input
  272. //and configure them to be validated
  273. if (input.get('value') == 'InputFormat') {
  274. inputFormatDetails.reveal().get('reveal').chain(function(){
  275. inputFormatDetails.getElements('input').each(function(input){
  276. getOT(input).enable();
  277. input.removeClass('ignoreValidation').addClass('required');
  278. });
  279. });
  280. } else {
  281. //else hide the input format details and disable the validation
  282. inputFormatDetails.dissolve().getElements('input').each(function(input){
  283. getOT(input).disable();
  284. input.addClass('ignoreValidation');
  285. getValidator(input.getParent('form')).validate();
  286. });
  287. }
  288. });
  289. });
  290. /* file location */
  291. var fileLocation = $(this).getElement('.bw-file_location');
  292. var defLabel = fileLocation.getElement('.bw-default_location label');
  293. var fileLoc = fileLocation.getElement('.bw-external_loc');
  294. var fileLocInput = fileLoc.getElement('input').addClass('reqiured');
  295. var input = fileLocation.getElement('.bw-default_location input').set('checked', true);
  296. //private method to toggle style state, validation, and the file location input
  297. var checkFileLoc = function(e){
  298. if (input.get('checked')) {
  299. defLabel.addClass('hue-bw-selected');
  300. getOT(fileLocInput).disable();
  301. fileLocInput.addClass('ignoreValidation');
  302. fileLoc.dissolve();
  303. //only validate if this was a user-initiated event
  304. if (e) getValidator(input.getParent('form')).validate();
  305. } else {
  306. defLabel.removeClass('hue-bw-selected');
  307. fileLoc.removeClass('ignoreValidation').reveal().get('reveal').chain(function(){
  308. getOT(fileLocInput).enable();
  309. });
  310. }
  311. };
  312. //check the file state when the user changes the input value
  313. input.addEvent('click', checkFileLoc);
  314. //call it now to set it up.
  315. checkFileLoc();
  316. /* column tables */
  317. var columns = $(this).getElements('div.bw-column');
  318. var columnHeaders = $(this).getElements('.bw-columns dt.bw-column_header');
  319. var columnSections = $(this).getElements('.bw-columns dd.bw-column');
  320. //the column layout is an accordion; starts off with all closed.
  321. this.columnAccordion = new Fx.Accordion(columnHeaders, columnSections, {
  322. onActive: function(toggler, section) {
  323. toggler.addClass('bw-active').removeClass('bw-inactive');
  324. },
  325. onBackground: function(toggler, section) {
  326. toggler.removeClass('bw-active').addClass('bw-inactive');
  327. },
  328. display: -1
  329. });
  330. //create a column form instance for each column form.
  331. columns.each(function(column, i) {
  332. new ColumnForm(column, this.columnAccordion, {
  333. onRemove: this.removeColumn.bind(this)
  334. });
  335. }, this);
  336. //create buttons to add new columns
  337. ['column', 'partition'].each(function(type) {
  338. $(this).getElement('.bw-add_' + type + ' button').addEvent('click', function(e) {
  339. e.stop();
  340. this.addColumn(type, this.columnAccordion);
  341. }.bind(this));
  342. }, this);
  343. },
  344. /*
  345. finds the column templates in the jframe response and removes them before
  346. jframe sets them up like all the other forms
  347. container - (object) see JFrame's beforeRender event
  348. options - (object) ditto
  349. */
  350. stripColumnTemplates: function(container, options){
  351. this.columnTemplates = {};
  352. ['column', 'partition'].each(function(type) {
  353. var el;
  354. container.elements.some(function(section) {
  355. if (!el) el = section.getElement('.beeswax_' + type + '_form_template');
  356. return el;
  357. });
  358. if (el) {
  359. this.columnTemplates[type] = el.get('html');
  360. el.destroy();
  361. }
  362. }, this);
  363. },
  364. /*
  365. adds a new column form
  366. type - either "column" or "partition"
  367. accordion - the accordion that the column is controlled by
  368. */
  369. addColumn: function(type, accordion){
  370. //find the current count
  371. var count_input = $(this).getElement('input[name=' + type +'s-next_form_id]');
  372. //increment it and store it back into the input (as we're adding here)
  373. var count = count_input.get('value').toInt();
  374. count_input.set('value', count + 1);
  375. //create a new column from the template
  376. var column = Elements.from(this.columnTemplates[type].replace(/TEMPLATE/g, count))[0];
  377. //store some metadata about it, inject it into the list.
  378. column.store('bw:col-type', type).store('bw:col-count', count);
  379. column.hide().inject($(this).getElement('.bw-' + type + '-forms'));
  380. //apply JFrame magic
  381. this.jframe.applyFilters(column);
  382. //get the header and then add it to the accordion
  383. var columnHeader = column.getElement('dt.bw-column_header');
  384. accordion.addSection(columnHeader, column.getElement('dd.bw-column'));
  385. //expose the new section
  386. column.reveal();
  387. //get the header in that column and, now that it's visible, show its overtext
  388. var input = column.getElement('dt.bw-column_header input');
  389. var ot = input.retrieve('OverText');
  390. if (!ot) ot = new OverText(input);
  391. ot.enable();
  392. //create a new instance of ColumnForm with our new column
  393. new ColumnForm(column, accordion, {
  394. onRemove: this.removeColumn.bind(this)
  395. });
  396. //add the fields in our column to the validator for the form.
  397. getValidator(column.getParent('form')).watchFields(column.getElements('input, textarea, select'));
  398. },
  399. removeColumn: function(column){
  400. column.getElements('input, select, textarea').addClass('ignoreValidation');
  401. column.getParent('form').validate();
  402. column.dissolve();
  403. var column_id = column.retrieve('bw:col-count');
  404. new Element('input', {
  405. 'type': 'hidden',
  406. 'name': 'columns-' + (column_id == null ? "0" : column_id) + '-_deleted',
  407. 'value': 'True'
  408. }).inject(column);
  409. },
  410. setupEditor: function(){
  411. //get the editor area, textarea input, & header
  412. var editor = $(this).getElement('.bw-query_bottom');
  413. var ta = editor.getElement('textarea');
  414. //this resizer method resizes the editor area to fill the remaining space below the header
  415. //the shade event is passed true/false if the shade is engaged or not
  416. var resizer = function(){
  417. var header = $(this).getElement('.bw-query_header').getSize().y;
  418. editor.setStyle('height', this.contentSize.y - header - 2);
  419. ta.setStyle('height', this.contentSize.y - header - 2);
  420. }.bind(this);
  421. //do so at startup
  422. resizer();
  423. //and any time the window is resized
  424. this.addEvent('unshade', resizer);
  425. //remove this resize handler on unload
  426. this.jframe.markForCleanup(function(){
  427. this.removeEvent('unshade', resizer);
  428. }.bind(this));
  429. //when the user clicks the save as link, show a popup with the save as form
  430. //when we fire the click manually, it means we want to submit the form
  431. var saveAs = $(this).getElement('.bw-query_save_as');
  432. var saver = $(this).getElement('.bw-query_save_form');
  433. var saveIt = function(){
  434. //grab the container of the save as inputs and clone them
  435. //(clone them because our windows destroy themselves on hide)
  436. //Hide on clone to ensure that display property is set to none.
  437. var form = saver.clone().hide();
  438. //prompt the user w/ the form
  439. var prompt = this.prompt('Save This Query', form.show(), function(){
  440. //replace the saver form with the one the user filled out
  441. form.replaces(saver).hide();
  442. //now we're ready to submit the form
  443. saving = true;
  444. //and submit the form
  445. saveAs.click();
  446. //back to showing the popup
  447. saving = false;
  448. });
  449. }.bind(this);
  450. var saving;
  451. saveAs.addEvent('click', function(e){
  452. //if we aren't trying to submit the form, show the popup
  453. if (!saving) {
  454. //this is the callback for when the user hits the "ok" button
  455. //TODO add some keyboard love for the enter button in fields?
  456. e.stop();
  457. saveIt();
  458. }
  459. }.bind(this));
  460. if (saver.getElements('.beeswax_error li').length) saveIt();
  461. //add the settings toggle
  462. var splitEl = $(this).getElement('div[data-filters*=SplitView]');
  463. if (splitEl) {
  464. //get the instance of splitview
  465. var split = splitEl.get('widget');
  466. if (split) {
  467. //let's not show the scrollbar over on the left side
  468. split.left.setStyle('overflow-x', 'hidden');
  469. split.addClass('bw-editor');
  470. //when the user clicks the toggle
  471. $(this).getElement('.bw-query_settings_toggle').addEvent('click', function(e){
  472. e.preventDefault();
  473. //expose or hide the left column
  474. split.chain(resizer).fold('left', split.left.getSize().x > 0 ? 0 : 230);
  475. }.bind(this));
  476. }
  477. }
  478. }
  479. });
  480. //returns an inline form validator for a form or creates one if there isn't one yet
  481. var getValidator = function(form) {
  482. var validator = form.retrieve('validator');
  483. if (!validator) {
  484. validator = new Form.Validator.Inline(form, {
  485. useTitles: true
  486. });
  487. var bwValidateNameRE = /^[a-zA-Z_]\w*/;
  488. validator.add('bw-validate-name', {
  489. errorMsg: 'Please enter a value only containing numbers, letters, underscores - the value cannot begin with a number.',
  490. test: function(element){
  491. return Form.Validator.getValidator('IsEmpty').test(element) ||
  492. (bwValidateNameRE).test(element.get('value'));
  493. }
  494. });
  495. }
  496. return validator;
  497. };
  498. /*
  499. column form class; needed because we add these dynamically
  500. column - the container of the form
  501. header - the header of the section
  502. */
  503. var ColumnForm = new Class({
  504. Implements: [Options, Events],
  505. initialize: function(column, accordion, options) {
  506. this.column = column;
  507. this.header = column.getElement('dt.bw-column_header');
  508. this.accordion = accordion;
  509. this.setOptions(options);
  510. this.setupHeader();
  511. this.setupMapData();
  512. column.getElement('.bw-remove_column button').addEvent('click', function(e){
  513. e.stop();
  514. this.fireEvent('remove', column);
  515. }.bind(this));
  516. },
  517. setupHeader: function(){
  518. //get the header and style it's input behaviors
  519. var input = this.header.getElement('input');
  520. input.addEvents({
  521. focus: function(){
  522. this.header.removeClass('bw-filled');
  523. input.addClass('bw-focused').select();
  524. this.accordion.display(this.accordion.togglers.indexOf(this.header));
  525. }.bind(this),
  526. blur: function(){
  527. if (input.get('value')) this.header.addClass('bw-filled');
  528. else this.header.removeClass('bw-filled');
  529. input.removeClass('bw-focused');
  530. }.bind(this)
  531. });
  532. this.header.getElement('label').addEvents({
  533. mouseover: function(){
  534. input.addClass('bw-focused');
  535. }.bind(this),
  536. mouseleave: function(){
  537. input.removeClass('bw-focused');
  538. }.bind(this)
  539. });
  540. },
  541. setupMapData: function(){
  542. var mapSection = this.column.getElement('.bw-map_data');
  543. if (!mapSection) return; //partition tables don't have these
  544. var arrayTypeSection = this.column.getElement('.bw-array_type');
  545. var typeSelect = this.column.getElement('.bw-col_type select');
  546. var arrayTypeSelect = this.column.getElement('.bw-array_type select');
  547. var checkType = function(){
  548. if (typeSelect.getSelected()[0].get('value') == 'array') arrayTypeSection.fade('in');
  549. else arrayTypeSection.fade('out');
  550. if (typeSelect.getSelected()[0].get('value') == 'map') mapSection.reveal();
  551. else mapSection.dissolve();
  552. };
  553. //for each column table, hide its mapdata config unless the user selects that in the array type
  554. arrayTypeSelect.addEvent('change', checkType.pass(true));
  555. typeSelect.addEvent('change', checkType.pass(true));
  556. checkType();
  557. }
  558. });
  559. })();