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
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export const IntegratedServices = Object.freeze({
QCG: 'qcg',
QC: 'qc',
CCDB: 'ccdb',
KAFKA: 'kafka',
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
export const ServiceStatus = Object.freeze({
NOT_ASKED: 'NOT_ASKED',
LOADING: 'LOADING',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR',
SUCCESS: 'SUCCESS',
NOT_CONFIGURED: 'NOT_CONFIGURED',
});
21 changes: 11 additions & 10 deletions QualityControl/lib/QCModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ export const setupQcModel = async (eventEmitter) => {
logger.warnMessage('No database configuration found, skipping database initialization');
}

const layoutRepository = new LayoutRepository(jsonFileService);
const userRepository = new UserRepository(jsonFileService);
const chartRepository = new ChartRepository(jsonFileService);

const userController = new UserController(userRepository);
const layoutController = new LayoutController(layoutRepository);

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

if (config?.kafka?.enabled) {
try {
const validConfig = await KafkaConfigDto.validateAsync(config.kafka);
Expand All @@ -89,22 +99,13 @@ export const setupQcModel = async (eventEmitter) => {
logLevel: logLevel.NOTHING,
});
const aliEcsSynchronizer = new AliEcsSynchronizer(kafkaClient, consumerGroups, eventEmitter);
statusService.aliEcsSynchronizer = aliEcsSynchronizer;
aliEcsSynchronizer.start();
} catch (error) {
logger.errorMessage(`Kafka initialization/connection failed: ${error.message}`);
}
}

const layoutRepository = new LayoutRepository(jsonFileService);
const userRepository = new UserRepository(jsonFileService);
const chartRepository = new ChartRepository(jsonFileService);

const userController = new UserController(userRepository);
const layoutController = new LayoutController(layoutRepository);

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

const qcdbDownloadService = new QcdbDownloadService(config.ccdb);

const ccdbService = CcdbService.setup(config.ccdb);
Expand Down
47 changes: 42 additions & 5 deletions QualityControl/lib/services/Status.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { exec } from 'node:child_process';

import { LogManager } from '@aliceo2/web-ui';
import { IntegratedServices } from './../../common/library/enums/Status/integratedServices.enum.js';
import { ServiceStatus } from '../../common/library/enums/Status/serviceStatus.enum.js';

const QC_VERSION_EXEC_COMMAND = 'yum info o2-QualityControl | awk \'/Version/ {print $3}\'';
const execPromise = promisify(exec);
Expand All @@ -43,6 +44,11 @@ export class StatusService {
*/
this._ws = undefined;

/**
* @type {?AliEcsSynchronizer}
*/
this._aliEcsSynchronizer = undefined;

this._packageInfo = packageInfo;
this._config = config;
}
Expand All @@ -64,6 +70,9 @@ export class StatusService {
case IntegratedServices.CCDB:
result = await this.retrieveDataServiceStatus();
break;
case IntegratedServices.KAFKA:
result = this.retrieveKafkaServiceStatus();
break;
}
return result;
}
Expand All @@ -75,7 +84,7 @@ export class StatusService {
retrieveOwnStatus() {
return {
name: 'QCG',
status: { ok: true },
status: { ok: true, category: ServiceStatus.SUCCESS },
version: this._packageInfo?.version ?? '',
extras: {
clients: this._ws?.server?.clients?.size ?? -1,
Expand All @@ -88,15 +97,16 @@ export class StatusService {
* @returns {string} - version of QC deployed on the system
*/
async retrieveQcVersion() {
let status = { ok: true };
let status = { ok: false, category: ServiceStatus.NOT_CONFIGURED };
let version = 'Not part of an FLP deployment';

if (this._config.qc?.enabled) {
try {
const { stdout } = await execPromise(QC_VERSION_EXEC_COMMAND, { timeout: 6000 });
version = stdout.trim();
status = { ok: true, category: ServiceStatus.SUCCESS };
} catch (error) {
status = { ok: false, message: error.message || error };
status = { ok: false, category: ServiceStatus.ERROR, message: error.message || error };
this._logger.errorMessage(error, { level: 99, system: 'GUI', facility: 'qcg/status-service' });
}
}
Expand All @@ -109,17 +119,35 @@ export class StatusService {
* @returns {Promise<{object}>} - status of the data service
*/
async retrieveDataServiceStatus() {
let status = { ok: true };
let status = { ok: true, category: ServiceStatus.SUCCESS };
let version = '';
try {
const { version: dataServiceVersion } = await this._dataService.getVersion();
version = dataServiceVersion;
} catch (err) {
status = { ok: false, message: err.message || err };
status = { ok: false, category: ServiceStatus.ERROR, message: err.message || err };
}
return { name: 'CCDB', status, version, extras: {} };
}

/**
* Retrieve the kafka service status response
* @returns {object} - status of the kafka service
*/
retrieveKafkaServiceStatus() {
const status = this._aliEcsSynchronizer?.status;
return {
name: IntegratedServices.KAFKA,
status: {
ok: status === ServiceStatus.SUCCESS,
category: status ?? ServiceStatus.NOT_CONFIGURED,
},
extras: {
...this._aliEcsSynchronizer?.extraInfo ?? {},
},
};
}

/*
* Getters & Setters
*/
Expand All @@ -141,4 +169,13 @@ export class StatusService {
set ws(ws) {
this._ws = ws;
}

/**
* Set instance of `AliEcsSynchronizer`
* @param {AliEcsSynchronizer} aliEcsSynchronizer - instance of the `AliEcsSynchronizer`
* @returns {void}
*/
set aliEcsSynchronizer(aliEcsSynchronizer) {
this._aliEcsSynchronizer = aliEcsSynchronizer;
}
}
39 changes: 33 additions & 6 deletions QualityControl/lib/services/external/AliEcsSynchronizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import { AliEcsEventMessagesConsumer, LogManager } from '@aliceo2/web-ui';
import { EmitterKeys } from './../../../common/library/enums/emitterKeys.enum.js';
import { ServiceStatus } from '../../../common/library/enums/Status/serviceStatus.enum.js';

const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/ecs-synchronizer`;
const RUN_TOPICS = ['aliecs.run'];
Expand All @@ -26,7 +27,7 @@
* @param {import('kafkajs').Kafka} kafkaClient - configured kafka client
* @param {KafkaConfiguration.consumerGroups} consumerGroups - consumer groups to be used for various topics
* @param {EventEmitter} eventEmitter - event emitter to be used to emit events when new data is available
* @param {class} ConsumerClass - class to be used for creating the consumer, defaults to AliEcsEventMessagesConsumer

Check warning on line 30 in QualityControl/lib/services/external/AliEcsSynchronizer.js

View workflow job for this annotation

GitHub Actions / Check eslint rules on ubuntu-latest

Syntax error in type: class
*/
constructor(kafkaClient, consumerGroups, eventEmitter, ConsumerClass = AliEcsEventMessagesConsumer) {
this._logger = LogManager.getLogger(LOG_FACILITY);
Expand All @@ -38,18 +39,28 @@
RUN_TOPICS,
);
this._ecsRunConsumer.onMessageReceived(this._onRunMessage.bind(this));

this._status = ServiceStatus.NOT_ASKED;
this._extraInfo = {};
}

/**
* Start the synchronization process and listen to events from various topics via their consumers
* @returns {void}
* @returns {Promise<void>}
*/
start() {
async start() {
this._logger.infoMessage('Starting to consume AliECS messages for topics:');
this._ecsRunConsumer
.start()
.catch((error) =>
this._logger.errorMessage(`Error when starting ECS run consumer: ${error.message}\n${error.stack}`));
this._status = ServiceStatus.LOADING;
try {
await this._ecsRunConsumer.start();
this._status = ServiceStatus.SUCCESS;
} catch (error) {
this._logger.errorMessage(`Error when starting ECS run consumer: ${error.message}\n${error.stack}`);
this._status = ServiceStatus.ERROR;
this._extraInfo = {
message: error.message,
};
}
}

/**
Expand All @@ -75,4 +86,20 @@
});
}
}

/**
* Returns the current kafka service status
* @returns {ServiceStatus} - The kafka service status
*/
get status() {
return this._status;
}

/**
* Returns extra information about the current kafka service
* @returns {object} - The extra information of the kafka service
*/
get extraInfo() {
return this._extraInfo;
}
}
7 changes: 7 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 { IntegratedServices } from '../library/enums/Status/integratedServices.enum.js';

/**
* Represents the application's state and actions as a class
Expand Down Expand Up @@ -115,6 +116,12 @@
height: 10,
};

// For active run monitoring, the kafka service must be available.
// If we do not yet know the kafka service status, we should request it from the backend
if (!this.aboutViewModel.findService(IntegratedServices.KAFKA)) {
this.aboutViewModel.retrieveIndividualServiceStatus(IntegratedServices.KAFKA);
}

/*
* Init first page
*/
Expand Down Expand Up @@ -276,7 +283,7 @@

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

Check warning on line 286 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
10 changes: 5 additions & 5 deletions QualityControl/public/common/filters/filterViews.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { filterInput, dynamicSelector, ongoingRunsSelector } from './filter.js';
import { FilterType } from './filterTypes.js';
import { filtersConfig, runModeFilterConfig } from './filtersConfig.js';
import { runModeCheckbox } from './runMode/runModeCheckbox.js';
import { runModeComponent } from './runMode/runModeCheckbox.js';
import {
cleanRunInformationPanel,
detectorsQualitiesPanel,
Expand Down Expand Up @@ -82,15 +82,15 @@ export function filtersPanel(filterModel, viewModel) {
ONGOING_RUN_INTERVAL_MS: refreshRate,
runInformation,
} = filterModel;
if (!isVisible) {
return null;
}
const { fetchOngoingRuns } = filterService;
const onInputCallback = setFilterValue.bind(filterModel);
const onChangeCallback = setFilterValue.bind(filterModel);
const onFocusCallback = fetchOngoingRuns.bind(filterService);
const onEnterCallback = () => filterModel.triggerFilter(viewModel);
const clearFilterCallback = clearFiltersAndTrigger.bind(filterModel, viewModel);
if (!isVisible) {
return null;
}
const filtersList = isRunModeActivated
? runModeFilterConfig(filterService)
: filtersConfig(filterService);
Expand All @@ -100,7 +100,7 @@ export function filtersPanel(filterModel, viewModel) {
'.w-100.flex-column.p2.g2.justify-center#filterElement',
[
h('.flex-row.g2.justify-center.items-center', [
runModeCheckbox(filterModel, viewModel),
runModeComponent(filterModel, viewModel),
!isRunModeActivated &&
[triggerFiltersButton(onEnterCallback, filterModel), clearFiltersButton(clearFilterCallback)],
...filtersList.map((filter) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,45 @@
* or submit itself to any jurisdiction.
*/

import { h } from '/js/src/index.js';
import { h, iconWarning, switchCase } from '/js/src/index.js';
import { IntegratedServices } from '../../../../library/enums/Status/integratedServices.enum.js';
import { ServiceStatus } from '../../../../library/enums/Status/serviceStatus.enum.js';
import { spinner } from '../../spinner.js';

/**
* This component determines whether the Run Mode toggle should be displayed
* based on the availability and configuration state of the Kafka integrated service.
* Behavior by service state:
* - Loading: Displays a spinner while checking whether Run Mode is configured.
* - Failure: Displays an error box with a warning icon and the failure message returned by the service.
* - Success:
* - {@link ServiceStatus.SUCCESS}: Renders the Run Mode checkbox component.
* - {@link ServiceStatus.NOT_CONFIGURED}: Renders nothing (Run Mode is intentionally unavailable).
* - Any other state: Displays a generic error box instructing the user to contact an administrator.
* - Other: Unsupported or irrelevant state.
* @param {object} filterModel - The filter model containing the aboutViewModel used to locate integrated services.
* @param {object} viewModel - The view model associated with the current view.
* @returns {vnode|null} A vnode representing the RunMode switch or kafka state, or `null` if Kafka is not configured.
*/
export const runModeComponent = (filterModel, viewModel) =>
filterModel.model.aboutViewModel.findService(IntegratedServices.KAFKA)?.match({
Loading: () => spinner(2, 'Checking if RunMode is configured'),
Failure: (payload) => h('.error-box.danger.flex-column.justify-center.f6.text-center', { id: 'run-mode-failure' }, [
h('span.error-icon', { title: 'RunMode is unavailable. Please contact administrator.' }, iconWarning()),
h('span', payload.status.message),
]),
Success: (payload) => switchCase(payload.status.category, {
[ServiceStatus.SUCCESS]: () => runModeCheckbox(filterModel, viewModel),
NOT_CONFIGURED: () => null,
}, () => h('.error-box.danger.flex-column.justify-center.f6.text-center', { id: 'run-mode-failure' }, [
h('span.error-icon', {
title: 'RunMode is unavailable. Please contact administrator.',
}, iconWarning()),
h('span', 'Contact an administrator and include this information:'),
h('span', `Kafka service returned status '${payload.status.category ?? '?'}'`),
]))(),
Other: () => {},
});

/**
* Render a run mode switch
Expand Down
21 changes: 18 additions & 3 deletions QualityControl/public/pages/aboutView/AboutViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,31 @@ export default class AboutViewModel extends BaseViewModel {
if (!ok) {
this.services[ServiceStatus.ERROR][service] = RemoteData.failure({
name: service,
status: { ok: false, message: result.message },
status: { ok: false, category: ServiceStatus.ERROR, message: result.message },
});
} else {
const { status: { ok } } = result;
const category = ok ? ServiceStatus.SUCCESS : ServiceStatus.ERROR;
const { status: { category } } = result;
this.services[category][service] = RemoteData.success(result);
}
this.notify();
} catch (error) {
this.model.notification.show(`Error fetching data for ${service}: ${error.message}`, 'danger', 2000);
}
}

/**
* Iterates through all known {@link ServiceStatus} values and returns the
* first matching service found. This assumes that a given service can exist
* in at most one {@link ServiceStatus} at a time.
* @param {string} service - The service identifier to look up
* @returns {RemoteData|undefined} - The service instance under any `ServiceStatus`, or `undefined` if not found.
*/
findService(service) {
for (const status of Object.values(ServiceStatus)) {
if (this.services[status][service]) {
return this.services[status][service];
}
}
return undefined;
}
}
Loading
Loading