Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions QualityControl/public/common/downloadRootImageButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @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, imagE } from '/js/src/index.js';
import { downloadRoot, getFileExtensionFromName } from './utils.js';
import { isObjectOfTypeChecker } from '../../library/qcObject/utils.js';

/**
* Download root image button.
* @param {string} filename - The name of the downloaded file including its extension.
* @param {RootObject} root - The JSROOT RootObject to render.
* @param {string[]} [drawingOptions=[]] - Optional array of JSROOT drawing options.
* @returns {vnode} - Download root image button element.
*/
export function downloadRootImageButton(filename, root, drawingOptions = []) {
const filetype = getFileExtensionFromName(filename);
return !isObjectOfTypeChecker(root) && h(`button.btn.download-root-image-${filetype}-button`, {
title: `Download as ${filetype.toUpperCase()}`,
onclick: async (event) => {
try {
event.target.disabled = true;
await downloadRoot(filename, root, drawingOptions);
} finally {
event.target.disabled = false;
}
},
}, imagE());
}
72 changes: 72 additions & 0 deletions QualityControl/public/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
*/

import { isUserRoleSufficient } from '../../../../library/userRole.enum.js';
import { generateDrawingOptionString } from '../../library/qcObject/utils.js';

/* global JSROOT */

/**
* Map of allowed `ROOT.makeImage` file extensions to MIME types
* @type {Map<string, string>}
*/
const SUPPORTED_ROOT_IMAGE_FILE_TYPES = new Map([
['svg', 'image/svg+xml'],
['png', 'file/png'],
['jpg', 'file/jpeg'],
['jpeg', 'file/jpeg'],
['webp', 'file/webp'],
]);

/**
* Generates a new ObjectId
Expand Down Expand Up @@ -171,3 +186,60 @@ export const camelToTitleCase = (text) => {
const titleCase = spaced.charAt(0).toUpperCase() + spaced.slice(1);
return titleCase;
};

/**
* Get the file extension from a filename
* @param {string} filename - The file name including the file extension
* @returns {string} - the file extension
*/
export const getFileExtensionFromName = (filename) =>
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase().trim();

/**
* Helper to trigger a download for a file
* @param {string} url - The URL to the file source
* @param {string} filename - The name of the file including the file extension
*/
export const triggerDownload = (url, filename) => {
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
};

/**
* Downloads a file
* @param {Blob|MediaSource} file - The file to download
* @param {string} filename - The name of the file including the file extension
*/
export const downloadFile = (file, filename) => {
const url = URL.createObjectURL(file);
try {
triggerDownload(url, filename);
} finally {
URL.revokeObjectURL(url);
}
};

