From 5b4119131819e0949ca7a8e745d17d476adfdbef Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:58:45 +0100 Subject: [PATCH 1/2] If kafka is not configured, RunMode should not be displayed --- .../enums/Status/integratedServices.enum.js | 1 + QualityControl/lib/QCModel.js | 21 +- QualityControl/lib/services/Status.service.js | 35 ++++ .../services/external/AliEcsSynchronizer.js | 27 ++- QualityControl/public/Model.js | 7 + .../public/common/filters/filterViews.js | 43 ++++- .../public/pages/aboutView/AboutViewModel.js | 16 ++ .../test/lib/services/StatusService.test.js | 65 +++++++ .../test/public/features/runMode.test.js | 180 ++++++++++++++++-- .../test/public/pages/about-page.test.js | 6 +- 10 files changed, 361 insertions(+), 40 deletions(-) diff --git a/QualityControl/common/library/enums/Status/integratedServices.enum.js b/QualityControl/common/library/enums/Status/integratedServices.enum.js index 64acc6354..b5c55b93f 100644 --- a/QualityControl/common/library/enums/Status/integratedServices.enum.js +++ b/QualityControl/common/library/enums/Status/integratedServices.enum.js @@ -21,4 +21,5 @@ export const IntegratedServices = Object.freeze({ QCG: 'qcg', QC: 'qc', CCDB: 'ccdb', + KAFKA: 'kafka', }); diff --git a/QualityControl/lib/QCModel.js b/QualityControl/lib/QCModel.js index aa5364af6..b774a06e9 100644 --- a/QualityControl/lib/QCModel.js +++ b/QualityControl/lib/QCModel.js @@ -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); @@ -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); diff --git a/QualityControl/lib/services/Status.service.js b/QualityControl/lib/services/Status.service.js index c6d6e33a9..dd31b5d7a 100644 --- a/QualityControl/lib/services/Status.service.js +++ b/QualityControl/lib/services/Status.service.js @@ -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); @@ -43,6 +44,11 @@ export class StatusService { */ this._ws = undefined; + /** + * @type {?AliEcsSynchronizer} + */ + this._aliEcsSynchronizer = undefined; + this._packageInfo = packageInfo; this._config = config; } @@ -64,6 +70,9 @@ export class StatusService { case IntegratedServices.CCDB: result = await this.retrieveDataServiceStatus(); break; + case IntegratedServices.KAFKA: + result = this.retrieveKafkaServiceStatus(); + break; } return result; } @@ -120,6 +129,23 @@ export class StatusService { 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, + }, + extras: { + state: status ?? 'NOT_CONFIGURED', + }, + }; + } + /* * Getters & Setters */ @@ -141,4 +167,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; + } } diff --git a/QualityControl/lib/services/external/AliEcsSynchronizer.js b/QualityControl/lib/services/external/AliEcsSynchronizer.js index bb2b0d902..8b3124e4a 100644 --- a/QualityControl/lib/services/external/AliEcsSynchronizer.js +++ b/QualityControl/lib/services/external/AliEcsSynchronizer.js @@ -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']; @@ -38,18 +39,24 @@ export class AliEcsSynchronizer { RUN_TOPICS, ); this._ecsRunConsumer.onMessageReceived(this._onRunMessage.bind(this)); + + this._status = ServiceStatus.NOT_ASKED; } /** * Start the synchronization process and listen to events from various topics via their consumers - * @returns {void} + * @returns {Promise} */ - 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; + } } /** @@ -75,4 +82,12 @@ export class AliEcsSynchronizer { }); } } + + /** + * Returns the current kafka service status + * @returns {ServiceStatus} - The kafka service status + */ + get status() { + return this._status; + } } diff --git a/QualityControl/public/Model.js b/QualityControl/public/Model.js index 4c8a238f8..30f401502 100644 --- a/QualityControl/public/Model.js +++ b/QualityControl/public/Model.js @@ -29,6 +29,7 @@ import AboutViewModel from './pages/aboutView/AboutViewModel.js'; 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 @@ -115,6 +116,12 @@ export default class Model extends Observable { 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 */ diff --git a/QualityControl/public/common/filters/filterViews.js b/QualityControl/public/common/filters/filterViews.js index 0e6fc1362..dfba10db6 100644 --- a/QualityControl/public/common/filters/filterViews.js +++ b/QualityControl/public/common/filters/filterViews.js @@ -15,9 +15,12 @@ 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 { lastUpdatePanel, runStatusPanel } from './runMode/runStatusPanel.js'; -import { h, iconChevronBottom, iconChevronTop } from '/js/src/index.js'; +import { h, iconChevronBottom, iconChevronTop, iconWarning } from '/js/src/index.js'; +import { IntegratedServices } from '../../../library/enums/Status/integratedServices.enum.js'; +import { spinner } from '../spinner.js'; +import { ServiceStatus } from '../../../library/enums/Status/serviceStatus.enum.js'; +import { runModeCheckbox } from './runMode/runModeCheckbox.js'; /** * Creates an input element for a specific metadata field; @@ -76,15 +79,16 @@ export function filtersPanel(filterModel, viewModel) { lastRefresh, ONGOING_RUN_INTERVAL_MS: refreshRate, } = 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 kafkaService = filterModel.model.aboutViewModel.findService(IntegratedServices.KAFKA); const filtersList = isRunModeActivated ? runModeFilterConfig(filterService) : filtersConfig(filterService); @@ -93,7 +97,34 @@ 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), + kafkaService?.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) => { + switch (payload.extras.state) { + case ServiceStatus.SUCCESS: + return runModeCheckbox(filterModel, viewModel); + case 'NOT_CONFIGURED': + return null; + default: + return 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 code '${payload.extras.state ?? '?'}'`), + ]); + } + }, + Other: () => {}, + }), !isRunModeActivated && [triggerFiltersButton(onEnterCallback, filterModel), clearFiltersButton(clearFilterCallback)], ...filtersList.map((filter) => diff --git a/QualityControl/public/pages/aboutView/AboutViewModel.js b/QualityControl/public/pages/aboutView/AboutViewModel.js index db24bbacf..0a34119b8 100644 --- a/QualityControl/public/pages/aboutView/AboutViewModel.js +++ b/QualityControl/public/pages/aboutView/AboutViewModel.js @@ -74,4 +74,20 @@ export default class AboutViewModel extends BaseViewModel { 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; + } } diff --git a/QualityControl/test/lib/services/StatusService.test.js b/QualityControl/test/lib/services/StatusService.test.js index 0bec75015..d976b6fb3 100644 --- a/QualityControl/test/lib/services/StatusService.test.js +++ b/QualityControl/test/lib/services/StatusService.test.js @@ -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 { ServiceStatus } from '../../../common/library/enums/Status/serviceStatus.enum.js'; export const statusServiceTestSuite = async () => { suite('`retrieveDataServiceStatus()` tests', () => { @@ -47,17 +48,20 @@ export const statusServiceTestSuite = async () => { test('should successfully build an object with framework information from all used sources', async () => { const statusService = new StatusService(); statusService.dataService = { getVersion: stub().resolves({ version: '0.0.1-beta' }) }; + statusService.aliEcsSynchronizer = { status: ServiceStatus.SUCCESS }; const statusInfo = await Promise.all([ statusService.retrieveServiceStatus('qcg'), statusService.retrieveServiceStatus('qc'), statusService.retrieveServiceStatus('ccdb'), + statusService.retrieveServiceStatus('kafka'), ]); const expectedResults = [ { name: 'QCG', version: '', status: { ok: true }, extras: { clients: -1 } }, { name: 'QC', status: { ok: true }, version: 'Not part of an FLP deployment', extras: {} }, { name: 'CCDB', status: { ok: true }, version: '0.0.1-beta', extras: {} }, + { name: 'kafka', status: { ok: true }, extras: { state: ServiceStatus.SUCCESS } }, ]; deepStrictEqual(statusInfo, expectedResults); @@ -100,4 +104,65 @@ export const statusServiceTestSuite = async () => { }); }); }); + + suite('`retrieveKafkaServiceStatus()` tests', () => { + test('marks Kafka service as healthy when synchronizer reports SUCCESS', async () => { + const statusService = new StatusService(); + statusService.aliEcsSynchronizer = { status: ServiceStatus.SUCCESS }; + const result = statusService.retrieveKafkaServiceStatus(); + + deepStrictEqual(result, { + name: 'kafka', + status: { ok: true }, + extras: { state: ServiceStatus.SUCCESS }, + }); + }); + + test('marks Kafka service as idle when synchronizer has not been queried', async () => { + const statusService = new StatusService(); + statusService.aliEcsSynchronizer = { status: ServiceStatus.NOT_ASKED }; + const result = statusService.retrieveKafkaServiceStatus(); + + deepStrictEqual(result, { + name: 'kafka', + status: { ok: false }, + extras: { state: ServiceStatus.NOT_ASKED }, + }); + }); + + test('marks Kafka service as unhealthy when synchronizer reports an error', async () => { + const statusService = new StatusService(); + statusService.aliEcsSynchronizer = { status: ServiceStatus.ERROR }; + const result = statusService.retrieveKafkaServiceStatus(); + + deepStrictEqual(result, { + name: 'kafka', + status: { ok: false }, + extras: { state: ServiceStatus.ERROR }, + }); + }); + + test('marks Kafka service as initializing while synchronizer is loading', async () => { + const statusService = new StatusService(); + statusService.aliEcsSynchronizer = { status: ServiceStatus.LOADING }; + const result = statusService.retrieveKafkaServiceStatus(); + + deepStrictEqual(result, { + name: 'kafka', + status: { ok: false }, + extras: { state: ServiceStatus.LOADING }, + }); + }); + + test('marks Kafka service as not configured when no synchronizer is present', async () => { + const statusService = new StatusService(); + const result = statusService.retrieveKafkaServiceStatus(); + + deepStrictEqual(result, { + name: 'kafka', + status: { ok: false }, + extras: { state: 'NOT_CONFIGURED' }, + }); + }); + }); }; diff --git a/QualityControl/test/public/features/runMode.test.js b/QualityControl/test/public/features/runMode.test.js index 1c500f01b..8f6cbdc25 100644 --- a/QualityControl/test/public/features/runMode.test.js +++ b/QualityControl/test/public/features/runMode.test.js @@ -14,6 +14,8 @@ import { strictEqual, ok } from 'node:assert'; import { delay } from '../../testUtils/delay.js'; +import { IntegratedServices } from '../../../common/library/enums/Status/integratedServices.enum.js'; +import { ServiceStatus } from '../../../common/library/enums/Status/serviceStatus.enum.js'; // If using nock for HTTP mocking (uncomment if available) // import nock from 'nock'; @@ -37,20 +39,170 @@ export const runModeTests = async (url, page, timeout = 5000, testParent) => { } }); - await testParent.test('should have a switch to enable run mode', { timeout }, async () => { - await page.goto( - `${url}?page=objectTree`, - { waitUntil: 'networkidle0' }, - ); - await delay(100); - // Prevent the 'get run status' from re-triggering mid test - await page.evaluate(() => { - window.model.filterModel.ONGOING_RUN_INTERVAL_MS = 12000000; - }); - await page.locator('.form-check-label > .switch'); - const runsModeTitle = await page.evaluate(() => - document.querySelector('.form-check-label').textContent); - strictEqual(runsModeTitle, 'Run mode', 'The text displayed is not `Runs mode`'); + await testParent.test('when kafka service is not configured the run mode toggle should be hidden', { timeout }, async () => { + const requestHandler = (interceptedRequest) => { + const url = interceptedRequest.url(); + + if (url.includes(`/api/status/${IntegratedServices.KAFKA}`)) { + interceptedRequest.respond({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + name: IntegratedServices.KAFKA, + status: { + ok: false, + }, + extras: { + state: 'NOT_CONFIGURED', + }, + }), + }); + } else { + interceptedRequest.continue(); + } + }; + + try { + // Enable interception and attach the handler + await page.setRequestInterception(true); + page.on('request', requestHandler); + + await page.goto( + `${url}?page=objectTree`, + { waitUntil: 'networkidle0' }, + ); + await delay(100); + // Prevent the 'get run status' from re-triggering mid test + await page.evaluate(() => { + window.model.filterModel.ONGOING_RUN_INTERVAL_MS = 12000000; + }); + + const runsModeToggleNoExist = await page.evaluate(() => document.querySelector('.form-check-label') === null); + ok(runsModeToggleNoExist, 'The RunMode switch should not be displayed'); + + const runsModeErrorNoExist = await page.evaluate(() => document.querySelector('#run-mode-failure') === null); + ok(runsModeErrorNoExist, 'The RunMode switch should not be displayed'); + } catch (error) { + // Test failed + ok(false, error.message); + } finally { + // Cleanup: remove listener and disable interception + page.off('request', requestHandler); + await page.setRequestInterception(false); + } + }); + + await testParent.test('when kafka service is unavailable an error should be displayed instead of the run mode toggle', { timeout }, async () => { + const requestHandler = (interceptedRequest) => { + const url = interceptedRequest.url(); + + if (url.includes(`/api/status/${IntegratedServices.KAFKA}`)) { + interceptedRequest.respond({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + name: IntegratedServices.KAFKA, + status: { + ok: false, + }, + extras: { + state: ServiceStatus.ERROR, + }, + }), + }); + } else { + interceptedRequest.continue(); + } + }; + + try { + // Enable interception and attach the handler + await page.setRequestInterception(true); + page.on('request', requestHandler); + + await page.goto( + `${url}?page=objectTree`, + { waitUntil: 'networkidle0' }, + ); + await delay(100); + // Prevent the 'get run status' from re-triggering mid test + await page.evaluate(() => { + window.model.filterModel.ONGOING_RUN_INTERVAL_MS = 12000000; + }); + + const runsModeNoExist = await page.evaluate(() => document.querySelector('.form-check-label') === null); + ok(runsModeNoExist, 'The RunMode switch should not be displayed'); + + const runModeErrorMessage = await page.evaluate(() => { + const spans = document.querySelectorAll('#run-mode-failure > span'); + return Array.from(spans) + .map((span) => span.textContent.trim()) + .filter((text) => text !== '') + .join(' '); + }); + strictEqual( + runModeErrorMessage, + `Contact an administrator and include this information: Kafka service returned status code '${ServiceStatus.ERROR}'`, + 'RunMode failure should have the correct error message', + ); + } catch (error) { + // Test failed + ok(false, error.message); + } finally { + // Cleanup: remove listener and disable interception + page.off('request', requestHandler); + await page.setRequestInterception(false); + } + }); + + await testParent.test('should have a switch to enable run mode when kafka service is available', { timeout }, async () => { + const requestHandler = (interceptedRequest) => { + const url = interceptedRequest.url(); + + if (url.includes(`/api/status/${IntegratedServices.KAFKA}`)) { + interceptedRequest.respond({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + name: IntegratedServices.KAFKA, + status: { + ok: true, + }, + extras: { + state: ServiceStatus.SUCCESS, + }, + }), + }); + } else { + interceptedRequest.continue(); + } + }; + + try { + // Enable interception and attach the handler + await page.setRequestInterception(true); + page.on('request', requestHandler); + + await page.goto( + `${url}?page=objectTree`, + { waitUntil: 'networkidle0' }, + ); + await delay(100); + // Prevent the 'get run status' from re-triggering mid test + await page.evaluate(() => { + window.model.filterModel.ONGOING_RUN_INTERVAL_MS = 12000000; + }); + await page.locator('.form-check-label > .switch'); + const runsModeTitle = await page.evaluate(() => document.querySelector('.form-check-label').textContent); + strictEqual(runsModeTitle, 'Run mode', 'The text displayed is not `Run mode`'); + } catch (error) { + // Test failed + ok(false, error.message); + } finally { + // Cleanup: remove listener and disable interception + page.off('request', requestHandler); + await page.setRequestInterception(false); + } }); await testParent.test('should activate run mode', { timeout }, async () => { diff --git a/QualityControl/test/public/pages/about-page.test.js b/QualityControl/test/public/pages/about-page.test.js index 2fb102596..18cad6a6a 100644 --- a/QualityControl/test/public/pages/about-page.test.js +++ b/QualityControl/test/public/pages/about-page.test.js @@ -12,7 +12,6 @@ */ import { strictEqual } from 'node:assert'; -import { ServiceStatus } from '../../../common/library/enums/Status/serviceStatus.enum.js'; const ABOUT_PAGE_PARAM = '?page=about'; @@ -25,6 +24,7 @@ export const aboutPageTests = async (url, page, timeout = 5000, testParent) => { await testServiceStatus(testParent, page, 'qcg', timeout); await testServiceStatus(testParent, page, 'qc', timeout); await testServiceStatus(testParent, page, 'ccdb', timeout); + await testServiceStatus(testParent, page, 'kafka', timeout); }; const testServiceStatus = async (testParent, page, serviceName, timeout = 5000) => { @@ -34,10 +34,8 @@ const testServiceStatus = async (testParent, page, serviceName, timeout = 5000) { timeout }, async () => { const kind = await page.evaluate( - (service, serviceStatus) => - window.model.aboutViewModel.services[serviceStatus.SUCCESS][service].kind, + (service) => window.model.aboutViewModel.findService(service)?.kind, serviceName, - ServiceStatus, ); strictEqual(kind, 'Success'); From 79f4f82d61252154c36fb9d3e9fce437b7b2556d Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:48:58 +0100 Subject: [PATCH 2/2] Extract run mode rendering logic into dedicated component --- .../public/common/filters/filterViews.js | 37 ++------------ .../common/filters/runMode/runModeCheckbox.js | 50 ++++++++++++++++++- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/QualityControl/public/common/filters/filterViews.js b/QualityControl/public/common/filters/filterViews.js index dfba10db6..34330d41d 100644 --- a/QualityControl/public/common/filters/filterViews.js +++ b/QualityControl/public/common/filters/filterViews.js @@ -15,12 +15,9 @@ import { filterInput, dynamicSelector, ongoingRunsSelector } from './filter.js'; import { FilterType } from './filterTypes.js'; import { filtersConfig, runModeFilterConfig } from './filtersConfig.js'; +import { runModeComponent } from './runMode/runModeCheckbox.js'; import { lastUpdatePanel, runStatusPanel } from './runMode/runStatusPanel.js'; -import { h, iconChevronBottom, iconChevronTop, iconWarning } from '/js/src/index.js'; -import { IntegratedServices } from '../../../library/enums/Status/integratedServices.enum.js'; -import { spinner } from '../spinner.js'; -import { ServiceStatus } from '../../../library/enums/Status/serviceStatus.enum.js'; -import { runModeCheckbox } from './runMode/runModeCheckbox.js'; +import { h, iconChevronBottom, iconChevronTop } from '/js/src/index.js'; /** * Creates an input element for a specific metadata field; @@ -88,7 +85,6 @@ export function filtersPanel(filterModel, viewModel) { const onFocusCallback = fetchOngoingRuns.bind(filterService); const onEnterCallback = () => filterModel.triggerFilter(viewModel); const clearFilterCallback = clearFiltersAndTrigger.bind(filterModel, viewModel); - const kafkaService = filterModel.model.aboutViewModel.findService(IntegratedServices.KAFKA); const filtersList = isRunModeActivated ? runModeFilterConfig(filterService) : filtersConfig(filterService); @@ -97,34 +93,7 @@ export function filtersPanel(filterModel, viewModel) { '.w-100.flex-column.p2.g2.justify-center#filterElement', [ h('.flex-row.g2.justify-center.items-center', [ - kafkaService?.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) => { - switch (payload.extras.state) { - case ServiceStatus.SUCCESS: - return runModeCheckbox(filterModel, viewModel); - case 'NOT_CONFIGURED': - return null; - default: - return 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 code '${payload.extras.state ?? '?'}'`), - ]); - } - }, - Other: () => {}, - }), + runModeComponent(filterModel, viewModel), !isRunModeActivated && [triggerFiltersButton(onEnterCallback, filterModel), clearFiltersButton(clearFilterCallback)], ...filtersList.map((filter) => diff --git a/QualityControl/public/common/filters/runMode/runModeCheckbox.js b/QualityControl/public/common/filters/runMode/runModeCheckbox.js index a4cdb48da..0f528abda 100644 --- a/QualityControl/public/common/filters/runMode/runModeCheckbox.js +++ b/QualityControl/public/common/filters/runMode/runModeCheckbox.js @@ -11,7 +11,55 @@ * or submit itself to any jurisdiction. */ -import { h } from '/js/src/index.js'; +import { h, iconWarning } 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. + * - '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) => { + switch (payload.extras.state) { + case ServiceStatus.SUCCESS: + return runModeCheckbox(filterModel, viewModel); + case 'NOT_CONFIGURED': + return null; + default: + return 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 code '${payload.extras.state ?? '?'}'`), + ]); + } + }, + Other: () => {}, + }); /** * Render a run mode switch