Add sorting and filtering to flags page (v2) (#8988)

* Add sorting and filtering to flags page

Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>

* Make filter understand

Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>

* split big state object into smaller ones

Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>

* use fuzzy match and sanitize html for search results

Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>

* use fuzzy.filter

Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>

* replace fuzzy lib by @nexucis/fuzzy + fix flags issues

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* replace fuzzy by @nexucis/fuzzy in ExpressionInput.tsx

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* remove fuzzy lib from package.json

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix flags test

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* simplify the input in the fuzzy search

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* cleanup html to be easily compatible with the dark theme

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix filtering when there is no result

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* use id to fix the test

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

Co-authored-by: Dustin Hooten <dustinhooten@gmail.com>
This commit is contained in:
Augustin Husson 2021-07-01 21:15:06 +02:00 committed by GitHub
parent 441e6cd7d6
commit f72cabb437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1114 additions and 254 deletions

View File

@ -19,13 +19,13 @@
"@fortawesome/fontawesome-svg-core": "^1.2.14",
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@nexucis/fuzzy": "^0.2.2",
"@reach/router": "^1.2.1",
"bootstrap": "^4.6.0",
"codemirror-promql": "^0.16.0",
"css.escape": "^1.5.1",
"downshift": "^3.4.8",
"enzyme-to-json": "^3.4.3",
"fuzzy": "^0.1.3",
"i": "^0.3.6",
"jquery": "^3.5.1",
"jquery.flot.tooltip": "^0.9.0",

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { FlagsContent } from './Flags';
import { Table } from 'reactstrap';
import { Input, Table } from 'reactstrap';
import toJson from 'enzyme-to-json';
const sampleFlagsResponse = {
@ -55,11 +55,60 @@ describe('Flags', () => {
striped: true,
});
});
it('should not fail if data is missing', () => {
expect(shallow(<FlagsContent />)).toHaveLength(1);
});
it('should match snapshot', () => {
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
expect(toJson(w)).toMatchSnapshot();
});
it('is sorted by flag by default', (): void => {
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
const td = w
.find('tbody')
.find('td')
.find('span')
.first();
expect(td.html()).toBe('<span>--alertmanager.notification-queue-capacity</span>');
});
it('sorts', (): void => {
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
const th = w
.find('thead')
.find('td')
.filterWhere((td): boolean => td.hasClass('Flag'));
th.simulate('click');
const td = w
.find('tbody')
.find('td')
.find('span')
.first();
expect(td.html()).toBe('<span>--web.user-assets</span>');
});
it('filters by flag name', (): void => {
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
const input = w.find(Input);
input.simulate('change', { target: { value: 'timeout' } });
const tds = w
.find('tbody')
.find('td')
.filterWhere(code => code.hasClass('flag-item'));
expect(tds.length).toEqual(3);
});
it('filters by flag value', (): void => {
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
const input = w.find(Input);
input.simulate('change', { target: { value: '10s' } });
const tds = w
.find('tbody')
.find('td')
.filterWhere(code => code.hasClass('flag-value'));
expect(tds.length).toEqual(1);
});
});

View File

