Skip to content

Commit 38eae5f

Browse files
authored
[O2B-1348] Filtering by not bad data fraction for each detector (#1941)
* WIP * filtering * cleanup * fix * wip * use abastraction * wip * a * fix * cleanup * simplify * rm or * refactor filter popover * use filters by each detector on fronT * cleanup * add header * add tooltip * fix * fix * fix * ajdust * test * fix test * add ui test * fix * fix * docs * logic fixes * fix * fix error
1 parent 19367e5 commit 38eae5f

13 files changed

Lines changed: 381 additions & 91 deletions

File tree

lib/domain/dtos/filters/RunFilterDto.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ exports.RunFilterDto = Joi.object({
9090
odcTopologyFullName: Joi.string().trim(),
9191
detectors: DetectorsFilterDto,
9292
lhcPeriods: Joi.string().trim(),
93+
lhcPeriodIds: Joi.array().items(Joi.number().positive().integer()),
9394
dataPassIds: Joi.array().items(Joi.number()),
9495
simulationPassIds: Joi.array().items(Joi.number()),
9596
runTypes: CustomJoi.stringArray().items(Joi.string()).single().optional(),
@@ -116,4 +117,24 @@ exports.RunFilterDto = Joi.object({
116117
),
117118
mcReproducibleAsNotBad: Joi.boolean().optional(),
118119
}),
120+
121+
detectorsQc: Joi.object()
122+
.pattern(
123+
Joi.string().regex(/^_\d+$/), // Detector id with '_' prefix
124+
Joi.object({ notBadFraction: FloatComparisonDto }),
125+
)
126+
.keys({
127+
mcReproducibleAsNotBad: Joi.boolean().optional(),
128+
})
129+
.custom((detectorsQcObj, helpers) => {
130+
const [{ dataPassIds, simulationPassIds, lhcPeriodIds }] = helpers.state.ancestors;
131+
const runsCollectionFilters = [dataPassIds, simulationPassIds, lhcPeriodIds].filter(({ length } = {}) => length >= 1);
132+
133+
if (runsCollectionFilters.length !== 1 || runsCollectionFilters[0].length !== 1) {
134+
return helpers.message('Filtering by detector not-bad fraction is allowed only with exactly one of: ' +
135+
'dataPassIds, simulationPassIds, lhcPeriodIds with exactly one ID.');
136+
}
137+
138+
return detectorsQcObj;
139+
}),
119140
});

lib/public/app.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,29 @@ th.text-center, td.text-center {
226226
}
227227

228228

229+
.section-divider {
230+
display: flex;
231+
align-items: center;
232+
text-align: center;
233+
color: #333;
234+
margin: 1rem 0;
235+
}
236+
237+
.section-divider::before,
238+
.section-divider::after {
239+
content: "";
240+
flex: 1;
241+
border-bottom: 1px solid #333;
242+
}
243+
244+
.section-divider::before {
245+
margin-right: 0.75em;
246+
}
247+
248+
.section-divider::after {
249+
margin-left: 0.75em;
250+
}
251+
229252
/* alerts */
230253

231254
.alert {

lib/public/components/Filters/common/FilteringModel.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,21 @@ export class FilteringModel extends Observable {
104104

105105
return this._filters[key];
106106
}
107+
108+
/**
109+
* Add new filter
110+
*
111+
* @param {string} key key of a new filter
112+
* @param {FilterModel} filter the new filter
113+
*/
114+
put(key, filter) {
115+
if (key in this._filters) {
116+
throw new Error(`Filter under key ${key} already exists`);
117+
}
118+
119+
this._filters[key] = filter;
120+
this._filterModels.push(filter);
121+
filter.bubbleTo(this);
122+
filter.visualChange$?.bubbleTo(this._visualChange$);
123+
}
107124
}

lib/public/components/Filters/common/filtersPanelPopover.js

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ import { profiles } from '../../common/table/profiles.js';
1515
import { applyProfile } from '../../../utilities/applyProfile.js';
1616
import { tooltip } from '../../common/popover/tooltip.js';
1717

