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
7 changes: 6 additions & 1 deletion QualityControl/lib/QCModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ export const setupQcModel = async (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);

const qcdbDownloadService = new QcdbDownloadService(config.ccdb);
Expand All @@ -116,6 +119,8 @@ export const setupQcModel = async (eventEmitter) => {
const intervalsService = new IntervalsService();

const bookkeepingService = new BookkeepingService(config.bookkeeping);
statusService.bookkeepingService = bookkeepingService;

const filterService = new FilterService(bookkeepingService, config);
const runModeService = new RunModeService(config.bookkeeping, bookkeepingService, ccdbService, eventEmitter);
const objectController = new ObjectController(qcObjectService, runModeService, qcdbDownloadService);
Expand Down
1 change: 1 addition & 0 deletions QualityControl/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const setup = async (http, ws, eventEmitter) => {
statusController.getServiceStatusHandler.bind(statusController),
{ public: true },
);
http.get('/services', statusController.getServicesConfigurationHandler.bind(statusController));

http.get('/checkUser', userController.addUserHandler.bind(userController));

Expand Down
10 changes: 10 additions & 0 deletions QualityControl/lib/controllers/StatusController.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,14 @@ export class StatusController {
);
}
}

/**
* Send back the configuration of the connected services for the frontend
* @param {Request} _ - HTTP request object
* @param {Response} res - HTTP response object
* @returns {undefined}
*/
async getServicesConfigurationHandler(_, res) {
res.status(200).json(this._statusService.retrieveServicesConfiguration());
}
}
12 changes: 12 additions & 0 deletions QualityControl/lib/services/BookkeepingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ export class BookkeepingService {
}
}

/**
* Retrieve the configured URL for Bookkeeping
* @returns {string | false} - URL for Bookkeeping, if not configured returns `false`
*/
retrieveBookkeepingURL() {
if (!this.active) {
this._logger.warnMessage('Bookkeeping not configured');
return false;
}
return `${this._protocol}${this._hostname}${this._port}`;
}

/**
* Helper method to construct a URL path with the required authentication token.
* Appends the service's token as a query parameter to the provided path.
Expand Down
31 changes: 31 additions & 0 deletions QualityControl/lib/services/Status.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export class StatusService {
*/
this._dataService = undefined;

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

/**
* @type {WebSocket}
*/
Expand Down Expand Up @@ -120,6 +125,23 @@ export class StatusService {
return { name: 'CCDB', status, version, extras: {} };
}

/**
* Retrieve the configurations of the services for the front end.
* @returns {object} - object containing the configurations of the services for the front end.
*/
retrieveServicesConfiguration() {
const serviceConfig = {};

if (this._bookkeepingService?.active) {
serviceConfig.bookkeeping = {
BASE_URL: this._config.bookkeeping.url,
PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=',
};
}

return serviceConfig;
}

/*
* Getters & Setters
*/
Expand All @@ -133,6 +155,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
2 changes: 2 additions & 0 deletions QualityControl/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import LayoutListModel from './pages/layoutListView/model/LayoutListModel.js';
import { RequestFields } from './common/RequestFields.enum.js';
import FilterModel from './common/filters/model/FilterModel.js';
import StatusService from './services/Status.service.js';

/**
* Represents the application's state and actions as a class
Expand Down Expand Up @@ -97,6 +98,7 @@
this.services = {
object: new QCObjectService(this),
layout: new LayoutService(this),
status: new StatusService(this),
};

this.loader.get('/api/checkUser');
Expand Down Expand Up @@ -276,7 +278,7 @@

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

Check warning on line 281 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
17 changes: 16 additions & 1 deletion QualityControl/public/common/object/objectInfoCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

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

const SPECIFIC_KEY_LABELS = {
Expand Down Expand Up @@ -65,7 +66,21 @@ const infoRow = (key, value, infoRowAttributes) => {

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,
),
model.services.status.isConfigured('bookkeeping') && key === 'runNumber' && hasValue
? h('a.ph2.text-right.actionable-icon', {
id: 'openRunInBookkeeping',
title: 'Open run in Bookkeeping',
href: model.services.status.buildBookkeepingUrl(value),
target: '_blank',
}, iconExternalLink())
: '',
]),
]);
};

Expand Down
81 changes: 81 additions & 0 deletions QualityControl/public/services/Status.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @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 { RemoteData } from '/js/src/index.js';

/**
* @typedef {object} ServicePayload
* @property {object} [bookkeeping] - Configuration for the Bookkeeping service.
* @property {string} bookkeeping.BASE_URL - The root URL of the Bookkeeping application.
* @property {string} bookkeeping.PARTIAL_RUN_DETAILS - The URL path/query parameters for run details.
*/

