Skip to content

Commit

Permalink
ui: Add Stores page to React UI (#2754)
Browse files Browse the repository at this point in the history
* ui: Add store page to React UI

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

* query: Remove unnecessary field 'store_type' from api response

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

* ui: React: Show infinity icon the time is too big

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

* ui: Add tests for the Stores page in React UI

Signed-off-by: Prem Kumar <[email protected]>
  • Loading branch information
onprem authored Jun 25, 2020
1 parent 5051a1e commit 1aca077
Show file tree
Hide file tree
Showing 16 changed files with 838 additions and 137 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ We use *breaking* word for marking changes that are not backward compatible (rel

- [#2671](https://github.com/thanos-io/thanos/pull/2671) Tools: bucket replicate now allows passing repeated `--compaction` and `--resolution` flags.
- [#2657](https://github.com/thanos-io/thanos/pull/2657) Querier: Now, has the ability to perform concurrent select request per query.
- [#2754](https://github.com/thanos-io/thanos/pull/2671) UI: add stores page in the React UI.

## [v0.13.0](https://github.com/thanos-io/thanos/releases/tag/v0.13.0) - 2020.06.22

Expand Down
12 changes: 6 additions & 6 deletions pkg/query/storeset.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ type RuleSpec interface {

type StoreStatus struct {
Name string `json:"name"`
LastCheck time.Time `json:"last_check"`
LastError error `json:"last_error"`
LabelSets []storepb.LabelSet `json:"label_sets"`
StoreType component.StoreAPI `json:"store_type"`
MinTime int64 `json:"min_time"`
MaxTime int64 `json:"max_time"`
LastCheck time.Time `json:"lastCheck"`
LastError error `json:"lastError"`
LabelSets []storepb.LabelSet `json:"labelSets"`
StoreType component.StoreAPI `json:"-"`
MinTime int64 `json:"minTime"`
MaxTime int64 `json:"maxTime"`
}

type grpcStoreSpec struct {
Expand Down
256 changes: 128 additions & 128 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pkg/ui/react-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBSt
import PathPrefixProps from './types/PathPrefixProps';
import ThanosComponentProps from './thanos/types/ThanosComponentProps';
import Navigation from './thanos/Navbar';
import { ErrorBoundary } from './thanos/pages';
import { Stores, ErrorBoundary } from './thanos/pages';

import './App.css';

Expand All @@ -31,6 +31,7 @@ const App: FC<PathPrefixProps & ThanosComponentProps> = ({ pathPrefix, thanosCom
<Status path="/status" pathPrefix={pathPrefix} />
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
<Targets path="/targets" pathPrefix={pathPrefix} />
<Stores path="/stores" />
</Router>
</Container>
</ErrorBoundary>
Expand Down
5 changes: 4 additions & 1 deletion pkg/ui/react-app/src/thanos/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ interface NavConfig {
}

const navConfig: { [component: string]: NavConfig[] } = {
query: [{ name: 'Graph', uri: '/new/graph' }],
query: [
{ name: 'Graph', uri: '/new/graph' },
{ name: 'Stores', uri: '/new/stores' },
],
};

const Navigation: FC<PathPrefixProps & ThanosComponentProps> = ({ pathPrefix, thanosComponent }) => {
Expand Down
3 changes: 2 additions & 1 deletion pkg/ui/react-app/src/thanos/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Stores from './stores/Stores';
import ErrorBoundary from './errorBoundary/ErrorBoundary';

export { ErrorBoundary };
export { ErrorBoundary, Stores };
25 changes: 25 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/stores/StoreLabels.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import { ListGroup, ListGroupItem } from 'reactstrap';
import StoreLabels from './StoreLabels';
import { sampleAPIResponse } from './__testdata__/testdata';

describe('storeLabels', () => {
const { labelSets } = sampleAPIResponse.data.store[0];
const storeLabels = shallow(<StoreLabels labelSets={labelSets} />);

it('renders a listGroup', () => {
const listGroup = storeLabels.find(ListGroup);
expect(listGroup).toHaveLength(1);
});

it('renders a ListGroupItem for each labelSet', () => {
const listGroupItems = storeLabels.find(ListGroupItem);
expect(listGroupItems).toHaveLength(labelSets.length);
});

it('renders discovered labels', () => {
expect(toJson(storeLabels)).toMatchSnapshot();
});
});
21 changes: 21 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/stores/StoreLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { FC } from 'react';
import { Badge, ListGroup, ListGroupItem } from 'reactstrap';
import { Labels } from './store';

export type StoreLabelsProps = { labelSets: Labels[] };

export const StoreLabels: FC<StoreLabelsProps> = ({ labelSets }) => {
return (
<ListGroup>
{labelSets.map(({ labels }, idx) => (
<ListGroupItem key={idx}>
{labels.map(label => (
<Badge key={label.name} color="primary" style={{ margin: '0px 5px' }}>{`${label.name}="${label.value}"`}</Badge>
))}
</ListGroupItem>
))}
</ListGroup>
);
};

export default StoreLabels;
132 changes: 132 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/stores/StorePoolPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from 'react';
import { mount } from 'enzyme';
import { Button, Collapse, Table, Badge } from 'reactstrap';
import StorePoolPanel, { StorePoolPanelProps, MAX_TIME } from './StorePoolPanel';
import StoreLabels from './StoreLabels';
import { getColor } from '../../../pages/targets/target';
import { formatTime, parseTime } from '../../../utils';
import { sampleAPIResponse } from './__testdata__/testdata';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

describe('StorePoolPanel', () => {
const defaultProps: StorePoolPanelProps = {
title: 'sidecar',
storePool: sampleAPIResponse.data.sidecar,
};

const storePoolPanel = mount(<StorePoolPanel {...defaultProps} />);

it('renders a container', () => {
const div = storePoolPanel.find('div').filterWhere(elem => elem.hasClass('container-fluid'));
expect(div).toHaveLength(1);
});

describe('Header', () => {
it('renders a span with title', () => {
const span = storePoolPanel.find('h3 > span');
expect(span).toHaveLength(1);
expect(span.text()).toEqual('sidecar');
});

it('collapses the table when clicked on show less button', () => {
const btn = storePoolPanel.find(Button);
expect(btn).toHaveLength(1);
btn.simulate('click');

const collapse = storePoolPanel.find(Collapse);
expect(collapse.prop('isOpen')).toBe(false);
});

it('expands the table again after clicking show more button', () => {
const btn = storePoolPanel.find(Button);
expect(btn).toHaveLength(1);
btn.simulate('click');

const collapse = storePoolPanel.find(Collapse);
expect(collapse.prop('isOpen')).toBe(true);
});
});

it('renders an open Collapse component by default', () => {
const collapse = storePoolPanel.find(Collapse);
expect(collapse.prop('isOpen')).toBe(true);
});

describe('for each store', () => {
const table = storePoolPanel.find(Table);
defaultProps.storePool.forEach((store, idx) => {
const { name, minTime, maxTime, labelSets, lastCheck, lastError } = store;
const row = table.find('tr').at(idx + 1);

it('renders store endpoint', () => {
const td = row.find({ 'data-testid': 'endpoint' });
expect(td).toHaveLength(1);
expect(td.text()).toBe(name);
});

it('renders a badge for health', () => {
const health = lastError ? 'down' : 'up';
const td = row.find({ 'data-testid': 'health' });
expect(td).toHaveLength(1);

const badge = td.find(Badge);
expect(badge).toHaveLength(1);
expect(badge.prop('color')).toEqual(getColor(health));
expect(badge.text()).toEqual(health.toUpperCase());
});

it('renders labelSets', () => {
const td = row.find({ 'data-testid': 'storeLabels' });
expect(td).toHaveLength(1);

const storeLabels = td.find(StoreLabels);
expect(storeLabels).toHaveLength(1);
expect(storeLabels.prop('labelSets')).toEqual(labelSets);
});

it('renders minTime', () => {
const td = row.find({ 'data-testid': 'minTime' });
expect(td).toHaveLength(1);

if (minTime >= MAX_TIME) {
const infinityIcon = td.find(FontAwesomeIcon);
expect(infinityIcon).toHaveLength(1);
} else {
expect(td.text()).toBe(formatTime(minTime));
}
});

it('renders maxTime', () => {
const td = row.find({ 'data-testid': 'maxTime' });
expect(td).toHaveLength(1);

if (maxTime >= MAX_TIME) {
const infinityIcon = td.find(FontAwesomeIcon);
expect(infinityIcon).toHaveLength(1);
} else {
expect(td.text()).toBe(formatTime(maxTime));
}
});

it('renders lastCheck', () => {
const td = row.find({ 'data-testid': 'lastCheck' });
expect(td).toHaveLength(1);

if (parseTime(lastCheck) >= MAX_TIME) {
const infinityIcon = td.find(FontAwesomeIcon);
expect(infinityIcon).toHaveLength(1);
}
});

it('renders a badge for Errors', () => {
const td = row.find({ 'data-testid': 'lastError' });
const badge = td.find(Badge);
expect(badge).toHaveLength(lastError ? 1 : 0);
if (lastError) {
expect(badge.prop('color')).toEqual('danger');
expect(badge.children().text()).toEqual(lastError);
}
});
});
});
});
84 changes: 84 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/stores/StorePoolPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { FC } from 'react';
import { Container, Collapse, Table, Badge } from 'reactstrap';
import { now } from 'moment';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInfinity } from '@fortawesome/free-solid-svg-icons';
import { ToggleMoreLess } from '../../../components/ToggleMoreLess';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { getColor } from '../../../pages/targets/target';
import { formatRelative, formatTime, parseTime } from '../../../utils';
import { Store } from './store';
import StoreLabels from './StoreLabels';

export type StorePoolPanelProps = { title: string; storePool: Store[] };

export const columns = [
'Endpoint',
'Status',
'Announced LabelSets',
'Min Time',
'Max Time',
'Last Successful Health Check',
'Last Message',
];

export const MAX_TIME = 9223372036854775807;

export const StorePoolPanel: FC<StorePoolPanelProps> = ({ title, storePool }) => {
const [{ expanded }, setOptions] = useLocalStorage(`store-pool-${title}-expanded`, { expanded: true });

return (
<Container fluid>
<ToggleMoreLess event={(): void => setOptions({ expanded: !expanded })} showMore={expanded}>
<span style={{ textTransform: 'capitalize' }}>{title}</span>
</ToggleMoreLess>
<Collapse isOpen={expanded}>
<Table size="sm" bordered hover>
<thead>
<tr key="header">
{columns.map(column => (
<th key={column}>{column}</th>
))}
</tr>
</thead>
<tbody>
{storePool.map((store: Store) => {
const { name, minTime, maxTime, labelSets, lastCheck, lastError } = store;
const health = lastError ? 'down' : 'up';
const color = getColor(health);

return (
<tr key={name}>
<td data-testid="endpoint">{name}</td>
<td data-testid="health">
<Badge color={color}>{health.toUpperCase()}</Badge>
</td>
<td data-testid="storeLabels">
<StoreLabels labelSets={labelSets} />
</td>
<td data-testid="minTime">
{minTime >= MAX_TIME ? <FontAwesomeIcon icon={faInfinity} /> : formatTime(minTime)}
</td>
<td data-testid="maxTime">
{maxTime >= MAX_TIME ? <FontAwesomeIcon icon={faInfinity} /> : formatTime(maxTime)}
</td>
<td data-testid="lastCheck">
{parseTime(lastCheck) >= MAX_TIME ? (
<FontAwesomeIcon icon={faInfinity} />
) : (
formatRelative(lastCheck, now())
)}{' '}
ago
</td>
<td data-testid="lastError">{lastError ? <Badge color={color}>{lastError}</Badge> : null}</td>
</tr>
);
})}
</tbody>
</Table>
</Collapse>
</Container>
);
};

export default StorePoolPanel;
Loading

0 comments on commit 1aca077

Please sign in to comment.