diff --git a/lib/public/app.css b/lib/public/app.css index bb67bc66bd..e33f0cdf52 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -718,6 +718,33 @@ label { opacity: 0.5; } +.active-filters-indicator { + border-radius: .25rem; + padding: var(--space-xs) var(--space-s) var(--space-xs) var(--space-s); + margin: 0 0 0 var(--space-s); +} + +.inactive { + opacity: 0.5; + pointer-events: none; +} + +.pulse-green { + animation: pulseGreen 2s infinite; +} + +@keyframes pulseGreen { + 0% { + box-shadow: 0 0 0px rgba(102, 255, 7, 0.6); + } + 50% { + box-shadow: 0 0 10px rgba(102, 255, 7, 0.9); + } + 100% { + box-shadow: 0 0 0px rgba(102, 255, 7, 0.6); + } +} + /** * Breakpoints : * small : x < 600 (default styles) diff --git a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js index 6dadd9f363..7bc3ae5592 100644 --- a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js @@ -76,6 +76,13 @@ export class MultiCompositionFilterModel extends FilterModel { return Object.values(this._filters).every((filter) => filter.isEmpty); } + /** + * @inheritDoc + */ + get isInactive() { + return Object.values(this._filters).every((filter) => filter.isInactive); + } + /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index 2aae7b1a10..d16f1226f7 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -77,6 +77,15 @@ export class FilterModel extends Observable { return this._visualChange$; } + /** + * States if the filter is active. By default this is equivalent to isEmpty + * + * @return {boolean} true if the filter is active + */ + get isInactive() { + return this.isEmpty; + } + /** * Utility function to register a filter model as sub-filter model * diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 251dbd07a8..174b78f37d 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -95,12 +95,7 @@ export class FilteringModel extends Observable { * @return {boolean} true if at least one filter is active */ isAnyFilterActive() { - for (const model of this._filterModels) { - if (!model.isEmpty) { - return true; - } - } - return false; + return !this._filterModels.every((model) => model.isInactive); } /** diff --git a/lib/public/components/Filters/common/RadioButtonFilterModel.js b/lib/public/components/Filters/common/RadioButtonFilterModel.js index 5e93205bfc..0aaa6e70af 100644 --- a/lib/public/components/Filters/common/RadioButtonFilterModel.js +++ b/lib/public/components/Filters/common/RadioButtonFilterModel.js @@ -22,13 +22,27 @@ export class RadioButtonFilterModel extends SelectionModel { * * @param {SelectionOption[]} [availableOptions] the list of possible operators * @param {function} [setDefault] function that selects the default from the list of available options. Selects first entry by default + * @param {boolean} [defaultIsEmpty] if true, the default selection will be treated as empty */ - constructor(availableOptions, setDefault = (options) => [options[0]]) { + constructor(availableOptions, setDefault = (options) => [options[0]], defaultIsEmpty = true) { super({ availableOptions, defaultSelection: setDefault(availableOptions), multiple: false, allowEmpty: false, }); + + this._defaultIsEmpty = defaultIsEmpty; + } + + /** + * @inheritdoc + */ + get isEmpty() { + if (this._defaultIsEmpty) { + return this.hasOnlyDefaultSelection(); + } + + return false; } } diff --git a/lib/public/components/Filters/common/filters/ToggleFilterModel.js b/lib/public/components/Filters/common/filters/ToggleFilterModel.js index b5a98fd4a4..e964815e83 100644 --- a/lib/public/components/Filters/common/filters/ToggleFilterModel.js +++ b/lib/public/components/Filters/common/filters/ToggleFilterModel.js @@ -60,4 +60,19 @@ export class ToggleFilterModel extends SelectionModel { return false; } + + /** + * Returns if the toggle filter is considered 'inactive' + * If _falseIsEmpty is true, this getter is synonymous with isEmpty + * + * @return {boolean} + */ + get isInactive() { + if (!this._falseIsEmpty) { + return this.isEmpty; + } + + // If the filter has its default selection, it should not be considered 'inactive' + return this.hasOnlyDefaultSelection(); + } } diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 8f0ca39a4b..3eafb54006 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -168,6 +168,21 @@ const pasteButtonOption = (model) => { }, 'Paste filters'); }; +/** + * A indicates if any filters are currently active on the page + * + * @param {boolean} activeFilters if true, component will turn green and glow + * @returns {Component} the active filters indicator + */ +const activeFilterIndicator = (activeFilters) => { + const innerText = `Filters ${activeFilters ? 'Active' : 'Inactive'}`; + + let element = '.active-filters-indicator.b1'; + element += activeFilters ? '.pulse-green.b-success.success' : '.inactive'; + + return h(element, innerText); +}; + /** * Return component composed of the filter popover button and a dropdown trigger * @@ -195,6 +210,7 @@ export const filtersPanelPopover = (filteringModel, filtersConfiguration, config ], ), ), + activeFilterIndicator(hasActiveFilters), ], ); }; diff --git a/lib/public/components/common/selection/SelectionModel.js b/lib/public/components/common/selection/SelectionModel.js index 9b812eabf5..20e03d4b9e 100644 --- a/lib/public/components/common/selection/SelectionModel.js +++ b/lib/public/components/common/selection/SelectionModel.js @@ -113,6 +113,15 @@ export class SelectionModel extends Observable { return selected.length === defaultSelection.length && selected.every((item) => defaultSelection.includes(item)); } + /** + * States if the filter is active. By default this is equivalent to isEmpty + * + * @return {boolean} true if the filter is active + */ + get isInactive() { + return this.isEmpty; + } + /** * Reset the selection to the default *