Skip to content
Snippets Groups Projects
Commit 5e0693ff authored by Hahn Axel (hahn)'s avatar Hahn Axel (hahn)
Browse files

update js and css

parent c48f3d98
No related branches found
No related tags found
1 merge request!1update js and css
docs/images/screenshot_main.png

67.6 KiB | W: | H:

docs/images/screenshot_main.png

90.8 KiB | W: | H:

docs/images/screenshot_main.png
docs/images/screenshot_main.png
docs/images/screenshot_main.png
docs/images/screenshot_main.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -13,10 +13,12 @@ ...@@ -13,10 +13,12 @@
<header> <header>
<h1><span>📋</span> Appmonitor Dashboard</h1> <h1><span>📋</span> Appmonitor Dashboard</h1>
<p id="header-section"></p>
</header> </header>
<section id="tag-section"></section> <h2><span>📢</span> App status</h2>
<section id="app-section"></setion> <section id="tag-section"><h2><span></span> Please wait ...</h2></section>
<section id="app-section"></section>
</div> </div>
...@@ -26,6 +28,7 @@ ...@@ -26,6 +28,7 @@
<a href="https://github.com/iml-it/appmonitor" target="_blank">Appmonitor</a> <a href="https://github.com/iml-it/appmonitor" target="_blank">Appmonitor</a>
</footer> </footer>
<script src="javascript/ubd.class.js" type="text/javascript"></script>
<script src="javascript/inc_config.js" type="text/javascript"></script> <script src="javascript/inc_config.js" type="text/javascript"></script>
<script src="javascript/functions.js" type="text/javascript"></script> <script src="javascript/functions.js" type="text/javascript"></script>
......
/* /*
IML APPMONITOR DASHBOARD
*/ */
...@@ -16,32 +16,34 @@ const AM_RESULTS={ ...@@ -16,32 +16,34 @@ const AM_RESULTS={
}; };
const AM_ICONS={ const AM_ICONS={
'wait': '', 'connect': '🔌',
'tag': '🏷️', 'tag': '🏷️',
'app': '🪧',
'delete': '', 'delete': '',
'result0': '',
'result1': '',
'result2': '',
'result3': '🔴',
}; };
const OUT_ID_MAIN='header-section';
const OUT_ID_APPS='app-section'; const OUT_ID_APPS='app-section';
const OUT_ID_TAGS='tag-section'; const OUT_ID_TAGS='tag-section';
const ID_TAGINPUT='E_TAGS'; const ID_TAGINPUT='E_TAGS';
// callback object after changing a tag filter
const FILTER_CALLBACK="oUbdApps.update()";
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// VARS // VARS
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
var AM_TAGURL=false; var AM_TAGURL=false;
var AM_PRETTYURL=false; var AM_PRETTYURL=false;
var AM_TIMER=false;
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// FUNCTIONS // FUNCTIONS
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
/**
* generate a api url to the appmonitor
* @param {string} sPath
* @returns
*/
function _getAMApiUrl(sPath){ function _getAMApiUrl(sPath){
return AM_PRETTYURL return AM_PRETTYURL
? AM_SERVER_URL+sPath ? AM_SERVER_URL+sPath
...@@ -62,7 +64,15 @@ function _getUrlForTags(){ ...@@ -62,7 +64,15 @@ function _getUrlForTags(){
* @param {string} tags list of tags - separated by comma * @param {string} tags list of tags - separated by comma
* @returns * @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'); return _getAMApiUrl('/v1/apps/tags/'+tags+'/all');
} }
...@@ -89,10 +99,15 @@ function _2digits(i){ ...@@ -89,10 +99,15 @@ function _2digits(i){
// FUNCTIONS :: TAGS // FUNCTIONS :: TAGS
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
/**
* toggle a tag
* callback function of a button with a tagname
* @param {string} tagname
*/
function tagToggle(tagname){ function tagToggle(tagname){
var o=document.getElementById(ID_TAGINPUT); var o=document.getElementById(ID_TAGINPUT);
var s=o.value; var sLast=o.value;
var s=sLast;
var re = new RegExp(",*"+tagname); var re = new RegExp(",*"+tagname);
var s2=s.replace(re, ""); var s2=s.replace(re, "");
if(s2!==s) { if(s2!==s) {
...@@ -101,78 +116,53 @@ function tagToggle(tagname){ ...@@ -101,78 +116,53 @@ function tagToggle(tagname){
} else { } else {
s+=(s ? "," : "" ) + tagname; s+=(s ? "," : "" ) + tagname;
} }
if(s!==sLast){
o.value=s; o.value=s;
getAppstatus(); // execute update of the app list
eval(FILTER_CALLBACK);
}
} }
/**
* clear all tags
* callback function of the [x] button
*/
function tagClear(){ function tagClear(){
var o=document.getElementById(ID_TAGINPUT); var o=document.getElementById(ID_TAGINPUT);
o.value=""; o.value="";
getAppstatus();
// execute update of the app list
eval(FILTER_CALLBACK);
} }
/** /**
* called from getTags * called from getTags
* @param {string} sData JSOM Response * @param {object} aData JSON Response
* @returns * @returns
*/ */
function _getTaglist(sData){ function _getTaglist(aData){
var sReturn=''; var sReturn='';
var sTags=''; var sTags='';
let aData=JSON.parse(sData); if(!aData['tags']){
aData=JSON.parse(aData);
}
// sReturn+='<code>'+sData+'</code><br>'; // sReturn+='<code>'+sData+'</code><br>';
sReturn+='<input id="'+ID_TAGINPUT+'" type="text" size="20" value="'+AM_TAGS+'"' sReturn+='<input id="'+ID_TAGINPUT+'" type="text" size="20" value="'+AM_TAGS+'"'
+' onkeypress="getAppstatus()"' +' onkeyup="eval(FILTER_CALLBACK)"'
+' onkeydown="getAppstatus()"' +' onchange="eval(FILTER_CALLBACK)"'
+' onkeyup="getAppstatus()"'
+'>' +'>'
+'<button onclick="tagClear();return false;"> ❌ </button><br>' +'<button onclick="tagClear();return false;"> ❌ </button><br>'
; ;
for (var s in aData['tags']){ for (var s in aData['tags']){
sTags+=(sTags ? ",": "") + aData['tags'][s]; 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; 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 // FUNCTIONS :: APPS
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
...@@ -182,19 +172,19 @@ function _getTaglist(sData){ ...@@ -182,19 +172,19 @@ function _getTaglist(sData){
* @param {string} sData response body from api request (JSON string) * @param {string} sData response body from api request (JSON string)
* @returns string * @returns string
*/ */
function _getAllAppsStatus(sData){ function _getAllAppsStatus(aAllData){
var sReturn=""; var sReturn="";
var oDate=new Date; var oDate=new Date;
// sReturn+='<code>'+sData+'</code><br>'; // sReturn+='<code>'+sData+'</code><br>';
sReturn+='' sReturn+=''
+'<h3>Tags: ' + tags + '</h3>' // +'<h3>Tags: ' + tags + '</h3>'
+ '<p>' // + '<p>'
+_2digits(oDate.getHours()) +_2digits(oDate.getHours())
+':'+_2digits(oDate.getMinutes()) +':'+_2digits(oDate.getMinutes())
+':'+_2digits(oDate.getSeconds()) +':'+_2digits(oDate.getSeconds())
+' (update every '+REFRESHTIME+' sec)' +' (update every '+REFRESHTIME+' sec)'
+'</p>'; // +'</p>';
let aAllData=JSON.parse(sData); // let aAllData=JSON.parse(sData);
for (var key in aAllData){ for (var key in aAllData){
sReturn+=_getSingleAppStatus(aAllData[key]); sReturn+=_getSingleAppStatus(aAllData[key]);
} }
...@@ -216,13 +206,17 @@ function toggleAppDetails(oLink){ ...@@ -216,13 +206,17 @@ function toggleAppDetails(oLink){
* @returns string * @returns string
*/ */
function _getSingleAppStatus(aData){ function _getSingleAppStatus(aData){
// let aData=JSON.parse(sData);
if(!aData.result){
return '';
}
// DEBUG // DEBUG
// console.log("----- _getSingleAppStatus")
// console.log(aData); // console.log(aData);
// ------ checks // ------ checks
let sChecks=''; let sChecks='';
if(aData.checks) {
for (var j=0; j<aData.checks.length; j++){ for (var j=0; j<aData.checks.length; j++){
let tmpCheck=aData.checks[j]; let tmpCheck=aData.checks[j];
sChecks+='<tr class="result'+tmpCheck.result+'">' sChecks+='<tr class="result'+tmpCheck.result+'">'
...@@ -232,14 +226,15 @@ function _getSingleAppStatus(aData){ ...@@ -232,14 +226,15 @@ function _getSingleAppStatus(aData){
+'</tr>' +'</tr>'
; ;
} }
sChecks=sChecks ? '<table>'+sChecks+'</table>' : ' (No checks were found)'; }
sChecks=sChecks ? '<table class="checks">'+sChecks+'</table>' : ' (No checks were found)';
// ----- generate output // ----- 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);">' +'<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>' +'<span class="float-right url">'+aData.result.url+'</span>'
+aData.meta.website +aData.result.website
+'</div>' +'</div>'
; ;
...@@ -247,15 +242,16 @@ function _getSingleAppStatus(aData){ ...@@ -247,15 +242,16 @@ function _getSingleAppStatus(aData){
let iAge=Math.round((new Date()).getTime() / 1000)- aData.result.ts; let iAge=Math.round((new Date()).getTime() / 1000)- aData.result.ts;
sReturn+='' sReturn+=''
+'<table class="details" style="display: none;">' +'<div class="details" style="display: none;">'
+_appItem('Summary', 'Application status: '+ AM_RESULTS[aData.meta.result] +'<br><table>'
+ ' | Checks: ' + aData.checks.length +_appItem('Summary', 'Application status: '+ AM_RESULTS[aData.result.result]
// + ' | Checks: ' + aData.checks.length
+ ' | Age: ' + iAge + ' sec' + ' | Age: ' + iAge + ' sec'
+ ' | TTL: ' + aData.result.ttl + ' | TTL: ' + aData.result.ttl
) )
+_appItem('Checks', sChecks) +_appItem('Checks', sChecks)
+'</table>' +'</table></div>'
; ;
sReturn+='</div>'; sReturn+='</div>';
...@@ -263,62 +259,48 @@ function _getSingleAppStatus(aData){ ...@@ -263,62 +259,48 @@ function _getSingleAppStatus(aData){
return sReturn; 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 // MAIN
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// for auth header with basic auth // for auth header with basic auth
let AM_AUTH=(AM_USER) let oHeaders=(AM_USER)
? { "Authorization": "Basic " + btoa(AM_USER + ":" + AM_PASSWORD) } ? { "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_MAIN).innerHTML=''+AM_ICONS['connect']+' Connected instance <a href="'+AM_SERVER_URL+'">'+AM_SERVER_URL+'</a>';
document.getElementById(OUT_ID_APPS).innerHTML='<h2><span>'+AM_ICONS['wait']+'</span> wait ...</h2>';
// 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(); // cyclic updates of the app status view
AM_TIMER=window.setTimeout("getAppstatus()", 500); window.setInterval("oUbdApps.update();", REFRESHTIME*1000);
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
\ No newline at end of file
...@@ -2,7 +2,7 @@ const AM_SERVER_URL='https://appmonitor.example.com/api'; ...@@ -2,7 +2,7 @@ const AM_SERVER_URL='https://appmonitor.example.com/api';
const AM_TAGS='live,myapp'; const AM_TAGS='live,myapp';
// optional: BASIC AUTH // optional: BASIC AUTH
// const AM_USER='api'; const AM_USER='';
// const AM_PASSWORD='password-of-api-user'; const AM_PASSWORD='';
const REFRESHTIME=30; // in sec const REFRESHTIME=30; // in sec
/**
* ======================================================================
*
* 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
:root{ :root{
--color-0: #345; --color-0: #345;
--color-h1: #c54; --color-h1: #c54;
...@@ -21,20 +19,21 @@ body{ ...@@ -21,20 +19,21 @@ body{
font-family: verdana,arial; 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{ 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, #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;} button:active{ border:1px solid #fc2;}
input{ border:1px solid #ccc; padding: 0.4em;} 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;} 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)} h1{color: var(--color-h1); border-bottom: 1px solid; background: linear-gradient(#fff, #f0f4f8);}
h2{color: var(--color-h2); margin-left: -1em;} h2{color: var(--color-h2); margin-left: 0em;}
h1>span, h2>span{font-size: 200%;} h1>span, h2>span{font-size: 200%;}
section{ section{
margin: 0 0 2em ; margin: 0 0 2em ;
padding: 0.2em 2em; padding: 1em 2em;
border-top: 0px solid #abc; border-top: 0px dashed #e0e4f0;
background: linear-gradient(10deg, #fff, #f0f4f8, #fff);
border-radius: 1em; border-radius: 1em;
} }
...@@ -45,9 +44,10 @@ td{vertical-align: top;} ...@@ -45,9 +44,10 @@ td{vertical-align: top;}
border: 3px solid #bbb; border: 3px solid #bbb;
border-radius: 1em; border-radius: 1em;
box-shadow: 0 0 3em rgba(0,0,0,0.2); box-shadow: 0 0 3em rgba(0,0,0,0.2);
margin: 1em 5% 6em; margin: 1em auto 6em;
padding: 1em; padding: 1em;
min-height: 35em; min-height: 35em;
max-width: 80em;
} }
.app{ .app{
...@@ -62,9 +62,10 @@ td{vertical-align: top;} ...@@ -62,9 +62,10 @@ td{vertical-align: top;}
.app .title{font-weight: bold; font-size: 130%; cursor: pointer;} .app .title{font-weight: bold; font-size: 130%; cursor: pointer;}
.app .url{font-size: 80%; font-weight: normal; margin-right: 2em;} .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;} .result1{background:#eee !important; background: linear-gradient(#eee,#ddd)!important; color:#666;}
.result2{background:#fff8d0 !important; background: linear-gradient(#fff0d0,#ffe0a0)!important; color:#870;} .result2{background:#fff8d0 !important; background: linear-gradient(#fff0d0,#ffe0a0)!important; color:#870;}
.result3{background:#fcd !important; background: linear-gradient(#fcd,#faa)!important; color:#800;} .result3{background:#fcd !important; background: linear-gradient(#fcd,#faa)!important; color:#800;}
table.checks{border: 2px solid rgba(0,0,0,0.1);}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment