mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-24 02:58:43 +02:00
462 lines
15 KiB
JavaScript
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);
|