/**
* Generates a rasterized image of a JSROOT RootObject and triggers download.
* @param {string} filename - The name of the downloaded file including its extension.
* @param {RootObject} root - The JSROOT RootObject to render.
* @param {string[]} [drawingOptions=[]] - Optional array of JSROOT drawing options.
*/
export const downloadRoot = async (filename, root, drawingOptions = []) => {
const filetype = getFileExtensionFromName(filename);
const mime = SUPPORTED_ROOT_IMAGE_FILE_TYPES.get(filetype);
if (!mime) {
throw new Error(`The file extension (${filetype}) is not supported`);
}

const image = await JSROOT.makeImage({
object: root,
option: generateDrawingOptionString(root, drawingOptions),
format: filetype,
as_buffer: true,
});
const blob = new Blob([image], { type: mime });
downloadFile(blob, filename);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
import { downloadButton } from '../../../common/downloadButton.js';
import { defaultRowAttributes, qcObjectInfoPanel } from './../../../common/object/objectInfoCard.js';
import { h, iconResizeBoth, info } from '/js/src/index.js';
import { downloadRootImageButton } from '../../../common/downloadRootImageButton.js';

/**
* Builds 2 actionable buttons which are to be placed on top of a JSROOT plot
* Buttons shall appear on hover of the plot
* @param {Model} model - root model of the application
* @param {object} tabObject - tab dto representation
* @param {TabObject} tabObject - tab dto representation
* @returns {vnode} - virtual node element
*/
export const objectInfoResizePanel = (model, tabObject) => {
const { name } = tabObject;
const { name, options: drawingOptions = [], ignoreDefaults } = tabObject;
const { filterModel, router, object, services } = model;
const isSelectedOpen = object.selectedOpen;
const objectRemoteData = services.object.objectsLoadedMap[name];
Expand All @@ -34,6 +35,10 @@ export const objectInfoResizePanel = (model, tabObject) => {
.forEach(([key, value]) => {
uri += `&${key}=${encodeURI(value)}`;
});
const { displayHints = [], drawOptions = [] } = objectRemoteData?.payload ?? {};
const toUseDrawingOptions = Array.from(new Set(ignoreDefaults
? drawingOptions
: [...drawingOptions, ...displayHints, ...drawOptions]));
return h('.text-right.resize-element.item-action-row.flex-row.g1', {
style: 'display: none; padding: .25rem .25rem 0rem .25rem;',
}, [
Expand All @@ -51,12 +56,18 @@ export const objectInfoResizePanel = (model, tabObject) => {
h('.p1', qcObjectInfoPanel(objectRemoteData.payload, {}, defaultRowAttributes(model.notification))),
),
]),
objectRemoteData.isSuccess() &&
downloadButton({
href: model.objectViewModel.getDownloadQcdbObjectUrl(objectRemoteData.payload.id),
title: 'Download object',
id: `download-button-${objectRemoteData.payload.id}`,
}),
objectRemoteData.isSuccess() && [
downloadRootImageButton(
`${objectRemoteData.payload.name}.png`,
objectRemoteData.payload.qcObject.root,
toUseDrawingOptions,
),
downloadButton({
href: model.objectViewModel.getDownloadQcdbObjectUrl(objectRemoteData.payload.id),
title: 'Download root object',
id: `download-button-${objectRemoteData.payload.id}`,
}),
],
h('a.btn', {
title: 'Open object plot in full screen',
href: uri,
Expand Down
9 changes: 6 additions & 3 deletions QualityControl/public/object/objectTreePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import virtualTable from './virtualTable.js';
import { defaultRowAttributes, qcObjectInfoPanel } from '../common/object/objectInfoCard.js';
import { downloadButton } from '../common/downloadButton.js';
import { resizableDivider } from '../common/resizableDivider.js';
import { downloadRootImageButton } from '../common/downloadRootImageButton.js';

/**
* Shows a page to explore though a tree of objects with a preview on the right if clicked
Expand Down Expand Up @@ -93,15 +94,17 @@ function objectPanel(model) {
* @returns {vnode} - virtual node element
*/
const drawPlot = (model, object) => {
const { name, validFrom, id } = 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(object.id),
title: 'Download object',
href: model.objectViewModel.getDownloadQcdbObjectUrl(id),
title: 'Download root object',
}),
h(
'a.btn#fullscreen-button',
Expand Down
4 changes: 3 additions & 1 deletion QualityControl/public/pages/objectView/ObjectViewPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { dateSelector } from '../../common/object/dateSelector.js';
import { defaultRowAttributes, qcObjectInfoPanel } from '../../common/object/objectInfoCard.js';
import { downloadButton } from '../../common/downloadButton.js';
import { visibilityToggleButton } from '../../common/visibilityButton.js';
import { downloadRootImageButton } from '../../common/downloadRootImageButton.js';

/**
* Shows a page to view an object on the whole page
Expand Down Expand Up @@ -65,9 +66,10 @@ const objectPlotAndInfo = (objectViewModel) =>
),
),
h('.item-action-row.flex-row.g1.p2', [
downloadRootImageButton(`${qcObject.name}.png`, qcObject.qcObject.root, drawingOptions),
downloadButton({
href: objectViewModel.getDownloadQcdbObjectUrl(qcObject.id),
title: 'Download object',
title: 'Download root object',
}),
visibilityToggleButton(
{
Expand Down
8 changes: 8 additions & 0 deletions QualityControl/test/public/pages/layout-list.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent)
strictEqual(downloadCount, 0);
});

await testParent.test('should not show a download root as image button when there is no data', async () => {
await page.goto(`${url}?page=layoutShow&layoutId=671b8c22402408122e2f20dd&tab=main`, { waitUntil: 'networkidle0' });

const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null);

strictEqual(exists, false);
});

await testParent.test('should successfully load layoutList page "/"', { timeout }, async () => {
await page.goto(`${url}${LAYOUT_LIST_PAGE_PARAM}`, { waitUntil: 'networkidle0' });
const location = await page.evaluate(() => window.location);
Expand Down
10 changes: 10 additions & 0 deletions QualityControl/test/public/pages/layout-show.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) =>
},
);

await testParent.test(
'should have a correctly made download root as image button',
{ timeout },
async () => {
const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null);

ok(exists, 'Expected ROOT image download button to exist');
},
);

await testParent.test('should remove query param only if option is invalid for any filter', { timeout }, async () => {
const baseParams = `?page=layoutShow&layoutId=${LAYOUT_ID}&tab=main`;

Expand Down
12 changes: 11 additions & 1 deletion QualityControl/test/public/pages/object-tree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
});

await testParent.test(
'should have a correctly made download button',
'should have a correctly made download root as object button',
{ timeout },
async () => {
const objectId = '016fa8ac-f3b6-11ec-b9a9-c0a80209250c';
Expand All @@ -81,6 +81,16 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
},
);

await testParent.test(
'should have a correctly made download root as image button',
{ timeout },
async () => {
const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null);

ok(exists, 'Expected ROOT image download button to exist');
},
);

await testParent.test(
'should have default panel width of 50% when width is null in localStorage',
{ timeout },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* or submit itself to any jurisdiction.
*/

import { strictEqual, deepStrictEqual, match } from 'node:assert';
import {strictEqual, deepStrictEqual, match, ok} from 'node:assert';
import { delay } from '../../testUtils/delay.js';
import { StorageKeysEnum } from '../../../public/common/enums/storageKeys.enum.js';
import {
Expand Down Expand Up @@ -102,6 +102,16 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t
},
);

await testParent.test(
'should have a correctly made download root as image button',
{ timeout },
async () => {
const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null);

ok(exists, 'Expected ROOT image download button to exist');
},
);

await testParent.test(
'should take back the user to page=layoutShow when clicking "Back to layout"',
{ timeout },
Expand Down
Loading