Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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 @@ -79,6 +79,16 @@ export const setupQcModel = async (ws, 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 @@ -90,22 +100,13 @@ export const setupQcModel = async (ws, 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
48 changes: 43 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 { 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 @@
*/
this._ws = undefined;

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

this._packageInfo = packageInfo;
this._config = config;
}
Expand All @@ -64,6 +70,9 @@
case IntegratedServices.CCDB:
result = await this.retrieveDataServiceStatus();
break;
case IntegratedServices.KAFKA:
result = this.retrieveKafkaServiceStatus();
break;
}
return result;
}
Expand All @@ -75,7 +84,7 @@
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 @@
* @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,36 @@
* @returns {Promise<{object}>} - status of the data service
*/
async retrieveDataServiceStatus() {
let status = { ok: true };
let status = { ok: true, category: ServiceStatus.LOADING };
Comment thread Fixed
let version = '';
try {
const { version: dataServiceVersion } = await this._dataService.getVersion();
status = { ok: true, category: ServiceStatus.SUCCESS };
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 +170,13 @@
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;
}
}
43 changes: 37 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,32 @@
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.ERROR;
this._extraInfo = {
// KafkaConsumer is currently not supporting "active" status checking [OGUI-1872]
message: 'Kafka is configured but the service has not started yet',
};
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 +90,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';
import NotificationRunStartModel from './common/notifications/model/NotificationRunStartModel.js';

/**
Expand Down Expand Up @@ -119,6 +120,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 @@ -280,7 +287,7 @@

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

Check warning on line 290 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 @@ -21,7 +21,7 @@
} 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 All @@ -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 @@ -92,15 +92,15 @@
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 @@ -110,7 +110,7 @@
'.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,43 @@
* 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),
},
() => {},
)(),
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