Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan

### Changed

- [#46](https://github.com/kobsio/kobs/pull/46): Support multiple types for the legend in a Prometheus chart and use a custom component to render the legend.

## [v0.2.0](https://github.com/kobsio/kobs/releases/tag/v0.2.0) (2021-04-23)

### Added
Expand Down
2 changes: 1 addition & 1 deletion app/src/plugins/prometheus/PrometheusAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const PrometheusAutocomplete: React.FunctionComponent<IPrometheusAutocomp
query,
setQuery,
onEnter,
}: IPrometheusAutocomplete): JSX.Element => {
}: IPrometheusAutocomplete) => {
const inputRef = useRef<HTMLInputElement>(null);
const [data, setData] = useState<string[]>([]);
const [inputFocused, setInputFocused] = useState(false);
Expand Down
107 changes: 73 additions & 34 deletions app/src/plugins/prometheus/PrometheusChartDefault.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, { useEffect, useRef, useState } from 'react';

import { Data, Metrics } from 'proto/prometheus_grpc_web_pb';
import { IData, transformData } from 'plugins/prometheus/helpers';
import PrometheusChartDefaultLegend from 'plugins/prometheus/PrometheusChartDefaultLegend';
import { formatTime } from 'utils/helpers';

interface ILabels {
Expand All @@ -24,7 +25,7 @@ export interface IPrometheusChartDefaultProps {
type: string;
unit: string;
stacked: boolean;
disableLegend?: boolean;
legend: string;
metrics: Metrics.AsObject[];
}

Expand All @@ -42,12 +43,25 @@ const PrometheusChartDefault: React.FunctionComponent<IPrometheusChartDefaultPro
type,
unit,
stacked,
disableLegend,
legend,
metrics,
}: IPrometheusChartDefaultProps) => {
const refChart = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(0);
const [hiddenMetrics, setHiddenMetrics] = useState<string[]>([]);

const toogleMetric = (index: string): void => {
let tmpHiddenMetrics = [...hiddenMetrics];

if (tmpHiddenMetrics.includes(index)) {
tmpHiddenMetrics = tmpHiddenMetrics.filter((f) => f !== index);
} else {
tmpHiddenMetrics.push(index);
}

setHiddenMetrics(tmpHiddenMetrics);
};

// useEffect is executed on every render of this component. This is needed, so that we are able to use a width of 100%
// and a static height for the chart.
Expand All @@ -64,45 +78,70 @@ const PrometheusChartDefault: React.FunctionComponent<IPrometheusChartDefaultPro
const legendData = metrics.map((metric, index) => ({ childName: `index${index}`, name: metric.label }));
const series = metrics.map((metric, index) =>
type === 'area' ? (
<ChartArea key={index} data={transformData(metric.dataList)} interpolation="monotoneX" name={`index${index}`} />
<ChartArea
key={index}
data={transformData(metric.dataList, hiddenMetrics.includes(`index${index}`))}
interpolation="monotoneX"
name={`index${index}`}
/>
) : type === 'bar' ? (
<ChartBar key={index} data={transformData(metric.dataList)} name={`index${index}`} />
<ChartBar
key={index}
data={transformData(metric.dataList, hiddenMetrics.includes(`index${index}`))}
name={`index${index}`}
/>
) : (
<ChartLine key={index} data={transformData(metric.dataList)} interpolation="monotoneX" name={`index${index}`} />
<ChartLine
key={index}
data={transformData(metric.dataList, hiddenMetrics.includes(`index${index}`))}
interpolation="monotoneX"
name={`index${index}`}
/>
),
);

return (
<div style={{ height: '300px', width: '100%' }} ref={refChart}>
<Chart
containerComponent={
<CursorVoronoiContainer
cursorDimension="x"
labels={({ datum }: ILabels): string => (datum.y ? `${datum.y} ${unit}` : 'N/A')}
labelComponent={
<ChartLegendTooltip
legendData={legendData}
title={(point: Data.AsObject): string => formatTime(Math.floor(point.x / 1000))}
/>
}
mouseFollowTooltips
voronoiDimension="x"
voronoiPadding={0}
/>
}
height={height}
legendData={legendData}
legendPosition={disableLegend ? undefined : 'bottom'}
padding={{ bottom: disableLegend ? 0 : 60, left: 60, right: 0, top: 0 }}
scale={{ x: 'time', y: 'linear' }}
themeColor={ChartThemeColor.multiOrdered}
width={width}
<React.Fragment>
<div
style={{ height: legend === 'table' ? '240px' : legend === 'disabled' ? '336px' : '270px', width: '100%' }}
ref={refChart}
>
<ChartAxis dependentAxis={false} showGrid={false} />
<ChartAxis dependentAxis={true} showGrid={true} label={unit} />
{stacked ? <ChartStack>{series}</ChartStack> : <ChartGroup>{series}</ChartGroup>}
</Chart>
</div>
<Chart
containerComponent={
<CursorVoronoiContainer
cursorDimension="x"
labels={({ datum }: ILabels): string | null => (datum.y ? `${datum.y} ${unit}` : null)}
labelComponent={
<ChartLegendTooltip
legendData={legendData}
title={(point: Data.AsObject): string => formatTime(Math.floor(point.x / 1000))}
/>
}
mouseFollowTooltips
voronoiDimension="x"
voronoiPadding={{ bottom: 30, left: 60, right: 0, top: 0 }}
/>
}
height={height}
legendPosition={undefined}
padding={{ bottom: 30, left: 60, right: 0, top: 0 }}
scale={{ x: 'time', y: 'linear' }}
themeColor={ChartThemeColor.multiOrdered}
width={width}
>
<ChartAxis dependentAxis={false} showGrid={false} />
<ChartAxis dependentAxis={true} showGrid={true} label={unit} />
{stacked ? <ChartStack>{series}</ChartStack> : <ChartGroup>{series}</ChartGroup>}
</Chart>
</div>
<PrometheusChartDefaultLegend
legend={legend}
legendData={legendData}
metrics={metrics}
hiddenMetrics={hiddenMetrics}
toogleMetric={toogleMetric}
/>
</React.Fragment>
);
};

