diff --git a/src/plugins/condition/components/inspector/StylesView.vue b/src/plugins/condition/components/inspector/StylesView.vue index 932f51e522..ebd56158c7 100644 --- a/src/plugins/condition/components/inspector/StylesView.vue +++ b/src/plugins/condition/components/inspector/StylesView.vue @@ -344,6 +344,11 @@ export default { const layoutItem = selectionItem[0].context.layoutItem; const isChildItem = selectionItem.length > 1; + if (!item && !layoutItem) { + // cases where selection is used for table cells + return; + } + if (!isChildItem) { domainObject = item; itemStyle = getApplicableStylesForItem(item); diff --git a/src/plugins/condition/utils/styleUtils.js b/src/plugins/condition/utils/styleUtils.js index f67649b901..8581cf3f15 100644 --- a/src/plugins/condition/utils/styleUtils.js +++ b/src/plugins/condition/utils/styleUtils.js @@ -104,7 +104,7 @@ export function getConsolidatedStyleValues(multipleItemStyles) { const properties = Object.keys(styleProps); properties.forEach((property) => { const values = aggregatedStyleValues[property]; - if (values.length) { + if (values && values.length) { if (values.every(value => value === values[0])) { styleValues[property] = values[0]; } else { diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js index e1a7a00ed8..f69a1f1995 100644 --- a/src/plugins/telemetryTable/TelemetryTable.js +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -94,6 +94,7 @@ define([ initialize() { if (this.domainObject.type === 'table') { this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters); + this.filters = this.domainObject.configuration.filters; this.loadComposition(); } else { this.addTelemetryObject(this.domainObject); @@ -138,7 +139,18 @@ define([ this.emit('object-added', telemetryObject); } - updateFilters() { + updateFilters(updatedFilters) { + let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + + if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { + this.filters = deepCopiedFilters; + this.clearAndResubscribe(); + } else { + this.filters = deepCopiedFilters; + } + } + + clearAndResubscribe() { this.filteredRows.clear(); this.boundedRows.clear(); Object.keys(this.subscriptions).forEach(this.unsubscribe, this); diff --git a/src/plugins/telemetryTable/TelemetryTableViewProvider.js b/src/plugins/telemetryTable/TelemetryTableViewProvider.js index f313fcda56..92008397a1 100644 --- a/src/plugins/telemetryTable/TelemetryTableViewProvider.js +++ b/src/plugins/telemetryTable/TelemetryTableViewProvider.js @@ -100,6 +100,9 @@ define([ destroy: function (element) { component.$destroy(); component = undefined; + }, + _getTable: function () { + return table; } }; diff --git a/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js b/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js index c99c1f4d07..fda46eefd0 100644 --- a/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js +++ b/src/plugins/telemetryTable/collections/FilteredTableRowCollection.js @@ -46,6 +46,7 @@ define( filter = filter.trim().toLowerCase(); let rowsToFilter = this.getRowsToFilter(columnKey, filter); + if (filter.length === 0) { delete this.columnFilters[columnKey]; } else { @@ -56,6 +57,16 @@ define( this.emit('filter'); } + setColumnRegexFilter(columnKey, filter) { + filter = filter.trim(); + + let rowsToFilter = this.masterCollection.getRows(); + + this.columnFilters[columnKey] = new RegExp(filter); + this.rows = rowsToFilter.filter(this.matchesFilters, this); + this.emit('filter'); + } + /** * @private */ @@ -71,6 +82,10 @@ define( * @private */ isSubsetOfCurrentFilter(columnKey, filter) { + if (this.columnFilters[columnKey] instanceof RegExp) { + return false; + } + return this.columnFilters[columnKey] && filter.startsWith(this.columnFilters[columnKey]) // startsWith check will otherwise fail when filter cleared @@ -97,7 +112,11 @@ define( return false; } - doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; + if (this.columnFilters[key] instanceof RegExp) { + doesMatchFilters = this.columnFilters[key].test(formattedValue); + } else { + doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; + } }); return doesMatchFilters; diff --git a/src/plugins/telemetryTable/components/table.vue b/src/plugins/telemetryTable/components/table.vue index 8bf94c6f55..8f4be187dc 100644 --- a/src/plugins/telemetryTable/components/table.vue +++ b/src/plugins/telemetryTable/components/table.vue @@ -188,7 +188,17 @@ class="c-table__search" @input="filterChanged(key)" @clear="clearFilter(key)" - /> + > + + + @@ -361,6 +371,7 @@ export default { paused: false, markedRows: [], isShowingMarkedRowsOnly: false, + enableRegexSearch: {}, hideHeaders: configuration.hideHeaders, totalNumberOfRows: 0 }; @@ -618,7 +629,16 @@ export default { this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft; }, filterChanged(columnKey) { - this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]); + if (this.enableRegexSearch[columnKey]) { + if (this.isCompleteRegex(this.filters[columnKey])) { + this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1)); + } else { + return; + } + } else { + this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]); + } + this.setHeight(); }, clearFilter(columnKey) { @@ -956,6 +976,18 @@ export default { this.$nextTick().then(this.calculateColumnWidths); }, + toggleRegex(key) { + this.$set(this.filters, key, ''); + + if (this.enableRegexSearch[key] === undefined) { + this.$set(this.enableRegexSearch, key, true); + } else { + this.$set(this.enableRegexSearch, key, !this.enableRegexSearch[key]); + } + }, + isCompleteRegex(string) { + return (string.length > 2 && string[0] === '/' && string[string.length - 1] === '/'); + }, getViewContext() { return { type: 'telemetry-table', diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js index 0c2bdf519c..cf56a55cb2 100644 --- a/src/plugins/telemetryTable/pluginSpec.js +++ b/src/plugins/telemetryTable/pluginSpec.js @@ -113,6 +113,7 @@ describe("the plugin", () => { let applicableViews; let tableViewProvider; let tableView; + let tableInstance; beforeEach(() => { testTelemetryObject = { @@ -179,6 +180,8 @@ describe("the plugin", () => { tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); tableView.show(child, true); + tableInstance = tableView._getTable(); + return telemetryPromise.then(() => Vue.nextTick()); }); @@ -228,5 +231,41 @@ describe("the plugin", () => { expect(toColumnText).toEqual(firstColumnText); }); }); + + it("Supports filtering telemetry by regular text search", () => { + tableInstance.filteredRows.setColumnFilter("some-key", "1"); + + return Vue.nextTick().then(() => { + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(filteredRowElements.length).toEqual(1); + + tableInstance.filteredRows.setColumnFilter("some-key", ""); + + return Vue.nextTick().then(() => { + let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(allRowElements.length).toEqual(3); + }); + }); + }); + + it("Supports filtering using Regex", () => { + tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$"); + + return Vue.nextTick().then(() => { + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(filteredRowElements.length).toEqual(0); + + tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value"); + + return Vue.nextTick().then(() => { + let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(allRowElements.length).toEqual(3); + }); + }); + }); }); }); diff --git a/src/ui/components/search.scss b/src/ui/components/search.scss index 9062b56861..fe615c6e8a 100644 --- a/src/ui/components/search.scss +++ b/src/ui/components/search.scss @@ -1,6 +1,11 @@ +@mixin visibleRegexButton { + opacity: 1; + padding: 1px 3px; + width: 24px; +} + .c-search { @include wrappedInput(); - padding-top: 2px; padding-bottom: 2px; @@ -9,11 +14,46 @@ content: $glyph-icon-magnify; } + &__use-regex { + // Button + $c: $colorBodyFg; + background: rgba($c, 0.2); + border: 1px solid rgba($c, 0.3); + color: $c; + border-radius: $controlCr; + font-weight: bold; + letter-spacing: 1px; + font-size: 0.8em; + margin-left: $interiorMarginSm; + min-width: 0; + opacity: 0; + order: 2; + overflow: hidden; + padding: 1px 0; + transform-origin: left; + transition: $transOut; + width: 0; + + &.is-active { + $c: $colorBtnActiveBg; + @include visibleRegexButton(); + background: rgba($c, 0.3); + border-color: $c; + color: $c; + } + } + &__clear-input { display: none; + order: 99; + padding: 1px 0; } &.is-active { + .c-search__use-regex { + margin-left: 0; + } + .c-search__clear-input { display: block; } @@ -21,6 +61,15 @@ input[type='text'], input[type='search'] { + margin-left: $interiorMargin; + order: 3; text-align: left; } + + &:hover { + .c-search__use-regex { + @include visibleRegexButton(); + transition: $transIn; + } + } } diff --git a/src/ui/components/search.vue b/src/ui/components/search.vue index 7a230ac5f8..46d069bffa 100644 --- a/src/ui/components/search.vue +++ b/src/ui/components/search.vue @@ -15,6 +15,7 @@ class="c-search__clear-input icon-x-in-circle" @click="clearInput" > +