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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#149](https://github.com/kobsio/kobs/pull/149): Add SQL plugin to run queries against a configured SQL database instance. For now we are supporting the `clickhouse`, `postgres` and `mysql` driver.
- [#151](https://github.com/kobsio/kobs/pull/151): Add actions for in log details view of the ClickHouse, so that users can filter based on the value of a field.
- [#156](https://github.com/kobsio/kobs/pull/156): Add tags for applications.
- [#159](https://github.com/kobsio/kobs/pull/159): Allow users to select a time range within the logs chart in the ClickHouse plugin.

### Fixed

Expand Down
4 changes: 2 additions & 2 deletions plugins/clickhouse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
},
"dependencies": {
"@kobsio/plugin-core": "*",
"@nivo/bar": "^0.73.1",
"@nivo/tooltip": "^0.73.0",
"@patternfly/react-charts": "^6.15.23",
"@patternfly/react-core": "^4.128.2",
"@patternfly/react-icons": "^4.10.11",
"@patternfly/react-table": "^4.27.24",
Expand All @@ -21,6 +20,7 @@
"@types/react-router-dom": "^5.1.7",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-intersection-observer": "^8.32.1",
"react-query": "^3.17.2",
"react-router-dom": "^5.2.0",
"typescript": "^4.3.4"
Expand Down
7 changes: 2 additions & 5 deletions plugins/clickhouse/pkg/instance/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,8 @@ func (i *Instance) GetLogs(ctx context.Context, query, order, orderBy string, ti
}

buckets = append(buckets, Bucket{
Interval: intervalData.Unix(),
IntervalFormatted: "",
Count: countData,
// Formatting is handled on the client side.
// IntervalFormatted: intervalData.Format("01-02 15:04:05"),
Interval: intervalData.Unix(),
Count: countData,
})
}

Expand Down
5 changes: 2 additions & 3 deletions plugins/clickhouse/pkg/instance/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ type Row struct {
// Bucket is the struct which is used to represent the distribution of the returned rows for a logs query for the given
// time range.
type Bucket struct {
Interval int64 `json:"interval"`
IntervalFormatted string `json:"intervalFormatted"`
Count int64 `json:"count"`
Interval int64 `json:"interval"`
Count int64 `json:"count"`
}
4 changes: 3 additions & 1 deletion plugins/clickhouse/src/components/page/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface IPageLogsProps {
orderBy: string;
query: string;
addFilter: (filter: string) => void;
changeTime: (times: IPluginTimes) => void;
selectField: (field: string) => void;
times: IPluginTimes;
}
Expand All @@ -41,6 +42,7 @@ const PageLogs: React.FunctionComponent<IPageLogsProps> = ({
query,
addFilter,
selectField,
changeTime,
times,
}: IPageLogsProps) => {
const history = useHistory();
Expand Down Expand Up @@ -136,7 +138,7 @@ const PageLogs: React.FunctionComponent<IPageLogsProps> = ({
<CardActions>{isFetching && <Spinner size="md" />}</CardActions>
</CardHeader>
<CardBody>
<LogsChart buckets={data.buckets} />
<LogsChart buckets={data.buckets} changeTime={changeTime} />
</CardBody>
</Card>

Expand Down
4 changes: 2 additions & 2 deletions plugins/clickhouse/src/components/page/LogsToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ const LogsToolbar: React.FunctionComponent<ILogsToolbarProps> = ({
};

useEffect(() => {
setData({ ...data, query: query });
setData({ ...data, query: query, times: times });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
}, [query, times]);

return (
<Toolbar id="clickhouse-logs-toolbar" style={{ paddingBottom: '0px', zIndex: 300 }}>
Expand Down
7 changes: 6 additions & 1 deletion plugins/clickhouse/src/components/page/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { PageSection, PageSectionVariants, Title } from '@patternfly/react-core'
import React, { useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';

import { IPluginPageProps, IPluginTimes } from '@kobsio/plugin-core';
import { IOptions } from '../../utils/interfaces';
import { IPluginPageProps } from '@kobsio/plugin-core';
import Logs from './Logs';
import LogsToolbar from './LogsToolbar';
import { getOptionsFromSearch } from '../../utils/helpers';
Expand Down Expand Up @@ -47,6 +47,10 @@ const Page: React.FunctionComponent<IPluginPageProps> = ({ name, displayName, de
changeOptions({ ...options, query: `${options.query} ${filter}` });
};

const changeTime = (times: IPluginTimes): void => {
changeOptions({ ...options, times: times });
};

// useEffect is used to set the options every time the search location for the current URL changes. The URL is changed
// via the changeOptions function. When the search location is changed we modify the options state.
useEffect(() => {
Expand Down Expand Up @@ -84,6 +88,7 @@ const Page: React.FunctionComponent<IPluginPageProps> = ({ name, displayName, de
order={options.order}
orderBy={options.orderBy}
addFilter={addFilter}
changeTime={changeTime}
selectField={selectField}
times={options.times}
/>
Expand Down
170 changes: 87 additions & 83 deletions plugins/clickhouse/src/components/panel/LogsChart.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,101 @@
import React from 'react';
import { ResponsiveBarCanvas } from '@nivo/bar';
import { SquareIcon } from '@patternfly/react-icons';
import { TooltipWrapper } from '@nivo/tooltip';
import {
Chart,
ChartAxis,
ChartBar,
ChartLegendTooltip,
ChartThemeColor,
createContainer,
} from '@patternfly/react-charts';
import React, { useEffect, useRef, useState } from 'react';

import { IBucket } from '../../utils/interfaces';
import { IBucket, IDatum, IDomain, ILabel } from '../../utils/interfaces';
import { IPluginTimes, formatTime } from '@kobsio/plugin-core';

interface ILogsChartProps {
buckets?: IBucket[];
changeTime?: (times: IPluginTimes) => void;
}

const LogsChart: React.FunctionComponent<ILogsChartProps> = ({ buckets }: ILogsChartProps) => {
if (!buckets || buckets.length === 0) {
const LogsChart: React.FunctionComponent<ILogsChartProps> = ({ buckets, changeTime }: ILogsChartProps) => {
const refChart = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(0);

// 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.
useEffect(() => {
if (refChart && refChart.current) {
setWidth(refChart.current.getBoundingClientRect().width);
setHeight(refChart.current.getBoundingClientRect().height);
}
}, []);

const data: IDatum[] | undefined =
!buckets || buckets.length === 0
? undefined
: buckets.map((bucket) => {
return {
x: new Date(bucket.interval * 1000),
y: bucket.count,
};
});

if (!data) {
return <div style={{ height: '250px' }}></div>;
}

const data: IBucket[] = buckets.map((bucket) => {
const d = new Date(bucket.interval * 1000);

return {
count: bucket.count,
interval: bucket.interval,
intervalFormatted: `${('0' + (d.getMonth() + 1)).slice(-2)}-${('0' + d.getDate()).slice(-2)} ${(
'0' + d.getHours()
).slice(-2)}:${('0' + d.getMinutes()).slice(-2)}:${('0' + d.getSeconds()).slice(-2)}`,
};
});
const CursorVoronoiContainer = changeTime
? createContainer('voronoi', 'brush')
: createContainer('voronoi', 'cursor');
const legendData = [{ childName: 'count', name: 'Document Count' }];

return (
<div style={{ height: '250px' }}>
<ResponsiveBarCanvas
axisBottom={{
legend: '',
tickValues: data.filter((bucket, index) => index % 2 === 0).map((bucket) => bucket.intervalFormatted),
}}
axisLeft={{
format: '>-.0s',
legend: 'Count',
legendOffset: -40,
legendPosition: 'middle',
}}
borderColor={{ from: 'color', modifiers: [['darker', 1.6]] }}
borderRadius={0}
borderWidth={0}
colorBy="id"
colors={['#0066cc']}
data={data}
enableLabel={false}
enableGridX={false}
enableGridY={true}
groupMode="stacked"
indexBy="intervalFormatted"
indexScale={{ round: true, type: 'band' }}
isInteractive={true}
keys={['count']}
layout="vertical"
margin={{ bottom: 25, left: 50, right: 0, top: 0 }}
maxValue="auto"
minValue="auto"
reverse={false}
theme={{
background: '#ffffff',
fontFamily: 'RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif',
fontSize: 10,
textColor: '#000000',
}}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
tooltip={(tooltip) => {
const isFirstHalf = tooltip.index < data.length / 2;

return (
<TooltipWrapper anchor={isFirstHalf ? 'right' : 'left'} position={[0, 20]}>
<div
className="pf-u-box-shadow-sm"
style={{
background: '#ffffff',
fontSize: '12px',
padding: '12px',
whiteSpace: 'nowrap',
}}
>
<div>
<b>{tooltip.data.intervalFormatted}</b>
</div>
<div>
<SquareIcon color="#0066cc" /> Documents: {tooltip.data.count || 0}
</div>
</div>
</TooltipWrapper>
);
}}
valueFormat=""
valueScale={{ type: 'linear' }}
/>
<div style={{ height: '250px' }} ref={refChart}>
<Chart
containerComponent={
<CursorVoronoiContainer
cursorDimension="x"
brushDimension="x"
labels={({ datum }: ILabel): string => `${datum.y}`}
labelComponent={
<ChartLegendTooltip
legendData={legendData}
title={(datum: IDatum): string => formatTime(Math.floor(datum.x.getTime() / 1000))}
/>
}
mouseFollowTooltips={true}
onBrushDomainChangeEnd={(domain: IDomain): void => {
if (changeTime && domain.x.length === 2) {
changeTime({
time: 'custom',
timeEnd: Math.floor(domain.x[1].getTime() / 1000),
timeStart: Math.floor(domain.x[0].getTime() / 1000),
});
}
}}
voronoiDimension="x"
voronoiPadding={0}
/>
}
height={height}
legendData={legendData}
legendPosition={undefined}
padding={{ bottom: 30, left: 0, right: 0, top: 0 }}
scale={{ x: 'time', y: 'linear' }}
themeColor={ChartThemeColor.multiOrdered}
width={width}
>
<ChartAxis
dependentAxis={false}
tickFormat={(tick: Date): string =>
`${('0' + (tick.getMonth() + 1)).slice(-2)}-${('0' + tick.getDate()).slice(-2)} ${(
'0' + tick.getHours()
).slice(-2)}:${('0' + tick.getMinutes()).slice(-2)}:${('0' + tick.getSeconds()).slice(-2)}`
}
showGrid={false}
/>
<ChartBar data={data} name="count" barWidth={width / data.length} />
</Chart>
</div>
);
};
Expand Down
22 changes: 18 additions & 4 deletions plugins/clickhouse/src/utils/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { BarDatum } from '@nivo/bar';

import { IPluginTimes } from '@kobsio/plugin-core';

// IOptions is the interface for all options, which can be set for an ClickHouse query.
Expand Down Expand Up @@ -41,8 +39,24 @@ export interface IDocument {
[key: string]: any;
}

export interface IBucket extends BarDatum {
export interface IBucket {
interval: number;
intervalFormatted: string;
count: number;
}

// IDatum, ILabel and IDomain interfaces are used for the logs chart. IDatum is the formate of the data points required
// by '@patternfly/react-charts. ILabel is the formate of the label and IDomain is the formate returned by the
// onBrushDomainChangeEnd function.
export interface IDatum {
x: Date;
y: number;
}

export interface ILabel {
datum: IDatum;
}

export interface IDomain {
x: Date[];
y: number[];
}
Loading