sqlblock.class.inc.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <?php
  2. // Copyright (C) 2010 Combodo SARL
  3. //
  4. // This program is free software; you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation; version 3 of the License.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. /**
  17. * SqlBlock - display tables or charts, given an SQL query - use cautiously!
  18. *
  19. *
  20. * @author Erwan Taloc <erwan.taloc@combodo.com>
  21. * @author Romain Quetiez <romain.quetiez@combodo.com>
  22. * @author Denis Flaven <denis.flaven@combodo.com>
  23. * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL
  24. */
  25. require_once(APPROOT.'/application/webpage.class.inc.php');
  26. require_once(APPROOT.'/application/utils.inc.php');
  27. require_once(APPROOT.'/pages/php-ofc-library/open-flash-chart.php');
  28. /**
  29. * Helper class to design optimized dashboards, based on an SQL query
  30. *
  31. */
  32. class SqlBlock
  33. {
  34. protected $m_sQuery;
  35. protected $m_aColumns;
  36. protected $m_sTitle;
  37. protected $m_sType;
  38. public function __construct($sQuery, $aColumns, $sTitle, $sType)
  39. {
  40. $this->m_sQuery = $sQuery;
  41. $this->m_aColumns = $aColumns;
  42. $this->m_sTitle = $sTitle;
  43. $this->m_sType = $sType;
  44. }
  45. /**
  46. * Constructs a SqlBlock object from an XML template
  47. /*
  48. *
  49. * <sqlblock>
  50. * <sql>SELECT date_format(start_date, '%d') AS Date, count(*) AS Count FROM ticket WHERE DATE_SUB(NOW(), INTERVAL 15 DAY) &lt; start_date AND finalclass = 'UserIssue' GROUP BY date_format(start_date, '%d')</sql>
  51. * <type>table</type>
  52. * <title>UserRequest:Overview-Title</title>
  53. * <column>
  54. * <name>Date</name>
  55. * <label>UserRequest:Overview-Date</label>
  56. * <drilldown></drilldown>
  57. * </column>
  58. * <column>
  59. * <name>Count</name>
  60. * <label>UserRequest:Overview-Count</label>
  61. * <drilldown>SELECT UserIssue WHERE date_format(start_date, '%d') = :Date</drilldown>
  62. * </column>
  63. * </sqlblock>
  64. *
  65. * Tags
  66. * - sql: a (My)SQL query. Do not forget to use html entities (e.g. &lt; for <)
  67. * - type: table (default), bars or pie. If bars or pie is selected only the two first columns are taken into account.
  68. * - title: optional title, typed in clear or given as a dictionnary entry
  69. * - column: specification of a column (not displayed if omitted)
  70. * - column / name: name of the column in the SQL query (use aliases)
  71. * - column / label: label, typed in clear or given as a dictionnary entry
  72. * - column / drilldown: NOT IMPLEMENTED YET - OQL with parameters corresponding to column names (in the query)
  73. *
  74. * @param $sTemplate string The XML template
  75. * @return DisplayBlock The DisplayBlock object, or null if the template is invalid
  76. */
  77. public static function FromTemplate($sTemplate)
  78. {
  79. $oXml = simplexml_load_string('<root>'.$sTemplate.'</root>', 'SimpleXMLElement', LIBXML_NOCDATA);
  80. if (false)
  81. {
  82. // Debug
  83. echo "<pre>\n";
  84. print_r($oXml);
  85. echo "</pre>\n";
  86. }
  87. if (isset($oXml->title))
  88. {
  89. $sTitle = (string)$oXml->title;
  90. }
  91. if (isset($oXml->type))
  92. {
  93. $sType = (string)$oXml->type;
  94. }
  95. else
  96. {
  97. $sType = 'table';
  98. }
  99. if (!isset($oXml->sql))
  100. {
  101. throw new Exception('Missing tag "sql" in sqlblock');
  102. }
  103. $sQuery = (string)$oXml->sql;
  104. $aColumns = array();
  105. if (isset($oXml->column))
  106. {
  107. foreach ($oXml->column AS $oColumnData)
  108. {
  109. if (!isset($oColumnData->name))
  110. {
  111. throw new Exception("Missing tag 'name' in sqlblock/column");
  112. }
  113. $sName = (string) $oColumnData->name;
  114. if (strlen($sName) == 0)
  115. {
  116. throw new Exception("Empty tag 'name' in sqlblock/column");
  117. }
  118. $aColumns[$sName] = array();
  119. if (isset($oColumnData->label))
  120. {
  121. $sLabel = (string)$oColumnData->label;
  122. if (strlen($sLabel) > 0)
  123. {
  124. $aColumns[$sName]['label'] = Dict::S($sLabel);
  125. }
  126. }
  127. if (isset($oColumnData->drilldown))
  128. {
  129. $sDrillDown = (string)$oColumnData->drilldown;
  130. if (strlen($sDrillDown) > 0)
  131. {
  132. $aColumns[$sName]['drilldown'] = $sDrillDown;
  133. }
  134. }
  135. }
  136. }
  137. return new SqlBlock($sQuery, $aColumns, $sTitle, $sType);
  138. }
  139. public function RenderContent(WebPage $oPage, $aExtraParams = array())
  140. {
  141. if (empty($aExtraParams['currentId']))
  142. {
  143. $sId = 'sqlblock_'.$oPage->GetUniqueId(); // Works only if the page is not an Ajax one !
  144. }
  145. else
  146. {
  147. $sId = $aExtraParams['currentId'];
  148. }
  149. // $oPage->add($this->GetRenderContent($oPage, $aExtraParams, $sId));
  150. $res = CMDBSource::Query($this->m_sQuery);
  151. $aQueryCols = CMDBSource::GetColumns($res);
  152. // Prepare column definitions (check + give default values)
  153. //
  154. foreach($this->m_aColumns as $sName => $aColumnData)
  155. {
  156. if (!in_array($sName, $aQueryCols))
  157. {
  158. throw new Exception("Unknown column name '$sName' in sqlblock column");
  159. }
  160. if (!isset($aColumnData['label']))
  161. {
  162. $this->m_aColumns[$sName]['label'] = $sName;
  163. }
  164. if (isset($aColumnData['drilldown']) && !empty($aColumnData['drilldown']))
  165. {
  166. // Check if the OQL is valid
  167. try
  168. {
  169. $this->m_aColumns[$sName]['filter'] = DBObjectSearch::FromOQL($aColumnData['drilldown']);
  170. }
  171. catch(OQLException $e)
  172. {
  173. unset($aColumnData['drilldown']);
  174. }
  175. }
  176. }
  177. if (strlen($this->m_sTitle) > 0)
  178. {
  179. $oPage->add("<h2>".Dict::S($this->m_sTitle)."</h2>\n");
  180. }
  181. switch ($this->m_sType)
  182. {
  183. case 'bars':
  184. case 'pie':
  185. $aColNames = array_keys($this->m_aColumns);
  186. $sXColName = $aColNames[0];
  187. $sYColName = $aColNames[1];
  188. $aData = array();
  189. $aRows = array();
  190. while($aRow = CMDBSource::FetchArray($res))
  191. {
  192. $aData[$aRow[$sXColName]] = $aRow[$sYColName];
  193. $aRows[$aRow[$sXColName]] = $aRow;
  194. }
  195. $this->RenderChart($oPage, $sId, $aData, $this->m_aColumns[$sYColName]['drilldown'], $aRows);
  196. break;
  197. default:
  198. case 'table':
  199. $oAppContext = new ApplicationContext();
  200. $sContext = $oAppContext->GetForLink();
  201. if (!empty($sContext))
  202. {
  203. $sContext = '&'.$sContext;
  204. }
  205. $aDisplayConfig = array();
  206. foreach($this->m_aColumns as $sName => $aColumnData)
  207. {
  208. $aDisplayConfig[$sName] = array('label' => $aColumnData['label'], 'description' => '');
  209. }
  210. $aDisplayData = array();
  211. while($aRow = CMDBSource::FetchArray($res))
  212. {
  213. $aSQLColNames = array_keys($aRow);
  214. $aDisplayRow = array();
  215. foreach($this->m_aColumns as $sName => $aColumnData)
  216. {
  217. if (isset($aColumnData['filter']))
  218. {
  219. $sFilter = $aColumnData['drilldown'];
  220. $sClass = $aColumnData['filter']->GetClass();
  221. $sFilter = str_replace('SELECT '.$sClass, '', $sFilter);
  222. foreach($aSQLColNames as $sColName)
  223. {
  224. $sFilter = str_replace(':'.$sColName, "'".addslashes( $aRow[$sColName] )."'", $sFilter);
  225. }
  226. $sURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search_oql&search_form=0&oql_class='.$sClass.'&oql_clause='.urlencode($sFilter).'&format=html'.$sContext;
  227. $aDisplayRow[$sName] = '<a href="'.$sURL.'">'.$aRow[$sName]."</a>";
  228. }
  229. else
  230. {
  231. $aDisplayRow[$sName] = $aRow[$sName];
  232. }
  233. }
  234. $aDisplayData[] = $aDisplayRow;
  235. }
  236. $oPage->table($aDisplayConfig, $aDisplayData);
  237. break;
  238. }
  239. }
  240. public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId)
  241. {
  242. $sHtml = '';
  243. return $sHtml;
  244. }
  245. protected function RenderChart($oPage, $sId, $aValues, $sDrillDown = '', $aRows = array())
  246. {
  247. // 1- Compute Open Flash Chart data
  248. //
  249. $aValueKeys = array();
  250. $index = 0;
  251. if ($sDrillDown != '')
  252. {
  253. $oFilter = DBObjectSearch::FromOQL($sDrillDown);
  254. $sClass = $oFilter->GetClass();
  255. $sOQLClause = str_replace('SELECT '.$sClass, '', $sDrillDown);
  256. $aSQLColNames = array_keys(current($aRows)); // Read the list of columns from the current (i.e. first) element of the array
  257. $oAppContext = new ApplicationContext();
  258. $sURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search_oql&search_form=0&oql_class='.$sClass.'&format=html&'.$oAppContext->GetForLink().'&oql_clause=';
  259. $aURLs = array();
  260. }
  261. foreach($aValues as $key => $value)
  262. {
  263. // Make sure that values are integers (so that max() will work....)
  264. // and build an array of STRING with the keys (numeric keys are transformed into string by PHP :-(
  265. $aValues[$key] = (int)$value;
  266. $aValueKeys[] = (string)$key;
  267. // Build the custom query for the 'drill down' on each element
  268. if ($sDrillDown != '')
  269. {
  270. $sFilter = $sOQLClause;
  271. foreach($aSQLColNames as $sColName)
  272. {
  273. $sFilter = str_replace(':'.$sColName, "'".addslashes( $aRows[$key][$sColName] )."'", $sFilter);
  274. $aURLs[$index] = $sURL.urlencode($sFilter);
  275. }
  276. }
  277. $index++;
  278. }
  279. $oChart = new open_flash_chart();
  280. if ($this->m_sType == 'bars')
  281. {
  282. $oChartElement = new bar_glass();
  283. $maxValue = max($aValues);
  284. $oYAxis = new y_axis();
  285. $aMagicValues = array(1,2,5,10);
  286. $iMultiplier = 1;
  287. $index = 0;
  288. $iTop = $aMagicValues[$index % count($aMagicValues)]*$iMultiplier;
  289. while($maxValue > $iTop)
  290. {
  291. $index++;
  292. $iTop = $aMagicValues[$index % count($aMagicValues)]*$iMultiplier;
  293. if (($index % count($aMagicValues)) == 0)
  294. {
  295. $iMultiplier = $iMultiplier * 10;
  296. }
  297. }
  298. //echo "oYAxis->set_range(0, $iTop, $iMultiplier);\n";
  299. $oYAxis->set_range(0, $iTop, $iMultiplier);
  300. $oChart->set_y_axis( $oYAxis );
  301. $aBarValues = array();
  302. foreach($aValues as $iValue)
  303. {
  304. $oBarValue = new bar_value($iValue);
  305. $oBarValue->on_click("ofc_drilldown_{$sId}");
  306. $aBarValues[] = $oBarValue;
  307. }
  308. $oChartElement->set_values($aBarValues);
  309. //$oChartElement->set_values(array_values($aValues));
  310. $oXAxis = new x_axis();
  311. $oXLabels = new x_axis_labels();
  312. // set them vertical
  313. $oXLabels->set_vertical();
  314. // set the label text
  315. $oXLabels->set_labels($aValueKeys);
  316. // Add the X Axis Labels to the X Axis
  317. $oXAxis->set_labels( $oXLabels );
  318. $oChart->set_x_axis( $oXAxis );
  319. }
  320. else
  321. {
  322. $oChartElement = new pie();
  323. $oChartElement->set_start_angle( 35 );
  324. $oChartElement->set_animate( true );
  325. $oChartElement->set_tooltip( '#label# - #val# (#percent#)' );
  326. $oChartElement->set_colours( array('#FF8A00', '#909980', '#2C2B33', '#CCC08D', '#596664') );
  327. $aData = array();
  328. foreach($aValues as $sValue => $iValue)
  329. {
  330. $oPieValue = new pie_value($iValue, $sValue); //@@ BUG: not passed via ajax !!!
  331. $oPieValue->on_click("ofc_drilldown_{$sId}");
  332. $aData[] = $oPieValue;
  333. }
  334. $oChartElement->set_values( $aData );
  335. $oChart->x_axis = null;
  336. }
  337. // Title given in HTML
  338. //$oTitle = new title($this->m_sTitle);
  339. //$oChart->set_title($oTitle);
  340. $oChart->set_bg_colour('#FFFFFF');
  341. $oChart->add_element( $oChartElement );
  342. $sData = $oChart->toPrettyString();
  343. $sData = json_encode($sData);
  344. $sURLList = '';
  345. foreach($aURLs as $index => $sURL)
  346. {
  347. $sURLList .= "\taURLs[$index] = '".addslashes($sURL)."';\n";
  348. }
  349. // 2- Declare the Javascript function that will render the chart data\
  350. //
  351. $oPage->add_script(
  352. <<< EOF
  353. function ofc_get_data_{$sId}()
  354. {
  355. return $sData;
  356. }
  357. function ofc_drilldown_{$sId}(index)
  358. {
  359. var aURLs = new Array();
  360. {$sURLList}
  361. var sURL = aURLs[index];
  362. window.location.href = sURL; // Navigate !
  363. }
  364. EOF
  365. );
  366. // 3- Insert the Open Flash chart
  367. //
  368. $oPage->add("<div id=\"$sId\"><div>\n");
  369. $oPage->add_ready_script(
  370. <<<EOF
  371. swfobject.embedSWF( "../images/open-flash-chart.swf",
  372. "{$sId}",
  373. "100%", "300","9.0.0",
  374. "expressInstall.swf",
  375. {"get-data":"ofc_get_data_{$sId}", "id":"{$sId}"},
  376. {'wmode': 'transparent'}
  377. );
  378. EOF
  379. );
  380. }
  381. }
  382. ?>