bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/web/static/js/config.ts (about)

     1  /// <reference path="0-bosun.ts" />
     2  interface IConfigScope extends IBosunScope {
     3  	// text loading/navigation
     4  	config_text: string;
     5  	selected_alert: string;
     6  	items: { [type: string]: string[]; };
     7  	scrollTo: (type: string, name: string) => void;
     8  	aceLoaded: (editor: any) => void;
     9  	editor: any;
    10  	validate: () => void;
    11  	validationResult: string;
    12  	saveResult: string;
    13  	selectAlert: (alert: string) => void;
    14  	reparse: () => void;
    15  	aceTheme: string;
    16  	aceMode: string;
    17  	aceToggleHighlight: () => void;
    18  	quickJumpTarget: string;
    19  	quickJump: () => void;
    20  	downloadConfig: () => void;
    21  	saveConfig: () => void;
    22  	saveClass: () => string;
    23  	sectionToDocs: { [type: string]: string; };
    24  
    25  	//rule execution options
    26  	fromDate: string;
    27  	toDate: string;
    28  	fromTime: string;
    29  	toTime: string;
    30  	intervals: number;
    31  	duration: number;
    32  	email: string;
    33  	template_group: string;
    34  	setInterval: () => void;
    35  	setDuration: () => void;
    36  
    37  	//rule execution
    38  	running: boolean;
    39  	errors: string[];
    40  	warning: string[];
    41  	test: () => void;
    42  	sets: any;
    43  	alert_history: any;
    44  	subject: string;
    45  	emailSubject: string;
    46  	emailBody: string;
    47  	body: string;
    48  	customTemplates: { [name: string]: string };
    49  	notifications: { [name: string]: any };
    50  	actionNotifications: {[name: string]: {[at: string]:any}};
    51  	notificationToShow: string;
    52  	data: any;
    53  	tab: string;
    54  	zws: (v: string) => string;
    55  	setTemplateGroup: (group: any) => void;
    56  	scrollToInterval: (v: string) => void;
    57  	show: (v: any) => void;
    58  	loadTimelinePanel: (entry: any, v: any) => void;
    59  	incidentId: number;
    60  
    61  	// saving
    62  	message: string;
    63  	diff: string;
    64  	diffConfig: () => void;
    65  	expandDiff: boolean;
    66  	runningHash: string;
    67  	runningChanged: boolean;
    68  	runningChangedHelp: string;
    69  	runningHashResult: string;
    70  	getRunningHash: () => void;
    71  }
    72  
    73  bosunControllers.controller('ConfigCtrl', ['$scope', '$http', '$location', '$route', '$timeout', '$sce', function ($scope: IConfigScope, $http: ng.IHttpService, $location: ng.ILocationService, $route: ng.route.IRouteService, $timeout: ng.ITimeoutService, $sce: ng.ISCEService) {
    74  	var search = $location.search();
    75  	$scope.fromDate = search.fromDate || '';
    76  	$scope.fromTime = search.fromTime || '';
    77  	$scope.toDate = search.toDate || '';
    78  	$scope.toTime = search.toTime || '';
    79  	$scope.intervals = +search.intervals || 5;
    80  	$scope.duration = +search.duration || null;
    81  	$scope.runningHash = search.runningHash || null;
    82  	$scope.runningChanged = search.runningChanged || false;
    83  	$scope.config_text = 'Loading config...';
    84  	$scope.selected_alert = search.alert || '';
    85  	$scope.email = search.email || '';
    86  	$scope.template_group = search.template_group || '';
    87  	$scope.items = parseItems();
    88  	$scope.tab = search.tab || 'results';
    89  	$scope.aceTheme = 'chrome';
    90  	$scope.actionTypeToShow = "Acknowledged";
    91  	$scope.incidentId = 42;
    92  
    93  	$scope.aceMode = 'bosun';
    94  	$scope.expandDiff = false;
    95  	$scope.customTemplates = {};
    96  	$scope.runningChangedHelp = "The running config has been changed. This means you are in danger of overwriting someone else's changes. To view the changes open the 'Save Dialogue' and you will see a unified diff. The only way to get rid of the error panel is to open a new instance of the rule editor and copy your changes into it. You are still permitted to save without doing this, but then you must be very careful not to overwrite anyone else's changes.";
    97  
    98  	$scope.sectionToDocs = {
    99  		"alert": "https://bosun.org/definitions#alert-definitions",
   100  		"template": "https://bosun.org/definitions#templates",
   101  		"lookup": "https://bosun.org/definitions#lookup-tables",
   102  		"notification": "https://bosun.org/definitions#notifications",
   103  		"macro": "https://bosun.org/definitions#macros"
   104  	}
   105  
   106  	var expr = search.expr;
   107  	function buildAlertFromExpr() {
   108  		if (!expr) return;
   109  		var newAlertName = "test";
   110  		var idx = 1;
   111  		//find a unique alert name
   112  		while ($scope.items["alert"].indexOf(newAlertName) != -1 || $scope.items["template"].indexOf(newAlertName) != -1) {
   113  			newAlertName = "test" + idx;
   114  			idx++;
   115  		}
   116  		var text = '\n\ntemplate ' + newAlertName + ' {\n' +
   117  			'	subject = {{.Last.Status}}: {{.Alert.Name}} on {{.Group.host}}\n' +
   118  			'	body = `<p>Name: {{.Alert.Name}}\n' +
   119  			'	<p>Tags:\n' +
   120  			'	<table>\n' +
   121  			'		{{range $k, $v := .Group}}\n' +
   122  			'			<tr><td>{{$k}}</td><td>{{$v}}</td></tr>\n' +
   123  			'		{{end}}\n' +
   124  			'	</table>`\n' +
   125  			'}\n\n';
   126  		var expression = atob(expr);
   127  		var lines = expression.split("\n").map(function (l) { return l.trim(); });
   128  		lines[lines.length - 1] = "crit = " + lines[lines.length - 1]
   129  		expression = lines.join("\n    ");
   130  		text += 'alert ' + newAlertName + ' {\n' +
   131  			'	template = ' + newAlertName + '\n' +
   132  			'	' + expression + '\n' +
   133  			'}\n';
   134  		$scope.config_text += text;
   135  		$scope.items = parseItems();
   136  		$timeout(() => {
   137  			//can't scroll editor until after control is updated. Defer it.
   138  			$scope.scrollTo("alert", newAlertName);
   139  		})
   140  	}
   141  
   142  	function parseItems(): { [type: string]: string[]; } {
   143  		var configText = $scope.config_text;
   144  		var re = /^\s*(alert|template|notification|lookup|macro)\s+([\w\-\.\$]+)\s*\{/gm;
   145  		var match;
   146  		var items: { [type: string]: string[]; } = {};
   147  		items["alert"] = [];
   148  		items["template"] = [];
   149  		items["lookup"] = [];
   150  		items["notification"] = [];
   151  		items["macro"] = [];
   152  		while (match = re.exec(configText)) {
   153  			var type = match[1];
   154  			var name = match[2];
   155  			var list = items[type];
   156  			if (!list) {
   157  				list = [];
   158  				items[type] = list;
   159  			}
   160  			list.push(name);
   161  		}
   162  		return items;
   163  	}
   164  
   165  	$http.get('/api/config?hash=' + encodeURIComponent(search.hash || ''))
   166  		.success((data: any) => {
   167  			$scope.config_text = data;
   168  			$scope.items = parseItems();
   169  			buildAlertFromExpr();
   170  			if (!$scope.selected_alert && $scope.items["alert"].length) {
   171  				$scope.selected_alert = $scope.items["alert"][0];
   172  			}
   173  			$timeout(() => {
   174  				//can't scroll editor until after control is updated. Defer it.
   175  				$scope.scrollTo("alert", $scope.selected_alert);
   176  			})
   177  
   178  		})
   179  		.error(function (data) {
   180  			$scope.validationResult = "Error fetching config: " + data;
   181  		})
   182  
   183  	$scope.reparse = function () {
   184  		$scope.items = parseItems();
   185  	}
   186  	var editor;
   187  	$scope.aceLoaded = function (_editor) {
   188  		editor = _editor;
   189  		$scope.editor = editor;
   190  		editor.focus();
   191  		editor.getSession().setUseWrapMode(true);
   192  		editor.on("blur", function () {
   193  			$scope.$apply(function () {
   194  				$scope.items = parseItems();
   195  			});
   196  		});
   197  	};
   198  	var syntax = true;
   199  	$scope.aceToggleHighlight = function () {
   200  		if (syntax) {
   201  			editor.getSession().setMode();
   202  			syntax = false;
   203  			return;
   204  		}
   205  		syntax = true;
   206  		editor.getSession().setMode({
   207  			path: 'ace/mode/' + $scope.aceMode,
   208  			v: Date.now()
   209  		});
   210  	}
   211  	$scope.scrollTo = (type: string, name: string) => {
   212  		var searchRegex = new RegExp("^\\s*" + type + "\\s+" + name, "g");
   213  		editor.find(searchRegex, {
   214  			backwards: false,
   215  			wrap: true,
   216  			caseSensitive: false,
   217  			wholeWord: false,
   218  			regExp: true,
   219  		});
   220  		if (type == "alert") { $scope.selectAlert(name); }
   221  	}
   222  
   223  	$scope.scrollToInterval = (id: string) => {
   224  		document.getElementById('time-' + id).scrollIntoView();
   225  		$scope.show($scope.sets[id]);
   226  	};
   227  
   228  	$scope.show = (set: any) => {
   229  		set.show = 'loading...';
   230  		$scope.animate();
   231  		var url = '/api/rule?' +
   232  			'alert=' + encodeURIComponent($scope.selected_alert) +
   233  			'&from=' + encodeURIComponent(set.Time);
   234  		$http.post(url, $scope.config_text)
   235  			.success((data: any) => {
   236  				procResults(data);
   237  				set.Results = data.Sets[0].Results;
   238  			})
   239  			.error((error) => {
   240  				$scope.errors = [error];
   241  			})
   242  			.finally(() => {
   243  				$scope.stop();
   244  				delete (set.show);
   245  			});
   246  	};
   247  
   248  	$scope.getRunningHash = () => {
   249  		if (!$scope.saveEnabled) {
   250  			return
   251  		}
   252  		(function tick() {
   253  			$http.get('/api/config/running_hash')
   254  				.success((data: any) => {
   255  					$scope.runningHashResult = '';
   256  					$timeout(tick, 15 * 1000);
   257  					if ($scope.runningHash) {
   258  						if (data.Hash != $scope.runningHash) {
   259  							$scope.runningChanged = true;
   260  							return
   261  						}
   262  					}
   263  					$scope.runningHash = data.Hash;
   264  					$scope.runningChanged = false;
   265  				})
   266  				.error(function (data) {
   267  					$scope.runningHashResult = "Error getting running config hash: " + data;
   268  				})
   269  		})()
   270  	};
   271  
   272  	$scope.getRunningHash();
   273  
   274  
   275  	$scope.setInterval = () => {
   276  		var from = moment.utc($scope.fromDate + ' ' + $scope.fromTime);
   277  		var to = moment.utc($scope.toDate + ' ' + $scope.toTime);
   278  		if (!from.isValid() || !to.isValid()) {
   279  			return;
   280  		}
   281  		var diff = from.diff(to);
   282  		if (!diff) {
   283  			return;
   284  		}
   285  		var intervals = +$scope.intervals;
   286  		if (intervals < 2) {
   287  			return;
   288  		}
   289  		diff /= 1000 * 60;
   290  		var d = Math.abs(Math.round(diff / intervals));
   291  		if (d < 1) {
   292  			d = 1;
   293  		}
   294  		$scope.duration = d;
   295  	};
   296  	$scope.setDuration = () => {
   297  		var from = moment.utc($scope.fromDate + ' ' + $scope.fromTime);
   298  		var to = moment.utc($scope.toDate + ' ' + $scope.toTime);
   299  		if (!from.isValid() || !to.isValid()) {
   300  			return;
   301  		}
   302  		var diff = from.diff(to);
   303  		if (!diff) {
   304  			return;
   305  		}
   306  		var duration = +$scope.duration;
   307  		if (duration < 1) {
   308  			return;
   309  		}
   310  		$scope.intervals = Math.abs(Math.round(diff / duration / 1000 / 60));
   311  	};
   312  
   313  	$scope.selectAlert = (alert: string) => {
   314  		$scope.selected_alert = alert;
   315  		$location.search("alert", alert);
   316  		// Attempt to find `template = foo` in order to set up quick jump between template and alert
   317  		var searchRegex = new RegExp("^\\s*alert\\s+" + alert, "g");
   318  		var lines = $scope.config_text.split("\n");
   319  		$scope.quickJumpTarget = null;
   320  		for (var i = 0; i < lines.length; i++) {
   321  			if (searchRegex.test(lines[i])) {
   322  				for (var j = i + 1; j < lines.length; j++) {
   323  					// Close bracket at start of line means end of alert.
   324  					if (/^\s*\}/m.test(lines[j])) {
   325  						return;
   326  					}
   327  					var found = /^\s*template\s*=\s*([\w\-\.\$]+)/m.exec(lines[j]);
   328  					if (found) {
   329  						$scope.quickJumpTarget = "template " + found[1];
   330  					}
   331  				}
   332  			}
   333  		}
   334  	}
   335  
   336  	$scope.quickJump = () => {
   337  		var parts = $scope.quickJumpTarget.split(" ");
   338  		if (parts.length != 2) { return; }
   339  		$scope.scrollTo(parts[0], parts[1]);
   340  		if (parts[0] == "template" && $scope.selected_alert) {
   341  			$scope.quickJumpTarget = "alert " + $scope.selected_alert;
   342  		}
   343  	}
   344  
   345  	$scope.setTemplateGroup = (group) => {
   346  		var match = group.match(/{(.*)}/);
   347  		if (match) {
   348  			$scope.template_group = match[1];
   349  		}
   350  	}
   351  
   352  	$scope.setNotificationToShow = (n:string)=>{
   353  		$scope.notificationToShow = n;
   354  	}
   355  	
   356  	var line_re = /test:(\d+)/;
   357  	$scope.validate = () => {
   358  		$http.post('/api/config_test', $scope.config_text)
   359  			.success((data: any) => {
   360  				if (data == "") {
   361  					$scope.validationResult = "Valid";
   362  					$timeout(() => {
   363  						$scope.validationResult = "";
   364  					}, 2000)
   365  				} else {
   366  					$scope.validationResult = data;
   367  					var m = data.match(line_re);
   368  					if (angular.isArray(m) && (m.length > 1)) {
   369  						editor.gotoLine(m[1]);
   370  					}
   371  				}
   372  			})
   373  			.error((error) => {
   374  				$scope.validationResult = 'Error validating: ' + error;
   375  			});
   376  	}
   377  
   378  	$scope.test = () => {
   379  		$scope.errors = [];
   380  		$scope.running = true;
   381  		$scope.warning = [];
   382  		$location.search('fromDate', $scope.fromDate || null);
   383  		$location.search('fromTime', $scope.fromTime || null);
   384  		$location.search('toDate', $scope.toDate || null);
   385  		$location.search('toTime', $scope.toTime || null);
   386  		$location.search('intervals', String($scope.intervals) || null);
   387  		$location.search('duration', String($scope.duration) || null);
   388  		$location.search('email', $scope.email || null);
   389  		$location.search('template_group', $scope.template_group || null);
   390  		$location.search('runningHash', $scope.runningHash)
   391  		$location.search('runningChanged', $scope.runningChanged)
   392  		$scope.animate();
   393  		var from = moment.utc($scope.fromDate + ' ' + $scope.fromTime);
   394  		var to = moment.utc($scope.toDate + ' ' + $scope.toTime);
   395  		if (!from.isValid()) {
   396  			from = to;
   397  		}
   398  		if (!to.isValid()) {
   399  			to = from;
   400  		}
   401  		if (!from.isValid() && !to.isValid()) {
   402  			from = to = moment.utc();
   403  		}
   404  		var diff = from.diff(to);
   405  		var intervals;
   406  		if (diff == 0) {
   407  			intervals = 1;
   408  		} else if (Math.abs(diff) < 60 * 1000) { // 1 minute
   409  			intervals = 2;
   410  		} else {
   411  			intervals = +$scope.intervals;
   412  		}
   413  		var url = '/api/rule?' +
   414  			'alert=' + encodeURIComponent($scope.selected_alert) +
   415  			'&from=' + encodeURIComponent(from.format()) +
   416  			'&to=' + encodeURIComponent(to.format()) +
   417  			'&intervals=' + encodeURIComponent(intervals) +
   418  			'&email=' + encodeURIComponent($scope.email) +
   419  			'&incidentId=' + $scope.incidentId +
   420  			'&template_group=' + encodeURIComponent($scope.template_group);
   421  		$http.post(url, $scope.config_text)
   422  			.success((data: any) => {
   423  				$scope.sets = data.Sets;
   424  				$scope.alert_history = data.AlertHistory;
   425  				if (data.Hash) {
   426  					$location.search('hash', data.Hash);
   427  				}
   428  				procResults(data);
   429  			})
   430  			.error((error) => {
   431  				$scope.errors = [error];
   432  			})
   433  			.finally(() => {
   434  				$scope.running = false;
   435  				$scope.stop();
   436  			});
   437  	}
   438  
   439  	$scope.zws = (v: string) => {
   440  		return v.replace(/([,{}()])/g, '$1\u200b');
   441  	};
   442  
   443  	$scope.loadTimelinePanel = (entry: any, v: any) => {
   444  		if (v.doneLoading && !v.error) { return; }
   445  		v.error = null;
   446  		v.doneLoading = false;
   447  		var ak = entry.key;
   448  		var openBrack = ak.indexOf("{");
   449  		var closeBrack = ak.indexOf("}");
   450  		var alertName = ak.substr(0, openBrack);
   451  		var template = ak.substring(openBrack + 1, closeBrack);
   452  		var url = '/api/rule?' +
   453  			'alert=' + encodeURIComponent(alertName) +
   454  			'&from=' + encodeURIComponent(moment.utc(v.Time).format()) +
   455  			'&template_group=' + encodeURIComponent(template);
   456  		$http.post(url, $scope.config_text)
   457  			.success((data: any) => {
   458  				v.subject = data.Subject;
   459  				v.body = $sce.trustAsHtml(data.Body);
   460  			})
   461  			.error((error) => {
   462  				v.error = error;
   463  			})
   464  			.finally(() => {
   465  				v.doneLoading = true;
   466  			});
   467  	};
   468  
   469  	function procResults(data: any) {
   470  		$scope.subject = data.Subject;
   471  		$scope.body = $sce.trustAsHtml(data.Body);
   472  		if (data.EmailSubject) {
   473  			data.EmailSubject = atob(data.EmailSubject)
   474  		}
   475  		$scope.emailSubject = data.EmailSubject
   476  		if (data.EmailBody) {
   477  			data.EmailBody = atob(data.EmailBody)
   478  		}
   479  		$scope.emailBody = $sce.trustAsHtml(data.EmailBody)
   480  		$scope.customTemplates = {};
   481  		for (var k in data.Custom) {
   482  			$scope.customTemplates[k] = data.Custom[k];
   483  		}
   484  		var nots = {};
   485  		_(data.Notifications).each((val,n)=>{
   486  			if(val.Email){
   487  				nots["Email "+ n] = val.Email;
   488  			}
   489  			if(val.Print != ""){
   490  				nots["Print " +n] = {Print: val.Print};
   491  			}
   492  			_(val.HTTP).each((hp)=>{
   493  				nots[hp.Method+" "+n] = hp;
   494  			})
   495  		})
   496  		$scope.notifications = nots;
   497  		var aNots = {};
   498  		_(data.ActionNotifications).each((ts,n)=>{
   499  			$scope.notificationToShow = "" + n;
   500  			aNots[n] = {};
   501  			_(ts).each((val,at)=>{
   502  				if(val.Email){
   503  					aNots[n]["Email ("+at+")"] = val.Email;
   504  				}
   505  				_(val.HTTP).each((hp)=>{
   506  					aNots[n][hp.Method+" ("+at+")"] = hp;
   507  				})
   508  			})
   509  		})
   510  
   511  		$scope.actionNotifications = aNots;
   512  		$scope.data = JSON.stringify(data.Data, null, '  ');
   513  		$scope.errors = data.Errors;
   514  		$scope.warning = data.Warnings;
   515  	}
   516  
   517  	$scope.downloadConfig = () => {
   518  		var blob = new Blob([$scope.config_text], { type: "text/plain;charset=utf-8" });
   519  		saveAs(blob, "bosun.conf");
   520  	}
   521  
   522  	$scope.diffConfig = () => {
   523  		$http.post('/api/config/diff',
   524  			{
   525  				"Config": $scope.config_text,
   526  				"Message": $scope.message
   527  			})
   528  			.success((data: any) => {
   529  				$scope.diff = data || "No Diff";
   530  				// Reset running hash if there is no difference?
   531  			})
   532  			.error((error) => {
   533  				$scope.diff = "Failed to load diff: " + error;
   534  			});
   535  	}
   536  
   537  
   538  	$scope.saveConfig = () => {
   539  		if (!$scope.saveEnabled) {
   540  			return;
   541  		}
   542  		$scope.saveResult = "Saving; Please Wait"
   543  		$http.post('/api/config/save', {
   544  			"Config": $scope.config_text,
   545  			"Diff": $scope.diff,
   546  			"Message": $scope.message
   547  		})
   548  			.success((data: any) => {
   549  				$scope.saveResult = "Config Saved; Reloading";
   550  				$scope.runningHash = undefined;
   551  			})
   552  			.error((error) => {
   553  				$scope.saveResult = error;
   554  			});
   555  	}
   556  
   557  	$scope.saveClass = () => {
   558  		if ($scope.saveResult == "Saving; Please Wait") {
   559  			return "alert-warning"
   560  		}
   561  		if ($scope.saveResult == "Config Saved; Reloading") {
   562  			return "alert-success"
   563  		}
   564  		return "alert-danger"
   565  	}
   566  
   567  	return $scope;
   568  }]);
   569  
   570  // declared in FileSaver.js
   571  declare var saveAs: any;
   572  
   573  class NotificationController {
   574  	dat: any;
   575  	test = () => {
   576  		this.dat.msg = "sending"
   577  		this.$http.post('/api/rule/notification/test', this.dat)
   578  			.success((rDat: any) => {
   579  				if (rDat.Error) {
   580  					this.dat.msg = "Error: " + rDat.Error;
   581  				} else {
   582  					this.dat.msg = "Success! Status Code: " + rDat.Status;
   583  				}
   584  			})
   585  			.error((error) => {
   586  				this.dat.msg = "Error: " + error;
   587  			});
   588  	};
   589  	static $inject = ['$http'];
   590      constructor(private $http: ng.IHttpService) {
   591      }
   592  }
   593  
   594  bosunApp.component('notification', {
   595  	bindings: {
   596  		dat: "<",
   597  	},
   598  	controller: NotificationController,
   599  	controllerAs: 'ct',
   600  	templateUrl : '/static/partials/notification.html',
   601  });