diff --git a/docs/images/screenshot_main.png b/docs/images/screenshot_main.png index 7825f38a784c6b8f57ddaea7e16d5007d849ba7b..897e4b1215fcf16e412e7be6506f450d9c126e5b 100644 Binary files a/docs/images/screenshot_main.png and b/docs/images/screenshot_main.png differ diff --git a/public_html/index.html b/public_html/index.html index b1f5963c84e31944a24e715e8bcde51d034f5f9c..49055b1d9ec4183a997bd5845651477c95626180 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -13,10 +13,12 @@ <header> <h1><span>📋</span> Appmonitor Dashboard</h1> + <p id="header-section"></p> </header> - - <section id="tag-section"></section> - <section id="app-section"></setion> + + <h2><span>📢</span> App status</h2> + <section id="tag-section"><h2><span>⏳</span> Please wait ...</h2></section> + <section id="app-section"></section> </div> @@ -26,6 +28,7 @@ <a href="https://github.com/iml-it/appmonitor" target="_blank">Appmonitor</a> </footer> + <script src="javascript/ubd.class.js" type="text/javascript"></script> <script src="javascript/inc_config.js" type="text/javascript"></script> <script src="javascript/functions.js" type="text/javascript"></script> diff --git a/public_html/javascript/functions.js b/public_html/javascript/functions.js index ea86bb88ad9c1665801cc3faf8206bca109e9b82..69497a179787593fdd88ea0bcbe6df4cb32bdf21 100644 --- a/public_html/javascript/functions.js +++ b/public_html/javascript/functions.js @@ -1,6 +1,6 @@ /* - + IML APPMONITOR DASHBOARD */ @@ -16,32 +16,34 @@ const AM_RESULTS={ }; const AM_ICONS={ - 'wait': '⏳', + 'connect': '🔌', 'tag': '🏷️', - 'app': '🪧', 'delete': '❌', - 'result0': '✔', - 'result1': '❔', - 'result2': '❗', - 'result3': '🔴', - }; +const OUT_ID_MAIN='header-section'; const OUT_ID_APPS='app-section'; const OUT_ID_TAGS='tag-section'; const ID_TAGINPUT='E_TAGS'; +// callback object after changing a tag filter +const FILTER_CALLBACK="oUbdApps.update()"; + // ---------------------------------------------------------------------- // VARS // ---------------------------------------------------------------------- var AM_TAGURL=false; var AM_PRETTYURL=false; -var AM_TIMER=false; // ---------------------------------------------------------------------- // FUNCTIONS // ---------------------------------------------------------------------- +/** + * generate a api url to the appmonitor + * @param {string} sPath + * @returns + */ function _getAMApiUrl(sPath){ return AM_PRETTYURL ? AM_SERVER_URL+sPath @@ -62,7 +64,15 @@ function _getUrlForTags(){ * @param {string} tags list of tags - separated by comma * @returns */ -function _getUrlWithTags(tags){ +function _getUrlWithTags(){ + var o=document.getElementById(ID_TAGINPUT); + var tags=""; + if(o) { + tags+=o.value + } else { + tags+=AM_TAGS + }; + return _getAMApiUrl('/v1/apps/tags/'+tags+'/all'); } @@ -89,10 +99,15 @@ function _2digits(i){ // FUNCTIONS :: TAGS // ---------------------------------------------------------------------- - +/** + * toggle a tag + * callback function of a button with a tagname + * @param {string} tagname + */ function tagToggle(tagname){ var o=document.getElementById(ID_TAGINPUT); - var s=o.value; + var sLast=o.value; + var s=sLast; var re = new RegExp(",*"+tagname); var s2=s.replace(re, ""); if(s2!==s) { @@ -101,78 +116,53 @@ function tagToggle(tagname){ } else { s+=(s ? "," : "" ) + tagname; } - o.value=s; - getAppstatus(); + + if(s!==sLast){ + o.value=s; + // execute update of the app list + eval(FILTER_CALLBACK); + } } +/** + * clear all tags + * callback function of the [x] button + */ function tagClear(){ var o=document.getElementById(ID_TAGINPUT); o.value=""; - getAppstatus(); + + // execute update of the app list + eval(FILTER_CALLBACK); } /** * called from getTags - * @param {string} sData JSOM Response + * @param {object} aData JSON Response * @returns */ -function _getTaglist(sData){ +function _getTaglist(aData){ var sReturn=''; var sTags=''; - let aData=JSON.parse(sData); + if(!aData['tags']){ + aData=JSON.parse(aData); + } // sReturn+='<code>'+sData+'</code><br>'; sReturn+='<input id="'+ID_TAGINPUT+'" type="text" size="20" value="'+AM_TAGS+'"' - +' onkeypress="getAppstatus()"' - +' onkeydown="getAppstatus()"' - +' onkeyup="getAppstatus()"' - +'>' + +' onkeyup="eval(FILTER_CALLBACK)"' + +' onchange="eval(FILTER_CALLBACK)"' + +'>' +'<button onclick="tagClear();return false;"> ❌ </button><br>' ; for (var s in aData['tags']){ sTags+=(sTags ? ",": "") + aData['tags'][s]; - sReturn+='<button onclick="tagToggle(\''+aData['tags'][s]+'\'); return false;">'+aData['tags'][s]+'</button>' + sReturn+='<button onclick="tagToggle(\''+aData['tags'][s]+'\'); return false;">'+AM_ICONS['tag'] + ' ' + aData['tags'][s]+'</button>' } return sReturn; } - - -/** - * fetch appmonitor api - taglist - * called from getAppstatus - */ - async function getTags(){ - AM_TAGURL=_getUrlForTags(); - - let out = '<h2><span>'+AM_ICONS['tag']+'</span> Tags</h2>'; - - try{ - let response = await fetch(AM_TAGURL, { "headers": - AM_AUTH - } ); - - // let response = await fetch(AM_TAGURL); - if (response.ok) { - out+=_getTaglist(await response.text()); - } else { - out+='<div class="app result1">' - +'ERROR '+response.status+': '+response.statusText + ' - ' - +AM_TAGURL - +'</div>'; - } - } catch { - out+='<div class="app result1">' - +'UNKNOWN: no response from ' - +AM_TAGURL - +'</div>'; - } - - document.getElementById(OUT_ID_TAGS).innerHTML=out; -} - - // ---------------------------------------------------------------------- // FUNCTIONS :: APPS // ---------------------------------------------------------------------- @@ -182,19 +172,19 @@ function _getTaglist(sData){ * @param {string} sData response body from api request (JSON string) * @returns string */ -function _getAllAppsStatus(sData){ +function _getAllAppsStatus(aAllData){ var sReturn=""; var oDate=new Date; // sReturn+='<code>'+sData+'</code><br>'; sReturn+='' - +'<h3>Tags: ' + tags + '</h3>' - + '<p>' + // +'<h3>Tags: ' + tags + '</h3>' + // + '<p>' +_2digits(oDate.getHours()) +':'+_2digits(oDate.getMinutes()) +':'+_2digits(oDate.getSeconds()) +' (update every '+REFRESHTIME+' sec)' - +'</p>'; - let aAllData=JSON.parse(sData); + // +'</p>'; + // let aAllData=JSON.parse(sData); for (var key in aAllData){ sReturn+=_getSingleAppStatus(aAllData[key]); } @@ -216,30 +206,35 @@ function toggleAppDetails(oLink){ * @returns string */ function _getSingleAppStatus(aData){ - // let aData=JSON.parse(sData); + if(!aData.result){ + return ''; + } // DEBUG + // console.log("----- _getSingleAppStatus") // console.log(aData); // ------ checks let sChecks=''; - for (var j=0; j<aData.checks.length; j++){ - let tmpCheck=aData.checks[j]; - sChecks+='<tr class="result'+tmpCheck.result+'">' - +'<td>'+tmpCheck.name+'</td>' - +'<td>'+tmpCheck.description+'</td>' - +'<td>'+tmpCheck.value+'</td>' - +'</tr>' - ; + if(aData.checks) { + for (var j=0; j<aData.checks.length; j++){ + let tmpCheck=aData.checks[j]; + sChecks+='<tr class="result'+tmpCheck.result+'">' + +'<td>'+tmpCheck.name+'</td>' + +'<td>'+tmpCheck.description+'</td>' + +'<td>'+tmpCheck.value+'</td>' + +'</tr>' + ; + } } - sChecks=sChecks ? '<table>'+sChecks+'</table>' : ' (No checks were found)'; + sChecks=sChecks ? '<table class="checks">'+sChecks+'</table>' : ' (No checks were found)'; // ----- generate output - let sReturn='<div class="app result'+ aData.meta.result +'">' + let sReturn='<div class="app result'+ aData.result.result +'">' +'<div class="title" onclick="toggleAppDetails(this);">' - +'<span class="float-right">'+AM_RESULTS[aData.meta.result]+'</span>' + +'<span class="float-right">'+AM_RESULTS[aData.result.result]+'</span>' +'<span class="float-right url">'+aData.result.url+'</span>' - +aData.meta.website + +aData.result.website +'</div>' ; @@ -247,15 +242,16 @@ function _getSingleAppStatus(aData){ let iAge=Math.round((new Date()).getTime() / 1000)- aData.result.ts; sReturn+='' - +'<table class="details" style="display: none;">' - +_appItem('Summary', 'Application status: '+ AM_RESULTS[aData.meta.result] - + ' | Checks: ' + aData.checks.length + +'<div class="details" style="display: none;">' + +'<br><table>' + +_appItem('Summary', 'Application status: '+ AM_RESULTS[aData.result.result] + // + ' | Checks: ' + aData.checks.length + ' | Age: ' + iAge + ' sec' + ' | TTL: ' + aData.result.ttl ) +_appItem('Checks', sChecks) - +'</table>' + +'</table></div>' ; sReturn+='</div>'; @@ -263,62 +259,48 @@ function _getSingleAppStatus(aData){ return sReturn; } -/** - * fetch appmonitor api - status of apps - */ -async function getAppstatus(){ - - - var o=document.getElementById(ID_TAGINPUT); - tags=o ? o.value : AM_TAGS; - - let out = '<h2><span>'+AM_ICONS['app']+'</span> Applications</h2>'; - let apiurl=_getUrlWithTags(tags) - // let myfunction="_getAllAppsStatus"; - try{ - let response = await fetch(apiurl, { "headers": - AM_AUTH - } ); - if (response.ok) { - - out+=_getAllAppsStatus(await response.text()); - // out+=eval(myfunction+'(await '+response.text()+')'); - - } else { - out+='<div class="app result1">' - +'ERROR '+response.status+': '+response.statusText + ' - ' - +apiurl - +'</div>'; - } - } - catch { - out+='<div class="app result1">' - +'UNKNOWN: no response from ' - +apiurl - +'</div>'; - } - // } - document.getElementById(OUT_ID_APPS).innerHTML=out; - clearTimeout(AM_TIMER); - AM_TIMER=window.setTimeout("getAppstatus()", REFRESHTIME*1000); -} - - // ---------------------------------------------------------------------- // MAIN // ---------------------------------------------------------------------- // for auth header with basic auth -let AM_AUTH=(AM_USER) +let oHeaders=(AM_USER) ? { "Authorization": "Basic " + btoa(AM_USER + ":" + AM_PASSWORD) } : {} ; -document.getElementById(OUT_ID_TAGS).innerHTML='<h2><span>'+AM_ICONS['wait']+'</span> wait ...</h2>'; -document.getElementById(OUT_ID_APPS).innerHTML='<h2><span>'+AM_ICONS['wait']+'</span> wait ...</h2>'; +document.getElementById(OUT_ID_MAIN).innerHTML=''+AM_ICONS['connect']+' Connected instance <a href="'+AM_SERVER_URL+'">'+AM_SERVER_URL+'</a>'; + +// initialize tags +var oUbdTag=new ubd( + { + 'domid': OUT_ID_TAGS, + 'url': _getUrlForTags(), + 'header': { "headers": oHeaders }, + 'renderer': _getTaglist, + 'ttl': 0, + } +); + +// initialize visible apps +var oUbdApps=new ubd( + { + 'domid': OUT_ID_APPS, + 'url': _getUrlWithTags, // remark: this is a function + 'header': { "headers": oHeaders }, + 'renderer': _getAllAppsStatus, + 'ttl': 0, + } +); + + + +// fill in initial values +oUbdTag.update(); +oUbdApps.update(); -getTags(); -AM_TIMER=window.setTimeout("getAppstatus()", 500); +// cyclic updates of the app status view +window.setInterval("oUbdApps.update();", REFRESHTIME*1000); // ---------------------------------------------------------------------- \ No newline at end of file diff --git a/public_html/javascript/inc_config.js.dist b/public_html/javascript/inc_config.js.dist index 4fe918905914b39d5889ac98c0dda57e6963e1d5..fb490522c81e494072c063dc6741e2e062ed113b 100644 --- a/public_html/javascript/inc_config.js.dist +++ b/public_html/javascript/inc_config.js.dist @@ -2,7 +2,7 @@ const AM_SERVER_URL='https://appmonitor.example.com/api'; const AM_TAGS='live,myapp'; // optional: BASIC AUTH -// const AM_USER='api'; -// const AM_PASSWORD='password-of-api-user'; +const AM_USER=''; +const AM_PASSWORD=''; const REFRESHTIME=30; // in sec diff --git a/public_html/javascript/ubd.class.js b/public_html/javascript/ubd.class.js new file mode 100644 index 0000000000000000000000000000000000000000..49f54a4c86762f48bc34456ef0d4b7a3fc68bab5 --- /dev/null +++ b/public_html/javascript/ubd.class.js @@ -0,0 +1,197 @@ +/** + * ====================================================================== + * + * U B D + * + * Url binded to a dom id + * + * ---------------------------------------------------------------------- + * + * This is a helper class to update the content of a dom object by a + * given Url after a ttl. + * + * ---------------------------------------------------------------------- + * 2022-06-26 www.axel-hahn.de first lines... + * ====================================================================== + */ + +var ubd = function(){ + + this._sDomId=''; + this._oDomObject=false; + this._sUrl2Fetch=false; // static value or reference of a function + this._oHeader={}; + this._sRenderfunction=false; + this._iTTL=false; + + this._oTimer=false; + + this._body=''; + + /** + * initialize data for a dom object + * @parm {object} oConfig optional config object with those subkeys: + * domid - id of a dom object + * url - url to an api + * header - http request header data + * renderer - renderer function to visualize data + * ttl - ttl in sec (TODO) + * @returns {undefined} + */ + this.init = function(oConfig){ + if(oConfig['domid']){ + this.setDomid(oConfig['domid']); + } + if(oConfig['url']){ + this.setUrl(oConfig['url']); + } + if(oConfig['header']){ + this.setHeaders(oConfig['header']); + } + if(oConfig['renderer']){ + this.setRenderfunction(oConfig['renderer']); + } + if(oConfig['ttl']){ + this.setTtl(oConfig['ttl']); + } + }, + + // ---------------------------------------------------------------------- + // public SETTER for properties + // ---------------------------------------------------------------------- + + /** + * set domid that will by updated + * @param {string} sDomid if of a domobject + */ + this.setDomid = function(sDomid){ + if (document.getElementById(sDomid)){ + this._sDomId=sDomid; + this._oDomObject=document.getElementById(sDomid); + } else { + this._sDomId=false; + this._oDomObject=false; + console.error('ERROR: setDomid("'+sDomid+'") got an invalid string - this domid does not exist.'); + } + }, + + + /** + * set a rendering function that visualized data after a http request + * @param {string|function} oFunction + */ + this.setRenderfunction = function(oFunction){ + this._sRenderfunction=oFunction; + }, + + /** + * Set time to live in seconds + * @param {int} iTTL + */ + this.setTtl = function(iTTL){ + this._iTTL=iTTL/1; + this.resetTimer(); + }, + + /** + * set an url to be requested + * @param {string|function} sUrl static value or reference of a function + */ + this.setUrl = function(sUrl){ + this._sUrl2Fetch=sUrl; + }, + + /** + * set header obejct for 2nd param in javascript fetch() function + * @param {object} oHeader + */ + this.setHeaders = function(oHeader){ + this._oHeader=oHeader; + }, + + /** + * helper: dump current object instance to console + */ + this.dumpme = function(){ + console.log('---------- DUMP ubd'); + console.log(this); + console.log('---------- /DUMP ubd'); + }, + // ---------------------------------------------------------------------- + // public ACTIONS + // ---------------------------------------------------------------------- + + /** + * show rendered html content into set domid using the render function + * Optionally you can set a string to display an error message. + * + * @param {string} sHtml optional: htmlcode of an error message + */ + this.render = function(sHtml) { + let out = sHtml ? sHtml : this._sRenderfunction(this._body); + this._oDomObject.innerHTML=out; + }, + + /** + * reset timer to update the content in dom id after reaching TTL + * used in setTtl + * + * WIP: repeating the update braks out from current instance. + * But what works is + * var oUbd=new ubd(...) + * by setting ttl = 0 and + * window.setInterval("oUbd.update();", 3000); + */ + this.resetTimer = function(){ + clearTimeout(this._oTimer); + // clearInterval(this._oTimer); + if(this._iTTL) { + let self = this; + self._oTimer=window.setTimeout(self.update, this._iTTL*1000); + // self._oTimer=window.setInterval(self.update, this._iTTL*1000); + } + }, + + /** + * make http request and call the renderer + */ + this.update = async function(){ + let self = this; + let url=( typeof this._sUrl2Fetch == "function" ) ? this._sUrl2Fetch() : this._sUrl2Fetch; + console.log("update from url [" + url + "]"); + if (url == undefined){ + console.error("SKIP update - there is no url in this object instance (anymore) :-/"); + this.dumpme(); + return 0; + } + try{ + let response = await fetch(url, self._oHeader); + if (response.ok) { + self._body=await response.json(); + + this.render(); + } else { + this.render('<div class="app result1">' + +'ERROR '+response.status+': '+response.statusText + ' - ' + +url + +'</div>'); + } + } catch(e) { + this.render('<div class="app result1">' + +'UNKNOWN: no response from ' + +this._sUrl2Fetch + +'</div>'); + console.error(e); + } + + } + + // ---------------------------------------------------------------------- + // MAIN + // ---------------------------------------------------------------------- + + if (arguments) { + this.init(arguments[0]); + } + +}; \ No newline at end of file diff --git a/public_html/main.css b/public_html/main.css index 341a7a0a2705c7ca33aef67df8409166e12d5a89..d07dc7917e6a7d93e0ba8ba1f0a45d1b91bcf40d 100644 --- a/public_html/main.css +++ b/public_html/main.css @@ -1,5 +1,3 @@ - - :root{ --color-0: #345; --color-h1: #c54; @@ -21,20 +19,21 @@ body{ font-family: verdana,arial; } -button{ border: 1px solid rgba(0,0,0,0.05); background: linear-gradient(#f8f8f8, #eee); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;} -button:hover{ background: linear-gradient(#eee, #ddd); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;} +button{ border: 1px solid rgba(0,0,0,0.1); background: linear-gradient(#f8f8f8, #ddd); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;} +button:hover{ background: linear-gradient(#eee, #ccc); border-radius: 0.3em; margin: 0 0.5em 0.5em 0; padding: 0.5em;} button:active{ border:1px solid #fc2;} input{ border:1px solid #ccc; padding: 0.4em;} -footer{ position: fixed; bottom: 0; left: 0; width: 100%; padding: 1em; background: var(--bg-footer); text-align: center;} -h1{color: var(--color-h1)} -h2{color: var(--color-h2); margin-left: -1em;} +footer{ position: fixed; bottom: 0; left: 0; width: 100%; padding: 1em; background: var(--bg-footer); border-top: 2px solid rgba(255,255,255,0.5); text-align: center;} +h1{color: var(--color-h1); border-bottom: 1px solid; background: linear-gradient(#fff, #f0f4f8);} +h2{color: var(--color-h2); margin-left: 0em;} h1>span, h2>span{font-size: 200%;} section{ margin: 0 0 2em ; - padding: 0.2em 2em; - border-top: 0px solid #abc; + padding: 1em 2em; + border-top: 0px dashed #e0e4f0; + background: linear-gradient(10deg, #fff, #f0f4f8, #fff); border-radius: 1em; } @@ -45,9 +44,10 @@ td{vertical-align: top;} border: 3px solid #bbb; border-radius: 1em; box-shadow: 0 0 3em rgba(0,0,0,0.2); - margin: 1em 5% 6em; + margin: 1em auto 6em; padding: 1em; min-height: 35em; + max-width: 80em; } .app{ @@ -62,9 +62,10 @@ td{vertical-align: top;} .app .title{font-weight: bold; font-size: 130%; cursor: pointer;} .app .url{font-size: 80%; font-weight: normal; margin-right: 2em;} -.app .details{} -.result0{background:#dfd !important; background: linear-gradient(#dfd,#beb)!important; color:#080} +.result0{background:#dfd !important; background: linear-gradient(#ded,#beb)!important; color:#383} .result1{background:#eee !important; background: linear-gradient(#eee,#ddd)!important; color:#666;} .result2{background:#fff8d0 !important; background: linear-gradient(#fff0d0,#ffe0a0)!important; color:#870;} .result3{background:#fcd !important; background: linear-gradient(#fcd,#faa)!important; color:#800;} + +table.checks{border: 2px solid rgba(0,0,0,0.1);}