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 @@ -12,6 +12,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan

- [#4](https://github.com/kobsio/kobs/pull/4): Add Custom Resource Definition for Applications.
- [#6](https://github.com/kobsio/kobs/pull/6): Add Prometheus as datasource for Application metrics.
- [#8](https://github.com/kobsio/kobs/pull/8): Add new page to directly query a configured Prometheus datasource.

### Fixed

Expand Down
Binary file added app/public/img/datasources/prometheus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import '@patternfly/patternfly/patternfly-charts.css';

import Application from 'components/applications/Application';
import Applications from 'components/applications/Applications';
import Datasource from 'components/datasources/Datasource';
import Datasources from 'components/datasources/Datasources';
import HeaderLogo from 'components/shared/HeaderLogo';
import Overview from 'components/overview/Overview';
import Resources from 'components/resources/Resources';
Expand All @@ -26,6 +28,8 @@ const App: React.FunctionComponent = () => {
<Route exact={true} path="/" component={Overview} />
<Route exact={true} path="/applications" component={Applications} />
<Route exact={true} path="/applications/:cluster/:namespace/:name" component={Application} />
<Route exact={true} path="/datasources" component={Datasources} />
<Route exact={true} path="/datasources/:type/:name" component={Datasource} />
<Route exact={true} path="/resources/:kind" component={Resources} />
</Switch>
</Page>
Expand Down
36 changes: 7 additions & 29 deletions app/src/components/applications/details/TabsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,19 @@ import { IDatasourceOptions } from 'utils/proto';
import Metrics from 'components/applications/details/metrics/Metrics';
import Resources from 'components/applications/details/resources/Resources';

// IParsedDatasourceOptions is the interface for the parsed query parameters. It must contain the same keys as the
// IDatasourceOptions options, but all keys must be of type string.
interface IParsedDatasourceOptions {
resolution: string;
timeEnd: string;
timeStart: string;
}

// datasourceOptionsFromLocationSearch is used to parse all query parameters during the first rendering of the
// TabsContent component. When the parameters are not set we return some default options for the datasources. Because it
// could happen that only some parameters are set via location.search, we have to check each property if it contains a
// valid value. If this is the case we are overwriting the default value.
const datasourceOptionsFromLocationSearch = (): IDatasourceOptions => {
const search = window.location.search;
const options: IDatasourceOptions = {
resolution: '',
timeEnd: Math.floor(Date.now() / 1000),
timeStart: Math.floor(Date.now() / 1000) - 3600,
const params = new URLSearchParams(window.location.search);
return {
resolution: params.get('resolution') ? (params.get('resolution') as string) : '',
timeEnd: params.get('timeEnd') ? parseInt(params.get('timeEnd') as string) : Math.floor(Date.now() / 1000),
timeStart: params.get('timeStart')
? parseInt(params.get('timeStart') as string)
: Math.floor(Date.now() / 1000) - 3600,
};

if (search !== '') {
try {
const parsedOptions: IParsedDatasourceOptions = JSON.parse(
'{"' + search.substr(1).replace(/&/g, '", "').replace(/=/g, '": "') + '"}',
);

if (parsedOptions.resolution) options.resolution = parsedOptions.resolution;
if (parsedOptions.timeEnd) options.timeEnd = parseInt(parsedOptions.timeEnd);
if (parsedOptions.timeStart) options.timeStart = parseInt(parsedOptions.timeStart);
} catch (err) {
return options;
}
}

return options;
};

// createSearch creates a string, which can be used within the history.push function as search parameter. For that we
Expand Down
36 changes: 32 additions & 4 deletions app/src/components/applications/details/metrics/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Alert, AlertActionLink, AlertVariant, Card, CardBody, CardTitle } from '@patternfly/react-core';
import {
Alert,
AlertActionLink,
AlertVariant,
Card,
CardActions,
CardBody,
CardHeader,
CardHeaderMain,
} from '@patternfly/react-core';
import React, { useCallback, useEffect, useState } from 'react';

import { DatasourceMetrics, GetMetricsRequest, GetMetricsResponse } from 'generated/proto/datasources_pb';
Expand All @@ -8,6 +17,7 @@ import {
convertApplicationMetricsVariablesToProto,
convertDatasourceOptionsToProto,
} from 'utils/proto';
import Actions from 'components/applications/details/metrics/charts/Actions';
import { ApplicationMetricsChart } from 'generated/proto/application_pb';
import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb';
import DefaultChart from 'components/applications/details/metrics/charts/Default';
Expand All @@ -19,6 +29,7 @@ const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null);

interface IChartProps {
datasourceName: string;
datasourceType: string;
datasourceOptions: IDatasourceOptions;
variables: IApplicationMetricsVariable[];
chart: ApplicationMetricsChart;
Expand All @@ -27,11 +38,13 @@ interface IChartProps {
// Chart component is used to fetch the data for an chart and to render the chart within a Card component.
const Chart: React.FunctionComponent<IChartProps> = ({
datasourceName,
datasourceType,
datasourceOptions,
variables,
chart,
}: IChartProps) => {
const [data, setData] = useState<DatasourceMetrics[]>([]);
const [interpolatedQueries, setInterpolatedQueries] = useState<string[]>([]);
const [error, setError] = useState<string>('');

// fetchData fetchs the data for a chart. If the gRPC call returns an error, we catch the error and set the
Expand All @@ -47,6 +60,7 @@ const Chart: React.FunctionComponent<IChartProps> = ({

const getMetricsResponse: GetMetricsResponse = await datasourcesService.getMetrics(getMetricsRequest, null);

setInterpolatedQueries(getMetricsResponse.getInterpolatedqueriesList());
setData(getMetricsResponse.getMetricsList());
setError('');
}
Expand All @@ -63,7 +77,9 @@ const Chart: React.FunctionComponent<IChartProps> = ({
if (error) {
return (
<Card isFlat={true}>
<CardTitle>{chart.getTitle()}</CardTitle>
<CardHeader>
<CardHeaderMain>{chart.getTitle()}</CardHeaderMain>
</CardHeader>
<CardBody>
<Alert
variant={AlertVariant.danger}
Expand All @@ -87,7 +103,9 @@ const Chart: React.FunctionComponent<IChartProps> = ({
if (data.length === 0) {
return (
<Card isFlat={true}>
<CardTitle>{chart.getTitle()}</CardTitle>
<CardHeader>
<CardHeaderMain>{chart.getTitle()}</CardHeaderMain>
</CardHeader>
<CardBody>
<EmptyStateSpinner />
</CardBody>
Expand All @@ -97,7 +115,17 @@ const Chart: React.FunctionComponent<IChartProps> = ({

return (
<Card isFlat={true}>
<CardTitle>{chart.getTitle()}</CardTitle>
<CardHeader>
<CardHeaderMain>{chart.getTitle()}</CardHeaderMain>
<CardActions>
<Actions
datasourceName={datasourceName}
datasourceType={datasourceType}
datasourceOptions={datasourceOptions}
interpolatedQueries={interpolatedQueries}
/>
</CardActions>
</CardHeader>
<CardBody>
{chart.getType() === 'sparkline' ? (
<SparklineChart unit={chart.getUnit()} metrics={data} />
Expand Down
3 changes: 3 additions & 0 deletions app/src/components/applications/details/metrics/Charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Chart from 'components/applications/details/metrics/Chart';

interface IChartsProps {
datasourceName: string;
datasourceType: string;
datasourceOptions: IDatasourceOptions;
variables: IApplicationMetricsVariable[];
charts: ApplicationMetricsChart[];
Expand All @@ -17,6 +18,7 @@ interface IChartsProps {
// for each chart. If the width is below this value each chart will be rendered accross the complete width of the grid.
const Charts: React.FunctionComponent<IChartsProps> = ({
datasourceName,
datasourceType,
datasourceOptions,
variables,
charts,
Expand Down Expand Up @@ -51,6 +53,7 @@ const Charts: React.FunctionComponent<IChartsProps> = ({
) : (
<Chart
datasourceName={datasourceName}
datasourceType={datasourceType}
datasourceOptions={datasourceOptions}
variables={variables}
chart={chart}
Expand Down
12 changes: 9 additions & 3 deletions app/src/components/applications/details/metrics/Metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ const Metrics: React.FunctionComponent<IMetricsProps> = ({
null,
);

setDatasourceName(getDatasourceResponse.getName());
setDatasourceType(getDatasourceResponse.getType());
setError('');
const datasource = getDatasourceResponse.getDatasource();
if (datasource) {
setDatasourceName(datasource.getName());
setDatasourceType(datasource.getType());
setError('');
} else {
throw new Error('Datasource is not defined.');
}
}
} catch (err) {
setError(err.message);
Expand Down Expand Up @@ -113,6 +118,7 @@ const Metrics: React.FunctionComponent<IMetricsProps> = ({

<Charts
datasourceName={datasourceName}
datasourceType={datasourceType}
datasourceOptions={datasourceOptions}
variables={variables}
charts={metrics.getChartsList()}
Expand Down
47 changes: 47 additions & 0 deletions app/src/components/applications/details/metrics/charts/Actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';

import { IDatasourceOptions } from 'utils/proto';

interface IActionsProps {
datasourceName: string;
datasourceType: string;
datasourceOptions: IDatasourceOptions;
interpolatedQueries: string[];
}

// Actions is a dropdown component, which provides various actions for a chart. For example it can be used to display a
// link for each query in the chart. When the user click on the link, he will be redirected to the corresponding
// datasource page, with the query and time data prefilled.
const Actions: React.FunctionComponent<IActionsProps> = ({
datasourceName,
datasourceType,
datasourceOptions,
interpolatedQueries,
}: IActionsProps) => {
const [show, setShow] = useState<boolean>(false);

return (
<Dropdown
toggle={<KebabToggle onToggle={(): void => setShow(!show)} />}
isOpen={show}
isPlain={true}
position="right"
dropdownItems={interpolatedQueries.map((query, index) => (
<DropdownItem
key={index}
component={
<Link
to={`/datasources/${datasourceType}/${datasourceName}?query=${query}&resolution=${datasourceOptions.resolution}&timeEnd=${datasourceOptions.timeEnd}&timeStart=${datasourceOptions.timeStart}`}
>
Explore {query}
</Link>
}
/>
))}
/>
);
};

export default Actions;
13 changes: 10 additions & 3 deletions app/src/components/applications/details/metrics/charts/Default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface IDefaultProps {
type: string;
unit: string;
stacked: boolean;
disableLegend?: boolean;
metrics: DatasourceMetrics[];
}

Expand All @@ -37,7 +38,13 @@ export interface IDefaultProps {
//
// NOTE: Currently it is not possible to select a single time series in the chart. This should be changed in the future,
// by using an interactive legend: https://www.patternfly.org/v4/charts/legend-chart#interactive-legend
const Default: React.FunctionComponent<IDefaultProps> = ({ type, unit, stacked, metrics }: IDefaultProps) => {
const Default: React.FunctionComponent<IDefaultProps> = ({
type,
unit,
stacked,
disableLegend,
metrics,
}: IDefaultProps) => {
const refChart = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const [height, setHeight] = useState<number>(0);
Expand Down Expand Up @@ -83,8 +90,8 @@ const Default: React.FunctionComponent<IDefaultProps> = ({ type, unit, stacked,
}
height={height}
legendData={legendData}
legendPosition="bottom"
padding={{ bottom: 60, left: 60, right: 0, top: 0 }}
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}
Expand Down
44 changes: 44 additions & 0 deletions app/src/components/datasources/Datasource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Alert, AlertActionLink, AlertVariant, PageSection, PageSectionVariants } from '@patternfly/react-core';
import { useHistory, useParams } from 'react-router-dom';
import React from 'react';

import Prometheus from 'components/datasources/prometheus/Prometheus';

interface IDatasourceParams {
type: string;
name: string;
}

// Datasource is the component, which checks the provided type from the URL and renders the corresponding component for
// the datasource type.
const Datasource: React.FunctionComponent = () => {
const params = useParams<IDatasourceParams>();
const history = useHistory();

const goToDatasources = (): void => {
history.push('/');
};

if (params.type === 'prometheus') {
return <Prometheus name={params.name} />;
}

// When the provided datasource type, isn't valid, the user will see the following error, with an action to go back to
// the datasource page.
return (
<PageSection variant={PageSectionVariants.default}>
<Alert
variant={AlertVariant.danger}
isInline={false}
title="Invalid datasource type"
actionLinks={
<React.Fragment>
<AlertActionLink onClick={goToDatasources}>Datasources</AlertActionLink>
</React.Fragment>
}
/>
</PageSection>
);
};

export default Datasource;
Loading