PageRenderTime 60ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/src/appserver/static/js/views/LookupEditView.js

https://gitlab.com/Blueprint-Marketing/lookup-editor
JavaScript | 1434 lines | 855 code | 289 blank | 290 comment | 205 complexity | eadce2148f81aa51c2cd188ee3382a10 MD5 | raw file
  1. require.config({
  2. paths: {
  3. Handsontable: "../app/lookup_editor/js/lib/handsontable.full.min",
  4. text: "../app/lookup_editor/js/lib/text",
  5. console: '../app/lookup_editor/js/lib/console',
  6. csv: '../app/lookup_editor/js/lib/csv',
  7. kv_store_field_editor: '../app/lookup_editor/js/views/KVStoreFieldEditor'
  8. },
  9. shim: {
  10. 'Handsontable': {
  11. deps: ['jquery']
  12. }
  13. }
  14. });
  15. define([
  16. "underscore",
  17. "backbone",
  18. "models/SplunkDBase",
  19. "collections/SplunkDsBase",
  20. "splunkjs/mvc",
  21. "util/splunkd_utils",
  22. "jquery",
  23. "splunkjs/mvc/simplesplunkview",
  24. "splunkjs/mvc/simpleform/input/text",
  25. "splunkjs/mvc/simpleform/input/dropdown",
  26. "text!../app/lookup_editor/js/templates/LookupEdit.html",
  27. "kv_store_field_editor",
  28. "csv",
  29. "Handsontable",
  30. "bootstrap.dropdown",
  31. "splunk.util",
  32. "css!../app/lookup_editor/css/LookupEdit.css",
  33. "css!../app/lookup_editor/css/lib/handsontable.full.min.css"
  34. ], function(
  35. _,
  36. Backbone,
  37. SplunkDBaseModel,
  38. SplunkDsBaseCollection,
  39. mvc,
  40. splunkd_utils,
  41. $,
  42. SimpleSplunkView,
  43. TextInput,
  44. DropdownInput,
  45. Template,
  46. KVStoreFieldEditor
  47. ){
  48. var Apps = SplunkDsBaseCollection.extend({
  49. url: "apps/local?count=-1",
  50. initialize: function() {
  51. SplunkDsBaseCollection.prototype.initialize.apply(this, arguments);
  52. }
  53. });
  54. var KVLookup = SplunkDBaseModel.extend({
  55. initialize: function() {
  56. SplunkDBaseModel.prototype.initialize.apply(this, arguments);
  57. }
  58. });
  59. var Backup = Backbone.Model.extend();
  60. var Backups = Backbone.Collection.extend({
  61. url: Splunk.util.make_full_url("/custom/lookup_editor/lookup_edit/get_lookup_backups_list"),
  62. model: Backup
  63. });
  64. // Define the custom view class
  65. var LookupEditView = SimpleSplunkView.extend({
  66. className: "LookupEditView",
  67. defaults: {
  68. },
  69. /**
  70. * Initialize the class.
  71. */
  72. initialize: function() {
  73. this.options = _.extend({}, this.defaults, this.options);
  74. this.backups = null;
  75. // The information for the loaded lookup
  76. this.lookup = null;
  77. this.namespace = null;
  78. this.owner = null;
  79. this.lookup_type = null;
  80. this.lookup_config = null;
  81. this.field_types = {}; // This will store the expected types for each field
  82. this.field_types_enforced = false; // This will store whether this lookup enforces types
  83. this.is_read_only = false; // We will update this to true if the lookup cannot be edited
  84. this.table_header = null; // This will store the header of the table so that can recall the relative offset of the fields in the table
  85. this.kv_store_fields_editor = null;
  86. this.forgiving_checkbox_editor = null;
  87. // Get the apps
  88. this.apps = new Apps();
  89. this.apps.on('reset', this.gotApps.bind(this), this);
  90. this.apps.fetch({
  91. success: function() {
  92. console.info("Successfully retrieved the list of applications");
  93. },
  94. error: function() {
  95. console.error("Unable to fetch the apps");
  96. }
  97. });
  98. this.is_new = true;
  99. this.info_message_posted_time = null;
  100. setInterval(this.hideInfoMessageIfNecessary.bind(this), 1000);
  101. // Listen to changes in the KV field editor so that the validation can be refreshed
  102. this.listenTo(Backbone, "kv_field:changed", this.validateForm.bind(this));
  103. },
  104. events: {
  105. // Filtering
  106. "click #save" : "doSaveLookup",
  107. "click .backup-version" : "doLoadBackup",
  108. "click #choose-import-file" : "chooseImportFile",
  109. "click #import-file" : "openFileImportModal",
  110. "change #import-file-input" : "importFile",
  111. "dragenter #lookup-table" : "onDragFileEnter",
  112. "dragleave #lookup-table": "onDragFileEnd"
  113. },
  114. /**
  115. * Hide the informational message if it is old enough
  116. */
  117. hideInfoMessageIfNecessary: function(){
  118. if(this.info_message_posted_time && ((this.info_message_posted_time + 5000) < new Date().getTime() )){
  119. this.info_message_posted_time = null;
  120. $("#info-message", this.$el).fadeOut(200);
  121. }
  122. },
  123. /**
  124. * For some reason the backbone handlers don't work.
  125. */
  126. setupDragDropHandlers: function(){
  127. // Setup a handler for handling files dropped on the table
  128. var drop_zone = document.getElementById('lookup-table');
  129. this.setupDragDropHandlerOnElement(drop_zone);
  130. // Setup a handler for handling files dropped on the import dialog
  131. drop_zone2 = document.getElementById('import-file-modal');
  132. this.setupDragDropHandlerOnElement(drop_zone2);
  133. },
  134. setupDragDropHandlerOnElement: function(drop_zone){
  135. drop_zone.ondragover = function (e) {
  136. e.preventDefault();
  137. e.dataTransfer.dropEffect = 'copy';
  138. }.bind(this);
  139. drop_zone.ondrop = function (e) {
  140. e.preventDefault();
  141. this.onDropFile(e);
  142. return false;
  143. }.bind(this);
  144. },
  145. /**
  146. * Get the field name for the column.
  147. */
  148. getFieldForColumn: function(col){
  149. var row_header = this.getTableHeader();
  150. return row_header[col];
  151. },
  152. /**
  153. * Get the table header.
  154. */
  155. getTableHeader: function(use_cached){
  156. // Assign a default argument to use_cached
  157. if(typeof use_cached === 'undefined'){
  158. use_cached = true;
  159. }
  160. // Use the cache if available
  161. if(use_cached && this.table_header !== null){
  162. return this.table_header;
  163. }
  164. // If the lookup is a CSV, then the first row is the header
  165. if(this.lookup_type === "csv"){
  166. this.table_header = handsontable.getDataAtRow(0);
  167. }
  168. // If the lookup is a KV store lookup, then ask handsontable for the header
  169. else{
  170. this.table_header = $("#lookup-table").data('handsontable').getColHeader();
  171. }
  172. return this.table_header;
  173. },
  174. /**
  175. * Get the column that has a given field name.
  176. */
  177. getColumnForField: function(field_name){
  178. var row_header = this.getTableHeader();
  179. for(var c = 0; c < row_header.length; c++){
  180. if(row_header[c] === field_name){
  181. return c;
  182. }
  183. }
  184. console.warn('Unable to find the field with the name "' + field_name + '"')
  185. return null;
  186. },
  187. /**
  188. * Determine if the cell type is invalid for KV cells that have enforced data-types.
  189. */
  190. isCellTypeInvalid: function(row, col, value){
  191. // Stop if type enforcement is off
  192. if(!this.field_types_enforced){
  193. return false;
  194. }
  195. // Determine the type of the field
  196. var handsontable = $("#lookup-table").data('handsontable');
  197. var row_header = this.getTableHeader();
  198. var field_name = row_header[col];
  199. // If we have a field type, then check it
  200. if(field_name in this.field_types){
  201. var field_type = this.field_types[field_name];
  202. // Check it if it is an number
  203. if(field_type === 'number' && !/^[-]?\d+$/.test(value)){
  204. return true;
  205. }
  206. // Check it if it is an boolean
  207. else if(field_type === 'boolean' && !/^(true)|(false)$/.test(value)){
  208. return true;
  209. }
  210. }
  211. return false;
  212. },
  213. /**
  214. * Cell renderer for HandsOnTable
  215. */
  216. lookupRenderer: function(instance, td, row, col, prop, value, cellProperties) {
  217. td.innerHTML = this.escapeHtml(value);
  218. var is_a_string = false;
  219. if(value){
  220. is_a_string = (typeof value.toLowerCase === 'function');
  221. }
  222. if(row !== 0 && this.isCellTypeInvalid(row, col, value)) { // Cell type is incorrect
  223. td.className = 'cellInvalidType';
  224. }
  225. else if(!value || value === '') {
  226. td.className = 'cellEmpty';
  227. }
  228. else if(this.getFieldForColumn(col) === "_key"){
  229. td.className = 'cellKey';
  230. }
  231. else if (parseFloat(value) < 0) { //if row contains negative number
  232. td.className = 'cellNegative';
  233. }
  234. else if( String(value).substring(0, 7) == "http://" || String(value).substring(0, 8) == "https://"){
  235. td.className = 'cellHREF';
  236. }
  237. else if (parseFloat(value) > 0) { //if row contains positive number
  238. td.className = 'cellPositive';
  239. }
  240. else if(row === 0 && this.lookup_type === 'csv') {
  241. td.className = 'cellHeader';
  242. }
  243. else if(value !== null && is_a_string && value.toLowerCase() === 'true') {
  244. td.className = 'cellTrue';
  245. }
  246. else if(value !== null && is_a_string && value.toLowerCase() ==='false') {
  247. td.className = 'cellFalse';
  248. }
  249. else if(value !== null && is_a_string && value.toLowerCase() === 'unknown') {
  250. td.className = 'cellUrgencyUnknown';
  251. }
  252. else if(value !== null && is_a_string && value.toLowerCase() === 'informational') {
  253. td.className = 'cellUrgencyInformational';
  254. }
  255. else if(value !== null && is_a_string && value.toLowerCase() === 'low') {
  256. td.className = 'cellUrgencyLow';
  257. }
  258. else if(value !== null && is_a_string && value.toLowerCase() === 'medium') {
  259. td.className = 'cellUrgencyMedium';
  260. }
  261. else if(value !== null && is_a_string && value.toLowerCase() === 'high') {
  262. td.className = 'cellUrgencyHigh';
  263. }
  264. else if(value !== null && is_a_string && value.toLowerCase() === 'critical') {
  265. td.className = 'cellUrgencyCritical';
  266. }
  267. else {
  268. td.className = '';
  269. }
  270. if(cellProperties.readOnly) {
  271. td.style.opacity = 0.7;
  272. }
  273. },
  274. /**
  275. * Open the modal for importing a file.
  276. */
  277. openFileImportModal: function(){
  278. $('.dragging').removeClass('dragging');
  279. $('#import-file-modal', this.$el).modal();
  280. // Setuo handlers for drag & drop
  281. $('.modal-backdrop').on('dragenter', function(){
  282. $('.modal-body').addClass('dragging');
  283. console.log("enter");
  284. });
  285. $('.modal-backdrop').on('dragleave', function(){
  286. $('.modal-body').removeClass('dragging');
  287. console.log("leave");
  288. });
  289. $('#import-file-modal').on('dragenter', function(){
  290. $('.modal-body').addClass('dragging');
  291. console.log("enter");
  292. });
  293. /*
  294. $('#import-file-modal').on('dragleave', function(){
  295. $('.modal-body').removeClass('dragging');
  296. console.log("leave");
  297. });
  298. */
  299. },
  300. /**
  301. * Open the file dialog to select a file to import.
  302. */
  303. chooseImportFile: function(){
  304. $("#import-file-input").click();
  305. },
  306. /**
  307. * Load the selected lookup from from the history.
  308. *
  309. * @param version The version of the lookup file to load (a value of null will load the latest version)
  310. */
  311. loadBackupFile: function(version){
  312. // Load a default for the version
  313. if( typeof version == 'undefined' ){
  314. version = null;
  315. }
  316. var r = confirm('This version the lookup file will now be loaded.\n\nUnsaved changes will be overridden.');
  317. if (r == true) {
  318. this.loadLookupContents(this.lookup, this.namespace, this.owner, this.lookup_type, false, version);
  319. return true;
  320. }
  321. else{
  322. return false;
  323. }
  324. },
  325. /**
  326. * Hide the warning message.
  327. */
  328. hideWarningMessage: function(){
  329. this.hide($("#warning-message", this.$el));
  330. },
  331. /**
  332. * Hide the informational message
  333. */
  334. hideInfoMessage: function(){
  335. this.hide($("#info-message", this.$el));
  336. },
  337. /**
  338. * Hide the messages.
  339. */
  340. hideMessages: function(){
  341. this.hideWarningMessage();
  342. this.hideInfoMessage();
  343. },
  344. /**
  345. * Show a warning noting that something bad happened.
  346. */
  347. showWarningMessage: function(message){
  348. $("#warning-message > .message", this.$el).text(message);
  349. this.unhide($("#warning-message", this.$el));
  350. },
  351. /**
  352. * Show a warning noting that something bad happened.
  353. */
  354. showInfoMessage: function(message){
  355. $("#info-message > .message", this.$el).text(message);
  356. this.unhide($("#info-message", this.$el));
  357. this.info_message_posted_time = new Date().getTime();
  358. },
  359. /**
  360. * Load the list of backup lookup files.
  361. *
  362. * @param lookup_file The name of the lookup file
  363. * @param namespace The app where the lookup file exists
  364. * @param user The user that owns the file (in the case of user-based lookups)
  365. */
  366. loadLookupBackupsList: function(lookup_file, namespace, user){
  367. var data = {"lookup_file":lookup_file,
  368. "namespace":namespace};
  369. // Populate the default parameter in case user wasn't provided
  370. if( typeof user === 'undefined' ){
  371. user = null;
  372. }
  373. // If a user was defined, then pass the name as a parameter
  374. if(user !== null){
  375. data["owner"] = user;
  376. }
  377. // Fetch them
  378. this.backups = new Backups();
  379. this.backups.fetch({
  380. data: $.param(data),
  381. success: this.renderBackupsList.bind(this)
  382. });
  383. },
  384. onDragFile: function(evt){
  385. evt.stopPropagation();
  386. evt.preventDefault();
  387. evt.dataTransfer.dropEffect = 'copy'; // Make it clear this is a copy
  388. },
  389. onDragFileEnter: function(evt){
  390. evt.preventDefault();
  391. //$('#drop-zone', this.$el).show();
  392. //$('#drop-zone', this.$el).height($('#lookup-table', this.$el).height());
  393. //$('#lookup-table', this.$el).addClass('drop-target');
  394. return false;
  395. },
  396. onDragFileEnd: function(){
  397. console.log("Dragging stopped");
  398. this.$el.removeClass('dragging');
  399. },
  400. /**
  401. * Import the dropped file.
  402. */
  403. onDropFile: function(evt){
  404. console.log("Got a file via drag and drop");
  405. evt.stopPropagation();
  406. evt.preventDefault();
  407. var files = evt.dataTransfer.files;
  408. this.importFile(evt);
  409. },
  410. /*
  411. * Use the browser's built-in functionality to quickly and safely escape a string of HTML.
  412. */
  413. escapeHtml: function(str) {
  414. var div = document.createElement('div');
  415. div.appendChild(document.createTextNode(str));
  416. return div.innerHTML;
  417. },
  418. /**
  419. * Import the given file into the lookup.
  420. */
  421. importFile: function(evt){
  422. // Stop if this is a KV collection; importing isn't yet supported
  423. if(this.lookup_type !== "csv"){
  424. this.showWarningMessage("Drag & drop importing on KV store lookups is not currently supported");
  425. console.info("Drag and dropping on a KV store lookup being ignored");
  426. return false;
  427. }
  428. // Stop if this is read-only
  429. if(this.read_only){
  430. console.info("Drag and dropping on a read-only lookup being ignored");
  431. return false;
  432. }
  433. // Stop if the browser doesn't support processing files in Javascript
  434. if(!window.FileReader){
  435. alert("Your browser doesn't support file reading in Javascript; thus, I cannot parse your uploaded file");
  436. return false;
  437. }
  438. // Get a reader so that we can read in the file
  439. var reader = new FileReader();
  440. // Setup an onload handler that will process the file
  441. reader.onload = function(evt) {
  442. console.log("Running file reader onload handler");
  443. // Stop if the ready state isn't "loaded"
  444. if(evt.target.readyState != 2){
  445. return;
  446. }
  447. // Stop if the file could not be processed
  448. if(evt.target.error) {
  449. // Hide the loading message
  450. $(".table-loading-message").hide();
  451. // Show an error
  452. this.showWarningMessage("Unable to import the file");
  453. return;
  454. }
  455. // Get the file contents
  456. var filecontent = evt.target.result;
  457. // Import the file into the view
  458. var data = new CSV(filecontent, { }).parse();
  459. // Render the lookup file
  460. this.renderLookup(data);
  461. // Hide the import dialog
  462. $('#import-file-modal', this.$el).modal('hide');
  463. // Show a message noting that the file was imported
  464. this.showInfoMessage("File imported successfully");
  465. }.bind(this);
  466. var files = [];
  467. // Get the files from the file input widget if available
  468. if(evt.target.files && evt.target.files.length > 0){
  469. files = evt.target.files;
  470. }
  471. // Get the files from the drag & drop if available
  472. else if(evt.dataTransfer && evt.dataTransfer.files.length > 0){
  473. files = evt.dataTransfer.files;
  474. }
  475. // Stop if no files where provided (user likely pressed cancel)
  476. if(files.length > 0 ){
  477. // Set the file name if this is a new file and a filename was not set yet
  478. if(this.is_new && (!mvc.Components.getInstance("lookup-name").val() || mvc.Components.getInstance("lookup-name").val().length <= 0)){
  479. mvc.Components.getInstance("lookup-name").val(files[0].name);
  480. }
  481. // Start the process of processing file
  482. reader.readAsText(files[0]);
  483. }
  484. else{
  485. // Hide the loading message
  486. $(".table-loading-message").hide();
  487. }
  488. },
  489. /**
  490. * Render the list of backup files.
  491. */
  492. renderBackupsList: function(){
  493. var backup_list_template = '<a class="btn active btn-primary dropdown-toggle" data-toggle="dropdown" href="#"> \
  494. Revert to previous version \
  495. <span class="caret"></span> \
  496. </a> \
  497. <ul class="dropdown-menu"> \
  498. <% for(var c = 0; c < backups.length; c++){ %> \
  499. <li><a class="backup-version" href="#" data-backup-time="<%- backups[c].time %>"><%- backups[c].time_readable %></a></li> \
  500. <% } %> \
  501. <% if(backups.length == 0){ %> \
  502. <li><a class="backup-version" href="#">No backup versions available</a></li> \
  503. <% } %> \
  504. </ul>';
  505. // Render the list of backups
  506. $('#load-backup', this.$el).html(_.template(backup_list_template, {
  507. 'backups' : this.backups.toJSON()
  508. }));
  509. // Show the list of backup lookups
  510. if(this.read_only !== true){
  511. $('#load-backup', this.$el).show();
  512. }
  513. else{
  514. $('#load-backup', this.$el).hide();
  515. }
  516. },
  517. /**
  518. * Make a new KV store lookup
  519. */
  520. makeKVStoreLookup: function(namespace, lookup_file, owner){
  521. // Set a default value for the owner
  522. if( typeof owner == 'undefined' ){
  523. owner = 'nobody';
  524. }
  525. // Make the data that will be posted to the server
  526. var data = {
  527. "name": lookup_file
  528. };
  529. // Perform the call
  530. $.ajax({
  531. url: splunkd_utils.fullpath(['/servicesNS', owner, namespace, 'storage/collections/config'].join('/')),
  532. data: data,
  533. type: 'POST',
  534. // On success, populate the table
  535. success: function(data) {
  536. console.info('KV store lookup file created');
  537. // Remember the specs on the created file
  538. this.lookup = lookup_file;
  539. this.namespace = namespace;
  540. this.owner = owner;
  541. this.lookup_type = "kv";
  542. this.kv_store_fields_editor.modifyKVStoreLookupSchema(this.namespace, this.lookup, 'nobody', function(){
  543. this.showInfoMessage("Lookup created successfully");
  544. document.location = "?lookup=" + lookup_file + "&owner=" + owner + "&type=kv&namespace=" + namespace;
  545. }.bind(this));
  546. }.bind(this),
  547. // Handle cases where the file could not be found or the user did not have permissions
  548. complete: function(jqXHR, textStatus){
  549. if( jqXHR.status == 403){
  550. console.info('Inadequate permissions');
  551. this.showWarningMessage("You do not have permission to make a KV store collection", true);
  552. }
  553. else if( jqXHR.status == 409){
  554. console.info('Lookup name already exists');
  555. $('#lookup-name-control-group', this.$el).addClass('error');
  556. this.showWarningMessage("Lookup name already exists, please select another");
  557. }
  558. this.setSaveButtonTitle();
  559. }.bind(this),
  560. // Handle errors
  561. error: function(jqXHR, textStatus, errorThrown){
  562. if( jqXHR.status != 403 && jqXHR.status != 409 ){
  563. console.info('KV store collection creation failed');
  564. this.showWarningMessage("The KV store collection could not be created", true);
  565. }
  566. }.bind(this)
  567. });
  568. },
  569. /**
  570. * Load the lookup file contents from the server and populate the editor.
  571. *
  572. * @param lookup_file The name of the lookup file
  573. * @param namespace The app where the lookup file exists
  574. * @param user The user that owns the file (in the case of user-based lookups)
  575. * @param lookup_type Indicates whether this is a KV store or a CSV lookup (needs to be either "kv" or "csv")
  576. * @param header_only Indicates if only the header row should be retrieved
  577. * @param version The version to get from the archived history
  578. */
  579. loadLookupContents: function(lookup_file, namespace, user, lookup_type, header_only, version){
  580. // Set a default value for header_only
  581. if( typeof header_only == 'undefined' ){
  582. header_only = false;
  583. }
  584. var data = {"lookup_file":lookup_file,
  585. "namespace" :namespace,
  586. "header_only":header_only,
  587. "lookup_type":lookup_type};
  588. // Set a default value for version
  589. if( typeof version == 'undefined' ){
  590. version = undefined;
  591. }
  592. // Show the loading message
  593. $(".table-loading-message").show(); // TODO replace
  594. // Set the version parameter if we are asking for an old version
  595. if( version !== undefined && version ){
  596. data.version = version;
  597. }
  598. // If a user was defined, then pass the name as a parameter
  599. if(user !== null){
  600. data["owner"] = user;
  601. }
  602. // Make the URL
  603. url = Splunk.util.make_full_url("/custom/lookup_editor/lookup_edit/get_lookup_contents", data);
  604. // Started recording the time so that we figure out how long it took to load the lookup file
  605. var populateStart = new Date().getTime();
  606. // Perform the call
  607. $.ajax({
  608. url: url,
  609. cache: false,
  610. // On success, populate the table
  611. success: function(data) {
  612. console.info('JSON of lookup table was successfully loaded');
  613. this.renderLookup(data);
  614. var elapsed = new Date().getTime()-populateStart;
  615. console.info("Lookup loaded and rendered in " + elapsed + "ms");
  616. // Remember the specs on the loaded file
  617. this.lookup = lookup_file;
  618. this.namespace = namespace;
  619. this.owner = user;
  620. this.lookup_type = lookup_type;
  621. }.bind(this),
  622. // Handle cases where the file could not be found or the user did not have permissions
  623. complete: function(jqXHR, textStatus){
  624. if( jqXHR.status == 404){
  625. console.info('Lookup file was not found');
  626. this.showWarningMessage("The requested lookup file does not exist", true);
  627. }
  628. else if( jqXHR.status == 403){
  629. console.info('Inadequate permissions');
  630. this.showWarningMessage("You do not have permission to view this lookup file", true);
  631. }
  632. else if( jqXHR.status == 420){
  633. console.info('File is too large');
  634. this.showWarningMessage("The file is too big to be edited (must be less than 10 MB)");
  635. }
  636. // Hide the loading message
  637. $(".table-loading-message").hide();
  638. // Start the loading of the history
  639. if( version === undefined && this.lookup_type === "csv" ){
  640. this.loadLookupBackupsList(lookup_file, namespace, user);
  641. }
  642. else if(this.lookup_type === "csv"){
  643. // Show a message noting that the backup was imported
  644. this.showInfoMessage("Backup file was loaded successfully");
  645. }
  646. }.bind(this),
  647. // Handle errors
  648. error: function(jqXHR, textStatus, errorThrown){
  649. if( jqXHR.status != 404 && jqXHR.status != 403 && jqXHR.status != 420 ){
  650. console.info('Lookup file could not be loaded');
  651. this.showWarningMessage("The lookup could not be loaded from the server", true);
  652. }
  653. this.read_only = true;
  654. this.hideEditingControls();
  655. }.bind(this)
  656. });
  657. },
  658. /**
  659. * Hide the editing controls
  660. */
  661. hideEditingControls: function(hide){
  662. // Load a default for the version
  663. if( typeof hide === 'undefined' ){
  664. hide = true;
  665. }
  666. if(hide){
  667. $('.btn', this.$el).hide();
  668. }
  669. else{
  670. $('.btn', this.$el).show();
  671. }
  672. },
  673. /**
  674. * Validate that the lookup contents are a valid file
  675. *
  676. * @param data The data (array of array) representing the table
  677. * @returns {Boolean}
  678. */
  679. validate: function(data) {
  680. // If the cell is the first row, then ensure that the new value is not blank
  681. if( data[0][0] === 0 && data[0][3].length === 0 ){
  682. return false;
  683. }
  684. },
  685. /**
  686. * Validate the content of the form
  687. */
  688. validateForm: function(){
  689. var issues = 0;
  690. // By default assume everything passes
  691. $('#lookup-name-control-group', this.$el).removeClass('error');
  692. $('#lookup-app-control-group', this.$el).removeClass('error');
  693. this.hideWarningMessage();
  694. // Make sure the lookup name is defined
  695. if(this.is_new && (!mvc.Components.getInstance("lookup-name").val() || mvc.Components.getInstance("lookup-name").val().length <= 0)){
  696. $('#lookup-name-control-group', this.$el).addClass('error');
  697. this.showWarningMessage("Please enter a lookup name");
  698. issues = issues + 1;
  699. }
  700. // Make sure the lookup name is acceptable
  701. else if(this.is_new && !mvc.Components.getInstance("lookup-name").val().match(/^[-A-Z0-9_ ]+([.][-A-Z0-9_ ]+)*$/gi)){
  702. $('#lookup-name-control-group', this.$el).addClass('error');
  703. this.showWarningMessage("Lookup name is invalid");
  704. issues = issues + 1;
  705. }
  706. // Make sure the lookup app is defined
  707. if(this.is_new && (! mvc.Components.getInstance("lookup-app").val() || mvc.Components.getInstance("lookup-app").val().length <= 0)){
  708. $('#lookup-app-control-group', this.$el).addClass('error');
  709. this.showWarningMessage("Select the app where the lookup will reside");
  710. issues = issues + 1;
  711. }
  712. // Make sure at least one field is defined (for KV store lookups only)
  713. if(this.is_new && this.lookup_type === "kv" ){
  714. var validate_response = this.kv_store_fields_editor.validate();
  715. if(validate_response !== true){
  716. this.showWarningMessage(validate_response);
  717. issues = issues + 1;
  718. }
  719. }
  720. // Determine if the validation passed
  721. if(issues > 0){
  722. return false;
  723. }
  724. else{
  725. return true;
  726. }
  727. },
  728. /**
  729. * Get the list of apps as choices.
  730. */
  731. getAppsChoices: function(){
  732. // If we don't have the apps yet, then just return an empty list for now
  733. if(!this.apps){
  734. return [];
  735. }
  736. var choices = [];
  737. for(var c = 0; c < this.apps.models.length; c++){
  738. choices.push({
  739. 'label': this.apps.models[c].entry.associated.content.attributes.label,
  740. 'value': this.apps.models[c].entry.attributes.name
  741. });
  742. }
  743. return choices;
  744. },
  745. /**
  746. * Get the apps
  747. */
  748. gotApps: function(){
  749. // Update the list
  750. if(mvc.Components.getInstance("lookup-app")){
  751. mvc.Components.getInstance("lookup-app").settings.set("choices", this.getAppsChoices());
  752. }
  753. },
  754. /**
  755. * Set the title of the save button
  756. */
  757. setSaveButtonTitle: function(title){
  758. if(typeof title == 'undefined' ){
  759. $("#save").text("Save Lookup");
  760. }
  761. else{
  762. $("#save").text(title);
  763. }
  764. },
  765. /**
  766. * Pad an integer with zeroes.
  767. */
  768. pad: function(num, size) {
  769. var s = num+"";
  770. while (s.length < size) s = "0" + s;
  771. return s;
  772. },
  773. /**
  774. * Update the modification time
  775. */
  776. updateTimeModified: function(){
  777. var today = new Date();
  778. var am_or_pm = today.getHours() > 12 ? "PM" : "AM";
  779. $("#modification-time").text("Modified: " + today.getFullYear() + "/" + this.pad(today.getMonth() + 1, 2) + "/" + today.getDate() + " " + this.pad((today.getHours() % 12),2) + ":" + this.pad(today.getMinutes(), 2) + ":" + this.pad(today.getSeconds(),2) + " " + am_or_pm);
  780. $(".modification-time-holder > i").show();
  781. $(".modification-time-holder > i").fadeOut(1000);
  782. },
  783. /**
  784. * Load the selected backup.
  785. */
  786. doLoadBackup: function(evt){
  787. var version = evt.currentTarget.dataset.backupTime;
  788. if(version){
  789. this.loadBackupFile(version);
  790. }
  791. },
  792. /**
  793. * Perform the operation to save the lookup
  794. *
  795. * @returns {Boolean}
  796. */
  797. doSaveLookup: function(evt){
  798. // Determine if we are making a new entry
  799. var making_new_lookup = this.is_new;
  800. // Change the title
  801. this.setSaveButtonTitle("Saving...");
  802. // Started recording the time so that we figure out how long it took to save the lookup file
  803. var populateStart = new Date().getTime();
  804. // Hide the warnings. We will repost them if the input is still invalid
  805. this.hideMessages();
  806. // Stop if the form didn't validate
  807. if(!this.validateForm()){
  808. this.setSaveButtonTitle();
  809. return;
  810. }
  811. // If we are making a new KV store lookup, then make it
  812. if(making_new_lookup && this.lookup_type === "kv"){
  813. this.makeKVStoreLookup(mvc.Components.getInstance("lookup-app").val(), mvc.Components.getInstance("lookup-name").val());
  814. }
  815. // Otherwise, save the lookup
  816. else{
  817. // Get a reference to the handsontable plugin
  818. var handsontable = $("#lookup-table").data('handsontable');
  819. // Get the row data
  820. row_data = handsontable.getData();
  821. // Convert the data to JSON
  822. json = JSON.stringify(row_data);
  823. // Make the arguments
  824. var data = {
  825. lookup_file : this.lookup,
  826. namespace : this.namespace,
  827. contents : json
  828. };
  829. // If a user was defined, then pass the name as a parameter
  830. if(this.owner !== null){
  831. data["owner"] = this.owner;
  832. }
  833. // Validate the input if it is new
  834. if(making_new_lookup){
  835. // Get the lookup file name from the form if we are making a new lookup
  836. data["lookup_file"] = mvc.Components.getInstance("lookup-name").val();
  837. // Make sure that the file name was included; stop if it was not
  838. if (data["lookup_file"] === ""){
  839. $("#lookup_file_error").text("Please define a file name"); // TODO
  840. $("#lookup_file_error").show();
  841. this.setSaveButtonTitle();
  842. return false;
  843. }
  844. // Make sure that the file name is valid; stop if it is not
  845. if( !data["lookup_file"].match(/^[-A-Z0-9_ ]+([.][-A-Z0-9_ ]+)*$/gi) ){
  846. $("#lookup_file_error").text("The file name contains invalid characters"); // TODO
  847. $("#lookup_file_error").show();
  848. this.setSaveButtonTitle();
  849. return false;
  850. }
  851. // Get the namespace from the form if we are making a new lookup
  852. data["namespace"] = mvc.Components.getInstance("lookup-app").val();
  853. // Make sure that the namespace was included; stop if it was not
  854. if (data["namespace"] === ""){
  855. $("#lookup_namespace_error").text("Please define a namespace");
  856. $("#lookup_namespace_error").show();
  857. this.setSaveButtonTitle();
  858. return false;
  859. }
  860. }
  861. // Make sure at least a header exists; stop if not enough content is present
  862. if(row_data.length === 0){
  863. this.showWarningMessage("Lookup files must contain at least one row (the header)");
  864. return false;
  865. }
  866. // Make sure the headers are not empty.
  867. // If the editor is allowed to add extra columns then ignore the last row since this for adding a new column thus is allowed
  868. for( i = 0; i < row_data[0].length; i++){
  869. // Determine if this row has an empty header cell
  870. if( row_data[0][i] === "" ){
  871. this.showWarningMessage("Header rows cannot contain empty cells (column " + (i + 1) + " of the header is empty)");
  872. return false;
  873. }
  874. }
  875. // Perform the request to save the lookups
  876. $.ajax( {
  877. url: Splunk.util.make_url('/custom/lookup_editor/lookup_edit/save'),
  878. type: 'POST',
  879. data: data,
  880. success: function(){
  881. console.log("Lookup file saved successfully");
  882. this.showInfoMessage("Lookup file saved successfully");
  883. this.setSaveButtonTitle();
  884. // Persist the information about the lookup
  885. if(this.is_new){
  886. this.lookup = data["lookup_file"];
  887. this.namespace = data["namespace"];
  888. this.owner = data["owner"];
  889. }
  890. }.bind(this),
  891. // Handle cases where the file could not be found or the user did not have permissions
  892. complete: function(jqXHR, textStatus){
  893. var elapsed = new Date().getTime()-populateStart;
  894. console.info("Lookup save operation completed in " + elapsed + "ms");
  895. var success = true;
  896. if(jqXHR.status == 404){
  897. console.info('Lookup file was not found');
  898. this.showWarningMessage("This lookup file could not be found");
  899. success = false;
  900. }
  901. else if(jqXHR.status == 403){
  902. console.info('Inadequate permissions');
  903. this.showWarningMessage("You do not have permission to edit this lookup file");
  904. success = false;
  905. }
  906. else if(jqXHR.status == 400){
  907. console.info('Invalid input');
  908. this.showWarningMessage("This lookup file could not be saved because the input is invalid");
  909. success = false;
  910. }
  911. else if(jqXHR.status == 500){
  912. this.showWarningMessage("The lookup file could not be saved");
  913. success = false;
  914. }
  915. this.setSaveButtonTitle();
  916. // If we made a new lookup, then switch modes
  917. if(this.is_new){
  918. this.changeToEditMode();
  919. }
  920. // Update the lookup backup list
  921. if(success){
  922. this.loadLookupBackupsList(this.lookup, this.namespace, this.owner);
  923. }
  924. }.bind(this),
  925. error: function(jqXHR,textStatus,errorThrown) {
  926. console.log("Lookup file not saved");
  927. this.showWarningMessage("Lookup file could not be saved");
  928. }.bind(this)
  929. }
  930. );
  931. }
  932. return false;
  933. },
  934. /**
  935. * Do an edit to a row cell (for KV store lookups since edits are dynamic).
  936. */
  937. doEditCell: function(row, col, new_value){
  938. // Stop if we are in read-only mode
  939. if(this.read_only){
  940. return;
  941. }
  942. var handsontable = $("#lookup-table").data('handsontable');
  943. // First, we need to get the _key of the edited row
  944. var row_data = handsontable.getDataAtRow(row);
  945. var _key = row_data[this.getColumnForField('_key')];
  946. if(_key === undefined){
  947. console.error("Unable to get the _key for editing the cell at (" + row + ", " + col + ")");
  948. return;
  949. }
  950. // Second, we need to get all of the data from the given row because we must re-post all of the cell data
  951. var record_data = this.makeRowJSON(row);
  952. // If _key doesn't exist, then we will create a new row
  953. var url = Splunk.util.make_url("/splunkd/servicesNS/" + this.owner + "/" + this.namespace + "/storage/collections/data/" + this.lookup + "/" + _key);
  954. if(!_key){
  955. url = Splunk.util.make_url("/splunkd/servicesNS/" + this.owner + "/" + this.namespace + "/storage/collections/data/" + this.lookup);
  956. }
  957. // Third, we need to do a post to update the row
  958. $.ajax({
  959. url: url,
  960. type: "POST",
  961. dataType: "json",
  962. data: JSON.stringify(record_data),
  963. contentType: "application/json; charset=utf-8",
  964. // On success
  965. success: function(data) {
  966. this.hideWarningMessage();
  967. // If this is a new row, then populate the _key
  968. if(!_key){
  969. _key = data['_key'];
  970. handsontable.setDataAtCell(row, this.getColumnForField("_key"), _key, "key_update");
  971. console.info('KV store entry creation completed for entry ' + _key);
  972. }
  973. else{
  974. console.info('KV store entry edit completed for entry ' + _key);
  975. }
  976. this.updateTimeModified();
  977. }.bind(this),
  978. // On complete
  979. complete: function(jqXHR, textStatus){
  980. if( jqXHR.status == 403){
  981. console.info('Inadequate permissions');
  982. this.showWarningMessage("You do not have permission to edit this lookup", true);
  983. }
  984. }.bind(this),
  985. // Handle errors
  986. error: function(jqXHR, textStatus, errorThrown){
  987. this.showWarningMessage("Entry could not be saved to the KV store lookup; make sure the value matches the expected type", true);
  988. }.bind(this)
  989. });
  990. },
  991. /**
  992. * Do the removal of a row (for KV store lookups since edits are dynamic).
  993. */
  994. doRemoveRow: function(row){
  995. // Stop if we are in read-only mode
  996. if(this.read_only){
  997. return;
  998. }
  999. var handsontable = $("#lookup-table").data('handsontable');
  1000. // First, we need to get the _key of the edited row
  1001. var row_data = handsontable.getDataAtRow(row);
  1002. var _key = row_data[0];
  1003. // Second, we need to do a post to remove the row
  1004. $.ajax({
  1005. url: Splunk.util.make_url("/splunkd/servicesNS/" + this.owner + "/" + this.namespace + "/storage/collections/data/" + this.lookup + "/" + _key),
  1006. type: "DELETE",
  1007. // On success
  1008. success: function(data) {
  1009. console.info('KV store entry removal completed for entry ' + _key);
  1010. this.hideWarningMessage();
  1011. this.updateTimeModified();
  1012. }.bind(this),
  1013. // On complete
  1014. complete: function(jqXHR, textStatus){
  1015. if( jqXHR.status == 403){
  1016. console.info('Inadequate permissions');
  1017. this.showWarningMessage("You do not have permission to edit this lookup", true);
  1018. }
  1019. }.bind(this),
  1020. // Handle errors
  1021. error: function(jqXHR, textStatus, errorThrown){
  1022. this.showWarningMessage("An entry could not be removed from the KV store lookup", true);
  1023. }.bind(this)
  1024. });
  1025. },
  1026. /**
  1027. * Add the given field to the data with the appropriate hierarchy.
  1028. */
  1029. addFieldToJSON: function(json_data, field, value){
  1030. var split_field = [];
  1031. split_field = field.split(".");
  1032. // If the field has a period, then this is hierarchical field
  1033. // For these, we need to build the heirarchy or make sure it exists.
  1034. if(split_field.length > 1){
  1035. // If the top-most field doesn't exist, create it
  1036. if(!(split_field[0] in json_data)){
  1037. json_data[split_field[0]] = {};
  1038. }
  1039. // Recurse to add the children
  1040. return this.addFieldToJSON(json_data[split_field[0]], split_field.slice(1).join("."), value);
  1041. }
  1042. // For non-hierarchical fields, we can just add them
  1043. else{
  1044. json_data[field] = value;
  1045. // This is the base case
  1046. return json_data;
  1047. }
  1048. },
  1049. /**
  1050. * Make JSON for the given row.
  1051. */
  1052. makeRowJSON: function(row){
  1053. var handsontable = $("#lookup-table").data('handsontable');
  1054. // We need to get the row meta-data and the
  1055. var row_header = this.getTableHeader();
  1056. var row_data = handsontable.getDataAtRow(row);
  1057. // This is going to hold the data for the row
  1058. var json_data = {};
  1059. // Add each field / column
  1060. for(var c=1; c < row_header.length; c++){
  1061. this.addFieldToJSON(json_data, row_header[c], (row_data[c] === undefined ? '' : row_data[c]) );
  1062. }
  1063. // Return the created JSON
  1064. return json_data;
  1065. },
  1066. /**
  1067. * Do the creation of a row (for KV store lookups since edits are dynamic).
  1068. */
  1069. doCreateRows: function(row, count){
  1070. // Stop if we are in read-only mode
  1071. if(this.read_only){
  1072. return;
  1073. }
  1074. var handsontable = $("#lookup-table").data('handsontable');
  1075. // Create entries for each row to create
  1076. var record_data = [];
  1077. for(var c=0; c < count; c++){
  1078. record_data.push(this.makeRowJSON(row + c));
  1079. }
  1080. // Third, we need to do a post to create the row
  1081. $.ajax({
  1082. url: Splunk.util.make_url("/splunkd/servicesNS/" + this.owner + "/" + this.namespace + "/storage/collections/data/" + this.lookup + "/batch_save"),
  1083. type: "POST",
  1084. dataType: "json",
  1085. data: JSON.stringify(record_data),
  1086. contentType: "application/json; charset=utf-8",
  1087. // On success
  1088. success: function(data) {
  1089. // Update the _key values in the cells
  1090. for(var c=0; c < data.length; c++){
  1091. handsontable.setDataAtCell(row + c, this.getColumnForField("_key"), data[c], "key_update")
  1092. }
  1093. this.hideWarningMessage();
  1094. this.updateTimeModified();
  1095. }.bind(this),
  1096. // On complete
  1097. complete: function(jqXHR, textStatus){
  1098. if( jqXHR.status == 403){
  1099. console.info('Inadequate permissions');
  1100. this.showWarningMessage("You do not have permission to edit this lookup", true);
  1101. }
  1102. }.bind(this),
  1103. // Handle errors
  1104. error: function(jqXHR, textStatus, errorThrown){
  1105. // This error can be thrown when the lookup requires a particular type
  1106. //this.showWarningMessage("Entries could not be saved to the KV store lookup", true);
  1107. }.bind(this)
  1108. });
  1109. },
  1110. /**
  1111. * Get colummn configuration data for the columns so that the table presents a UI for editing the cells appropriately.
  1112. */
  1113. getColumnsMetadata: function(){
  1114. // Stop if we don't have the required data yet
  1115. if(!this.getTableHeader()){
  1116. console.warn("The table header is not available yet")
  1117. }
  1118. // IF this is a CSV lookup, then add a column renderer to excape the content
  1119. var table_header = this.getTableHeader();
  1120. var column = null;
  1121. var columns = [];
  1122. // Stop if we didn't get the types necessary
  1123. if(!this.field_types){
  1124. console.warn("The table field types are not available yet")
  1125. }
  1126. // This variable will contain the meta-data about the columns
  1127. // Columns is going to have a single field by default for the _key field which is not included in the field-types
  1128. var field_info = null;
  1129. for(var c = 0; c < table_header.length; c++){
  1130. field_info = this.field_types[table_header[c]];
  1131. column = {};
  1132. // Use a checkbox for the boolean
  1133. if(field_info === 'boolean'){
  1134. column['type'] = 'checkbox';
  1135. column['editor'] = this.getCheckboxRenderer();
  1136. }
  1137. else if(field_info === 'time'){
  1138. //column['type'] = 'checkbox';
  1139. }
  1140. columns.push(column);
  1141. }
  1142. return columns;
  1143. /*
  1144. // Get a reference to Hands-on-table
  1145. var handsontable = $("#lookup-table").data('