Skip to content
Merged
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
21 changes: 21 additions & 0 deletions lib/domain/dtos/filters/RunFilterDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ exports.RunFilterDto = Joi.object({
odcTopologyFullName: Joi.string().trim(),
detectors: DetectorsFilterDto,
lhcPeriods: Joi.string().trim(),
lhcPeriodIds: Joi.array().items(Joi.number().positive().integer()),
dataPassIds: Joi.array().items(Joi.number()),
simulationPassIds: Joi.array().items(Joi.number()),
runTypes: CustomJoi.stringArray().items(Joi.string()).single().optional(),
Expand All @@ -116,4 +117,24 @@ exports.RunFilterDto = Joi.object({
),
mcReproducibleAsNotBad: Joi.boolean().optional(),
}),

detectorsQc: Joi.object()
.pattern(
Joi.string().regex(/^_\d+$/), // Detector id with '_' prefix
Joi.object({ notBadFraction: FloatComparisonDto }),
)
.keys({
mcReproducibleAsNotBad: Joi.boolean().optional(),
})
.custom((detectorsQcObj, helpers) => {
const [{ dataPassIds, simulationPassIds, lhcPeriodIds }] = helpers.state.ancestors;
const runsCollectionFilters = [dataPassIds, simulationPassIds, lhcPeriodIds].filter(({ length } = {}) => length >= 1);

if (runsCollectionFilters.length !== 1 || runsCollectionFilters[0].length !== 1) {
return helpers.message('Filtering by detector not-bad fraction is allowed only with exactly one of: ' +
'dataPassIds, simulationPassIds, lhcPeriodIds with exactly one ID.');
}

return detectorsQcObj;
}),
});
23 changes: 23 additions & 0 deletions lib/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,29 @@ th.text-center, td.text-center {
}


.section-divider {
display: flex;
align-items: center;
text-align: center;
color: #333;
margin: 1rem 0;
}

.section-divider::before,
.section-divider::after {
content: "";
flex: 1;
border-bottom: 1px solid #333;
}

.section-divider::before {
margin-right: 0.75em;
}

.section-divider::after {
margin-left: 0.75em;
}

/* alerts */

.alert {
Expand Down
17 changes: 17 additions & 0 deletions lib/public/components/Filters/common/FilteringModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,21 @@ export class FilteringModel extends Observable {

return this._filters[key];
}

/**
* Add new filter
*
* @param {string} key key of a new filter
* @param {FilterModel} filter the new filter
*/
put(key, filter) {
if (key in this._filters) {
throw new Error(`Filter under key ${key} already exists`);
Comment thread
xsalonx marked this conversation as resolved.
}

this._filters[key] = filter;
this._filterModels.push(filter);
filter.bubbleTo(this);
filter.visualChange$?.bubbleTo(this._visualChange$);
}
}
113 changes: 74 additions & 39 deletions lib/public/components/Filters/common/filtersPanelPopover.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ import { profiles } from '../../common/table/profiles.js';
import { applyProfile } from '../../../utilities/applyProfile.js';
import { tooltip } from '../../common/popover/tooltip.js';

/**
* @typedef FilterConfiguration
*
* @property {string} name
* @property {function|Component} filter
* @property {string|Component} filterTooltip
* @property {object|string|string[]} profiles
*/

/**
* @typedef FiltersConfiguration
*
* @type {object<string, FilterConfiguration>} mapping: filterable property -> filter configuration
*/

/**
* Return the filters panel popover trigger
*
Expand All @@ -23,67 +38,87 @@ import { tooltip } from '../../common/popover/tooltip.js';
const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary', 'Filters');

/**
* Return the filters panel popover content (i.e. the actual filters)
*
* TODO Separate filters from active columns
* Create main header of the filters panel
* @param {FilteringModel} filteringModel filtering model
* @returns {Component} main panel header
*/
const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-between', [
h('.f4', 'Filters'),
h(
'button#reset-filters.btn.btn-danger',
{
onclick: () => filteringModel.resetFiltering
? filteringModel.resetFiltering()
: filteringModel.reset(true),
disabled: !filteringModel.isAnyFilterActive(),
},
'Reset all filters',
),
]);

