From 722caf7b6d6c78507e4111fb661c217a6f9b5e44 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 10:23:35 +0100 Subject: [PATCH 01/32] chore: remove redundant model assignment. --- lib/public/views/Logs/Overview/LogsOverviewModel.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index cce376438b..323fcb2473 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -36,8 +36,6 @@ export class LogsOverviewModel extends Observable { constructor(model, excludeAnonymous = false) { super(); - this.model = model; - // Sub-models this._listingTagsFilterModel = new TagFilterModel(tagsProvider.items$); this._listingTagsFilterModel.observe(() => this._applyFilters()); From f161cae69844ffe9fb27239bb9193d68644705d0 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 10:37:43 +0100 Subject: [PATCH 02/32] chore: move all filters denoted as so to a filteringModel --- .../Filters/LogsFilter/author/authorFilter.js | 13 ++--- .../Logs/ActiveColumns/logsActiveColumns.js | 24 +++++++-- .../views/Logs/Overview/LogsOverviewModel.js | 53 ++++++++++--------- lib/public/views/Logs/Overview/index.js | 2 +- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/lib/public/components/Filters/LogsFilter/author/authorFilter.js b/lib/public/components/Filters/LogsFilter/author/authorFilter.js index d5fe5a7a45..778934ba23 100644 --- a/lib/public/components/Filters/LogsFilter/author/authorFilter.js +++ b/lib/public/components/Filters/LogsFilter/author/authorFilter.js @@ -55,11 +55,12 @@ export const excludeAnonymousLogAuthorToggle = (authorFilterModel) => switchInpu /** * Returns a authorFilter component with text input, reset button, and anonymous exclusion button. * - * @param {LogModel} logModel the log model object - * @returns {Component} the author filter component + * @param {LogsOverviewModel} logsOverviewModel the log overview model + * @param {FilteringModel} logsOverviewModel.filteringModel the runs overview model + * @return {Component} the filter component */ -export const authorFilter = ({ authorFilter }) => h('.flex-row.items-center.g3', [ - authorFilterTextInput(authorFilter), - resetAuthorFilterButton(authorFilter), - excludeAnonymousLogAuthorToggle(authorFilter), +export const authorFilter = ({ filteringModel }) => h('.flex-row.items-center.g3', [ + authorFilterTextInput(filteringModel.get('authorFilter')), + resetAuthorFilterButton(filteringModel.get('authorFilter')), + excludeAnonymousLogAuthorToggle(filteringModel.get('authorFilter')), ]); diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index c43b04b917..45d626777a 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -71,8 +71,16 @@ export const logsActiveColumns = { visible: true, sortable: true, size: 'w-30', - filter: ({ titleFilter }) => textFilter( - titleFilter, + + /** + * Title filter component + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => textFilter( + filteringModel.get('titleFilter'), { id: 'titleFilterText', class: 'w-75 mt1', @@ -92,8 +100,16 @@ export const logsActiveColumns = { name: 'Content', visible: false, size: 'w-10', - filter: ({ contentFilter }) => textFilter( - contentFilter, + + /** + * Content filter component + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => textFilter( + filteringModel.get('contentFilter'), { id: 'contentFilterText', class: 'w-75 mt1', diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 323fcb2473..afb61a725a 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -20,6 +20,7 @@ import { AuthorFilterModel } from '../../../components/Filters/LogsFilter/author import { PaginationModel } from '../../../components/Pagination/PaginationModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { tagsProvider } from '../../../services/tag/tagsProvider.js'; +import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; /** * Model representing handlers for log entries page @@ -36,6 +37,12 @@ export class LogsOverviewModel extends Observable { constructor(model, excludeAnonymous = false) { super(); + this._filteringModel = new FilteringModel({ + authorFilter: new AuthorFilterModel(), + titleFilter: new FilterInputModel(), + contentFilter: new FilterInputModel(), + }); + // Sub-models this._listingTagsFilterModel = new TagFilterModel(tagsProvider.items$); this._listingTagsFilterModel.observe(() => this._applyFilters()); @@ -49,16 +56,6 @@ export class LogsOverviewModel extends Observable { this._pagination.observe(() => this.fetchLogs()); this._pagination.itemsPerPageSelector$.observe(() => this.notify()); - // Filtering models - this._authorFilter = new AuthorFilterModel(); - this._registerFilter(this._authorFilter); - - this._titleFilter = new FilterInputModel(); - this._registerFilter(this._titleFilter); - - this._contentFilter = new FilterInputModel(); - this._registerFilter(this._contentFilter); - this._logs = RemoteData.NotAsked(); const updateDebounceTime = () => { @@ -67,7 +64,7 @@ export class LogsOverviewModel extends Observable { model.appConfiguration$.observe(() => updateDebounceTime()); updateDebounceTime(); - excludeAnonymous && this._authorFilter.update('!Anonymous'); + excludeAnonymous && this._filteringModel.get('authorFilter').update('!Anonymous'); this.reset(false); } @@ -119,10 +116,7 @@ export class LogsOverviewModel extends Observable { * @return {undefined} */ reset(fetch = true) { - this.titleFilter.reset(); - this.contentFilter.reset(); - this.authorFilter.reset(); - + this._filteringModel.reset(); this.createdFilterFrom = ''; this.createdFilterTo = ''; @@ -153,9 +147,7 @@ export class LogsOverviewModel extends Observable { */ isAnyFilterActive() { return ( - !this._titleFilter.isEmpty - || !this._contentFilter.isEmpty - || !this._authorFilter.isEmpty + !this._filteringModel.isAnyFilterActive() || this.createdFilterFrom !== '' || this.createdFilterTo !== '' || !this.listingTagsFilterModel.isEmpty @@ -289,6 +281,15 @@ export class LogsOverviewModel extends Observable { } } + /** + * Return the model managing all filters + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; + } + /** * Return the model handling the filtering on tags * @@ -375,16 +376,18 @@ export class LogsOverviewModel extends Observable { _getFilterQueryParams() { const sortOn = this._overviewSortModel.appliedOn; const sortDirection = this._overviewSortModel.appliedDirection; - + const titleFilter = this._filteringModel.get('titleFilter'); + const contentFilter = this._filteringModel.get('contentFilter'); + const authorFilter = this._filteringModel.get('authorFilter'); return { - ...!this._titleFilter.isEmpty && { - 'filter[title]': this._titleFilter.value, + ...!titleFilter.isEmpty && { + 'filter[title]': titleFilter.value, }, - ...!this._contentFilter.isEmpty && { - 'filter[content]': this._contentFilter.value, + ...!contentFilter.isEmpty && { + 'filter[content]': contentFilter.value, }, - ...!this._authorFilter.isEmpty && { - 'filter[author]': this._authorFilter.value, + ...!authorFilter.isEmpty && { + 'filter[author]': authorFilter, }, ...this.createdFilterFrom && { 'filter[created][from]': diff --git a/lib/public/views/Logs/Overview/index.js b/lib/public/views/Logs/Overview/index.js index 012f6e7bfe..ed5c7a860c 100644 --- a/lib/public/views/Logs/Overview/index.js +++ b/lib/public/views/Logs/Overview/index.js @@ -39,7 +39,7 @@ const logOverviewScreen = ({ logs: { overviewModel: logsOverviewModel } }) => { h('#main-action-bar.flex-row.justify-between.header-container.pv2', [ h('.flex-row.g3', [ filtersPanelPopover(logsOverviewModel, logsActiveColumns), - excludeAnonymousLogAuthorToggle(logsOverviewModel.authorFilter), + excludeAnonymousLogAuthorToggle(logsOverviewModel.filteringModel.get('authorFilter')), ]), actionButtons(), ]), From 9faffaf8b3668364237ef61644cfebbc66825237 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 11:34:06 +0100 Subject: [PATCH 03/32] Make filterInputModel extend filterModel --- .../common/filters/FilterInputModel.js | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/lib/public/components/Filters/common/filters/FilterInputModel.js b/lib/public/components/Filters/common/filters/FilterInputModel.js index 8860edf61d..7e3600ec7b 100644 --- a/lib/public/components/Filters/common/filters/FilterInputModel.js +++ b/lib/public/components/Filters/common/filters/FilterInputModel.js @@ -10,48 +10,43 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { Observable } from '/js/src/index.js'; +import { FilterModel } from '../FilterModel'; /** * Model for a generic filter input */ -export class FilterInputModel extends Observable { +export class FilterInputModel extends FilterModel { /** * Constructor + * + * @param {callback} parse function called to parse a value from a raw value */ - constructor() { + constructor(parse) { super(); + this.parse = parse; this._value = null; this._raw = ''; - - this._visualChange$ = new Observable(); } /** * Define the current value of the filter * * @param {string} raw the raw value of the filter + * @override * @return {void} */ update(raw) { - const previousValues = this.value; - - this._value = this.valueFromRaw(raw); - this._raw = raw; + const value = this._parse(raw); - if (this.areValuesEquals(this.value, previousValues)) { - // Only raw value changed - this._visualChange$.notify(); - } else { + if (!this.areValuesEquals(this._value, value)) { + this._value = value; this.notify(); } } /** - * Reset the filter to its default value - * - * @return {void} + * @inheritdoc */ reset() { this._value = null; @@ -86,23 +81,10 @@ export class FilterInputModel extends Observable { } /** - * Returns the observable notified any time there is a visual change which has no impact on the actual filter value - * - * @return {Observable} the observable - */ - get visualChange$() { - return this._visualChange$; - } - - /** - * Returns the processed value from raw input - * - * @param {string} raw the raw input value - * @return {*} the processed value - * @protected + * @inheritdoc */ - valueFromRaw(raw) { - return raw.trim(); + get normalized() { + return this.value; } /** From 247943b153c996a6db1102740e82fb8daab9792a Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 12:49:59 +0100 Subject: [PATCH 04/32] rename inputfilter to ParsedInputFilter --- ...{FilterInputModel.js => ParsedInputFilterModel.js} | 11 ++++++----- .../components/Filters/common/filters/textFilter.js | 2 +- lib/public/views/Logs/Overview/LogsOverviewModel.js | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) rename lib/public/components/Filters/common/filters/{FilterInputModel.js => ParsedInputFilterModel.js} (90%) diff --git a/lib/public/components/Filters/common/filters/FilterInputModel.js b/lib/public/components/Filters/common/filters/ParsedInputFilterModel.js similarity index 90% rename from lib/public/components/Filters/common/filters/FilterInputModel.js rename to lib/public/components/Filters/common/filters/ParsedInputFilterModel.js index 7e3600ec7b..7cdd6dc8ef 100644 --- a/lib/public/components/Filters/common/filters/FilterInputModel.js +++ b/lib/public/components/Filters/common/filters/ParsedInputFilterModel.js @@ -10,12 +10,13 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { FilterModel } from '../FilterModel'; + +import { FilterModel } from '../FilterModel.js'; /** - * Model for a generic filter input + * Model that parses raw intput into a value */ -export class FilterInputModel extends FilterModel { +export class ParsedInputFilterModel extends FilterModel { /** * Constructor * @@ -24,9 +25,8 @@ export class FilterInputModel extends FilterModel { constructor(parse) { super(); - this.parse = parse; + this._parse = parse; this._value = null; - this._raw = ''; } /** @@ -37,6 +37,7 @@ export class FilterInputModel extends FilterModel { * @return {void} */ update(raw) { + this._raw = raw; const value = this._parse(raw); if (!this.areValuesEquals(this._value, value)) { diff --git a/lib/public/components/Filters/common/filters/textFilter.js b/lib/public/components/Filters/common/filters/textFilter.js index 6b288d54ac..529f9e7692 100644 --- a/lib/public/components/Filters/common/filters/textFilter.js +++ b/lib/public/components/Filters/common/filters/textFilter.js @@ -16,7 +16,7 @@ import { h } from '/js/src/index.js'; /** * Returns a text filter component * - * @param {FilterInputModel|TextTokensFilterModel} filterInputModel the model of the text filter + * @param {ParsedInputFilterModel|TextTokensFilterModel} filterInputModel the model of the text filter * @param {Object} attributes the additional attributes to pass to the component, such as id and classes * @return {Component} the filter component */ diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index afb61a725a..a9a851ac70 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -15,7 +15,7 @@ import { buildUrl, Observable, RemoteData } from '/js/src/index.js'; import { TagFilterModel } from '../../../components/Filters/common/TagFilterModel.js'; import { SortModel } from '../../../components/common/table/SortModel.js'; import { debounce } from '../../../utilities/debounce.js'; -import { FilterInputModel } from '../../../components/Filters/common/filters/FilterInputModel.js'; +import { ParsedInputFilterModel } from '../../../components/Filters/common/filters/ParsedInputFilterModel.js'; import { AuthorFilterModel } from '../../../components/Filters/LogsFilter/author/AuthorFilterModel.js'; import { PaginationModel } from '../../../components/Pagination/PaginationModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; @@ -39,8 +39,8 @@ export class LogsOverviewModel extends Observable { this._filteringModel = new FilteringModel({ authorFilter: new AuthorFilterModel(), - titleFilter: new FilterInputModel(), - contentFilter: new FilterInputModel(), + titleFilter: new ParsedInputFilterModel((raw) => raw.trim()), + contentFilter: new ParsedInputFilterModel((raw) => raw.trim()), }); // Sub-models From 114285d1dbeaa590c7dd3ba4a23d1d7499c4b219 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 12:51:47 +0100 Subject: [PATCH 05/32] implement authorfilter with ParsedInputFilterModel changes --- .../LogsFilter/author/AuthorFilterModel.js | 34 +++++++++---------- .../Filters/LogsFilter/author/authorFilter.js | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js index 1b7a133916..2c2294b2a2 100644 --- a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js +++ b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js @@ -11,19 +11,31 @@ * or submit itself to any jurisdiction. */ -import { FilterInputModel } from '../../common/filters/FilterInputModel.js'; +import { ParsedInputFilterModel } from '../../common/filters/ParsedInputFilterModel.js'; + +/** + * Parse raw author input into a normalized array. + * + * @param {string} raw a raw, comma-seperated string + * @returns {string} + */ +const parseAuthors = (raw) => raw + .split(',') + .map((author) => author.trim()) + .filter(Boolean) + .join(','); /** * Model to handle the state of the Author Filter */ -export class AuthorFilterModel extends FilterInputModel { +export class AuthorFilterModel extends ParsedInputFilterModel { /** * Constructor * * @constructor */ constructor() { - super(); + super(parseAuthors); } /** @@ -49,21 +61,7 @@ export class AuthorFilterModel extends FilterInputModel { this._raw += super.isEmpty ? '!Anonymous' : ', !Anonymous'; } - this._value = this.valueFromRaw(this._raw); - this.notify(); - } - - /** - * Reset the filter to its default value and notify the observers. - * - * @return {void} - */ - clear() { - if (this.isEmpty) { - return; - } - - super.reset(); + this._value = this._parse(this._raw); this.notify(); } } diff --git a/lib/public/components/Filters/LogsFilter/author/authorFilter.js b/lib/public/components/Filters/LogsFilter/author/authorFilter.js index 778934ba23..60efee8106 100644 --- a/lib/public/components/Filters/LogsFilter/author/authorFilter.js +++ b/lib/public/components/Filters/LogsFilter/author/authorFilter.js @@ -36,7 +36,7 @@ const authorFilterTextInput = (authorFilterModel) => h('input.w-40', { */ const resetAuthorFilterButton = (authorFilterModel) => h( '.btn.btn-pill.f7', - { disabled: authorFilterModel.isEmpty, onclick: () => authorFilterModel.clear() }, + { disabled: authorFilterModel.isEmpty, onclick: () => authorFilterModel.reset() }, iconX(), ); From f39f7ae408ee29919f55fd70e1eeb9de61c3ffc8 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 13:18:35 +0100 Subject: [PATCH 06/32] re-implement filter --- lib/public/views/Logs/Overview/LogsOverviewModel.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index a9a851ac70..e5bc4b3251 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -43,6 +43,9 @@ export class LogsOverviewModel extends Observable { contentFilter: new ParsedInputFilterModel((raw) => raw.trim()), }); + this._filteringModel.observe(() => this._applyFilters()); + this._filteringModel.visualChange$.bubbleTo(this); + // Sub-models this._listingTagsFilterModel = new TagFilterModel(tagsProvider.items$); this._listingTagsFilterModel.observe(() => this._applyFilters()); @@ -387,7 +390,7 @@ export class LogsOverviewModel extends Observable { 'filter[content]': contentFilter.value, }, ...!authorFilter.isEmpty && { - 'filter[author]': authorFilter, + 'filter[author]': authorFilter.value, }, ...this.createdFilterFrom && { 'filter[created][from]': From df1fea32a8bd0e2dd6ddf13c7a5c3f65ac7a18c0 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 13:40:27 +0100 Subject: [PATCH 07/32] add tag-filters to the filtering object --- .../Logs/ActiveColumns/logsActiveColumns.js | 6 ++-- .../views/Logs/Overview/LogsOverviewModel.js | 36 ++++--------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 45d626777a..28e6cf0c7a 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -153,10 +153,12 @@ export const logsActiveColumns = { /** * Tag filter component - * @param {LogsOverviewModel} logsModel the log model + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model * @return {Component} the filter component */ - filter: (logsModel) => tagFilter(logsModel.listingTagsFilterModel), + filter: ({ filteringModel }) => tagFilter(filteringModel.get('listingTagsFilterModel')), balloon: true, profiles: [profiles.none, 'embeded'], }, diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index e5bc4b3251..43991f7ef2 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -41,16 +41,13 @@ export class LogsOverviewModel extends Observable { authorFilter: new AuthorFilterModel(), titleFilter: new ParsedInputFilterModel((raw) => raw.trim()), contentFilter: new ParsedInputFilterModel((raw) => raw.trim()), + listingTagsFilterModel: new TagFilterModel(tagsProvider.items$), }); this._filteringModel.observe(() => this._applyFilters()); this._filteringModel.visualChange$.bubbleTo(this); // Sub-models - this._listingTagsFilterModel = new TagFilterModel(tagsProvider.items$); - this._listingTagsFilterModel.observe(() => this._applyFilters()); - this._listingTagsFilterModel.visualChange$.bubbleTo(this); - this._overviewSortModel = new SortModel(); this._overviewSortModel.observe(() => this._applyFilters(true)); this._overviewSortModel.visualChange$.bubbleTo(this); @@ -123,8 +120,6 @@ export class LogsOverviewModel extends Observable { this.createdFilterFrom = ''; this.createdFilterTo = ''; - this.listingTagsFilterModel.reset(); - this.runFilterOperation = 'AND'; this.runFilterValues = []; this._runFilterRawValue = ''; @@ -153,7 +148,6 @@ export class LogsOverviewModel extends Observable { !this._filteringModel.isAnyFilterActive() || this.createdFilterFrom !== '' || this.createdFilterTo !== '' - || !this.listingTagsFilterModel.isEmpty || this.runFilterValues.length !== 0 || this.environmentFilterValues.length !== 0 || this.lhcFillFilterValues.length !== 0 @@ -293,15 +287,6 @@ export class LogsOverviewModel extends Observable { return this._filteringModel; } - /** - * Return the model handling the filtering on tags - * - * @return {TagFilterModel} the filtering model - */ - get listingTagsFilterModel() { - return this._listingTagsFilterModel; - } - /** * Returns the model handling the overview page table sort * @@ -358,17 +343,6 @@ export class LogsOverviewModel extends Observable { now ? this.fetchLogs() : this._debouncedFetchAllLogs(); } - /** - * Register a new filter model - * @param {FilterInputModel} filter the filter to register - * @return {void} - * @private - */ - _registerFilter(filter) { - filter.visualChange$.bubbleTo(this); - filter.observe(() => this._applyFilters()); - } - /** * Returns the list of URL params corresponding to the currently applied filter * @@ -382,6 +356,8 @@ export class LogsOverviewModel extends Observable { const titleFilter = this._filteringModel.get('titleFilter'); const contentFilter = this._filteringModel.get('contentFilter'); const authorFilter = this._filteringModel.get('authorFilter'); + const listingTagsFilterModel = this._filteringModel.get('listingTagsFilterModel'); + return { ...!titleFilter.isEmpty && { 'filter[title]': titleFilter.value, @@ -400,9 +376,9 @@ export class LogsOverviewModel extends Observable { 'filter[created][to]': new Date(`${this.createdFilterTo.replace(/\//g, '-')}T23:59:59.999`).getTime(), }, - ...!this.listingTagsFilterModel.isEmpty && { - 'filter[tags][values]': this.listingTagsFilterModel.selected.join(), - 'filter[tags][operation]': this.listingTagsFilterModel.combinationOperator, + ...!listingTagsFilterModel.isEmpty && { + 'filter[tags][values]': listingTagsFilterModel.selected.join(), + 'filter[tags][operation]': listingTagsFilterModel.combinationOperator, }, ...this.runFilterValues.length > 0 && { 'filter[run][values]': this.runFilterValues.join(), From 96ca85f87d0aa2715423bf4bca7d9a710a3a74c0 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 14:02:35 +0100 Subject: [PATCH 08/32] replace titleFilter and contentFilter with RawTextFilterModels --- lib/public/views/Logs/Overview/LogsOverviewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 43991f7ef2..c9398ffb68 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -15,12 +15,12 @@ import { buildUrl, Observable, RemoteData } from '/js/src/index.js'; import { TagFilterModel } from '../../../components/Filters/common/TagFilterModel.js'; import { SortModel } from '../../../components/common/table/SortModel.js'; import { debounce } from '../../../utilities/debounce.js'; -import { ParsedInputFilterModel } from '../../../components/Filters/common/filters/ParsedInputFilterModel.js'; import { AuthorFilterModel } from '../../../components/Filters/LogsFilter/author/AuthorFilterModel.js'; import { PaginationModel } from '../../../components/Pagination/PaginationModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { tagsProvider } from '../../../services/tag/tagsProvider.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; /** * Model representing handlers for log entries page @@ -39,8 +39,8 @@ export class LogsOverviewModel extends Observable { this._filteringModel = new FilteringModel({ authorFilter: new AuthorFilterModel(), - titleFilter: new ParsedInputFilterModel((raw) => raw.trim()), - contentFilter: new ParsedInputFilterModel((raw) => raw.trim()), + titleFilter: new RawTextFilterModel(), + contentFilter: new RawTextFilterModel(), listingTagsFilterModel: new TagFilterModel(tagsProvider.items$), }); From 93b3291d0703d04fbee905ab22001d1798d6663f Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 14:42:20 +0100 Subject: [PATCH 09/32] make Authorfilter an implementation of RawTextFilterModel --- .../LogsFilter/author/AuthorFilterModel.js | 30 +++++++++---------- .../Filters/LogsFilter/author/authorFilter.js | 20 ++++--------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js index 2c2294b2a2..506f52e4bb 100644 --- a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js +++ b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js @@ -11,31 +11,20 @@ * or submit itself to any jurisdiction. */ -import { ParsedInputFilterModel } from '../../common/filters/ParsedInputFilterModel.js'; - -/** - * Parse raw author input into a normalized array. - * - * @param {string} raw a raw, comma-seperated string - * @returns {string} - */ -const parseAuthors = (raw) => raw - .split(',') - .map((author) => author.trim()) - .filter(Boolean) - .join(','); +import { RawTextFilterModel } from '../../common/filters/RawTextFilterModel.js'; /** * Model to handle the state of the Author Filter */ -export class AuthorFilterModel extends ParsedInputFilterModel { +export class AuthorFilterModel extends RawTextFilterModel { /** * Constructor * * @constructor */ constructor() { - super(parseAuthors); + super(); + this._raw = ''; } /** @@ -61,7 +50,16 @@ export class AuthorFilterModel extends ParsedInputFilterModel { this._raw += super.isEmpty ? '!Anonymous' : ', !Anonymous'; } - this._value = this._parse(this._raw); + this._value = this._raw.trim(); this.notify(); } + + /** + * @inheritdoc + * @override + */ + reset() { + super.reset(); + this._raw = ''; + } } diff --git a/lib/public/components/Filters/LogsFilter/author/authorFilter.js b/lib/public/components/Filters/LogsFilter/author/authorFilter.js index 60efee8106..7cfc2b7d7e 100644 --- a/lib/public/components/Filters/LogsFilter/author/authorFilter.js +++ b/lib/public/components/Filters/LogsFilter/author/authorFilter.js @@ -14,19 +14,7 @@ import { h } from '/js/src/index.js'; import { iconX } from '/js/src/icons.js'; import { switchInput } from '../../../common/form/switchInput.js'; - -/** - * Returns a text input field that can be used to filter logs by author - * - * @param {AuthorFilterModel} authorFilterModel The author filter model object - * @returns {Component} A text box that allows the user to enter an author substring to match against all logs - */ -const authorFilterTextInput = (authorFilterModel) => h('input.w-40', { - type: 'text', - id: 'authorFilterText', - value: authorFilterModel.raw, - oninput: (e) => authorFilterModel.update(e.target.value), -}); +import { rawTextFilter } from '../../common/filters/rawTextFilter.js'; /** * Returns a button that can be used to reset the author filter. @@ -60,7 +48,11 @@ export const excludeAnonymousLogAuthorToggle = (authorFilterModel) => switchInpu * @return {Component} the filter component */ export const authorFilter = ({ filteringModel }) => h('.flex-row.items-center.g3', [ - authorFilterTextInput(filteringModel.get('authorFilter')), + rawTextFilter(filteringModel.get('authorFilter'), { + classes: ['w-40'], + id: 'authorFilterText', + value: filteringModel.get('authorFilter').raw, + }), resetAuthorFilterButton(filteringModel.get('authorFilter')), excludeAnonymousLogAuthorToggle(filteringModel.get('authorFilter')), ]); From 5a5e830615fd01dbc178f88f2048dd29747bb602 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 15:31:19 +0100 Subject: [PATCH 10/32] add runs filter to the filteringmodel --- .../Logs/ActiveColumns/logsActiveColumns.js | 23 +++++++++++++++---- .../views/Logs/Overview/LogsOverviewModel.js | 6 +++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 28e6cf0c7a..26cf700590 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -16,7 +16,6 @@ import { iconCommentSquare, iconPaperclip } from '/js/src/icons.js'; import { authorFilter } from '../../../components/Filters/LogsFilter/author/authorFilter.js'; import createdFilter from '../../../components/Filters/LogsFilter/created.js'; -import runsFilter from '../../../components/Filters/LogsFilter/runs.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { frontLinks } from '../../../components/common/navigation/frontLinks.js'; @@ -28,6 +27,7 @@ import { environmentFilter } from '../../../components/Filters/LogsFilter/enviro import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; import { lhcFillsFilter } from '../../../components/Filters/LogsFilter/lhcFill.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; +import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; /** * A method to display a small and simple number/icon collection as a column @@ -108,11 +108,11 @@ export const logsActiveColumns = { * @param {FilteringModel} logOverviewModel.filteringModel filtering model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textFilter( + filter: ({ filteringModel }) => rawTextFilter( filteringModel.get('contentFilter'), { id: 'contentFilterText', - class: 'w-75 mt1', + classes: ['w-75', 'mt1'], }, ), }, @@ -168,7 +168,22 @@ export const logsActiveColumns = { sortable: true, size: 'w-15', format: formatRunsList, - filter: runsFilter, + + /** + * Runs filter component + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => rawTextFilter( + filteringModel.get('run'), + { + id: 'runsFilterText', + classes: ['w-75', 'mt1'], + placeholder: 'e.g. 553203, 553221, ...', + }, + ), balloon: true, profiles: [profiles.none, 'embeded'], }, diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index c9398ffb68..fd6143d390 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -42,6 +42,7 @@ export class LogsOverviewModel extends Observable { titleFilter: new RawTextFilterModel(), contentFilter: new RawTextFilterModel(), listingTagsFilterModel: new TagFilterModel(tagsProvider.items$), + run: new RawTextFilterModel(), }); this._filteringModel.observe(() => this._applyFilters()); @@ -357,6 +358,7 @@ export class LogsOverviewModel extends Observable { const contentFilter = this._filteringModel.get('contentFilter'); const authorFilter = this._filteringModel.get('authorFilter'); const listingTagsFilterModel = this._filteringModel.get('listingTagsFilterModel'); + const run = this._filteringModel.get('run'); return { ...!titleFilter.isEmpty && { @@ -380,8 +382,8 @@ export class LogsOverviewModel extends Observable { 'filter[tags][values]': listingTagsFilterModel.selected.join(), 'filter[tags][operation]': listingTagsFilterModel.combinationOperator, }, - ...this.runFilterValues.length > 0 && { - 'filter[run][values]': this.runFilterValues.join(), + ...!run.isEmpty && { + 'filter[run][values]': run.normalized, 'filter[run][operation]': this.runFilterOperation.toLowerCase(), }, ...this.environmentFilterValues.length > 0 && { From b5e7148d07257a41fdff38097e9329740b3b9a5e Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 15:53:49 +0100 Subject: [PATCH 11/32] add environments filter to the filteringmodel --- .../Logs/ActiveColumns/logsActiveColumns.js | 18 ++++++++- .../views/Logs/Overview/LogsOverviewModel.js | 37 ++----------------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 26cf700590..eab0bcd278 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -23,7 +23,6 @@ import { tagFilter } from '../../../components/Filters/common/filters/tagFilter. import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { profiles } from '../../../components/common/table/profiles.js'; import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; -import { environmentFilter } from '../../../components/Filters/LogsFilter/environments.js'; import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; import { lhcFillsFilter } from '../../../components/Filters/LogsFilter/lhcFill.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; @@ -200,7 +199,22 @@ export const logsActiveColumns = { parameters: { environmentId: id }, }), ), - filter: environmentFilter, + + /** + * Environment filter component + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => rawTextFilter( + filteringModel.get('environments'), + { + id: 'runsFilterText', + classes: ['w-75', 'mt1'], + placeholder: 'e.g. Dxi029djX, TDI59So3d...', + }, + ), balloon: true, profiles: [profiles.none, 'embeded'], }, diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index fd6143d390..13504a6e31 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -43,6 +43,7 @@ export class LogsOverviewModel extends Observable { contentFilter: new RawTextFilterModel(), listingTagsFilterModel: new TagFilterModel(tagsProvider.items$), run: new RawTextFilterModel(), + environments: new RawTextFilterModel(), }); this._filteringModel.observe(() => this._applyFilters()); @@ -122,13 +123,7 @@ export class LogsOverviewModel extends Observable { this.createdFilterTo = ''; this.runFilterOperation = 'AND'; - this.runFilterValues = []; - this._runFilterRawValue = ''; - this.environmentFilterOperation = 'AND'; - this.environmentFilterValues = []; - this._environmentFilterRawValue = ''; - this.lhcFillFilterOperation = 'AND'; this.lhcFillFilterValues = []; this._lhcFillFilterRawValue = ''; @@ -149,8 +144,6 @@ export class LogsOverviewModel extends Observable { !this._filteringModel.isAnyFilterActive() || this.createdFilterFrom !== '' || this.createdFilterTo !== '' - || this.runFilterValues.length !== 0 - || this.environmentFilterValues.length !== 0 || this.lhcFillFilterValues.length !== 0 ); } @@ -163,29 +156,6 @@ export class LogsOverviewModel extends Observable { return this._runFilterRawValue; } - /** - * Add a run to the filter - * @param {string} rawRuns The runs to be added to the filter criteria - * @returns {undefined} - */ - setRunsFilter(rawRuns) { - this._runFilterRawValue = rawRuns; - const runs = []; - const valuesRegex = /([0-9]+),?/g; - - let match = valuesRegex.exec(rawRuns); - while (match) { - runs.push(parseInt(match[1], 10)); - match = valuesRegex.exec(rawRuns); - } - - // Allow empty runs only if raw runs is an empty string - if (runs.length > 0 || rawRuns.length === 0) { - this.runFilterValues = runs; - this._applyFilters(); - } - } - /** * Returns the raw current environment filter * @returns {string} the raw current environment filter @@ -359,6 +329,7 @@ export class LogsOverviewModel extends Observable { const authorFilter = this._filteringModel.get('authorFilter'); const listingTagsFilterModel = this._filteringModel.get('listingTagsFilterModel'); const run = this._filteringModel.get('run'); + const environments = this._filteringModel.get('environments'); return { ...!titleFilter.isEmpty && { @@ -386,8 +357,8 @@ export class LogsOverviewModel extends Observable { 'filter[run][values]': run.normalized, 'filter[run][operation]': this.runFilterOperation.toLowerCase(), }, - ...this.environmentFilterValues.length > 0 && { - 'filter[environments][values]': this.environmentFilterValues, + ...!environments.isEmpty && { + 'filter[environments][values]': environments.normalized, 'filter[environments][operation]': this.environmentFilterOperation.toLowerCase(), }, ...this.lhcFillFilterValues.length > 0 && { From 3507ba068b99427803507a5d9d16a27ab876200e Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 16:12:12 +0100 Subject: [PATCH 12/32] add lhcFills filter to the filteringmodel --- .../Logs/ActiveColumns/logsActiveColumns.js | 22 ++++++++-- .../views/Logs/Overview/LogsOverviewModel.js | 40 +++++-------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index eab0bcd278..cce5a551fd 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -24,7 +24,6 @@ import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { profiles } from '../../../components/common/table/profiles.js'; import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; -import { lhcFillsFilter } from '../../../components/Filters/LogsFilter/lhcFill.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; @@ -157,7 +156,7 @@ export const logsActiveColumns = { * @param {FilteringModel} logOverviewModel.filteringModel filtering model * @return {Component} the filter component */ - filter: ({ filteringModel }) => tagFilter(filteringModel.get('listingTagsFilterModel')), + filter: ({ filteringModel }) => tagFilter(filteringModel.get('tags')), balloon: true, profiles: [profiles.none, 'embeded'], }, @@ -210,7 +209,7 @@ export const logsActiveColumns = { filter: ({ filteringModel }) => rawTextFilter( filteringModel.get('environments'), { - id: 'runsFilterText', + id: 'environmentFilterText', classes: ['w-75', 'mt1'], placeholder: 'e.g. Dxi029djX, TDI59So3d...', }, @@ -224,7 +223,22 @@ export const logsActiveColumns = { sortable: false, size: 'w-10', format: formatLhcFillsList, - filter: lhcFillsFilter, + + /** + * LhcFills filter component + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => rawTextFilter( + filteringModel.get('lhcFills'), + { + id: 'lhcFillsFilterText', + classes: ['w-75', 'mt1'], + placeholder: 'e.g. 11392, 11383, 7625', + }, + ), balloon: true, profiles: [profiles.none, 'embeded'], }, diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 13504a6e31..1e9715d3d9 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -41,9 +41,10 @@ export class LogsOverviewModel extends Observable { authorFilter: new AuthorFilterModel(), titleFilter: new RawTextFilterModel(), contentFilter: new RawTextFilterModel(), - listingTagsFilterModel: new TagFilterModel(tagsProvider.items$), + tags: new TagFilterModel(tagsProvider.items$), run: new RawTextFilterModel(), environments: new RawTextFilterModel(), + lhcFills: new RawTextFilterModel(), }); this._filteringModel.observe(() => this._applyFilters()); @@ -125,8 +126,6 @@ export class LogsOverviewModel extends Observable { this.runFilterOperation = 'AND'; this.environmentFilterOperation = 'AND'; this.lhcFillFilterOperation = 'AND'; - this.lhcFillFilterValues = []; - this._lhcFillFilterRawValue = ''; this._pagination.reset(); @@ -144,7 +143,6 @@ export class LogsOverviewModel extends Observable { !this._filteringModel.isAnyFilterActive() || this.createdFilterFrom !== '' || this.createdFilterTo !== '' - || this.lhcFillFilterValues.length !== 0 ); } @@ -198,27 +196,6 @@ export class LogsOverviewModel extends Observable { return this._lhcFillFilterRawValue; } - /** - * Add a lhcFill to the filter - * @param {string} rawLhcFills The LHC fills to be added to the filter criteria - * @returns {void} - */ - setLhcFillsFilter(rawLhcFills) { - this._lhcFillFilterRawValue = rawLhcFills; - - // Split the lhc fills string by comma or whitespace, remove falsy values like empty strings, and convert to int - const lhcFills = rawLhcFills - .split(/[ ,]+/) - .filter(Boolean) - .map((fillNumberStr) => parseInt(fillNumberStr.trim(), 10)); - - // Allow empty lhcFills only if raw lhcFills is an empty string - if (lhcFills.length > 0 || rawLhcFills.length === 0) { - this.lhcFillFilterValues = lhcFills; - this._applyFilters(); - } - } - /** * Returns the current minimum creation datetime * @returns {Integer} The current minimum creation datetime @@ -327,9 +304,10 @@ export class LogsOverviewModel extends Observable { const titleFilter = this._filteringModel.get('titleFilter'); const contentFilter = this._filteringModel.get('contentFilter'); const authorFilter = this._filteringModel.get('authorFilter'); - const listingTagsFilterModel = this._filteringModel.get('listingTagsFilterModel'); + const tags = this._filteringModel.get('tags'); const run = this._filteringModel.get('run'); const environments = this._filteringModel.get('environments'); + const lhcFills = this._filteringModel.get('lhcFills'); return { ...!titleFilter.isEmpty && { @@ -349,9 +327,9 @@ export class LogsOverviewModel extends Observable { 'filter[created][to]': new Date(`${this.createdFilterTo.replace(/\//g, '-')}T23:59:59.999`).getTime(), }, - ...!listingTagsFilterModel.isEmpty && { - 'filter[tags][values]': listingTagsFilterModel.selected.join(), - 'filter[tags][operation]': listingTagsFilterModel.combinationOperator, + ...!tags.isEmpty && { + 'filter[tags][values]': tags.selected.join(), + 'filter[tags][operation]': tags.combinationOperator, }, ...!run.isEmpty && { 'filter[run][values]': run.normalized, @@ -361,8 +339,8 @@ export class LogsOverviewModel extends Observable { 'filter[environments][values]': environments.normalized, 'filter[environments][operation]': this.environmentFilterOperation.toLowerCase(), }, - ...this.lhcFillFilterValues.length > 0 && { - 'filter[lhcFills][values]': this.lhcFillFilterValues.join(), + ...!lhcFills.isEmpty && { + 'filter[lhcFills][values]': lhcFills.normalized, 'filter[lhcFills][operation]': this.lhcFillFilterOperation.toLowerCase(), }, ...sortOn && sortDirection && { From 9c63409629ff400d84bb76a9537540e9c164e95f Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 16:31:33 +0100 Subject: [PATCH 13/32] add created filter to the filteringmodel --- .../Logs/ActiveColumns/logsActiveColumns.js | 12 +- .../views/Logs/Overview/LogsOverviewModel.js | 133 ++---------------- 2 files changed, 20 insertions(+), 125 deletions(-) diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index cce5a551fd..2f56db5f00 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -15,7 +15,6 @@ import { h } from '/js/src/index.js'; import { iconCommentSquare, iconPaperclip } from '/js/src/icons.js'; import { authorFilter } from '../../../components/Filters/LogsFilter/author/authorFilter.js'; -import createdFilter from '../../../components/Filters/LogsFilter/created.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { frontLinks } from '../../../components/common/navigation/frontLinks.js'; @@ -26,6 +25,7 @@ import { textFilter } from '../../../components/Filters/common/filters/textFilte import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; +import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; /** * A method to display a small and simple number/icon collection as a column @@ -129,7 +129,15 @@ export const logsActiveColumns = { sortable: true, size: 'w-10', format: (timestamp) => formatTimestamp(timestamp, false), - filter: createdFilter, + + /** + * Created filter component + * + * @param {LogsOverviewModel} logOverviewModel the logs overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => timeRangeFilter(filteringModel.get('created')), profiles: { embeded: { format: (timestamp) => formatTimestamp(timestamp), diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 1e9715d3d9..ffee7549c1 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -21,6 +21,7 @@ import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice. import { tagsProvider } from '../../../services/tag/tagsProvider.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; +import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; /** * Model representing handlers for log entries page @@ -45,6 +46,7 @@ export class LogsOverviewModel extends Observable { run: new RawTextFilterModel(), environments: new RawTextFilterModel(), lhcFills: new RawTextFilterModel(), + created: new TimeRangeInputModel(), }); this._filteringModel.observe(() => this._applyFilters()); @@ -120,8 +122,6 @@ export class LogsOverviewModel extends Observable { */ reset(fetch = true) { this._filteringModel.reset(); - this.createdFilterFrom = ''; - this.createdFilterTo = ''; this.runFilterOperation = 'AND'; this.environmentFilterOperation = 'AND'; @@ -139,91 +139,7 @@ export class LogsOverviewModel extends Observable { * @returns {boolean} If any filter is active */ isAnyFilterActive() { - return ( - !this._filteringModel.isAnyFilterActive() - || this.createdFilterFrom !== '' - || this.createdFilterTo !== '' - ); - } - - /** - * Returns the current title substring filter - * @returns {string} The current title substring filter - */ - getRunsFilterRaw() { - return this._runFilterRawValue; - } - - /** - * Returns the raw current environment filter - * @returns {string} the raw current environment filter - */ - getEnvFilterRaw() { - return this._environmentFilterRawValue; - } - - /** - * Returns the current environment filter - * @returns {string[]} The current environment filter - */ - getEnvFilter() { - return this.environmentFilterValues; - } - - /** - * Sets the environment filter - * @param {string} rawEnvironments The environments to apply to the filter - * @returns {undefined} - */ - setEnvFilter(rawEnvironments) { - this._environmentFilterRawValue = rawEnvironments; - const envs = rawEnvironments - .split(/[ ,]+/) - .filter(Boolean) - .map((id) => id.trim()); - - if (envs.length > 0 || rawEnvironments.length === 0) { - this.environmentFilterValues = envs; - this._applyFilters(); - } - } - - /** - * Returns the current title substring filter - * @returns {string} The current title substring filter - */ - getLhcFillsFilterRaw() { - return this._lhcFillFilterRawValue; - } - - /** - * Returns the current minimum creation datetime - * @returns {Integer} The current minimum creation datetime - */ - getCreatedFilterFrom() { - return this.createdFilterFrom; - } - - /** - * Returns the current maximum creation datetime - * @returns {Integer} The current maximum creation datetime - */ - getCreatedFilterTo() { - return this.createdFilterTo; - } - - /** - * Set a datetime for the creation datetime filter - * @param {string} key The filter value to apply the datetime to - * @param {Object} date The datetime to be applied to the creation datetime filter - * @param {boolean} valid Whether the inserted date passes validity check - * @returns {undefined} - */ - setCreatedFilter(key, date, valid) { - if (valid) { - this[`createdFilter${key}`] = date; - this._applyFilters(); - } + return !this._filteringModel.isAnyFilterActive(); } /** @@ -244,32 +160,6 @@ export class LogsOverviewModel extends Observable { return this._overviewSortModel; } - /** - * Returns the filter model for author filter - * - * @return {FilterInputModel} the filter model - */ - get authorFilter() { - return this._authorFilter; - } - - /** - * Returns the filter model for title filter - * - * @return {FilterInputModel} the filter model - */ - get titleFilter() { - return this._titleFilter; - } - - /** - * Returns the model for body filter - * @return {FilterInputModel} the filter model - */ - get contentFilter() { - return this._contentFilter; - } - /** * Returns the pagination model * @@ -308,24 +198,21 @@ export class LogsOverviewModel extends Observable { const run = this._filteringModel.get('run'); const environments = this._filteringModel.get('environments'); const lhcFills = this._filteringModel.get('lhcFills'); + const created = this._filteringModel.get('created'); return { ...!titleFilter.isEmpty && { - 'filter[title]': titleFilter.value, + 'filter[title]': titleFilter.normalized, }, ...!contentFilter.isEmpty && { - 'filter[content]': contentFilter.value, + 'filter[content]': contentFilter.normalized, }, ...!authorFilter.isEmpty && { - 'filter[author]': authorFilter.value, - }, - ...this.createdFilterFrom && { - 'filter[created][from]': - new Date(`${this.createdFilterFrom.replace(/\//g, '-')}T00:00:00.000`).getTime(), + 'filter[author]': authorFilter.normalized, }, - ...this.createdFilterTo && { - 'filter[created][to]': - new Date(`${this.createdFilterTo.replace(/\//g, '-')}T23:59:59.999`).getTime(), + ...!created.isEmpty && { + 'filter[created][from]': created.normalized.from, + 'filter[created][to]': created.normalized.to, }, ...!tags.isEmpty && { 'filter[tags][values]': tags.selected.join(), From 5500b3e443404d669b43bd61b5104e170e973bae Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 17:23:26 +0100 Subject: [PATCH 14/32] move the sort 'filter' to fetchlogs, since it doesn't actually filter anything --- lib/public/views/Logs/Overview/LogsOverviewModel.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index ffee7549c1..fb14e9f770 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -80,6 +80,8 @@ export class LogsOverviewModel extends Observable { */ async fetchLogs() { const keepExisting = this._pagination.currentPage > 1 && this._pagination.isInfiniteScrollEnabled; + const sortOn = this._overviewSortModel.appliedOn; + const sortDirection = this._overviewSortModel.appliedDirection; if (!keepExisting) { this._logs = RemoteData.loading(); @@ -87,6 +89,9 @@ export class LogsOverviewModel extends Observable { } const params = { + ...sortOn && sortDirection && { + [`sort[${sortOn}]`]: sortDirection, + }, ...this._getFilterQueryParams(), 'page[offset]': this._pagination.firstItemOffset, 'page[limit]': this._pagination.itemsPerPage, @@ -189,8 +194,6 @@ export class LogsOverviewModel extends Observable { * @private */ _getFilterQueryParams() { - const sortOn = this._overviewSortModel.appliedOn; - const sortDirection = this._overviewSortModel.appliedDirection; const titleFilter = this._filteringModel.get('titleFilter'); const contentFilter = this._filteringModel.get('contentFilter'); const authorFilter = this._filteringModel.get('authorFilter'); @@ -230,9 +233,6 @@ export class LogsOverviewModel extends Observable { 'filter[lhcFills][values]': lhcFills.normalized, 'filter[lhcFills][operation]': this.lhcFillFilterOperation.toLowerCase(), }, - ...sortOn && sortDirection && { - [`sort[${sortOn}]`]: sortDirection, - }, }; } } From a09ae8b4922e4f43c44bfa458a3edf902e8dbd41 Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 16:56:12 +0100 Subject: [PATCH 15/32] change filter to rawTextFilter for title --- lib/public/views/Logs/ActiveColumns/logsActiveColumns.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 2f56db5f00..21402ca680 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -21,7 +21,6 @@ import { frontLinks } from '../../../components/common/navigation/frontLinks.js' import { tagFilter } from '../../../components/Filters/common/filters/tagFilter.js'; import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { profiles } from '../../../components/common/table/profiles.js'; -import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; @@ -77,7 +76,7 @@ export const logsActiveColumns = { * @param {FilteringModel} logOverviewModel.filteringModel filtering model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textFilter( + filter: ({ filteringModel }) => rawTextFilter( filteringModel.get('titleFilter'), { id: 'titleFilterText', From 512999b8c7e21b379d805d5d6edb0039ab28f93b Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 16:58:51 +0100 Subject: [PATCH 16/32] fix test by changing event type to 'change' --- test/public/logs/overview.test.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index 39119d7ef1..14ec5e1700 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -91,16 +91,13 @@ module.exports = () => { it('can filter by log title', async () => { await waitForTableLength(page, 10); - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#titleFilterText'); - - await fillInput(page, '#titleFilterText', 'first'); + await openFilteringPanel(page) + await fillInput(page, '#titleFilterText', 'first', ['change']); await waitForTableLength(page, 1); - await fillInput(page, '#titleFilterText', 'bogusbogusbogus'); + await fillInput(page, '#titleFilterText', 'bogusbogusbogus', ['change']); await waitForEmptyTable(page); - - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('should successfully provide an input to filter on log content', async () => { From 0f15812b68ac49429ac953ec16a84ecd53b6c709 Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 17:08:30 +0100 Subject: [PATCH 17/32] fix content and author tests --- test/public/logs/overview.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index 14ec5e1700..91dfd04806 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -103,29 +103,29 @@ module.exports = () => { it('should successfully provide an input to filter on log content', async () => { await waitForTableLength(page, 10); - await fillInput(page, '#contentFilterText', 'particle'); + await fillInput(page, '#contentFilterText', 'particle', ['change']); await waitForTableLength(page, 2); - await fillInput(page, '#titleFilterText', 'this-content-do-not-exists-anywhere'); + await fillInput(page, '#titleFilterText', 'this-content-do-not-exists-anywhere', ['change']); await waitForEmptyTable(page); - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('can filter by log author', async () => { await waitForTableLength(page, 10); - await fillInput(page, '#authorFilterText', 'Jane'); + await fillInput(page, '#authorFilterText', 'Jane', ['change']); await waitForEmptyTable(page); - await pressElement(page, '#reset-filters'); + await resetFilters(page); await waitForTableLength(page, 10); - await fillInput(page, '#authorFilterText', 'John'); + await fillInput(page, '#authorFilterText', 'John', ['change']); await waitForTableLength(page, 5); - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('should successfully provide an easy-to-access button to filter in/out anonymous logs', async () => { From 7b641ca3768baf1a3cea85c4b1f81ee9998b3cac Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 17:10:40 +0100 Subject: [PATCH 18/32] import openFilteringPanel and resetFilters --- test/public/logs/overview.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index 91dfd04806..20c95db194 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -34,6 +34,8 @@ const { waitForEmptyTable, waitForTableTotalRowsCountToEqual, waitForTableFirstRowIndexToEqual, + openFilteringPanel, + resetFilters, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); From 5ab82185a6e414d5788ca402f4ef56b37df6d592 Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 17:50:37 +0100 Subject: [PATCH 19/32] fix createdAt filter test --- test/public/logs/overview.test.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index 20c95db194..6d438ff053 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -153,17 +153,25 @@ module.exports = () => { }); it('can filter by creation date', async () => { - await pressElement(page, '#openFilterToggle'); + await openFilteringPanel(page); + + const popoverTrigger = '.createdAt-filter .popover-trigger'; + const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); await waitForTableTotalRowsCountToEqual(page, 119); - // Insert a minimum date into the filter + const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); + const limit = '2020-02-02'; - await fillInput(page, '#createdFilterFrom', limit); - await fillInput(page, '#createdFilterTo', limit); - await waitForTableLength(page, 1); + + await fillInput(page, fromDateSelector, limit, ['change']); + await fillInput(page, toDateSelector, limit, ['change']); + await fillInput(page, fromTimeSelector, '11:00', ['change']); + await fillInput(page, toTimeSelector, '12:00', ['change']); - await pressElement(page, '#reset-filters'); + await waitForTableLength(page, 1); + await openFilteringPanel(page); + await resetFilters(page); }); it('can filter by tags', async () => { @@ -190,7 +198,7 @@ module.exports = () => { await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); await waitForTableLength(page, 3); - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('can filter by environments', async () => { From 4c96d73705977f970ff49812e4962eccc2050ade Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 18:07:39 +0100 Subject: [PATCH 20/32] change event types to change --- test/public/logs/overview.test.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index 6d438ff053..357584425c 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -36,6 +36,7 @@ const { waitForTableFirstRowIndexToEqual, openFilteringPanel, resetFilters, + getPeriodInputsSelectors, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -204,16 +205,14 @@ module.exports = () => { it('can filter by environments', async () => { await waitForTableLength(page, 10); - await fillInput(page, '.environments-filter input', '8E4aZTjY'); + await fillInput(page, '.environments-filter input', '8E4aZTjY', ['change']); await waitForTableLength(page, 3); - - await pressElement(page, '#reset-filters'); + await resetFilters(page); await waitForTableLength(page, 10); - await fillInput(page, '.environments-filter input', 'abcdefgh'); + await fillInput(page, '.environments-filter input', 'abcdefgh', ['change']); await waitForEmptyTable(page); - - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('can search for tag in the dropdown', async () => { @@ -241,31 +240,29 @@ module.exports = () => { await waitForTableLength(page, 10); // Insert some text into the filter - await fillInput(page, '#runsFilterText', '1, 2'); + await fillInput(page, '#runsFilterText', '1, 2', ['change']); await waitForTableLength(page, 2); + await resetFilters(page); - await pressElement(page, '#reset-filters'); await waitForTableLength(page, 10); - await fillInput(page, '#runsFilterText', '1234567890'); + await fillInput(page, '#runsFilterText', '1234567890', ['change']); await waitForEmptyTable(page); - - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('can filter by lhc fill number', async () => { await waitForTableLength(page, 10); - await fillInput(page, '#lhcFillsFilter', '1, 6'); + await fillInput(page, '#lhcFillsFilterText', '1, 6', ['change']); await waitForTableLength(page, 1); + await resetFilters(page); - await pressElement(page, '#reset-filters'); await waitForTableLength(page, 10); - await fillInput(page, '#lhcFillsFilter', '1234567890'); + await fillInput(page, '#lhcFillsFilterText', '1234567890', ['change']); await waitForEmptyTable(page); - - await pressElement(page, '#reset-filters'); + await resetFilters(page); }); it('can sort by columns in ascending and descending manners', async () => { From 18ce4dcc0e8d26e5cd4986bf46fb955c2967e87f Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 19:06:32 +0100 Subject: [PATCH 21/32] fix isAnyFilterActive --- lib/public/views/Logs/Overview/LogsOverviewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index fb14e9f770..23946cec78 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -144,7 +144,7 @@ export class LogsOverviewModel extends Observable { * @returns {boolean} If any filter is active */ isAnyFilterActive() { - return !this._filteringModel.isAnyFilterActive(); + return this._filteringModel.isAnyFilterActive(); } /** From 685dd38f4650aba9b4386b259c943f35f58cd7c8 Mon Sep 17 00:00:00 2001 From: Guust Date: Sat, 21 Feb 2026 19:49:24 +0100 Subject: [PATCH 22/32] remove _raw as from authorfilterModel, as it serves no purpose --- .../LogsFilter/author/AuthorFilterModel.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js index 506f52e4bb..6e76b7b3e1 100644 --- a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js +++ b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js @@ -24,7 +24,6 @@ export class AuthorFilterModel extends RawTextFilterModel { */ constructor() { super(); - this._raw = ''; } /** @@ -33,7 +32,7 @@ export class AuthorFilterModel extends RawTextFilterModel { * @return {boolean} true if '!Anonymous' is included in the raw filter string, false otherwise. */ isAnonymousExcluded() { - return this._raw.includes('!Anonymous'); + return this._value.includes('!Anonymous'); } /** @@ -43,23 +42,13 @@ export class AuthorFilterModel extends RawTextFilterModel { */ toggleAnonymousFilter() { if (this.isAnonymousExcluded()) { - this._raw = this._raw.split(',') + this._value = this._value.split(',') .filter((author) => author.trim() !== '!Anonymous') .join(','); } else { - this._raw += super.isEmpty ? '!Anonymous' : ', !Anonymous'; + this._value += super.isEmpty ? '!Anonymous' : ', !Anonymous'; } - this._value = this._raw.trim(); this.notify(); } - - /** - * @inheritdoc - * @override - */ - reset() { - super.reset(); - this._raw = ''; - } } From 4e132fdc91fe1a62f570b279c67109558317b54a Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Mon, 2 Mar 2026 12:08:00 +0100 Subject: [PATCH 23/32] chore: removed ParsedInputFilterModel --- .../common/filters/ParsedInputFilterModel.js | 102 ------------------ .../Filters/common/filters/textFilter.js | 8 +- 2 files changed, 4 insertions(+), 106 deletions(-) delete mode 100644 lib/public/components/Filters/common/filters/ParsedInputFilterModel.js diff --git a/lib/public/components/Filters/common/filters/ParsedInputFilterModel.js b/lib/public/components/Filters/common/filters/ParsedInputFilterModel.js deleted file mode 100644 index 7cdd6dc8ef..0000000000 --- a/lib/public/components/Filters/common/filters/ParsedInputFilterModel.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { FilterModel } from '../FilterModel.js'; - -/** - * Model that parses raw intput into a value - */ -export class ParsedInputFilterModel extends FilterModel { - /** - * Constructor - * - * @param {callback} parse function called to parse a value from a raw value - */ - constructor(parse) { - super(); - - this._parse = parse; - this._value = null; - } - - /** - * Define the current value of the filter - * - * @param {string} raw the raw value of the filter - * @override - * @return {void} - */ - update(raw) { - this._raw = raw; - const value = this._parse(raw); - - if (!this.areValuesEquals(this._value, value)) { - this._value = value; - this.notify(); - } - } - - /** - * @inheritdoc - */ - reset() { - this._value = null; - this._raw = ''; - } - - /** - * Returns the raw value of the filter (the user input) - * - * @return {string} the raw value - */ - get raw() { - return this._raw; - } - - /** - * Return the parsed values of the filter - * - * @return {*} the parsed values - */ - get value() { - return this._value; - } - - /** - * States if the filter has been filled - * - * @return {boolean} true if the filter has been filled - */ - get isEmpty() { - return !this.value; - } - - /** - * @inheritdoc - */ - get normalized() { - return this.value; - } - - /** - * Compares two values - * - * @param {*} first the first value - * @param {*} second the second value - * @return {boolean} true if the values are equals - * @protected - */ - areValuesEquals(first, second) { - return first === second; - } -} diff --git a/lib/public/components/Filters/common/filters/textFilter.js b/lib/public/components/Filters/common/filters/textFilter.js index 529f9e7692..d6ae0cdfa4 100644 --- a/lib/public/components/Filters/common/filters/textFilter.js +++ b/lib/public/components/Filters/common/filters/textFilter.js @@ -16,13 +16,13 @@ import { h } from '/js/src/index.js'; /** * Returns a text filter component * - * @param {ParsedInputFilterModel|TextTokensFilterModel} filterInputModel the model of the text filter + * @param {TextTokensFilterModel} textTokensFilterModel the model of the text filter * @param {Object} attributes the additional attributes to pass to the component, such as id and classes * @return {Component} the filter component */ -export const textFilter = (filterInputModel, attributes) => h('input', { +export const textFilter = (textTokensFilterModel, attributes) => h('input', { ...attributes, type: 'text', - value: filterInputModel.raw, - oninput: (e) => filterInputModel.update(e.target.value), + value: textTokensFilterModel.raw, + oninput: (e) => textTokensFilterModel.update(e.target.value), }, ''); From caf9812fef510fc1f2ff72fbde757d4eda2661ba Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Mon, 2 Mar 2026 12:10:55 +0100 Subject: [PATCH 24/32] chore: add toLowerCase to filterQueryParam computation --- lib/public/views/Logs/Overview/LogsOverviewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 23946cec78..d85021f607 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -219,7 +219,7 @@ export class LogsOverviewModel extends Observable { }, ...!tags.isEmpty && { 'filter[tags][values]': tags.selected.join(), - 'filter[tags][operation]': tags.combinationOperator, + 'filter[tags][operation]': tags.combinationOperator.toLowerCase(), }, ...!run.isEmpty && { 'filter[run][values]': run.normalized, From 41c5a72a7c3d06d117c48d563b2ed9f1afa416d8 Mon Sep 17 00:00:00 2001 From: NarrowsProjects <150556207+NarrowsProjects@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:00:33 +0100 Subject: [PATCH 25/32] [O2B-1530] Lhc fills add sb duration filter (#2080) * Filtering by stableBeamsStart and stableBeamsEnd has been added to LHC Fills overview page * lhcFills endpoint & DTO validation modified and testing added for the aforementioned changes --------- Co-authored-by: GuustMetz Co-authored-by: Guust --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 3 + .../ActiveColumns/lhcFillsActiveColumns.js | 17 ++ .../Overview/LhcFillsOverviewModel.js | 3 + lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 14 +- test/api/lhcFills.test.js | 172 ++++++++++++++++++ .../lhcFill/GetAllLhcFillsUseCase.test.js | 56 ++++++ test/public/lhcFills/overview.test.js | 68 ++++++- 7 files changed, 323 insertions(+), 10 deletions(-) diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 3338f44517..3fe9578388 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -14,6 +14,7 @@ const Joi = require('joi'); const { validateRange, RANGE_INVALID } = require('../../../utilities/rangeUtils'); const { validateBeamTypes, BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); const { validateTimeDuration } = require('../../../utilities/validateTime'); +const { FromToFilterDto } = require('./FromToFilterDto.js'); exports.LhcFillsFilterDto = Joi.object({ hasStableBeams: Joi.boolean(), @@ -23,6 +24,8 @@ exports.LhcFillsFilterDto = Joi.object({ }), runDuration: validateTimeDuration, beamDuration: validateTimeDuration, + stableBeamsStart: FromToFilterDto, + stableBeamsEnd: FromToFilterDto, schemeName: Joi.string().trim().max(64), beamTypes: Joi.string() .trim() diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 9dbd83ae4f..b2657c8cfd 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -28,6 +28,7 @@ import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fil import { durationFilter } from '../../../components/Filters/LhcFillsFilter/durationFilter.js'; import { beamTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamTypeFilter.js'; import { schemeNameFilter } from '../../../components/Filters/LhcFillsFilter/schemeNameFilter.js'; +import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; /** * List of active columns for a lhc fills table @@ -65,6 +66,14 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (timestamp) => formatTimestamp(timestamp, false), + + /** + * Stable Beam start filter component + * + * @param {RunsOverviewModel} lhcFillsOverviewModel the lhcFills overview model + * @return {Component} the filter component + */ + filter: (lhcFillsOverviewModel) => timeRangeFilter(lhcFillsOverviewModel.filteringModel.get('stableBeamsStart').timeRangeInputModel), profiles: { lhcFill: true, environment: true, @@ -80,6 +89,14 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (timestamp) => formatTimestamp(timestamp, false), + + /** + * Stable Beam end filter component + * + * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the lhcFills overview model + * @return {Component} the filter component + */ + filter: (lhcFillsOverviewModel) => timeRangeFilter(lhcFillsOverviewModel.filteringModel.get('stableBeamsEnd').timeRangeInputModel), profiles: { lhcFill: true, environment: true, diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 910eacc644..c57ae69c25 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -19,6 +19,7 @@ import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; import { BeamTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamTypeFilterModel.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; +import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; /** * Model for the LHC fills overview page @@ -39,6 +40,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { beamDuration: new TextComparisonFilterModel(), runDuration: new TextComparisonFilterModel(), hasStableBeams: new StableBeamFilterModel(), + stableBeamsStart: new TimeRangeFilterModel(), + stableBeamsEnd: new TimeRangeFilterModel(), beamTypes: new BeamTypeFilterModel(), schemeName: new RawTextFilterModel(), }); diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 898ec5d3de..4315cf9e1a 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -47,12 +47,24 @@ class GetAllLhcFillsUseCase { let associatedStatisticsRequired = false; if (filter) { - const { hasStableBeams, fillNumbers, schemeName, beamDuration, runDuration, beamTypes } = filter; + const { hasStableBeams, fillNumbers, schemeName, beamDuration, stableBeamsStart, stableBeamsEnd, runDuration, beamTypes } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); } + if (stableBeamsStart) { + const from = stableBeamsStart.from !== undefined ? stableBeamsStart.from : 0; + const to = stableBeamsStart.to !== undefined ? stableBeamsStart.to : new Date().getTime(); + queryBuilder.where('stableBeamsStart').between(from, to); + } + + if (stableBeamsEnd) { + const from = stableBeamsEnd.from !== undefined ? stableBeamsEnd.from : 0; + const to = stableBeamsEnd.to !== undefined ? stableBeamsEnd.to : new Date().getTime(); + queryBuilder.where('stableBeamsEnd').between(from, to); + } + if (fillNumbers) { const fillNumberCriteria = splitStringToStringsTrimmed(fillNumbers, SEARCH_ITEMS_SEPARATOR); diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index f468e1c3de..dd84946b07 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -503,6 +503,7 @@ module.exports = () => { }); }); + it('should return 200 and an LHCFill array for runs duration filter, > 03:00:00', (done) => { request(server) .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>&filter[runDuration][limit]=03:00:00') @@ -519,6 +520,177 @@ module.exports = () => { done(); }); }); + + it('should return 400 when stableBeamEnd filter "from" is greater than the current time', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsEnd][from]=2647867600000') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + const { errors: [error] } = res.body; + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal('"query.filter.stableBeamsEnd.from" must be less than "now"'); + done() + }); + }); + + it('should return 400 when stableBeamStart filter "from" is greater than the current time', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsStart][from]=2647867600000') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + const { errors: [error] } = res.body; + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal('"query.filter.stableBeamsStart.from" must be less than "now"'); + done() + }); + }); + + it('should return 400 when stableBeamEnd filter "from" is greater than "to"', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsEnd][from]=1647867699999&filter[stableBeamsEnd][to]=1647867600000') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + const { errors: [error] } = res.body; + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal('"query.filter.stableBeamsEnd.to" must be greater than "ref:from"'); + done() + }); + }); + + it('should return 400 when stableBeamStart filters are strings', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsStart][from]=bogus&filter[stableBeamsStart][to]=bogus') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + const { errors } = res.body; + + expect(errors.map(e => e.detail)).to.have.members([ + '"query.filter.stableBeamsStart.from" must be a valid date', + '"query.filter.stableBeamsStart.to" must be a valid date', + ]); + + expect(errors.every(e => e.title === 'Invalid Attribute')).to.be.true; + done() + }); + }); + + it('should return 400 when stableBeamEnd filters are strings', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsEnd][from]=bogus&filter[stableBeamsEnd][to]=bogus') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + const { errors } = res.body; + + expect(errors.map(e => e.detail)).to.have.members([ + '"query.filter.stableBeamsEnd.from" must be a valid date', + '"query.filter.stableBeamsEnd.to" must be a valid date', + ]); + + expect(errors.every(e => e.title === 'Invalid Attribute')).to.be.true; + done() + }); + }); + + it('should return 400 when stableBeamStart filter "from" is greater than "to"', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsStart][from]=1647867699999&filter[stableBeamsStart][to]=1647867600000') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + const { errors: [error] } = res.body; + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal('"query.filter.stableBeamsStart.to" must be greater than "ref:from"'); + done() + }); + }); + + it('should return 200 and a LHCFill array for only "from" filters set for stableBeamStart and end', (done) => { + const fromValue = 1647867600000; + + request(server) + .get(`/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsStart][from]=${fromValue}&filter[stableBeamsEnd][from]=${fromValue}`) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(3); + res.body.data.forEach(fill => { + expect(fill.stableBeamsStart).to.be.at.least(fromValue); + expect(fill.stableBeamsEnd).to.be.at.least(fromValue); + }); + + done(); + }); + }); + + it('should return 200 and a LHCFill array for only "to" filters set for stableBeamStart and end', (done) => { + const toValue = 2000000000000; + + request(server) + .get(`/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsStart][to]=${toValue}&filter[stableBeamsEnd][to]=${toValue}`) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(4); + + res.body.data.forEach(fill => { + expect(fill.stableBeamsStart).to.be.at.most(toValue); + expect(fill.stableBeamsEnd).to.be.at.most(toValue); + }); + done(); + }); + }); + + it('should return 200 and a LHCFill array for stableBeamStart and end filter set', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[stableBeamsStart][from]=1647867600000&filter[stableBeamsStart][to]=1647867600001&filter[stableBeamsEnd][from]=1647961200000&filter[stableBeamsEnd][to]=1647961200001') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(3); + done(); + }); + }); it('should return 200 and an LHCFill array for beam types filter, correct', (done) => { request(server) diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 852c559da3..8fbb5f2781 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -317,4 +317,60 @@ module.exports = () => { expect(lhcFills).to.be.an('array').and.lengthOf(0) }) + + it('should return an array with only \'from\' values given', async () => { + getAllLhcFillsDto.query = { + filter: { + stableBeamsStart: { + from: 1647867600000, + }, + stableBeamsEnd: { + from: 1647867600000, + }, + }, + }; + + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + expect(lhcFills).to.be.an('array'); + expect(lhcFills).to.have.lengthOf(3); + }); + + it('should return an array with only \'to\' values given', async () => { + getAllLhcFillsDto.query = { + filter: { + stableBeamsStart: { + to: 2000000000000 + }, + stableBeamsEnd: { + to: 2000000000000 + }, + }, + }; + + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + expect(lhcFills).to.be.an('array'); + expect(lhcFills).to.have.lengthOf(4); + }); + + it('should return an array with fills on certain timestamps', async () => { + getAllLhcFillsDto.query = { + filter: { + stableBeamsStart: { + from: 1647867600000, + to: 1647867600000, + }, + stableBeamsEnd: { + from: 1647961200000, + to: 1647961200000, + }, + }, + }; + + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + expect(lhcFills).to.be.an('array'); + expect(lhcFills).to.have.lengthOf(3); + }); }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 2d1f72bb28..dca414570f 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -26,6 +26,8 @@ const { openFilteringPanel, expectAttributeValue, fillInput, + getPeriodInputsSelectors, + getPopoverSelector, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -161,16 +163,19 @@ module.exports = () => { }); it('fill dropdown menu should be correct', async() => { - // activate the popover - await pressElement(page, `#row6-fillNumber-text > div:nth-child(1) > div:nth-child(2)`) - await page.waitForSelector(`body > div:nth-child(3) > div:nth-child(1)`); - await expectInnerText(page, `#copy-6 > div:nth-child(1)`, 'Copy Fill Number') + const popoverTrigger = '#row6-fillNumber-text > div:nth-child(1) > div:nth-child(2)'; - await expectLink(page, 'body > div:nth-child(4) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > a:nth-child(3)', { + await pressElement(page, popoverTrigger); + await expectInnerText(page, '#copy-6 > div:nth-child(1)', 'Copy Fill Number'); + + const popoverSelector = await getPopoverSelector(await page.waitForSelector(popoverTrigger)); + + + await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { href: `http://localhost:4000/?page=log-create&lhcFillNumbers=6`, innerText: ' Add log to this fill' }) // disable the popover - await pressElement(page, `#row6-fillNumber-text > div:nth-child(1) > div:nth-child(2)`) + await pressElement(page, popoverTrigger) }) it('can set how many lhcFills are available per page', async () => { @@ -272,12 +277,14 @@ module.exports = () => { it('should successfully display filter elements', async () => { const filterSBExpect = { selector: '.stableBeams-filter .w-30', value: 'Stable Beams Only' }; const filterFillNRExpect = {selector: 'div.items-baseline:nth-child(1) > div:nth-child(1)', value: 'Fill #'}; - const filterSBDurationExpect = {selector: 'div.items-baseline:nth-child(3) > div:nth-child(1)', value: 'SB Duration'}; + const filterSBStartExpect = {selector: 'div.items-baseline:nth-child(2) > div:nth-child(1)', value: 'SB START'}; + const filterSBEndExpect = {selector: 'div.items-baseline:nth-child(3) > div:nth-child(1)', value: 'SB END'}; + const filterSBDurationExpect = {selector: 'div.items-baseline:nth-child(5) > div:nth-child(1)', value: 'SB Duration'}; const filterSBDurationPlaceholderExpect = {selector: '#beam-duration-filter-operand', value: 'e.g 16:14:15 (HH:MM:SS)'} - const filterRunDurationExpect = {selector: 'div.flex-row:nth-child(4) > div:nth-child(1)', value: 'Total runs duration'} + const filterRunDurationExpect = {selector: 'div.flex-row:nth-child(6) > div:nth-child(1)', value: 'Total runs duration'} const filterRunDurationPlaceholderExpect = {selector: '#run-duration-filter-operand', value: 'e.g 16:14:15 (HH:MM:SS)'}; const filterSBDurationOperatorExpect = { value: true }; - const filterBeamTypeExpect = {selector: 'div.flex-row:nth-child(5) > div:nth-child(1)', value: 'Beam Type'} + const filterBeamTypeExpect = {selector: 'div.flex-row:nth-child(7) > div:nth-child(1)', value: 'Beam Type'} const filterSchemeNamePlaceholderExpect = {selector: '.fillingSchemeName-filter input', value: 'e.g. Single_12b_8_1024_8_2018'} await goToPage(page, 'lhc-fill-overview'); @@ -287,6 +294,8 @@ module.exports = () => { expect(await page.evaluate(() => document.querySelector('#beam-duration-filter-operator > option:nth-child(3)').selected)).to.equal(filterSBDurationOperatorExpect.value); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); + await expectInnerText(page, filterSBStartExpect.selector, filterSBStartExpect.value); + await expectInnerText(page, filterSBEndExpect.selector, filterSBEndExpect.value); await expectInnerText(page, filterSBDurationExpect.selector, filterSBDurationExpect.value); await expectAttributeValue(page, filterSBDurationPlaceholderExpect.selector, 'placeholder', filterSBDurationPlaceholderExpect.value); await expectInnerText(page, filterRunDurationExpect.selector, filterRunDurationExpect.value); @@ -354,6 +363,47 @@ module.exports = () => { await waitForTableLength(page, 2); }); + it('should successfully apply stableBeamStart filter', async () => { + const popoverTrigger = '.stableBeamsStart-filter .popover-trigger'; + + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + await page.waitForSelector('.column-stableBeamsStart'); + await openFilteringPanel(page); + + const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); + const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); + + await fillInput(page, fromDateSelector, '2019-08-08', ['change']); + await fillInput(page, toDateSelector, '2019-08-08', ['change']); + await fillInput(page, fromTimeSelector, '10:00', ['change']); + await fillInput(page, toTimeSelector, '12:00', ['change']); + + await openFilteringPanel(page); + await pressElement(page, popoverTrigger); + await waitForTableLength(page, 1); + }); + + it('should successfully apply stableBeamEnd filter', async () => { + const popoverTrigger = '.stableBeamsEnd-filter .popover-trigger'; + + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + await page.waitForSelector('.column-stableBeamsEnd'); + await openFilteringPanel(page); + + const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); + const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); + + await fillInput(page, fromDateSelector, '2022-03-22', ['change']); + await fillInput(page, toDateSelector, '2022-03-22', ['change']); + await fillInput(page, fromTimeSelector, '01:00', ['change']); + await fillInput(page, toTimeSelector, '23:59', ['change']); + await openFilteringPanel(page); + await pressElement(page, popoverTrigger); + await waitForTableLength(page, 3); + }); + it('should successfully apply scheme name filter', async () => { const filterSchemeNameInputField= '.fillingSchemeName-filter input'; await goToPage(page, 'lhc-fill-overview'); From f38f597931600d2ad5b75f4691f42597800754a1 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:13:27 +0100 Subject: [PATCH 26/32] [O2B-1544] Fix pagination for filtered envs and add a test (#2096) * Replaced the two-query pattern with a single queryBuilder in GetAllEnvironmentsUseCase. The previous approach was redundant following Sequelize performance improvements; furthermore, the original implementation's logic was flawed which resulted in the pagination bug. --- .../environment/GetAllEnvironmentsUseCase.js | 47 ++++++------------- .../GetAllEnvironmentsUseCase.test.js | 36 ++++++++++++++ 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/lib/usecases/environment/GetAllEnvironmentsUseCase.js b/lib/usecases/environment/GetAllEnvironmentsUseCase.js index c742c53b62..83366aff4e 100644 --- a/lib/usecases/environment/GetAllEnvironmentsUseCase.js +++ b/lib/usecases/environment/GetAllEnvironmentsUseCase.js @@ -69,18 +69,11 @@ class GetAllEnvironmentsUseCase { const { filter, page = {} } = query; const { limit = ApiConfig.pagination.limit, offset = 0 } = page; - /** - * Prepare a query builder with ordering, limit and offset - * - * @return {QueryBuilder} the created query builder - */ - const prepareQueryBuilder = () => dataSource.createQueryBuilder() + const queryBuilder = dataSource.createQueryBuilder() .orderBy('updatedAt', 'desc') .limit(limit) .offset(offset); - const fetchQueryBuilder = prepareQueryBuilder(); - if (filter) { const { ids: idsExpression, @@ -90,12 +83,10 @@ class GetAllEnvironmentsUseCase { created, } = filter; - const filterQueryBuilder = prepareQueryBuilder(); - if (created) { const from = created.from !== undefined ? created.from : 0; const to = created.to !== undefined ? created.to : Date.now(); - filterQueryBuilder.where('createdAt').between(from, to); + queryBuilder.where('createdAt').between(from, to); } if (idsExpression) { @@ -103,12 +94,12 @@ class GetAllEnvironmentsUseCase { // Filter should be like with only one filter if (filters.length === 1) { - filterQueryBuilder.where('id').substring(filters[0]); + queryBuilder.where('id').substring(filters[0]); } // Filters should be exact with more than one filter if (filters.length > 1) { - filterQueryBuilder.andWhere({ id: { [Op.in]: filters } }); + queryBuilder.andWhere({ id: { [Op.in]: filters } }); } } @@ -116,12 +107,12 @@ class GetAllEnvironmentsUseCase { const filters = currentStatusExpression.split(',').map((status) => status.trim()); // Filter the environments by current status using the subquery - filterQueryBuilder.literalWhere( + queryBuilder.literalWhere( `${ENVIRONMENT_LATEST_HISTORY_ITEM_SUBQUERY} IN (:filters)`, { filters }, ); - filterQueryBuilder.includeAttribute({ + queryBuilder.includeAttribute({ query: ENVIRONMENT_LATEST_HISTORY_ITEM_SUBQUERY, alias: 'currentStatus', }); @@ -157,7 +148,7 @@ class GetAllEnvironmentsUseCase { * Use OR condition to match subsequences ending with either DESTROYED or DONE * Filter the environments by using LIKE for subsequence matching */ - filterQueryBuilder.literalWhere( + queryBuilder.literalWhere( `(${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFiltersWithDestroyed OR ` + `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFiltersWithDone)`, { @@ -166,17 +157,17 @@ class GetAllEnvironmentsUseCase { }, ); - filterQueryBuilder.includeAttribute({ + queryBuilder.includeAttribute({ query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, alias: 'statusHistory', }); } else { - filterQueryBuilder.literalWhere( + queryBuilder.literalWhere( `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFilters`, { statusFilters: `%${statusFilters.join(',')}%` }, ); - filterQueryBuilder.includeAttribute({ + queryBuilder.includeAttribute({ query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, alias: 'statusHistory', }); @@ -190,7 +181,7 @@ class GetAllEnvironmentsUseCase { // Check that the final run numbers list contains at least one valid run number if (finalRunNumberList.length > 0) { - filterQueryBuilder.include({ + queryBuilder.include({ association: 'runs', where: { // Filter should be like with only one filter and exact with more than one filter @@ -198,22 +189,12 @@ class GetAllEnvironmentsUseCase { }, }); } - }; - - const filteredEnvironmentsIds = (await EnvironmentRepository.findAll(filterQueryBuilder)).map(({ id }) => id); - // If no environments match the filter, return an empty result - if (filteredEnvironmentsIds.length === 0) { - return { - count: 0, - environments: [], - }; } - fetchQueryBuilder.where('id').oneOf(filteredEnvironmentsIds); } - fetchQueryBuilder.include({ association: 'runs' }); - fetchQueryBuilder.include({ association: 'historyItems' }); - const { count, rows } = await EnvironmentRepository.findAndCountAll(fetchQueryBuilder); + queryBuilder.include({ association: 'runs' }); + queryBuilder.include({ association: 'historyItems' }); + const { count, rows } = await EnvironmentRepository.findAndCountAll(queryBuilder); return { count, environments: rows.map((environment) => environmentAdapter.toEntity(environment)), diff --git a/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js b/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js index 96b4ee1c11..5f1e816571 100644 --- a/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js +++ b/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js @@ -225,4 +225,40 @@ module.exports = () => { expect(environments).to.be.an('array'); expect(environments.length).to.be.equal(0); // Environments from seeders }); + + it('should return correct total count and all filtered results across pages', async () => { + const totalMatchingFilter = 6; // 'RUNNING, ERROR' matches 6 environments at this point + const limit = 2; + + // First page + getAllEnvsDto.query = { page: { limit, offset: 0 }, filter: { currentStatus: 'RUNNING, ERROR' } }; + const page1 = await new GetAllEnvironmentsUseCase().execute(getAllEnvsDto); + + expect(page1.count).to.be.equal(totalMatchingFilter); + expect(page1.environments).to.be.an('array'); + expect(page1.environments.length).to.be.equal(limit); + + // Second page + getAllEnvsDto.query = { page: { limit, offset: 2 }, filter: { currentStatus: 'RUNNING, ERROR' } }; + const page2 = await new GetAllEnvironmentsUseCase().execute(getAllEnvsDto); + + expect(page2.count).to.be.equal(totalMatchingFilter); + expect(page2.environments).to.be.an('array'); + expect(page2.environments.length).to.be.equal(limit); + + // Third page + getAllEnvsDto.query = { page: { limit, offset: 4 }, filter: { currentStatus: 'RUNNING, ERROR' } }; + const page3 = await new GetAllEnvironmentsUseCase().execute(getAllEnvsDto); + + expect(page3.count).to.be.equal(totalMatchingFilter); + expect(page3.environments).to.be.an('array'); + expect(page3.environments.length).to.be.equal(limit); + + // Collect all environment IDs and verify no duplicates and all present + const allIds = [page1, page2, page3].flatMap(({ environments })=> environments.map(({ id }) => id)); + + expect(allIds.length).to.be.equal(totalMatchingFilter); + expect(new Set(allIds).size).to.be.equal(totalMatchingFilter); + expect(allIds).to.have.members(['SomeId', 'newId', 'CmCvjNbg', 'EIDO13i3D', '8E4aZTjY', 'Dxi029djX']); + }); }; From 3efdf1d668376df87e0e5f6477df4a95337b1bfd Mon Sep 17 00:00:00 2001 From: Guust Date: Thu, 5 Mar 2026 13:28:24 +0100 Subject: [PATCH 27/32] remove the combination operator from runs --- lib/domain/dtos/GetAllLogsDto.js | 7 ++----- .../Logs/ActiveColumns/logsActiveColumns.js | 2 +- .../views/Logs/Overview/LogsOverviewModel.js | 9 ++++---- lib/usecases/log/GetAllLogsUseCase.js | 21 +++++-------------- 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/lib/domain/dtos/GetAllLogsDto.js b/lib/domain/dtos/GetAllLogsDto.js index 8f6be452d7..3ac480990e 100644 --- a/lib/domain/dtos/GetAllLogsDto.js +++ b/lib/domain/dtos/GetAllLogsDto.js @@ -19,10 +19,7 @@ const { TagsFilterDto } = require('./filters/TagsFilterDto.js'); const { FromToFilterDto } = require('./filters/FromToFilterDto.js'); const { EnvironmentsFilterDto } = require('./filters/EnvironmentsFilterDto'); -const RunFilterDto = Joi.object({ - values: CustomJoi.stringArray().items(EntityIdDto).single().required(), - operation: Joi.string().valid('and', 'or').required(), -}); +const RunFilterDto = CustomJoi.stringArray().items(EntityIdDto).single().required(); const LhcFillFilterDto = Joi.object({ values: CustomJoi.stringArray().items(EntityIdDto).single().required(), @@ -36,7 +33,7 @@ const FilterDto = Joi.object({ created: FromToFilterDto, tags: TagsFilterDto, lhcFills: LhcFillFilterDto, - run: RunFilterDto, + runNumbers: RunFilterDto, origin: Joi.string() .valid('human', 'process'), parentLog: EntityIdDto, diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 21402ca680..1f941cd038 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -182,7 +182,7 @@ export const logsActiveColumns = { * @return {Component} the filter component */ filter: ({ filteringModel }) => rawTextFilter( - filteringModel.get('run'), + filteringModel.get('runNumbers'), { id: 'runsFilterText', classes: ['w-75', 'mt1'], diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index d85021f607..190ff480fe 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -43,7 +43,7 @@ export class LogsOverviewModel extends Observable { titleFilter: new RawTextFilterModel(), contentFilter: new RawTextFilterModel(), tags: new TagFilterModel(tagsProvider.items$), - run: new RawTextFilterModel(), + runNumbers: new RawTextFilterModel(), environments: new RawTextFilterModel(), lhcFills: new RawTextFilterModel(), created: new TimeRangeInputModel(), @@ -198,7 +198,7 @@ export class LogsOverviewModel extends Observable { const contentFilter = this._filteringModel.get('contentFilter'); const authorFilter = this._filteringModel.get('authorFilter'); const tags = this._filteringModel.get('tags'); - const run = this._filteringModel.get('run'); + const runNumbers = this._filteringModel.get('runNumbers'); const environments = this._filteringModel.get('environments'); const lhcFills = this._filteringModel.get('lhcFills'); const created = this._filteringModel.get('created'); @@ -221,9 +221,8 @@ export class LogsOverviewModel extends Observable { 'filter[tags][values]': tags.selected.join(), 'filter[tags][operation]': tags.combinationOperator.toLowerCase(), }, - ...!run.isEmpty && { - 'filter[run][values]': run.normalized, - 'filter[run][operation]': this.runFilterOperation.toLowerCase(), + ...!runNumbers.isEmpty && { + 'filter[runNumbers]': runNumbers.normalized, }, ...!environments.isEmpty && { 'filter[environments][values]': environments.normalized, diff --git a/lib/usecases/log/GetAllLogsUseCase.js b/lib/usecases/log/GetAllLogsUseCase.js index b1f7ea72b5..013eb98953 100644 --- a/lib/usecases/log/GetAllLogsUseCase.js +++ b/lib/usecases/log/GetAllLogsUseCase.js @@ -39,7 +39,7 @@ const { checkForFilterExclusion } = require('../common/checkForFilterExclusion.j * @return {Promise} resolves once the filter has been applied */ const applyFilter = async (dataSource, queryBuilder, filter) => { - const { title, content, author, created, origin, parentLog, rootLog, rootOnly } = filter; + const { title, content, author, created, origin, parentLog, rootLog, rootOnly, runNumbers } = filter; if (title) { queryBuilder.where('title').substring(title); @@ -112,26 +112,15 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { queryBuilder.where('id').oneOf(...logIds); } - if (filter.run?.values?.length > 0) { + if (runNumbers.length > 0) { const runQueryBuilder = dataSource.createQueryBuilder(); runQueryBuilder.include({ association: 'run', - where: { runNumber: { [Op.in]: filter.run.values } }, + where: { runNumber: { [Op.in]: runNumbers } }, }).orderBy('logId', 'asc'); - let logRuns; - switch (filter.run.operation) { - case 'and': - logRuns = await LogRunsRepository - .findAllAndGroup(runQueryBuilder); - logRuns = logRuns - .filter((logRun) => filter.run.values.every((runNumber) => logRun.runNumbers.includes(runNumber))); - break; - case 'or': - logRuns = await LogRunsRepository - .findAll(runQueryBuilder); - break; - } + let logRuns = await LogRunsRepository.findAllAndGroup(runQueryBuilder); + logRuns = logRuns.filter((logRun) => runNumbers.every((runNumber) => logRun.runNumbers.includes(runNumber))); const logIds = logRuns.map((logRun) => logRun.logId); queryBuilder.where('id').oneOf(...logIds); From fc61bea32380ee4074041cce7e461630016b1421 Mon Sep 17 00:00:00 2001 From: Guust Date: Thu, 5 Mar 2026 13:57:42 +0100 Subject: [PATCH 28/32] remove the combination operator from envirionments --- lib/domain/dtos/GetAllLogsDto.js | 6 ++--- .../dtos/filters/EnvironmentsFilterDto.js | 20 ----------------- .../Logs/ActiveColumns/logsActiveColumns.js | 2 +- .../views/Logs/Overview/LogsOverviewModel.js | 11 ++++------ lib/usecases/log/GetAllLogsUseCase.js | 22 ++++++------------- 5 files changed, 15 insertions(+), 46 deletions(-) delete mode 100644 lib/domain/dtos/filters/EnvironmentsFilterDto.js diff --git a/lib/domain/dtos/GetAllLogsDto.js b/lib/domain/dtos/GetAllLogsDto.js index 3ac480990e..02949589da 100644 --- a/lib/domain/dtos/GetAllLogsDto.js +++ b/lib/domain/dtos/GetAllLogsDto.js @@ -17,9 +17,9 @@ const PaginationDto = require('./PaginationDto'); const { CustomJoi } = require('./CustomJoi.js'); const { TagsFilterDto } = require('./filters/TagsFilterDto.js'); const { FromToFilterDto } = require('./filters/FromToFilterDto.js'); -const { EnvironmentsFilterDto } = require('./filters/EnvironmentsFilterDto'); -const RunFilterDto = CustomJoi.stringArray().items(EntityIdDto).single().required(); +const RunFilterDto = CustomJoi.stringArray().items(EntityIdDto).single(); +const EnvironmentsFilterDto = CustomJoi.stringArray().items(Joi.string()).single(); const LhcFillFilterDto = Joi.object({ values: CustomJoi.stringArray().items(EntityIdDto).single().required(), @@ -39,7 +39,7 @@ const FilterDto = Joi.object({ parentLog: EntityIdDto, rootLog: EntityIdDto, rootOnly: Joi.boolean(), - environments: EnvironmentsFilterDto, + environmentIds: EnvironmentsFilterDto, }); const SortDto = Joi.object({ diff --git a/lib/domain/dtos/filters/EnvironmentsFilterDto.js b/lib/domain/dtos/filters/EnvironmentsFilterDto.js deleted file mode 100644 index 3baa97a747..0000000000 --- a/lib/domain/dtos/filters/EnvironmentsFilterDto.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const Joi = require('joi'); -const { CustomJoi } = require('../CustomJoi.js'); - -exports.EnvironmentsFilterDto = Joi.object({ - values: CustomJoi.stringArray().items(Joi.string()).single().required(), - operation: Joi.string().valid('and', 'or').required(), -}); diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 1f941cd038..3d71b61942 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -214,7 +214,7 @@ export const logsActiveColumns = { * @return {Component} the filter component */ filter: ({ filteringModel }) => rawTextFilter( - filteringModel.get('environments'), + filteringModel.get('environmentIds'), { id: 'environmentFilterText', classes: ['w-75', 'mt1'], diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 190ff480fe..8a6940ff7b 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -44,7 +44,7 @@ export class LogsOverviewModel extends Observable { contentFilter: new RawTextFilterModel(), tags: new TagFilterModel(tagsProvider.items$), runNumbers: new RawTextFilterModel(), - environments: new RawTextFilterModel(), + environmentIds: new RawTextFilterModel(), lhcFills: new RawTextFilterModel(), created: new TimeRangeInputModel(), }); @@ -128,8 +128,6 @@ export class LogsOverviewModel extends Observable { reset(fetch = true) { this._filteringModel.reset(); - this.runFilterOperation = 'AND'; - this.environmentFilterOperation = 'AND'; this.lhcFillFilterOperation = 'AND'; this._pagination.reset(); @@ -199,7 +197,7 @@ export class LogsOverviewModel extends Observable { const authorFilter = this._filteringModel.get('authorFilter'); const tags = this._filteringModel.get('tags'); const runNumbers = this._filteringModel.get('runNumbers'); - const environments = this._filteringModel.get('environments'); + const environmentIds = this._filteringModel.get('environmentIds'); const lhcFills = this._filteringModel.get('lhcFills'); const created = this._filteringModel.get('created'); @@ -224,9 +222,8 @@ export class LogsOverviewModel extends Observable { ...!runNumbers.isEmpty && { 'filter[runNumbers]': runNumbers.normalized, }, - ...!environments.isEmpty && { - 'filter[environments][values]': environments.normalized, - 'filter[environments][operation]': this.environmentFilterOperation.toLowerCase(), + ...!environmentIds.isEmpty && { + 'filter[environmentIds]': environmentIds.normalized, }, ...!lhcFills.isEmpty && { 'filter[lhcFills][values]': lhcFills.normalized, diff --git a/lib/usecases/log/GetAllLogsUseCase.js b/lib/usecases/log/GetAllLogsUseCase.js index 013eb98953..0d1df29790 100644 --- a/lib/usecases/log/GetAllLogsUseCase.js +++ b/lib/usecases/log/GetAllLogsUseCase.js @@ -39,7 +39,7 @@ const { checkForFilterExclusion } = require('../common/checkForFilterExclusion.j * @return {Promise} resolves once the filter has been applied */ const applyFilter = async (dataSource, queryBuilder, filter) => { - const { title, content, author, created, origin, parentLog, rootLog, rootOnly, runNumbers } = filter; + const { title, content, author, created, origin, parentLog, rootLog, rootOnly, runNumbers, environmentIds } = filter; if (title) { queryBuilder.where('title').substring(title); @@ -112,7 +112,7 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { queryBuilder.where('id').oneOf(...logIds); } - if (runNumbers.length > 0) { + if (runNumbers?.length > 0) { const runQueryBuilder = dataSource.createQueryBuilder(); runQueryBuilder.include({ association: 'run', @@ -150,25 +150,17 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { queryBuilder.where('id').oneOf(...logIds); } - if (filter.environments?.values?.length > 0) { - const validEnvironments = await EnvironmentRepository.findAll({ where: { id: { [Op.in]: filter.environments.values } } }); + if (environmentIds?.length > 0) { + const validEnvironments = await EnvironmentRepository.findAll({ where: { id: { [Op.in]: environmentIds} } }); const logEnvironmentQueryBuilder = dataSource.createQueryBuilder() .where('environmentId') .oneOf(...validEnvironments.map(({ id }) => id)) .orderBy('logId', 'asc'); - let logIds; - switch (filter.environments.operation) { - case 'and': - logIds = groupByProperty(await LogEnvironmentsRepository.findAll(logEnvironmentQueryBuilder), 'logId') - .filter(({ values }) => validEnvironments.every((env) => values.some((item) => item.environmentId === env.id))) - .map(({ index }) => index); - break; - case 'or': - logIds = (await LogEnvironmentsRepository.findAll(logEnvironmentQueryBuilder)).map(({ logId }) => logId); - break; - } + const logIds = groupByProperty(await LogEnvironmentsRepository.findAll(logEnvironmentQueryBuilder), 'logId') + .filter(({ values }) => validEnvironments.every((env) => values.some((item) => item.environmentId === env.id))) + .map(({ index }) => index); queryBuilder.where('id').oneOf(...logIds); } From 9c4df68f0d91a354226a94437773a50e36d46d23 Mon Sep 17 00:00:00 2001 From: Guust Date: Thu, 5 Mar 2026 14:09:45 +0100 Subject: [PATCH 29/32] remove the combination operator from lhcFills --- lib/domain/dtos/GetAllLogsDto.js | 8 ++--- .../Logs/ActiveColumns/logsActiveColumns.js | 2 +- .../views/Logs/Overview/LogsOverviewModel.js | 9 +++--- lib/usecases/log/GetAllLogsUseCase.js | 32 +++++++++++-------- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/domain/dtos/GetAllLogsDto.js b/lib/domain/dtos/GetAllLogsDto.js index 02949589da..7a0ef08306 100644 --- a/lib/domain/dtos/GetAllLogsDto.js +++ b/lib/domain/dtos/GetAllLogsDto.js @@ -20,11 +20,7 @@ const { FromToFilterDto } = require('./filters/FromToFilterDto.js'); const RunFilterDto = CustomJoi.stringArray().items(EntityIdDto).single(); const EnvironmentsFilterDto = CustomJoi.stringArray().items(Joi.string()).single(); - -const LhcFillFilterDto = Joi.object({ - values: CustomJoi.stringArray().items(EntityIdDto).single().required(), - operation: Joi.string().valid('and', 'or').required(), -}); +const LhcFillFilterDto = CustomJoi.stringArray().items(EntityIdDto).single(); const FilterDto = Joi.object({ title: Joi.string().trim(), @@ -32,7 +28,7 @@ const FilterDto = Joi.object({ author: Joi.string().trim(), created: FromToFilterDto, tags: TagsFilterDto, - lhcFills: LhcFillFilterDto, + fillNumbers: LhcFillFilterDto, runNumbers: RunFilterDto, origin: Joi.string() .valid('human', 'process'), diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 3d71b61942..46486e5f02 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -239,7 +239,7 @@ export const logsActiveColumns = { * @return {Component} the filter component */ filter: ({ filteringModel }) => rawTextFilter( - filteringModel.get('lhcFills'), + filteringModel.get('fillNumbers'), { id: 'lhcFillsFilterText', classes: ['w-75', 'mt1'], diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 8a6940ff7b..e1f11054bd 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -45,7 +45,7 @@ export class LogsOverviewModel extends Observable { tags: new TagFilterModel(tagsProvider.items$), runNumbers: new RawTextFilterModel(), environmentIds: new RawTextFilterModel(), - lhcFills: new RawTextFilterModel(), + fillNumbers: new RawTextFilterModel(), created: new TimeRangeInputModel(), }); @@ -198,7 +198,7 @@ export class LogsOverviewModel extends Observable { const tags = this._filteringModel.get('tags'); const runNumbers = this._filteringModel.get('runNumbers'); const environmentIds = this._filteringModel.get('environmentIds'); - const lhcFills = this._filteringModel.get('lhcFills'); + const fillNumbers = this._filteringModel.get('fillNumbers'); const created = this._filteringModel.get('created'); return { @@ -225,9 +225,8 @@ export class LogsOverviewModel extends Observable { ...!environmentIds.isEmpty && { 'filter[environmentIds]': environmentIds.normalized, }, - ...!lhcFills.isEmpty && { - 'filter[lhcFills][values]': lhcFills.normalized, - 'filter[lhcFills][operation]': this.lhcFillFilterOperation.toLowerCase(), + ...!fillNumbers.isEmpty && { + 'filter[fillNumbers]': fillNumbers.normalized, }, }; } diff --git a/lib/usecases/log/GetAllLogsUseCase.js b/lib/usecases/log/GetAllLogsUseCase.js index 0d1df29790..c1972de498 100644 --- a/lib/usecases/log/GetAllLogsUseCase.js +++ b/lib/usecases/log/GetAllLogsUseCase.js @@ -39,7 +39,19 @@ const { checkForFilterExclusion } = require('../common/checkForFilterExclusion.j * @return {Promise} resolves once the filter has been applied */ const applyFilter = async (dataSource, queryBuilder, filter) => { - const { title, content, author, created, origin, parentLog, rootLog, rootOnly, runNumbers, environmentIds } = filter; + const { + title, + content, + author, + created, + origin, + parentLog, + rootLog, + rootOnly, + runNumbers, + environmentIds, + fillNumbers, + } = filter; if (title) { queryBuilder.where('title').substring(title); @@ -126,24 +138,16 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { queryBuilder.where('id').oneOf(...logIds); } - if (filter.lhcFills?.values?.length > 0) { + if (fillNumbers?.length > 0) { const logLhcFillQueryBuilder = dataSource.createQueryBuilder(); logLhcFillQueryBuilder.include({ association: 'lhcFill', - where: { fill_number: { [Op.in]: filter.lhcFills.values } }, + where: { fill_number: { [Op.in]: fillNumbers } }, }).orderBy('logId', 'asc'); - let logLhcFills; - switch (filter.lhcFills.operation) { - case 'and': - logLhcFills = await LogLhcFillsRepository.findAllAndGroup(logLhcFillQueryBuilder); - logLhcFills = logLhcFills - .filter((logLhcFill) => filter.lhcFills.values.every((fillNumber) => logLhcFill.fillNumbers.includes(fillNumber))); - break; - case 'or': - logLhcFills = await LogLhcFillsRepository.findAll(logLhcFillQueryBuilder); - break; - } + let logLhcFills = await LogLhcFillsRepository.findAllAndGroup(logLhcFillQueryBuilder); + logLhcFills = logLhcFills.filter((logLhcFill) => + fillNumbers.every((fillNumber) => logLhcFill.fillNumbers.includes(fillNumber))); const logIds = logLhcFills.map((logLhcFill) => logLhcFill.logId); From b860320b8ec411a0170d3953265cd410cc135a5b Mon Sep 17 00:00:00 2001 From: Guust Date: Thu, 5 Mar 2026 14:40:27 +0100 Subject: [PATCH 30/32] make filter computation much more compact using filteringmodel.normalize --- .../Filters/LogsFilter/author/authorFilter.js | 8 +-- .../Logs/ActiveColumns/logsActiveColumns.js | 4 +- .../views/Logs/Overview/LogsOverviewModel.js | 60 ++----------------- lib/public/views/Logs/Overview/index.js | 2 +- 4 files changed, 12 insertions(+), 62 deletions(-) diff --git a/lib/public/components/Filters/LogsFilter/author/authorFilter.js b/lib/public/components/Filters/LogsFilter/author/authorFilter.js index 7cfc2b7d7e..993ce6f957 100644 --- a/lib/public/components/Filters/LogsFilter/author/authorFilter.js +++ b/lib/public/components/Filters/LogsFilter/author/authorFilter.js @@ -48,11 +48,11 @@ export const excludeAnonymousLogAuthorToggle = (authorFilterModel) => switchInpu * @return {Component} the filter component */ export const authorFilter = ({ filteringModel }) => h('.flex-row.items-center.g3', [ - rawTextFilter(filteringModel.get('authorFilter'), { + rawTextFilter(filteringModel.get('author'), { classes: ['w-40'], id: 'authorFilterText', - value: filteringModel.get('authorFilter').raw, + value: filteringModel.get('author').raw, }), - resetAuthorFilterButton(filteringModel.get('authorFilter')), - excludeAnonymousLogAuthorToggle(filteringModel.get('authorFilter')), + resetAuthorFilterButton(filteringModel.get('author')), + excludeAnonymousLogAuthorToggle(filteringModel.get('author')), ]); diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 46486e5f02..fd91851980 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -77,7 +77,7 @@ export const logsActiveColumns = { * @return {Component} the filter component */ filter: ({ filteringModel }) => rawTextFilter( - filteringModel.get('titleFilter'), + filteringModel.get('title'), { id: 'titleFilterText', class: 'w-75 mt1', @@ -106,7 +106,7 @@ export const logsActiveColumns = { * @return {Component} the filter component */ filter: ({ filteringModel }) => rawTextFilter( - filteringModel.get('contentFilter'), + filteringModel.get('content'), { id: 'contentFilterText', classes: ['w-75', 'mt1'], diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index e1f11054bd..4cb565e9b9 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -39,9 +39,9 @@ export class LogsOverviewModel extends Observable { super(); this._filteringModel = new FilteringModel({ - authorFilter: new AuthorFilterModel(), - titleFilter: new RawTextFilterModel(), - contentFilter: new RawTextFilterModel(), + author: new AuthorFilterModel(), + title: new RawTextFilterModel(), + content: new RawTextFilterModel(), tags: new TagFilterModel(tagsProvider.items$), runNumbers: new RawTextFilterModel(), environmentIds: new RawTextFilterModel(), @@ -69,7 +69,7 @@ export class LogsOverviewModel extends Observable { model.appConfiguration$.observe(() => updateDebounceTime()); updateDebounceTime(); - excludeAnonymous && this._filteringModel.get('authorFilter').update('!Anonymous'); + excludeAnonymous && this._filteringModel.get('author').update('!Anonymous'); this.reset(false); } @@ -92,7 +92,7 @@ export class LogsOverviewModel extends Observable { ...sortOn && sortDirection && { [`sort[${sortOn}]`]: sortDirection, }, - ...this._getFilterQueryParams(), + filter: this._filteringModel.normalized, 'page[offset]': this._pagination.firstItemOffset, 'page[limit]': this._pagination.itemsPerPage, }; @@ -127,9 +127,6 @@ export class LogsOverviewModel extends Observable { */ reset(fetch = true) { this._filteringModel.reset(); - - this.lhcFillFilterOperation = 'AND'; - this._pagination.reset(); if (fetch) { @@ -183,51 +180,4 @@ export class LogsOverviewModel extends Observable { this._pagination.silentlySetCurrentPage(1); now ? this.fetchLogs() : this._debouncedFetchAllLogs(); } - - /** - * Returns the list of URL params corresponding to the currently applied filter - * - * @return {Object} the URL params - * - * @private - */ - _getFilterQueryParams() { - const titleFilter = this._filteringModel.get('titleFilter'); - const contentFilter = this._filteringModel.get('contentFilter'); - const authorFilter = this._filteringModel.get('authorFilter'); - const tags = this._filteringModel.get('tags'); - const runNumbers = this._filteringModel.get('runNumbers'); - const environmentIds = this._filteringModel.get('environmentIds'); - const fillNumbers = this._filteringModel.get('fillNumbers'); - const created = this._filteringModel.get('created'); - - return { - ...!titleFilter.isEmpty && { - 'filter[title]': titleFilter.normalized, - }, - ...!contentFilter.isEmpty && { - 'filter[content]': contentFilter.normalized, - }, - ...!authorFilter.isEmpty && { - 'filter[author]': authorFilter.normalized, - }, - ...!created.isEmpty && { - 'filter[created][from]': created.normalized.from, - 'filter[created][to]': created.normalized.to, - }, - ...!tags.isEmpty && { - 'filter[tags][values]': tags.selected.join(), - 'filter[tags][operation]': tags.combinationOperator.toLowerCase(), - }, - ...!runNumbers.isEmpty && { - 'filter[runNumbers]': runNumbers.normalized, - }, - ...!environmentIds.isEmpty && { - 'filter[environmentIds]': environmentIds.normalized, - }, - ...!fillNumbers.isEmpty && { - 'filter[fillNumbers]': fillNumbers.normalized, - }, - }; - } } diff --git a/lib/public/views/Logs/Overview/index.js b/lib/public/views/Logs/Overview/index.js index ed5c7a860c..93f58c8c40 100644 --- a/lib/public/views/Logs/Overview/index.js +++ b/lib/public/views/Logs/Overview/index.js @@ -39,7 +39,7 @@ const logOverviewScreen = ({ logs: { overviewModel: logsOverviewModel } }) => { h('#main-action-bar.flex-row.justify-between.header-container.pv2', [ h('.flex-row.g3', [ filtersPanelPopover(logsOverviewModel, logsActiveColumns), - excludeAnonymousLogAuthorToggle(logsOverviewModel.filteringModel.get('authorFilter')), + excludeAnonymousLogAuthorToggle(logsOverviewModel.filteringModel.get('author')), ]), actionButtons(), ]), From 7a000433572254f753c5379cffdaa2064f632c71 Mon Sep 17 00:00:00 2001 From: Guust Date: Thu, 5 Mar 2026 15:05:34 +0100 Subject: [PATCH 31/32] add happy-flow tests for logs api --- test/api/logs.test.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/api/logs.test.js b/test/api/logs.test.js index ada81f070e..02e7dbaeed 100644 --- a/test/api/logs.test.js +++ b/test/api/logs.test.js @@ -233,7 +233,7 @@ module.exports = () => { }); it('should successfully filter by run number', async () => { - const response = await request(server).get('/api/logs?filter[run][values]=1,2&filter[run][operation]=and'); + const response = await request(server).get('/api/logs?filter[runNumbers]=1,2'); expect(response.status).to.equal(200); expect(response.body.data).to.be.an('array'); @@ -244,6 +244,30 @@ module.exports = () => { } }); + it('should successfully filter by lhcFillNumber', async () => { + const response = await request(server).get('/api/logs?filter[fillNumbers]=1,4,6'); + expect(response.status).to.equal(200); + + expect(response.body.data).to.be.an('array'); + expect(response.body.data).to.lengthOf(1); + for (const { lhcFills } of response.body.data) { + const fillNumbers = lhcFills.map(({ fillNumber }) => fillNumber); + expect([1, 4, 6].every((fillNumber) => fillNumbers.includes(fillNumber))).to.be.true; + } + }); + + it('should successfully filter by EnvironmentIds', async () => { + const response = await request(server).get('/api/logs?filter[environmentIds]=Dxi029djX,eZF99lH6'); + expect(response.status).to.equal(200); + + expect(response.body.data).to.be.an('array'); + expect(response.body.data).to.lengthOf(1); + for (const { environments } of response.body.data) { + const environmentIds = environments.map(({ id }) => id); + expect(["Dxi029djX", "eZF99lH6"].every((environmentId) => environmentIds.includes(environmentId))).to.be.true; + } + }); + it('should successfully filter by content', async () => { const response = await request(server).get('/api/logs?filter[content]=particle'); expect(response.status).to.equal(200); From ada3eb3f9372979196d6e3e5370ee3a0d9da4cd5 Mon Sep 17 00:00:00 2001 From: Guust Date: Thu, 5 Mar 2026 15:35:18 +0100 Subject: [PATCH 32/32] fix usecase unit tests --- lib/usecases/log/GetAllLogsUseCase.js | 2 +- .../usecases/log/GetAllLogsUseCase.test.js | 52 ++++--------------- 2 files changed, 10 insertions(+), 44 deletions(-) diff --git a/lib/usecases/log/GetAllLogsUseCase.js b/lib/usecases/log/GetAllLogsUseCase.js index c1972de498..8c69a3c4b8 100644 --- a/lib/usecases/log/GetAllLogsUseCase.js +++ b/lib/usecases/log/GetAllLogsUseCase.js @@ -155,7 +155,7 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { } if (environmentIds?.length > 0) { - const validEnvironments = await EnvironmentRepository.findAll({ where: { id: { [Op.in]: environmentIds} } }); + const validEnvironments = await EnvironmentRepository.findAll({ where: { id: { [Op.in]: environmentIds } } }); const logEnvironmentQueryBuilder = dataSource.createQueryBuilder() .where('environmentId') diff --git a/test/lib/usecases/log/GetAllLogsUseCase.test.js b/test/lib/usecases/log/GetAllLogsUseCase.test.js index 61a402cdb8..d4475d2d60 100644 --- a/test/lib/usecases/log/GetAllLogsUseCase.test.js +++ b/test/lib/usecases/log/GetAllLogsUseCase.test.js @@ -73,7 +73,7 @@ module.exports = () => { it('should successfully filter on run numbers', async () => { const runNumbers = [1, 2]; - getAllLogsDto.query = { filter: { run: { operation: 'and', values: runNumbers } } }; + getAllLogsDto.query = { filter: { runNumbers } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); @@ -83,17 +83,6 @@ module.exports = () => { expect(runNumbers.every((runNumber) => relatedRunNumbers.includes(runNumber))).to.be.true; } } - - getAllLogsDto.query = { filter: { run: { operation: 'or', values: runNumbers } } }; - - { - const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); - expect(filteredResult).to.lengthOf(6); - for (const log of filteredResult) { - const relatedRunNumbers = log.runs.map(({ runNumber }) => runNumber); - expect(runNumbers.some((runNumber) => relatedRunNumbers.includes(runNumber))).to.be.true; - } - } }); it('should successfully filter on log content', async () => { @@ -117,9 +106,9 @@ module.exports = () => { }); it('should successfully filter on lhc fills', async () => { - const lhcFills = [1, 6]; + const fillNumbers = [1, 6]; - getAllLogsDto.query = { filter: { lhcFills: { operation: 'and', values: lhcFills } } }; + getAllLogsDto.query = { filter: { fillNumbers } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); expect(filteredResult).to.have.lengthOf(1); @@ -128,47 +117,24 @@ module.exports = () => { // For each returned log, check at least one of the associated fill numbers was in the filter query expect(fillNumbersPerLog.every((logFillNumbers) => - logFillNumbers.includes(lhcFills[0]) && logFillNumbers.includes(lhcFills[1]))).to.be.true; - } - - getAllLogsDto.query = { filter: { lhcFills: { operation: 'or', values: lhcFills } } }; - { - const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); - expect(filteredResult).to.have.lengthOf(3); - - const fillNumbersPerLog = filteredResult.map(({ lhcFills }) => lhcFills.map(({ fillNumber }) => fillNumber)); - - // For each returned log, check at least one of the associated fill numbers was in the filter query - expect(fillNumbersPerLog.every((logFillNumbers) => - logFillNumbers.includes(lhcFills[0]) || logFillNumbers.includes(lhcFills[1]))).to.be.true; + logFillNumbers.includes(fillNumbers[0]) && logFillNumbers.includes(fillNumbers[1]))).to.be.true; } }); it ('should successfully filter on log environment', async () => { - const environments = ['8E4aZTjY', 'eZF99lH6']; - getAllLogsDto.query = { filter: { environments: { operation: 'and', values: environments } } }; + const environmentIds = ['8E4aZTjY', 'eZF99lH6']; + getAllLogsDto.query = { filter: { environmentIds } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); expect(filteredResult).to.lengthOf(2); for (const log of filteredResult) { - const relatedEnvironments = log.environments.map(({ id }) => id); - expect(environments.every((env) => relatedEnvironments.includes(env))).to.be.true; - } - } - - getAllLogsDto.query = { filter: { environments: { operation: 'or', values: environments } } }; - - { - const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); - expect(filteredResult).to.lengthOf(5); - for (const log of filteredResult) { - const relatedEnvironments = log.environments.map(({ id }) => id); - expect(environments.some((env) => relatedEnvironments.includes(env))).to.be.true; + const relatedenvironmentIds = log.environments.map(({ id }) => id); + expect(environmentIds.every((env) => relatedenvironmentIds.includes(env))).to.be.true; } } - getAllLogsDto.query = { filter: { environments: { operation: 'and', values: ['non-existent-environment'] } } }; + getAllLogsDto.query = { filter: { environmentIds: ['non-existent-environment'] } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto);