Skip to content

Commit

Permalink
ui: Migrate block viewer to React (#2980)
Browse files Browse the repository at this point in the history
* ui: Migrate block viewer to React

Signed-off-by: Prem Kumar <[email protected]>

* ui: Color blocks on basis of resolution

Signed-off-by: Prem Kumar <[email protected]>

* ui: Block: Allow filtering by time range

Signed-off-by: Prem Kumar <[email protected]>

* Hanlde errors; use different shades for different compaction levels

Signed-off-by: Prem Kumar <[email protected]>

* Add minTime and maxTime to query parameters

Signed-off-by: Prem Kumar <[email protected]>

* Make block details div sticky

Signed-off-by: Prem Kumar <[email protected]>

* Add tests for Blocks page

Signed-off-by: Prem Kumar <[email protected]>

* Fix the width of of labels

Signed-off-by: Prem Kumar <[email protected]>

* Add an entry to CHANGELOG.md

Signed-off-by: Prem Kumar <[email protected]>

* Fix rendering a gap between consecutive blocks

Signed-off-by: Prem Kumar <[email protected]>

* Add link to new UI in block viewer

Signed-off-by: Prem Kumar <[email protected]>

* Use semantic HTML tags

Signed-off-by: Prem Kumar <[email protected]>
  • Loading branch information
onprem authored Aug 21, 2020
1 parent f32877f commit 5db8135
Show file tree
Hide file tree
Showing 22 changed files with 3,081 additions and 97 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ We use *breaking* word for marking changes that are not backward compatible (rel
- [#2964](https://github.com/thanos-io/thanos/pull/2964) Query: Add time range parameters to label APIs. Add `start` and `end` fields to Store API `LabelNamesRequest` and `LabelValuesRequest`.
- [#2996](https://github.com/thanos-io/thanos/pull/2996) Sidecar: Add `reloader_config_apply_errors_total` metric. Add new flags `--reloader.watch-interval`, and `--reloader.retry-interval`.
- [#2973](https://github.com/thanos-io/thanos/pull/2973) Add Thanos Query Frontend component.
- [#2980](https://github.com/thanos-io/thanos/pull/2980) Bucket Viewer: Migrate block viewer to React.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ $(REACT_APP_NODE_MODULES_PATH): $(REACT_APP_PATH)/package.json $(REACT_APP_PATH)

$(REACT_APP_OUTPUT_DIR): $(REACT_APP_NODE_MODULES_PATH) $(REACT_APP_SOURCE_FILES)
@echo ">> building React app"
@./scripts/build-react-app.sh
@scripts/build-react-app.sh

.PHONY: assets
assets: # Repacks all static assets into go file for easier deploy.
Expand Down
148 changes: 74 additions & 74 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pkg/ui/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"moment": "^2.24.0",
"moment-timezone": "^0.5.23",
"popper.js": "^1.14.3",
"query-string": "^6.13.1",
"rc-slider": "^9.3.1",
"react": "^16.7.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.7.0",
Expand All @@ -40,7 +42,8 @@
"sanitize-html": "^1.20.1",
"tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.0.3",
"typescript": "^3.3.3"
"typescript": "^3.3.3",
"use-query-params": "^1.1.6"
},
"scripts": {
"start": "react-scripts start",
Expand Down
42 changes: 24 additions & 18 deletions pkg/ui/react-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React, { FC } from 'react';
import { Container } from 'reactstrap';
import { Router, Redirect } from '@reach/router';
import { Router, Redirect, globalHistory } from '@reach/router';
import { QueryParamProvider } from 'use-query-params';

import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages';
import PathPrefixProps from './types/PathPrefixProps';
import ThanosComponentProps from './thanos/types/ThanosComponentProps';
import Navigation from './thanos/Navbar';
import { Stores, ErrorBoundary } from './thanos/pages';
import { Stores, ErrorBoundary, Blocks } from './thanos/pages';

import './App.css';

const defaultRouteConfig: { [component: string]: string } = {
query: '/graph',
rule: '/alerts',
bucket: '/blocks',
compact: '/blocks',
};

const App: FC<PathPrefixProps & ThanosComponentProps> = ({ pathPrefix, thanosComponent }) => {
Expand All @@ -24,24 +27,27 @@ const App: FC<PathPrefixProps & ThanosComponentProps> = ({ pathPrefix, thanosCom
defaultRoute={defaultRouteConfig[thanosComponent]}
/>
<Container fluid style={{ paddingTop: 70 }}>
<Router basepath={`${pathPrefix}/new`}>
<Redirect from="/" to={`${pathPrefix}/new${defaultRouteConfig[thanosComponent]}`} />
<QueryParamProvider reachHistory={globalHistory}>
<Router basepath={`${pathPrefix}/new`}>
<Redirect from="/" to={`${pathPrefix}/new${defaultRouteConfig[thanosComponent]}`} />

{/*
NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go.
{/*
NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go.
*/}
<PanelList path="/graph" pathPrefix={pathPrefix} />
<Alerts path="/alerts" pathPrefix={pathPrefix} />
<Config path="/config" pathPrefix={pathPrefix} />
<Flags path="/flags" pathPrefix={pathPrefix} />
<Rules path="/rules" pathPrefix={pathPrefix} />
<ServiceDiscovery path="/service-discovery" pathPrefix={pathPrefix} />
<Status path="/status" pathPrefix={pathPrefix} />
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
<Targets path="/targets" pathPrefix={pathPrefix} />
<Stores path="/stores" pathPrefix={pathPrefix} />
</Router>
<PanelList path="/graph" pathPrefix={pathPrefix} />
<Alerts path="/alerts" pathPrefix={pathPrefix} />
<Config path="/config" pathPrefix={pathPrefix} />
<Flags path="/flags" pathPrefix={pathPrefix} />
<Rules path="/rules" pathPrefix={pathPrefix} />
<ServiceDiscovery path="/service-discovery" pathPrefix={pathPrefix} />
<Status path="/status" pathPrefix={pathPrefix} />
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
<Targets path="/targets" pathPrefix={pathPrefix} />
<Stores path="/stores" pathPrefix={pathPrefix} />
<Blocks path="/blocks" pathPrefix={pathPrefix} />
</Router>
</QueryParamProvider>
</Container>
</ErrorBoundary>
);
Expand Down
13 changes: 12 additions & 1 deletion pkg/ui/react-app/src/thanos/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ const navConfig: { [component: string]: (NavConfig | NavDropDown)[] } = {
{ name: 'Alerts', uri: '/new/alerts' },
{ name: 'Rules', uri: '/new/rules' },
],
bucket: [{ name: 'Blocks', uri: '/new/blocks' }],
compact: [{ name: 'Blocks', uri: '/new/blocks' }],
};

const defaultClassicUIRoute: { [component: string]: string } = {
query: '/graph',
rule: '/alerts',
bucket: '/',
compact: '/loaded',
};

interface NavigationProps {
Expand Down Expand Up @@ -88,7 +97,9 @@ const Navigation: FC<PathPrefixProps & NavigationProps> = ({ pathPrefix, thanosC
<NavLink href="https://thanos.io/getting-started.md/">Help</NavLink>
</NavItem>
<NavItem>
<NavLink href={`${pathPrefix}${defaultRoute}${window.location.search}`}>Classic UI</NavLink>
<NavLink href={`${pathPrefix}${defaultClassicUIRoute[thanosComponent]}${window.location.search}`}>
Classic UI
</NavLink>
</NavItem>
</Nav>
</Collapse>
Expand Down
91 changes: 91 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/BlockDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { mount } from 'enzyme';
import moment from 'moment';
import { BlockDetails, BlockDetailsProps } from './BlockDetails';
import { sampleAPIResponse } from './__testdata__/testdata';

const sampleBlock = sampleAPIResponse.data.blocks[0];
const formatTime = (time: number): string => {
return moment.unix(time / 1000).format('LLL');
};

describe('BlockDetails', () => {
const defaultProps: BlockDetailsProps = {
block: sampleBlock,
selectBlock: (): void => {
// do nothing
},
};

const blockDetails = mount(<BlockDetails {...defaultProps} />);

it('renders a heading with block ulid', () => {
const title = blockDetails.find({ 'data-testid': 'ulid' });
expect(title).toHaveLength(1);
expect(title.text()).toEqual(sampleBlock.ulid);
});

it('renders start time of the block', () => {
const div = blockDetails.find({ 'data-testid': 'start-time' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(formatTime(sampleBlock.minTime));
});

it('renders end time of the block', () => {
const div = blockDetails.find({ 'data-testid': 'end-time' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(formatTime(sampleBlock.maxTime));
});

it('renders duration of the block', () => {
const div = blockDetails.find({ 'data-testid': 'duration' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(moment.duration(sampleBlock.maxTime - sampleBlock.minTime, 'ms').humanize());
});

it('renders total number of series in the block', () => {
const div = blockDetails.find({ 'data-testid': 'series' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(sampleBlock.stats.numSeries.toString());
});

it('renders total number of samples in the block', () => {
const div = blockDetails.find({ 'data-testid': 'samples' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(sampleBlock.stats.numSamples.toString());
});

it('renders total number of chunks in the block', () => {
const div = blockDetails.find({ 'data-testid': 'chunks' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(sampleBlock.stats.numChunks.toString());
});

it('renders downsampling resolution of the block', () => {
const div = blockDetails.find({ 'data-testid': 'resolution' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(sampleBlock.thanos.downsample.resolution.toString());
});

it('renders compaction level of the block', () => {
const div = blockDetails.find({ 'data-testid': 'level' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(sampleBlock.compaction.level.toString());
});

it('renders source of the block', () => {
const div = blockDetails.find({ 'data-testid': 'source' });
expect(div).toHaveLength(1);
expect(div.find('span').text()).toBe(sampleBlock.thanos.source);
});

it('renders a list of the labels', () => {
const div = blockDetails.find({ 'data-testid': 'labels' });
const list = div.find('ul');
expect(div).toHaveLength(1);
expect(list).toHaveLength(1);

const labels = list.find('li');
expect(labels).toHaveLength(Object.keys(sampleBlock.thanos.labels).length);
});
});
70 changes: 70 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/BlockDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { FC } from 'react';
import { Block } from './block';
import styles from './blocks.module.css';
import moment from 'moment';

export interface BlockDetailsProps {
block: Block | undefined;
selectBlock: React.Dispatch<React.SetStateAction<Block | undefined>>;
}

export const BlockDetails: FC<BlockDetailsProps> = ({ block, selectBlock }) => {
return (
<div className={`${styles.blockDetails} ${block && styles.open}`}>
{block && (
<>
<div className={styles.detailsTop}>
<span className={styles.header} data-testid="ulid">
{block.ulid}
</span>
<button className={styles.closeBtn} onClick={(): void => selectBlock(undefined)}>
&times;
</button>
</div>
<hr />
<div data-testid="start-time">
<b>Start Time:</b> <span>{moment.unix(block.minTime / 1000).format('LLL')}</span>
</div>
<div data-testid="end-time">
<b>End Time:</b> <span>{moment.unix(block.maxTime / 1000).format('LLL')}</span>
</div>
<div data-testid="duration">
<b>Duration:</b> <span>{moment.duration(block.maxTime - block.minTime, 'ms').humanize()}</span>
</div>
<hr />
<div data-testid="series">
<b>Series:</b> <span>{block.stats.numSeries}</span>
</div>
<div data-testid="samples">
<b>Samples:</b> <span>{block.stats.numSamples}</span>
</div>
<div data-testid="chunks">
<b>Chunks:</b> <span>{block.stats.numChunks}</span>
</div>
<hr />
<div data-testid="resolution">
<b>Resolution:</b> <span>{block.thanos.downsample.resolution}</span>
</div>
<div data-testid="level">
<b>Level:</b> <span>{block.compaction.level}</span>
</div>
<div data-testid="source">
<b>Source:</b> <span>{block.thanos.source}</span>
</div>
<hr />
<div data-testid="labels">
<b>Labels:</b>
<ul>
{Object.entries(block.thanos.labels).map(([key, value]) => (
<li key={key}>
<b>{key}: </b>
{value}
</li>
))}
</ul>
</div>
</>
)}
</div>
);
};
29 changes: 29 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/BlockSpan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { FC } from 'react';
import { Block } from './block';
import styles from './blocks.module.css';

interface BlockSpanProps {
block: Block;
gridMinTime: number;
gridMaxTime: number;
selectBlock: React.Dispatch<React.SetStateAction<Block | undefined>>;
}

export const BlockSpan: FC<BlockSpanProps> = ({ block, gridMaxTime, gridMinTime, selectBlock }) => {
const viewWidth = gridMaxTime - gridMinTime;
const spanWidth = ((block.maxTime - block.minTime) / viewWidth) * 100;
const spanOffset = ((block.minTime - gridMinTime) / viewWidth) * 100;

return (
<button
onClick={(): void => selectBlock(block)}
className={`${styles.blockSpan} ${styles[`res-${block.thanos.downsample.resolution}`]} ${
styles[`level-${block.compaction.level}`]
}`}
style={{
width: `calc(${spanWidth.toFixed(4)}% + 1px)`,
left: `${spanOffset.toFixed(4)}%`,
}}
/>
);
};
Loading

0 comments on commit 5db8135

Please sign in to comment.