/**
* Return the filters panel popover content section
*
* @param {object} filteringModel the filtering model
* @param {object} columns the list of columns containing filters
* @param {FilteringModel} filteringModel the filtering model
* @param {FiltersConfiguration} filtersConfiguration filters configuration
* @param {object} [configuration] additional configuration
* @param {string} [configuration.profile = profiles.none] profile which filters should be rendered @see Column
* @return {Component} the filters panel
* @return {Component} the filters section
*/
const filtersToggleContent = (
filteringModel,
columns,
{ profile: appliedProfile = profiles.none } = {},
) => h('.w-l.flex-column.p3.g3', [
h('.flex-row.justify-between', [
h('.f4', 'Filters'),
h(
'button#reset-filters.btn.btn-danger',
{
onclick: () => filteringModel.resetFiltering
? filteringModel.resetFiltering()
: filteringModel.reset(true),
disabled: !filteringModel.isAnyFilterActive(),
},
'Reset all filters',
),
]),
export const filtersSection = (filteringModel, filtersConfiguration, { profile: appliedProfile = profiles.none } = {}) =>
h('.flex-column.g2', [
Object.entries(columns)
Object.entries(filtersConfiguration)
.filter(([_, column]) => {
let columnProfiles = column.profiles ?? [profiles.none];
if (typeof columnProfiles === 'string') {
columnProfiles = [columnProfiles];
}
return applyProfile(column, appliedProfile, columnProfiles)?.filter;
})
.map(([columnKey, { name, filterTooltip, filter }]) => [
h(`.flex-row.items-baseline.${columnKey}-filter`, [
h('.w-30.f5.flex-row.items-center.g2', [
name,
filterTooltip ? tooltip(info(), filterTooltip) : null,
]),
h('.w-70', typeof filter === 'function' ? filter(filteringModel) : filter),
]),
]),
]),
.map(([columnKey, { name, filterTooltip, filter }]) =>
name
? [
h(`.flex-row.items-baseline.${columnKey}-filter`, [
h('.w-30.f5.flex-row.items-center.g2', [
name,
filterTooltip ? tooltip(info(), filterTooltip) : null,
]),
h('.w-70', typeof filter === 'function' ? filter(filteringModel) : filter),
]),
]
: typeof filter === 'function' ? filter(filteringModel) : filter),
]);

/**
* Return the filters panel popover content (i.e. the actual filters)
*
* @param {FilteringModel} filteringModel the filtering model
* @param {FiltersConfiguration} filtersConfiguration filters configuration
* @param {object} [configuration] additional configuration
* @param {string} [configuration.profile = profiles.none] profile which filters should be rendered @see Column
* @return {Component} the filters panel
*/
const filtersToggleContent = (
filteringModel,
filtersConfiguration,
configuration = {},
) => h('.w-l.flex-column.p3.g3', [
filtersToggleContentHeader(filteringModel),
filtersSection(filteringModel, filtersConfiguration, configuration),
]);

