From 9c2a9774c30bddf7f274c0afc2aa037c9e158c86 Mon Sep 17 00:00:00 2001 From: crashdance Date: Sun, 18 Jan 2026 15:51:36 +0100 Subject: [PATCH 01/10] feature: display action buttons and object versions if error on object tree page --- .../public/common/timestampSelectForm.js | 57 ++++------ QualityControl/public/object/QCObject.js | 6 +- .../public/object/objectTreePage.js | 103 +++++++++++------- 3 files changed, 93 insertions(+), 73 deletions(-) diff --git a/QualityControl/public/common/timestampSelectForm.js b/QualityControl/public/common/timestampSelectForm.js index 4046b6887..79b314329 100644 --- a/QualityControl/public/common/timestampSelectForm.js +++ b/QualityControl/public/common/timestampSelectForm.js @@ -20,38 +20,29 @@ import { prettyFormatDate } from './utils.js'; * @param {Model} model - root model of the application * @returns {vnode} - virtual node element */ -export default ({ object: objectModel }) => { - const { objects, selected } = objectModel; - const isObjectLoaded = selected && objects?.[selected.name]?.isSuccess(); - return h( +export const timestampSelectForm = ({ versions = [], selectedId = null, onSelect }) => + h( '.w-100.flex-row', - isObjectLoaded && - h('select.form-control.gray-darker.text-center', { - onchange: (e) => { - const { value } = e.target; - if (selected && value !== 'Invalid Timestamp') { - const valueJson = JSON.parse(value); - objectModel.loadObjectByName(selected.name, valueJson.validFrom, valueJson.id); - } - }, - }, [ - objectModel.getObjectVersions(selected.name) - .map((version) => { - const versionString = JSON.stringify(version); - const object = objects[selected.name].payload; - return h('option.text-center', { - id: versionString, - key: versionString, - value: versionString, - selected: version.createdAt === object.createdAt ? true : false, - }, [ - 'Created: ', - prettyFormatDate(version.createdAt), - ' (id: ', - version.id, - ')', - ]); - }), - ]), + h('select.form-control.gray-darker.text-center', { + onchange: (e) => { + const { value } = e.target; + if (value && value !== 'Invalid Timestamp') { + onSelect?.(JSON.parse(value)); + } + }, + }, versions.map((version) => { + const versionString = JSON.stringify(version); + return h('option.text-center', { + id: versionString, + key: versionString, + value: versionString, + selected: selectedId ? version.id === selectedId : false, + }, [ + 'Created: ', + prettyFormatDate(version.createdAt), + ' (id: ', + version.id, + ')', + ]); + })), ); -}; diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index 72780449e..c8758fbfc 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -345,11 +345,11 @@ export default class QCObject extends BaseViewModel { /** * Indicate that the object loaded is wrong. Used after trying to print it with jsroot * @param {string} name - name of the object - * @param {string} reason - the reason for invalidating the object + * @param {object} details - object containing detail information for invalidation * @returns {undefined} */ - invalidObject(name, reason) { - this.objects[name] = RemoteData.failure(reason || 'JSROOT was unable to draw this object'); + invalidObject(name, details) { + this.objects[name] = RemoteData.failure(details || 'JSROOT was unable to draw this object'); this.notify(); } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 7a9b19716..6b017ad07 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -1,5 +1,5 @@ /** - * @license +* @license * Copyright 2019-2020 CERN and copyright holders of ALICE O2. * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. * All rights not expressly granted are reserved. @@ -23,7 +23,7 @@ import { } from '/js/src/index.js'; import { spinner } from '../common/spinner.js'; import { draw } from '../common/object/draw.js'; -import timestampSelectForm from './../common/timestampSelectForm.js'; +import { timestampSelectForm } from './../common/timestampSelectForm.js'; import virtualTable from './virtualTable.js'; import { defaultRowAttributes, qcObjectInfoPanel } from '../common/object/objectInfoCard.js'; import { downloadButton } from '../common/downloadButton.js'; @@ -93,14 +93,13 @@ export default (model) => { */ function objectPanel(model) { const selectedObjectName = model.object.selected.name; - if (model.object.objects && model.object.objects[selectedObjectName]) { + if (model.object.objects?.[selectedObjectName]) { return model.object.objects[selectedObjectName].match({ NotAsked: () => null, Loading: () => h('.h-100.w-100.flex-column.items-center.justify-center.f5', [spinner(3), h('', 'Loading Object')]), Success: (data) => drawPlot(model, data), - Failure: (error) => - h('.h-100.w-100.flex-column.items-center.justify-center.f5', [h('.f1', iconCircleX()), error]), + Failure: (invalidObjectDetails) => drawFailure(model, invalidObjectDetails), }); } return null; @@ -113,46 +112,76 @@ function objectPanel(model) { * @returns {vnode} - virtual node element */ const drawPlot = (model, object) => { - const { name, qcObject, validFrom, id } = object; - const { root } = qcObject; - const href = validFrom ? - `?page=objectView&objectName=${name}&ts=${validFrom}&id=${id}` - : `?page=objectView&objectName=${name}`; - return h('', { style: 'height:100%; display: flex; flex-direction: column' }, [ - h('.item-action-row.flex-row.g1.p1', [ - downloadRootImageButton(`${name}.png`, root, ['stat']), - downloadButton({ - href: model.objectViewModel.getDownloadQcdbObjectUrl(id), - title: 'Download root object', - }), - h( - 'a.btn#fullscreen-button', - { - title: 'Open object plot in full screen', - href, - onclick: (e) => model.router.handleLinkEvent(e), - }, - iconResizeBoth(), - ), - h( - 'a.btn#close-button', - { - title: 'Close the object plot', - onclick: () => model.object.select(), - }, - iconCircleX(), - ), - ]), + const { name, qcObject, validFrom, id, versions } = object; + return h('.h-100.flex-column', [ + actionButtonsRow(model, name, qcObject, validFrom, id), h('', { style: 'height:77%;' }, draw(model.object.objects[name], { }, ['stat'], (error) => { - model.object.invalidObject(name, error.message); + const invalidObjectDetails = { name, message: error.message, validFrom, id, versions }; + model.object.invalidObject(name, invalidObjectDetails); })), h('.scroll-y', {}, [ - h('.w-100.flex-row', { style: 'justify-content: center' }, h('.w-80', timestampSelectForm(model))), + h('.w-100.flex-row.justify-center', h('.w-80', timestampSelectForm({ + versions: versions ?? [], + selectedId: id ?? null, + onSelect: (version) => model.object.loadObjectByName(name, version.validFrom, version.id), + }))), qcObjectInfoPanel(object, { 'font-size': '.875rem;' }, defaultRowAttributes(model.notification)), ]), ]); }; +/** + * Draw the failure message when object cannot be drawn + * @param {Model} model - root model of the application + * @param {object} invalidObjectDetails - details about the invalid object + * @returns {vnode} - virtual node element + */ +const drawFailure = (model, invalidObjectDetails) => { + const { name, message, validFrom, id, versions } = invalidObjectDetails ?? {}; + return h('.h-100.flex-column', [ + actionButtonsRow(model, name, null, validFrom, id), + h( + '.h-100.flex-column.items-center.justify-center.text-center.f5', + [h('.f1', iconCircleX()), message], + ), + h('.w-100.flex-row.justify-center.pv2', h('.w-80', timestampSelectForm({ + versions: versions ?? [], + selectedId: id ?? null, + onSelect: (version) => model.object.loadObjectByName(name, version.validFrom, version.id), + }))), + ]); +}; + +const actionButtonsRow = (model, objectName, root, validFrom, id) => { + const href = validFrom + ? `?page=objectView&objectName=${objectName}&ts=${validFrom}&id=${id}` + : `?page=objectView&objectName=${objectName}`; + return h('.item-action-row.flex-row.g1.p1', [ + root && downloadRootImageButton(`${objectName}.png`, root, ['stat']), + downloadButton({ + href: model.objectViewModel.getDownloadQcdbObjectUrl(id), + title: 'Download root object', + }), + h( + 'a.btn#fullscreen-button', + { + title: 'Open object plot in full screen', + href, + onclick: (e) => model.router.handleLinkEvent(e), + }, + iconResizeBoth(), + ), + h( + 'a.btn#close-button', + { + title: 'Close the object plot', + onclick: () => model.object.select(), + }, + iconCircleX(), + ), + ]); +}; + /** * Shows status of current tree with its options (online, loaded, how many) * @param {Model} model - root model of the application From 4207f18614f7b63a22ed483fc82844021a2e28fb Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 19 Jan 2026 20:09:03 +0100 Subject: [PATCH 02/10] Stage commit until further details --- .../public/common/enums/root.enum.js | 15 +++++++ .../public/common/timestampSelectForm.js | 45 ++++++++++++------- QualityControl/public/object/QCObject.js | 4 +- .../public/object/objectTreePage.js | 7 ++- 4 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 QualityControl/public/common/enums/root.enum.js diff --git a/QualityControl/public/common/enums/root.enum.js b/QualityControl/public/common/enums/root.enum.js new file mode 100644 index 000000000..050b73dd7 --- /dev/null +++ b/QualityControl/public/common/enums/root.enum.js @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export const ROOT_ERROR_LABEL = 'ROOT_ERROR'; diff --git a/QualityControl/public/common/timestampSelectForm.js b/QualityControl/public/common/timestampSelectForm.js index 79b314329..8838886cf 100644 --- a/QualityControl/public/common/timestampSelectForm.js +++ b/QualityControl/public/common/timestampSelectForm.js @@ -17,7 +17,10 @@ import { prettyFormatDate } from './utils.js'; /** * Display a select form with the latest timestamps of the current selected object - * @param {Model} model - root model of the application + * @param {object} config - root model of the application + * @param {Array<{id: string, createdAt: string}>} config.versions - list of versions to display + * @param {string|null} config.selectedId - currently selected version id + * @param {onselect} config.onSelect - callback when a version is selected * @returns {vnode} - virtual node element */ export const timestampSelectForm = ({ versions = [], selectedId = null, onSelect }) => @@ -30,19 +33,29 @@ export const timestampSelectForm = ({ versions = [], selectedId = null, onSelect onSelect?.(JSON.parse(value)); } }, - }, versions.map((version) => { - const versionString = JSON.stringify(version); - return h('option.text-center', { - id: versionString, - key: versionString, - value: versionString, - selected: selectedId ? version.id === selectedId : false, - }, [ - 'Created: ', - prettyFormatDate(version.createdAt), - ' (id: ', - version.id, - ')', - ]); - })), + }, versions.map((version) => versionOptionNode(version, selectedId === version.id))), ); + +/** + * Create an option HTML element for a version + * @param {object} version - version object + * @param {string} version.id - version id + * @param {string} version.createdAt - version creation timestamp + * @param {boolean} isSelected - whether the version is selected + * @returns {vnode} - virtual node element + */ +const versionOptionNode = (version, isSelected = false) => { + const versionString = JSON.stringify(version); + return h('option.text-center', { + id: versionString, + key: versionString, + value: versionString, + selected: isSelected, + }, [ + 'Created: ', + prettyFormatDate(version.createdAt), + ' (id: ', + version.id, + ')', + ]); +}; diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index c8758fbfc..61f3947af 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -18,6 +18,7 @@ import { prettyFormatDate, setBrowserTabTitle } from './../common/utils.js'; import { isObjectOfTypeChecker } from './../library/qcObject/utils.js'; import { BaseViewModel } from '../common/abstracts/BaseViewModel.js'; import { StorageKeysEnum } from '../common/enums/storageKeys.enum.js'; +import { ROOT_ERROR_LABEL } from '../common/enums/root.enum.js'; /** * Model namespace for all about QC's objects (not javascript objects) @@ -349,7 +350,8 @@ export default class QCObject extends BaseViewModel { * @returns {undefined} */ invalidObject(name, details) { - this.objects[name] = RemoteData.failure(details || 'JSROOT was unable to draw this object'); + const errorMessage = `${ROOT_ERROR_LABEL}: ${details || 'JSROOT was unable to draw this object'}`; + this.objects[name] = RemoteData.failure(errorMessage); this.notify(); } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 6b017ad07..28ada6405 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -100,6 +100,7 @@ function objectPanel(model) { h('.h-100.w-100.flex-column.items-center.justify-center.f5', [spinner(3), h('', 'Loading Object')]), Success: (data) => drawPlot(model, data), Failure: (invalidObjectDetails) => drawFailure(model, invalidObjectDetails), + // draw failure is already applied in drawPlot -> draw }); } return null; @@ -137,14 +138,16 @@ const drawPlot = (model, object) => { * @returns {vnode} - virtual node element */ const drawFailure = (model, invalidObjectDetails) => { - const { name, message, validFrom, id, versions } = invalidObjectDetails ?? {}; + const { name, message, validFrom, id } = invalidObjectDetails ?? {}; + const versions = undefined; + const isRootError = message?.includes('ROOT_ERROR'); return h('.h-100.flex-column', [ actionButtonsRow(model, name, null, validFrom, id), h( '.h-100.flex-column.items-center.justify-center.text-center.f5', [h('.f1', iconCircleX()), message], ), - h('.w-100.flex-row.justify-center.pv2', h('.w-80', timestampSelectForm({ + isRootError && h('.w-100.flex-row.justify-center.pv2', h('.w-80', timestampSelectForm({ versions: versions ?? [], selectedId: id ?? null, onSelect: (version) => model.object.loadObjectByName(name, version.validFrom, version.id), From 45ec4548e882956784d7cdb09e3425ca3c5af02f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 14:44:44 +0100 Subject: [PATCH 03/10] Add rootError to display if failure to plot --- QualityControl/public/common/object/draw.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/QualityControl/public/common/object/draw.js b/QualityControl/public/common/object/draw.js index b51acdcfa..e54978ad2 100644 --- a/QualityControl/public/common/object/draw.js +++ b/QualityControl/public/common/object/draw.js @@ -14,10 +14,11 @@ /* global JSROOT */ -import { h, iconWarning } from '/js/src/index.js'; +import { h } from '/js/src/index.js'; import { generateDrawingOptionString, isObjectOfTypeChecker } from './../../../library/qcObject/utils.js'; import checkersPanel from './checkersPanel.js'; import { keyedTimerDebouncer, pointerId } from '../utils.js'; +import { failureToDrawPanel } from './failureToDrawPanel.js'; /** * Renders a QCObject as a virtual DOM node using JSROOT. @@ -32,14 +33,11 @@ import { keyedTimerDebouncer, pointerId } from '../utils.js'; * @param {(Error) => void} failFn - optional function to execute upon drawing failure * @returns {vnode} output virtual-dom, a single div with JSROOT attached to it */ -export const draw = (remoteData, options = {}, drawingOptions = [], failFn = () => {}) => +export const draw = (remoteData = {}, options = {}, drawingOptions = [], failFn = () => {}) => remoteData?.match({ NotAsked: () => null, Loading: () => h('.flex-column.items-center.justify-center', [h('.animate-slow-appearance', 'Loading')]), - Failure: (error) => h('.error-box.danger.flex-column.justify-center.f6.text-center', {}, [ - h('span.error-icon', { title: 'Error' }, iconWarning()), - h('span', error), - ]), + Failure: (error) => failureToDrawPanel(error), Success: (data) => drawObject(data, options, drawingOptions, failFn), }); @@ -55,9 +53,11 @@ export const draw = (remoteData, options = {}, drawingOptions = [], failFn = () */ export const drawObject = (object, options = {}, drawingOptions = [], failFn = () => {}) => { const { qcObject, etag } = object; - const { root } = qcObject; + const { root, rootError } = qcObject; if (isObjectOfTypeChecker(root)) { return checkersPanel(root); + } else if (rootError) { + return failureToDrawPanel(rootError); } drawingOptions = Array.from(new Set(drawingOptions)); @@ -123,15 +123,11 @@ const drawOnCreate = async (dom, root, drawingOptions, failFn) => { const finalDrawingOptions = generateDrawingOptionString(root, drawingOptions); JSROOT.draw(dom, root, finalDrawingOptions).then((painter) => { if (painter === null) { - // eslint-disable-next-line no-console - console.error('null painter in JSROOT'); if (typeof failFn === 'function') { failFn(new Error('null painter in JSROOT')); } } }).catch((error) => { - // eslint-disable-next-line no-console - console.error(error); if (typeof failFn === 'function') { failFn(error); } @@ -235,8 +231,6 @@ const redraw = (dom, root, drawingOptions, failFn) => { try { JSROOT.redraw(dom, root, finalDrawingOptions); } catch (error) { - // eslint-disable-next-line no-console - console.error(error); if (typeof failFn === 'function') { failFn(error); } From f653a363d069c5d0d2046b57f72b6e25c1f70802 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 14:44:58 +0100 Subject: [PATCH 04/10] Add components to display in case of JSROOT failure to plot --- .../public/common/enums/root.enum.js | 3 +- .../common/object/failureToDrawPanel.js | 25 ++++++++++ .../updateWithPlotErrorOnQcRemoteData.js | 46 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 QualityControl/public/common/object/failureToDrawPanel.js create mode 100644 QualityControl/public/common/object/updateWithPlotErrorOnQcRemoteData.js diff --git a/QualityControl/public/common/enums/root.enum.js b/QualityControl/public/common/enums/root.enum.js index 050b73dd7..1d481d342 100644 --- a/QualityControl/public/common/enums/root.enum.js +++ b/QualityControl/public/common/enums/root.enum.js @@ -12,4 +12,5 @@ * or submit itself to any jurisdiction. */ -export const ROOT_ERROR_LABEL = 'ROOT_ERROR'; +export const JS_ROOT_ERROR_LABEL = 'ROOT_ERROR'; +export const JS_ROOT_FAILED_TO_PLOT_MESSAGE = 'JSROOT failed to plot the object'; diff --git a/QualityControl/public/common/object/failureToDrawPanel.js b/QualityControl/public/common/object/failureToDrawPanel.js new file mode 100644 index 000000000..16beab0e5 --- /dev/null +++ b/QualityControl/public/common/object/failureToDrawPanel.js @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h, iconWarning } from '/js/src/index.js'; + +/** + * Panel to show when an object failed to be drawn + * @param {string} error - error message to show + * @returns {vnode} - virtual node element + */ +export const failureToDrawPanel = (error) =>h('.error-box.danger.flex-column.justify-center.f6.text-center', {}, [ + h('span.error-icon', { title: 'Error' }, iconWarning()), + h('span', error), +]); diff --git a/QualityControl/public/common/object/updateWithPlotErrorOnQcRemoteData.js b/QualityControl/public/common/object/updateWithPlotErrorOnQcRemoteData.js new file mode 100644 index 000000000..c0b7e56c6 --- /dev/null +++ b/QualityControl/public/common/object/updateWithPlotErrorOnQcRemoteData.js @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @type {QcObjectRemoteData} + * should contain: + * { + * ...objectProperties as per ObjectDTO: '' // built specifically for the page + * root: JSON version of the root object to plot + * rootError: '' // error message if root object could not be retrieved + * timestampList: '', + * } + */ + +import { RemoteData } from '/js/src/index.js'; +import { JS_ROOT_ERROR_LABEL, JS_ROOT_FAILED_TO_PLOT_MESSAGE } from '../enums/root.enum.js'; + +/** + * Update the RemoteData object to include an error message on the qcObject + * @param {QcObjectRemoteData} qcObjectRemoteData - the RemoteData object containing the qcObject + * @param {string} error - the failure message to display + * @returns {QcObjectRemoteData} - updated RemoteData object with error message + */ +export const updateWithPlotErrorOnQcRemoteData = (qcObjectRemoteData, error) => { + if (qcObjectRemoteData.isSuccess()) { + const updatedQcObject = { + ...qcObjectRemoteData.payload.qcObject, + rootError: `${JS_ROOT_ERROR_LABEL}: ${error || JS_ROOT_FAILED_TO_PLOT_MESSAGE}`, + }; + qcObjectRemoteData = RemoteData.success({ ...qcObjectRemoteData.payload, qcObject: updatedQcObject }); + } else { + qcObjectRemoteData = RemoteData.failure('Cannot update error message on a non-successful RemoteData object'); + } + return qcObjectRemoteData; +}; From efad39b83071216ee7251199796966e0ea523e7b Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 14:45:09 +0100 Subject: [PATCH 05/10] Use dedicated components in case JSROOT fails to plot --- QualityControl/public/layout/view/page.js | 2 +- QualityControl/public/object/QCObject.js | 5 ++--- QualityControl/public/object/objectTreePage.js | 3 +-- .../public/pages/objectView/ObjectViewModel.js | 4 +++- .../public/pages/objectView/ObjectViewPage.js | 18 ++++++++++-------- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/QualityControl/public/layout/view/page.js b/QualityControl/public/layout/view/page.js index 62bc8abe6..54da1c7f7 100644 --- a/QualityControl/public/layout/view/page.js +++ b/QualityControl/public/layout/view/page.js @@ -223,7 +223,7 @@ const drawComponent = (model, tabObject) => { display: 'flex', 'flex-direction': 'column', }, - }, draw( + }, objectFromQcdbAsRemoteData && draw( objectFromQcdbAsRemoteData, {}, toUseDrawingOptions, diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index 61f3947af..b8ff093d0 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -18,7 +18,7 @@ import { prettyFormatDate, setBrowserTabTitle } from './../common/utils.js'; import { isObjectOfTypeChecker } from './../library/qcObject/utils.js'; import { BaseViewModel } from '../common/abstracts/BaseViewModel.js'; import { StorageKeysEnum } from '../common/enums/storageKeys.enum.js'; -import { ROOT_ERROR_LABEL } from '../common/enums/root.enum.js'; +import { updateWithPlotErrorOnQcRemoteData } from '../common/object/updateWithPlotErrorOnQcRemoteData.js'; /** * Model namespace for all about QC's objects (not javascript objects) @@ -350,8 +350,7 @@ export default class QCObject extends BaseViewModel { * @returns {undefined} */ invalidObject(name, details) { - const errorMessage = `${ROOT_ERROR_LABEL}: ${details || 'JSROOT was unable to draw this object'}`; - this.objects[name] = RemoteData.failure(errorMessage); + this.objects[name] = updateWithPlotErrorOnQcRemoteData(this.objects[name], details); this.notify(); } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 28ada6405..36744e246 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -117,8 +117,7 @@ const drawPlot = (model, object) => { return h('.h-100.flex-column', [ actionButtonsRow(model, name, qcObject, validFrom, id), h('', { style: 'height:77%;' }, draw(model.object.objects[name], { }, ['stat'], (error) => { - const invalidObjectDetails = { name, message: error.message, validFrom, id, versions }; - model.object.invalidObject(name, invalidObjectDetails); + model.object.invalidObject(name, error?.message); })), h('.scroll-y', {}, [ h('.w-100.flex-row.justify-center', h('.w-80', timestampSelectForm({ diff --git a/QualityControl/public/pages/objectView/ObjectViewModel.js b/QualityControl/public/pages/objectView/ObjectViewModel.js index 0063f7fc8..d91b812e1 100644 --- a/QualityControl/public/pages/objectView/ObjectViewModel.js +++ b/QualityControl/public/pages/objectView/ObjectViewModel.js @@ -16,6 +16,7 @@ import { BaseViewModel } from '../../common/abstracts/BaseViewModel.js'; import { setBrowserTabTitle } from '../../common/utils.js'; import { RemoteData, BrowserStorage } from '/js/src/index.js'; import { StorageKeysEnum } from '../../common/enums/storageKeys.enum.js'; +import { updateWithPlotErrorOnQcRemoteData } from '../../common/object/updateWithPlotErrorOnQcRemoteData.js'; /** * Model namespace for ObjectViewPage @@ -36,6 +37,7 @@ export default class ObjectViewModel extends BaseViewModel { * { * ...objectProperties as per ObjectDTO: '' // built specifically for the page * root: JSON version of the root object to plot + * rootError: '' // error message if root object could not be retrieved * timestampList: '', * } */ @@ -217,7 +219,7 @@ export default class ObjectViewModel extends BaseViewModel { * @param {string} message - the failure message to display */ drawingFailureOccurred(message) { - this.selected = RemoteData.failure(message || 'Failed to draw JSROOT plot'); + this.selected = updateWithPlotErrorOnQcRemoteData(this.selected, message); this.notify(); } } diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index a7576a438..5174afb9a 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -41,20 +41,24 @@ const objectPlotAndInfo = (objectViewModel) => NotAsked: () => null, Loading: () => spinner(10, 'Loading object...'), Failure: (error) => errorDiv(error), - Success: (qcObject) => { + Success: (qcObjectData) => { const { id, + name, + qcObject, validFrom, ignoreDefaults = false, drawOptions = [], displayHints = [], layoutDisplayOptions = [], versions, - } = qcObject; + } = qcObjectData; const drawingOptions = ignoreDefaults ? layoutDisplayOptions : [...drawOptions, ...displayHints, ...layoutDisplayOptions]; const isObjectInfoVisible = objectViewModel.objectInfoVisible; + const onFailureToDraw = objectViewModel.drawingFailureOccurred.bind(objectViewModel); + return h('.w-100.h-100.flex-column.scroll-off#ObjectPlot', [ h('.flex-row.justify-center.items-center.h-10', [ h( @@ -66,9 +70,9 @@ const objectPlotAndInfo = (objectViewModel) => ), ), h('.item-action-row.flex-row.g1.p2', [ - downloadRootImageButton(`${qcObject.name}.png`, qcObject.qcObject.root, drawingOptions), + downloadRootImageButton(`${name}.png`, qcObject.root, drawingOptions), downloadButton({ - href: objectViewModel.getDownloadQcdbObjectUrl(qcObject.id), + href: objectViewModel.getDownloadQcdbObjectUrl(id), title: 'Download root object', }), visibilityToggleButton( @@ -84,14 +88,12 @@ const objectPlotAndInfo = (objectViewModel) => h('.flex-grow', { // Key change forces redraw when toggling info panel key: isObjectInfoVisible ? 'objectPlotWithoutInfoPanel' : 'objectPlotWithInfoPanel', - }, drawObject(qcObject, {}, drawingOptions, (error) => { - objectViewModel.drawingFailureOccurred(error.message); - })), + }, drawObject(qcObjectData, {}, drawingOptions, onFailureToDraw)), isObjectInfoVisible && h('.scroll-y.w-30', { key: 'objectInfoPanel', }, [ h('h3.text-center', 'Object information'), - qcObjectInfoPanel(qcObject, { gap: '.5em' }, defaultRowAttributes(model.notification)), + qcObjectInfoPanel(qcObjectData, { gap: '.5em' }, defaultRowAttributes(model.notification)), ]), ]), ]); From 67a458b415fe0031e8955e42c1e87b8eed55cb00 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 14:50:21 +0100 Subject: [PATCH 06/10] Revert changes from early commit --- .../public/object/objectTreePage.js | 99 +++++++------------ 1 file changed, 36 insertions(+), 63 deletions(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 36744e246..5f0d651b7 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -1,5 +1,5 @@ /** -* @license + * @license * Copyright 2019-2020 CERN and copyright holders of ALICE O2. * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. * All rights not expressly granted are reserved. @@ -99,8 +99,8 @@ function objectPanel(model) { Loading: () => h('.h-100.w-100.flex-column.items-center.justify-center.f5', [spinner(3), h('', 'Loading Object')]), Success: (data) => drawPlot(model, data), - Failure: (invalidObjectDetails) => drawFailure(model, invalidObjectDetails), - // draw failure is already applied in drawPlot -> draw + Failure: (error) => + h('.h-100.w-100.flex-column.items-center.justify-center.f5', [h('.f1', iconCircleX()), error]), }); } return null; @@ -113,15 +113,42 @@ function objectPanel(model) { * @returns {vnode} - virtual node element */ const drawPlot = (model, object) => { - const { name, qcObject, validFrom, id, versions } = object; - return h('.h-100.flex-column', [ - actionButtonsRow(model, name, qcObject, validFrom, id), + const { name, qcObject, validFrom, id } = object; + const { root } = qcObject; + const href = validFrom ? + `?page=objectView&objectName=${name}&ts=${validFrom}&id=${id}` + : `?page=objectView&objectName=${name}`; + return h('', { style: 'height:100%; display: flex; flex-direction: column' }, [ + h('.item-action-row.flex-row.g1.p1', [ + downloadRootImageButton(`${name}.png`, root, ['stat']), + downloadButton({ + href: model.objectViewModel.getDownloadQcdbObjectUrl(id), + title: 'Download root object', + }), + h( + 'a.btn#fullscreen-button', + { + title: 'Open object plot in full screen', + href, + onclick: (e) => model.router.handleLinkEvent(e), + }, + iconResizeBoth(), + ), + h( + 'a.btn#close-button', + { + title: 'Close the object plot', + onclick: () => model.object.select(), + }, + iconCircleX(), + ), + ]), h('', { style: 'height:77%;' }, draw(model.object.objects[name], { }, ['stat'], (error) => { - model.object.invalidObject(name, error?.message); + model.object.invalidObject(name, error.message); })), h('.scroll-y', {}, [ - h('.w-100.flex-row.justify-center', h('.w-80', timestampSelectForm({ - versions: versions ?? [], + h('.w-100.flex-row', { style: 'justify-content: center' }, h('.w-80', timestampSelectForm({ + versions: object.versions ?? [], selectedId: id ?? null, onSelect: (version) => model.object.loadObjectByName(name, version.validFrom, version.id), }))), @@ -130,60 +157,6 @@ const drawPlot = (model, object) => { ]); }; -/** - * Draw the failure message when object cannot be drawn - * @param {Model} model - root model of the application - * @param {object} invalidObjectDetails - details about the invalid object - * @returns {vnode} - virtual node element - */ -const drawFailure = (model, invalidObjectDetails) => { - const { name, message, validFrom, id } = invalidObjectDetails ?? {}; - const versions = undefined; - const isRootError = message?.includes('ROOT_ERROR'); - return h('.h-100.flex-column', [ - actionButtonsRow(model, name, null, validFrom, id), - h( - '.h-100.flex-column.items-center.justify-center.text-center.f5', - [h('.f1', iconCircleX()), message], - ), - isRootError && h('.w-100.flex-row.justify-center.pv2', h('.w-80', timestampSelectForm({ - versions: versions ?? [], - selectedId: id ?? null, - onSelect: (version) => model.object.loadObjectByName(name, version.validFrom, version.id), - }))), - ]); -}; - -const actionButtonsRow = (model, objectName, root, validFrom, id) => { - const href = validFrom - ? `?page=objectView&objectName=${objectName}&ts=${validFrom}&id=${id}` - : `?page=objectView&objectName=${objectName}`; - return h('.item-action-row.flex-row.g1.p1', [ - root && downloadRootImageButton(`${objectName}.png`, root, ['stat']), - downloadButton({ - href: model.objectViewModel.getDownloadQcdbObjectUrl(id), - title: 'Download root object', - }), - h( - 'a.btn#fullscreen-button', - { - title: 'Open object plot in full screen', - href, - onclick: (e) => model.router.handleLinkEvent(e), - }, - iconResizeBoth(), - ), - h( - 'a.btn#close-button', - { - title: 'Close the object plot', - onclick: () => model.object.select(), - }, - iconCircleX(), - ), - ]); -}; - /** * Shows status of current tree with its options (online, loaded, how many) * @param {Model} model - root model of the application From a8c6ec1ebd97e996f32cea6cc1ec1c1a69b6f7bf Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 14:56:08 +0100 Subject: [PATCH 07/10] Fix issue from merge conflicts --- QualityControl/public/pages/objectView/ObjectViewPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index 4cfa8d6b1..45f079575 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -70,7 +70,7 @@ const objectPlotAndInfo = (objectViewModel) => ), ), h('.item-action-row.flex-row.g1.p2', [ - downloadRootImageDropdown(qcObject.name, qcObject.qcObject.root, drawingOptions), + downloadRootImageDropdown(name, qcObject.root, drawingOptions), downloadButton({ href: objectViewModel.getDownloadQcdbObjectUrl(id), title: 'Download root object', From 8b4c1e5fb03db41860156180121a24938dc1d3da Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 15:04:29 +0100 Subject: [PATCH 08/10] Revert more initial changes --- .../public/common/timestampSelectForm.js | 56 +++++++++---------- .../public/object/objectTreePage.js | 8 +-- .../public/pages/objectView/ObjectViewPage.js | 15 ++--- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/QualityControl/public/common/timestampSelectForm.js b/QualityControl/public/common/timestampSelectForm.js index 8838886cf..a2ab18c1b 100644 --- a/QualityControl/public/common/timestampSelectForm.js +++ b/QualityControl/public/common/timestampSelectForm.js @@ -21,41 +21,41 @@ import { prettyFormatDate } from './utils.js'; * @param {Array<{id: string, createdAt: string}>} config.versions - list of versions to display * @param {string|null} config.selectedId - currently selected version id * @param {onselect} config.onSelect - callback when a version is selected + * @param config.object * @returns {vnode} - virtual node element */ -export const timestampSelectForm = ({ versions = [], selectedId = null, onSelect }) => - h( +export default ({ object: objectModel }) => { + const { objects, selected } = objectModel; + const isObjectLoaded = selected && objects?.[selected.name]?.isSuccess(); + return h( '.w-100.flex-row', + isObjectLoaded && h('select.form-control.gray-darker.text-center', { onchange: (e) => { const { value } = e.target; - if (value && value !== 'Invalid Timestamp') { - onSelect?.(JSON.parse(value)); + if (selected && value !== 'Invalid Timestamp') { + const valueJson = JSON.parse(value); + objectModel.loadObjectByName(selected.name, valueJson.validFrom, valueJson.id); } }, - }, versions.map((version) => versionOptionNode(version, selectedId === version.id))), + }, [ + objectModel.getObjectVersions(selected.name) + .map((version) => { + const versionString = JSON.stringify(version); + const object = objects[selected.name].payload; + return h('option.text-center', { + id: versionString, + key: versionString, + value: versionString, + selected: version.createdAt === object.createdAt ? true : false, + }, [ + 'Created: ', + prettyFormatDate(version.createdAt), + ' (id: ', + version.id, + ')', + ]); + }), + ]), ); - -/** - * Create an option HTML element for a version - * @param {object} version - version object - * @param {string} version.id - version id - * @param {string} version.createdAt - version creation timestamp - * @param {boolean} isSelected - whether the version is selected - * @returns {vnode} - virtual node element - */ -const versionOptionNode = (version, isSelected = false) => { - const versionString = JSON.stringify(version); - return h('option.text-center', { - id: versionString, - key: versionString, - value: versionString, - selected: isSelected, - }, [ - 'Created: ', - prettyFormatDate(version.createdAt), - ' (id: ', - version.id, - ')', - ]); }; diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 6fc4b9c9b..443e2bcbe 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -23,7 +23,7 @@ import { } from '/js/src/index.js'; import { spinner } from '../common/spinner.js'; import { draw } from '../common/object/draw.js'; -import { timestampSelectForm } from './../common/timestampSelectForm.js'; +import timestampSelectForm from './../common/timestampSelectForm.js'; import virtualTable from './virtualTable.js'; import { defaultRowAttributes, qcObjectInfoPanel } from '../common/object/objectInfoCard.js'; import { downloadButton } from '../common/downloadButton.js'; @@ -147,11 +147,7 @@ const drawPlot = (model, object) => { model.object.invalidObject(name, error.message); })), h('.scroll-y', {}, [ - h('.w-100.flex-row', { style: 'justify-content: center' }, h('.w-80', timestampSelectForm({ - versions: object.versions ?? [], - selectedId: id ?? null, - onSelect: (version) => model.object.loadObjectByName(name, version.validFrom, version.id), - }))), + h('.w-100.flex-row', { style: 'justify-content: center' }, h('.w-80', timestampSelectForm(model))), qcObjectInfoPanel(object, { 'font-size': '.875rem;' }, defaultRowAttributes(model.notification)), ]), ]); diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index 45f079575..7bc68ad0a 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -41,23 +41,22 @@ const objectPlotAndInfo = (objectViewModel) => NotAsked: () => null, Loading: () => spinner(10, 'Loading object...'), Failure: (error) => errorDiv(error), - Success: (qcObjectData) => { + Success: (qcObject) => { const { id, name, - qcObject, + qcObject: { root = {} } = {}, validFrom, ignoreDefaults = false, drawOptions = [], displayHints = [], layoutDisplayOptions = [], versions, - } = qcObjectData; + } = qcObject; const drawingOptions = ignoreDefaults ? layoutDisplayOptions : [...drawOptions, ...displayHints, ...layoutDisplayOptions]; const isObjectInfoVisible = objectViewModel.objectInfoVisible; - const onFailureToDraw = objectViewModel.drawingFailureOccurred.bind(objectViewModel); return h('.w-100.h-100.flex-column.scroll-off#ObjectPlot', [ h('.flex-row.justify-center.items-center.h-10', [ @@ -70,7 +69,7 @@ const objectPlotAndInfo = (objectViewModel) => ), ), h('.item-action-row.flex-row.g1.p2', [ - downloadRootImageDropdown(name, qcObject.root, drawingOptions), + downloadRootImageDropdown(name, root, drawingOptions), downloadButton({ href: objectViewModel.getDownloadQcdbObjectUrl(id), title: 'Download root object', @@ -88,12 +87,14 @@ const objectPlotAndInfo = (objectViewModel) => h('.flex-grow', { // Key change forces redraw when toggling info panel key: isObjectInfoVisible ? 'objectPlotWithoutInfoPanel' : 'objectPlotWithInfoPanel', - }, drawObject(qcObjectData, {}, drawingOptions, onFailureToDraw)), + }, drawObject(qcObject, {}, drawingOptions, (error) => { + objectViewModel.drawingFailureOccurred(error.message); + })), isObjectInfoVisible && h('.scroll-y.w-30', { key: 'objectInfoPanel', }, [ h('h3.text-center', 'Object information'), - qcObjectInfoPanel(qcObjectData, { gap: '.5em' }, defaultRowAttributes(model.notification)), + qcObjectInfoPanel(qcObject, { gap: '.5em' }, defaultRowAttributes(model.notification)), ]), ]), ]); From 23a20f1f8da71828f96159dbab032ab94a052075 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 15:19:14 +0100 Subject: [PATCH 09/10] Revert more changes --- .../public/common/timestampSelectForm.js | 60 +++++++++---------- .../public/object/objectTreePage.js | 2 +- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/QualityControl/public/common/timestampSelectForm.js b/QualityControl/public/common/timestampSelectForm.js index a2ab18c1b..4046b6887 100644 --- a/QualityControl/public/common/timestampSelectForm.js +++ b/QualityControl/public/common/timestampSelectForm.js @@ -17,11 +17,7 @@ import { prettyFormatDate } from './utils.js'; /** * Display a select form with the latest timestamps of the current selected object - * @param {object} config - root model of the application - * @param {Array<{id: string, createdAt: string}>} config.versions - list of versions to display - * @param {string|null} config.selectedId - currently selected version id - * @param {onselect} config.onSelect - callback when a version is selected - * @param config.object + * @param {Model} model - root model of the application * @returns {vnode} - virtual node element */ export default ({ object: objectModel }) => { @@ -30,32 +26,32 @@ export default ({ object: objectModel }) => { return h( '.w-100.flex-row', isObjectLoaded && - h('select.form-control.gray-darker.text-center', { - onchange: (e) => { - const { value } = e.target; - if (selected && value !== 'Invalid Timestamp') { - const valueJson = JSON.parse(value); - objectModel.loadObjectByName(selected.name, valueJson.validFrom, valueJson.id); - } - }, - }, [ - objectModel.getObjectVersions(selected.name) - .map((version) => { - const versionString = JSON.stringify(version); - const object = objects[selected.name].payload; - return h('option.text-center', { - id: versionString, - key: versionString, - value: versionString, - selected: version.createdAt === object.createdAt ? true : false, - }, [ - 'Created: ', - prettyFormatDate(version.createdAt), - ' (id: ', - version.id, - ')', - ]); - }), - ]), + h('select.form-control.gray-darker.text-center', { + onchange: (e) => { + const { value } = e.target; + if (selected && value !== 'Invalid Timestamp') { + const valueJson = JSON.parse(value); + objectModel.loadObjectByName(selected.name, valueJson.validFrom, valueJson.id); + } + }, + }, [ + objectModel.getObjectVersions(selected.name) + .map((version) => { + const versionString = JSON.stringify(version); + const object = objects[selected.name].payload; + return h('option.text-center', { + id: versionString, + key: versionString, + value: versionString, + selected: version.createdAt === object.createdAt ? true : false, + }, [ + 'Created: ', + prettyFormatDate(version.createdAt), + ' (id: ', + version.id, + ')', + ]); + }), + ]), ); }; diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 443e2bcbe..b5fb15ef8 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -93,7 +93,7 @@ export default (model) => { */ function objectPanel(model) { const selectedObjectName = model.object.selected.name; - if (model.object.objects?.[selectedObjectName]) { + if (model.object.objects && model.object.objects?.[selectedObjectName]) { return model.object.objects[selectedObjectName].match({ NotAsked: () => null, Loading: () => From 8c2b748384f382d652e0ec60ece7a66a0815a1ab Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 20 Jan 2026 15:42:43 +0100 Subject: [PATCH 10/10] Fix test for sidetree --- .../layout/view/panels/objectTreeSidebar.js | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/QualityControl/public/layout/view/panels/objectTreeSidebar.js b/QualityControl/public/layout/view/panels/objectTreeSidebar.js index 6ecbe619d..ad90cd187 100644 --- a/QualityControl/public/layout/view/panels/objectTreeSidebar.js +++ b/QualityControl/public/layout/view/panels/objectTreeSidebar.js @@ -31,11 +31,12 @@ export default (model) => Loading: () => h('.flex-column.items-center', [spinner(2), h('.f6', 'Loading Objects')]), Success: (objects) => { let objectsToDisplay = []; - const { searchInput = '' } = model.object; + const { searchInput = '', selectedObject, objects: objectsRemoteDataMap = {} } = model.object; if (searchInput.trim() !== '') { objectsToDisplay = objects.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); } + const objectRemoteData = objectsRemoteDataMap[selectedObject?.name]; return [ searchForm(model), h('.flex-column.flex-grow', {}, [ @@ -43,7 +44,7 @@ export default (model) => ? virtualTable(model, 'side', objectsToDisplay) : h('.scroll-y', treeTable(model)), ]), - objectPreview(model), + objectRemoteData && objectPreview(selectedObject?.name, objectRemoteData), ]; }, Failure: (error) => h('.f6.danger.flex-column.text-center', [ @@ -186,19 +187,17 @@ const leafRow = (model, sideTree, level) => { /** * Shows a JSROOT plot of selected object inside the tree of sidebar allowing the user to preview object and decide * if it should be added to layout - * @param {Model} model - root model of the application + * @param {string} name - name of the selected object + * @param {QcObjectRemoteData} objectRemoteData - RemoteData of the selected object * @returns {vnode} - virtual node element */ -const objectPreview = (model) => { - const isSelected = model.object.selected; - if (isSelected) { - return isSelected && h( +const objectPreview = (name, objectRemoteData = null) => + objectRemoteData + ? h( '.bg-white', { style: 'height: 20em' }, - draw(model.object.objects[model.object.selected.name], {}, [], (error) => { - model.object.invalidObject(model.object.selected.name, error.message); + draw(objectRemoteData, {}, [], (error) => { + model.object.invalidObject(name, error.message); }), - ); - } - return null; -}; + ) + : null;