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 @@ -19,6 +19,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#16](https://github.com/kobsio/kobs/pull/16): Add support for multiple queries in the Prometheus plugin page.
- [#18](https://github.com/kobsio/kobs/pull/18): Add metrics and logs for the gRPC server.
- [#19](https://github.com/kobsio/kobs/pull/19): Use multiple colors in the Jaeger plugin. Each service in a trace has a unique color now, which is used for the charts.
- [#21](https://github.com/kobsio/kobs/pull/21): Add preview for Applications via plugins.

### Fixed

Expand Down
9 changes: 8 additions & 1 deletion app/src/components/applications/ApplicationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Card, CardBody, CardTitle } from '@patternfly/react-core';
import React from 'react';

import { Application } from 'proto/application_pb';
import Preview from 'components/plugins/Preview';

interface IApplicationItemProps {
application: Application.AsObject;
Expand All @@ -19,7 +20,13 @@ const ApplicationItem: React.FunctionComponent<IApplicationItemProps> = ({
<Card style={{ cursor: 'pointer' }} isHoverable={true} onClick={(): void => selectApplication(application)}>
<CardTitle>{application.name}</CardTitle>
<CardBody>
{application.details ? application.details.description : `${application.namespace} (${application.cluster})`}
{application.details && application.details.plugin ? (
<Preview plugin={application.details.plugin} />
) : application.details ? (
application.details.description
) : (
`${application.namespace} (${application.cluster})`
)}
</CardBody>
</Card>
);
Expand Down
29 changes: 29 additions & 0 deletions app/src/components/plugins/Preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Alert, AlertVariant } from '@patternfly/react-core';
import React, { useContext } from 'react';

import { IPluginsContext, PluginsContext } from 'context/PluginsContext';
import { Plugin as IPlugin } from 'proto/plugins_grpc_web_pb';
import { plugins } from 'utils/plugins';

interface IPreviewProps {
plugin: IPlugin.AsObject;
}

const Preview: React.FunctionComponent<IPreviewProps> = ({ plugin }: IPreviewProps) => {
const pluginsContext = useContext<IPluginsContext>(PluginsContext);
const pluginDetails = pluginsContext.getPluginDetails(plugin.name);

if (!pluginDetails || !plugins.hasOwnProperty(pluginDetails.type)) {
return <Alert variant={AlertVariant.danger} isInline={true} title="Plugin was not found." />;
}

const Component = plugins[pluginDetails.type].preview;

if (!Component) {
return <Alert variant={AlertVariant.danger} isInline={true} title="Plugin doesn't support the preview mode." />;
}

return <Component name={plugin.name} description={pluginDetails.description} plugin={plugin} />;
};

export default Preview;
13 changes: 11 additions & 2 deletions app/src/components/resources/ResourcesListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ const ResourcesListItem: React.FunctionComponent<IResourcesListItemProps> = ({
);
};

export default memo(ResourcesListItem, () => {
return true;
export default memo(ResourcesListItem, (prevProps, nextProps) => {
if (
JSON.stringify(prevProps.clusters) === JSON.stringify(nextProps.clusters) &&
JSON.stringify(prevProps.namespaces) === JSON.stringify(nextProps.namespaces) &&
JSON.stringify(prevProps.resource) === JSON.stringify(nextProps.resource) &&
prevProps.selector === nextProps.selector
) {
return true;
}

return false;
});
19 changes: 19 additions & 0 deletions app/src/plugins/elasticsearch/ElasticsearchPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Alert, AlertVariant } from '@patternfly/react-core';
import React from 'react';

import ElasticsearchPreviewChart from 'plugins/elasticsearch/ElasticsearchPreviewChart';
import { IPluginProps } from 'utils/plugins';

const ElasticsearchPreview: React.FunctionComponent<IPluginProps> = ({ name, description, plugin }: IPluginProps) => {
if (
!plugin.elasticsearch ||
plugin.elasticsearch.queriesList.length !== 1 ||
plugin.elasticsearch.queriesList[0].query === ''
) {
return <Alert variant={AlertVariant.danger} isInline={true} title="Elasticsearch properties are invalid." />;
}

return <ElasticsearchPreviewChart name={name} query={plugin.elasticsearch.queriesList[0]} />;
};

export default ElasticsearchPreview;
93 changes: 93 additions & 0 deletions app/src/plugins/elasticsearch/ElasticsearchPreviewChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Alert, AlertVariant } from '@patternfly/react-core';
import { ChartBar, ChartGroup } from '@patternfly/react-charts';
import React, { useCallback, useEffect, useRef, useState } from 'react';

import {
Bucket,
ElasticsearchPromiseClient,
GetLogsRequest,
GetLogsResponse,
Query,
} from 'proto/elasticsearch_grpc_web_pb';
import { apiURL } from 'utils/constants';

// elasticsearchService is the gRPC service to get the number of documents and buckets for the defined query.
const elasticsearchService = new ElasticsearchPromiseClient(apiURL, null, null);

interface IDataState {
buckets: Bucket.AsObject[];
error: string;
hits: number;
}

interface IElasticsearchPreviewChartProps {
name: string;
query: Query.AsObject;
}

const ElasticsearchPreviewChart: React.FunctionComponent<IElasticsearchPreviewChartProps> = ({
name,
query,
}: IElasticsearchPreviewChartProps) => {
const refChart = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(0);
const [data, setData] = useState<IDataState>({ buckets: [], error: '', hits: 0 });

const fetchData = useCallback(async () => {
try {
const q = new Query();
q.setQuery(query.query);

const getLogsRequest = new GetLogsRequest();
getLogsRequest.setName(name);
getLogsRequest.setScrollid('');
getLogsRequest.setTimeend(Math.floor(Date.now() / 1000));
getLogsRequest.setTimestart(Math.floor(Date.now() / 1000) - 900);
getLogsRequest.setQuery(q);

const getLogsResponse: GetLogsResponse = await elasticsearchService.getLogs(getLogsRequest, null);
const tmpLogsResponse = getLogsResponse.toObject();

setData({ buckets: tmpLogsResponse.bucketsList, error: '', hits: tmpLogsResponse.hits });
} catch (err) {
setData({ buckets: [], error: err.message, hits: 0 });
}
}, [name, query]);

// 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);
}
}, [data.buckets]);

useEffect(() => {
fetchData();
}, [fetchData]);

if (data.error) {
return (
<Alert variant={AlertVariant.danger} isInline={true} title={data.error ? data.error : 'Metrics not found.'} />
);
}

return (
<React.Fragment>
<div className="pf-u-font-size-lg pf-u-text-nowrap pf-u-text-truncate">{data.hits} Hits</div>
{query.name ? (
<div className="pf-u-font-size-sm pf-u-color-400 pf-u-text-nowrap pf-u-text-truncates">{query.name}</div>
) : null}

<div style={{ height: '75px', position: 'relative', width: '100%' }} ref={refChart}>
<ChartGroup height={height} padding={0} width={width}>
<ChartBar data={data.buckets} name="count" barWidth={width / data.buckets.length} />
</ChartGroup>
</div>
</React.Fragment>
);
};

