diff --git a/QualityControl/public/common/enums/root.enum.js b/QualityControl/public/common/enums/root.enum.js new file mode 100644 index 000000000..1d481d342 --- /dev/null +++ b/QualityControl/public/common/enums/root.enum.js @@ -0,0 +1,16 @@ +/** + * @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 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/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); } 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; +}; 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/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; diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index 659a22945..95507a4c7 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -18,6 +18,7 @@ import { simpleDebouncer, prettyFormatDate, setBrowserTabTitle } from './../comm import { isObjectOfTypeChecker } from './../library/qcObject/utils.js'; import { BaseViewModel } from '../common/abstracts/BaseViewModel.js'; import { StorageKeysEnum } from '../common/enums/storageKeys.enum.js'; +import { updateWithPlotErrorOnQcRemoteData } from '../common/object/updateWithPlotErrorOnQcRemoteData.js'; /** * Model namespace for all about QC's objects (not javascript objects) @@ -347,11 +348,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] = updateWithPlotErrorOnQcRemoteData(this.objects[name], details); this.notify(); } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index c3a608f5d..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 && model.object.objects[selectedObjectName]) { + if (model.object.objects && model.object.objects?.[selectedObjectName]) { return model.object.objects[selectedObjectName].match({ NotAsked: () => null, Loading: () => 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 d7e076c9f..7bc68ad0a 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -44,6 +44,8 @@ const objectPlotAndInfo = (objectViewModel) => Success: (qcObject) => { const { id, + name, + qcObject: { root = {} } = {}, validFrom, ignoreDefaults = false, drawOptions = [], @@ -55,6 +57,7 @@ const objectPlotAndInfo = (objectViewModel) => layoutDisplayOptions : [...drawOptions, ...displayHints, ...layoutDisplayOptions]; const isObjectInfoVisible = objectViewModel.objectInfoVisible; + return h('.w-100.h-100.flex-column.scroll-off#ObjectPlot', [ h('.flex-row.justify-center.items-center.h-10', [ h( @@ -66,9 +69,9 @@ const objectPlotAndInfo = (objectViewModel) => ), ), h('.item-action-row.flex-row.g1.p2', [ - downloadRootImageDropdown(qcObject.name, qcObject.qcObject.root, drawingOptions), + downloadRootImageDropdown(name, root, drawingOptions), downloadButton({ - href: objectViewModel.getDownloadQcdbObjectUrl(qcObject.id), + href: objectViewModel.getDownloadQcdbObjectUrl(id), title: 'Download root object', }), visibilityToggleButton(