diff --git a/.gitignore b/.gitignore index e7719900d590f77637a881ba53800d59425caa25..c1ff376a9e1cdcc8d0aea89a7e13a1cac5464794 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /cronjobs.html /winscp.rnd config/inc_cronlog.php -/data__/ \ No newline at end of file +/data__/ +allcronlogs/* diff --git a/classes/cronlog-renderer.class.php b/classes/cronlog-renderer.class.php index 388820040cb3430809a3cb10e2d8cced08d55c1c..b7b9fb33ba38fca846024756f16aabc202d72aac 100644 --- a/classes/cronlog-renderer.class.php +++ b/classes/cronlog-renderer.class.php @@ -32,434 +32,472 @@ require_once 'cronlog.class.php'; * * @license GNU GPL 3.0 * @author Axel Hahn <axel.hahn@iml.unibe.ch> + * + * 2024-10-01 <axel.hahn@unibe.ch> added type declarations; update php docs */ -class cronlogrenderer extends cronlog{ +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; - + protected int $_iMinTime4Timeline = 60; + + /** + * Max count of entries in history table to prevent freezing of the screen + * when displaying a lot of entries, eg all logs of 100+ servers + * @var int + */ + protected int $_iHistoryLimit = 1000; + /** - * show date of last data and last access; used in rendering methods to display it on top + * 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){ + protected function _renderAccessAndAge(int $iLast): string + { + if (!$iLast) { return ''; } - $iAge=round((date('U')-$iLast)/60); + $iAge = round((date('U') - $iLast) / 60); + $sAge = ($iAge > 60*24 ? '<span class="message-error">'.$iAge.'</span>' : $iAge); return '<div class="accessandage">' - . sprintf($this->t("request-time"), date("Y-m-d H:i:s")).'<br>' - . sprintf($this->t("last-entry"), $iAge) - .'</div>' - ; + . sprintf($this->t("request-time"), date("Y-m-d H:i:s")) . '<br>' + . sprintf($this->t("last-entry"), $sAge) + .($iAge > 60*24 + ? " <a href=\"?deleteserverlogs=$this->_sActiveServer\" class=\"btn btn-danger\">Delete server</a>" + : '' + ) + . '</div>' + ; } - + /** - * get onclick value to filter a datatable + * 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;'; + 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 + * 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(){ + protected function _getDatatableLanguage(): string + { 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").'", + "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").'" + "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 + * 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){ + public function renderCronlogs(array $aData = []): string + { + $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; + $sHtml = $this->_getCacheData($sTaskId); + if ($sHtml) { return $sHtml; } - $sHtml=''; - - if(!$aData){ - $aData=array_merge($this->getRunningJobs(), $this->getServersLastLog()); + $sHtml = ''; + + if (!count($aData)) { + $aData = array_merge($this->getRunningJobs(), $this->getServersLastLog()); } - $sTblHead=''; - $iOK=0; - $iErrors=0; - $iLast=false; + $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"), + foreach ($aData as $sDtakey => $aEntry) { + if (!$sTblHead) { + foreach ([ + $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>'; + ] 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); + $iLast = max([$iLast, date("U", $aEntry['SCRIPTSTARTTIME'])]); + + $aErrors = []; + $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[]=[ + $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']), + sprintf($this->t('error-host-in-log-differs-servername-description'), $sServerFromLogfile, $aEntry['server']), ]; } - - if(!strstr($sServerFromLogfile, ".")){ - $aErrors[]=[ + + if (!strstr($sServerFromLogfile, ".")) { + $aErrors[] = [ $this->t('error-no-fqdn-label'), - sprintf($this->t('error-no-fqdn-description'),$sServerFromLogfile), + sprintf($this->t('error-no-fqdn-description'), $sServerFromLogfile), ]; } - - if($iNextRunErr < date("U")){ - $aErrors[]=[ + + 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']), + if ($aEntry['SCRIPTRC'] > 0) { + $aErrors[] = [ + sprintf($this->t('error-exitcode-label'), (int) $aEntry['SCRIPTRC']), $this->t('error-exitcode-description'), ]; } - if(!$aEntry['SCRIPTLABEL']){ - $aErrors[]=[ + if (!$aEntry['SCRIPTLABEL']) { + $aErrors[] = [ $this->t('error-no-label-label'), $this->t('error-no-label-description'), ]; } - - - if(count($aErrors)){ + + + 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 = ''; + 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>'; + $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>'; + $sColStatus = '<ul>' . $sColStatus . '</ul>'; } else { - $sColStatus.=$this->t('status-ok'); + $sColStatus .= $this->t('status-ok'); } } else { - $bIsRunning=true; - $sColStatus.=$this->t('status-running'); + $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'] - ; + $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>' - ; + $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 align="right"' + . ((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 align="right"' + . ($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 align="right" 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__.' --> + $sIdTable = 'datatable1'; + $sHtml = ' + <!-- START ' . __METHOD__ . ' --> ' - - . '<h3>'.$this->t('logs-head').'</h3>' + + . '<h3>' . $this->t('logs-head') . '</h3>' . '<p class="hint">' - . $this->t('logs-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({ + ? '<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().' + ' . $this->_getDatatableLanguage() . ' }); });' - . '</script>' - : '' + . '</script>' + : '' ) // init datatable . ' - <!-- ENDE '.__METHOD__.' --> + <!-- ENDE ' . __METHOD__ . ' --> ' - ; + ; $this->_writeCacheData($sTaskId, $sHtml); return $sHtml; } - + /** - * get html code for a table with events of executed cronjobs for ALL servers + * 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){ + public function renderCronlogsOfAllServers(): string + { + $aData = []; + foreach (array_keys($this->getServers()) as $sServer) { $this->setServer($sServer); - $aData=array_merge($aData, $this->getRunningJobs(),$this->getServersLastLog()); + $aData = array_merge($aData, $this->getRunningJobs(), $this->getServersLastLog()); } $this->setServer('ALL'); return $this->renderCronlogs($aData); } - + /** - * get html code for a switcher of multiple instances + * Get html code for a switcher of multiple instances. + * It returns an empty string if there is no / only one instance * * @param array $aData result of $oCL->getServerLogs() * @return string */ - public function renderInstances(){ - if(count($this->_aInstances) < 2){ - return false; + public function renderInstances(): string + { + if (count($this->_aInstances) < 2) { + return ''; } - $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 = ''; + $sServer = isset($_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>'; + $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 + * 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){ + public function renderHistoryTable(array $aData = []): string + { + $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; + $sHtml = $this->_getCacheData($sTaskId); + if ($sHtml) { return $sHtml; } - $sHtml=''; - - if(!$aData){ - $aData=$this->getServerJobHistory(); + $sHtml = ''; + + if (!$aData) { + $aData = $this->getServerJobHistory(); + } + + // sort by starting time - especially before cutting the list + $aTmp=[]; + foreach($aData as $sKey => $aEntry) { + $aTmp[$aEntry['start'].'__'.$aEntry['host'].'__'.$aEntry['job']]=$aEntry; } + krsort($aTmp); + $aData = array_values($aTmp); - $sTblHead=''; - $iOK=0; - $iErrors=0; - $iLast=false; + // render table + $sTblHead = ''; + $iOK = 0; + $iErrors = 0; + $iLast = false; + $iCounter = 0; // 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"), + foreach ($aData as $aEntry) { + $iCounter++; + if($iCounter>$this->_iHistoryLimit) { + break; + } + if (!$sTblHead) { + foreach ([ + $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>'; + ] as $sKey) { + $sTblHead .= '<th>' . $sKey . '</th>'; } } - $iLast=max(array($iLast, date("U", $aEntry['start']))); - $sClass='message-'.($aEntry['rc']?'error':'ok'); - - if($aEntry['rc']){ + $iLast = max([$iLast, date("U", $aEntry['start'])]); + + 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>' - ; + + $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 align="right"' + . ($aEntry['exectime'] > $this->_iMinTime4Timeline ? ' class="message-warning"' : '') + . '>' + . $aEntry['exectime'] . 's' + . ($aEntry['exectime'] > 100 ? ' (' . round($aEntry['exectime'] / 60) . 'min)' : '') + . '</td>' + . '<td align="right">' . $aEntry['ttl'] . '</td>' + . '<td align="right" 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__.' --> + $sIdTable = 'datatable2'; + $sHtml = ' + <!-- START ' . __METHOD__ . ' --> ' - . '<h3>'.$this->t('history-head').'</h3>' + . '<h3>' . $this->t('history-head') . '</h3>' . '<p class="hint">' - . $this->t('history-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>' : '' ) + . $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>' + . '<table id="' . $sIdTable . '">' + . '<thead><tr>' . $sTblHead . '</tr></thead>' . '<tbody>' - .$sHtml - .'</tbody>' + . $sHtml + . '</tbody>' . '</table>' // init datatable . (count($aData) ? '<script>' - . '$(document).ready( function () {$(\'#'.$sIdTable.'\').DataTable({ + . '$(document).ready( function () {$(\'#' . $sIdTable . '\').DataTable({ "retrieve": true, "aaSorting":[[0,"desc"]], "aLengthMenu":[[25,100,-1],[25,100,"---"]] - '.$this->_getDatatableLanguage().' + ' . $this->_getDatatableLanguage() . ' });} );' - . '</script>' + . '</script>' : '' - ) + ) . ' - <!-- ENDE '.__METHOD__.' --> + <!-- 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){ + public function renderHistoryOfAllServers(): string + { + $aData = []; + foreach (array_keys($this->getServers()) as $sServer) { $this->setServer($sServer); - $aData=array_merge($aData, $this->getServerJobHistory()); + $aData = array_merge($aData, $this->getServerJobHistory()); } $this->setServer('ALL'); - return $this->renderJoblist($aData); + return $this->renderHistoryTable($aData); } - + /** * get html code for a timeline with events of executed cronjobs * @@ -470,32 +508,33 @@ class cronlogrenderer extends cronlog{ * @param array $aData result of $oCL->getServerLogs() * @return string */ - public function renderJobGraph($aData=false){ - $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer; - $sHtml=$this->_getCacheData($sTaskId); - if($sHtml){ + public function renderJobGraph(array $aData = []): string + { + $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; + $sHtml = $this->_getCacheData($sTaskId); + if ($sHtml) { return $sHtml; } - $sHtml=''; + $sHtml = ''; static $iGraphCounter; - if(!isset($iGraphCounter)){ - $iGraphCounter=0; + if (!isset($iGraphCounter)) { + $iGraphCounter = 0; } $iGraphCounter++; - if(!$aData){ - $aData=$this->getServerJobHistory(false); + if (!count($aData)) { + $aData = $this->getServerJobHistory(false); } - $sDivId='vis-timeline-'.$iGraphCounter; - - $aDataset=array(); - $iLast=false; - $iEntry=0; - foreach($aData as $aEntry){ - if($aEntry['exectime']>$this->_iMinTime4Timeline){ + $sDivId = 'vis-timeline-' . $iGraphCounter; + + $aDataset = []; + $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, + $iLast = max([$iLast, date("U", $aEntry['start'])]); + $aDataset[] = [ + 'id' => $iEntry, /* 'start'=>(int)date("U", $aEntry['start']), 'end'=>(int)date("U", $aEntry['end']), @@ -507,47 +546,47 @@ class cronlogrenderer extends cronlog{ '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>' + '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)' : '') + . $aEntry['exectime'] . 's' + . ($aEntry['exectime'] > 100 ? ' (' . round($aEntry['exectime'] / 60) . 'min)' : '') . '<br>' - . 'rc = ' . $aEntry['rc'].'<br>' - , - ); + . 'rc = ' . $aEntry['rc'] . '<br>' + , + ]; // if($iEntry>=265){break;} } } + + $sHtml .= ' - $sHtml.=' - - <!-- START '.__METHOD__.' --> + <!-- START ' . __METHOD__ . ' --> ' - . '<h3>'.$this->t('timeline-head').'</h3>' - . '<p class="hint">' + . '<h3>' . $this->t('timeline-head') . '</h3>' + . '<p class="hint">' . sprintf($this->t('timeline-hint'), $this->_iMinTime4Timeline) - . '</p> + . '</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> + . 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.'"); + var container = document.getElementById("' . $sDivId . '"); // Create a DataSet (allows two way data-binding) - var items = new vis.DataSet('.json_encode($aDataset).'); + var items = new vis.DataSet(' . json_encode($aDataset) . '); // Configuration for the Timeline var options = { @@ -557,144 +596,155 @@ class cronlogrenderer extends cronlog{ // Create a Timeline var timeline = new vis.Timeline(container, items, options); + + // set focus to newest jobs + // timeline.moveTo("'.date("Y-m-d H:i:s", time()-60*60*24*4).'"); // fix: some timelines do not properly work ... but I make them visible - $(\'#'.$sDivId.' .vis-timeline\').css(\'visibility\', \'visible\'); + $(\'#' . $sDivId . ' .vis-timeline\').css(\'visibility\', \'visible\'); </script> - ' - : $this->t('graph-no-data').'<br>' - ) - .' + ' + : $this->t('graph-no-data') . '<br>' + ) + . ' - <!-- ENDE '.__METHOD__.'--> + <!-- ENDE ' . __METHOD__ . '--> '; $this->_writeCacheData($sTaskId, $sHtml); - + return '' - .$sHtml + . $sHtml // . strlen($sHtml).' byte - ' . '<pre>'.htmlentities($sHtml).'</pre><br>' - ; + ; } - - public function renderJobGraphOfAllServers(){ - $aData=array(); - foreach (array_keys($this->getServers()) as $sServer){ + + /** + * Get html code for the job graph of all servers + * @return string + */ + public function renderJobGraphOfAllServers(): string + { + $aData = []; + foreach (array_keys($this->getServers()) as $sServer) { $this->setServer($sServer); - $aData=array_merge($aData, $this->getServerJobHistory()); + $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 + * used in config/page-replacements.php + * @return string */ - public function renderJSLang(){ - $aReturn=[]; + public function renderJSLang(): string + { + $aReturn = []; - foreach($this->_aLang as $sKey=>$sText){ - if(preg_match('/^JS_/', $sKey)){ - $aReturn[preg_replace('/^JS_/', '', $sKey)]=$sText; + 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" + . '// 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){ + + /** + * Get html code to show a single log file with syntax highligt an marking the reeturn code + * + * @param string $sLogfile logfile; [server]/[filename.log] + * @return string + */ + public function renderLogfile(string $sLogfile): string + { + $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, '..')){ + if (strstr($sLogfile, '..')) { return $sHtml . $this->t('error-dots-not-allowed'); } // $sMyFile=$this->_getServerlogDir().'/'.$sLogfile; - $sMyFile=$this->_sDataDir.'/'.$sLogfile; - if(!file_exists($sMyFile)){ + $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>'; + $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>'; + $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){ + $sKey = trim(preg_replace('/=.*/', '', $line)); + $sValue = preg_replace('/^([A-Z]*\=)/', '', $line); + $sDivClass = ''; + switch ($sKey) { case 'SCRIPTRC': - $sDivClass=(int)$sValue===0 ? 'message-ok' : 'message-error'; + $sDivClass = (int) $sValue === 0 ? 'message-ok' : 'message-error'; break; case 'JOBEXPIRE': - $sDivClass=date('U') < (int)$sValue ? 'message-ok' : 'message-error'; + $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); + $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>'; + $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; - } - + $sHtml .= '</pre></div>'; + } + return $sHtml; + } + /** - * get html code for a select box with all servers + * 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>'; + public function renderServerlist(string $sSelectedItem = ''): string + { + $sHtml = ''; + $iMaxItemsToShow = 30; + $sHtml .= '<option value="ALL"' + . ($sSelectedItem === '' || $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; + $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([count($this->getServers()) + 1, $iMaxItemsToShow])) . '"' + // . ' size="1"' + . ' onchange="setServer(this.value); return false;"' + . '>' . $sHtml . '</select>' + : false; return $sHtml; } - + } \ No newline at end of file diff --git a/classes/cronlog.class.php b/classes/cronlog.class.php index de0ed21f8c2a3165bfc952c1f64ed99d8436d3e3..2340d1fd37af0b58a70d24feb66586b1e7e744e8 100644 --- a/classes/cronlog.class.php +++ b/classes/cronlog.class.php @@ -31,110 +31,205 @@ * * @license GNU GPL 3.0 * @author Axel Hahn <axel.hahn@iml.unibe.ch> + * + * 2024-10-01 <axel.hahn@unibe.ch> added type declarations; update php docs */ -class cronlog { - - - protected $_sDataDir = "__APPDIR__/data"; - protected $_iTtlCache = 60; // in sec +class cronlog +{ + /** - * when show an error for expired jobs (latency to execute job and sync logs) + * Data dir with subdirs per host and its cronjob logs + * @var string + */ + protected string $_sDataDir = "__APPDIR__/data"; + + /** + * TTL for cached rendered html output of logs/ history/ timeline + * @var int + */ + protected int $_iTtlCache = 60; // in sec + + /** + * When show an error for expired jobs (keep in mind the latency to execute job and sync logs) * @var integer */ - protected $_iExpiredJobsFailAfter = 60*30; // in sec - protected $_iMinTtl = 0; // in sec - protected $_aSkipJoblogs = array(); - - protected $_aInstances = array(); + protected int $_iExpiredJobsFailAfter = 60 * 30; // in sec - protected $_aServers = array(); - protected $_sActiveServer = false; + /** + * Minimal TTL for a cronjob + * @var int + */ + protected int $_iMinTtl = 0; // in sec + + /** + * List of cronjobs to hide in the web view + * @var array + */ + protected array $_aSkipJoblogs = []; + + /** + * Array of cronlog viewer instances to show a navigation to switch between them + * @var array + */ + protected array $_aInstances = []; + + /** + * Array of all servers with cronjobs + * @var array + */ + protected array $_aServers = []; + + /** + * Current server + * @var string + */ + protected string $_sActiveServer = ''; + + /** + * Filefilter for finished joblogs + * @var string + */ + protected string $_sFileFilter_serverjoblog = '*joblog*.done'; + + /** + * Filefilter for history logs + * @var string + */ + protected string $_sFileFilter_joblog = '*.log'; - protected $_sFileFilter_serverjoblog = '*joblog*.done'; - protected $_sFileFilter_joblog = '*.log'; - protected $_sFileFilter_jobrunning = '*.log.running*'; + /** + * Filefilter for running jobs + * @var string + */ + protected string $_sFileFilter_jobrunning = '*.log.running*'; - protected $_sLang = ''; // language ... read from config file - protected $_aLang = []; // language data + /** + * Current language + * @var string + */ + protected string $_sLang = ''; // language ... read from config file + + /** + * Array of language texts + * @var array + */ + protected array $_aLang = []; // language data // ---------------------------------------------------------------------- // MAIN // ---------------------------------------------------------------------- /** - * init - * @return boolean + * Constructor */ - public function __construct() { - + public function __construct() + { // read config - if (file_exists(__DIR__.'/../config/inc_cronlog.php')){ - $aCfgTemp=include(__DIR__.'/../config/inc_cronlog.php'); + if (file_exists(__DIR__ . '/../config/inc_cronlog.php')) { + $aCfgTemp = include(__DIR__ . '/../config/inc_cronlog.php'); $this->_sDataDir = isset($aCfgTemp['sDatadir']) ? $aCfgTemp['sDatadir'] : $this->_sDataDir; - $this->_iTtlCache = isset($aCfgTemp['iTtlCache']) ? (int)$aCfgTemp['iTtlCache'] : $this->_iTtlCache; - $this->_iMinTtl = isset($aCfgTemp['iMinTtl']) ? (int)$aCfgTemp['iMinTtl'] : $this->_iMinTtl; - $this->_iExpiredJobsFailAfter = isset($aCfgTemp['iExpiredJobsFailAfter']) ? (int)$aCfgTemp['iExpiredJobsFailAfter'] : $this->_iExpiredJobsFailAfter; - $this->_aSkipJoblogs = isset($aCfgTemp['aHidelogs']) && is_array($aCfgTemp['aHidelogs']) ? $aCfgTemp['aHidelogs'] : $this->_aSkipJoblogs; + $this->_iTtlCache = isset($aCfgTemp['iTtlCache']) ? (int) $aCfgTemp['iTtlCache'] : $this->_iTtlCache; + $this->_iMinTtl = isset($aCfgTemp['iMinTtl']) ? (int) $aCfgTemp['iMinTtl'] : $this->_iMinTtl; + $this->_iExpiredJobsFailAfter = isset($aCfgTemp['iExpiredJobsFailAfter']) ? (int) $aCfgTemp['iExpiredJobsFailAfter'] : $this->_iExpiredJobsFailAfter; + $this->_aSkipJoblogs = isset($aCfgTemp['aHidelogs']) && is_array($aCfgTemp['aHidelogs']) ? $aCfgTemp['aHidelogs'] : $this->_aSkipJoblogs; $this->_aInstances = isset($aCfgTemp['instances']) ? $aCfgTemp['instances'] : []; - $this->_sLang=isset($aCfgTemp['lang']) && $aCfgTemp['lang'] ? $aCfgTemp['lang'] : 'en-en'; - if(!file_exists(__DIR__.'/../config/lang_'.$this->_sLang.'.php')){ + $this->_sLang = isset($aCfgTemp['lang']) && $aCfgTemp['lang'] ? $aCfgTemp['lang'] : 'en-en'; + if (!file_exists(__DIR__ . '/../config/lang_' . $this->_sLang . '.php')) { header('HTTP/1.1 503 Service Temporarily Unavailable'); header('Status: 503 Service Temporarily Unavailable'); - die('ERROR: lang file for lang => "'.$this->_sLang.'" not found.<br>config/lang_'.$this->_sLang.'.php<br>does not exist.'); + die('ERROR: lang file for lang => "' . $this->_sLang . '" not found.<br>config/lang_' . $this->_sLang . '.php<br>does not exist.'); } - $this->_aLang=$aCfgTemp=include(__DIR__.'/../config/lang_'.$this->_sLang.'.php'); + $this->_aLang = $aCfgTemp = include(__DIR__ . '/../config/lang_' . $this->_sLang . '.php'); } $this->_sDataDir = str_replace("__APPDIR__", dirname(dirname(__FILE__)), $this->_sDataDir); $this->_sDataDir = str_replace('\\', '/', $this->_sDataDir); - + + if (isset($_GET['deleteserverlogs']) && $_GET['deleteserverlogs']){ + $this->_deleteServerlogs($_GET['deleteserverlogs']); + } + $this->getServers(); - - return true; } - + // ---------------------------------------------------------------------- // private // ---------------------------------------------------------------------- - + + protected function _deleteServerlogs(string $sServer): bool + { + $dirCronlog="$this->_sDataDir/$sServer"; + + if (! is_dir($dirCronlog)) { + // throw new InvalidArgumentException("$dirCronlog must be a directory"); + return false; + } + if (substr($dirCronlog, strlen($dirCronlog) - 1, 1) != '/') { + $dirCronlog .= '/'; + } + $files = array_merge( + glob($dirCronlog . '*', GLOB_MARK), + glob($this->_getCacheDir() . "/*$sServer", GLOB_MARK) + ); + foreach ($files as $file) { + /* + if (is_dir($file)) { + deleteDir($file); + } else { + */ + @unlink($file); + //} + } + return @rmdir($dirCronlog); + } /** * chaching: get the full path of directory for caching * @return string */ - protected function _getCacheDir(){ - return $this->_sDataDir.'/__cache'; + protected function _getCacheDir(): string + { + if(!is_dir("$this->_sDataDir/__cache")) { + mkdir("$this->_sDataDir/__cache", 0750, true); + } + return "$this->_sDataDir/__cache"; } /** - * caching: get full path of a caching item - * @param string $sTaskId + * Caching: get full path of a caching item + * @param string $sTaskId Name of a task * @return string */ - protected function _getCacheFile($sTaskId){ - return $this->_getCacheDir().'/'.$sTaskId; + protected function _getCacheFile(string $sTaskId): string + { + return $this->_getCacheDir() . "/$sTaskId"; } /** - * read logs: get full path to a servers cronjob logdata + * Read logs: get full path to a servers cronjob logdata * @return string */ - protected function _getServerlogDir(){ - return $this->_sDataDir.'/'.$this->_sActiveServer; + protected function _getServerlogDir(): string + { + return "$this->_sDataDir/$this->_sActiveServer"; } /** - * caching: get cached data if they exist and aren't expired - * @param string $sTaskId - * @return mixed boolean|array + * Caching: get cached data for rendered html output + * if they exist and isn't expired + * + * @param string $sTaskId Name of the task + * @return mixed string|array */ - protected function _getCacheData($sTaskId){ - // DISABLE CACHE return false; - $sFile=$this->_getCacheFile($sTaskId); - if(file_exists($sFile)){ - if (filemtime($sFile)>(date('U')-$this->_iTtlCache)){ + protected function _getCacheData(string $sTaskId): string|array + { + // DISABLE CACHE + // return false; + $sFile = $this->_getCacheFile($sTaskId); + if (file_exists($sFile)) { + if (filemtime($sFile) > (date('U') - $this->_iTtlCache)) { // echo "USE cache $sFile<br>"; return unserialize(file_get_contents($sFile)); } else { @@ -142,24 +237,25 @@ class cronlog { unlink($sFile); } } - return false; + return ''; } /** - * read logs: parse a single line in the joblog and return has with all key value items + * Read logs: parse a single line in the joblog and return has with all key value items * @param string $sLine single line in the log * @return array */ - protected function _parseJoblogLine($sLine){ - $aReturn=array(); + protected function _parseJoblogLine(string $sLine): array + { + $aReturn = []; // echo "DEBUG $sLine<br>"; // job=dok-kvm-instances:host=kalium:start=1538358001:end=1538358001:exectime=0:ttl=60:rc=0 - $sLine=str_replace("\n", '', $sLine); - $sLine=str_replace(':', '", "', $sLine); - $sLine=str_replace('=', '": "', $sLine); - $sLine='{"'.$sLine.'"}'; + $sLine = str_replace("\n", '', $sLine); + $sLine = str_replace(':', '", "', $sLine); + $sLine = str_replace('=', '": "', $sLine); + $sLine = '{"' . $sLine . '"}'; // echo "DEBUG $sLine<br><br>"; - $aReturn=json_decode($sLine, 1); - if(!is_array($aReturn)){ + $aReturn = json_decode($sLine, 1); + if (!is_array($aReturn)) { echo "not a JSON string<br>"; echo "DEBUG $sLine<br><br>"; die(); @@ -168,50 +264,53 @@ class cronlog { } /** - * read logs: parse the whole cronwrapper logfile and return a hash + * Read logs: parse the whole cronwrapper logfile and return a hash * @param string $sFile filename with full path + * @return array */ - protected function _parseLogfile($sFile) { - $aReturn=array( - 'SCRIPTNAME'=>false, - 'SCRIPTTTL'=>false, - 'SCRIPTSTARTTIME'=>false, - 'SCRIPTLABEL'=>false, - 'SCRIPTENDTIME'=>false, - 'SCRIPTEXECTIME'=>false, - 'SCRIPTRC'=>false, - // 'SCRIPTOUT'=>array(), - ); + protected function _parseLogfile(string $sFile): array + { + $aReturn = [ + 'SCRIPTNAME' => false, + 'SCRIPTTTL' => false, + 'SCRIPTSTARTTIME' => false, + 'SCRIPTLABEL' => false, + 'SCRIPTENDTIME' => false, + 'SCRIPTEXECTIME' => false, + 'SCRIPTRC' => false, + // 'SCRIPTOUT'=>[], + ]; $fileHandle = fopen($sFile, "r"); while (($line = fgets($fileHandle)) !== false) { // get key ... the part before "=" - $sKey=trim(preg_replace('/=.*/', '', $line)); - if($sKey && isset($aReturn[$sKey])){ + $sKey = trim(preg_replace('/=.*/', '', $line)); + if ($sKey && isset($aReturn[$sKey])) { // add value ... the part behind "=" - $aReturn[$sKey]=preg_replace('/^([A-Z]*\=)/', '', $line); + $aReturn[$sKey] = preg_replace('/^([A-Z]*\=)/', '', $line); } } fclose($fileHandle); - + // fetch unit timestamp from date values (they are like "2018-09-30 03:40:05, 1538278805") - $aReturn['SCRIPTSTARTTIME']=(int)preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']); - $aReturn['SCRIPTENDTIME']=(int)preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']); - + $aReturn['SCRIPTSTARTTIME'] = (int) preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']); + $aReturn['SCRIPTENDTIME'] = (int) preg_replace('/.*,\ /', '', $aReturn['SCRIPTSTARTTIME']); + // remove " s" from exec time value - $aReturn['SCRIPTEXECTIME']=preg_replace('/\ s$/', '', $aReturn['SCRIPTEXECTIME']); - + $aReturn['SCRIPTEXECTIME'] = preg_replace('/\ s$/', '', $aReturn['SCRIPTEXECTIME']); + return $aReturn; - + } - + /** * caching: write new data; it returns the success of write operation as bool * @param string $sTaskId - * @param [any] $data data to store; can be any serializable value - * @return boolean + * @param mixed $data data to store; can be any serializable value + * @return boolean|integer */ - protected function _writeCacheData($sTaskId, $data){ - $sFile=$this->_getCacheFile($sTaskId); + protected function _writeCacheData(string $sTaskId, mixed $data): bool|int + { + $sFile = $this->_getCacheFile($sTaskId); // echo "WRITE cache $sFile<br>"; return file_put_contents($sFile, serialize($data)); } @@ -219,37 +318,39 @@ class cronlog { // ---------------------------------------------------------------------- // public getter // ---------------------------------------------------------------------- - + /** - * get currently selected server + * Get currently selected server * @return string */ - public function getServer(){ + public function getServer(): string + { return $this->_sActiveServer; } - + /** - * get array with existing servers in data dir + * Get array with existing servers in data dir * @return array */ - public function getServers(){ - - if(is_array($this->_aServers) && count($this->_aServers)){ + public function getServers(): array + { + + if (is_array($this->_aServers) && count($this->_aServers)) { return $this->_aServers; } - - $this->_aServers=array(); + + $this->_aServers = []; // echo "DEBUG DATADIR: " . $this->_sDataDir."<br>"; - if (!is_dir($this->_sDataDir)){ + if (!is_dir($this->_sDataDir)) { echo "WARNING: no data. Check sDatadir in the config and set it to an existing directory.<br>"; die(); } if ($handle = opendir($this->_sDataDir)) { while (false !== ($entry = readdir($handle))) { - if ($entry != "." && $entry != ".." && $entry != "__cache" && is_dir($this->_sDataDir.'/'.$entry)) { + if ($entry != "." && $entry != ".." && $entry != "__cache" && is_dir($this->_sDataDir . '/' . $entry)) { // echo "DEBUG $entry<br>\n"; - $this->_aServers[$entry]=array(); + $this->_aServers[$entry] = []; } } closedir($handle); @@ -257,31 +358,32 @@ class cronlog { ksort($this->_aServers); return $this->_aServers; } - + /** - * get logs from jobfilea of the current or given server + * Get logs from jobfiles of the current or given server * @param boolean $bUseSkip hide jobs if their label matches the skip list; default: true * @return array */ - public function getServerJobHistory($bUseSkip=true){ - $aReturn=array(); - $sTaskId=__FUNCTION__.'-'.$this->_sActiveServer; + public function getServerJobHistory(bool $bUseSkip = true): bool|array + { + $aReturn = []; + $sTaskId = __FUNCTION__ . '-' . $this->_sActiveServer; - $aData=$this->_getCacheData($sTaskId); - if($aData){ + $aData = $this->_getCacheData($sTaskId); + if ($aData) { return $aData; } - $aData=array(); - - foreach(glob($this->_getServerlogDir().'/'.$this->_sFileFilter_serverjoblog) as $sMyJobfile){ + $aData = []; + + foreach (glob($this->_getServerlogDir() . '/' . $this->_sFileFilter_serverjoblog) as $sMyJobfile) { // echo "DEBUG: $sMyJobfile<br>"; $fileHandle = fopen($sMyJobfile, "r"); while (($line = fgets($fileHandle)) !== false) { // send the current file part to the browser - $aData=$this->_parseJoblogLine($line); - if(!$bUseSkip || array_search($aData['job'], $this->_aSkipJoblogs)===false){ - $aReturn[$aData['start']]=$aData; + $aData = $this->_parseJoblogLine($line); + if (!$bUseSkip || array_search($aData['job'], $this->_aSkipJoblogs) === false) { + $aReturn[$aData['start'].'__'.$aData['host'].'__'.$aData['job']] = $aData; } } fclose($fileHandle); @@ -290,75 +392,79 @@ class cronlog { $this->_writeCacheData($sTaskId, $aReturn); return $aReturn; } - + /** - * get logs from jobfilea of the current or given server + * Get logs from jobfilea of the current or given server * @return array */ - public function getServersLastLog(){ - $aReturn=array(); - $aData=array(); - foreach(glob($this->_getServerlogDir().'/'.$this->_sFileFilter_joblog) as $sMyJobfile){ + public function getServersLastLog(): array + { + $aReturn = []; + $aData = []; + foreach (glob($this->_getServerlogDir() . '/' . $this->_sFileFilter_joblog) as $sMyJobfile) { // echo "DEBUG: log file $sMyJobfile<br>"; - $aData=$this->_parseLogfile($sMyJobfile); - $aData['server']=$this->_sActiveServer; - $aData['logfile']= $this->_sActiveServer.'/'.basename($sMyJobfile); - $aReturn[$aData['SCRIPTSTARTTIME'].$sMyJobfile]=$aData; + $aData = $this->_parseLogfile($sMyJobfile); + $aData['server'] = $this->_sActiveServer; + $aData['logfile'] = $this->_sActiveServer . '/' . basename($sMyJobfile); + $aReturn[$aData['SCRIPTSTARTTIME'] . $sMyJobfile] = $aData; } rsort($aReturn); return $aReturn; } + /** * get logs from jobfilea of the current or given server * @return array */ - public function getRunningJobs(){ - $aReturn=array(); - $aData=array(); - foreach(glob($this->_getServerlogDir().'/'.$this->_sFileFilter_jobrunning) as $sMyJobfile){ + public function getRunningJobs(): array + { + $aReturn = []; + $aData = []; + foreach (glob($this->_getServerlogDir() . '/' . $this->_sFileFilter_jobrunning) as $sMyJobfile) { // echo "DEBUG: log file $sMyJobfile<br>"; - $aData=$this->_parseLogfile($sMyJobfile); - $aData['server']=$this->_sActiveServer; - $aData['logfile']= $this->_sActiveServer.'/'.basename($sMyJobfile); - $aReturn[$aData['SCRIPTSTARTTIME'].$sMyJobfile]=$aData; + $aData = $this->_parseLogfile($sMyJobfile); + $aData['server'] = $this->_sActiveServer; + $aData['logfile'] = $this->_sActiveServer . '/' . basename($sMyJobfile); + $aReturn[$aData['SCRIPTSTARTTIME'] . $sMyJobfile] = $aData; } rsort($aReturn); return $aReturn; } - + /** - * translate ... get a language specific text of a given key + * Translate: get a language specific text of a given key * @param string $id id of language text * @return string */ - public function t($id){ - return '' - .(isset($this->_aLang[$id]) ? $this->_aLang[$id] : '['.$id.'] ???') - ; + public function t($id): string + { + return $this->_aLang[$id] ?? '[' . $id . '] ???'; } // ---------------------------------------------------------------------- // public setter // ---------------------------------------------------------------------- - + /** - * set which server is selected + * Set which server is selected * The given server must exist as directory (that contains its logs) + * It returns false if the given server does not exist or has value 'ALL' * @param string $sServer server name - * @return string + * @return bool|string */ - public function setServer($sServer){ - $this->_sActiveServer=false; - if($sServer==='ALL'){ + public function setServer(string $sServer): bool|string + { + $this->_sActiveServer = false; + if ($sServer === 'ALL') { return false; } - if($sServer && !array_key_exists($sServer, $this->_aServers)){ + if ($sServer && !array_key_exists($sServer, $this->_aServers)) { echo "WARNING: server [$sServer] does not exist<br>"; return false; } - $this->_sActiveServer=$sServer; - + $this->_sActiveServer = $sServer; + return $this->_sActiveServer; } - + } \ No newline at end of file diff --git a/classes/htmlelements.class.php b/classes/htmlelements.class.php index 91943c7c97d4a3226b6a2c457496ed85b08ef447..b890fda4dd77302de87fa59e603fb353ce763543 100755 --- a/classes/htmlelements.class.php +++ b/classes/htmlelements.class.php @@ -15,27 +15,33 @@ * - icon - will be added as <i class="[icon value]"></i> to the label * * @author Axel + * + * 2024-07-04 <axel.hahn@unibe.ch> added type declarations; update php docs + * 2024-08-26 <axel.hahn@unibe.ch> remove unneeded methods; simplify icon methods; update phpdocs */ -class htmlelements { +class htmlelements +{ /** - * set of auto generated icon prefixes - * @var type + * Extracted label from array with attributes + * @var string */ - var $_aIcons=array( - // 'fa-'=>'fa ', - ); - var $_sLabel = ''; - var $_aAttributes = array(); - + + /** + * Array of attributes for a html tag + * @var array + */ + var $_aAttributes = []; + // ---------------------------------------------------------------------- // CONSTRUCTOR // ---------------------------------------------------------------------- - - public function __construct() { - return true; + + public function __construct() + { + // nothiung here } // ---------------------------------------------------------------------- @@ -43,27 +49,28 @@ class htmlelements { // PRIVATE FUNCTIONS // // ---------------------------------------------------------------------- - - + + /** * generate html attibutes with all internal attributes key -> values + * to be added in opening tag * @return string */ - protected function _addAttributes() { + protected function _addAttributes(): string + { $sReturn = ''; foreach ($this->_aAttributes as $sAttr => $sValue) { - if(is_array($sValue)){ - echo "ERROR: an html tag was defined with array in attribute [$sAttr]:<br><pre>".print_r($this->_aAttributes, 1)."</pre>"; + if (is_array($sValue)) { + echo "ERROR: an html tag was defined with array in attribute [$sAttr]:<br><pre>" . print_r($this->_aAttributes, 1) . "</pre>"; } - $sReturn .= ' '.$sAttr . '="' . $sValue . '"'; - + $sReturn .= " $sAttr=\"$sValue\""; } return $sReturn; } - - + + /** - * internal helper: fetch all attributes from key-value hash; + * Internal helper: fetch all attributes from key-value hash; * Specialties here: * - label will be extracted from key 'label' * - and optional existing key 'icon' will be added at beginning of a label @@ -71,17 +78,18 @@ class htmlelements { * @param array $aAttributes * @return boolean */ - protected function _setAttributes($aAttributes){ - $this->_sLabel=''; - if(isset($aAttributes['icon']) && $aAttributes['icon']){ - $this->_sLabel.=$this->getIcon($aAttributes['icon']); + protected function _setAttributes(array $aAttributes): bool + { + $this->_sLabel = ''; + if (isset($aAttributes['icon']) && $aAttributes['icon']) { + $this->_sLabel .= $this->getIcon($aAttributes['icon']); unset($aAttributes['icon']); } - if(isset($aAttributes['label']) && $aAttributes['label']){ + if (isset($aAttributes['label']) && $aAttributes['label']) { $this->_sLabel .= $aAttributes['label']; unset($aAttributes['label']); } - $this->_aAttributes=$aAttributes; + $this->_aAttributes = $aAttributes; return true; } @@ -91,21 +99,22 @@ class htmlelements { // HTML GENERIC // // ---------------------------------------------------------------------- - + /** - * generic function to get html code for a single tag + * Generic function to get html code for a single tag * * @param string $sTag tag name * @param array $aAttributes array with attributes (optional including 'icon' and 'label') * @param boolean $bCloseTag optional: set false if tag has no closing tag (= ending with "/>") - * @return type + * @return string html code */ - public function getTag($sTag, $aAttributes, $bCloseTag=true){ + public function getTag(string $sTag, array $aAttributes, bool $bCloseTag = true): string + { $sTpl = $bCloseTag ? "<$sTag%s>%s</$sTag>" : "<$sTag %s/>%s"; $this->_setAttributes($aAttributes); return sprintf($sTpl, $this->_addAttributes(), $this->_sLabel); } - + // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS @@ -114,109 +123,22 @@ class htmlelements { // ---------------------------------------------------------------------- /** - * helper detect prefix of a string add prefix of a framework + * Helper detect prefix of a string add prefix of a framework * i.e. value "fa-close" detects font awesome and adds "fa " as prefix * * @param string $sIconclass - * @return boolean + * @return string HTML code */ - public function getIcon($sIconclass=false){ - if(!$sIconclass){ + public function getIcon(string $sIconclass = ''): string + { + if (!$sIconclass) { return ''; } - $sPrefix=''; - foreach ($this->_aIcons as $sPrefix =>$add) { - if (strpos($sIconclass, $sPrefix)===0){ - $sPrefix=$add; - continue; - } - } - // do not use this .. it overrides internal attribute vars - // return $this->getTag('i', array('class'=>$sPrefix.$sIconclass)); - return '<i class="'.$sPrefix.$sIconclass.'"></i> '; - } - - - // ---------------------------------------------------------------------- - // - // PUBLIC FUNCTIONS - // HTML COMPONENTS - // - // ---------------------------------------------------------------------- - /** - * get html code for an input field - * - * @param array $aAttributes attributes of the select tag - * @return string - */ - public function getFormInput($aAttributes){ - $sTpl = '<input %s/>'; - $this->_setAttributes($aAttributes); - return sprintf($sTpl, $this->_addAttributes()); - } - /** - * get html code for an option field in a select drop down - * - * @param array $aAttributes attributes of the option tag - * @return string - */ - public function getFormOption($aAttributes){ - $sTpl = '<option %s>%s</option>'; - $this->_setAttributes($aAttributes); - return sprintf($sTpl, $this->_addAttributes(), $this->_sLabel); - } - /** - * get html code for a select drop down - * - * @param array $aAttributes attributes of the select tag - * @param array $aOptions array for all option fields - * @return string - */ - public function getFormSelect($aAttributes, $aOptions=array()){ - // $sTpl = '<select %s>%s</select>'; + // do not use this .. it overrides internal attribute vars + // return $this->getTag('i', ['class'=>$sIconclass]); - if(!count($aOptions)){ - return false; - } - $sOptions=''; - foreach($aOptions as $aOptionAttributes){ - // $sOptions.=$this->getFormOption($aOptionAttributes); - $sOptions.=$this->getTag('option', $aOptionAttributes); - } - $aAttributes['label']=$sOptions; - return $this->getTag('select', $aAttributes); - /* - $this->_setAttributes($aAttributes); - return sprintf($sTpl, $this->_addAttributes(), $sOptions); - * - */ + return "<i class=\"$sIconclass\"></i> "; } - public function getTable($aHead, $aBody, $aTableAttributes=array()){ - $sReturn=''; - $sTdata=''; - $sThead=''; - $sTpl = '<table %s>' - . '<thead><tr>%s</tr></thead>' - . '<tbody>%s</tbody>' - . '</table>'; - - foreach($aHead as $sTh){ - $sThead.='<th>'.$sTh.'</th>'; - } - foreach($aBody as $aTr){ - $sTdata.='<tr>'; - foreach($aTr as $sTd){ - $sTdata.='<td>'.$sTd.'</td>'; - } - $sTdata.='</tr>'; - } - $this->_setAttributes($aTableAttributes); - return sprintf($sTpl, - $this->_addAttributes(), - $sThead, - $sTdata - ); - } } diff --git a/classes/render-adminlte.class.php b/classes/render-adminlte.class.php index 0f25163c89698484a71b800007d0633e8fbf0991..83cf626c624afc23bc6237422c52be5c2b01d7b4 100755 --- a/classes/render-adminlte.class.php +++ b/classes/render-adminlte.class.php @@ -1,151 +1,171 @@ <?php require_once 'htmlelements.class.php'; /** - * ====================================================================== + * ______________________________________________________________________ + * + * _ __ __ _ + * | || \/ || |__ Institute for Medical Education + * |_||_|\/|_||____| University of Bern + * + * ______________________________________________________________________ * * RENDERER FOR ADNINLTE template https://adminlte.io - * DOCS: https://adminlte.io/docs/3.2/ - * https://adminlte.io/themes/v3/index3.html + * its docs: https://adminlte.io/docs/3.2/ + * https://adminlte.io/themes/v3/index3.html + * + * This is a php class to render + * - grid layout + * - navigation + * - widgets, components and forms * + * DOCS: https://os-docs.iml.unibe.ch/adminlte-renderer/ + * ---------------------------------------------------------------------- + * 2023-09-11 <axel.hahn@unibe.ch> add shadows on card + callout + * 2023-09-27 <axel.hahn@unibe.ch> add form input fields + * 2023-11-17 <axel.hahn@unibe.ch> add tabbed content; "=" renders hamburger item + * 2024-05-03 <axel.hahn@unibe.ch> add line in sidebar menu; add getFormSelect + * 2024-05-10 <axel.hahn@unibe.ch> add support for bootstrap-select in getFormSelect + * 2024-05-18 <axel.hahn@unibe.ch> add variable types + * 2024-07-04 <axel.hahn@unibe.ch> added type declarations * ====================================================================== - * - * @author Axel */ -class renderadminlte { +class renderadminlte +{ - var $aPresets=[ + protected array $aPresets = [ - 'bgcolor'=>[ - 'description'=>'background colors', - 'group'=>'styling', - 'values'=>[ + 'bgcolor' => [ + 'description' => 'background colors', + 'group' => 'styling', + 'values' => [ // https://adminlte.io/themes/v3/pages/UI/general.html - ''=>'no value', - 'indigo'=>'indigo', - 'lightblue'=>'', - 'navy'=>'', - 'purple'=>'', - 'fuchsia'=>'', - 'pink'=>'', - 'maroon'=>'', - 'orange'=>'', - 'lime'=>'', - 'teal'=>'', - 'olive'=>'', - - 'black'=>'black', - 'dark'=>'dark gray', - 'gray'=>'gray', - 'light'=>'light gray', + '' => 'no value', + 'indigo' => 'indigo', + 'lightblue' => '', + 'navy' => '', + 'purple' => '', + 'fuchsia' => '', + 'pink' => '', + 'maroon' => '', + 'orange' => '', + 'lime' => '', + 'teal' => '', + 'olive' => '', + + 'black' => 'black', + 'dark' => 'dark gray', + 'gray' => 'gray', + 'light' => 'light gray', ] ], - - 'type'=>[ - 'description'=>'type or status like info/ warning/ danger to define a color', - 'group'=>'styling', - 'values'=>[ - ''=>'no value', - 'danger'=>'red', - 'info'=>'aqua', - 'primary'=>'blue', - 'secondary'=>'gray', - 'success'=>'green', - 'warning'=>'yellow', - 'dark'=>'dark gray', - 'gray'=>'gray', + + 'type' => [ + 'description' => 'type or status like info/ warning/ danger to define a color', + 'group' => 'styling', + 'values' => [ + '' => 'no value', + 'danger' => 'red', + 'info' => 'aqua', + 'primary' => 'blue', + 'secondary' => 'gray', + 'success' => 'green', + 'warning' => 'yellow', + 'dark' => 'dark gray', + 'gray' => 'gray', ] ], - 'shadow'=>[ - 'description'=>'use a shadow', - 'group'=>'styling', - 'values'=>[ - ''=>'no value', - 'none'=>'none', - 'small'=>'small', - 'regular'=>'regular', - 'large'=>'large' + 'shadow' => [ + 'description' => 'use a shadow', + 'group' => 'styling', + 'values' => [ + '' => 'no value', + 'none' => 'none', + 'small' => 'small', + 'regular' => 'regular', + 'large' => 'large' ] ], - 'size'=>[ - 'description'=>'set a size', - 'group'=>'styling', - 'values'=>[ - ''=>'no value', - 'lg'=>'', - 'sm'=>'', - 'xs'=>'', - 'flat'=>'', + 'size' => [ + 'description' => 'set a size', + 'group' => 'styling', + 'values' => [ + '' => 'no value', + 'lg' => '', + 'sm' => '', + 'xs' => '', + 'flat' => '', ] ], - 'variant'=>[ - 'description'=>'coloring style', - 'group'=>'styling', - 'values'=>[ - ''=>'no value', - 'outline'=>'small stripe on top', - 'solid'=>'full filled widget', - 'gradient'=>'full filled with gradient', + 'variant' => [ + 'description' => 'coloring style', + 'group' => 'styling', + 'values' => [ + '' => 'no value', + 'outline' => 'small stripe on top', + 'solid' => 'full filled widget', + 'gradient' => 'full filled with gradient', ] ], - 'visibility'=>[ - 'description'=>'', - 'group'=>'customizing', - 'values'=>[ - ''=>'no value', - '0'=>'hide', - '1'=>'show', + 'visibility' => [ + 'description' => '', + 'group' => 'customizing', + 'values' => [ + '' => 'no value', + '0' => 'hide', + '1' => 'show', ] ], // for keys: state - 'windowstate'=>[ - 'description'=>'state of a resizable widget', - 'group'=>'customizing', - 'values'=>[ - ''=>'no value', - 'collapsed'=>'header only', - 'maximized'=>'full window', + 'windowstate' => [ + 'description' => 'state of a resizable widget', + 'group' => 'customizing', + 'values' => [ + '' => 'no value', + 'collapsed' => 'header only', + 'maximized' => 'full window', ] ], // for keys: dismissable - 'yesno'=>[ - 'description'=>'', - 'group'=>'customizing', - 'values'=>[ - ''=>'no value', - '0'=>'no', - '1'=>'yes', + 'yesno' => [ + 'description' => '', + 'group' => 'customizing', + 'values' => [ + '' => 'no value', + '0' => 'no', + '1' => 'yes', ] ], ]; - var $_aValueMappings=[ - 'shadow'=>[ - 'default' => '', - 'none' => 'shadow-none', - 'small' => 'shadow-small', - 'regular' => 'shadow', - 'large' => 'shadow-lg', + protected array $_aValueMappings = [ + 'shadow' => [ + 'default' => '', + 'none' => 'shadow-none', + 'small' => 'shadow-small', + 'regular' => 'shadow', + 'large' => 'shadow-lg', ] ]; - var $_aElements=[]; - + protected array $_aElements = []; + /** - * instance of htmlelements + * instance of htmlelements object * @var object */ - var $_oHtml=false; - - + protected object $_oHtml; + + // ---------------------------------------------------------------------- // // CONSTRUCTOR // // ---------------------------------------------------------------------- - public function __construct() { - $this->_oHtml=new htmlelements(); + public function __construct() + { + $this->_oHtml = new htmlelements(); $this->_initElements(); - return true; + // return true; } // ---------------------------------------------------------------------- @@ -153,233 +173,378 @@ class renderadminlte { // PRIVATE FUNCTIONS // // ---------------------------------------------------------------------- - - /** - * verify if an item has a correct value - * it returns false if a key is not defined to be checked - * it returns true if it was validated successfully - * it dies with an errror, if a value check failed - * - * @param string $sType type; key in $_aValidItems; one of bgcolor|color|type|size - * @param string $sValue value to check - * @param string $sReferrer optional: method that called this function - */ - protected function _DELETE_ME___checkValue($sType, $sValue, $sReferrer=false){ - if (!$sValue || !array_key_exists($sType, $this->_aValidItems)){ - return false; - } - if(array_search($sValue, $this->_aValidItems[$sType])===false){ - echo "ERROR: ".($sReferrer ? $sReferrer.' - ' : '')."value [$sValue] is not a valid for type [$sType]; it must be one of ".implode("|", $this->_aValidItems[$sType]).'<br>'; - } - return true; - } /** * used in cosntructor * initialize all element definitions + * @return void */ - protected function _initElements(){ - $this->_aElements=[ + protected function _initElements(): void + { + $this->_aElements = [ // ------------------------------------------------------------ - 'alert'=>[ - 'label'=>'Alert', - 'description'=>'Colored box with title and a text', - 'method'=>'getAlert', - - 'params'=>[ - 'type' => ['select'=>$this->aPresets['type'], 'example_value'=>'warning'], - 'dismissible' => ['select'=>$this->aPresets['yesno'], 'example_value'=>''], - 'title' => [ - 'description'=>'Title in a bit bigger font', - 'group'=>'content', - 'example_value'=>'Alert title' + 'alert' => [ + 'label' => 'Alert', + 'description' => 'Colored box with title and a text', + 'method' => 'getAlert', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'warning'], + 'dismissible' => ['select' => $this->aPresets['yesno'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' ], - 'text' => [ - 'description'=>'Message text', - 'group'=>'content', - 'example_value'=>'I am a message. Read me, please.' + 'title' => [ + 'description' => 'Title in a bit bigger font', + 'group' => 'content', + 'example_value' => 'Alert title' + ], + 'text' => [ + 'description' => 'Message text', + 'group' => 'content', + 'example_value' => 'I am a message. Read me, please.' ], ] ], // ------------------------------------------------------------ - 'badge'=>[ - 'label'=>'Badge', - 'description'=>'Tiny additional info; mostly as counter', - 'method'=>'getBadge', - - 'params'=>[ - 'type' => ['select'=>$this->aPresets['type'], 'example_value'=>'danger'], - 'bgcolor' => ['select'=>$this->aPresets['bgcolor'], 'example_value'=>''], - 'class' => [ - 'group'=>'styling', - 'description'=>'optional: css classes', - 'example_value'=>'' + 'badge' => [ + 'label' => 'Badge', + 'description' => 'Tiny additional info; mostly as counter', + 'method' => 'getBadge', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'danger'], + 'bgcolor' => ['select' => $this->aPresets['bgcolor'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' ], - 'id' => [ - 'group'=>'customizing', - 'description'=>'optional: id attribute', - 'example_value'=>'' + 'id' => [ + 'group' => 'customizing', + 'description' => 'optional: id attribute', + 'example_value' => '' ], - 'title' => [ - 'group'=>'content', - 'description'=>'optional: title attribute for mouseover', - 'example_value'=>'Errors: 5' + 'title' => [ + 'group' => 'content', + 'description' => 'optional: title attribute for mouseover', + 'example_value' => 'Errors: 5' ], - 'text' => [ - 'group'=>'content', - 'description'=>'Text or value in the badge', - 'example_value'=>'5' + 'text' => [ + 'group' => 'content', + 'description' => 'Text or value in the badge', + 'example_value' => '5' ], ] ], // ------------------------------------------------------------ - 'button'=>[ - 'label'=>'Button', - 'description'=>'Buttons<br>In this component you can add other parmeter keys too - these will be added as attributes in the button tag.', - 'method'=>'getButton', - - 'params'=>[ - 'type' => ['select'=>$this->aPresets['type'], 'example_value'=>'primary'], - 'size' => ['select'=>$this->aPresets['size'], 'example_value'=>''], - 'class' => [ - 'group'=>'styling', - 'description'=>'optional: css classes', - 'example_value'=>'' + 'button' => [ + 'label' => 'Button', + 'description' => 'Buttons<br>In this component you can add other parmeter keys too - these will be added as attributes in the button tag.', + 'method' => 'getButton', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'primary'], + 'size' => ['select' => $this->aPresets['size'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' ], - 'text' => [ - 'group'=>'content', - 'description'=>'Text/ html code on the button', - 'example_value'=>'Click me' + 'text' => [ + 'group' => 'content', + 'description' => 'Text/ html code on the button', + 'example_value' => 'Click me' ], ] ], // ------------------------------------------------------------ - 'callout'=>[ - 'label'=>'Callout', - 'description'=>'Kind of infobox', - 'method'=>'getCallout', - - 'params'=>[ - 'type' => ['select'=>$this->aPresets['type'], 'example_value'=>'danger'], - 'class' => [ - 'group'=>'styling', - 'description'=>'optional: css classes', - 'example_value'=>'' + 'callout' => [ + 'label' => 'Callout', + 'description' => 'Kind of infobox', + 'method' => 'getCallout', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'danger'], + 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' ], - 'title' => [ - 'group'=>'content', - 'description'=>'Title in a bit bigger font', - 'example_value'=>'I am a callout' + 'title' => [ + 'group' => 'content', + 'description' => 'Title in a bit bigger font', + 'example_value' => 'I am a callout' ], - 'text' => [ - 'group'=>'content', - 'description'=>'Message text', - 'example_value'=>'Here is some description to whatever.' + 'text' => [ + 'group' => 'content', + 'description' => 'Message text', + 'example_value' => 'Here is some description to whatever.' ], ] ], // ------------------------------------------------------------ - 'card'=>[ - 'label'=>'Card', - 'description'=>'Content box with header, text, footer', - 'method'=>'getCard', - - 'params'=>[ - 'type' => ['select'=>$this->aPresets['type'], 'example_value'=>'primary'], - 'variant' => ['select'=>$this->aPresets['variant'], 'example_value'=>'outline'], - 'class' => [ - 'group'=>'styling', - 'description'=>'optional: css classes', - 'example_value'=>'' + 'card' => [ + 'label' => 'Card', + 'description' => 'Content box with header, text, footer', + 'method' => 'getCard', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'primary'], + 'variant' => ['select' => $this->aPresets['variant'], 'example_value' => 'outline'], + 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' ], - 'state' => [ - 'group'=>'customizing', - 'select'=>$this->aPresets['windowstate'], - 'example_value'=>'' + 'state' => [ + 'group' => 'customizing', + 'select' => $this->aPresets['windowstate'], + 'example_value' => '' ], - - 'tb-collapse' => ['description'=>'show minus symbol as collapse button', 'select'=>$this->aPresets['visibility'], 'example_value'=>''], - 'tb-expand' => ['description'=>'show plus symbol to expand card', 'select'=>$this->aPresets['visibility'], 'example_value'=>''], - 'tb-maximize' => ['description'=>'show maximize button for fullscreen', 'select'=>$this->aPresets['visibility'], 'example_value'=>''], - 'tb-minimize' => ['description'=>'show minimize button to minimize', 'select'=>$this->aPresets['visibility'], 'example_value'=>''], - 'tb-remove' => ['description'=>'show cross symbol to remove card', 'select'=>$this->aPresets['visibility'], 'example_value'=>''], - - 'title' => [ - 'group'=>'content', - 'description'=>'Title in the top row', - 'example_value'=>'I am a card' + + 'tb-collapse' => ['description' => 'show minus symbol as collapse button', 'select' => $this->aPresets['visibility'], 'example_value' => ''], + 'tb-expand' => ['description' => 'show plus symbol to expand card', 'select' => $this->aPresets['visibility'], 'example_value' => ''], + 'tb-maximize' => ['description' => 'show maximize button for fullscreen', 'select' => $this->aPresets['visibility'], 'example_value' => ''], + 'tb-minimize' => ['description' => 'show minimize button to minimize', 'select' => $this->aPresets['visibility'], 'example_value' => ''], + 'tb-remove' => ['description' => 'show cross symbol to remove card', 'select' => $this->aPresets['visibility'], 'example_value' => ''], + + 'title' => [ + 'group' => 'content', + 'description' => 'Title in the top row', + 'example_value' => 'I am a card' ], - 'tools' => [ - 'group'=>'content', - 'description'=>'Html code for the top right', - 'example_value'=>'' + 'tools' => [ + 'group' => 'content', + 'description' => 'Html code for the top right', + 'example_value' => '' ], - 'text' => [ - 'group'=>'content', - 'description'=>'Main content', - 'example_value'=>'Here is some beautiful content.' + 'text' => [ + 'group' => 'content', + 'description' => 'Main content', + 'example_value' => 'Here is some beautiful content.' ], - 'footer' => [ - 'group'=>'content', - 'description'=>'optional: footer content', - 'example_value'=>'Footer' + 'footer' => [ + 'group' => 'content', + 'description' => 'optional: footer content', + 'example_value' => 'Footer' ], ] ], // ------------------------------------------------------------ - 'infobox'=>[ - 'label'=>'Info box', - 'description'=>'Box with icon to highlight a single value; optional with a progress bar', - 'method'=>'getInfobox', - - 'params'=>[ - 'type'=>['select'=>$this->aPresets['type'], 'example_value'=>''], - 'iconbg'=>['select'=>$this->aPresets['type'], 'example_value'=>'info'], - 'shadow'=>['select'=>$this->aPresets['shadow'], 'example_value'=>''], - 'icon'=>[ - 'group'=>'content', - 'description'=>'css class for an icon', - 'example_value'=>'far fa-thumbs-up' + 'infobox' => [ + 'label' => 'Info box', + 'description' => 'Box with icon to highlight a single value; optional with a progress bar', + 'method' => 'getInfobox', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => ''], + 'iconbg' => ['select' => $this->aPresets['type'], 'example_value' => 'info'], + 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' ], - 'text'=>[ - 'group'=>'content', - 'description'=>'short information text', - 'example_value'=>'Likes' + 'icon' => [ + 'group' => 'content', + 'description' => 'css class for an icon', + 'example_value' => 'fa-regular fa-thumbs-up' ], - 'number'=>[ - 'group'=>'content', - 'description'=>'a number to highlight', - 'example_value'=>"41,410" + 'text' => [ + 'group' => 'content', + 'description' => 'short information text', + 'example_value' => 'Likes' ], - 'progressvalue'=>[ - 'group'=>'content', - 'description'=>'optional: progress value 0..100 to draw a progress bar', - 'example_value'=>70 + 'number' => [ + 'group' => 'content', + 'description' => 'a number to highlight', + 'example_value' => "41,410" ], - 'progresstext'=>[ - 'group'=>'content', - 'description'=>'optional: text below progress bar', - 'example_value'=>'70% Increase in 30 Days' + 'progressvalue' => [ + 'group' => 'content', + 'description' => 'optional: progress value 0..100 to draw a progress bar', + 'example_value' => 70 + ], + 'progresstext' => [ + 'group' => 'content', + 'description' => 'optional: text below progress bar', + 'example_value' => '70% Increase in 30 Days' ] ] ], // ------------------------------------------------------------ - 'smallbox'=>[ - 'label'=>'Small box', - 'description'=>'Solid colored box to highlight a single value; optional with a link', - 'method'=>'getSmallbox', - - 'params'=>[ - 'type'=>['select'=>$this->aPresets['type'], 'example_value'=>'info'], - 'shadow'=>['select'=>$this->aPresets['shadow'], 'example_value'=>''], - - 'icon'=>['group'=>'content', 'description'=>'css class for an icon', 'example_value'=>'fas fa-shopping-cart'], - - 'text'=>['group'=>'content', 'description'=>'short information text', 'example_value'=>'New orders'], - 'number'=>['group'=>'content', 'description'=>'a number to highlight', 'example_value'=>"150"], - 'url'=>['group'=>'content', 'description'=>'optional: url to set a link on the bottom', 'example_value'=>'#'], - 'linktext'=>['group'=>'content', 'optional: description'=>'linktext', 'example_value'=>'More info'] + 'smallbox' => [ + 'label' => 'Small box', + 'description' => 'Solid colored box to highlight a single value; optional with a link', + 'method' => 'getSmallbox', + + 'params' => [ + 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'info'], + 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => '' + ], + 'icon' => ['group' => 'content', 'description' => 'css class for an icon', 'example_value' => 'fa-solid fa-shopping-cart'], + + 'text' => ['group' => 'content', 'description' => 'short information text', 'example_value' => 'New orders'], + 'number' => ['group' => 'content', 'description' => 'a number to highlight', 'example_value' => "150"], + 'url' => ['group' => 'content', 'description' => 'optional: url to set a link on the bottom', 'example_value' => '#'], + 'linktext' => ['group' => 'content', 'description' => 'used if a url was given: linked text', 'example_value' => 'More info'] + ] + ], + // ------------------------------------------------------------ + 'input' => [ + 'label' => 'Form: input', + 'description' => 'Input form fiels', + 'method' => 'getFormInput', + + 'params' => [ + 'label' => [ + 'group' => 'styling', + 'description' => 'label for the input field', + 'example_value' => 'Enter something' + ], + 'type' => [ + 'select' => [ + 'description' => 'type or input field', + 'group' => 'styling', + 'values' => [ + + 'button' => 'button', + 'checkbox' => 'checkbox', + 'color' => 'color', + 'date' => 'date', + 'datetime-local' => 'datetime-local', + 'email' => 'email', + 'file' => 'file', + 'hidden' => 'hidden', + 'image' => 'image', + 'month' => 'month', + 'number' => 'number', + 'password' => 'password', + 'radio' => 'radio', + 'range' => 'range', + 'reset' => 'reset', + 'search' => 'search', + 'submit' => 'submit', + 'tel' => 'tel', + 'text' => 'text', + 'time' => 'time', + 'url' => 'url', + 'week' => 'week', + ] + ], + 'example_value' => 'text' + ], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => 'myclass' + ], + 'prepend' => [ + 'group' => 'styling', + 'description' => 'optional: content on input start', + 'example_value' => '' + ], + 'append' => [ + 'group' => 'styling', + 'description' => 'optional: content on input end', + 'example_value' => '' + ], + 'name' => [ + 'group' => 'content', + 'description' => 'name attribute', + 'example_value' => 'firstname' + ], + 'value' => [ + 'group' => 'content', + 'description' => 'Value', + 'example_value' => 'Jack' + ], + ] + ], + // ------------------------------------------------------------ + // WIP + 'select' => [ + 'label' => 'Form: select', + 'description' => 'Select box', + 'method' => 'getFormSelect', + + 'params' => [ + 'label' => [ + 'group' => 'styling', + 'description' => 'label for the select field', + 'example_value' => 'Enter text' + ], + 'bootstrap-select' => [ + 'select' => [ + 'description' => 'Enable bootstrap-select plugin', + 'group' => 'styling', + 'values' => [ + '0' => 'no', + '1' => 'yes', + ] + ] + ], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => 'myclass' + ], + 'options' => [ + 'example_value' => [ + ["value" => "1", "label" => "one"], + ["value" => "2", "label" => "two"], + ["value" => "3", "label" => "three"], + ], + ] + ], + ], + // ------------------------------------------------------------ + 'textarea' => [ + 'label' => 'Form: textarea', + 'description' => 'textarea or html editor', + 'method' => 'getFormTextarea', + + 'params' => [ + 'label' => [ + 'group' => 'styling', + 'description' => 'label for the input field', + 'example_value' => 'Enter text' + ], + 'type' => [ + 'select' => [ + 'description' => 'type or input field', + 'group' => 'styling', + 'values' => [ + '' => 'text', + 'html' => 'html editor', + ] + ], + ], + 'class' => [ + 'group' => 'styling', + 'description' => 'optional: css classes', + 'example_value' => 'myclass' + ], + 'name' => [ + 'group' => 'content', + 'description' => 'name attribute', + 'example_value' => 'textdata' + ], + 'value' => [ + 'group' => 'content', + 'description' => 'Value', + 'example_value' => 'Here is some text...' + ], ] ], ]; @@ -388,12 +553,16 @@ class renderadminlte { * helper function: a shortcut for $this->_oHtml->getTag * @param string $sTag name of html tag * @param array $aAttributes array of its attributes + * @param string $sContent content between opening and closing tag + * @param bool $bClosetag flag: write a closing tag or not? default: true + * @return string */ - protected function _tag($sTag, $aAttributes, $sContent=false){ - if ($sContent){ - $aAttributes['label']=(isset($aAttributes['label']) ? $aAttributes['label'] : '') . $sContent; + protected function _tag(string $sTag, array $aAttributes, string $sContent = '', bool $bClosetag = true): string + { + if ($sContent) { + $aAttributes['label'] = (isset($aAttributes['label']) ? $aAttributes['label'] : '') . $sContent; } - return $this->_oHtml->getTag($sTag, $aAttributes); + return $this->_oHtml->getTag($sTag, $aAttributes, $bClosetag); } // ---------------------------------------------------------------------- // @@ -405,25 +574,29 @@ class renderadminlte { * render a page by using template * @param string $stemplate html template with placeholders * @param array $aReplace key = what to replace .. value = new value + * @return string */ - public function render($sTemplate, $aReplace){ + public function render(string $sTemplate, array $aReplace): string + { return str_replace( array_keys($aReplace), array_values($aReplace), $sTemplate ); } - /** * add a wrapper: wrap some content into a tag * + * @param string $sTag name of html tag + * @param array $aOptions array of its attributes * @param string $sContent html content inside * @return string */ - public function addWrapper($sTag, $aOptions, $sContent){ - $aOptions['label']=$sContent; - return $this->_tag($sTag, $aOptions)."\n"; + public function addWrapper(string $sTag, array $aOptions, string $sContent): string + { + $aOptions['label'] = $sContent; + return $this->_tag($sTag, $aOptions) . PHP_EOL; } // ---------------------------------------------------------------------- @@ -432,7 +605,6 @@ class renderadminlte { // // ---------------------------------------------------------------------- - /** * get a single navigation item on top bar * @@ -440,73 +612,93 @@ class renderadminlte { * @param integer $iLevel Menu level; 1=top bar; 2=pupup menu with subitems * @return string */ - public function getNavItem($aLink, $iLevel=1){ + public function getNavItem(array $aLink, int $iLevel = 1): string + { static $iCounter; - if(!isset($iCounter)){ - $iCounter=0; + if (!isset($iCounter)) { + $iCounter = 0; } - - // special menu entry: horizontal bar (label is "-") - if($aLink['label']=='-'){ - return '<div class="dropdown-divider"></div>'; + + switch ($aLink['label']) { + // special menu entry: horizontal bar (label is "-") + case '-': + return '<div class="dropdown-divider"></div>'; + break; + + // special menu entry: hamburger menu item (label is "=") + case '=': + return '<li class="nav-item"><a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a></li>'; + break; + + // special menu entry: hamburger menu item (label is "|") + // requires css: .navbar-nav li.divider{border-left: 1px solid rgba(0,0,0,0.2);} + case '|': + return '<li class="divider"></li>'; + break; } - $aChildren=isset($aLink['children']) && is_array($aLink['children']) && count($aLink['children']) ? $aLink['children'] : false; + $aChildren = isset($aLink['children']) && is_array($aLink['children']) && count($aLink['children']) ? $aLink['children'] : false; - $aLink['class']='nav-link' - . (isset($aLink['class']) ? ' '.$aLink['class'] : '') + $aLink['class'] = 'nav-link' + . (isset($aLink['class']) ? ' ' . $aLink['class'] : '') . ($aChildren ? ' dropdown-toggle' : '') - ; - if($aChildren){ + ; + if ($aChildren) { $iCounter++; - $sNavId="navbarDropdown".$iCounter; - $aLink=array_merge($aLink,[ - 'id'=>$sNavId, - 'role'=>"button", - 'data-toggle'=>"dropdown", - 'aria-haspopup'=>"true", - 'aria-expanded'=>"false", + $sNavId = "navbarDropdown" . $iCounter; + $aLink = array_merge($aLink, [ + 'id' => $sNavId, + 'role' => "button", + 'data-toggle' => "dropdown", + 'aria-haspopup' => "true", + 'aria-expanded' => "false", ]); unset($aLink['children']); // remove from html attributes to draw } - $sReturn=$this->_tag('a', $aLink)."\n"; - if($aChildren){ + $sReturn = $this->_tag('a', $aLink) . "\n"; + if ($aChildren) { $iLevel++; - $sReturn.=$this->addWrapper( - 'div', - [ - 'aria-labelledby'=> $sNavId, - 'class'=>'dropdown-menu' - ], - $this->getNavItems($aChildren, $iLevel) - )."\n"; + $sReturn .= $this->addWrapper( + 'div', + [ + 'aria-labelledby' => $sNavId, + 'class' => 'dropdown-menu' + ], + $this->getNavItems($aChildren, $iLevel) + ) . "\n"; $iLevel--; } - - if($iLevel==1){ - $sLiClass='nav-item'.($aChildren ? ' dropdown' : ''); - $sReturn=$this->addWrapper( - 'li', - ['class'=>$sLiClass], - $sReturn + + if ($iLevel == 1) { + $sLiClass = 'nav-item' . ($aChildren ? ' dropdown' : ''); + $sReturn = $this->addWrapper( + 'li', + ['class' => $sLiClass], + $sReturn ); } return $sReturn; } + /** - * get a navigation item for top bar + * get html code for navigation on top bar * + * @param array $aLinks array of navigation items + * @param int $iLevel current navigation level; default: 1 + * @return string|bool */ - public function getNavItems($aLinks, $iLevel=1){ - $sReturn=''; - if (!$aLinks || !is_array($aLinks) || !count($aLinks)){ + public function getNavItems(array $aLinks, int $iLevel = 1): string|bool + { + $sReturn = ''; + if (!$aLinks || !is_array($aLinks) || !count($aLinks)) { return false; } - foreach($aLinks as $aLink){ - $sReturn.=$this->getNavItem($aLink, $iLevel); + foreach ($aLinks as $aLink) { + $sReturn .= $this->getNavItem($aLink, $iLevel); } return $sReturn; } + /** * get a top left navigation for a top navigation bar * @@ -528,62 +720,94 @@ class renderadminlte { * .'</nav>' * </code> * - * @param array $aNavitems array of navigation items/ tree - * @param array $aUlOptions array of html attrubutes for wrapping UL tag + * @param array $aNavItems array of navigation items/ tree + * @param array $aUlOptions array of html attrubutes for wrapping UL tag + * @param array $aNavItemsRight array of html attrubutes for wrapping UL tag + * @param array $aUlOptionsRight array of html attrubutes for wrapping UL tag * @return string */ - public function getTopNavigation($aNavItems, $aUlOptions=['class'=>'navbar-nav']){ - array_unshift($aNavItems, ['class'=>'nav-link', 'data-widget'=>'pushmenu', 'href'=>'#', 'role'=>'button', 'label'=>'<i class="fas fa-bars"></i>']); - return $this->addWrapper('ul', $aUlOptions, $this->getNavItems($aNavItems)); + public function getTopNavigation(array $aNavItems, array $aUlOptions = [], array $aNavItemsRight = [], array $aUlOptionsRight = []): string + { + // array_unshift($aNavItems, ['class'=>'nav-link', 'data-widget'=>'pushmenu', 'href'=>'#', 'role'=>'button', 'label'=>'<i class="fa-solid fa-bars"></i>']); + $aUlOptLeft = count($aUlOptions) ? $aUlOptions : ['class' => 'navbar-nav']; + $aUlOptRight = count($aUlOptionsRight) ? $aUlOptionsRight : ['class' => 'navbar-nav ml-auto']; + return $this->addWrapper('ul', $aUlOptLeft, $this->getNavItems($aNavItems)) + . (count($aNavItemsRight) + ? $this->addWrapper('ul', $aUlOptRight, $this->getNavItems($aNavItemsRight)) + : '' + ) + ; } // ---------------------------------------------------------------------- /** - * get a navigation items for sidebar + * Get a navigation items for sidebar + * Links can be nested with the key "children". + * + * Remark: for a horizontal line ($aLink['label']='-') this css is required + * .nav-item hr{color: #505860; border-top: 1px solid; height: 1px; padding: 0; margin: 0; } + * * @param array $aLinks list of link items + * @return string|bool */ - public function getNavi2Items($aLinks){ - $sReturn=''; - if (!$aLinks || !is_array($aLinks) || !count($aLinks)){ + public function getNavi2Items(array $aLinks): string|bool + { + $sReturn = ''; + if (!$aLinks || !is_array($aLinks) || !count($aLinks)) { return false; } - foreach($aLinks as $aLink){ + + foreach ($aLinks as $aLink) { + + if ($aLink['label'] == '-') { + // TODO: draw a nice line + $sReturn .= '<li class="nav-item"><hr></li>'; + continue; + } // to render active or open links: - $aLink['class']='nav-link' . (isset($aLink['class']) ? ' '.$aLink['class'] : ''); - - $aChildren=isset($aLink['children']) ? $aLink['children'] : false; - $aLiClass='nav-item' . ($aChildren && strstr($aLink['class'], 'active') ? ' menu-open' : ''); - $sSubmenu=''; - if($aChildren){ - unset($aLink['children']); - $aLink['label'].='<i class="right fas fa-angle-left"></i>'; - $sSubmenu.=$this->getSidebarNavigation($aChildren, ['class'=>'nav nav-treeview']); + $aLink['class'] = 'nav-link' . (isset($aLink['class']) ? ' ' . $aLink['class'] : ''); + + $aChildren = isset($aLink['children']) ? $aLink['children'] : false; + $aLiClass = 'nav-item' . ($aChildren && strstr($aLink['class'], 'active') ? ' menu-open' : ''); + $sSubmenu = ''; + if ($aChildren) { + unset($aLink['children']); + $aLink['label'] .= '<i class="right fa-solid fa-angle-left"></i>'; + $sSubmenu .= $this->getSidebarNavigation($aChildren, ['class' => 'nav nav-treeview']); } - $aLink['label']=$this->addWrapper('p', [], $aLink['label']); - $sReturn.=$this->addWrapper( - 'li', ['class'=>$aLiClass], - $this->_tag('a', $aLink).$sSubmenu - )."\n"; + $aLink['label'] = $this->addWrapper('p', [], $aLink['label']); + $sReturn .= $this->addWrapper( + 'li', + ['class' => $aLiClass], + $this->_tag('a', $aLink) . $sSubmenu + ) . "\n"; } return $sReturn; } /** + * get html code for sidebar navigation * + * @param array $aNavItems navigation item + * @param array $aUlOptions aatributes for UL tag + * @param string */ - public function getSidebarNavigation($aNavItems, $aUlOptions=[ - 'class'=>'nav nav-pills nav-sidebar flex-column nav-flat_ nav-child-indent', - 'data-widget'=>'treeview', - 'role'=>'menu', - 'data-accordion'=>'false' - ]) - { + public function getSidebarNavigation( + array $aNavItems, + array $aUlOptions = [ + 'class' => 'nav nav-pills nav-sidebar flex-column nav-flat_ nav-child-indent', + 'data-widget' => 'treeview', + 'role' => 'menu', + 'data-accordion' => 'false' + ] + ): string { return $this->addWrapper( - 'ul', $aUlOptions, - $this->getNavi2Items($aNavItems) - )."\n"; + 'ul', + $aUlOptions, + $this->getNavi2Items($aNavItems) + ) . "\n"; } // ---------------------------------------------------------------------- @@ -591,15 +815,16 @@ class renderadminlte { // PUBLIC FUNCTIONS :: CONTENT - BASIC FUNCTIONS // // ---------------------------------------------------------------------- - + /** * add page row * * @param string $sContent html content inside * @return string */ - public function addRow($sContent){ - return $this->addWrapper('div', ['class'=>'row'], $sContent); + public function addRow(string $sContent): string + { + return $this->addWrapper('div', ['class' => 'row'], $sContent); } /** @@ -610,8 +835,9 @@ class renderadminlte { * @param string $sFloat css value for float attribute; default=false * @return string */ - public function addCol($sContent, $iCols=6, $sFloat=false){ - return $this->addWrapper('div', ['class'=>'col-md-'.$iCols, 'style'=>'float:'.$sFloat], $sContent); + public function addCol(string $sContent, int $iCols = 6, string $sFloat = ''): string + { + return $this->addWrapper('div', ['class' => 'col-md-' . $iCols, 'style' => 'float:' . $sFloat], $sContent); } // ---------------------------------------------------------------------- @@ -620,49 +846,52 @@ class renderadminlte { // // ---------------------------------------------------------------------- - /** * get a list of all defined components that can be rendered - * @param {bool} $bSendData flag: send including subkeys of the hash; default: false (keys only) - * @return {array} + * @param bool $bSendData flag: send including subkeys of the hash; default: false (keys only) + * @return array */ - public function getComponents($bSendData=false){ + public function getComponents(bool $bSendData = false): array + { return $bSendData ? $this->_aElements : array_keys($this->_aElements) ; } + /** * get data of a component - * @param {string} $sComponent id of the component - * @return {array} + * @param string $sComponent id of the component + * @return array|bool */ - public function getComponent($sComponent){ - if(!isset($this->_aElements[$sComponent])){ + public function getComponent(string $sComponent): array|bool + { + if (!isset($this->_aElements[$sComponent])) { return false; } - $aReturn=array_merge(['id'=>$sComponent], $this->_aElements[$sComponent]); + $aReturn = array_merge(['id' => $sComponent], $this->_aElements[$sComponent]); unset($aReturn['params']); return $aReturn; } /** * get parameter keys of a component - * @param {string} $sComponent id of the component - * @param {bool} $bSendData flag: send including subkeys of the hash; default: false (keys only) - * @return {array} + * @param string $sComponent id of the component + * @param bool $bSendData flag: send including subkeys of the hash; default: false (keys only) + * @return array|bool */ - public function getComponentParamkeys($sComponent, $bSendData=false){ - if(!isset($this->_aElements[$sComponent])){ + public function getComponentParamkeys(string $sComponent, bool $bSendData = false) + { + if (!isset($this->_aElements[$sComponent])) { return false; } - $aKeys=array_keys($this->_aElements[$sComponent]['params']); - if(!$bSendData){ + $aKeys = array_keys($this->_aElements[$sComponent]['params']); + if (!$bSendData) { return $aKeys; } - $aReturn=[]; - foreach($aKeys as $sKey){ - $aReturn[$sKey]=$this->getComponentParamkey($sComponent, $sKey); + $aReturn = []; + foreach ($aKeys as $sKey) { + $aReturn[$sKey] = $this->getComponentParamkey($sComponent, $sKey); } // $aReturn=$this->_aElements[$sComponent]['params']; return $aReturn; @@ -670,34 +899,36 @@ class renderadminlte { /** * get information a parameter keys of a component - * @param {string} $sComponent id of the component - * @param {string} $sKey key in the options array - * @return {array} + * @param string $sComponent id of the component + * @param string $sKey key in the options array + * @return array|bool */ - public function getComponentParamkey($sComponent, $sKey){ - if(!isset($this->_aElements[$sComponent]['params'][$sKey])){ + public function getComponentParamkey(string $sComponent, string $sKey): array|bool + { + if (!isset($this->_aElements[$sComponent]['params'][$sKey])) { return false; } - $aReturn=$this->_aElements[$sComponent]['params'][$sKey]; + $aReturn = $this->_aElements[$sComponent]['params'][$sKey]; // get description from a preset - if(!isset($aReturn['description']) && isset($aReturn['select']['description'])){ - $aReturn['description']=$aReturn['select']['description']; + if (!isset($aReturn['description']) && isset($aReturn['select']['description'])) { + $aReturn['description'] = $aReturn['select']['description']; } - if(!isset($aReturn['group']) && isset($aReturn['select']['group'])){ - $aReturn['group']=$aReturn['select']['group']; + if (!isset($aReturn['group']) && isset($aReturn['select']['group'])) { + $aReturn['group'] = $aReturn['select']['group']; } return $aReturn; } /** * get a flat list of valid parameters for a key in a component - * @param {string} $sComponent id of the component - * @param {string} $sKey key in the options array - * @return {array} + * @param string $sComponent id of the component + * @param string $sKey key in the options array + * @return array|bool */ - public function getValidParamValues($sComponent, $sKey){ - $aOptionkey=$this->getComponentParamkey($sComponent, $sKey); - if(!$aOptionkey || !isset($aOptionkey['select']['values'])){ + public function getValidParamValues(string $sComponent, string $sKey): array|bool + { + $aOptionkey = $this->getComponentParamkey($sComponent, $sKey); + if (!$aOptionkey || !isset($aOptionkey['select']['values'])) { return false; } return array_keys($aOptionkey['select']['values']); @@ -710,43 +941,49 @@ class renderadminlte { * helper: add a css value with prefix * this handles option keys in get[COMPONENT] methods * if a value is set then this function returns a space + prefix (param 2) + value - * @param {string} $sValue option value - * @param {string} $sPrefix prefix in front of css value - * @return {string} + * @param string $sValue option value + * @param string $sPrefix prefix in front of css value + * @return string */ - protected function _addClassValue($sValue, $sPrefix=''){ - return $sValue ? ' '.$sPrefix.$sValue : ''; + protected function _addClassValue(string $sValue, string $sPrefix = ''): string + { + return $sValue ? ' ' . $sPrefix . $sValue : ''; } /** * helper function for get[COMPONENTNAME] methods: - * ensure that all wanted keys exist in an array. Non existing keys will be added with value false + * ensure that all wanted keys exist in an array. Non existing keys will + * be added with value false + * + * @param string $sComponent id of the component + * @param array $aOptions hash with keys for all options * @return array */ - protected function _ensureOptions($sComponent, $aOptions=[]){ + protected function _ensureOptions(string $sComponent, array $aOptions = []): array + { - $aParams=$this->getComponentParamkeys($sComponent, 0); - if(!$aParams){ - $aOptions['_infos'][]="Warning: no definition was found for component $sComponent."; + $aParams = $this->getComponentParamkeys($sComponent, 0); + if (!$aParams) { + $aOptions['_infos'][] = "Warning: no definition was found for component $sComponent."; return $aOptions; } - foreach ($aParams as $sKey){ - if(!isset($aOptions) || !isset($aOptions[$sKey])){ - $aOptions[$sKey]=false; - if(!isset($aOptions['_infos'])){ - $aOptions['_infos']=[]; + foreach ($aParams as $sKey) { + if (!isset($aOptions) || !isset($aOptions[$sKey])) { + $aOptions[$sKey] = false; + if (!isset($aOptions['_infos'])) { + $aOptions['_infos'] = []; } - $aOptions['_infos'][]="added missing key: $sKey"; + $aOptions['_infos'][] = "added missing key: $sKey"; } // $aParamdata - $aValidvalues=$this->getValidParamValues($sComponent, $sKey); - if($aValidvalues){ - if(array_search($aOptions[$sKey], $aValidvalues)===false){ - echo "ERROR: [".$sComponent."] value "".$aOptions[$sKey]."" is not a valid for param key [".$sKey."]; it must be one of ".implode("|", $aValidvalues).'<br>'; + $aValidvalues = $this->getValidParamValues($sComponent, $sKey); + if ($aValidvalues) { + if (array_search($aOptions[$sKey], $aValidvalues) === false) { + echo "ERROR: [" . $sComponent . "] value "" . $aOptions[$sKey] . "" is not a valid for param key [" . $sKey . "]; it must be one of " . implode("|", $aValidvalues) . '<br>'; } } - + // $this->_checkValue($sKey, $aOptions[$sKey], __METHOD__); } // echo '<pre>' . print_r($aOptions, 1) . '</pre>'; @@ -762,35 +999,36 @@ class renderadminlte { * return a alert box * https://adminlte.io/themes/v3/pages/UI/general.html * - * @param type $aOptions hash with keys for all options + * @param array $aOptions hash with keys for all options * - type - one of [none]|danger|info|primary|success|warning * - dismissible - if dismissible - one of true|false; default: false * - title * - text * @return string */ - public function getAlert($aOptions){ - $aOptions=$this->_ensureOptions('alert', $aOptions); - $aAlertIcons=[ - 'danger'=>'icon fas fa-ban', - 'info'=>'icon fas fa-info', - 'warning'=>'icon fas fa-exclamation-triangle', - 'success'=>'icon fas fa-check', + public function getAlert(array $aOptions): string + { + $aOptions = $this->_ensureOptions('alert', $aOptions); + $aAlertIcons = [ + 'danger' => 'icon fa-solid fa-ban', + 'info' => 'icon fa-solid fa-info', + 'warning' => 'icon fa-solid fa-exclamation-triangle', + 'success' => 'icon fa-solid fa-check', ]; - $aElement=[ - 'class'=>'alert' + $aElement = [ + 'class' => 'alert' . $this->_addClassValue($aOptions['type'], 'alert-') . $this->_addClassValue($aOptions['dismissible'], 'alert-') - , - 'label'=>'' + , + 'label' => '' . ($aOptions['dismissible'] ? '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' : '') . $this->_tag('h5', [ - 'label'=> '' - .(isset($aAlertIcons[$aOptions['type']]) ? '<i class="'.$aAlertIcons[$aOptions['type']].'"></i> ' : '') - .$aOptions['title'] - ]) - .$aOptions['text'] + 'label' => '' + . (isset($aAlertIcons[$aOptions['type']]) ? '<i class="' . $aAlertIcons[$aOptions['type']] . '"></i> ' : '') + . $aOptions['title'] + ]) + . $aOptions['text'] ]; return $this->_tag('div', $aElement); @@ -812,20 +1050,22 @@ class renderadminlte { * - text - visible text * - title - optional: title attribute * - type - one of [none]|danger|dark|info|primary|secondary|success|warning + * @return string */ - public function getBadge($aOptions){ - $aOptions=$this->_ensureOptions('badge', $aOptions); - $aElement=[]; - $aElement['class']='badge' - . $this->_addClassValue($aOptions['class'], '') - . $this->_addClassValue($aOptions['type'], 'badge-') + public function getBadge(array $aOptions): string + { + $aOptions = $this->_ensureOptions('badge', $aOptions); + $aElement = []; + $aElement['class'] = 'badge' + . $this->_addClassValue($aOptions['class'], '') + . $this->_addClassValue($aOptions['type'], 'badge-') . $this->_addClassValue($aOptions['bgcolor'], 'bg-') - ; - if ($aOptions['id']){ - $aElement['id']=$aOptions['id']; + ; + if ($aOptions['id']) { + $aElement['id'] = $aOptions['id']; } - $aElement['title']=$aOptions['title']; - $aElement['label']=$aOptions['text']; + $aElement['title'] = $aOptions['title']; + $aElement['label'] = $aOptions['text']; return $this->_tag('span', $aElement); } @@ -837,7 +1077,7 @@ class renderadminlte { * https://adminlte.io/themes/v3/pages/UI/buttons.html * * <button type="button" class="btn btn-block btn-default">Default</button> - * @param type $aOptions hash with keys for all options + * @param array $aOptions hash with keys for all options * - type - one of [none]|danger|dark|info|primary|secondary|success|warning * - size - one of [none]|lg|sm|xs|flat * - class - any css class for customizing, eg. "disabled" @@ -845,16 +1085,17 @@ class renderadminlte { * - text - text on button * @return string */ - public function getButton($aOptions){ - $aOptions=$this->_ensureOptions('button', $aOptions); - $aElement=$aOptions; - $aElement['class']='btn' - . $this->_addClassValue($aOptions['type'], 'btn-') - . $this->_addClassValue($aOptions['size'], 'btn-') - . $this->_addClassValue($aOptions['class'], '') - ; - $aElement['label']=$aOptions['text'] ? $aOptions['text'] : ' '; - foreach(['_infos', 'type', 'size', 'icon', 'text'] as $sDeleteKey){ + public function getButton(array $aOptions): string + { + $aOptions = $this->_ensureOptions('button', $aOptions); + $aElement = $aOptions; + $aElement['class'] = 'btn' + . $this->_addClassValue($aOptions['type'], 'btn-') + . $this->_addClassValue($aOptions['size'], 'btn-') + . $this->_addClassValue($aOptions['class'], '') + ; + $aElement['label'] = $aOptions['text'] ? $aOptions['text'] : ' '; + foreach (['_infos', 'type', 'size', 'icon', 'text'] as $sDeleteKey) { unset($aElement[$sDeleteKey]); } return $this->_tag('button', $aElement); @@ -864,27 +1105,31 @@ class renderadminlte { * get a callout (box with coloered left border; has type, title + text) * https://adminlte.io/themes/v3/pages/UI/general.html * - * @param type $aOptions hash with keys for all options + * @param array $aOptions hash with keys for all options * >> styling * - type - one of [none]|danger|dark|info|primary|secondary|success|warning * - class - optional css class - * + * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * * >> texts/ html content * - title - text: title of the card * - text - text: content of the card * @return string */ - public function getCallout($aOptions){ - $aOptions=$this->_ensureOptions('callout', $aOptions); - $sClass='callout' - . $this->_addClassValue($aOptions['type'], 'callout-') - . $this->_addClassValue($aOptions['class'], '') - ; + public function getCallout(array $aOptions): string + { + $aOptions = $this->_ensureOptions('callout', $aOptions); + $sClass = 'callout' + . $this->_addClassValue($aOptions['type'], 'callout-') + . $this->_addClassValue($aOptions['class'], '') + . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) + ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') + ; return $this->addWrapper( - 'div', ['class'=>$sClass], - ($aOptions['title'] ? $this->_tag('h5', ['label'=>$aOptions['title']]) : '') - .($aOptions['text'] ? $this->_tag('p', ['label'=>$aOptions['text']]) : '') + 'div', + ['class' => $sClass], + ($aOptions['title'] ? $this->_tag('h5', ['label' => $aOptions['title']]) : '') + . ($aOptions['text'] ? $this->_tag('p', ['label' => $aOptions['text']]) : '') ); } @@ -893,13 +1138,14 @@ class renderadminlte { * https://adminlte.io/docs/3.2/components/cards.html * https://adminlte.io/docs/3.2/javascript/card-widget.html * - * @param type $aOptions hash with keys for all options + * @param array $aOptions hash with keys for all options * >> styling * - variant: "default" - titlebar is colored * "outline" - a small stripe on top border is colored * "solid" - whole card is colored * "gradient" - whole card is colored with a gradient * - type - one of [none]|danger|dark|info|primary|secondary|success|warning + * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * - class - any css class for customizing, eg. "disabled" * - state - one of [none]|collapsed|maximized * @@ -917,62 +1163,70 @@ class renderadminlte { * - footer - text: footer of the card * @return string */ - public function getCard($aOptions){ - $aOptions=$this->_ensureOptions('card', $aOptions); + public function getCard(array $aOptions): string + { + $aOptions = $this->_ensureOptions('card', $aOptions); // css class prefixes based on "variant" value - $aVariants=[ - 'default' => 'card-', - 'outline' => 'card-outline card-', - 'solid' => 'bg-', + $aVariants = [ + 'default' => 'card-', + 'outline' => 'card-outline card-', + 'solid' => 'bg-', 'gradient' => 'bg-gradient-', ]; // window states: css class and toolbar buttons to add - $aStates=[ - 'collapsed'=>[ 'class'=>'collapsed-card', 'tool'=>'tb-expand'], - 'maximized'=>[ 'class'=>'maximized-card', 'tool'=>'tb-minimize'], + $aStates = [ + 'collapsed' => ['class' => 'collapsed-card', 'tool' => 'tb-expand'], + 'maximized' => ['class' => 'maximized-card', 'tool' => 'tb-minimize'], ]; - $aTools=[ - 'tb-collapse'=>'<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-minus"></i></button>', - 'tb-expand'=>'<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-plus"></i></button>', + $aTools = [ + 'tb-collapse' => '<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fa-solid fa-minus"></i></button>', + 'tb-expand' => '<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fa-solid fa-plus"></i></button>', - 'tb-maximize'=>'<button type="button" class="btn btn-tool" data-card-widget="maximize"><i class="fas fa-expand"></i></button>', - 'tb-minimize'=>'<button type="button" class="btn btn-tool" data-card-widget="maximize"><i class="fas fa-compress"></i></button>', + 'tb-maximize' => '<button type="button" class="btn btn-tool" data-card-widget="maximize"><i class="fa-solid fa-expand"></i></button>', + 'tb-minimize' => '<button type="button" class="btn btn-tool" data-card-widget="maximize"><i class="fa-solid fa-compress"></i></button>', - 'tb-remove'=>'<button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fas fa-times"></i></button>', + 'tb-remove' => '<button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fa-solid fa-times"></i></button>', ]; // print_r($aOptions); - $sVariantPrefix=$aVariants[$aOptions['variant']] ? $aVariants[$aOptions['variant']] : $aVariants['default']; - $sClass='card' - . $this->_addClassValue($aOptions['type'], $sVariantPrefix) - . $this->_addClassValue($aOptions['class'], '') - ; + $sVariantPrefix = isset($aVariants[$aOptions['variant']]) ? $aVariants[$aOptions['variant']] : $aVariants['default']; + $sClass = 'card' + . $this->_addClassValue($aOptions['type'], $sVariantPrefix) + . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) + ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') + . $this->_addClassValue($aOptions['class'], '') + ; // check window state - foreach($aStates as $sStatus=>$aStatus){ - if($aOptions['state']===$sStatus){ - $sClass.=' '.$aStatus['class']; - $aOptions[$aStatus['tool']]=1; + foreach ($aStates as $sStatus => $aStatus) { + if ($aOptions['state'] === $sStatus) { + $sClass .= ' ' . $aStatus['class']; + $aOptions[$aStatus['tool']] = 1; } } // add toolbar buttons - from given options or by window state - foreach($aTools as $sTool=>$sHtml){ - $aOptions['tools'].=($aOptions[$sTool] ? $sHtml : ''); + foreach ($aTools as $sTool => $sHtml) { + $aOptions['tools'] .= ($aOptions[$sTool] ? $sHtml : ''); } // build parts of the card - $sCardHeader=$this->addWrapper('div', ['class'=>'card-header'], - $this->_tag('h3', ['class'=>'card-title', 'label'=>$aOptions['title']]) - . ($aOptions['tools'] ? $this->_tag('div', ['class'=>'card-tools', 'label'=>$aOptions['tools']]) : '') - ); + $sCardHeader = $aOptions['title'] + ? $this->addWrapper( + 'div', + ['class' => 'card-header'], + $this->_tag('h3', ['class' => 'card-title', 'label' => $aOptions['title']]) + . ($aOptions['tools'] ? $this->_tag('div', ['class' => 'card-tools', 'label' => $aOptions['tools']]) : '') + ) + : '' + ; - $sCardBody=$this->_tag('div', ['class'=>'card-body', 'label'=>$aOptions['text']]); - $sCardFooter=$aOptions['footer'] ? $this->_tag('div', ['class'=>'card-footer', 'label'=>$aOptions['footer']]) : ''; + $sCardBody = $this->_tag('div', ['class' => 'card-body', 'label' => $aOptions['text']]); + $sCardFooter = $aOptions['footer'] ? $this->_tag('div', ['class' => 'card-footer', 'label' => $aOptions['footer']]) : ''; // merge all - return $this->addWrapper('div', ['class'=>$sClass], $sCardHeader.$sCardBody.$sCardFooter); + return $this->addWrapper('div', ['class' => $sClass], $sCardHeader . $sCardBody . $sCardFooter); } @@ -981,7 +1235,7 @@ class renderadminlte { * A colored box with large icon, text and a value. * https://adminlte.io/docs/3.2/components/boxes.html * - * @param type $aOptions hash with keys for all options + * @param array $aOptions hash with keys for all options * styling: * - type - color of the box; one of [none]|danger|dark|info|primary|secondary|success|warning * - iconbg - background color or type of icon; use it for default type (type="") @@ -995,52 +1249,57 @@ class renderadminlte { * - progresstext - text below progress bar * @return string */ - public function getInfobox($aOptions){ - $aOptions=$this->_ensureOptions('infobox', $aOptions); + public function getInfobox(array $aOptions): string + { + $aOptions = $this->_ensureOptions('infobox', $aOptions); // print_r($aOptions); - $sClass='info-box' - . $this->_addClassValue($aOptions['type'], 'bg-') - . $this->_addClassValue($aOptions['class'], '') - .($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) - ? ' '.$this->_aValueMappings['shadow'][$aOptions['shadow']] : '') - ; - + $sClass = 'info-box' + . $this->_addClassValue($aOptions['type'], 'bg-') + . $this->_addClassValue($aOptions['class'], '') + . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) + ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') + ; + // build parts - $sIcon=$aOptions['icon'] + $sIcon = $aOptions['icon'] ? $this->addWrapper("span", [ - 'class'=>'info-box-icon'.($aOptions['iconbg'] ? ' bg-'.$aOptions['iconbg'] : '') - ], $this->_tag('i',['class'=>$aOptions['icon']])) + 'class' => 'info-box-icon' . ($aOptions['iconbg'] ? ' bg-' . $aOptions['iconbg'] : '') + ], $this->_tag('i', ['class' => $aOptions['icon']])) : '' - ; - $sContent=$this->addWrapper("div", ['class'=>'info-box-content'], + ; + $sContent = $this->addWrapper( + "div", + ['class' => 'info-box-content'], '' - . ($aOptions['text'] ? $this->_tag('span', ['class'=>'info-box-text', 'label'=>$aOptions['text']]) : '') - . ($aOptions['number'] ? $this->_tag('span', ['class'=>'info-box-number', 'label'=>$aOptions['number']]) : '') - . ($aOptions['progressvalue']!==false && $aOptions['progressvalue']!=='' - ? $this->addWrapper('div', ['class'=>'progress'], - $this->_tag('div', ['class'=>'progress-bar'. ($aOptions['iconbg'] ? ' bg-'.$aOptions['iconbg'] : ''), 'style'=>'width: '.(int)$aOptions['progressvalue'].'%' ]) - ) - . ($aOptions['progresstext'] ? $this->_tag('span', ['class'=>'progress-description', 'label'=>$aOptions['progresstext']]) : '' ) - : '' - ) + . ($aOptions['text'] ? $this->_tag('span', ['class' => 'info-box-text', 'label' => $aOptions['text']]) : '') + . ($aOptions['number'] ? $this->_tag('span', ['class' => 'info-box-number', 'label' => $aOptions['number']]) : '') + . ($aOptions['progressvalue'] !== false && $aOptions['progressvalue'] !== '' + ? $this->addWrapper( + 'div', + ['class' => 'progress'], + $this->_tag('div', ['class' => 'progress-bar' . ($aOptions['iconbg'] ? ' bg-' . $aOptions['iconbg'] : ''), 'style' => 'width: ' . (int) $aOptions['progressvalue'] . '%']) + ) + . ($aOptions['progresstext'] ? $this->_tag('span', ['class' => 'progress-description', 'label' => $aOptions['progresstext']]) : '') + : '' + ) ); // merge all - return $this->_tag('div', ['class'=>$sClass], $sIcon.$sContent); + return $this->_tag('div', ['class' => $sClass], $sIcon . $sContent); } /** - * return an info-box: + * return a small box: * A colored box with large icon, text and a value. * https://adminlte.io/docs/3.2/components/boxes.html * https://adminlte.io/themes/v3/pages/widgets.html * - * @param type $aOptions hash with keys for all options + * @param array $aOptions hash with keys for all options * styling: * - type - color of the box; one of [none]|danger|dark|info|primary|secondary|success|warning - + * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * content * - icon - icon class for icon on the right * - text - information text @@ -1049,90 +1308,352 @@ class renderadminlte { * - linktext- text below progress bar * @return string */ - public function getSmallbox($aOptions){ - $aOptions=$this->_ensureOptions('smallbox', $aOptions); - $aShadows=[ - 'default' => '', - 'none' => 'shadow-none', - 'small' => 'shadow-small', - 'regular' => 'shadow', - 'large' => 'shadow-lg', - ]; + public function getSmallbox(array $aOptions): string + { + $aOptions = $this->_ensureOptions('smallbox', $aOptions); // print_r($aOptions); - $sClass='small-box' - . $this->_addClassValue($aOptions['type'], 'bg-') - .($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) - ? ' '.$this->_aValueMappings['shadow'][$aOptions['shadow']] : '') - ; - + $sClass = 'small-box' + . $this->_addClassValue($aOptions['type'], 'bg-') + . $this->_addClassValue($aOptions['class'], '') + . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) + ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') + ; + // build parts - $sContent=$this->addWrapper("div", ['class'=>'inner'], + $sContent = $this->addWrapper( + "div", + ['class' => 'inner'], '' - . ($aOptions['number'] ? $this->_tag('h3', ['label'=>$aOptions['number']]) : '') - . ($aOptions['text'] ? $this->_tag('p', ['class'=>'info-box-text', 'label'=>$aOptions['text']]) : '') + . ($aOptions['number'] ? $this->_tag('h3', ['label' => $aOptions['number']]) : '') + . ($aOptions['text'] ? $this->_tag('p', ['class' => 'info-box-text', 'label' => $aOptions['text']]) : '') ); - $sIcon=$aOptions['icon'] - ? $this->addWrapper("div", ['class'=>'icon'], - $this->_tag('i',['class'=>$aOptions['icon']])) + $sIcon = $aOptions['icon'] + ? $this->addWrapper( + "div", + ['class' => 'icon'], + $this->_tag('i', ['class' => $aOptions['icon']]) + ) : '' - ; - $sFooter=($aOptions['url'] - ? $this->addWrapper("a", [ - 'class'=>'small-box-footer', - 'href'=>$aOptions['url'], + ; + $sFooter = ($aOptions['url'] + ? $this->addWrapper( + "a", + [ + 'class' => 'small-box-footer', + 'href' => $aOptions['url'], ], '' . ($aOptions['linktext'] ? $aOptions['linktext'] : $aOptions['url']) . ' ' - . $this->_tag('i',['class'=>'fas fa-arrow-circle-right']) + . $this->_tag('i', ['class' => 'fa-solid fa-arrow-circle-right']) ) : '' ); // merge all - return $this->_tag('div', ['class'=>$sClass], $sContent.$sIcon.$sFooter); + return $this->_tag('div', ['class' => $sClass], $sContent . $sIcon . $sFooter); } + // ---------------------------------------------------------------------- + // + // PUBLIC FUNCTIONS :: CONTENT - FORM + // + // ---------------------------------------------------------------------- -/* -<div class="info-box"> - <span class="info-box-icon bg-info"><i class="far fa-bookmark"></i></span> - <div class="info-box-content"> - <span class="info-box-text">Bookmarks</span> - <span class="info-box-number">41,410</span> - <div class="progress"> - <div class="progress-bar bg-info" style="width: 70%"></div> - </div> - <span class="progress-description"> - 70% Increase in 30 Days - </span> - </div> -</div> + /** + * Generates a horizontal form element with a label, input, and optional hint. + * + * @param string $sInput The HTML input element to be rendered. + * @param string $sLabel The label for the input element. + * @param string $sId The ID attribute for the label and input elements. + * @param string $sHint An optional hint to be displayed below the input element. + * @return string The generated HTML for the horizontal form element. + */ + public function getHorizontalFormElement(string $sInput, string $sLabel = '', string $sId = '', string $sHint=''): string + { + return '<div class="form-group row">' + . '<label for="' . $sId . '" class="col-sm-2 col-form-label">' . $sLabel . '</label>' + . '<div class="col-sm-10">' + . ($sHint + ? '<div class="text-navy hint">' . $sHint . '</div>' + : '') + . $sInput + . '</div>' + . '</div>' + ; + } + /** + * return a text input field: + * https://adminlte.io/themes/v3/pages/forms/general.html + * + * @param array $aOptions hash with keys for all options + * styling: + * - type - field type: text, email, password, hidden and all other html 5 input types + * content + * - label - label tag + * - name - name attribute for sending form + * - value - value in field + * more: + * - hint - hint to be displayed above the field + * If not set, no hint is displayed. + * css for ".row .hint" to customize look and feel + * @return string + */ + public function GetFormInput(array $aOptions): string + { + // $aOptions=$this->_ensureOptions('input', $aOptions); + $aElement = $aOptions; + $aElement['class'] = '' + . 'form-control ' + . (isset($aOptions['class']) ? $aOptions['class'] : '') + ; + $sFormid = (isset($aOptions['id']) + ? $aOptions['id'] + : (isset($aOptions['name']) ? $aOptions['name'] : 'field') . '-' . md5(microtime(true)) + ); + $aElement['id'] = $sFormid; + $sLabel = isset($aOptions['label']) ? $aOptions['label'] : ''; + $sHint = isset($aOptions['hint']) ? $aOptions['hint'] : ''; + $sPrepend = ''; + $sAppend = ''; - <div class="info-box bg-success"> - <span class="info-box-icon"><i class="far fa-thumbs-up"></i></span> - <div class="info-box-content"> - <span class="info-box-text">Likes</span> - <span class="info-box-number">41,410</span> - <div class="progress"> - <div class="progress-bar" style="width: 70%"></div> - </div> - <span class="progress-description"> - 70% Increase in 30 Days - </span> - </div> - </div> -*/ - + if (isset($aOptions['prepend']) && $aOptions['prepend']) { + $sWrapperclass = 'input-group'; + $sPrepend = $this->_tag( + 'div', + ['class' => 'input-group-prepend'], + $this->_tag('span', ['class' => 'input-group-text'], $aOptions['prepend']) + ); + } + if (isset($aOptions['append']) && $aOptions['append']) { + $sWrapperclass = 'input-group'; + $sAppend = $this->_tag( + 'div', + ['class' => 'input-group-append'], + $this->_tag('span', ['class' => 'input-group-text'], $aOptions['append']) + ); + } + foreach (['_infos', 'label', 'append', 'prepend', 'debug'] as $sDeleteKey) { + if (isset($aElement[$sDeleteKey])) { + unset($aElement[$sDeleteKey]); + } + } + // return data + + switch ($aElement['type']) { + case 'checkbox': + case 'radio': + $aElement['class'] = str_replace('form-control ', 'form-check-input', $aElement['class']); + $aElement['title'] = $aElement['title'] ?? $sHint; + return $this->_tag( + 'div', + ['class' => 'form-check'], + $this->_tag('input', $aElement, '', false) . $this->_tag('label', ['for' => $sFormid, 'label' => $sLabel], '') + ); + break; + case 'hidden': + $aElement['title'] = $aElement['title'] ?? $sHint; + return $this->_tag('input', $aElement, '', false); + break; + default: + return $this->getHorizontalFormElement( + $sPrepend . $this->_tag('input', $aElement, '', false) . $sAppend, + $sLabel, + $sFormid, + $sHint + ); + } + } + /** + * return a textarea field .. or html editor using summernote + * @param array $aOptions hash with keys for all options + * styling: + * - type - field type: [none]|html + * content + * - label - label tag + * - name - name attribute for sending form + * - value - value in + * more: + * - hint - hint to be displayed above the field + * If not set, no hint is displayed. + * css for ".row .hint" to customize look and feel + * @return string + */ + public function getFormTextarea(array $aOptions): string + { + // $aOptions=$this->_ensureOptions('textarea', $aOptions); + $aElement = $aOptions; + $aElement['class'] = '' + . 'form-control ' + . ((isset($aOptions['type']) && $aOptions['type'] == 'html') ? 'summernote ' : '') + . (isset($aOptions['class']) ? $aOptions['class'] : '') + ; + $sFormid = (isset($aOptions['id']) + ? $aOptions['id'] + : (isset($aOptions['name']) ? $aOptions['name'] : 'field') . '-' . md5(microtime(true)) + ); + $sLabel = isset($aOptions['label']) ? $aOptions['label'] : ''; + $sHint = isset($aOptions['hint']) ? $aOptions['hint'] : ''; + $aElement['id'] = $sFormid; + + $value = isset($aOptions['value']) ? $aOptions['value'] : ''; + + foreach (['_infos', 'label', 'debug', 'type', 'value'] as $sDeleteKey) { + if (isset($aElement[$sDeleteKey])) { + unset($aElement[$sDeleteKey]); + } + } + return $this->getHorizontalFormElement( + $this->_tag('textarea', $aElement, $value), + $sLabel, + $sFormid, + $sHint + ); + + } + + /** + * return a select box field + * @param array $aOptions hash with keys for all options + * option fields + * - options - array of options with keys per item: + * - value - value in the option + * - label - visible text in the option + * other keys are attributes in the option + * styling: + * - bootstrap-select - set true to enable select + * box with bootstrap-select and + * live search + * - class - css class + * select tag + * - label - label tag + * - name - name attribute for sending form + * other keys are attributes in the select + * more: + * - hint - hint to be displayed above the field + * If not set, no hint is displayed. + * css for ".row .hint" to customize look and feel + * @return string + */ + public function getFormSelect(array $aOptions): string + { + $aElement = $aOptions; + $aElement['class'] = '' + . 'form-control ' + . (isset($aOptions['class']) ? $aOptions['class'] . ' ' : '') + . (isset($aOptions['bootstrap-select']) ? 'selectpicker ' : '') //$aOptions + ; + if (isset($aOptions['bootstrap-select']) && $aOptions['bootstrap-select']) { + $aElement['data-live-search'] = "true"; + } + $sFormid = (isset($aOptions['id']) + ? $aOptions['id'] + : (isset($aOptions['name']) ? $aOptions['name'] : 'field') . '-' . md5(microtime(true)) + ); + $aElement['id'] = $sFormid; + $sLabel = isset($aOptions['label']) ? $aOptions['label'] : ''; + $sHint = isset($aOptions['hint']) ? $aOptions['hint'] : ''; + + $sOptionTags = ''; + foreach ($aOptions['options'] as $aField) { + $optionText = $aField['label']; + unset($aField['label']); + $sOptionTags .= $this->_tag('option', $aField, $optionText) . "\n"; + } + foreach (['_infos', 'label', 'debug', 'type', 'value', 'options'] as $sDeleteKey) { + if (isset($aElement[$sDeleteKey])) { + unset($aElement[$sDeleteKey]); + } + } + return $this->getHorizontalFormElement( + $this->_tag( + 'div', + ['class' => 'form-group'], + $this->_tag('select', $aElement, $sOptionTags) + ), + $sLabel, + $sFormid, + $sHint + ); + + } + + // ---------------------------------------------------------------------- + // + // PUBLIC FUNCTIONS :: CONTENT - TABBED CONTENT + // + // ---------------------------------------------------------------------- + + /** + * return a box with tabbed content + * @param array $aOptions hash with keys for all options + * - tabs {array} key=tab label; value=content + * @param bool $asArray optional flag: return hash with keys or as string + * @retunr bool|string|array + */ + public function getTabbedContent(array $aOptions, bool $asArray = false): bool|string|array + { + static $iTabCounter; + if (!isset($aOptions['tabs']) || !is_array($aOptions['tabs'])) { + return false; + } + if (!isset($iTabCounter)) { + $iTabCounter = 1; + } else { + $iTabCounter++; + } + + $id = 'tab-content-' . $iTabCounter; + $iCounter = 0; + + $sTabs = ''; + $sContent = ''; + foreach ($aOptions['tabs'] as $sLabel => $sTabContent) { + $iCounter++; + $sTabId = $id . '-tabitem-' . $iCounter . '-tab'; + $sContentId = $id . '-tabitem-' . $iCounter . '-content'; + + $sTabs .= $this->_tag( + 'li', + ['class' => 'nav-item'], + $this->_tag( + 'a', + [ + 'class' => 'nav-link' . ($iCounter == 1 ? ' active' : ''), + 'id' => $sTabId, + 'data-toggle' => 'tab', + 'href' => '#' . $sContentId, + 'role' => 'tab', + 'aria-controls' => 'custom-tabs-one-profile', + 'aria-selected' => ($iCounter == 1 ? true : false), + ], + $sLabel + ) + ); + $sContent .= $this->_tag('div', [ + 'class' => 'tab-pane fade' . ($iCounter == 1 ? ' active show' : ''), + 'id' => $sContentId, + 'role' => 'tabpanel', + 'aria-labelledby' => $sTabId, + ], $sTabContent); + } + $sTabs = $this->_tag('ul', ['class' => 'nav nav-tabs', 'role' => 'tablist'], $sTabs); + $sContent = $this->_tag('div', ['class' => 'tab-content'], $sContent); + + return $asArray + ? ['tabs' => $sTabs, 'content' => $sContent] + : $sTabs . $sContent + ; + } } diff --git a/config/inc_cronlog.php.dist b/config/inc_cronlog.php.dist index cc03cc295884066f75aef6be906a05875356f178..5f914266d32b0f32dfa76cfd7b2cf13b556c8ecb 100644 --- a/config/inc_cronlog.php.dist +++ b/config/inc_cronlog.php.dist @@ -16,7 +16,7 @@ * * it is included by classes/cronlog.class.php */ -return array( +return [ // starting directory with all servers cronwrapper logs 'sDatadir'=>'__APPDIR__/data', @@ -46,4 +46,4 @@ return array( 'Extern'=>'https://cronlogs.extern.example.com/', ], */ -); +]; diff --git a/config/lang_de-de.php b/config/lang_de-de.php index 70ddac699066b668bc260bb2ec726b06c223b6d2..ebb1aa87bdf96bbc9451c53249ac7ad042f6505d 100644 --- a/config/lang_de-de.php +++ b/config/lang_de-de.php @@ -26,7 +26,7 @@ return [ "history" => "History", "history-head" => "History", - "history-hint" => "Von den gestarteten Cronjobs werden die Ausführungszeiten und deren Exitcode für 6 Tage aufgehoben.", + "history-hint" => "Von den gestarteten Cronjobs werden die Ausführungszeiten und deren Exitcode für mehrere Tage aufgehoben.", "timeline" => "Timeline", "timeline-head" => "Graph mit Timeline", diff --git a/config/lang_en-en.php b/config/lang_en-en.php index 305b112f372ca802b6829752f6415a63fcb6b4aa..abfaabf5327df4769833c1ea2149ba90f949614c 100644 --- a/config/lang_en-en.php +++ b/config/lang_en-en.php @@ -26,7 +26,7 @@ return [ "history" => "History", "history-head" => "History", - "history-hint" => "The cronwrapper keeps data for 6 days. For each executed cronjob you can see execution times and exitcode.", + "history-hint" => "The cronwrapper keeps execution history data for multiples days. For each executed cronjob you can see execution times and exitcode.", "timeline" => "Timeline", "timeline-head" => "Graph with timeline", diff --git a/config/page.tpl.php b/config/page.tpl.php index d429d459a1f726aa95a63efdc18d410fd838d289..9a4720f4c7d4a7bdde2c656146a698e4e9bf4a3a 100644 --- a/config/page.tpl.php +++ b/config/page.tpl.php @@ -26,72 +26,7 @@ <link rel="stylesheet" href="{{DIR_ADMINLTE}}/css/adminlte.min.css?v=3.2.0"> <link rel="stylesheet" href="/main.css"> - - <!-- - <script nonce="b14481d0-1a31-4cec-b6e8-b5ad6020a5a9"> - (function(w, d) { - ! function(cM, cN, cO, cP) { - cM.zarazData = cM.zarazData || {}; - cM.zarazData.executed = []; - cM.zaraz = { - deferred: [], - listeners: [] - }; - cM.zaraz.q = []; - cM.zaraz._f = function(cQ) { - return function() { - var cR = Array.prototype.slice.call(arguments); - cM.zaraz.q.push({ - m: cQ, - a: cR - }) - } - }; - for (const cS of ["track", "set", "debug"]) cM.zaraz[cS] = cM.zaraz._f(cS); - cM.zaraz.init = () => { - var cT = cN.getElementsByTagName(cP)[0], - cU = cN.createElement(cP), - cV = cN.getElementsByTagName("title")[0]; - cV && (cM.zarazData.t = cN.getElementsByTagName("title")[0].text); - cM.zarazData.x = Math.random(); - cM.zarazData.w = cM.screen.width; - cM.zarazData.h = cM.screen.height; - cM.zarazData.j = cM.innerHeight; - cM.zarazData.e = cM.innerWidth; - cM.zarazData.l = cM.location.href; - cM.zarazData.r = cN.referrer; - cM.zarazData.k = cM.screen.colorDepth; - cM.zarazData.n = cN.characterSet; - cM.zarazData.o = (new Date).getTimezoneOffset(); - if (cM.dataLayer) - for (const cZ of Object.entries(Object.entries(dataLayer).reduce(((c_, da) => ({ - ...c_[1], - ...da[1] - }))))) zaraz.set(cZ[0], cZ[1], { - scope: "page" - }); - cM.zarazData.q = []; - for (; cM.zaraz.q.length;) { - const db = cM.zaraz.q.shift(); - cM.zarazData.q.push(db) - } - cU.defer = !0; - for (const dc of [localStorage, sessionStorage]) Object.keys(dc || {}).filter((de => de.startsWith("_zaraz_"))).forEach((dd => { - try { - cM.zarazData["z_" + dd.slice(7)] = JSON.parse(dc.getItem(dd)) - } catch { - cM.zarazData["z_" + dd.slice(7)] = dc.getItem(dd) - } - })); - cU.referrerPolicy = "origin"; - cU.src = "/cdn-cgi/zaraz/s.js?z=" + btoa(encodeURIComponent(JSON.stringify(cM.zarazData))); - cT.parentNode.insertBefore(cU, cT) - }; - ["complete", "interactive"].includes(cN.readyState) ? zaraz.init() : cM.addEventListener("DOMContentLoaded", zaraz.init) - }(w, d, 0, "script"); - })(window, document); - </script> - --> + </head> <body class="hold-transition {{PAGE_SKIN}} {{PAGE_LAYOUT}}"> diff --git a/get.php b/get.php index 6c44582165e46292c98bbf15c52ffe308cc67d0c..6a00219c180e02c00c503d68114272f14bf2991c 100644 --- a/get.php +++ b/get.php @@ -48,9 +48,9 @@ if($sServer){ switch ($sItem){ case 'cronhistory': if($sServer==='ALL'){ - $sHtml.=$oCL->renderJoblistOfAllServers(); + $sHtml.=$oCL->renderHistoryOfAllServers(); } else { - $sHtml.=$oCL->renderJoblist(); + $sHtml.=$oCL->renderHistoryTable(); } break; case 'cronlogs': @@ -68,7 +68,7 @@ switch ($sItem){ } break; case 'instances': - $sHtml.=$oCL->renderInstances($sServer); + $sHtml.=$oCL->renderInstances(); break; case 'selectserver': $sHtml.=$oCL->renderServerlist($sServer); diff --git a/index.php b/index.php index e924b3d252fadc4c9850dee78503bd36a3ea7108..2faac085160b6fc53a101bce3d758d95e157b775 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,6 @@ <?php -define("APP_VERSION", '2.1.4'); +define("APP_VERSION", '2.2.0'); require_once('classes/render-adminlte.class.php'); require_once('classes/cronlog-renderer.class.php');