Skip to content

Commit 9920e67

Browse files
deeptailorcharlesh88akhenry
authored
Regex search tables (nasa#2956)
Support regex searches in table columns Co-authored-by: charlesh88 <[email protected]> Co-authored-by: Andrew Henry <[email protected]>
1 parent 0e80a5b commit 9920e67

File tree

9 files changed

+166
-6
lines changed

9 files changed

+166
-6
lines changed

src/plugins/condition/components/inspector/StylesView.vue

+5
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,11 @@ export default {
344344
const layoutItem = selectionItem[0].context.layoutItem;
345345
const isChildItem = selectionItem.length > 1;
346346
347+
if (!item && !layoutItem) {
348+
// cases where selection is used for table cells
349+
return;
350+
}
351+
347352
if (!isChildItem) {
348353
domainObject = item;
349354
itemStyle = getApplicableStylesForItem(item);

src/plugins/condition/utils/styleUtils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export function getConsolidatedStyleValues(multipleItemStyles) {
104104
const properties = Object.keys(styleProps);
105105
properties.forEach((property) => {
106106
const values = aggregatedStyleValues[property];
107-
if (values.length) {
107+
if (values && values.length) {
108108
if (values.every(value => value === values[0])) {
109109
styleValues[property] = values[0];
110110
} else {

src/plugins/telemetryTable/TelemetryTable.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ define([
9494
initialize() {
9595
if (this.domainObject.type === 'table') {
9696
this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
97+
this.filters = this.domainObject.configuration.filters;
9798
this.loadComposition();
9899
} else {
99100
this.addTelemetryObject(this.domainObject);
@@ -138,7 +139,18 @@ define([
138139
this.emit('object-added', telemetryObject);
139140
}
140141

141-
updateFilters() {
142+
updateFilters(updatedFilters) {
143+
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
144+
145+
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
146+
this.filters = deepCopiedFilters;
147+
this.clearAndResubscribe();
148+
} else {
149+
this.filters = deepCopiedFilters;
150+
}
151+
}
152+
153+
clearAndResubscribe() {
142154
this.filteredRows.clear();
143155
this.boundedRows.clear();
144156
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);

src/plugins/telemetryTable/TelemetryTableViewProvider.js

+3
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ define([
100100
destroy: function (element) {
101101
component.$destroy();
102102
component = undefined;
103+
},
104+
_getTable: function () {
105+
return table;
103106
}
104107
};
105108

src/plugins/telemetryTable/collections/FilteredTableRowCollection.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ define(
4646
filter = filter.trim().toLowerCase();
4747

4848
let rowsToFilter = this.getRowsToFilter(columnKey, filter);
49+
4950
if (filter.length === 0) {
5051
delete this.columnFilters[columnKey];
5152
} else {
@@ -56,6 +57,16 @@ define(
5657
this.emit('filter');
5758
}
5859

60+
setColumnRegexFilter(columnKey, filter) {
61+
filter = filter.trim();
62+
63+
let rowsToFilter = this.masterCollection.getRows();
64+
65+
this.columnFilters[columnKey] = new RegExp(filter);
66+
this.rows = rowsToFilter.filter(this.matchesFilters, this);
67+
this.emit('filter');
68+
}
69+
5970
/**
6071
* @private
6172
*/
@@ -71,6 +82,10 @@ define(
7182
* @private
7283
*/
7384
isSubsetOfCurrentFilter(columnKey, filter) {
85+
if (this.columnFilters[columnKey] instanceof RegExp) {
86+
return false;
87+
}
88+
7489
return this.columnFilters[columnKey]
7590
&& filter.startsWith(this.columnFilters[columnKey])
7691
// startsWith check will otherwise fail when filter cleared
@@ -97,7 +112,11 @@ define(
97112
return false;
98113
}
99114

100-
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
115+
if (this.columnFilters[key] instanceof RegExp) {
116+
doesMatchFilters = this.columnFilters[key].test(formattedValue);
117+
} else {
118+
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
119+
}
101120
});
102121

103122
return doesMatchFilters;

src/plugins/telemetryTable/components/table.vue

+34-2
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,17 @@
188188
class="c-table__search"
189189
@input="filterChanged(key)"
190190
@clear="clearFilter(key)"
191-
/>
191+
>
192+
193+
<button
194+
class="c-search__use-regex"
195+
:class="{ 'is-active': enableRegexSearch[key] }"
196+
title="Click to enable regex: enter a string with slashes, like this: /regex_exp/"
197+
@click="toggleRegex(key)"
198+
>
199+
/R/
200+
</button>
201+
</search>
192202
</table-column-header>
193203
</tr>
194204
</thead>
@@ -361,6 +371,7 @@ export default {
361371
paused: false,
362372
markedRows: [],
363373
isShowingMarkedRowsOnly: false,
374+
enableRegexSearch: {},
364375
hideHeaders: configuration.hideHeaders,
365376
totalNumberOfRows: 0
366377
};
@@ -618,7 +629,16 @@ export default {
618629
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
619630
},
620631
filterChanged(columnKey) {
621-
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
632+
if (this.enableRegexSearch[columnKey]) {
633+
if (this.isCompleteRegex(this.filters[columnKey])) {
634+
this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
635+
} else {
636+
return;
637+
}
638+
} else {
639+
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
640+
}
641+
622642
this.setHeight();
623643
},
624644
clearFilter(columnKey) {
@@ -956,6 +976,18 @@ export default {
956976
957977
this.$nextTick().then(this.calculateColumnWidths);
958978
},
979+
toggleRegex(key) {
980+
this.$set(this.filters, key, '');
981+
982+
if (this.enableRegexSearch[key] === undefined) {
983+
this.$set(this.enableRegexSearch, key, true);
984+
} else {
985+
this.$set(this.enableRegexSearch, key, !this.enableRegexSearch[key]);
986+
}
987+
},
988+
isCompleteRegex(string) {
989+
return (string.length > 2 && string[0] === '/' && string[string.length - 1] === '/');
990+
},
959991
getViewContext() {
960992
return {
961993
type: 'telemetry-table',

src/plugins/telemetryTable/pluginSpec.js

+39
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe("the plugin", () => {
113113
let applicableViews;
114114
let tableViewProvider;
115115
let tableView;
116+
let tableInstance;
116117

117118
beforeEach(() => {
118119
testTelemetryObject = {
@@ -179,6 +180,8 @@ describe("the plugin", () => {
179180
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
180181
tableView.show(child, true);
181182

183+
tableInstance = tableView._getTable();
184+
182185
return telemetryPromise.then(() => Vue.nextTick());
183186
});
184187

@@ -228,5 +231,41 @@ describe("the plugin", () => {
228231
expect(toColumnText).toEqual(firstColumnText);
229232
});
230233
});
234+
235+
it("Supports filtering telemetry by regular text search", () => {
236+
tableInstance.filteredRows.setColumnFilter("some-key", "1");
237+
238+
return Vue.nextTick().then(() => {
239+
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
240+
241+
expect(filteredRowElements.length).toEqual(1);
242+
243+
tableInstance.filteredRows.setColumnFilter("some-key", "");
244+
245+
return Vue.nextTick().then(() => {
246+
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
247+
248+
expect(allRowElements.length).toEqual(3);
249+
});
250+
});
251+
});
252+
253+
it("Supports filtering using Regex", () => {
254+
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$");
255+
256+
return Vue.nextTick().then(() => {
257+
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
258+
259+
expect(filteredRowElements.length).toEqual(0);
260+
261+
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value");
262+
263+
return Vue.nextTick().then(() => {
264+
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
265+
266+
expect(allRowElements.length).toEqual(3);
267+
});
268+
});
269+
});
231270
});
232271
});

src/ui/components/search.scss

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
@mixin visibleRegexButton {
2+
opacity: 1;
3+
padding: 1px 3px;
4+
width: 24px;
5+
}
6+
17
.c-search {
28
@include wrappedInput();
3-
49
padding-top: 2px;
510
padding-bottom: 2px;
611

@@ -9,18 +14,62 @@
914
content: $glyph-icon-magnify;
1015
}
1116

17+
&__use-regex {
18+
// Button
19+
$c: $colorBodyFg;
20+
background: rgba($c, 0.2);
21+
border: 1px solid rgba($c, 0.3);
22+
color: $c;
23+
border-radius: $controlCr;
24+
font-weight: bold;
25+
letter-spacing: 1px;
26+
font-size: 0.8em;
27+
margin-left: $interiorMarginSm;
28+
min-width: 0;
29+
opacity: 0;
30+
order: 2;
31+
overflow: hidden;
32+
padding: 1px 0;
33+
transform-origin: left;
34+
transition: $transOut;
35+
width: 0;
36+
37+
&.is-active {
38+
$c: $colorBtnActiveBg;
39+
@include visibleRegexButton();
40+
background: rgba($c, 0.3);
41+
border-color: $c;
42+
color: $c;
43+
}
44+
}
45+
1246
&__clear-input {
1347
display: none;
48+
order: 99;
49+
padding: 1px 0;
1450
}
1551

1652
&.is-active {
53+
.c-search__use-regex {
54+
margin-left: 0;
55+
}
56+
1757
.c-search__clear-input {
1858
display: block;
1959
}
2060
}
2161

2262
input[type='text'],
2363
input[type='search'] {
64+
margin-left: $interiorMargin;
65+
order: 3;
2466
text-align: left;
2567
}
68+
69+
&:hover {
70+
.c-search__use-regex {
71+
@include visibleRegexButton();
72+
transition: $transIn;
73+
}
74+
}
2675
}

src/ui/components/search.vue

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
class="c-search__clear-input icon-x-in-circle"
1616
@click="clearInput"
1717
></a>
18+
<slot></slot>
1819
</div>
1920
</template>
2021

0 commit comments

Comments
 (0)