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 });