mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
Allow dashboard to refresh from backend, or refresh with new data format when switching from custom to default
This commit is contained in:
@@ -22,6 +22,11 @@ class IboDashboard extends HTMLElement {
|
||||
this.csrfToken = null;
|
||||
/** @type {number} Payload schema version */
|
||||
this.schemaVersion = 2;
|
||||
/** @type {boolean} Define is the current is dashboard is custom or not */
|
||||
this.bIsCustomDashboard = false;
|
||||
// TODO 3.3 Do not use file that come from frontend
|
||||
/** @type {string} File for the default dashboard */
|
||||
this.sFile = '';
|
||||
|
||||
/** @type {object|null} Last saved state for cancel functionality, unused yet */
|
||||
this.aLastSavedState = null;
|
||||
@@ -29,7 +34,10 @@ class IboDashboard extends HTMLElement {
|
||||
|
||||
connectedCallback() {
|
||||
this.sId = this.getAttribute("id");
|
||||
this.bEditMode = (this.getAttribute("data-edit-mode") === "edit");
|
||||
this.bEditMode = (this.getAttribute("data-edit-mode") === "edit")
|
||||
this.bIsCustomDashboard = this.getAttribute("data-is-custom") === "true";
|
||||
this.sFile = this.getAttribute("data-file") || '';
|
||||
|
||||
this.SetupGrid();
|
||||
|
||||
this.BindEvents();
|
||||
@@ -55,6 +63,10 @@ class IboDashboard extends HTMLElement {
|
||||
this.SetEditMode(false);
|
||||
});
|
||||
|
||||
document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]')?.addEventListener('change', (e) => {
|
||||
const bIsCustomDashboard = e.target.checked;
|
||||
this.SetIsCustomDashboard(bIsCustomDashboard);
|
||||
});
|
||||
// TODO 3.3 Add event listener to dashboard toggler to get custom/default dashboard switching
|
||||
// TODO 3.3 require load method that's not finished yet
|
||||
|
||||
@@ -69,27 +81,37 @@ class IboDashboard extends HTMLElement {
|
||||
|
||||
this.oGrid = this.querySelector('ibo-dashboard-grid');
|
||||
}
|
||||
|
||||
async SetIsCustomDashboard(bIsCustom) {
|
||||
this.bIsCustomDashboard = bIsCustom;
|
||||
this.setAttribute("data-custom-dashboard", bIsCustom ? "true" : "false");
|
||||
SetUserPreference(`display_original_dashboard_${this.sId}`, !bIsCustom, true);
|
||||
console.log(document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]'));
|
||||
document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]').checked = bIsCustom;
|
||||
return this.ReloadFromBackend(bIsCustom);
|
||||
}
|
||||
|
||||
GetEditMode() {
|
||||
return this.bEditMode;
|
||||
}
|
||||
|
||||
ToggleEditMode(){
|
||||
this.SetEditMode(!this.bEditMode);
|
||||
return this.SetEditMode(!this.bEditMode);
|
||||
}
|
||||
|
||||
SetEditMode(bEditMode) {
|
||||
async SetEditMode(bEditMode) {
|
||||
if (this.bIsCustomDashboard === false && bEditMode === true) {
|
||||
await this.SetIsCustomDashboard(true);
|
||||
}
|
||||
|
||||
this.bEditMode = bEditMode;
|
||||
|
||||
this.oGrid.SetEditable(this.bEditMode);
|
||||
if(this.bEditMode){
|
||||
if (this.bEditMode) {
|
||||
// TODO 3.3 If we are in default dashboard display, change to custom to allow editing
|
||||
// TODO 3.3 Get the custom dashboard and load it, show a tooltip on the dashboard toggler to explain that we switched to custom mode
|
||||
this.aLastSavedState = this.Serialize();
|
||||
this.setAttribute("data-edit-mode", "edit");
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
this.setAttribute("data-edit-mode", "view");
|
||||
}
|
||||
}
|
||||
@@ -292,12 +314,16 @@ class IboDashboard extends HTMLElement {
|
||||
this.oGrid.RemoveDashlet(sDashletId);
|
||||
}
|
||||
|
||||
RefreshFromBackend(bCustomDashboard = false) {
|
||||
const sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&custom=${bCustomDashboard ? 'true' : 'false'}`;
|
||||
ReloadFromBackend(bCustomDashboard = false) {
|
||||
let sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&is_custom=${bCustomDashboard ? 'true' : 'false'}`;
|
||||
if(!bCustomDashboard && this.sFile.length > 0) {
|
||||
sLoadDashboardUrl += `&file=${encodeURIComponent(this.sFile)}`;
|
||||
}
|
||||
|
||||
fetch(sLoadDashboardUrl)
|
||||
.then(async oResponse => {
|
||||
const oDashletData = await oResponse.json();
|
||||
this.Load(oDashletData);
|
||||
this.Load(oDashletData.data);
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -332,7 +358,7 @@ class IboDashboard extends HTMLElement {
|
||||
if(res.status === 'ok') {
|
||||
CombodoToast.OpenToast(res.message, 'success');
|
||||
this.aLastSavedState = this.Serialize();
|
||||
this.SetEditMode(false);
|
||||
await this.SetEditMode(false);
|
||||
} else {
|
||||
CombodoToast.OpenToast(res.message, 'error');
|
||||
}
|
||||
@@ -342,14 +368,14 @@ class IboDashboard extends HTMLElement {
|
||||
|
||||
Load(aSaveState) {
|
||||
try {
|
||||
// TODO 3.3 Maybe we won't need to validate schema version right now as we control both sides
|
||||
// Validate schema version
|
||||
if (aSaveState.schema_version !== this.schemaVersion) {
|
||||
if (false && aSaveState.schema_version !== this.schemaVersion) {
|
||||
CombodoToast.OpenToast('Somehow, we got an incompatible dashboard schema version.', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update dashboard data
|
||||
this.sId = aSaveState.id;
|
||||
this.sTitle = aSaveState.title || "";
|
||||
this.iRefreshRate = parseInt(aSaveState.refresh, 10) || 0;
|
||||
|
||||
@@ -376,14 +402,12 @@ class IboDashboard extends HTMLElement {
|
||||
const iWidth = aDashletData.width;
|
||||
const iHeight = aDashletData.height;
|
||||
const aDashlet = aDashletData.dashlet;
|
||||
let sDashletHtml = '';
|
||||
// Check if the dashlet state has HTML content
|
||||
|
||||
// We need to fetch dashlet HTML from server as scripts need to be executed again
|
||||
let oGetDashletPromise = this.GetDashlet(aDashlet.type, aDashlet.id, JSON.stringify(aDashlet.properties));
|
||||
|
||||
|
||||
oGetDashletPromise.then(async data => {
|
||||
let sDashletHtml = await data.text();
|
||||
// Add dashlet to grid with its position and size
|
||||
// TODO 3.3 Is there a way to avoid duplicating AddDashlet call but keep the promise to avoid waiting for fetch result in this loop ?
|
||||
if(aDashletData.html && aDashletData.html.length > 0) {
|
||||
sDashletHtml = aDashletData.html;
|
||||
this.oGrid.AddDashlet(sDashletHtml, {
|
||||
x: iPosX,
|
||||
y: iPosY,
|
||||
@@ -391,7 +415,22 @@ class IboDashboard extends HTMLElement {
|
||||
h: iHeight,
|
||||
autoPosition: false
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// We need to fetch dashlet HTML from server as scripts need to be executed again
|
||||
let oGetDashletPromise = this.GetDashlet(aDashlet.type, aDashlet.id, JSON.stringify(aDashlet.properties));
|
||||
|
||||
oGetDashletPromise.then(async data => {
|
||||
let sDashletHtml = await data.text();
|
||||
// Add dashlet to grid with its position and size
|
||||
this.oGrid.AddDashlet(sDashletHtml, {
|
||||
x: iPosX,
|
||||
y: iPosY,
|
||||
w: iWidth,
|
||||
h: iHeight,
|
||||
autoPosition: false
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update last saved state
|
||||
|
||||
@@ -18,7 +18,10 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\AjaxPage;
|
||||
use Combodo\iTop\Application\WebPage\DownloadPage;
|
||||
use Combodo\iTop\Application\WebPage\JsonPage;
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\FormBlockService;
|
||||
use Combodo\iTop\PropertyType\PropertyTypeDesign;
|
||||
use Combodo\iTop\PropertyType\Serializer\XMLSerializer;
|
||||
use Combodo\iTop\Service\ServiceLocator\ServiceLocator;
|
||||
use DBObjectSearch;
|
||||
@@ -163,7 +166,7 @@ class DashboardController extends Controller
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
if (!appUserPreferences::GetPref('display_original_dashboard_'.$sDashboardId, false)) {
|
||||
if (!filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$sDashboardId, false), FILTER_VALIDATE_BOOLEAN)) {
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
@@ -192,6 +195,85 @@ class DashboardController extends Controller
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationLoad()
|
||||
{
|
||||
$sDashboardId = utils::ReadParam('id', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
$bIsCustom = utils::ReadParam('is_custom', 'false', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA) === 'true';
|
||||
$sDashboardFile = APPROOT.utils::ReadParam('file', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
|
||||
$sDashboardFileSanitized = utils::RealPath($sDashboardFile, APPROOT);
|
||||
if (false === $sDashboardFileSanitized) {
|
||||
throw new SecurityException('Invalid dashboard file !');
|
||||
}
|
||||
|
||||
$sStatus = 'error';
|
||||
$sMessage = 'Unknown error';
|
||||
$aData = [];
|
||||
|
||||
try {
|
||||
if ($bIsCustom) {
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
|
||||
$oUDSearch->AddCondition('menu_code', $sDashboardId, '=');
|
||||
$oUDSet = new DBObjectSet($oUDSearch);
|
||||
if ($oUDSet->Count() > 0) {
|
||||
// Assuming there is at most one couple {user, menu}!
|
||||
$oUserDashboard = $oUDSet->Fetch();
|
||||
$sDashboardDefinition = $oUserDashboard->Get('contents');
|
||||
} else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$sDashboardDefinition = @file_get_contents($sDashboardFileSanitized);
|
||||
}
|
||||
|
||||
// TODO 3.3 If the dashboard definition is previous schema, we need to convert it to the new one
|
||||
|
||||
|
||||
$oDoc = new PropertyTypeDesign();
|
||||
$oDoc->loadXML($sDashboardDefinition);
|
||||
|
||||
$oMainNode = $oDoc->getElementsByTagName('dashboard')->item(0);
|
||||
|
||||
$aData = $this->oXMLSerializer->Deserialize($oMainNode, 'DashboardGrid', 'Dashboard');
|
||||
|
||||
// TODO 3.3 Re-render every dashlet to have their latest representation
|
||||
// Let's render every dashlet to prevent frontend from having to do it for each in individual ajax call
|
||||
// if (array_key_exists('pos_dashlets', $aData)) {
|
||||
// foreach ($aData['pos_dashlets'] as $sDashletId => $sPosValues) {
|
||||
// if(array_key_exists('dashlet', $sPosValues)) {
|
||||
// $sDashletClass = $sPosValues['dashlet']['type'];
|
||||
// $aValues = $sPosValues['dashlet']['properties'];
|
||||
//
|
||||
// $oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, $sDashletId);
|
||||
// $oDashlet->FromModelData($aValues);
|
||||
//
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
$sStatus = 'ok';
|
||||
$sMessage = 'Dashboard loaded';
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Exception($e->getMessage(), $e);
|
||||
$sStatus = 'error';
|
||||
$sMessage = $e->getMessage();
|
||||
}
|
||||
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetData([
|
||||
'status' => $sStatus,
|
||||
'message' => $sMessage,
|
||||
'data' => $aData
|
||||
]);
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
public function OperationImport()
|
||||
{
|
||||
$oPage = new JsonPage();
|
||||
|
||||
@@ -364,6 +364,8 @@ abstract class Dashboard
|
||||
}
|
||||
|
||||
$oDashboard = $oLayout->Render($oPage, $aDashlets, $bEditMode, $aExtraParams);
|
||||
$oDashboard->SetFile(utils::LocalPath($this->GetDefinitionFile()));
|
||||
|
||||
$oPage->AddUiBlock($oDashboard);
|
||||
|
||||
$bFromDashboardPage = isset($aExtraParams['from_dashboard_page']) ? isset($aExtraParams['from_dashboard_page']) : false;
|
||||
|
||||
@@ -245,6 +245,10 @@ class RuntimeDashboard extends Dashboard
|
||||
|
||||
$oDashboard = parent::Render($oPage, $bEditMode, $aRenderParams);
|
||||
|
||||
if($this->HasCustomDashboard() && !filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$this->GetId(), false), FILTER_VALIDATE_BOOLEAN)) {
|
||||
$oDashboard->SetIsCustom(true);
|
||||
}
|
||||
|
||||
if (isset($aExtraParams['query_params']['this->object()'])) {
|
||||
/** @var \DBObject $oObj */
|
||||
$oObj = $aExtraParams['query_params']['this->object()'];
|
||||
@@ -321,15 +325,12 @@ EOF
|
||||
|
||||
$sSwitchToStandard = Dict::S('UI:Toggle:SwitchToStandardDashboard');
|
||||
$sSwitchToCustom = Dict::S('UI:Toggle:SwitchToCustomDashboard');
|
||||
$bStandardSelected = appUserPreferences::GetPref('display_original_dashboard_'.$sId, false);
|
||||
$bStandardSelected = filter_var(appUserPreferences::GetPref('display_original_dashboard_'.$sId, false), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$sSelectorHtml = '<div id="ibo-dashboard-selector'.$sDivId.'" class="ibo-dashboard--selector" data-tooltip-content="'.($bStandardSelected ? $sSwitchToCustom : $sSwitchToStandard).'">';
|
||||
$sSelectorHtml .= '<label class="ibo-dashboard--switch"><input type="checkbox" onchange="ToggleDashboardSelector'.$sDivId.'();" '.($bStandardSelected ? '' : 'checked').'><span class="ibo-dashboard--slider"></span></label></input></label>';
|
||||
$sSelectorHtml = '<div class="ibo-dashboard--selector" data-dashboard-id="'.$sDivId.'" data-tooltip-content="'.($bStandardSelected ? $sSwitchToCustom : $sSwitchToStandard).'">';
|
||||
$sSelectorHtml .= '<label class="ibo-dashboard--switch"><input type="checkbox" '.($bStandardSelected ? '' : 'checked').'><span class="ibo-dashboard--slider"></span></label></input></label>';
|
||||
$sSelectorHtml .= '</div>';
|
||||
|
||||
$sFile = addslashes($this->GetDefinitionFile());
|
||||
$sReloadURL = json_encode($this->GetReloadURL());
|
||||
|
||||
$bFromDashboardPage = isset($aAjaxParams['from_dashboard_page']) ? isset($aAjaxParams['from_dashboard_page']) : false;
|
||||
if ($bFromDashboardPage) {
|
||||
if ($oPage instanceof iTopWebPage) {
|
||||
@@ -340,29 +341,6 @@ EOF
|
||||
$oToolbar = $oDashboard->GetToolbar();
|
||||
$oToolbar->AddHtml($sSelectorHtml);
|
||||
}
|
||||
|
||||
$oPage->add_script(
|
||||
<<<JS
|
||||
function ToggleDashboardSelector$sDivId()
|
||||
{
|
||||
var dashboard = $('.ibo-dashboard#$sDivId')
|
||||
dashboard.block();
|
||||
$.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
|
||||
{ operation: 'toggle_dashboard', dashboard_id: '$sId', file: '$sFile', extra_params: $sExtraParams, reload_url: '$sReloadURL' },
|
||||
function(data) {
|
||||
dashboard.html(data);
|
||||
dashboard.unblock();
|
||||
if ($('#ibo-dashboard-selector$sDivId input').prop("checked")) {
|
||||
$('#ibo-dashboard-selector$sDivId').attr('data-tooltip-content', '$sSwitchToStandard');
|
||||
} else {
|
||||
$('#ibo-dashboard-selector$sDivId').attr('data-tooltip-content', '$sSwitchToCustom');
|
||||
}
|
||||
CombodoTooltip.InitAllNonInstantiatedTooltips($('#ibo-dashboard-selector$sDivId').parent(), true);
|
||||
}
|
||||
);
|
||||
}
|
||||
JS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,6 +348,7 @@ JS
|
||||
*/
|
||||
protected function HasCustomDashboard()
|
||||
{
|
||||
// TODO 3.3 Make it more efficient by caching the result
|
||||
try {
|
||||
// Search for an eventual user defined dashboard
|
||||
$oUDSearch = new DBObjectSearch('UserDashboard');
|
||||
|
||||
@@ -43,6 +43,10 @@ class DashboardLayout extends UIBlock
|
||||
protected $oRefreshInput;
|
||||
protected $oButtonsToolbar;
|
||||
|
||||
protected $sFile;
|
||||
|
||||
protected $bIsCustom = false;
|
||||
|
||||
public function __construct(?string $sId = null)
|
||||
{
|
||||
parent::__construct($sId);
|
||||
@@ -53,6 +57,7 @@ class DashboardLayout extends UIBlock
|
||||
$this->oTitleInput = $this->MakeTitleInput();
|
||||
$this->oRefreshInput = $this->MakeRefreshInput();
|
||||
$this->oButtonsToolbar = $this->MakeButtonsToolbar();
|
||||
$this->sFile = '';
|
||||
}
|
||||
|
||||
public function MakeTitleInput()
|
||||
@@ -192,4 +197,28 @@ class DashboardLayout extends UIBlock
|
||||
{
|
||||
return $this->oButtonsToolbar;
|
||||
}
|
||||
|
||||
public function SetFile(string $sFile)
|
||||
{
|
||||
$this->sFile = $sFile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function GetFile()
|
||||
{
|
||||
return $this->sFile;
|
||||
}
|
||||
|
||||
public function IsCustom(): bool
|
||||
{
|
||||
return $this->bIsCustom;
|
||||
}
|
||||
|
||||
public function SetIsCustom(bool $bIsCustom)
|
||||
{
|
||||
$this->bIsCustom = $bIsCustom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,16 @@
|
||||
{{ render_block(oUIBlock.GetToolbar(), {aPage: aPage}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<ibo-dashboard id="{{ oUIBlock.GetId() }}" class="ibo-dashboard" data-role="ibo-dashboard" data-edit-mode="view">
|
||||
<ibo-dashboard id="{{ oUIBlock.GetId() }}"
|
||||
class="ibo-dashboard"
|
||||
data-role="ibo-dashboard"
|
||||
data-edit-mode="view"
|
||||
data-file="{{ oUIBlock.GetFile() }}"
|
||||
data-is-custom="{{ oUIBlock.IsCustom() ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="ibo-dashboard--form">
|
||||
<div class="ibo-dashboard--form--inputs">
|
||||
Editing{{ render_block(oUIBlock.GetTitleInput(), {aPage: aPage}) }}{{ render_block(oUIBlock.GetRefreshInput(), {aPage: aPage}) }}
|
||||
Editing {{ render_block(oUIBlock.GetTitleInput(), {aPage: aPage}) }}{{ render_block(oUIBlock.GetRefreshInput(), {aPage: aPage}) }}
|
||||
</div>
|
||||
<div class="ibo-dashboard--form--actions">
|
||||
{{ render_block(oUIBlock.GetButtonsToolbar(), {aPage: aPage}) }}
|
||||
|
||||
Reference in New Issue
Block a user