Expand Down
123 changes: 123 additions & 0 deletions app/src/plugins/prometheus/PrometheusChartDefaultLegend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Button, ButtonVariant } from '@patternfly/react-core';
import { ChartThemeColor, getDarkThemeColors } from '@patternfly/react-charts';
import { EyeSlashIcon, SquareIcon } from '@patternfly/react-icons';
import { TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import React from 'react';

import { Metrics } from 'proto/prometheus_grpc_web_pb';

// colors is an array with all the supported colors for a chart. These are the same colors as they are used for the
// bars, lines, areas in a chart.
export const colors = getDarkThemeColors(ChartThemeColor.multiOrdered).area.colorScale;

// getLegendColorClass returns the color class for an item in the legend. When we have more series then colors, we start
// again with the first color.
const getLegendColorClass = (index: number): string => {
return colors[index % colors.length];
};

interface ILegendItem {
childName: string;
name: string;
}

export interface IPrometheusChartDefaultLegendProps {
legend: string;
legendData: ILegendItem[];
metrics: Metrics.AsObject[];
hiddenMetrics: string[];
toogleMetric: (index: string) => void;
}

//
const PrometheusChartDefaultLegend: React.FunctionComponent<IPrometheusChartDefaultLegendProps> = ({
legend,
legendData,
metrics,
hiddenMetrics,
toogleMetric,
}: IPrometheusChartDefaultLegendProps) => {
if (legend === 'disabled') {
return null;
}

if (legend === 'table') {
return (
<div className="pf-u-mt-md" style={{ height: '80px', overflow: 'scroll' }}>
<TableComposable aria-label="Legend" variant={TableVariant.compact} borders={false}>
<Thead>
<Tr>
<Th style={{ fontSize: '12px', padding: 0 }}>Name</Th>
<Th style={{ fontSize: '12px', padding: 0 }}>Min</Th>
<Th style={{ fontSize: '12px', padding: 0 }}>Max</Th>
<Th style={{ fontSize: '12px', padding: 0 }}>Avg</Th>
<Th style={{ fontSize: '12px', padding: 0 }}>Current</Th>
</Tr>
</Thead>
<Tbody>
{legendData.map((legend, index) => (
<Tr key={index}>
<Td dataLabel="Name" style={{ fontSize: '12px', padding: 0 }}>
<Button
className={hiddenMetrics.includes(legend.childName) ? 'pf-u-color-400' : ''}
style={{ color: 'inherit', textDecoration: 'inherit' }}
variant={ButtonVariant.link}
isInline={true}
icon={
hiddenMetrics.includes(legend.childName) ? (
<EyeSlashIcon />
) : (
<SquareIcon color={getLegendColorClass(index)} />
)
}
onClick={(): void => toogleMetric(legend.childName)}
>
{legend.name}
</Button>
</Td>
<Td dataLabel="Min" style={{ fontSize: '12px', padding: 0 }}>
{metrics[index].min}
</Td>
<Td dataLabel="Max" style={{ fontSize: '12px', padding: 0 }}>
{metrics[index].max}
</Td>
<Td dataLabel="Avg" style={{ fontSize: '12px', padding: 0 }}>
{metrics[index].avg}
</Td>
<Td dataLabel="Current" style={{ fontSize: '12px', padding: 0 }}>
{metrics[index].dataList[metrics[index].dataList.length - 1].y}
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
</div>
);
}

return (
<div className="pf-u-mt-md pf-u-text-align-center" style={{ height: '50px', overflow: 'scroll' }}>
{legendData.map((legend, index) => (
<Button
key={index}
className={`pf-u-mx-md pf-u-font-size-xs ${hiddenMetrics.includes(legend.childName) ? 'pf-u-color-400' : ''}`}
style={{ color: 'inherit', textDecoration: 'inherit' }}
variant={ButtonVariant.link}
isInline={true}
icon={
hiddenMetrics.includes(legend.childName) ? (
<EyeSlashIcon />
) : (
<SquareIcon color={getLegendColorClass(index)} />
)
}
onClick={(): void => toogleMetric(legend.childName)}
>
{legend.name}
</Button>
))}
</div>
);
};

export default PrometheusChartDefaultLegend;
2 changes: 1 addition & 1 deletion app/src/plugins/prometheus/PrometheusPageData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const PrometheusPageData: React.FunctionComponent<IPrometheusPageDataProps> = ({
type={type}
unit=""
stacked={stacked}
disableLegend={true}
legend="disabled"
metrics={selectedMetrics.length === 0 ? metrics : selectedMetrics}
/>

Expand Down
8 changes: 7 additions & 1 deletion app/src/plugins/prometheus/PrometheusPluginChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ const PrometheusPluginChart: React.FunctionComponent<IPrometheusPluginChartProps
) : chart.type === 'sparkline' ? (
<PrometheusChartSparkline unit={chart.unit} metrics={data.metrics} mappings={chart.mappingsMap} />
) : (
<PrometheusChartDefault type={chart.type} unit={chart.unit} stacked={chart.stacked} metrics={data.metrics} />
<PrometheusChartDefault
type={chart.type}
unit={chart.unit}
stacked={chart.stacked}
legend={chart.legend}
metrics={data.metrics}
/>
)}
</CardBody>
</Card>
Expand Down
4 changes: 2 additions & 2 deletions app/src/plugins/prometheus/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ export interface IData {
// transformData is used to transform the returned data from the API. This is needed because, the API returns a NaN
// value for missing values, but Victory requires a null value.
// See: https://formidable.com/open-source/victory/gallery/victory-line-with-null-data/
export const transformData = (data: Data.AsObject[]): IData[] => {
export const transformData = (data: Data.AsObject[], isHidden?: boolean): IData[] => {
return data.map((d) => {
return { x: d.x, y: isNaN(d.y) ? null : d.y };
return { x: d.x, y: isNaN(d.y) || isHidden ? null : d.y };
});
};
4 changes: 4 additions & 0 deletions app/src/proto/prometheus_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ export class Chart extends jspb.Message {
getSize(): number;
setSize(value: number): void;

getLegend(): string;
setLegend(value: string): void;

getMappingsMap(): jspb.Map<string, string>;
clearMappingsMap(): void;
clearQueriesList(): void;
Expand All @@ -353,6 +356,7 @@ export namespace Chart {
unit: string,
stacked: boolean,
size: number,
legend: string,
mappingsMap: Array<[string, string]>,
queriesList: Array<Query.AsObject>,
}
Expand Down
Loading