Skip to content

Commit ae5702f

Browse files
committed
Improve search dropdowns
- Sort services and operations operations (case insensitive) - Filter options based on contains instead of starts with
1 parent 5162309 commit ae5702f

File tree

6 files changed

+82
-6
lines changed

6 files changed

+82
-6
lines changed

src/components/SearchTracePage/SearchDropdownInput.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import PropTypes from 'prop-types';
2222
import React, { Component } from 'react';
2323
import { Dropdown } from 'semantic-ui-react';
2424

25+
import regexpEscape from '../../utils/regexp-escape';
26+
2527
/**
2628
* We have to wrap the semantic ui component becuase it doesn't perform well
2729
* when there are 200+ suggestions.
@@ -33,21 +35,22 @@ export default class SearchDropdownInput extends Component {
3335
constructor(props) {
3436
super(props);
3537
this.state = {
36-
items: props.items,
3738
currentItems: props.items.slice(0, props.maxResults),
3839
};
40+
this.onSearch = this.onSearch.bind(this);
3941
}
4042
componentWillReceiveProps(nextProps) {
4143
if (this.props.items.map(i => i.text).join(',') !== nextProps.items.map(i => i.text).join(',')) {
4244
this.setState({
43-
items: nextProps.items,
4445
currentItems: nextProps.items.slice(0, nextProps.maxResults),
4546
});
4647
}
4748
}
48-
onSearch(items, v) {
49-
const { maxResults } = this.props;
50-
return this.state.items.filter(i => i.text.startsWith(v)).slice(0, maxResults);
49+
onSearch(_, searchText) {
50+
const { items, maxResults } = this.props;
51+
const rxStr = regexpEscape(searchText);
52+
const rx = new RegExp(rxStr, 'i');
53+
return items.filter(v => rx.test(v.text)).slice(0, maxResults);
5154
}
5255
render() {
5356
const { input: { value, onChange } } = this.props;
@@ -56,7 +59,7 @@ export default class SearchDropdownInput extends Component {
5659
<Dropdown
5760
value={value}
5861
text={value}
59-
search={(items, v) => this.onSearch(items, v)}
62+
search={this.onSearch}
6063
onChange={(e, { value: newValue }) => onChange(newValue)}
6164
options={currentItems}
6265
selection

src/reducers/services.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import { handleActions } from 'redux-actions';
2222

2323
import { fetchServices, fetchServiceOperations as fetchOps } from '../actions/jaeger-api';
24+
import { baseStringComparator } from '../utils/sort';
2425

2526
const initialState = {
2627
services: [],
@@ -35,6 +36,9 @@ function fetchStarted(state) {
3536

3637
function fetchServicesDone(state, { payload }) {
3738
const services = payload.data;
39+
if (Array.isArray(services)) {
40+
services.sort(baseStringComparator);
41+
}
3842
return { ...state, services, error: null, loading: false };
3943
}
4044

@@ -49,6 +53,9 @@ function fetchOpsStarted(state, { meta: { serviceName } }) {
4953

5054
function fetchOpsDone(state, { meta, payload }) {
5155
const { data: operations } = payload;
56+
if (Array.isArray(operations)) {
57+
operations.sort(baseStringComparator);
58+
}
5259
const operationsForService = { ...state.operationsForService, [meta.serviceName]: operations };
5360
return { ...state, operationsForService };
5461
}

src/utils/regexp-escape.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2017 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
export default function regexpEscape(s) {
22+
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
23+
}

src/utils/regexp-escape.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2017 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
import regexpEscape from './regexp-escape';
22+
23+
describe('regexp-escape', () => {
24+
const chars = '-/\\^$*+?.()|[]{}'.split('');
25+
chars.forEach(c => {
26+
it(`escapes "${c}" correctly`, () => {
27+
const result = regexpEscape(c);
28+
expect(result.length).toBe(2);
29+
expect(result[0]).toBe('\\');
30+
expect(result[1]).toBe(c);
31+
});
32+
});
33+
});

src/utils/sort.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1919
// THE SOFTWARE.
2020

21+
export function baseStringComparator(itemA, itemB) {
22+
return itemA.localeCompare(itemB, 'en', { sensitivity: 'base' });
23+
}
24+
2125
export function stringSortComparator(itemA, itemB) {
2226
return itemA.localeCompare(itemB);
2327
}

src/utils/sort.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import sinon from 'sinon';
2222

2323
import * as sortUtils from './sort';
2424

25+
it('baseStringComparator() provides a case-insensitive sort', () => {
26+
const arr = ['Z', 'ab', 'AC'];
27+
expect(arr.slice().sort()).toEqual(['AC', 'Z', 'ab']);
28+
expect(arr.slice().sort(sortUtils.baseStringComparator)).toEqual(['ab', 'AC', 'Z']);
29+
});
30+
2531
it('stringSortComparator() should properly sort a list of strings', () => {
2632
const arr = ['allen', 'Gustav', 'paul', 'Tim', 'abernathy', 'tucker', 'Steve', 'mike', 'John', 'Paul'];
2733

0 commit comments

Comments
 (0)