Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e2aef9a
feat: add new services endpoint for querying services information
AlexJanson Jan 8, 2026
d389085
feat: add external link icon to the run number when bookkeeping is co…
AlexJanson Jan 8, 2026
ba7882d
fix: update partial run details to proper link structure
AlexJanson Jan 8, 2026
ab22f44
fix: bookkeeping service still being present in service config
AlexJanson Jan 8, 2026
b018bb6
feat: add external link button for opening runs in bookkeeping if boo…
AlexJanson Jan 8, 2026
814d500
style: fix linting errors
AlexJanson Jan 8, 2026
205d7ce
test: fix failing test due to markup changes
AlexJanson Jan 8, 2026
43755bc
fix: when run number is null show no link to bookkeeping
AlexJanson Jan 9, 2026
df406f1
test: fix timing issue introduced with changes
AlexJanson Jan 9, 2026
f96fda1
feat: add id to bookkeeping link for easier testing
AlexJanson Jan 9, 2026
1b4ee22
test: add tests for the backend and frontend
AlexJanson Jan 9, 2026
59e9050
Merge branch 'dev' into feature/QCG/OGUI-1852/open-bookkeeping-from-r…
AlexJanson Jan 9, 2026
beb4ede
style: add missing semicolon
AlexJanson Jan 13, 2026
8c44de7
feat: add open in bookkeeping in run status panel
AlexJanson Jan 15, 2026
82c9335
Merge remote-tracking branch 'origin/dev' into feature/QCG/OGUI-1852/…
AlexJanson Jan 15, 2026
82ac3f8
Merge branch 'dev' into feature/QCG/OGUI-1852/open-bookkeeping-from-r…
AlexJanson Jan 15, 2026
0a31118
Merge branch 'dev' of github.com:AliceO2Group/WebUi into feature/QCG/…
graduta Jan 18, 2026
a409cb7
Simplify config retrieval and remove unused methods and tests
graduta Jan 18, 2026
78764c8
Refactor front-end for reusability
graduta Jan 18, 2026
49b2702
Fix unused imports
graduta Jan 18, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export const IntegratedServices = Object.freeze({
QC: 'qc',
CCDB: 'ccdb',
KAFKA: 'kafka',
BOOKKEEPING: 'bookkeeping',
});
9 changes: 8 additions & 1 deletion QualityControl/lib/QCModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ export const setupQcModel = async (ws, eventEmitter) => {
const userController = new UserController(userRepository);
const layoutController = new LayoutController(layoutRepository);

const statusService = new StatusService({ version: packageJSON?.version ?? '-' }, { qc: config.qc ?? {} });
const statusService = new StatusService(
{ version: packageJSON?.version ?? '-' },
{
qc: config.qc ?? {},
bookkeeping: config.bookkeeping ?? {},
},
);
const statusController = new StatusController(statusService);

if (config?.kafka?.enabled) {
Expand Down Expand Up @@ -118,6 +124,7 @@ export const setupQcModel = async (ws, eventEmitter) => {
const intervalsService = new IntervalsService();

const bookkeepingService = new BookkeepingService(config.bookkeeping);
statusService.bookkeepingService = bookkeepingService;
try {
await bookkeepingService.connect();
} catch (error) {
Expand Down
25 changes: 23 additions & 2 deletions QualityControl/lib/services/BookkeepingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { httpGetJson } from '../utils/httpRequests.js';
import { LogManager } from '@aliceo2/web-ui';
import { wrapRunStatus } from '../dtos/BookkeepingDto.js';

const GET_BKP_DATABASE_STATUS_PATH = '/api/status/database';
export const GET_BKP_GUI_STATUS_PATH = '/api/status/gui';
const GET_RUN_TYPES_PATH = '/api/runTypes';
const GET_RUN_PATH = '/api/runs';
export const GET_DETECTORS_PATH = '/api/detectors';
Expand All @@ -36,6 +36,7 @@ export class BookkeepingService {
this.active = false;
this.error = null;

this._url = '';
this._hostname = '';
this._port = null;
this._token = '';
Expand All @@ -56,6 +57,7 @@ export class BookkeepingService {
const { url, token } = this.config || {};
try {
const normalizedURL = new URL(url);
this._url = normalizedURL.href;
this._hostname = normalizedURL.hostname;
this._protocol = normalizedURL.protocol;
this._port = normalizedURL.port || (normalizedURL.protocol === 'https:' ? 443 : 80);
Expand Down Expand Up @@ -95,13 +97,14 @@ export class BookkeepingService {
const { data } = await httpGetJson(
this._hostname,
this._port,
`${GET_BKP_DATABASE_STATUS_PATH}?token=${this._token}`,
`${GET_BKP_GUI_STATUS_PATH}?token=${this._token}`,
{
protocol: this._protocol,
rejectUnauthorized: false,
},
);
if (data && data?.status?.ok && data?.status?.configured) {
this._version = data.version || 'unknown';
this._logger.infoMessage('Successfully connected to Bookkeeping');
return true;
} else {
Expand Down Expand Up @@ -278,4 +281,22 @@ export class BookkeepingService {
_createRunPath(runNumber) {
return this._createPath(`${GET_RUN_PATH}/${runNumber}`);
}

/**
* Get the URL of the bookkeeping service
* @readonly
* @returns {string} the URL of the bookkeeping service
*/
get url() {
return this._url;
}

/**
* Get the version of the bookkeeping service
* @readonly
* @returns {string} the version of the bookkeeping service
*/
get version() {
return this._version;
}
}
48 changes: 48 additions & 0 deletions QualityControl/lib/services/Status.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export class StatusService {
*/
this._dataService = undefined;

/**
* @type {BookkeepingService}
*/
this._bookkeepingService = undefined;

/**
* @type {WebSocket}
*/
Expand Down Expand Up @@ -73,6 +78,9 @@ export class StatusService {
case IntegratedServices.KAFKA:
result = this.retrieveKafkaServiceStatus();
break;
case IntegratedServices.BOOKKEEPING:
result = this.retrieveBookkeepingServiceStatus();
break;
}
return result;
}
Expand Down Expand Up @@ -153,6 +161,37 @@ export class StatusService {
};
}

/**
* Retrieve the bookkeeping service status response and its public configuration
* @returns {object} - status of the bookkeeping service
*/
retrieveBookkeepingServiceStatus() {
if (this._bookkeepingService?.active) {
return {
name: IntegratedServices.BOOKKEEPING,
version: this._bookkeepingService.version,
status: { ok: true, category: ServiceStatus.SUCCESS },
extras: {
BASE_URL: this._bookkeepingService.url,
PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=',
},
};
} else if (this._bookkeepingService.config) {
return {
name: IntegratedServices.BOOKKEEPING,
status: {
ok: false,
category: ServiceStatus.ERROR,
message: this._bookkeepingService.error || 'Unable to connect to Bookkeeping service',
},
};
}
return {
name: IntegratedServices.BOOKKEEPING,
status: { ok: false, category: ServiceStatus.NOT_CONFIGURED },
};
}

/*
* Getters & Setters
*/
Expand All @@ -166,6 +205,15 @@ export class StatusService {
this._dataService = dataService;
}

/**
* Set service to be used for querying status of the Bookkeeping service.
* @param {BookkeepingService} bookkeepingService - service used for retrieving Bookkeeping status
* @returns {void}
*/
set bookkeepingService(bookkeepingService) {
this._bookkeepingService = bookkeepingService;
}

/**
* Set instance of websocket server
* @param {WebSocket} ws - instance of the WS server
Expand Down
1 change: 1 addition & 0 deletions QualityControl/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
await this.filterModel.filterService.initFilterService();
await this.filterModel.setFilterFromURL();
this.filterModel.setFilterToURL();
await this.aboutViewModel.retrieveIndividualServiceStatus(IntegratedServices.BOOKKEEPING);

this.services.layout.getLayoutsByUserId(this.session.personid, RequestFields.LAYOUT_CARD);

Expand Down Expand Up @@ -287,7 +288,7 @@

/**
* Clear URL parameters and redirect to a certain page
* @param {*} pageName - name of the page to be redirected to

Check warning on line 291 in QualityControl/public/Model.js

View workflow job for this annotation

GitHub Actions / Check eslint rules on ubuntu-latest

Prefer a more specific type to `*`
* @returns {undefined}
*/
clearURL(pageName) {
Expand Down
2 changes: 1 addition & 1 deletion QualityControl/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
font-weight: 500;
}

&>div:hover {
& > div > div:hover {
font-weight: 700;
}
}
Expand Down
2 changes: 1 addition & 1 deletion QualityControl/public/common/filters/filterViews.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* Creates an input element for a specific metadata field;
* @param {object} config - The configuration for this particular field
* @param {object} filterMap - An object that contains the keys and values of the filters
* @param {Function} onInputCallback - A callback function that triggers upon Input

Check warning on line 37 in QualityControl/public/common/filters/filterViews.js

View workflow job for this annotation

GitHub Actions / Check eslint rules on ubuntu-latest

Prefer a more specific type to `Function`
* @param {Function} onEnterCallback - A callback function that triggers upon Enter
* @param {Function} onChangeCallback - A callback function that triggers upon Change
* @param onFocusCallback
Expand Down Expand Up @@ -118,7 +118,7 @@
isRunModeActivated && runStatusPanel(runStatus),
]),
lastUpdatePanel(runStatus, lastRefresh, refreshRate),
cleanRunInformationPanel(cleanRunInformation),
cleanRunInformationPanel(cleanRunInformation, filterModel.filterMap['RunNumber']),
detectorsQualitiesPanel(detectorsQualities),
],
);
Expand Down
21 changes: 17 additions & 4 deletions QualityControl/public/common/filters/runMode/runStatusPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
*/

import { RunStatus } from '../../../../../library/runStatus.enum.js';
import { h } from '/js/src/index.js';
import { camelToTitleCase } from '../../utils.js';
import { h, iconExternalLink } from '/js/src/index.js';
import { camelToTitleCase, getBkpRunDetailsUrl } from '../../utils.js';
import { statusBadge } from '../../badge.js';

/**
Expand Down Expand Up @@ -59,23 +59,36 @@ export const lastUpdatePanel = (runStatus, lastRefresh, refreshRate = 15000) =>
/**
* Renders the run information panel
* @param {object} cleanRunInformation - The `RunInformation` without `detectorsQualities`
* @param {string} runNumber - The current selected filter run number
* @returns {vnode} - virtual node element
*/
export const cleanRunInformationPanel = (cleanRunInformation) =>
export const cleanRunInformationPanel = (cleanRunInformation, runNumber) =>
cleanRunInformation && Object.keys(cleanRunInformation).length > 0 && h(
'.flex-row.g4.items-center.f7.gray-darker.text-center.ph4',
{
id: 'header-run-information',
style: 'overflow-x: auto; margin: 0 auto;',
},
Object.entries(cleanRunInformation).map(([key, value]) =>
[
h('.flex-row.g1', {
style: 'flex: 0 0 auto;',
}, [
h('span', 'Open run in Bookkeeping'),
h('a', {
id: 'openRunInBookkeeping',
title: 'Open run in Bookkeeping',
href: getBkpRunDetailsUrl(runNumber),
target: '_blank',
}, iconExternalLink()),
]),
Object.entries(cleanRunInformation).map(([key, value]) => h('.flex-row.g1', {
key: `${key}-${value}`,
style: 'flex: 0 0 auto;',
}, [
h('strong', `${camelToTitleCase(key)}:`),
h('span', `${value}`),
])),
],
);

/**
Expand Down
19 changes: 18 additions & 1 deletion QualityControl/public/common/object/objectInfoCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
*/

import { h, isContextSecure } from '/js/src/index.js';
import { iconExternalLink } from '/js/src/icons.js';
import { camelToTitleCase, copyToClipboard, prettyFormatDate } from './../utils.js';
import { getBkpRunDetailsUrl } from '../../common/utils.js';

const SPECIFIC_KEY_LABELS = {
id: 'ID (etag)',
Expand Down Expand Up @@ -62,10 +64,25 @@ const infoRow = (key, value, infoRowAttributes) => {
const formattedKey = getUILabel(key);

const hasValue = value != null && value !== '' && (!Array.isArray(value) || value.length !== 0);
const bkpRunDetailsUrl = key === 'runNumber' ? getBkpRunDetailsUrl(value) : null;

return h(`.flex-row.g2.info-row${highlightedClasses}`, [
h('b.w-25.w-wrapped', formattedKey),
h('.w-75.cursor-pointer', hasValue && infoRowAttributes(formattedKey, formattedValue), formattedValue),
h('.flex-row.w-75', [
h(
'.cursor-pointer.flex-row',
hasValue && infoRowAttributes(formattedKey, formattedValue),
formattedValue,
),
bkpRunDetailsUrl && hasValue
? h('a.ph2.text-right.actionable-icon', {
id: 'openRunInBookkeeping',
title: 'Open run in Bookkeeping',
href: bkpRunDetailsUrl,
target: '_blank',
}, iconExternalLink())
: '',
]),
]);
};

Expand Down
14 changes: 13 additions & 1 deletion QualityControl/public/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { isUserRoleSufficient } from '../../../../library/userRole.enum.js';
import { generateDrawingOptionString } from '../../library/qcObject/utils.js';

/* global JSROOT */
/* global JSROOT BOOKKEEPING */

/**
* Map of allowed `ROOT.makeImage` file extensions to MIME types
Expand Down Expand Up @@ -254,3 +254,15 @@ export const isOnLeftSideOfViewport = (element) => {
const isLeft = rect.left - rect.width < window.innerWidth / 2;
return isLeft;
};

/**
* Retrieves the URL to the run details page in Bookkeeping for the given run number
* @param {number|string} runNumber - The run number to generate the URL for
* @returns {string|null} The URL to the run details page, or null if Bookkeeping is not configured
*/
export const getBkpRunDetailsUrl = (runNumber) => {
if (typeof BOOKKEEPING !== 'undefined' && BOOKKEEPING && BOOKKEEPING.RUN_DETAILS) {
return BOOKKEEPING.RUN_DETAILS + runNumber;
}
return null;
};
5 changes: 5 additions & 0 deletions QualityControl/public/pages/aboutView/AboutViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export default class AboutViewModel extends BaseViewModel {
} else {
const { status: { category } } = result;
this.services[category][service] = RemoteData.success(result);
if (result.status.ok && service === IntegratedServices.BOOKKEEPING) {
window.BOOKKEEPING = {
RUN_DETAILS: result.extras.BASE_URL + result.extras.PARTIAL_RUN_DETAILS,
};
}
}
this.notify();
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions QualityControl/test/public/features/filterTest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => {
await page.locator('tr:last-of-type td').click();
await page.waitForSelector(versionsPath);

await delay(100);
let versionCount = await page.evaluate((path) => document.querySelectorAll(path).length, versionsPath);
strictEqual(versionCount, 1, 'Number of versions is not 1');

Expand Down
24 changes: 22 additions & 2 deletions QualityControl/test/public/pages/object-tree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { strictEqual, ok, deepStrictEqual, notDeepStrictEqual } from 'node:asser
import { delay } from '../../testUtils/delay.js';
import { getLocalStorage, getLocalStorageAsJson } from '../../testUtils/localStorage.js';
import { StorageKeysEnum } from '../../../public/common/enums/storageKeys.enum.js';
import { config } from '../../config.js';

const OBJECT_TREE_PAGE_PARAM = '?page=objectTree';

Expand Down Expand Up @@ -188,7 +189,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
const context = page.browserContext();
await context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']);

await page.click('#qcObjectInfoPanel > div > div');
await page.click('#qcObjectInfoPanel > div > div > div');

const clipboard = await page.evaluate(async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
Expand All @@ -207,7 +208,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
const context = page.browserContext();
await context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']);

await page.click('#qcObjectInfoPanel > div > div'); // copy path
await page.click('#qcObjectInfoPanel > div > div > div'); // copy path
await page.click('#qcObjectInfoPanel > div:nth-child(7) > div'); // try to copy empty value

const clipboard = await page.evaluate(async () => {
Expand All @@ -220,6 +221,25 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
},
);

await testParent.test(
'should have an external link to bookkeeping inline with the run number row',
{ timeout },
async () => {
const bookkeepingLink = await page.$('#openRunInBookkeeping');
await delay(2000);
ok(bookkeepingLink, 'The link to bookkeeping should be present in the DOM');

const href = await page.evaluate((element) => element.href, bookkeepingLink);
const runNumber =
await page.evaluate((element) => element.parentElement.children[0].textContent, bookkeepingLink);
const url = new URL(href);
const baseUrl = `${url.origin}${url.pathname}`;

strictEqual(baseUrl, `${config.bookkeeping.url}/`);
strictEqual(runNumber, url.searchParams.get('runNumber'));
},
);

await testParent.test(
'should close the object plot upon clicking the close button',
{ timeout },
Expand Down
Loading
Loading