export default ElasticsearchPreviewChart;
19 changes: 19 additions & 0 deletions app/src/plugins/prometheus/PrometheusPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Alert, AlertVariant } from '@patternfly/react-core';
import React from 'react';

import { IPluginProps } from 'utils/plugins';
import PrometheusPreviewChart from 'plugins/prometheus/PrometheusPreviewChart';

const PrometheusPreview: React.FunctionComponent<IPluginProps> = ({ name, description, plugin }: IPluginProps) => {
if (
!plugin.prometheus ||
plugin.prometheus.chartsList.length !== 1 ||
plugin.prometheus.chartsList[0].queriesList.length !== 1
) {
return <Alert variant={AlertVariant.danger} isInline={true} title="Prometheus properties are invalid." />;
}

return <PrometheusPreviewChart name={name} chart={plugin.prometheus.chartsList[0]} />;
};

export default PrometheusPreview;
96 changes: 96 additions & 0 deletions app/src/plugins/prometheus/PrometheusPreviewChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Alert, AlertVariant } from '@patternfly/react-core';
import { ChartArea, ChartGroup } from '@patternfly/react-charts';
import React, { useCallback, useEffect, useRef, useState } from 'react';

import {
Chart,
GetMetricsRequest,
GetMetricsResponse,
Metrics,
PrometheusPromiseClient,
Query,
} from 'proto/prometheus_grpc_web_pb';
import { apiURL } from 'utils/constants';