@ -1,10 +1,18 @@
import React, { FC } from 'react';
import React, { ChangeEvent, FC, useState } from 'react';
import { RouteComponentProps } from '@reach/router';
import { Table } from 'reactstrap';
import { Input, InputGroup, Table } from 'reactstrap';
import { withStatusIndicator } from '../../components/withStatusIndicator';
import { useFetch } from '../../hooks/useFetch';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants';
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import sanitizeHTML from 'sanitize-html';
import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';
const fuz = new Fuzzy({ pre: '<strong>', post: '</strong>', shouldSort: true });
const flagSeparator = '||';
interface FlagMap {
[key: string]: string;
@ -14,18 +22,99 @@ interface FlagsProps {
data?: FlagMap;
}
const compareAlphaFn = (keys: boolean, reverse: boolean) => (
[k1, v1]: [string, string],
[k2, v2]: [string, string]
): number => {
const a = keys ? k1 : v1;
const b = keys ? k2 : v2;
const reverser = reverse ? -1 : 1;
return reverser * a.localeCompare(b);
};
const getSortIcon = (b: boolean | undefined): IconDefinition => {
if (b === undefined) {
return faSort;
}
if (b) {
return faSortDown;
}
return faSortUp;
};
interface SortState {
name: string;
alpha: boolean;
focused: boolean;
}
export const FlagsContent: FC<FlagsProps> = ({ data = {} }) => {
const initialSearch = '';
const [searchState, setSearchState] = useState(initialSearch);
const initialSort: SortState = {
name: 'Flag',
alpha: true,
focused: true,
};
const [sortState, setSortState] = useState(initialSort);
const searchable = Object.entries(data)
.sort(compareAlphaFn(sortState.name === 'Flag', !sortState.alpha))
.map(([flag, value]) => `--${flag}${flagSeparator}${value}`);
let filtered = searchable;
if (searchState.length > 0) {
filtered = fuz.filter(searchState, searchable).map((value: FuzzyResult) => value.rendered);
}
return (
<>
<h2>Command-Line Flags</h2>
<Table bordered size="sm" striped>
<InputGroup>
<Input
autoFocus
placeholder="Filter by flag name or value..."
className="my-3"
value={searchState}
onChange={({ target }: ChangeEvent<HTMLInputElement>): void => {
setSearchState(target.value);
}}
/>
</InputGroup>
<Table bordered size="sm" striped hover>
<thead>
<tr>
{['Flag', 'Value'].map((col: string) => (
<td
key={col}
className={`px-4 ${col}`}
style={{ width: '50%' }}
onClick={(): void =>
setSortState({
name: col,
focused: true,
alpha: sortState.name === col ? !sortState.alpha : true,
})
}
>
<span className="mr-2">{col}</span>
<FontAwesomeIcon icon={getSortIcon(sortState.name !== col ? undefined : sortState.alpha)} />
</td>
))}
</tr>
</thead>
<tbody>
{Object.keys(data).map(key => (
<tr key={key}>
<th>{key}</th>
<td>{data[key]}</td>
</tr>
))}
{filtered.map((result: string) => {
const [flagMatchStr, valueMatchStr] = result.split(flagSeparator);
const sanitizeOpts = { allowedTags: ['strong'] };
return (
<tr key={flagMatchStr}>
<td className="flag-item">
<span dangerouslySetInnerHTML={{ __html: sanitizeHTML(flagMatchStr, sanitizeOpts) }} />
</td>
<td className="flag-value">
<span dangerouslySetInnerHTML={{ __html: sanitizeHTML(valueMatchStr, sanitizeOpts) }} />
</td>
</tr>
);
})}
</tbody>
</Table>
</>

View File

@ -2,12 +2,12 @@ import React, { Component } from 'react';
import { Button, InputGroup, InputGroupAddon, InputGroupText, Input } from 'reactstrap';
import Downshift, { ControllerStateAndHelpers } from 'downshift';
import fuzzy from 'fuzzy';
import sanitizeHTML from 'sanitize-html';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
import MetricsExplorer from './MetricsExplorer';
import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';
interface ExpressionInputProps {
value: string;
@ -24,6 +24,8 @@ interface ExpressionInputState {
showMetricsExplorer: boolean;
}
const fuz = new Fuzzy({ pre: '<strong>', post: '</strong>', shouldSort: true });
class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputState> {
private exprInputRef = React.createRef<HTMLInputElement>();
@ -71,10 +73,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
};
getSearchMatches = (input: string, expressions: string[]) => {
return fuzzy.filter(input.replace(/ /g, ''), expressions, {
pre: '<strong>',
post: '</strong>',
});
return fuz.filter(input.replace(/ /g, ''), expressions);
};
createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
@ -96,11 +95,11 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
<li className="autosuggest-dropdown-header">{title}</li>
{matches
.slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
.map(({ original, string: text }) => {
.map((result: FuzzyResult) => {
const itemProps = downshift.getItemProps({
key: original,
key: result.original,
index,
item: original,
item: result.original,
style: {
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
},
@ -109,7 +108,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
<li
key={title}
{...itemProps}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(text, { allowedTags: ['strong'] }) }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(result.rendered, { allowedTags: ['strong'] }) }}
/>
);
})}

View File

@ -1581,6 +1581,11 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@nexucis/fuzzy@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@nexucis/fuzzy/-/fuzzy-0.2.2.tgz#60b2bd611e50a82170634027bd52751042622bea"
integrity sha512-XcBAj4bePw7rvQB86AOCnDCKAkm5JkVZh4JyVFlo4XXC/yuI4u2oTr4IQZLBsqrhI4ekZwVy3kwmO5kQ02NZzA==
"@nodelib/fs.stat@^1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
@ -5631,11 +5636,6 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
fuzzy@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/fuzzy/-/fuzzy-0.1.3.tgz#4c76ec2ff0ac1a36a9dccf9a00df8623078d4ed8"
integrity sha1-THbsL/CsGjap3M+aAN+GIweNTtg=
gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"