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: Migrate block viewer to React #2980

Merged
merged 13 commits into from
Aug 21, 2020
Merged
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