Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: Add search block UI #4444

Merged
merged 6 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re
- [#4453](https://github.com/thanos-io/thanos/pull/4453) Tools: Add flag `--selector.relabel-config-file` / `--selector.relabel-config` / `--max-time` / `--min-time` to filter served blocks.
- [#4482](https://github.com/thanos-io/thanos/pull/4482) COS: Add http_config for cos object store client.
- [#4487](https://github.com/thanos-io/thanos/pull/4487) Query: Add memcached auto discovery support.
- [#4444](https://github.com/thanos-io/thanos/pull/4444) UI: Add search block UI.

### Fixed

Expand Down
258 changes: 129 additions & 129 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/BlockSearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { FC, ChangeEvent } from 'react';
import { Button, InputGroup, InputGroupAddon, InputGroupText, Input, Form } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import styles from './blocks.module.css';

interface BlockSearchInputProps {
onChange: ({ target }: ChangeEvent<HTMLInputElement>) => void;
onClick: () => void;
defaultValue: string;
}

export const BlockSearchInput: FC<BlockSearchInputProps> = ({ onChange, onClick, defaultValue }) => {
return (
<Form onSubmit={(e) => e.preventDefault()}>
<InputGroup className={styles.blockInput}>
<InputGroupAddon addonType="prepend">
<InputGroupText>
<FontAwesomeIcon icon={faSearch} />
</InputGroupText>
</InputGroupAddon>
<Input placeholder="Search block by ulid" onChange={onChange} defaultValue={defaultValue} />
<InputGroupAddon addonType="append">
<Button className="execute-btn" color="primary" onClick={onClick} type="submit">
Search
</Button>
</InputGroupAddon>
</InputGroup>
</Form>
);
};
71 changes: 45 additions & 26 deletions pkg/ui/react-app/src/thanos/pages/blocks/Blocks.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React, { FC, useMemo, useState } from 'react';
import React, { ChangeEvent, FC, useMemo, useState } from 'react';
import { RouteComponentProps } from '@reach/router';
import { UncontrolledAlert } from 'reactstrap';
import { useQueryParams, withDefault, NumberParam } from 'use-query-params';
import { useQueryParams, withDefault, NumberParam, StringParam } from 'use-query-params';
import { withStatusIndicator } from '../../../components/withStatusIndicator';
import { useFetch } from '../../../hooks/useFetch';
import PathPrefixProps from '../../../types/PathPrefixProps';
import { Block } from './block';
import { SourceView } from './SourceView';
import { BlockDetails } from './BlockDetails';
import { BlockSearchInput } from './BlockSearchInput';
import { sortBlocks } from './helpers';
import styles from './blocks.module.css';
import TimeRange from './TimeRange';

export interface BlockListProps {
blocks: Block[];
err: string | null;
Expand All @@ -21,6 +21,7 @@ export interface BlockListProps {

export const BlocksContent: FC<{ data: BlockListProps }> = ({ data }) => {
const [selectedBlock, selectBlock] = useState<Block>();
const [searchState, setSearchState] = useState<string>('');

const { blocks, label, err } = data;

Expand All @@ -42,47 +43,65 @@ export const BlocksContent: FC<{ data: BlockListProps }> = ({ data }) => {
return [0, 0];
}, [blocks, err]);

const [{ 'min-time': viewMinTime, 'max-time': viewMaxTime }, setQuery] = useQueryParams({
const [{ 'min-time': viewMinTime, 'max-time': viewMaxTime, ulid: blockSearchParam }, setQuery] = useQueryParams({
'min-time': withDefault(NumberParam, gridMinTime),
'max-time': withDefault(NumberParam, gridMaxTime),
ulid: withDefault(StringParam, ''),
});

const [blockSearch, setBlockSearch] = useState<string>(blockSearchParam);

const setViewTime = (times: number[]): void => {
setQuery({
'min-time': times[0],
'max-time': times[1],
});
};

const setBlockSearchInput = (searchState: string): void => {
setQuery({
ulid: searchState,
});
setBlockSearch(searchState);
};

if (err) return <UncontrolledAlert color="danger">{err.toString()}</UncontrolledAlert>;

return (
<>
{blocks.length > 0 ? (
<div className={styles.container}>
<div className={styles.grid}>
<div className={styles.sources}>
{Object.keys(blockPools).map((pk) => (
<SourceView
key={pk}
data={blockPools[pk]}
title={pk}
selectBlock={selectBlock}
gridMinTime={viewMinTime}
gridMaxTime={viewMaxTime}
/>
))}
<>
<BlockSearchInput
onChange={({ target }: ChangeEvent<HTMLInputElement>): void => setSearchState(target.value)}
onClick={() => setBlockSearchInput(searchState)}
defaultValue={blockSearchParam}
/>
<div className={styles.container}>
<div className={styles.grid}>
<div className={styles.sources}>
{Object.keys(blockPools).map((pk) => (
<SourceView
key={pk}
data={blockPools[pk]}
title={pk}
selectBlock={selectBlock}
gridMinTime={viewMinTime}
gridMaxTime={viewMaxTime}
blockSearch={blockSearch}
/>
))}
</div>
<TimeRange
gridMinTime={gridMinTime}
gridMaxTime={gridMaxTime}
viewMinTime={viewMinTime}
viewMaxTime={viewMaxTime}
onChange={setViewTime}
/>
</div>
<TimeRange
gridMinTime={gridMinTime}
gridMaxTime={gridMaxTime}
viewMinTime={viewMinTime}
viewMaxTime={viewMaxTime}
onChange={setViewTime}
/>
<BlockDetails selectBlock={selectBlock} block={selectedBlock} />
</div>
<BlockDetails selectBlock={selectBlock} block={selectedBlock} />
</div>
</>
) : (
<UncontrolledAlert color="warning">No blocks found.</UncontrolledAlert>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('Blocks SourceView', () => {
},
gridMinTime: 1596096000000,
gridMaxTime: 1595108031471,
blockSearch: '',
};

const sourceView = mount(<SourceView {...defaultProps} />);
Expand Down
18 changes: 13 additions & 5 deletions pkg/ui/react-app/src/thanos/pages/blocks/SourceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import React, { FC } from 'react';
import { Block, BlocksPool } from './block';
import { BlockSpan } from './BlockSpan';
import styles from './blocks.module.css';
import { getBlockByUlid } from './helpers';

export const BlocksRow: FC<{
blocks: Block[];
gridMinTime: number;
gridMaxTime: number;
selectBlock: React.Dispatch<React.SetStateAction<Block | undefined>>;
}> = ({ blocks, gridMinTime, gridMaxTime, selectBlock }) => {
blockSearch: string;
}> = ({ blocks, gridMinTime, gridMaxTime, selectBlock, blockSearch }) => {
const blockSearchValue = getBlockByUlid(blocks, blockSearch);

return (
<div className={styles.row}>
{blocks.map<JSX.Element>((b) => (
<BlockSpan selectBlock={selectBlock} block={b} gridMaxTime={gridMaxTime} gridMinTime={gridMinTime} key={b.ulid} />
))}
{blockSearchValue.map<JSX.Element>((b) => {
return (
<BlockSpan selectBlock={selectBlock} block={b} gridMaxTime={gridMaxTime} gridMinTime={gridMinTime} key={b.ulid} />
);
})}
</div>
);
};
Expand All @@ -24,9 +30,10 @@ export interface SourceViewProps {
gridMinTime: number;
gridMaxTime: number;
selectBlock: React.Dispatch<React.SetStateAction<Block | undefined>>;
blockSearch: string;
}

export const SourceView: FC<SourceViewProps> = ({ data, title, gridMaxTime, gridMinTime, selectBlock }) => {
export const SourceView: FC<SourceViewProps> = ({ data, title, gridMaxTime, gridMinTime, selectBlock, blockSearch }) => {
return (
<>
<div className={styles.source}>
Expand All @@ -43,6 +50,7 @@ export const SourceView: FC<SourceViewProps> = ({ data, title, gridMaxTime, grid
key={`${k}-${i}`}
gridMaxTime={gridMaxTime}
gridMinTime={gridMinTime}
blockSearch={blockSearch}
/>
))}
</React.Fragment>
Expand Down
4 changes: 4 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/blocks.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,7 @@
.level-6 {
background: var(--level-6);
}

.blockInput {
margin-bottom: 12px;
}
17 changes: 17 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LabelSet, Block, BlocksPool } from './block';
import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';

const stringify = (map: LabelSet): string => {
let t = '';
Expand Down Expand Up @@ -103,3 +104,19 @@ export const download = (blob: Block): string => {

return url;
};

export const getBlockByUlid = (blocks: Block[], ulid: string): Block[] => {
if (ulid === '') {
return blocks;
}

const ulidArray = blocks.map((block) => block.ulid);
const fuz = new Fuzzy({ caseSensitive: true });

const result: FuzzyResult[] = fuz.filter(ulid, ulidArray);

const resultIndex = result.map((value) => value.index);

const blockResult = blocks.filter((block, index) => resultIndex.includes(index));
return blockResult;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spoke with @adzshaf and mentioned that I think this could produce confusing experience for the user. If I provide full-length ULID that matches a block name exactly, but fuzzy search returns more than one result, then I would be very confused and asume that all of the blocks have the same name, which should not really be happening. I think it's fair to return multiple blocks if the ULID does not match any block exactly, i.e. it is not full-length or has a typo.

We can do this in a follow up of course, since this UI already provides a ton of value.

};