// prometheusService is the gRPC service to get the metrics for the defined query in a chart.
const prometheusService = new PrometheusPromiseClient(apiURL, null, null);

interface IDataState {
error: string;
metrics: Metrics.AsObject[];
}

interface IPrometheusPreviewChartProps {
name: string;
chart: Chart.AsObject;
}

const PrometheusPreviewChart: React.FunctionComponent<IPrometheusPreviewChartProps> = ({
name,
chart,
}: IPrometheusPreviewChartProps) => {
const refChart = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(0);
const [data, setData] = useState<IDataState>({ error: '', metrics: [] });

const fetchData = useCallback(async () => {
try {
const query = new Query();
query.setQuery(chart.queriesList[0].query);
query.setLabel(chart.queriesList[0].label);

const getMetricsRequest = new GetMetricsRequest();
getMetricsRequest.setName(name);
getMetricsRequest.setTimeend(Math.floor(Date.now() / 1000));
getMetricsRequest.setTimestart(Math.floor(Date.now() / 1000) - 900);
getMetricsRequest.setQueriesList([query]);
getMetricsRequest.setVariablesList([]);

const getMetricsResponse: GetMetricsResponse = await prometheusService.getMetrics(getMetricsRequest, null);
setData({ error: '', metrics: getMetricsResponse.toObject().metricsList });
} catch (err) {
setData({ error: err.message, metrics: [] });
}
}, [name, chart]);

// 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);
}
}, [data.metrics]);

useEffect(() => {
fetchData();
}, [fetchData]);

if (data.error || data.metrics.length === 0) {
return (
<Alert variant={AlertVariant.danger} isInline={true} title={data.error ? data.error : 'Metrics not found.'} />
);
}

return (
<React.Fragment>
<div className="pf-u-font-size-lg pf-u-text-nowrap pf-u-text-truncate">
{data.metrics[0].dataList[data.metrics[0].dataList.length - 1].y.toFixed(2)} {chart.unit}
</div>
{chart.title ? (
<div className="pf-u-font-size-sm pf-u-color-400 pf-u-text-nowrap pf-u-text-truncates">{chart.title}</div>
) : null}

<div style={{ height: '75px', position: 'relative', width: '100%' }} ref={refChart}>
<ChartGroup height={height} padding={0} width={width}>
{data.metrics.map((metric, index) => (
<ChartArea key={index} data={metric.dataList} interpolation="monotoneX" name={`index${index}`} />
))}
</ChartGroup>
</div>
</React.Fragment>
);
};

export default PrometheusPreviewChart;
6 changes: 6 additions & 0 deletions app/src/proto/application_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export class Details extends jspb.Message {
setLinksList(value: Array<Link>): void;
addLinks(value?: Link, index?: number): Link;

hasPlugin(): boolean;
clearPlugin(): void;
getPlugin(): plugins_pb.Plugin | undefined;
setPlugin(value?: plugins_pb.Plugin): void;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Details.AsObject;
static toObject(includeInstance: boolean, msg: Details): Details.AsObject;
Expand All @@ -73,6 +78,7 @@ export namespace Details {
export type AsObject = {
description: string,
linksList: Array<Link.AsObject>,
plugin?: plugins_pb.Plugin.AsObject,
}
}

Expand Down
Loading