-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ui: Add Stores page to React UI (#2754)
* 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
Showing
16 changed files
with
838 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
25
pkg/ui/react-app/src/thanos/pages/stores/StoreLabels.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
132
pkg/ui/react-app/src/thanos/pages/stores/StorePoolPanel.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
84
pkg/ui/react-app/src/thanos/pages/stores/StorePoolPanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.