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:
parent
441e6cd7d6
commit
f72cabb437
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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'] }) }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue