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 {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.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); }); // 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'); } GetEditMode() { return this.bEditMode; } ToggleEditMode(){ this.SetEditMode(!this.bEditMode); } SetEditMode(bEditMode) { 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(true); this.setAttribute("data-edit-mode", "edit"); } else{ this.setAttribute("data-edit-mode", "view"); } } AddNewDashlet(sDashletClass, sDashletValues, aDashletOptions = {}) { let sNewDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet&dashlet_class='+encodeURIComponent(sDashletClass); if(sDashletValues.length > 0) { sNewDashletUrl += '&values=' + encodeURIComponent(sDashletValues); } fetch(sNewDashletUrl) .then(async data => { const sDashletId = this.oGrid.AddDashlet(await data.text(), aDashletOptions); // TODO 3.3 Either open the dashlet form right away, or just enter edit mode this.EditDashlet(sDashletId); }) } RefreshDashlet(oDashlet) { let sGetDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet&dashlet_class=' + encodeURIComponent(oDashlet.sType) +'&dashlet_id=' + encodeURIComponent(oDashlet.sDashletId); if(oDashlet.formData.length > 0) { sGetDashletUrl += '&values=' + encodeURIComponent(oDashlet.formData); } return fetch(sGetDashletUrl) .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) { 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'); // 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', 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', 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'); this.ShowDashletTogglers(); this.ClearDashletForm(); // 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(); // Remove events document.addEventListener('itop:TurboStreamEvent', this._ListenToDashletFormSubmission); document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation); // Clean edit mode this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view'); this.ShowDashletTogglers(); this.ClearDashletForm(); // Re-enable dashboard buttons this.EnableFormButtons(); // TODO 3.3 If this is an addition, remove the previewed dashlet // TODO 3.3 If this is an edition, revert the dashlet to its initial state } CloneDashlet(sDashletId) { this.oGrid.CloneDashlet(sDashletId); } RemoveDashlet(sDashletId) { this.oGrid.RemoveDashlet(sDashletId); } RefreshFromBackend(bCustomDashboard = false) { const sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&custom=${bCustomDashboard ? 'true' : 'false'}`; fetch(sLoadDashboardUrl) .then(async oResponse => { const oDashletData = await oResponse.json(); this.Load(oDashletData); } ) } Serialize(bIncludeHtml = false) { 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(bIncludeHtml); 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&values='+encodeURIComponent(JSON.stringify(aPayload)); fetch(sSaveUrl) .then(async data => { const res = await data.json(); if(res.status === 'ok') { CombodoToast.OpenToast(res.message, 'success'); this.aLastSavedState = this.Serialize(true); this.SetEditMode(false); } else { CombodoToast.OpenToast(res.message, 'error'); } }) } Load(aSaveState) { try { // Validate schema version if (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; // 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; // We store the dashlet component in the HTML, not only the rendered dashlet const sDashetHtml = aDashlet.html; // Add dashlet to grid with its position and size this.oGrid.AddDashlet(sDashetHtml, { 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);