Regex search tables (#2956)

Support regex searches in table columns

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Deep Tailor 2021-05-05 17:50:14 -07:00 committed by GitHub
parent 0e80a5b8a0
commit 9920e67c83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 166 additions and 6 deletions

View File

@ -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);

View File

@ -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 {

View File

@ -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);

View File

@ -100,6 +100,9 @@ define([
destroy: function (element) {
component.$destroy();
component = undefined;
},
_getTable: function () {
return table;
}
};

View File

@ -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;

View File

@ -188,7 +188,17 @@
class="c-table__search"
@input="filterChanged(key)"
@clear="clearFilter(key)"
/>
>
<button
class="c-search__use-regex"
:class="{ 'is-active': enableRegexSearch[key] }"
title="Click to enable regex: enter a string with slashes, like this: /regex_exp/"
@click="toggleRegex(key)"
>
/R/
</button>
</search>
</table-column-header>
</tr>
</thead>
@ -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',

View File

@ -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);
});
});
});
});
});

View File

@ -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;
}
}
}

View File

@ -15,6 +15,7 @@
class="c-search__clear-input icon-x-in-circle"
@click="clearInput"
></a>
<slot></slot>
</div>
</template>