export default class StatusService {
/**
* Initialize service
* @param {Model} model - root model of the application
*/
constructor(model) {
this.model = model;
this.loader = model.loader;

/**
* @type {RemoteData<ServicePayload>}
*/
this.serviceConfig = RemoteData.notAsked();

this.initStatusService();
}

/**
* Fetches service configurations from the backend and updates the internal state.
* Notifies the model once the request completes (success or failure).
* @returns {Promise<void>}
*/
async initStatusService() {
const { result, ok } = await this.loader.get('api/services');
if (ok) {
this.serviceConfig = RemoteData.success(result || {});
} else {
this.serviceConfig = RemoteData.failure('Error retrieving services');
}

this.model.notify();
}

/**
* Checks if a specific service configuration is successfully loaded and available.
* @param {string} service - The name of the service to check (e.g. 'bookkeeping').
* @returns {boolean} - True if the service key exists in a successful payload.
*/
isConfigured(service) {
return this.serviceConfig.match({
Success: (config) => Object.hasOwn(config, service),
Other: () => false,
});
}

/**
* Constructs a full URL for the bookkeeping run details page.
* @param {string|number} runNumber - The specific run identifier to append to the URL.
* @returns {string|undefined} The formatted URL, or `undefined` if the service is not configured.
*/
buildBookkeepingUrl(runNumber) {
if (!this.isConfigured('bookkeeping')) {
return;
}
const { BASE_URL, PARTIAL_RUN_DETAILS } = this.serviceConfig.payload.bookkeeping;
return `${BASE_URL}/${PARTIAL_RUN_DETAILS}${runNumber}`;
}
}
26 changes: 26 additions & 0 deletions QualityControl/test/lib/controllers/StatusController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ok } from 'node:assert';
import { suite, test } from 'node:test';

import { StatusController } from './../../../lib/controllers/StatusController.js';
import { config } from '../../config.js';

export const statusControllerTestSuite = async () => {
suite('`getSetServiceStatusHandler()` tests', () => {
Expand Down Expand Up @@ -91,4 +92,29 @@ export const statusControllerTestSuite = async () => {
ok(res.json.calledWith(result));
});
});

suite('`getServicesConfigurationHandler()` tests', () => {
test('should successfully respond with result JSON with the configured services', () => {
const mock = {
bookkeeping: {
BASE_URL: config.bookkeeping.url,
PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=',
},
};

const statusService = {
retrieveServicesConfiguration: stub().returns(mock),
};
const statusController = new StatusController(statusService);
const res = {
status: stub().returnsThis(),
json: stub(),
};
statusController.getServicesConfigurationHandler({}, res);

ok(statusService.retrieveServicesConfiguration.calledOnce, 'Service method should be called once');
ok(res.status.calledWith(200), 'Response status should be 200');
ok(res.json.calledWith(mock), 'Response JSON should match the service output');
});
});
};
21 changes: 21 additions & 0 deletions QualityControl/test/lib/services/StatusService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { deepStrictEqual } from 'node:assert';
import { suite, test, before } from 'node:test';

import { StatusService } from './../../../lib/services/Status.service.js';
import { config } from '../../config.js';

export const statusServiceTestSuite = async () => {
suite('`retrieveDataServiceStatus()` tests', () => {
Expand Down Expand Up @@ -100,4 +101,24 @@ export const statusServiceTestSuite = async () => {
});
});
});

suite('`retrieveServicesConfiguration()` tests', () => {
test('should return bookkeeping configuration if bookkeeping service is active', () => {
const serviceConfig = {
bookkeeping: { url: config.bookkeeping.url },
};
const statusService = new StatusService({ version: '0.1.1' }, serviceConfig);

statusService.bookkeepingService = { active: true };

const result = statusService.retrieveServicesConfiguration();

deepStrictEqual(result, {
bookkeeping: {
BASE_URL: config.bookkeeping.url,
PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=',
},
});
});
});
};
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 @@ -258,6 +258,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
23 changes: 21 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 { 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';
const SORTING_BUTTON_PATH = 'header > div > div > div:nth-child(3) > div > button';
Expand Down Expand Up @@ -189,7 +190,7 @@
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 @@ -208,7 +209,7 @@
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 @@ -221,6 +222,24 @@
}
);

await testParent.test(
'should have an external link to bookkeeping inline with the run number row',
{ timeout },
async () => {
const bookkeepingLink = await page.$('#openRunInBookkeeping');
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'));
}
)

Check notice

Code scanning / CodeQL

Semicolon insertion Note test

Avoid automated semicolon insertion (95% of all statements in
the enclosing function
have an explicit semicolon).

Copilot Autofix

AI 1 day ago

To fix this, we should explicitly terminate the await testParent.test(...) statement that ends around line 241 with a semicolon. This means adding a ; immediately after the closing ) line so that the call is clearly a complete statement before the next await testParent.test(...) call begins. No imports, methods, or additional definitions are needed—this is a simple syntactic/style correction.

Concretely, in QualityControl/test/public/pages/object-tree.test.js, locate the await testParent.test( block starting at line 225 ('should have an external link to bookkeeping inline with the run number row') and ending at line 241. Change the final line of that call from ) to );. This preserves all existing functionality while eliminating reliance on automatic semicolon insertion.

Suggested changeset 1
QualityControl/test/public/pages/object-tree.test.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js
--- a/QualityControl/test/public/pages/object-tree.test.js
+++ b/QualityControl/test/public/pages/object-tree.test.js
@@ -238,7 +238,7 @@
       strictEqual(baseUrl, `${config.bookkeeping.url}/`);
       strictEqual(runNumber, url.searchParams.get('runNumber'));
     }
-  )
+  );
 
   await testParent.test(
     'should close the object plot upon clicking the close button',
EOF
@@ -238,7 +238,7 @@
strictEqual(baseUrl, `${config.bookkeeping.url}/`);
strictEqual(runNumber, url.searchParams.get('runNumber'));
}
)
);

await testParent.test(
'should close the object plot upon clicking the close button',
Copilot is powered by AI and may make mistakes. Always verify output.

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