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

Merge branch 'abstract_ubd_class' into 'master'

update js and css

See merge request !1
parents c48f3d98 5e0693ff
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