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
18 changes: 13 additions & 5 deletions lib/database/repositories/QcFlagRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,10 @@ class QcFlagRepository extends Repository {
* and informtion about missing and unverified flags
*
* @param {number} dataPassId the id of a data-pass
* @param {number} [runNumber] run number to filter by
* @return {Promise<Object.<number, RunGaqSubSummary>>} resolves with the map between run number and the corresponding run GAQ summary
*/
async getGaqCoverages(dataPassId) {
async getGaqCoverages(dataPassId, runNumber) {
const blockAggregationQuery = `
SELECT
gaq_periods.data_pass_id,
Expand Down Expand Up @@ -147,7 +148,8 @@ class QcFlagRepository extends Repository {
AND (qcfep.to IS NULL OR qcfep.\`to\` > gaq_periods.\`from\`)

WHERE gaq_periods.data_pass_id = :dataPassId
GROUP BY gaq_periods.data_pass_id, gaq_periods.run_number, gaq_periods.\`from\`, gaq_periods.to
${runNumber !== undefined && runNumber !== null ? 'AND gaq_periods.run_number = :runNumber' : ''}
GROUP BY gaq_periods.data_pass_id, gaq_periods.run_number, gaq_periods.\`from\`, gaq_periods.\`to\`
`;

const summaryQuery = `
Expand All @@ -164,14 +166,20 @@ class QcFlagRepository extends Repository {
FROM (${blockAggregationQuery}) AS gaq
GROUP BY gaq.data_pass_id, gaq.run_number;
`;
const [rows] = await this.model.sequelize.query(summaryQuery, { replacements: { dataPassId } });

const replacements = { dataPassId };
if (runNumber !== undefined && runNumber !== null) {
replacements.runNumber = runNumber;
}

const [rows] = await this.model.sequelize.query(summaryQuery, { replacements });
const entries = rows.map(({
run_number,
bad_coverage,
mcr_coverage,
good_coverage,
flags_list,
verifiedd_flags_list,
verified_flags_list,
undefined_quality_periods_count,
}) => [
run_number,
Expand All @@ -180,7 +188,7 @@ class QcFlagRepository extends Repository {
mcReproducibleCoverage: parseFloat(mcr_coverage ?? '0'),
goodCoverage: parseFloat(good_coverage ?? '0'),
flagsIds: [...new Set(flags_list?.split(','))],
verifiedFlagsIds: [...new Set(verifiedd_flags_list?.split(','))],
verifiedFlagsIds: [...new Set(verified_flags_list?.split(','))],
undefinedQualityPeriodsCount: undefined_quality_periods_count,
},
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SkimmingStage } from '../../../domain/enums/SkimmingStage.js';
import { NumericalComparisonFilterModel } from '../../../components/Filters/common/filters/NumericalComparisonFilterModel.js';
import { jsonFetch } from '../../../utilities/fetch/jsonFetch.js';
import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js';
import { RemoteDataSource } from '../../../utilities/fetch/RemoteDataSource.js';
import { DetectorType } from '../../../domain/enums/DetectorTypes.js';

const ALL_CPASS_PRODUCTIONS_REGEX = /cpass\d+/;
Expand Down Expand Up @@ -58,6 +59,12 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo
this._markAsSkimmableRequestResult$ = new ObservableData(RemoteData.notAsked());
this._markAsSkimmableRequestResult$.bubbleTo(this);

this._gaqSummary$ = new ObservableData({});
this._gaqSummary$.bubbleTo(this);

this._gaqSummarySource = null;
this._gaqSequenceAbortController = null;

this._skimmableRuns$ = new ObservableData(RemoteData.notAsked());
this._skimmableRuns$.bubbleTo(this);

Expand All @@ -71,6 +78,8 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo

this._discardAllQcFlagsActionState$ = new ObservableData(RemoteData.notAsked());
this._discardAllQcFlagsActionState$.bubbleTo(this);

this._item$.observe(() => this._fetchGaqSummaryForCurrentRuns());
}

/**
Expand Down Expand Up @@ -321,6 +330,87 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo
}
}

/**
* Cancel all ongoing and future GAQ summary fetches
* @return {void} promise
*/
_abortGaqFetches() {
// Aborts the overall sequence fetch, i.e. stops further individual run fetches
this._gaqSequenceAbortController?.abort();

// Aborts individual run fetch in-flight
this._gaqSummarySource?._abortController?.abort();
}

/**
* Fetch GAQ summary for given data pass and run
* @param {number} [runNumber] run number to filter by
* @return {Promise<void>} resolves once data has been fetched
*/
async _fetchGaqSummary(runNumber) {
this._gaqSummarySource = new RemoteDataSource();
// Pipe the result into the correct slot in the gaqSummary$ observable
this._gaqSummarySource.pipe({
setCurrent: (remoteData) => {
const current = this._gaqSummary$.getCurrent();
this._gaqSummary$.setCurrent({
...current,
[runNumber]: remoteData.apply({ Success: (response) => response.data }),
});
},
});
const url = buildUrl('/api/qcFlags/summary/gaq', {
dataPassId: this._dataPassId,
mcReproducibleAsNotBad: this._mcReproducibleAsNotBad,
runNumber: runNumber,
});
await this._gaqSummarySource.fetch(url);
}

/**
* Fetch GAQ summary for currently displayed (paginated) runs
* @return {void}
*/
_fetchGaqSummaryForCurrentRuns() {
// Stop any previous fetch (quickly changing filters, pagination, etc)
this._abortGaqFetches();

// Reset abort controller
this._gaqSequenceAbortController = new AbortController();
const { signal } = this._gaqSequenceAbortController;

this._item$.getCurrent().match({
Success: async (runs) => {
const runNumbers = runs.map((run) => run.runNumber);

// Prepare GAQ summary object with NotAsked RemoteData state for all runs
let gaqSummary = {};
for (const runNumber of runNumbers) {
gaqSummary = { ...gaqSummary, [runNumber]: RemoteData.notAsked() };
}
this._gaqSummary$.setCurrent(gaqSummary);

// Trigger GAQ summary fetch for each run
for (const runNumber of runNumbers) {
if (signal.aborted) {
return;
}

try {
await this._fetchGaqSummary(runNumber);
} catch {
if (signal.aborted) {
return;
}
}
}
},
Other: () => {
// Don't fetch if runs haven't loaded successfully yet
},
});
}

/**
* Fetch skimmable runs for given data pass
* @return {Promise<void>} resolves once data are fetched
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumb
import { qcSummaryLegendTooltip } from '../../../components/qcFlags/qcSummaryLegendTooltip.js';
import { isRunNotSubjectToQc } from '../../../components/qcFlags/isRunNotSubjectToQc.js';
import { frontLink } from '../../../components/common/navigation/frontLink.js';
import { getQcSummaryDisplay } from '../ActiveColumns/getQcSummaryDisplay.js';
import errorAlert from '../../../components/common/errorAlert.js';
import { switchInput } from '../../../components/common/form/switchInput.js';
import { PdpBeamType } from '../../../domain/enums/PdpBeamType.js';
Expand Down Expand Up @@ -102,6 +103,7 @@ export const RunsPerDataPassOverviewPage = ({
detectors: remoteDetectors,
dataPass: remoteDataPass,
qcSummary: remoteQcSummary,
gaqSummary: remoteGaqSummary,
displayOptions,
dataPassId,
sortModel,
Expand All @@ -118,6 +120,9 @@ export const RunsPerDataPassOverviewPage = ({

return h(
'.intermediate-flex-column',
{ onremove: () => {
perDataPassOverviewModel._abortGaqFetches();
} },
mergeRemoteData([remoteDataPass, remoteRuns, remoteDetectors, remoteQcSummary]).match({
NotAsked: () => null,
Failure: (errors) => errorAlert(errors),
Expand Down Expand Up @@ -160,14 +165,21 @@ export const RunsPerDataPassOverviewPage = ({
),
visible: true,
format: (_, { runNumber }) => {
const gaqDisplay = h('button.btn.btn-primary.w-100', [
'GAQ',
h(
'.d-inline-block.va-t-bottom',
tooltip(h('.f7', iconWarning()), 'GAQ Summary is disabled, please click to view GAQ flags'),
),
]);
return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber });
const gaqLoadingSpinner = h('.flex-row.items-center.justify-center.black', spinner({ size: 2, absolute: false }));
const runGaqSummary = remoteGaqSummary[runNumber];

return runGaqSummary.match({
Success: (gaqSummary) => {
const gaqDisplay = gaqSummary?.undefinedQualityPeriodsCount === 0
? getQcSummaryDisplay(gaqSummary)
: h('button.btn.btn-primary.w-100', 'GAQ');

return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber });
},
Loading: () => tooltip(gaqLoadingSpinner, 'Loading GAQ summary...'),
NotAsked: () => tooltip(gaqLoadingSpinner, 'Loading GAQ summary...'),
Failure: () => tooltip(iconWarning(), 'Failed to load GAQ summary'),
});
},
filter: ({ filteringModel }) => numericalComparisonFilter(
filteringModel.get('gaq[notBadFraction]'),
Expand Down
7 changes: 4 additions & 3 deletions lib/server/controllers/qcFlag.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,17 +382,18 @@ const getGaqQcFlagsHandler = async (request, response) => {
const getGaqSummaryHandler = async (request, response) => {
const validatedDTO = await dtoValidator(
DtoFactory.queryOnly(Joi.object({
dataPassId: Joi.number().required(),
dataPassId: Joi.number().positive().required(),
mcReproducibleAsNotBad: Joi.boolean().optional(),
runNumber: Joi.number().positive().required(),
})),
request,
response,
);
if (validatedDTO) {
try {
const { dataPassId, mcReproducibleAsNotBad = false } = validatedDTO.query;
const { dataPassId, mcReproducibleAsNotBad = false, runNumber } = validatedDTO.query;

const data = await gaqService.getSummary(dataPassId, { mcReproducibleAsNotBad });
const data = await gaqService.getSummary(dataPassId, { mcReproducibleAsNotBad, runNumber });
response.json({ data });
} catch (error) {
updateExpressResponseFromNativeError(response, error);
Expand Down
13 changes: 11 additions & 2 deletions lib/server/services/qualityControlFlag/GaqService.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ class GaqService {
* @param {object} [options] additional options
* @param {boolean} [options.mcReproducibleAsNotBad = false] if set to true,
* `Limited Acceptance MC Reproducible` flag type is treated as good one
* @param {number} [options.runNumber] Optional run number to filter by
* @return {Promise<GaqSummary>} Resolves with the GAQ Summary
*/
async getSummary(dataPassId, { mcReproducibleAsNotBad = false } = {}) {
async getSummary(dataPassId, { mcReproducibleAsNotBad = false, runNumber } = {}) {
await getOneDataPassOrFail({ id: dataPassId });
const gaqCoverages = await QcFlagRepository.getGaqCoverages(dataPassId);
const gaqCoverages = await QcFlagRepository.getGaqCoverages(dataPassId, runNumber);
const gaqSummary = Object.entries(gaqCoverages).map(([
runNumber,
{
Expand All @@ -71,6 +72,14 @@ class GaqService {
},
]);

/**
* If runNumber is specified, only one summary is returned but the getGaqCoverages
* returns still with runNumber as key, so we extract the single value from the array.
*/
if (runNumber && gaqSummary.length === 1) {
return Object.fromEntries(gaqSummary)[runNumber];
}

return Object.fromEntries(gaqSummary);
}

Expand Down
50 changes: 45 additions & 5 deletions test/api/qcFlags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,22 +556,62 @@ module.exports = () => {
relations,
);

const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=3');
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=3&runNumber=54');
expect(response.status).to.be.equal(200);
const { body: { data } } = response;
expect(data).to.be.eql({
54: {
missingVerificationsCount: 1,
mcReproducible: true,
badEffectiveRunCoverage: 1,
explicitlyNotBadEffectiveRunCoverage: 0,
undefinedQualityPeriodsCount: 0,
},
});
);
});

it('should return 400 when bad query parameter provided', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq');
it('should return empty GAQ summary if no data exists for given dataPassId & runNumber combination', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=3&runNumber=999');
expect(response.status).to.equal(200);
const { body: { data } } = response;
expect(data).to.eql({});
});

it('should return 400 if dataPassId is not positive', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=-1&runNumber=54');
expect(response.status).to.equal(400);
expect(response.body.errors[0].detail).to.equal('"query.dataPassId" must be a positive number');
});

it('should return 400 if runNumber is not positive', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=3&runNumber=-10');
expect(response.status).to.equal(400);
expect(response.body.errors[0].detail).to.equal('"query.runNumber" must be a positive number');
});

it('should return 400 if dataPassId is not a number', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=abc&runNumber=54');
expect(response.status).to.equal(400);
const { errors } = response.body;
expect(errors[0].detail).to.equal('"query.dataPassId" must be a number');
});

it('should return 400 if runNumber is not a number', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=3&runNumber=abc');
expect(response.status).to.equal(400);
const { errors } = response.body;
expect(errors[0].detail).to.equal('"query.runNumber" must be a number');
});

it('should return 400 when runNumber parameter is missing', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?dataPassId=3');
expect(response.status).to.be.equal(400);
const { errors } = response.body;
const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/runNumber');
expect(titleError.detail).to.equal('"query.runNumber" is required');
});

it('should return 400 when dataPassId parameter is missing', async () => {
const response = await request(server).get('/api/qcFlags/summary/gaq?runNumber=54');
expect(response.status).to.be.equal(400);
const { errors } = response.body;
const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/dataPassId');
Expand Down
Loading
Loading