N°1325 Dashboards: Unknown dashlets (eg. from an uninstalled extension) no longer raise an exception, a fallback is displayed and the XML configuration is still available in editor.

SVN:trunk[5384]
This commit is contained in:
Guillaume Lajarige
2018-03-06 14:07:33 +00:00
parent 34dab0c498
commit 68cdd6b8a9
5 changed files with 425 additions and 155 deletions

View File

@@ -115,15 +115,13 @@ abstract class Dashboard
$aDashletOrder = array();
foreach($oDashletList as $oDomNode)
{
$sDashletClass = $oDomNode->getAttribute('xsi:type');
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
if ($oRank)
{
$iRank = (float)$oRank->textContent;
}
$sId = $oDomNode->getAttribute('id');
$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
$oNewDashlet->FromDOMNode($oDomNode);
$oNewDashlet = $this->InitDashletFromDOMNode($oDomNode);
$aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet);
}
usort($aDashletOrder, array(get_class($this), 'SortOnRank'));
@@ -147,6 +145,22 @@ abstract class Dashboard
}
}
protected function InitDashletFromDOMNode($oDomNode)
{
$sId = $oDomNode->getAttribute('id');
$sClass = $oDomNode->getAttribute('xsi:type');
// Test if dashlet can be instanciated, otherwise (uninstalled, broken, ...) we display a placeholder
if(!class_exists($sClass))
{
$sClass = 'DashletUnknown';
}
$oNewDashlet = new $sClass($this->oMetaModel, $sId);
$oNewDashlet->FromDOMNode($oDomNode);
return $oNewDashlet;
}
static function SortOnRank($aItem1, $aItem2)
{
return ($aItem1['rank'] > $aItem2['rank']) ? +1 : -1;
@@ -414,24 +428,11 @@ EOF
$oPage->add('<div class="ui-widget-content ui-corner-all"><div class="ui-widget-header ui-corner-all" style="text-align:center; padding: 2px;">'.Dict::S('UI:DashboardEdit:Dashlets').'</div>');
$sUrl = utils::GetAbsoluteUrlAppRoot();
$oPage->add('<div id="select_dashlet" style="text-align:center">');
foreach( get_declared_classes() as $sDashletClass)
$oPage->add('<div id="select_dashlet" style="text-align:center; max-height:120px; overflow-y:auto;">');
$aAvailableDashlets = $this->GetAvailableDashlets();
foreach($aAvailableDashlets as $sDashletClass => $aInfo)
{
if (is_subclass_of($sDashletClass, 'Dashlet'))
{
$oReflection = new ReflectionClass($sDashletClass);
if (!$oReflection->isAbstract())
{
$aCallSpec = array($sDashletClass, 'IsVisible');
$bVisible = call_user_func($aCallSpec);
if ($bVisible)
{
$aCallSpec = array($sDashletClass, 'GetInfo');
$aInfo = call_user_func($aCallSpec);
$oPage->add('<span dashlet_class="'.$sDashletClass.'" class="dashlet_icon ui-widget-content ui-corner-all" id="dashlet_'.$sDashletClass.'" title="'.$aInfo['label'].'" style="width:34px; height:34px; display:inline-block; margin:2px;"><img src="'.$sUrl.$aInfo['icon'].'" /></span>');
}
}
}
$oPage->add('<span dashlet_class="'.$sDashletClass.'" class="dashlet_icon ui-widget-content ui-corner-all" id="dashlet_'.$sDashletClass.'" title="'.$aInfo['label'].'" style="width:34px; height:34px; display:inline-block; margin:2px;"><img src="'.$sUrl.$aInfo['icon'].'" /></span>');
}
$oPage->add('</div>');
@@ -466,6 +467,38 @@ EOF
$oPage->add('</div>');
}
/**
* Return an array of dashlets available for selection.
*
* @return array
*/
protected function GetAvailableDashlets()
{
$aDashlets = array();
foreach( get_declared_classes() as $sDashletClass)
{
// DashletUnknown is not among the selection as it is just a fallback for dashlets that can't instanciated.
if ( is_subclass_of($sDashletClass, 'Dashlet') && !in_array($sDashletClass, array('DashletUnknown', 'DashletProxy')) )
{
$oReflection = new ReflectionClass($sDashletClass);
if (!$oReflection->isAbstract())
{
$aCallSpec = array($sDashletClass, 'IsVisible');
$bVisible = call_user_func($aCallSpec);
if ($bVisible)
{
$aCallSpec = array($sDashletClass, 'GetInfo');
$aInfo = call_user_func($aCallSpec);
$aDashlets[$sDashletClass] = $aInfo;
}
}
}
}
return $aDashlets;
}
protected function GetNewDashletId()
{
$iNewId = 0;

View File

@@ -1,5 +1,5 @@
<?php
// Copyright (C) 2012-2017 Combodo SARL
// Copyright (C) 2012-2018 Combodo SARL
//
// This file is part of iTop.
//
@@ -313,6 +313,221 @@ EOF
}
}
/**
* Class DashletUnknown
*
* Used as a fallback in iTop for unknown dashlet classes.
*
* @since 2.5
*/
class DashletUnknown extends Dashlet
{
static protected $aClassList = null;
protected $sOriginalDashletClass;
protected $sOriginalDashletXML;
public function __construct($oModelReflection, $sId)
{
parent::__construct($oModelReflection, $sId);
$this->sOriginalDashletClass = 'Unknown';
$this->sOriginalDashletXML = '';
$this->aCSSClasses[] = 'dashlet-unknown';
}
public function GetOriginalDashletClass()
{
return $this->sOriginalDashletClass;
}
public function SetOriginalDashletClass($sOriginalDashletClass)
{
$this->sOriginalDashletClass = $sOriginalDashletClass;
}
public function FromDOMNode($oDOMNode)
{
// Parent won't do anything as there is no property declared
parent::FromDOMNode($oDOMNode);
// Original dashlet
// - Class
if($oDOMNode->hasAttribute('xsi:type'))
{
$this->sOriginalDashletClass = $oDOMNode->getAttribute('xsi:type');
}
// Build properties from XML
$this->sOriginalDashletXML = "";
foreach($oDOMNode->childNodes as $oDOMChildNode)
{
if($oDOMChildNode instanceof DOMElement)
{
$sProperty = $oDOMChildNode->tagName;
// For all properties but "rank" as it is handle by the dashboard.
if($sProperty !== 'rank')
{
// We need to initialize the property before setting it, otherwise it will guessed as NULL and not used.
$this->aProperties[$sProperty] = '';
$this->aProperties[$sProperty] = $this->PropertyFromDOMNode($oDOMChildNode, $sProperty);
// And build the original XML
$this->sOriginalDashletXML .= $oDOMChildNode->ownerDocument->saveXML($oDOMChildNode)."\n";
}
}
}
$this->OnUpdate();
}
public function FromParams($aParams)
{
// For unknown dashlet, parameters are not parsed but passed as a raw xml
if(array_key_exists('xml', $aParams))
{
// A namspace must be present for the "xsi:type" attribute, otherwise a warning will be thrown.
$sXML = '<dashlet id="'.$aParams['dashlet_id'].'" xsi:type="'.$aParams['dashlet_class'].'" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$aParams['xml'].'</dashlet>';
$this->FromXml($sXML);
}
$this->OnUpdate();
}
public function Render($oPage, $bEditMode = false, $aExtraParams = array())
{
$aInfos = static::GetInfo();
$sIconUrl = utils::GetAbsoluteUrlAppRoot().$aInfos['icon'];
$sExplainText = ($bEditMode) ? Dict::Format('UI:DashletUnknown:RenderText:Edit', $this->sOriginalDashletClass) : Dict::S('UI:DashletUnknown:RenderText:View');
$oPage->add('<div class="dashlet-content">');
$oPage->add('<div class="dashlet-ukn-image"><img src="'.$sIconUrl.'" /></div>');
$oPage->add('<div class="dashlet-ukn-text">'.$sExplainText.'</div>');
$oPage->add('</div>');
}
public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
{
$aInfos = static::GetInfo();
$sIconUrl = utils::GetAbsoluteUrlAppRoot().$aInfos['icon'];
$sExplainText = Dict::Format('UI:DashletUnknown:RenderNoDataText:Edit', $this->sOriginalDashletClass);
$oPage->add('<div class="dashlet-content">');
$oPage->add('<div class="dashlet-ukn-image"><img src="'.$sIconUrl.'" /></div>');
$oPage->add('<div class="dashlet-ukn-text">'.$sExplainText.'</div>');
$oPage->add('</div>');
}
public function GetPropertiesFields(DesignerForm $oForm)
{
$oField = new DesignerLongTextField('xml', Dict::S('UI:DashletUnknown:Prop-XMLConfiguration'), $this->sOriginalDashletXML);
$oForm->AddField($oField);
}
protected function PropertyFromDOMNode($oDOMNode, $sProperty)
{
$bHasSubProperties = false;
foreach($oDOMNode->childNodes as $oDOMChildNode)
{
if($oDOMChildNode->nodeType === XML_ELEMENT_NODE)
{
$bHasSubProperties = true;
break;
}
}
if($bHasSubProperties)
{
$sTmp = $oDOMNode->ownerDocument->saveXML($oDOMNode, LIBXML_NOENT);
$sTmp = trim(preg_replace("/(<".$oDOMNode->tagName.".*>|<\/".$oDOMNode->tagName.">)/", "", $sTmp));
return $sTmp;
}
else
{
return parent::PropertyFromDOMNode($oDOMNode, $sProperty);
}
}
protected function PropertyToDOMNode($oDOMNode, $sProperty, $value)
{
// Save subnodes
if(preg_match('/<(.*)>/', $value))
{
/** @var \DOMDocumentFragment $oDOMFragment */
$oDOMFragment = $oDOMNode->ownerDocument->createDocumentFragment();
$oDOMFragment->appendXML($value);
$oDOMNode->appendChild($oDOMFragment);
}
else
{
parent::PropertyToDOMNode($oDOMNode, $sProperty, $value);
}
}
public function Update($aValues, $aUpdatedFields)
{
$this->FromParams($aValues);
// OnUpdate() already done in FromParams()
return $this;
}
static public function GetInfo()
{
return array(
'label' => Dict::S('UI:DashletUnknown:Label'),
'icon' => 'images/dashlet-unknown.png',
'description' => Dict::S('UI:DashletUnknown:Description'),
);
}
}
class DashletProxy extends DashletUnknown
{
protected $sOriginalDashletClass;
protected $sOriginalDashletXML;
public function __construct($oModelReflection, $sId)
{
parent::__construct($oModelReflection, $sId);
// Remove DashletUnknown class
if( ($key = array_search('dashlet-unknown', $this->aCSSClasses)) !== false )
{
unset($this->aCSSClasses[$key]);
}
$this->sOriginalDashletClass = 'Proxy';
$this->sOriginalDashletXML = '';
$this->aCSSClasses[] = 'dashlet-proxy';
}
public function Render($oPage, $bEditMode = false, $aExtraParams = array())
{
// This should never be called.
$oPage->add('<div>This dashlet is not supposed to be rendered as it is just a proxy for third-party widgets.</div>');
}
public function RenderNoData($oPage, $bEditMode = false, $aExtraParams = array())
{
// TODO
$oPage->add('<div>RENDER NO DATA TO DO! (PREVIEW OR SO)</div>');
}
static public function GetInfo()
{
return array(
'label' => Dict::S('UI:DashletProxy:Label'),
'icon' => 'images/dashlet-proxy.png',
'description' => Dict::S('UI:DashletProxy:Description'),
);
}
}
class DashletEmptyCell extends Dashlet
{
public function __construct($oModelReflection, $sId)
@@ -537,29 +752,29 @@ abstract class DashletGroupBy extends Dashlet
{
switch($this->sFunction)
{
case 'hour':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Hour', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%H')"; // 0 -> 23
break;
case 'hour':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Hour', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%H')"; // 0 -> 23
break;
case 'month':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Month', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m')"; // yyyy-mm
break;
case 'month':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Month', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m')"; // yyyy-mm
break;
case 'day_of_week':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfWeek', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%w')";
break;
case 'day_of_week':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfWeek', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%w')";
break;
case 'day_of_month':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfMonth', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m-%d')"; // mm-dd
break;
case 'day_of_month':
$this->sGroupByLabel = Dict::Format('UI:DashletGroupBy:Prop-GroupBy:DayOfMonth', $sAttLabel);
$this->sGroupByExpr = "DATE_FORMAT($sClassAlias.{$this->sGroupByAttCode}, '%Y-%m-%d')"; // mm-dd
break;
default:
$this->sGroupByLabel = 'Unknown group by function '.$this->sFunction;
$this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode;
default:
$this->sGroupByLabel = 'Unknown group by function '.$this->sFunction;
$this->sGroupByExpr = $sClassAlias.'.'.$this->sGroupByAttCode;
}
}
else
@@ -596,37 +811,37 @@ abstract class DashletGroupBy extends Dashlet
{
switch($sStyle)
{
case 'bars':
$sType = 'chart';
$aExtraParams = array(
'chart_type' => 'bars',
'chart_title' => $sTitle,
'group_by' => $this->sGroupByExpr,
'group_by_label' => $this->sGroupByLabel,
);
$sHtmlTitle = ''; // done in the itop block
break;
case 'bars':
$sType = 'chart';
$aExtraParams = array(
'chart_type' => 'bars',
'chart_title' => $sTitle,
'group_by' => $this->sGroupByExpr,
'group_by_label' => $this->sGroupByLabel,
);
$sHtmlTitle = ''; // done in the itop block
break;
case 'pie':
$sType = 'chart';
$aExtraParams = array(
'chart_type' => 'pie',
'chart_title' => $sTitle,
'group_by' => $this->sGroupByExpr,
'group_by_label' => $this->sGroupByLabel,
);
$sHtmlTitle = ''; // done in the itop block
break;
case 'pie':
$sType = 'chart';
$aExtraParams = array(
'chart_type' => 'pie',
'chart_title' => $sTitle,
'group_by' => $this->sGroupByExpr,
'group_by_label' => $this->sGroupByLabel,
);
$sHtmlTitle = ''; // done in the itop block
break;
case 'table':
default:
$sHtmlTitle = htmlentities(Dict::S($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block
$sType = 'count';
$aExtraParams = array(
'group_by' => $this->sGroupByExpr,
'group_by_label' => $this->sGroupByLabel,
);
break;
case 'table':
default:
$sHtmlTitle = htmlentities(Dict::S($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block
$sType = 'count';
$aExtraParams = array(
'group_by' => $this->sGroupByExpr,
'group_by_label' => $this->sGroupByLabel,
);
break;
}
$oPage->add('<div style="text-align:center" class="dashlet-content">');
@@ -668,21 +883,21 @@ abstract class DashletGroupBy extends Dashlet
$aValues = array();
switch($this->sFunction)
{
case 'hour':
$aValues = array(8, 9, 15, 18);
break;
case 'hour':
$aValues = array(8, 9, 15, 18);
break;
case 'month':
$aValues = array('2013 '.Dict::S('Month-11'), '2013 '.Dict::S('Month-12'), '2014 '.Dict::S('Month-01'), '2014 '.Dict::S('Month-02'), '2014 '.Dict::S('Month-03'));
break;
case 'month':
$aValues = array('2013 '.Dict::S('Month-11'), '2013 '.Dict::S('Month-12'), '2014 '.Dict::S('Month-01'), '2014 '.Dict::S('Month-02'), '2014 '.Dict::S('Month-03'));
break;
case 'day_of_week':
$aValues = array(Dict::S('DayOfWeek-Monday'), Dict::S('DayOfWeek-Wednesday'), Dict::S('DayOfWeek-Thursday'), Dict::S('DayOfWeek-Friday'));
break;
case 'day_of_week':
$aValues = array(Dict::S('DayOfWeek-Monday'), Dict::S('DayOfWeek-Wednesday'), Dict::S('DayOfWeek-Thursday'), Dict::S('DayOfWeek-Friday'));
break;
case 'day_of_month':
$aValues = array(Dict::S('Month-03'). ' 30', Dict::S('Month-03'). ' 31', Dict::S('Month-04'). ' 01', Dict::S('Month-04'). ' 02', Dict::S('Month-04'). ' 03');
break;
case 'day_of_month':
$aValues = array(Dict::S('Month-03'). ' 30', Dict::S('Month-03'). ' 31', Dict::S('Month-04'). ' 01', Dict::S('Month-04'). ' 02', Dict::S('Month-04'). ' 03');
break;
}
foreach ($aValues as $sValue)
{
@@ -822,16 +1037,16 @@ abstract class DashletGroupBy extends Dashlet
{
// Style changed, mutate to the specified type of chart
case 'pie':
$oDashlet = new DashletGroupByPie($this->oModelReflection, $this->sId);
break;
$oDashlet = new DashletGroupByPie($this->oModelReflection, $this->sId);
break;
case 'bars':
$oDashlet = new DashletGroupByBars($this->oModelReflection, $this->sId);
break;
$oDashlet = new DashletGroupByBars($this->oModelReflection, $this->sId);
break;
case 'table':
$oDashlet = new DashletGroupByTable($this->oModelReflection, $this->sId);
break;
$oDashlet = new DashletGroupByTable($this->oModelReflection, $this->sId);
break;
}
$oDashlet->FromParams($aValues);
$oDashlet->bRedrawNeeded = true;
@@ -1299,8 +1514,8 @@ class DashletHeaderDynamic extends Dashlet
foreach ($aValues as $sValue)
{
$sValueLabel = $this->oModelReflection->GetValueLabel($sClass, $sGroupBy, $sValue);
$oPage->add(' <th>'.$sValueLabel.'</th>');
}
$oPage->add(' <th>'.$sValueLabel.'</th>');
}
$oPage->add('</tr>');
$oPage->add('<tr>');
foreach ($aValues as $sValue)
@@ -1547,4 +1762,3 @@ class DashletBadge extends Dashlet
);
}
}
?>

View File

@@ -1599,6 +1599,14 @@ td.prop_icon {
.dashlet-content .display_block {
text-align: left;
}
.dashlet-unknown .dashlet-content {
padding: 8px;
background-color: #f2f2f2;
text-align: center;
}
.dashlet-unknown .dashlet-content .dashlet-ukn-text {
margin-top: 10px;
}
.prop_apply .ui-icon-alert {
display: none;
}

View File

@@ -1769,6 +1769,17 @@ td.prop_icon {
.dashlet-content .display_block {
text-align:left;
}
.dashlet-unknown {
.dashlet-content {
padding: 8px;
background-color: #F2F2F2;
text-align: center;
.dashlet-ukn-text {
margin-top: 10px;
}
}
}
.prop_apply .ui-icon-alert {
display: none;
}

View File

@@ -1164,7 +1164,11 @@ When associated with a trigger, each action is given an "order" number, specifyi
'UI:DashletUnknown:RenderText:View' => 'Unable to render this dashlet.',
'UI:DashletUnknown:RenderText:Edit' => 'Unable to render this dashlet (class "%1$s"). Check with your administrator if it is still available.',
'UI:DashletUnknown:RenderNoDataText:Edit' => 'No preview available for this dashlet (class "%1$s").',
'UI:DashletUnknown:Prop-XMLConfiguration' => 'Configuration as XML',
'UI:DashletUnknown:Prop-XMLConfiguration' => 'Configuration (shown as raw XML)',
'UI:DashletProxy:Label' => 'Proxy',
'UI:DashletProxy:Description' => 'Proxy dashlet',
'UI:DashletProxy:Prop-XMLConfiguration' => 'Configuration (shown as raw XML)',
'UI:DashletPlainText:Label' => 'Text',
'UI:DashletPlainText:Description' => 'Plain text (no formatting)',