/**
* Return component composed of the filtering popover and its button trigger
*
* @param {object} filterModel the filter model
* @param {object} activeColumns the list of active columns containing the filtering configuration
* @param {FilteringModel} filteringModel the filtering model
* @param {FiltersConfiguration} filtersConfiguration filters configuration
* @param {object} [configuration] optional configuration
* @param {string} [configuration.profile] specify for which profile filtering should be enabled
* @return {Component} the filter component
*/
export const filtersPanelPopover = (filterModel, activeColumns, configuration) => popover(
export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => popover(
filtersToggleTrigger(),
filtersToggleContent(filterModel, activeColumns, configuration),
filtersToggleContent(filteringModel, filtersConfiguration, configuration),
{
...PopoverTriggerPreConfiguration.click,
anchor: PopoverAnchors.RIGHT_START,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
import { h, iconBan, iconX } from '/js/src/index.js';
import { h, iconBan, iconX, info } from '/js/src/index.js';
import { qcFlagCreationPanelLink } from '../../../components/qcFlags/qcFlagCreationPanelLink.js';
import { tooltip } from '../../../components/common/popover/tooltip.js';
import { isRunNotSubjectToQc } from '../../../components/qcFlags/isRunNotSubjectToQc.js';
Expand All @@ -20,6 +20,8 @@ import { qcFlagOverviewPanelLink } from '../../../components/qcFlags/qcFlagOverv
import { remoteDplDetectorUserHasAccessTo } from '../../../services/detectors/remoteDplDetectorUserHasAccessTo.js';
import errorAlert from '../../../components/common/errorAlert.js';
import spinner from '../../../components/common/spinner.js';
import { numericalComparisonFilter } from '../../../components/Filters/common/filters/numericalComparisonFilter.js';
import { filtersSection } from '../../../components/Filters/common/filtersPanelPopover.js';

/**
* Render QC summary for given run and detector
Expand Down Expand Up @@ -57,7 +59,7 @@ const runDetectorAsyncQualityDisplay = ({ dataPassId, simulationPassId }, run, d
* @param {DataPass} [monalisaProduction.dataPass] data pass containing the run -- exclusive with `simulationPass`
* @param {SimulationPass} [monalisaProduction.simulationPass] simulation pass containing the run -- exclusive with `dataPass`
* @param {object} configuration display configuration
* @param {object|string|string[]} [configuration.profiles] profiles which the column is restricted to
* @param {string} [configuration.profile] profile which the column is restricted to
* @param {QcSummary} [configuration.qcSummary] QC summary for given data/simulation pass
* @return {object} active columns configuration
*/
Expand All @@ -66,7 +68,7 @@ export const createRunDetectorsAsyncQcActiveColumns = (
dplDetectors,
remoteDplDetectorsUserHasAccessTo,
{ dataPass, simulationPass },
{ profiles, qcSummary } = {},
{ profile, qcSummary } = {},
) => {
if (!dataPass && !simulationPass) {
throw new Error('`dataPass` or `simulationPass` is required');
Expand All @@ -75,7 +77,7 @@ export const createRunDetectorsAsyncQcActiveColumns = (
throw new Error('`dataPass` and `simulationPass` are exclusive options');
}

return Object.fromEntries(dplDetectors?.map(({ name: detectorName, id: dplDetectorId }) => [
let activeColumnEntries = dplDetectors?.map(({ name: detectorName, id: dplDetectorId }) => [
detectorName,
{
name: detectorName.toUpperCase(),
Expand Down Expand Up @@ -136,7 +138,40 @@ export const createRunDetectorsAsyncQcActiveColumns = (
},
Loading: () => spinner(),
}),
profiles,
profiles: profile,
},
]) ?? []);
]) ?? [];

const filtersEntries = dplDetectors?.map(({ name: detectorName, id: dplDetectorId }) => [
detectorName,
{
name: detectorName.toUpperCase(),
visible: false,
profiles: profile,
filter: (filteringModel) => {
const filterModel = filteringModel.get(`detectorsQc[_${dplDetectorId}][notBadFraction]`);
return filterModel
? numericalComparisonFilter(filterModel, { step: 0.1, selectorPrefix: `detectorsQc-for-${dplDetectorId}-notBadFraction` })
: null;
},
},
]) ?? [];

if (activeColumnEntries.length > 0) {
activeColumnEntries = [
[
'detectorsQc',
{
name: null,
filter: ({ filteringModel }) => [
h('.section-divider', ['Detector QC', tooltip(info(), 'not-bad fraction expressed as a percentage')]),
filtersSection(filteringModel, Object.fromEntries(filtersEntries), { profile }),
],
profiles: profile,
},
], ...activeColumnEntries,
];
}

return Object.fromEntries(activeColumnEntries);
};
Loading