From 722caf7b6d6c78507e4111fb661c217a6f9b5e44 Mon Sep 17 00:00:00 2001 From: Guust Date: Fri, 20 Feb 2026 10:23:35 +0100 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 = ''; - } }