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
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
122 changes: 61 additions & 61 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pkg/ui/react-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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 { 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 Down Expand Up @@ -41,6 +43,7 @@ const App: FC<PathPrefixProps & ThanosComponentProps> = ({ pathPrefix, thanosCom
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
<Targets path="/targets" pathPrefix={pathPrefix} />
<Stores path="/stores" pathPrefix={pathPrefix} />
<Blocks path="/blocks" pathPrefix={pathPrefix} />
</Router>
</Container>
</ErrorBoundary>
Expand Down
2 changes: 2 additions & 0 deletions pkg/ui/react-app/src/thanos/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ 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' }],
};

interface NavigationProps {
Expand Down
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { Block } from './block';
import styles from './blocks.module.css';
import moment from 'moment';

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}>
<p>{block.ulid}</p>
<button className={styles.closeBtn} onClick={(): void => selectBlock(undefined)}>
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
<hr />
<div>
<b>Start Time:</b> {moment.unix(block.minTime / 1000).format('LLL')}
</div>
<div>
<b>End Time:</b> {moment.unix(block.maxTime / 1000).format('LLL')}
</div>
<div>
<b>Duration:</b> {moment.duration(block.maxTime - block.minTime, 'ms').humanize()}
</div>
<hr />
<div>
<b>Series:</b> {block.stats.numSeries}
</div>
<div>
<b>Samples:</b> {block.stats.numSamples}
</div>
<div>
<b>Chunks:</b> {block.stats.numChunks}
</div>
<hr />
<div>
<b>Resolution:</b> {block.thanos.downsample.resolution}
</div>
<div>
<b>Level:</b> {block.compaction.level}
</div>
<div>
<b>Source:</b> {block.thanos.source}
</div>
<hr />
<div>
<b>Labels:</b>
<ul>
{Object.entries(block.thanos.labels).map(([key, value]) => (
<li key={key}>
<b>{key}: </b>
{value}
</li>
))}
</ul>
</div>
</>
)}
</div>
);
};
27 changes: 27 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,27 @@
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}
style={{
width: `${spanWidth}%`,
left: `${spanOffset}%`,
}}
/>
);
};
81 changes: 81 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/Blocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { FC, useMemo, useState } from 'react';
import { RouteComponentProps } from '@reach/router';
import { Alert } from 'reactstrap';
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 { sortBlocks } from './helpers';
import styles from './blocks.module.css';

export interface BlockListProps {
blocks: Block[];
err: string | null;
label: string;
refreshedAt: string;
}

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

const { blocks, label } = data;

const blockPools = useMemo(() => sortBlocks(blocks, label), [blocks, label]);
const [gridMinTime, gridMaxTime] = useMemo(() => {
let gridMinTime = blocks[0].minTime;
let gridMaxTime = blocks[0].maxTime;
blocks.forEach(block => {
if (block.minTime < gridMinTime) {
gridMinTime = block.minTime;
}
if (block.maxTime > gridMaxTime) {
gridMaxTime = block.maxTime;
}
});
return [gridMinTime, gridMaxTime];
}, [blocks]);

return (
<>
{blocks.length > 0 ? (
<div className={styles.container}>
<div className={styles.grid}>
{Object.keys(blockPools).map(pk => (
<SourceView
key={pk}
data={blockPools[pk]}
title={pk}
selectBlock={selectBlock}
gridMinTime={gridMinTime}
gridMaxTime={gridMaxTime}
/>
))}
</div>
<BlockDetails selectBlock={selectBlock} block={selectedBlock} />
</div>
) : (
<Alert color="warning">No blocks found.</Alert>
)}
</>
);
};

const BlocksWithStatusIndicator = withStatusIndicator(BlocksContent);

export const Blocks: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
const { response, error, isLoading } = useFetch<BlockListProps>(`${pathPrefix}/api/v1/blocks`);
const { status: responseStatus } = response;
const badResponse = responseStatus !== 'success' && responseStatus !== 'start fetching';

return (
<BlocksWithStatusIndicator
data={response.data}
error={badResponse ? new Error(responseStatus) : error}
isLoading={isLoading}
/>
);
};

export default Blocks;
49 changes: 49 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/SourceView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { FC } from 'react';
import { Block, BlocksPool } from './block';
import { BlockSpan } from './BlockSpan';
import styles from './blocks.module.css';

const BlocksRow: FC<{
blocks: Block[];
gridMinTime: number;
gridMaxTime: number;
selectBlock: React.Dispatch<React.SetStateAction<Block | undefined>>;
}> = ({ blocks, gridMinTime, gridMaxTime, selectBlock }) => {
return (
<div className={styles.row}>
{blocks.map<JSX.Element>(b => (
<BlockSpan selectBlock={selectBlock} block={b} gridMaxTime={gridMaxTime} gridMinTime={gridMinTime} key={b.ulid} />
))}
</div>
);
};

export const SourceView: FC<{
data: BlocksPool;
title: string;
gridMinTime: number;
gridMaxTime: number;
selectBlock: React.Dispatch<React.SetStateAction<Block | undefined>>;
}> = ({ data, title, gridMaxTime, gridMinTime, selectBlock }) => {
return (
<>
<div className={styles.source}>
<div className={styles.title}>
<h4>{title}</h4>
</div>
<div className={styles.rowsContainer}>
{Object.keys(data).map(k => (
<BlocksRow
selectBlock={selectBlock}
blocks={data[k]}
key={k}
gridMaxTime={gridMaxTime}
gridMinTime={gridMinTime}
/>
))}
</div>
</div>
<hr />
</>
);
};
35 changes: 35 additions & 0 deletions pkg/ui/react-app/src/thanos/pages/blocks/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface Block {
compaction: {
level: number;
sources: string[];
parents?: {
maxTime: number;
minTime: number;
ulid: string;
}[];
};
maxTime: number;
minTime: number;
stats: {
numChunks: number;
numSamples: number;
numSeries: number;
};
thanos: {
downsample: {
resolution: number;
};
labels: LabelSet;
source: string;
};
ulid: string;
version: number;
}

export interface LabelSet {
[labelName: string]: string;
}

export interface BlocksPool {
[key: string]: Block[];
}
Loading