Skip to content

Commit

Permalink
Add sorting and filtering to flags page (#4403)
Browse files Browse the repository at this point in the history
* add sort/filter on flags

Signed-off-by: Augustin Husson <[email protected]>

* replace fuzzy lib by @nexucis/fuzzy

Signed-off-by: Augustin Husson <[email protected]>

* add changelog

Signed-off-by: Augustin Husson <[email protected]>
  • Loading branch information
Nexucis authored Jul 2, 2021
1 parent 06ae3b8 commit bc8421a
Show file tree
Hide file tree
Showing 8 changed files with 1,156 additions and 304 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re
### Added

- [#4394](https://github.com/thanos-io/thanos/pull/4394) Add error logs to receiver when write request rejected with invalid replica
- [#4384](https://github.com/thanos-io/thanos/pull/4384) Fix the experimental PromQL editor when used on multiple line.
- [#4403](https://github.com/thanos-io/thanos/pull/4403) UI: Add sorting and filtering to flags page
- [#4299](https://github.com/thanos-io/thanos/pull/4299) Tracing: Add tracing to exemplar APIs.
- [#4327](https://github.com/thanos-io/thanos/pull/4327) Add environment variable substitution to all YAML configuration flags.
- [#4239](https://github.com/thanos-io/thanos/pull/4239) Add penalty based deduplication mode for compactor.
Expand All @@ -22,6 +22,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re

### Fixed

- [#4384](https://github.com/thanos-io/thanos/pull/4384) Fix the experimental PromQL editor when used on multiple line.
- [#4342](https://github.com/thanos-io/thanos/pull/4342) ThanosSidecarUnhealthy doesn't fire if the sidecar is never healthy
- [#4388](https://github.com/thanos-io/thanos/pull/4388) Receive: fix bug in forwarding remote-write requests within the hashring via gRPC when TLS is enabled on the HTTP server but not on the gRPC server.

Expand Down
104 changes: 52 additions & 52 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/ui/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"@nexucis/fuzzy": "^0.2.2",
"@reach/router": "^1.3.4",
"bootstrap": "^4.6.0",
"codemirror-promql": "^0.16.0",
"css.escape": "^1.5.1",
"downshift": "^6.1.0",
"enzyme-to-json": "^3.6.1",
"fuzzy": "^0.1.3",
"i": "^0.3.6",
"jquery": "^3.5.1",
"jquery.flot.tooltip": "^0.9.0",
Expand Down
43 changes: 42 additions & 1 deletion pkg/ui/react-app/src/pages/flags/Flags.test.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -55,11 +55,52 @@ 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);
});
});
106 changes: 97 additions & 9 deletions pkg/ui/react-app/src/pages/flags/Flags.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
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 PathPrefixProps from '../../types/PathPrefixProps';
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 = '||';

export interface FlagMap {
[key: string]: string;
Expand All @@ -13,18 +21,98 @@ 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>
</>
Expand Down
Loading

0 comments on commit bc8421a

Please sign in to comment.