18+
/**
19+
* @typedef FilterConfiguration
20+
*
21+
* @property {string} name
22+
* @property {function|Component} filter
23+
* @property {string|Component} filterTooltip
24+
* @property {object|string|string[]} profiles
25+
*/
26+
27+
/**
28+
* @typedef FiltersConfiguration
29+
*
30+
* @type {object<string, FilterConfiguration>} mapping: filterable property -> filter configuration
31+
*/
32+
1833
/**
1934
* Return the filters panel popover trigger
2035
*
@@ -23,67 +38,87 @@ import { tooltip } from '../../common/popover/tooltip.js';
2338
const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary', 'Filters');
2439

2540
/**
26-
* Return the filters panel popover content (i.e. the actual filters)
27-
*
28-
* TODO Separate filters from active columns
41+
* Create main header of the filters panel
42+
* @param {FilteringModel} filteringModel filtering model
43+
* @returns {Component} main panel header
44+
*/
45+
const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-between', [
46+
h('.f4', 'Filters'),
47+
h(
48+
'button#reset-filters.btn.btn-danger',
49+
{
50+
onclick: () => filteringModel.resetFiltering
51+
? filteringModel.resetFiltering()
52+
: filteringModel.reset(true),
53+
disabled: !filteringModel.isAnyFilterActive(),
54+
},
55+
'Reset all filters',
56+
),
57+
]);
58+
59+
/**
60+
* Return the filters panel popover content section
2961
*
30-
* @param {object} filteringModel the filtering model
31-
* @param {object} columns the list of columns containing filters
62+
* @param {FilteringModel} filteringModel the filtering model
63+
* @param {FiltersConfiguration} filtersConfiguration filters configuration
3264
* @param {object} [configuration] additional configuration
3365
* @param {string} [configuration.profile = profiles.none] profile which filters should be rendered @see Column
34-
* @return {Component} the filters panel
66+
* @return {Component} the filters section
3567
*/
36-
const filtersToggleContent = (
37-
filteringModel,
38-
columns,
39-
{ profile: appliedProfile = profiles.none } = {},
40-
) => h('.w-l.flex-column.p3.g3', [
41-
h('.flex-row.justify-between', [
42-
h('.f4', 'Filters'),
43-
h(
44-
'button#reset-filters.btn.btn-danger',
45-
{
46-
onclick: () => filteringModel.resetFiltering
47-
? filteringModel.resetFiltering()
48-
: filteringModel.reset(true),
49-
disabled: !filteringModel.isAnyFilterActive(),
50-
},
51-
'Reset all filters',
52-
),
53-
]),
68+
export const filtersSection = (filteringModel, filtersConfiguration, { profile: appliedProfile = profiles.none } = {}) =>
5469
h('.flex-column.g2', [
55-
Object.entries(columns)
70+
Object.entries(filtersConfiguration)
5671
.filter(([_, column]) => {
5772
let columnProfiles = column.profiles ?? [profiles.none];
5873
if (typeof columnProfiles === 'string') {
5974
columnProfiles = [columnProfiles];
6075
}
6176
return applyProfile(column, appliedProfile, columnProfiles)?.filter;
6277
})
63-
.map(([columnKey, { name, filterTooltip, filter }]) => [
64-
h(`.flex-row.items-baseline.${columnKey}-filter`, [
65-
h('.w-30.f5.flex-row.items-center.g2', [
66-
name,
67-
filterTooltip ? tooltip(info(), filterTooltip) : null,
68-
]),
69-
h('.w-70', typeof filter === 'function' ? filter(filteringModel) : filter),
70-
]),
71-
]),
72-
]),
78+
.map(([columnKey, { name, filterTooltip, filter }]) =>
79+
name
80+
? [
81+
h(`.flex-row.items-baseline.${columnKey}-filter`, [
82+
h('.w-30.f5.flex-row.items-center.g2', [
83+
name,
84+
filterTooltip ? tooltip(info(), filterTooltip) : null,
85+
]),
86+
h('.w-70', typeof filter === 'function' ? filter(filteringModel) : filter),
87+
]),
88+
]
89+
: typeof filter === 'function' ? filter(filteringModel) : filter),
90+
]);
91+
92+
/**
93+
* Return the filters panel popover content (i.e. the actual filters)
94+
*
95+
* @param {FilteringModel} filteringModel the filtering model
96+
* @param {FiltersConfiguration} filtersConfiguration filters configuration
97+
* @param {object} [configuration] additional configuration
98+
* @param {string} [configuration.profile = profiles.none] profile which filters should be rendered @see Column
99+
* @return {Component} the filters panel
100+
*/
101+
const filtersToggleContent = (
102+
filteringModel,
103+
filtersConfiguration,
104+
configuration = {},
105+
) => h('.w-l.flex-column.p3.g3', [
106+
filtersToggleContentHeader(filteringModel),
107+
filtersSection(filteringModel, filtersConfiguration, configuration),
73108
]);
74109

75110
/**
76111
* Return component composed of the filtering popover and its button trigger
77112
*
78-
* @param {object} filterModel the filter model
79-
* @param {object} activeColumns the list of active columns containing the filtering configuration
113+
* @param {FilteringModel} filteringModel the filtering model
114+
* @param {FiltersConfiguration} filtersConfiguration filters configuration
80115
* @param {object} [configuration] optional configuration
81116
* @param {string} [configuration.profile] specify for which profile filtering should be enabled
82117
* @return {Component} the filter component
83118
*/
84-
export const filtersPanelPopover = (filterModel, activeColumns, configuration) => popover(
119+
export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => popover(
85120
filtersToggleTrigger(),
86-
filtersToggleContent(filterModel, activeColumns, configuration),
121+
filtersToggleContent(filteringModel, filtersConfiguration, configuration),
87122
{
88123
...PopoverTriggerPreConfiguration.click,
89124
anchor: PopoverAnchors.RIGHT_START,

lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* granted to it by virtue of its status as an Intergovernmental Organization
1111
* or submit itself to any jurisdiction.
1212
*/
13-
import { h, iconBan, iconX } from '/js/src/index.js';
13+
import { h, iconBan, iconX, info } from '/js/src/index.js';
1414
import { qcFlagCreationPanelLink } from '../../../components/qcFlags/qcFlagCreationPanelLink.js';
1515
import { tooltip } from '../../../components/common/popover/tooltip.js';
1616
import { isRunNotSubjectToQc } from '../../../components/qcFlags/isRunNotSubjectToQc.js';
@@ -20,6 +20,8 @@ import { qcFlagOverviewPanelLink } from '../../../components/qcFlags/qcFlagOverv
2020
import { remoteDplDetectorUserHasAccessTo } from '../../../services/detectors/remoteDplDetectorUserHasAccessTo.js';
2121
import errorAlert from '../../../components/common/errorAlert.js';
2222
import spinner from '../../../components/common/spinner.js';
23+
import { numericalComparisonFilter } from '../../../components/Filters/common/filters/numericalComparisonFilter.js';
24+
import { filtersSection } from '../../../components/Filters/common/filtersPanelPopover.js';
2325

2426
/**
2527
* Render QC summary for given run and detector
@@ -57,7 +59,7 @@ const runDetectorAsyncQualityDisplay = ({ dataPassId, simulationPassId }, run, d
5759
* @param {DataPass} [monalisaProduction.dataPass] data pass containing the run -- exclusive with `simulationPass`
5860
* @param {SimulationPass} [monalisaProduction.simulationPass] simulation pass containing the run -- exclusive with `dataPass`
5961
* @param {object} configuration display configuration
60-
* @param {object|string|string[]} [configuration.profiles] profiles which the column is restricted to
62+
* @param {string} [configuration.profile] profile which the column is restricted to
6163
* @param {QcSummary} [configuration.qcSummary] QC summary for given data/simulation pass
6264
* @return {object} active columns configuration
6365
*/
@@ -66,7 +68,7 @@ export const createRunDetectorsAsyncQcActiveColumns = (
6668
dplDetectors,
6769
remoteDplDetectorsUserHasAccessTo,
6870
{ dataPass, simulationPass },
69-
{ profiles, qcSummary } = {},
71+
{ profile, qcSummary } = {},
7072
) => {
7173
if (!dataPass && !simulationPass) {
7274
throw new Error('`dataPass` or `simulationPass` is required');
@@ -75,7 +77,7 @@ export const createRunDetectorsAsyncQcActiveColumns = (
7577
throw new Error('`dataPass` and `simulationPass` are exclusive options');
7678
}
7779

78-
return Object.fromEntries(dplDetectors?.map(({ name: detectorName, id: dplDetectorId }) => [
80+
let activeColumnEntries = dplDetectors?.map(({ name: detectorName, id: dplDetectorId }) => [
7981
detectorName,
8082
{
8183
name: detectorName.toUpperCase(),
@@ -136,7 +138,40 @@ export const createRunDetectorsAsyncQcActiveColumns = (
136138
},
137139
Loading: () => spinner(),
138140
}),
139-
profiles,
141+
profiles: profile,
140142
},
141-
]) ?? []);
143+
]) ?? [];
144+
145+
const filtersEntries = dplDetectors?.map(({ name: detectorName, id: dplDetectorId }) => [
146+
detectorName,
147+
{
148+
name: detectorName.toUpperCase(),
149+
visible: false,
150+
profiles: profile,
151+
filter: (filteringModel) => {
152+
const filterModel = filteringModel.get(`detectorsQc[_${dplDetectorId}][notBadFraction]`);
153+
return filterModel
154+
? numericalComparisonFilter(filterModel, { step: 0.1, selectorPrefix: `detectorsQc-for-${dplDetectorId}-notBadFraction` })
155+
: null;
156+
},
157+
},
158+
]) ?? [];
159+
160+
if (activeColumnEntries.length > 0) {
161+
activeColumnEntries = [
162+
[
163+
'detectorsQc',
164+
{
165+
name: null,
166+
filter: ({ filteringModel }) => [
167+
h('.section-divider', ['Detector QC', tooltip(info(), 'not-bad fraction expressed as a percentage')]),
168+
filtersSection(filteringModel, Object.fromEntries(filtersEntries), { profile }),
169+
],
170+
profiles: profile,
171+
},
172+
], ...activeColumnEntries,
173+
];
174+
}
175+
176+
return Object.fromEntries(activeColumnEntries);
142177
};

0 commit comments

Comments
 (0)