Files
iTop/js/layouts/dashboard/dashboard.js
2026-02-06 11:39:20 +01:00

462 lines
15 KiB
JavaScript

class IboDashboard extends HTMLElement {
constructor() {
super();
/** @type {string} */
this.sId = "";
/** @type {string} */
this.sTitle = "";
/** @type {boolean} unused yet */
this.isEditable = false;
/** @type {boolean} */
this.bEditMode = false;
/** @type {boolean} unused yet */
this.bAutoRefresh = false;
/** @type {number} unused yet */
this.iRefreshRate = 0;
/** @type {IboGrid|null} */
this.oGrid = null;
/** @type {string|null} unused yet */
this.refreshUrl = null;
/** @type {string|null} unused yet */
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;
}
connectedCallback() {
this.sId = this.getAttribute("id");
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();
}
BindEvents() {
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.addEventListener('click', (e) => {
e.preventDefault();
this.ToggleEditMode();
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.classList.toggle('active', this.GetEditMode());
});
this.querySelector('[data-role="ibo-button"][name="save"]')?.addEventListener('click', (e) => {
e.preventDefault();
this.Save()
});
this.querySelector('[data-role="ibo-button"][name="cancel"]')?.addEventListener('click', (e) => {
e.preventDefault();
if (this.aLastSavedState) {
this.Load(this.aLastSavedState);
}
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
// Bind IboDashboard object to these listener so we can access current instance
this._ListenToDashletFormSubmission = this._ListenToDashletFormSubmission.bind(this);
this._ListenToDashletFormCancellation = this._ListenToDashletFormCancellation.bind(this);
}
SetupGrid() {
if(this.oGrid !== null){
return;
}
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"]'));
let checkbox = document.querySelector('.ibo-dashboard--selector[data-dashboard-id="'+this.sId+'"] input[type="checkbox"]');
if (checkbox) {
checkbox.checked = bIsCustom;
}
return this.ReloadFromBackend(bIsCustom);
}
GetEditMode() {
return this.bEditMode;
}
ToggleEditMode(){
return this.SetEditMode(!this.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) {
// 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 {
this.setAttribute("data-edit-mode", "view");
}
}
AddNewDashlet(sDashletClass, sDashletValues, aDashletOptions = {}) {
let oGetDashletPromise = this.GetDashlet(sDashletClass, '', sDashletValues);
oGetDashletPromise.then(async data => {
const sDashletId = this.oGrid.AddDashlet(await data.text(), aDashletOptions);
// Specify that this dashlet is new
this.EditDashlet(sDashletId, true);
})
}
GetDashlet(sDashletClass, sDashletId = '', sDashletValues = '') {
let sGetDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet&dashlet_class='+encodeURIComponent(sDashletClass);
if(sDashletId.length > 0) {
sGetDashletUrl += '&dashlet_id=' + encodeURIComponent(sDashletId);
}
if(sDashletValues.length > 0) {
sGetDashletUrl += '&values=' + encodeURIComponent(sDashletValues);
}
return fetch(sGetDashletUrl);
}
RefreshDashlet(oDashlet) {
let oGetDashletPromise = this.GetDashlet(oDashlet.sType, oDashlet.sDashletId, oDashlet.formData);
return oGetDashletPromise.then(async data => {
this.oGrid.RefreshDashlet(await data.text());
});
}
HideDashletTogglers() {
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
aTogglers.classList.add('ibo-is-hidden');
}
ShowDashletTogglers() {
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
aTogglers.classList.remove('ibo-is-hidden');
}
SetDashletForm(sFormData) {
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
oFormContainer.innerHTML = sFormData;
oFormContainer.classList.remove('ibo-is-hidden');
}
ClearDashletForm() {
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
oFormContainer.innerHTML = '';
oFormContainer.classList.add('ibo-is-hidden');
}
DisableFormButtons() {
const aButtons = this.querySelectorAll('.ibo-dashboard--form--actions button');
aButtons.forEach( (oButton) => {
oButton.setAttribute('disabled', 'disabled');
});
}
EnableFormButtons() {
const aButtons = this.querySelectorAll('.ibo-dashboard--form--actions button');
aButtons.forEach( (oButton) => {
oButton.removeAttribute('disabled');
});
}
EditDashlet(sDashletId, bIsNew = false) {
const oDashlet = this.oGrid.GetDashletElement(sDashletId);
const me = this;
// Create backdrop to block interactions with other dashlets
if(this.oGrid.querySelector('.ibo-dashboard--grid--backdrop') === null) {
const oBackdrop = document.createElement("div");
oBackdrop.classList.add('ibo-dashboard--grid--backdrop');
this.oGrid.append(oBackdrop);
}
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'edit');
const oPanelElement = document.querySelector('.ibo-dashlet-panel');
// Choose what we'll write as title
// Also store this information in a data attribute to be able to differentiate between addition and edition on form submission/cancellation
if(bIsNew) {
this.SetDashletPanelTitle('Add a dashlet ' + oDashlet.sType);
oPanelElement.setAttribute('data-dashlet-form-mode', 'add');
}
else {
this.SetDashletPanelTitle('Edit dashlet ' + oDashlet.sType);
oPanelElement.setAttribute('data-dashlet-form-mode', 'edit');
}
// Disable dashboard buttons so we need to finish this edition first
this.DisableFormButtons();
// Fetch dashlet form from server
let sGetashletFormUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet_form&dashlet_class='+encodeURIComponent(oDashlet.sType);
if(oDashlet.formData.length > 0) {
sGetashletFormUrl += '&values=' + encodeURIComponent(oDashlet.formData);
}
fetch(sGetashletFormUrl)
.then(async formData => {
const sFormData = await formData.text();
this.HideDashletTogglers();
this.SetDashletForm(sFormData);
// Listen to form submission event and cancellation
document.addEventListener('itop:TurboStreamEvent:Complete', me._ListenToDashletFormSubmission);
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').addEventListener('click', me._ListenToDashletFormCancellation);
});
}
_ListenToDashletFormSubmission(event) {
const oDashlet = this.querySelector('ibo-dashlet[data-edit-mode="edit"]');
const sDashletId = oDashlet.GetDashletId();
if(event.detail.id === oDashlet.sType + '-turbo-stream-event' && event.detail.valid === "1") {
// Remove events
document.addEventListener('itop:TurboStreamEvent:Complete', this._ListenToDashletFormSubmission);
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation);
// Notify it all went well
CombodoToast.OpenToast('Dashlet created/updated', 'success');
// Clean edit mode
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view');
document.querySelector('.ibo-dashlet-panel').removeAttribute('data-dashlet-form-mode');
this.ShowDashletTogglers();
this.ClearDashletForm();
this.SetDashletPanelTitle();
// Update local dashlet and refresh it
oDashlet.formData = event.detail.view_data;
this.RefreshDashlet(oDashlet);
// Re-enable dashboard buttons
this.EnableFormButtons();
}
}
_ListenToDashletFormCancellation(event) {
const oDashlet = this.querySelector('ibo-dashlet[data-edit-mode="edit"]');
const sDashletId = oDashlet.GetDashletId();
// If we are cancelling an addition, remove the dashlet from the grid
const oPanelElement = document.querySelector('.ibo-dashlet-panel');
const sDashletFormMode = oPanelElement.getAttribute('data-dashlet-form-mode');
if(sDashletFormMode === 'add') {
this.oGrid.RemoveDashlet(sDashletId);
}
else if(sDashletFormMode === 'edit') {
// Just exit edit mode
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view');
// TODO 3.3 If we refresh dashlet view in edit mode, we should restore previous form data + rendering
}
// Remove events
document.addEventListener('itop:TurboStreamEvent:Complete', this._ListenToDashletFormSubmission);
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation);
// Clean edit mode
this.ShowDashletTogglers();
this.ClearDashletForm();
this.SetDashletPanelTitle();
// Re-enable dashboard buttons
this.EnableFormButtons();
}
SetDashletPanelTitle(sTitle = '') {
const oTitleElement = document.querySelector('.ibo-dashlet-panel .ibo-dashlet-panel--title');
if (sTitle === '') {
sTitle = 'Add a dashlet';
}
if (oTitleElement) {
oTitleElement.innerText = sTitle;
}
}
CloneDashlet(sDashletId) {
this.oGrid.CloneDashlet(sDashletId);
}
RemoveDashlet(sDashletId) {
this.oGrid.RemoveDashlet(sDashletId);
}
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.data);
}
)
}
Serialize() {
const sDashboardTitle = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]').value;
const sDashboardRefreshRate = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]').value;
const aSerializedGrid = this.oGrid.Serialize();
return {
schema_version: this.schemaVersion,
id: this.sId,
title: sDashboardTitle,
refresh: sDashboardRefreshRate,
pos_dashlets: aSerializedGrid,
_token: ":)"
};
}
Save() {
// This payload shape is expected by the server
const aPayload = this.Serialize();
let sSaveUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.save';
fetch(sSaveUrl, {
method: "POST",
body: new URLSearchParams({ values: JSON.stringify(aPayload) }),
})
.then(async data => {
const res = await data.json();
if(res.status === 'ok') {
CombodoToast.OpenToast(res.message, 'success');
this.aLastSavedState = this.Serialize();
await this.SetEditMode(false);
} else {
CombodoToast.OpenToast(res.message, 'error');
}
})
}
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 (false && aSaveState.schema_version !== this.schemaVersion) {
CombodoToast.OpenToast('Somehow, we got an incompatible dashboard schema version.', 'error');
return false;
}
// Update dashboard data
this.sTitle = aSaveState.title || "";
this.iRefreshRate = parseInt(aSaveState.refresh, 10) || 0;
// Update form inputs if they exist
const oTitleInput = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]');
if (oTitleInput) {
oTitleInput.value = this.sTitle;
}
const oRefreshSelect = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]');
if (oRefreshSelect) {
oRefreshSelect.value = aSaveState.refresh;
}
// Clear existing grid
this.ClearGrid();
// Load dashlets
const aDashletSlots = aSaveState.pos_dashlets || {};
for (const [sDashletId, aDashletData] of Object.entries(aDashletSlots)) {
const iPosX = aDashletData.position_x;
const iPosY = aDashletData.position_y;
const iWidth = aDashletData.width;
const iHeight = aDashletData.height;
const aDashlet = aDashletData.dashlet;
let sDashletHtml = '';
// Check if the dashlet state has HTML content
// 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,
w: iWidth,
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
this.aLastSavedState = aSaveState;
return true;
} catch (error) {
console.error('Error loading dashboard state:', error);
CombodoToast.OpenToast('Error loading dashboard state', 'error');
return false;
}
}
ClearGrid() {
this.oGrid.ClearGrid();
}
DisplayError(sMessage, sSeverity = 'error') {
// TODO 3.3: Make this real
this.setAttribute("data-edit-mode", "error");
}
}
customElements.define('ibo-dashboard', IboDashboard);