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 @@ -15,6 +15,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#32](https://github.com/kobsio/kobs/pull/32): Add support for container logs via the Kubernetes API.
- [#34](https://github.com/kobsio/kobs/pull/34): Add a new Custom Resource Definition for Teams. Teams can be used to define the ownership for Applications and other Kubernetes resources. :warning: *Breaking change:* :warning: We are now using the `apiextensions.k8s.io/v1` API for the Custom Resource Definitions of kobs.
- [#39](https://github.com/kobsio/kobs/pull/39): Add Opsgenie plugin to view alerts within an Application.
- [#40](https://github.com/kobsio/kobs/pull/39): Prometheus plugin metric name suggestions.

### Fixed

Expand Down
156 changes: 156 additions & 0 deletions app/src/plugins/prometheus/PrometheusAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Button, ButtonVariant, TextArea } from '@patternfly/react-core';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { TimesIcon } from '@patternfly/react-icons';

import { MetricLookupRequest, MetricLookupResponse, PrometheusPromiseClient } from 'proto/prometheus_grpc_web_pb';
import { apiURL } from 'utils/constants';

// prometheusService is the gRPC service to get the suggestions for the PromQL query.
const prometheusService = new PrometheusPromiseClient(apiURL, null, null);

interface IPrometheusAutocomplete {
name: string;
query: string;
setQuery: (q: string) => void;
onEnter: (e: React.KeyboardEvent<HTMLTextAreaElement> | undefined) => void;
}

// PrometheusAutocomplete is used as input for the PromQL query. The component also fetches a list of suggestions for
// the provided input.
export const PrometheusAutocomplete: React.FunctionComponent<IPrometheusAutocomplete> = ({
name,
query,
setQuery,
onEnter,
}: IPrometheusAutocomplete): JSX.Element => {
const inputRef = useRef<HTMLInputElement>(null);
const [data, setData] = useState<string[]>([]);
const [inputFocused, setInputFocused] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [hoveringMetricNamesList, setHovering] = useState(false);

// onFocus is used to set the inputFocused variable to true, when the TextArea is focused.
const onFocus = (): void => {
setInputFocused(true);
};

// onBlur is used to set the inputFocused variable to false, when the TextArea looses the focus.
const onBlur = (): void => {
setInputFocused(false);
};

// onKeyDown is used to navigate to the suggestion list via the arrow up and arrow down key. When a item is selected
// and the user presses the enter key, the selected item will be used for the query. When no item is selected and the
// user presses the enter key, the search is executed.
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement> | undefined): void => {
if (e?.key === 'ArrowUp') {
if (selectedIndex === 0) {
setSelectedIndex(data.length - 1);
} else {
setSelectedIndex(selectedIndex - 1);
}
} else if (e?.key === 'ArrowDown') {
if (selectedIndex + 1 === data.length) {
setSelectedIndex(0);
} else {
setSelectedIndex(selectedIndex + 1);
}
} else if (e?.key === 'Enter' && selectedIndex > -1) {
e.preventDefault();
setQuery(data[selectedIndex]);
setSelectedIndex(-1);
} else {
onEnter(e);
}
};

// onMouseDown is used, when a user selects an item from the suggestion via the mouse. When the item is selected, we
// switch the focus back to the TextArea component.
const onMouseDown = (result: string): void => {
setQuery(result);
setHovering(false);

setTimeout(() => {
inputRef.current?.focus();
}, 50);
};

// fetchData is used to retrieve the metrics for the given queries in the selected time range with the selected
// resolution.
const fetchData = useCallback(async (): Promise<void> => {
try {
const metricLookupRequest = new MetricLookupRequest();
metricLookupRequest.setName(name);
metricLookupRequest.setMatcher(query);

const metricLookupResponse: MetricLookupResponse = await prometheusService.metricLookup(
metricLookupRequest,
null,
);

setData(metricLookupResponse.toObject().namesList);
} catch (err) {
setData([]);
}
}, [name, query]);

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

return (
<React.Fragment>
<div className="pf-c-search-input" style={{ width: '100%' }}>
<div className="pf-c-search-input__bar">
<span className="pf-c-search-input__text">
<TextArea
ref={inputRef}
aria-label="PromQL Query"
resizeOrientation="vertical"
rows={1}
type="text"
value={query}
onChange={(value): void => setQuery(value)}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
/>
</span>
</div>
{/* Show metric name suggestions only if there are results and the result is not equal to current text value */}
{inputFocused && data.length > 0 && !(data.length === 1 && data[0] === query) && (
<div
className="pf-c-search-input__menu"
onMouseEnter={(): void => setHovering(true)}
onMouseLeave={(): void => setHovering(false)}
>
<ul className="pf-c-search-input__menu-list">
{data.map((result: string, index) => {
return (
<li
className="pf-c-search-input__menu-list-item"
key={result}
style={
selectedIndex === index && !hoveringMetricNamesList ? { backgroundColor: '#f0f0f0' } : undefined
}
>
<button
className="pf-c-search-input__menu-item"
type="button"
onMouseDown={(): void => onMouseDown(result)}
>
<span className="pf-c-search-input__menu-item-text">{result}</span>
</button>
</li>
);
})}
</ul>
</div>
)}
</div>
<Button variant={ButtonVariant.control} onClick={(): void => setQuery('')}>
<TimesIcon />
</Button>
</React.Fragment>
);
};
12 changes: 12 additions & 0 deletions app/src/plugins/prometheus/PrometheusPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { useHistory, useLocation } from 'react-router-dom';
import {
GetMetricsRequest,
GetMetricsResponse,
MetricLookupRequest,
MetricLookupResponse,
Metrics,
PrometheusPromiseClient,
Query,
Expand Down Expand Up @@ -80,6 +82,15 @@ const PrometheusPage: React.FunctionComponent<IPluginPageProps> = ({ name, descr
getMetricsRequest.setResolution(options.resolution);
getMetricsRequest.setQueriesList(queries);

const metricLookupRequest = new MetricLookupRequest();
metricLookupRequest.setName(name);
metricLookupRequest.setMatcher(options.queries[0]);
const metricLookupResponse: MetricLookupResponse = await prometheusService.metricLookup(
metricLookupRequest,
null,
);
console.log(metricLookupResponse.getNamesList());

const getMetricsResponse: GetMetricsResponse = await prometheusService.getMetrics(getMetricsRequest, null);
setData({ error: '', isLoading: false, metrics: getMetricsResponse.toObject().metricsList });
}
Expand Down Expand Up @@ -107,6 +118,7 @@ const PrometheusPage: React.FunctionComponent<IPluginPageProps> = ({ name, descr
</Title>
<p>{description}</p>
<PrometheusPageToolbar
name={name}
queries={options.queries}
resolution={options.resolution}
timeEnd={options.timeEnd}
Expand Down
17 changes: 8 additions & 9 deletions app/src/plugins/prometheus/PrometheusPageToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Flex,
FlexItem,
InputGroup,
TextArea,
Toolbar,
ToolbarContent,
ToolbarGroup,
Expand All @@ -16,17 +15,20 @@ import React, { useState } from 'react';

import Options, { IAdditionalFields } from 'components/Options';
import { IPrometheusOptions } from 'plugins/prometheus/helpers';
import { PrometheusAutocomplete } from './PrometheusAutocomplete';

// IPrometheusPageToolbarProps is the interface for all properties, which can be passed to the PrometheusPageToolbar
// component. This are all available Prometheus options and a function to write changes to these properties back to the
// parent component.
interface IPrometheusPageToolbarProps extends IPrometheusOptions {
name: string;
setOptions: (data: IPrometheusOptions) => void;
}

// PrometheusPageToolbar is the toolbar for the Prometheus plugin page. It allows a user to specify query and to select
// a start time, end time and resolution for the query.
const PrometheusPageToolbar: React.FunctionComponent<IPrometheusPageToolbarProps> = ({
name,
queries,
resolution,
timeEnd,
Expand Down Expand Up @@ -95,14 +97,11 @@ const PrometheusPageToolbar: React.FunctionComponent<IPrometheusPageToolbarProps
{data.queries.map((query, index) => (
<FlexItem key={index}>
<InputGroup>
<TextArea
aria-label={`PromQL Query ${index}`}
resizeOrientation="vertical"
rows={1}
type="text"
value={query}
onChange={(value): void => changeQuery(index, value)}
onKeyDown={onEnter}
<PrometheusAutocomplete
name={name}
query={query}
setQuery={(value): void => changeQuery(index, value)}
onEnter={onEnter}
/>
{index === 0 ? (
<Button variant={ButtonVariant.control} onClick={addQuery}>
Expand Down
80 changes: 80 additions & 0 deletions app/src/proto/prometheus_grpc_web_pb.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,85 @@ proto.plugins.prometheus.PrometheusPromiseClient.prototype.getMetrics =
};


/**
* @const
* @type {!grpc.web.MethodDescriptor<
* !proto.plugins.prometheus.MetricLookupRequest,
* !proto.plugins.prometheus.MetricLookupResponse>}
*/
const methodDescriptor_Prometheus_MetricLookup = new grpc.web.MethodDescriptor(
'/plugins.prometheus.Prometheus/MetricLookup',
grpc.web.MethodType.UNARY,
proto.plugins.prometheus.MetricLookupRequest,
proto.plugins.prometheus.MetricLookupResponse,
/**
* @param {!proto.plugins.prometheus.MetricLookupRequest} request
* @return {!Uint8Array}
*/
function(request) {
return request.serializeBinary();
},
proto.plugins.prometheus.MetricLookupResponse.deserializeBinary
);


/**
* @const
* @type {!grpc.web.AbstractClientBase.MethodInfo<
* !proto.plugins.prometheus.MetricLookupRequest,
* !proto.plugins.prometheus.MetricLookupResponse>}
*/
const methodInfo_Prometheus_MetricLookup = new grpc.web.AbstractClientBase.MethodInfo(
proto.plugins.prometheus.MetricLookupResponse,
/**
* @param {!proto.plugins.prometheus.MetricLookupRequest} request
* @return {!Uint8Array}
*/
function(request) {
return request.serializeBinary();
},
proto.plugins.prometheus.MetricLookupResponse.deserializeBinary
);


/**
* @param {!proto.plugins.prometheus.MetricLookupRequest} request The
* request proto
* @param {?Object<string, string>} metadata User defined
* call metadata
* @param {function(?grpc.web.Error, ?proto.plugins.prometheus.MetricLookupResponse)}
* callback The callback function(error, response)
* @return {!grpc.web.ClientReadableStream<!proto.plugins.prometheus.MetricLookupResponse>|undefined}
* The XHR Node Readable Stream
*/
proto.plugins.prometheus.PrometheusClient.prototype.metricLookup =
function(request, metadata, callback) {
return this.client_.rpcCall(this.hostname_ +
'/plugins.prometheus.Prometheus/MetricLookup',
request,
metadata || {},
methodDescriptor_Prometheus_MetricLookup,
callback);
};


/**
* @param {!proto.plugins.prometheus.MetricLookupRequest} request The
* request proto
* @param {?Object<string, string>} metadata User defined
* call metadata
* @return {!Promise<!proto.plugins.prometheus.MetricLookupResponse>}
* Promise that resolves to the response
*/
proto.plugins.prometheus.PrometheusPromiseClient.prototype.metricLookup =
function(request, metadata) {
return this.client_.unaryCall(this.hostname_ +
'/plugins.prometheus.Prometheus/MetricLookup',
request,
metadata || {},
methodDescriptor_Prometheus_MetricLookup);
};


module.exports = proto.plugins.prometheus;

46 changes: 46 additions & 0 deletions app/src/proto/prometheus_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,52 @@ export namespace GetMetricsResponse {
}
}

export class MetricLookupRequest extends jspb.Message {
getName(): string;
setName(value: string): void;

getMatcher(): string;
setMatcher(value: string): void;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): MetricLookupRequest.AsObject;
static toObject(includeInstance: boolean, msg: MetricLookupRequest): MetricLookupRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: MetricLookupRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): MetricLookupRequest;
static deserializeBinaryFromReader(message: MetricLookupRequest, reader: jspb.BinaryReader): MetricLookupRequest;
}

export namespace MetricLookupRequest {
export type AsObject = {
name: string,
matcher: string,
}
}

export class MetricLookupResponse extends jspb.Message {
clearNamesList(): void;
getNamesList(): Array<string>;
setNamesList(value: Array<string>): void;
addNames(value: string, index?: number): string;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): MetricLookupResponse.AsObject;
static toObject(includeInstance: boolean, msg: MetricLookupResponse): MetricLookupResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: MetricLookupResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): MetricLookupResponse;
static deserializeBinaryFromReader(message: MetricLookupResponse, reader: jspb.BinaryReader): MetricLookupResponse;
}

export namespace MetricLookupResponse {
export type AsObject = {
namesList: Array<string>,
}
}

export class Metrics extends jspb.Message {
getLabel(): string;
setLabel(value: string): void;
Expand Down
Loading