<?php require_once 'cronlog.class.php'; /** * ______________________________________________________________________ * * _____ _ _ _ * / __ \ (_) | | (_) * | / \/_ __ ___ _ __ _ ___ | |__ __ ___ _____ _____ _ __ * | | | '__/ _ \| '_ \| |/ _ \| '_ \ \ \ / / |/ _ \ \ /\ / / _ \ '__| * | \__/\ | | (_) | | | | | (_) | |_) | \ V /| | __/\ V V / __/ | * \____/_| \___/|_| |_| |\___/|_.__/ \_/ |_|\___| \_/\_/ \___|_| * _/ | * |__/ * ______________________________________________________________________ * * The cronjob viewer for centralized monitoring of cronjobs using * Axels cronrwapper (see <https://github.com/axelhahn/cronwrapper>). * * You can browse all servers to see * - the last status of all cronjobs * - results an execution times of running jonbs * - a timeline for all jobs running > 60s * * * Free software. Open Source. GNU GPL 3. * SOURCE: <https://git-repo.iml.unibe.ch/iml-open-source/cronlog-viewer> * * ______________________________________________________________________ * * The class cronlog-renderer contains visual methods to render html * @see cronlog.class.php * * @license GNU GPL 3.0 * @author Axel Hahn <axel.hahn@iml.unibe.ch> */ class cronlogrenderer extends cronlog{ /** * minimal length for execution time of a job to be rendered in the timeline; value is in seconds * @var integer */ protected $_iMinTime4Timeline = 60; /** * show date of last data and last access; used in rendering methods to display it on top * @param integer $iLast unix timestamp of last log entry * @return string */ protected function _renderAccessAndAge($iLast){ if(!$iLast){ return ''; } $iAge=round((date('U')-$iLast)/60); return '<div class="accessandage">' . sprintf($this->t("request-time"), date("Y-m-d H:i:s")).'<br>' . sprintf($this->t("last-entry"), $iAge) .'</div>' ; } /** * get onclick value to filter a datatable * @param string $sDatatable id of table * @param string $sFiltertext text to filter * @return string * */ protected function _filterDatatable($sDatatable, $sFiltertext){ return '$(\'#'.$sDatatable.'\').dataTable().fnFilter(\''.$sFiltertext.'\'); return false;'; } /** * helper function to be used in methods that render datatable tables * get javascript code to be added in init options and set language specifi texts * @return string */ protected function _getDatatableLanguage(){ return $this->t("dt-USE") ? ', "oLanguage":{ "sProcessing":"'.$this->t("dt-sProcessing").'", "sLengthMenu":"'.$this->t("dt-sLengthMenu").'", "sZeroRecords":"'.$this->t("dt-sZeroRecords").'", "sInfo":"'.$this->t("dt-sInfo").'", "sInfoEmpty":"'.$this->t("dt-sInfoEmpty").'", "sInfoFiltered":"'.$this->t("dt-sInfoFiltered").'", "sInfoPostFix":"'.$this->t("dt-sInfoPostFix").'", "sSearch":"'.$this->t("dt-sSearch").'", "sUrl":"'.$this->t("dt-sUrl").'", "oPaginate":{ "sFirst":"'.$this->t("dt-sFirst").'", "sPrevious":"'.$this->t("dt-sPrevious").'", "sNext":"'.$this->t("dt-sNext").'", "sLast":"'.$this->t("dt-sLast").'" } }' : '' ; } /** * get html code for a table with events of executed cronjobs * * @param array $aData result of $this->getServerLogs() * @return string */ public function renderCronlogs($aData=false){ $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer; $sHtml=$this->_getCacheData($sTaskId); if($sHtml){ return $sHtml; } $sHtml=''; if(!$aData){ $aData=array_merge($this->getRunningJobs(), $this->getServersLastLog()); } $sTblHead=''; $iOK=0; $iErrors=0; $iLast=false; // Array ( [SCRIPTNAME] => apt-get update [SCRIPTTTL] => 1440 [SCRIPTSTARTTIME] => 2016-06-21 06:00:02, 1466481602 [SCRIPTLABEL] => apt-get [SCRIPTENDTIME] => 2016-06-21 06:00:49, 1466481649 [SCRIPTEXECTIME] => 47 s [SCRIPTRC] => 0 ) foreach($aData as $sDtakey=>$aEntry){ if(!$sTblHead){ foreach(array( $this->t("col-starting-time"), $this->t("col-label"), $this->t("col-server"), $this->t("col-duration"), $this->t("col-ttl"), $this->t("col-rc"), $this->t("col-expired"), $this->t("col-status"), ) as $sKey){ $sTblHead.='<th>'.$sKey.'</th>'; } } // $sViewerUrl='viewer.php?host='.$aEntry['host'].'&job='.$aEntry['job']; // $sClass='message-'.($aEntry['SCRIPTRC']?'error':'ok'); $iLast=max(array($iLast, date("U", $aEntry['SCRIPTSTARTTIME']))); $aErrors=array(); $iTtlUsed=max($aEntry['SCRIPTTTL'], $this->_iMinTtl); $iNextRun=$aEntry['SCRIPTSTARTTIME']+((int)$aEntry['SCRIPTTTL']*60); $iNextRunWarn=$aEntry['SCRIPTSTARTTIME']+((int)$iTtlUsed*60); $iNextRunErr=$aEntry['SCRIPTSTARTTIME']+(((int)$aEntry['SCRIPTTTL']+(int)$this->_iExpiredJobsFailAfter)*60); // ticket #5850 - check hostname vs. servername in logfile $sServerFromLogfile=preg_replace('/_.*/', '', basename($aEntry['logfile'])); if($sServerFromLogfile!=$aEntry['server']){ $aErrors[]=[ $this->t('error-host-in-log-differs-servername-label'), sprintf($this->t('error-host-in-log-differs-servername-description'),$sServerFromLogfile, $aEntry['server']), ]; } if(!strstr($sServerFromLogfile, ".")){ $aErrors[]=[ $this->t('error-no-fqdn-label'), sprintf($this->t('error-no-fqdn-description'),$sServerFromLogfile), ]; } if($iNextRunErr < date("U")){ $aErrors[]=[ $this->t('error-expired-label'), $this->t('error-expired-description'), ]; } if($aEntry['SCRIPTRC']>0){ $aErrors[]=[ sprintf($this->t('error-exitcode-label'), (int)$aEntry['SCRIPTRC']), $this->t('error-exitcode-description'), ]; } if(!$aEntry['SCRIPTLABEL']){ $aErrors[]=[ $this->t('error-no-label-label'), $this->t('error-no-label-description'), ]; } if(count($aErrors)){ $iErrors++; } else { $iOK++; } // human readable TTL value ... SCRIPTTTL is in [min] $sTtlHr=''; if((int)$aEntry['SCRIPTTTL']>60*24*3){ $sTtlHr.='='.round((int)$aEntry['SCRIPTTTL']/60/24).'d '; } else if((int)$aEntry['SCRIPTTTL']>60*3){ $sTtlHr.='='.round((int)$aEntry['SCRIPTTTL']/60).'h'; } $sTtlHr=$sTtlHr ? '('.$sTtlHr.')' : ''; $sColStatus=''; $bIsRunning=false; if($aEntry['SCRIPTRC']){ if (count($aErrors)){ foreach($aErrors as $aErr){ $sColStatus.='<li><abbr title="'.$aErr[1].'">'.$this->t('status-error').': '.$aErr[0].'</abbr></li>'; } $sColStatus='<ul>'.$sColStatus.'</ul>'; } else { $sColStatus.=$this->t('status-ok'); } } else { $bIsRunning=true; $sColStatus.=$this->t('status-running'); } // execution time of time of currently running job $iExectime=$aEntry['SCRIPTEXECTIME'] ? (int)$aEntry['SCRIPTEXECTIME'] : date('U')-$aEntry['SCRIPTSTARTTIME'] ; // render table of last logfile per cron job $sHtml.='<tr onclick="showFile(\''.$aEntry['logfile'].'\');" title="'.sprintf($this->t('row-click-show-logfile'), $aEntry['logfile']).'"' .($bIsRunning ? ' class="message-running"' : '') .'>' . '<td>'.date("Y-m-d H:i:s", $aEntry['SCRIPTSTARTTIME']).'</td>' // . '<td>'.$aEntry['SCRIPTNAME'].'</td>' . '<td>'.$aEntry['SCRIPTLABEL'].'</td>' . '<td>'.$aEntry['server'].'</td>' . '<td' .((int)$aEntry['SCRIPTEXECTIME']>$this->_iMinTime4Timeline ? ' class="message-warning"' : '' ) . '>' .'<span style="display: none">'.str_pad((int)$aEntry['SCRIPTEXECTIME'], 6, '0', STR_PAD_LEFT).'</span>' .$iExectime.'s' .($iExectime>100 ? ' ('.round($iExectime/60).'min)' : '') .'</td>' . '<td' .($aEntry['SCRIPTTTL']<$this->_iMinTtl ? ' class="message-warning" title="(using minimal TTL = '.$this->_iMinTtl.' min)"' : '') . '>' .'<span style="display: none">'.str_pad((int)$aEntry['SCRIPTTTL'], 6, '0', STR_PAD_LEFT).'</span>' .$aEntry['SCRIPTTTL'] .$sTtlHr .'</td>' . '<td class="'.($aEntry['SCRIPTRC'] ? ($aEntry['SCRIPTRC']>0 ? 'message-error' : 'message-ok') : '' ).'">' .($aEntry['SCRIPTRC'] ? $aEntry['SCRIPTRC'] : '⏳') .'</td>' . '<td class="'.($iNextRunWarn < date("U") ? ($iNextRunErr < date("U") ? 'message-error' : 'message-warning') : '' ).'">' . date("Y-m-d H:i", $iNextRun).'</td>' . '<td'.(count($aErrors) ? ' class="message-error" title=""' : '' ).'>' .$sColStatus.'</td>' // .(count($aErrors) ? 'FEHLER' : 'OK').'</td>' // . '<td><button onclick="showFile(\''.$aEntry['logfile'].'\');">Ansehen</button></td>' . '</tr>' ; } $sIdTable='datatable1'; $sHtml=' <!-- START '.__METHOD__.' --> ' . '<h3>'.$this->t('logs-head').'</h3>' . '<p class="hint">' . $this->t('logs-hint') . '</p>' . (count($aData) ? '<div>' . $this->_renderAccessAndAge($iLast) . ($iErrors ? '<a href="#" class="btn bg-danger" onclick="'.$this->_filterDatatable($sIdTable, $this->t('status-error')).'"><i class="fas fa-exclamation-circle"></i> ' . $iErrors.'</a> ' : '') . ( $iOK ? '<a href="#" class="btn bg-success" onclick="'.$this->_filterDatatable($sIdTable, $this->t('status-ok')).'"><i class="fas fa-check"></i> ' . $iOK.'</a>' : '') . ($iErrors && $iOK ? ' ... '.$this->t('total').': <a href="#" class="btn bg-gray" onclick="'.$this->_filterDatatable($sIdTable, "").'"><i class="fas fa-th-large"></i> ' . count($aData).'</a>' : '' ) . '</div><br>' . '<table id="'.$sIdTable.'" class="table-striped">' . '<thead><tr>'.$sTblHead.'</tr></thead>' . '<tbody>' .$sHtml .'</tbody>' . '</table>' . '<script>' . '$(document).ready( function () { $(\'#'.$sIdTable.'\').DataTable({ "retrieve": true, "bPaginate":false, "aaSorting":[[0,"desc"]] '.$this->_getDatatableLanguage().' }); });' . '</script>' : '' ) // init datatable . ' <!-- ENDE '.__METHOD__.' --> ' ; $this->_writeCacheData($sTaskId, $sHtml); return $sHtml; } /** * get html code for a table with events of executed cronjobs for ALL servers * * @param array $aData result of $this->getServerLogs() * @return string */ public function renderCronlogsOfAllServers(){ $aData=array(); foreach (array_keys($this->getServers()) as $sServer){ $this->setServer($sServer); $aData=array_merge($aData, $this->getRunningJobs(),$this->getServersLastLog()); } $this->setServer('ALL'); return $this->renderCronlogs($aData); } /** * get html code for a switcher of multiple instances * * @param array $aData result of $oCL->getServerLogs() * @return string */ public function renderInstances(){ if(count($this->_aInstances) < 2){ return false; } $sReturn=''; $sServer=isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : false; $sReturn.='<li class="nav-item"><a class="nav-link nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a></li>' . '<li class="nav-item d-none d-sm-inline-block"><a href="#" class="nav-link">'.$this->t('instances').':</a></li>'; foreach($this->_aInstances as $sInstance => $sUrl){ $sHost=parse_url($sUrl, PHP_URL_HOST); $sClass=($sServer && $sServer==$sHost) ? 'active bg-gray' : ''; // $sReturn.='<a class="'.$sClass.'" href="'.$sUrl.'" title="'.$sUrl.'">'.$sInstance.'</a> '; $sReturn.='<li class="nav-item d-none d-sm-inline-block '.$sClass.'"><a href="'.$sUrl.'" class="nav-link">'.$sInstance.'</a></li>'; } return $sReturn; } /** * get html code for a table with history of executed cronjobs * * @param array $aData result of $oCL->getServerLogs() * @return string */ public function renderJoblist($aData=false){ $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer; $sHtml=$this->_getCacheData($sTaskId); if($sHtml){ return $sHtml; } $sHtml=''; if(!$aData){ $aData=$this->getServerJobHistory(); } $sTblHead=''; $iOK=0; $iErrors=0; $iLast=false; // job=dok-kvm-instances:host=kalium:start=1538358001:end=1538358001:exectime=0:ttl=60:rc=0 foreach($aData as $aEntry){ if(!$sTblHead){ foreach(array( $this->t("col-starting-time"), $this->t("col-label"), $this->t("col-server"), $this->t("col-duration"), $this->t("col-ttl"), $this->t("col-rc"), $this->t("col-status"), ) as $sKey){ $sTblHead.='<th>'.$sKey.'</th>'; } } $iLast=max(array($iLast, date("U", $aEntry['start']))); $sClass='message-'.($aEntry['rc']?'error':'ok'); if($aEntry['rc']){ $iErrors++; } else { $iOK++; } $sHtml.='<tr>' . '<td>'.date("Y-m-d H:i:s", $aEntry['start']).'</td>' // . '<td>'.date("Y-m-d H:i:s", $aEntry['end']).'</td>' . '<td>'.$aEntry['job'].'</td>' . '<td>'.$aEntry['host'].'</td>' . '<td' .($aEntry['exectime']>$this->_iMinTime4Timeline ? ' class="message-warning"' : '' ) . '>' .$aEntry['exectime'].'s' .($aEntry['exectime']>100 ? ' ('.round($aEntry['exectime']/60).'min)' : '') . '</td>' . '<td>'.$aEntry['ttl'].'</td>' . '<td class="' .($aEntry['rc']>0 ? 'message-error': 'message-ok') . '">'.$aEntry['rc'].'</td>' . '<td>'.( $aEntry['rc'] ? $this->t('status-error') : $this->t('status-ok') ).'</td>' . '</tr>' ; } $sIdTable='datatable2'; $sHtml=' <!-- START '.__METHOD__.' --> ' . '<h3>'.$this->t('history-head').'</h3>' . '<p class="hint">' . $this->t('history-hint') . '</p>' . '<div>' . $this->_renderAccessAndAge($iLast) . ($iErrors ? '<a href="#" class="btn bg-danger" onclick="'.$this->_filterDatatable($sIdTable, $this->t('status-error')).'"><i class="fas fa-exclamation-circle"></i> ' . $iErrors.'</a> ' : '') . ( $iOK ? '<a href="#" class="btn bg-success" onclick="'.$this->_filterDatatable($sIdTable, $this->t('status-ok')).'"><i class="fas fa-check"></i> ' . $iOK.'</a>' : '') . ($iErrors && $iOK ? ' ... '.$this->t('total').': <a href="#" class="btn bg-gray" onclick="'.$this->_filterDatatable($sIdTable, "").'"><i class="fas fa-th-large"></i> ' . count($aData).'</a>' : '' ) . '</div>' . '<br>' . '<table id="'.$sIdTable.'">' . '<thead><tr>'.$sTblHead.'</tr></thead>' . '<tbody>' .$sHtml .'</tbody>' . '</table>' // init datatable . (count($aData) ? '<script>' . '$(document).ready( function () {$(\'#'.$sIdTable.'\').DataTable({ "retrieve": true, "aaSorting":[[0,"desc"]], "aLengthMenu":[[25,100,-1],[25,100,"---"]] '.$this->_getDatatableLanguage().' });} );' . '</script>' : '' ) . ' <!-- ENDE '.__METHOD__.' --> ' ; $this->_writeCacheData($sTaskId, $sHtml); return $sHtml; } /** * get html code for a joblist of the selected server * it uses the filter for hidden joblog entries (aHidelogs in config) * @return string */ public function renderJoblistOfAllServers(){ $aData=array(); foreach (array_keys($this->getServers()) as $sServer){ $this->setServer($sServer); $aData=array_merge($aData, $this->getServerJobHistory()); } $this->setServer('ALL'); return $this->renderJoblist($aData); } /** * get html code for a timeline with events of executed cronjobs * * TODO: * for rendering of several hosts ... see grouping in * http://visjs.org/examples/timeline/groups/groupsOrdering.html * * @param array $aData result of $oCL->getServerLogs() * @return string */ public function renderJobGraph($aData=false){ $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer; $sHtml=$this->_getCacheData($sTaskId); if($sHtml){ return $sHtml; } $sHtml=''; static $iGraphCounter; if(!isset($iGraphCounter)){ $iGraphCounter=0; } $iGraphCounter++; if(!$aData){ $aData=$this->getServerJobHistory(false); } $sDivId='vis-timeline-'.$iGraphCounter; $aDataset=array(); $iLast=false; $iEntry=0; foreach($aData as $aEntry){ if($aEntry['exectime']>$this->_iMinTime4Timeline){ $iEntry++; $iLast=max(array($iLast, date("U", $aEntry['start']))); $aDataset[]=array( 'id'=>$iEntry, /* 'start'=>(int)date("U", $aEntry['start']), 'end'=>(int)date("U", $aEntry['end']), * 'start'=>'Date'.date('Y-m-d\TH:i:s.000\Z', $aEntry['start']), 'end'=>'Date'.date('Y-m-d\TH:i:s.000\Z', $aEntry['end']), * 'start'=>date('Y-m-d\TH:i:s.000\Z', $aEntry['start']), 'end'=>date('Y-m-d\TH:i:s.000\Z', $aEntry['end']), * */ 'start'=>(int)date("U", $aEntry['start'])*1000, 'end'=>(int)date("U", $aEntry['end'])*1000, 'content'=>$aEntry['job'].'@'.$aEntry['host'], 'className'=>'timeline-result-'.($aEntry['rc'] ? 'error' : 'ok'), 'title'=>'<strong>'.$aEntry['job'].'@'.$aEntry['host'].'</strong><br>' . 'start: ' . date("Y-m-d H:i:s", $aEntry['start']).'<br>' . 'end: ' . date("Y-m-d H:i:s", $aEntry['end']).'<br>' . 'exectime: ' .$aEntry['exectime'].'s' .($aEntry['exectime']>100 ? ' ('.round($aEntry['exectime']/60).'min)' : '') . '<br>' . 'rc = ' . $aEntry['rc'].'<br>' , ); // if($iEntry>=265){break;} } } $sHtml.=' <!-- START '.__METHOD__.' --> ' . '<h3>'.$this->t('timeline-head').'</h3>' . '<p class="hint">' . sprintf($this->t('timeline-hint'), $this->_iMinTime4Timeline) . '</p> <p>' .sprintf($this->t('graph-rendered-jobs'), $this->_iMinTime4Timeline).': <strong>'.count($aDataset).'</strong> ' .'('.$this->t('total').': '.count($aData).')<br><br>' .(count($aDataset) ? $this->_renderAccessAndAge($iLast) : '' ) .'</p>' .(count($aDataset) ? '<div id="'.$sDivId.'"></div> <script type="text/javascript"> // DOM element where the Timeline will be attached var container = document.getElementById("'.$sDivId.'"); // Create a DataSet (allows two way data-binding) var items = new vis.DataSet('.json_encode($aDataset).'); // Configuration for the Timeline var options = { zoomMin: 1000 * 60 * 60, // an hour zoomMax: 1000 * 60 * 60 * 24 * 14 // 2 weeks }; // Create a Timeline var timeline = new vis.Timeline(container, items, options); // fix: some timelines do not properly work ... but I make them visible $(\'#'.$sDivId.' .vis-timeline\').css(\'visibility\', \'visible\'); </script> ' : $this->t('graph-no-data').'<br>' ) .' <!-- ENDE '.__METHOD__.'--> '; $this->_writeCacheData($sTaskId, $sHtml); return '' .$sHtml // . strlen($sHtml).' byte - ' . '<pre>'.htmlentities($sHtml).'</pre><br>' ; } public function renderJobGraphOfAllServers(){ $aData=array(); foreach (array_keys($this->getServers()) as $sServer){ $this->setServer($sServer); $aData=array_merge($aData, $this->getServerJobHistory()); } $this->setServer('ALL'); return $this->renderJobGraph($aData); } /** * used in config/page-replacements.php * generate an array of javascript lang texts */ public function renderJSLang(){ $aReturn=[]; foreach($this->_aLang as $sKey=>$sText){ if(preg_match('/^JS_/', $sKey)){ $aReturn[preg_replace('/^JS_/', '', $sKey)]=$sText; } } return "\n" .'// generated by '.__METHOD__."\n" .'var aLang='.json_encode($aReturn, JSON_PRETTY_PRINT).'; '."\n" ; } /** * show a single log file * * @param string $sLogfile logfile; [server]/[filename.log] * @return string */ public function renderLogfile($sLogfile){ $sHtml='' . '<button style="position: fixed;" onclick="closeOverlay();" class="btn btn-default"><i class="fas fa-chevron-left"></i> '.$this->t('back').'</button><br><br>' . '<h3>'. $this->t('logfile').' '.basename($sLogfile).'</h3>' ; if(!$sLogfile){ return $sHtml . $this->t('error-nologfile'); } if(strstr($sLogfile, '..')){ return $sHtml . $this->t('error-dots-not-allowed'); } // $sMyFile=$this->_getServerlogDir().'/'.$sLogfile; $sMyFile=$this->_sDataDir.'/'.$sLogfile; if(!file_exists($sMyFile)){ return $sHtml . sprintf($this->t('error-logfile-not-found'), $sMyFile); } if ($fileHandle = fopen($sMyFile, "r")) { $sHtml.='<div style="float: left;"><pre>'; while (($line = fgets($fileHandle)) !== false) { # do same stuff with the $line $bIsComment=strstr($line, 'REM '); if($bIsComment){ $sHtml.='<div class="log-rem">'.$line.'</div>'; } else { $sKey=trim(preg_replace('/=.*/', '', $line)); $sValue=preg_replace('/^([A-Z]*\=)/', '', $line); $sDivClass=''; switch($sKey){ case 'SCRIPTRC': $sDivClass=(int)$sValue===0 ? 'message-ok' : 'message-error'; break; case 'JOBEXPIRE': $sDivClass=date('U') < (int)$sValue ? 'message-ok' : 'message-error'; break; } $sValue=preg_replace('/(rc=0)/', '<span class="message-ok">$1</span>', $sValue); $sValue=preg_replace('/(rc=[1-9][0-9]*)/', '<span class="message-error">$1</span>', $sValue); $sValue=preg_replace('/(rc=[1-9])/', '<span class="message-error">$1</span>', $sValue); // remove terminal color $sValue=preg_replace('/(\[[0-9]{1,3}m)/', '', $sValue); $sHtml.='<div'.($sDivClass ? ' class="'.$sDivClass.'"' : '') // . ' title="'.$sKey.'='.$sValue.'" ' . '><span class="log-var">'.$sKey.'</span>=<span class="log-value">'.$sValue.'</span></div>'; } } $sHtml.='</pre></div>'; } return $sHtml; } /** * get html code for a select box with all servers * @return string */ public function renderServerlist($sSelectedItem=false){ $sHtml=''; $iMaxItemsToShow=30; $sHtml.='<option value="ALL"' .($sSelectedItem===false || $sSelectedItem==='ALL' ? ' selected="selected"' : '') . '>['.$this->t('ALL').' ('.count($this->getServers()).')]</option>'; foreach($this->getServers() as $sServer=>$aData){ $sHtml.='<option value="'.$sServer.'"' .($sSelectedItem===$sServer ? ' selected="selected"' : '') .'>'.$sServer.'</option>'; } $sHtml=$sHtml ? '' /* .'<input id="serverfiltertext" type="text" placeholder="filter server" value="" onchange="filterServers();" onkeypress="filterServers();" onkeyup="filterServers();" ><button onclick="$(\'#serverfiltertext\').val(\'\'); filterServers();">X</button><br><br>' */ .'<select' . ' size="'.( min(array(count($this->getServers())+1 , $iMaxItemsToShow)) ).'"' // . ' size="1"' . ' onchange="setServer(this.value); return false;"' . '>'.$sHtml.'</select>' : false; return $sHtml; } }