Skip to content

Commit

Permalink
[core] Rework variables handling (#262)
Browse files Browse the repository at this point in the history
This commit changes the variables handling, like it was discussed in
issue #250. This means that each plugin can now export a "variables"
function to load variables in a dashboard. The only exception is the
"core" plugin, where the variables are still handled within the
Dashboards component.

This change allows us, to introduced variables for more plugins, like a
list of resource groups via the Azure plugin or a list of field values
via the klogs plugin.

We also adjusted the naming and location of some interfaces for this
change. So that the interfaces for the CRDs are now prefixed with the
name of the CRD (e.g. "IPlaceholder" becomes "IDashboardPlaceholder").
This was necessary to reduce conflicts in names and to avoid cycle
imports.

The documentation for the variables options which can be used in a
dashboard, should be placed on the corresponding plugin page and a link
should be added to the "Variable Plugin Options" section in the
dashboards documentation.

Closes #250.
  • Loading branch information
ricoberger committed Dec 28, 2021
1 parent 53665a6 commit a9d15fb
Show file tree
Hide file tree
Showing 23 changed files with 198 additions and 156 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#241](https://github.com/kobsio/kobs/pull/241): [core] :warning: _Breaking change:_ :warning: Rework authentication / authorization middleware and adjust the Custom Resource Definition for Users and Teams.
- [#236](https://github.com/kobsio/kobs/pull/236): [core] Improve filtering in select components for various plugins.
- [#260](https://github.com/kobsio/kobs/pull/260): [opsgenie] Adjust permission handling and add actions for incidents.
- [#262](https://github.com/kobsio/kobs/pull/262): [core] Rework variables handling in dashboards.

## [v0.7.0](https://github.com/kobsio/kobs/releases/tag/v0.7.0) (2021-11-19)

Expand Down
11 changes: 11 additions & 0 deletions docs/plugins/prometheus.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ The y axis can be customized for line and area charts. It is possible to use the
| unit | string | An optional unit for the column values. | No |
| mappings | map<string, string> | Specify value mappings for the column. **Note:** The value must be provided as string (e.g. `"1": "Green"`). | No |

## Variables

If the Prometheus plugin is used to set variables in a dashboard, the following options can be used.

| Field | Type | Description | Required |
| ----- | ---- | ----------- | -------- |
| type | string | The query type to get the values for the variable. At the moment this must be `labelValues` | Yes |
| label | string | The Prometheus label which should be used to get the values for the variable. | Yes |
| query | string | The PromQL query. | Yes |
| allowAll | boolean | If this is set to `true` an additional option for the variable will be added, which contains all other values. | No |

## Example

The following dashboard, shows the CPU and Memory usage of a selected Pod. When this dashboard is used in via a team or application, it is possible to set the namespace and a regular expression to pre select all the Pods. These values are then used to get the names of all Pods and a user can then select the name of a Pod via the `var_pod` variable.
Expand Down
9 changes: 2 additions & 7 deletions docs/resources/dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,9 @@ If the `core` plugin is used to get the values for a variable the options from t
- myvalue3
```

If a Prometheus instance is used to get the variable values, the options from the following table can be used.
It is also possible to use other plugins, to get a list of variable values. These plugins are:

| Field | Type | Description | Required |
| ----- | ---- | ----------- | -------- |
| type | string | The query type to get the values for the variable. At the moment this must be `labelValues` | Yes |
| label | string | The Prometheus label which should be used to get the values for the variable. | Yes |
| query | string | The PromQL query. | Yes |
| allowAll | boolean | If this is set to `true` an additional option for the variable will be added, which contains all other values. | No |
- [Prometheus](../plugins/prometheus.md#variables)

### Row

Expand Down
7 changes: 7 additions & 0 deletions plugins/core/src/context/PluginsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react
import { QueryObserverResult, useQuery } from 'react-query';
import React from 'react';

import { IDashboardVariableValues } from '../crds/dashboard';

// TTime is the type with all possible values for the time property. A value of "custom" identifies that a user
// specified a custom start and end time via the text input fields. The other values are used for the quick select
// options.
Expand Down Expand Up @@ -97,6 +99,11 @@ export interface IPluginComponent {
page?: React.FunctionComponent<IPluginPageProps>;
panel: React.FunctionComponent<IPluginPanelProps>;
preview?: React.FunctionComponent<IPluginPreviewProps>;
variables?: (
variable: IDashboardVariableValues,
variables: IDashboardVariableValues[],
times: IPluginTimes,
) => Promise<IDashboardVariableValues>;
}

// IPlugins is the interface for a list of plugins. The key of this interface is the plugin type and must correspond
Expand Down
8 changes: 4 additions & 4 deletions plugins/core/src/crds/application.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IReference as IDashboardReference, IPlugin } from './dashboard';
import { IDashboardPlugin, IDashboardReference } from './dashboard';

// IApplication implements the Application CR, which can be created by a user to describe an application. While we have
// to omit the cluster, namespace and name field in the Go implementation of the CR, we can assume that these fields are
Expand All @@ -12,13 +12,13 @@ export interface IApplication {
links?: IApplicationLink[];
teams?: IApplicationReference[];
dependencies?: IApplicationReference[];
preview?: IPreview;
preview?: IApplicationPreview;
dashboards?: IDashboardReference[];
}

export interface IPreview {
export interface IApplicationPreview {
title: string;
plugin: IPlugin;
plugin: IDashboardPlugin;
}

export interface IApplicationLink {
Expand Down
44 changes: 26 additions & 18 deletions plugins/core/src/crds/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,69 @@ export interface IDashboard {
name: string;
title: string;
description?: string;
placeholders?: IPlaceholder[];
variables?: IVariable[];
rows: IRow[];
placeholders?: IDashboardPlaceholder[];
variables?: IDashboardVariable[];
rows: IDashboardRow[];
}

export interface IPlaceholder {
export interface IDashboardPlaceholder {
name: string;
description?: string;
}

export interface IVariable {
export interface IDashboardVariable {
name: string;
label?: string;
hide?: boolean;
plugin: IPlugin;
plugin: IDashboardPlugin;
}

export interface IRow {
export interface IDashboardRow {
title?: string;
description?: string;
size?: number;
panels: IPanel[];
panels: IDashboardPanel[];
}

export interface IPanel {
export interface IDashboardPanel {
title: string;
description?: string;
colSpan?: number;
rowSpan?: number;
plugin: IPlugin;
plugin: IDashboardPlugin;
}

export interface IPlugin {
export interface IDashboardPlugin {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options?: any;
}

// IReference is the interface for a dashboard reference in the Team or Application CR. If the cluster or namespace is
// not specified in the reference we assume the dashboard is in the same namespace as the team or application.
export interface IReference {
// IDashboardReference is the interface for a dashboard reference in the Team or Application CR. If the cluster or
// namespace is not specified in the reference we assume the dashboard is in the same namespace as the team or
// application.
export interface IDashboardReference {
cluster?: string;
namespace?: string;
name?: string;
title: string;
description?: string;
placeholders?: IPlaceholders;
inline?: IReferenceInline;
inline?: IDashboardReferenceInline;
}

export interface IPlaceholders {
[key: string]: string;
}

export interface IReferenceInline {
variables?: IVariable[];
rows: IRow[];
export interface IDashboardReferenceInline {
variables?: IDashboardVariable[];
rows: IDashboardRow[];
}

// IDashboardVariableValues is an extension of the IDashboardVariable interface. It contains the additional fields for the
// selected variable value and all possible variable values.
export interface IDashboardVariableValues extends IDashboardVariable {
value: string;
values: string[];
}
2 changes: 1 addition & 1 deletion plugins/core/src/crds/team.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IReference as IDashboardReference } from './dashboard';
import { IDashboardReference } from './dashboard';

// The ITeam interface implements the Team CRD.
export interface ITeam {
Expand Down
1 change: 1 addition & 0 deletions plugins/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from './utils/chart';
export * from './utils/colors';
export * from './utils/fileDownload';
export * from './utils/gravatar';
export * from './utils/interpolate';
export * from './utils/manifests';
export * from './utils/resources';
export * from './utils/time';
Expand Down
53 changes: 53 additions & 0 deletions plugins/core/src/utils/interpolate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IDashboardVariableValues } from '../crds/dashboard';
import { IPluginTimes } from '../context/PluginsContext';

// IVariables is a map of variable names with the current value. This interface should only be used by the interpolate
// function, to convert a given array of variables to the format, which is required by the function.
interface IVariables {
[key: string]: string;
}

// interpolate is used to replace the variables in a given string with the current value for this variable. Before we
// can replace the variables in a string we have to convert the array of variables to a map of variable names and there
// value.
// The default interpolator/delimiter is "{%" and "%}", so that it doesn't conflict with the delimiter used for the
// placeholder. We can not use the same, because the are replaced at different points in our app logic.
// See: https://stackoverflow.com/a/57598892/4104109
export const interpolate = (
str: string,
variables: IDashboardVariableValues[],
times: IPluginTimes,
interpolator: string[] = ['{%', '%}'],
): string => {
const vars: IVariables = {};

for (const variable of variables) {
vars[variable.name] = variable.value;
}

vars['__timeStart'] = `${times.timeStart}`;
vars['__timeEnd'] = `${times.timeEnd}`;

return str
.split(interpolator[0])
.map((s1, i) => {
if (i === 0) {
return s1;
}

const s2 = s1.split(interpolator[1]);
if (s1 === s2[0]) {
return interpolator[0] + s2[0];
}

if (s2.length > 1) {
s2[0] =
s2[0] && vars.hasOwnProperty(s2[0].trim().substring(1))
? vars[s2[0].trim().substring(1)]
: interpolator.join(` ${s2[0]} `);
}

return s2.join('');
})
.join('');
};
60 changes: 18 additions & 42 deletions plugins/dashboards/src/components/dashboards/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import {
ClustersContext,
IClusterContext,
IDashboard,
IDashboardRow,
IDashboardVariableValues,
IPluginDefaults,
IPluginTimes,
IPluginsContext,
IRow,
PluginPanel,
PluginsContext,
interpolate,
} from '@kobsio/plugin-core';
import { interpolate, rowHeight, toGridSpans } from '../../utils/dashboard';
import { rowHeight, toGridSpans } from '../../utils/dashboard';
import DashboardToolbar from './DashboardToolbar';
import { IVariableValues } from '../../utils/interfaces';

interface IDashboardProps {
activeKey: string;
Expand Down Expand Up @@ -51,9 +52,9 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
});

// variables is the state for the dashboard variables. The initial state is undefined or when the user provided some
// variables we convert them to the IVariableValues interface which contains the currently selected value and all
// possible values.
const [variables, setVariables] = useState<IVariableValues[] | undefined>(
// variables we convert them to the IDashboardVariableValues interface which contains the currently selected value and
// all possible values.
const [variables, setVariables] = useState<IDashboardVariableValues[] | undefined>(
dashboard.variables?.map((variable) => {
return {
...variable,
Expand All @@ -71,7 +72,7 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
// Prometheus plugin. For the Prometheus plugin the user must specify the name of the Prometheus instance via the name
// parameter in the options. When the user changes the variables, we keep the old variable values, so that we not have
// to rerender all the panels in the dashboard.
const { isError, error, data, refetch } = useQuery<IVariableValues[] | null, Error>(
const { isError, error, data, refetch } = useQuery<IDashboardVariableValues[] | null, Error>(
['dashboard/variables', dashboard, variables, times, activeKey],
async () => {
if (activeKey !== eventKey) {
Expand Down Expand Up @@ -103,40 +104,13 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
}
} else {
const pluginDetails = pluginsContext.getPluginDetails(tmpVariables[i].plugin.name);
const variablesFunc =
pluginDetails && pluginsContext.components.hasOwnProperty(pluginDetails.type)
? pluginsContext.components[pluginDetails.type].variables
: undefined;

if (pluginDetails?.type === 'prometheus') {
const response = await fetch(`/api/plugins/prometheus/${tmpVariables[i].plugin.name}/variable`, {
body: JSON.stringify({
label: tmpVariables[i].plugin.options.label,
query: interpolate(tmpVariables[i].plugin.options.query, tmpVariables, times),
timeEnd: times.timeEnd,
timeStart: times.timeStart,
type: tmpVariables[i].plugin.options.type,
}),
method: 'post',
});
const json = await response.json();

if (response.status >= 200 && response.status < 300) {
if (json && Array.isArray(json) && json.length > 0) {
if (json && json.length > 1 && tmpVariables[i].plugin.options.allowAll) {
json.unshift(json.join('|'));
}

tmpVariables[i].values = json ? json : [''];
tmpVariables[i].value =
json && json.includes(tmpVariables[i].value) ? tmpVariables[i].value : json ? json[0] : '';
} else {
tmpVariables[i].values = [''];
tmpVariables[i].value = '';
}
} else {
if (json.error) {
throw new Error(json.error);
} else {
throw new Error('An unknown error occured');
}
}
if (variablesFunc) {
tmpVariables[i] = await variablesFunc(tmpVariables[i], tmpVariables, times);
}
}
}
Expand All @@ -152,7 +126,7 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
// We do not use the dashboard.rows array directly to render the dashboard. Instead we are replacing all the variables
// in the dashboard first with users selected values. For that we have to convert the array to a string first so that
// we can replace the variables in the string and then we have to convert it back to an array,
const rows: IRow[] = JSON.parse(interpolate(JSON.stringify(dashboard.rows), data ? data : [], times));
const rows: IDashboardRow[] = JSON.parse(interpolate(JSON.stringify(dashboard.rows), data ? data : [], times));

if (isError) {
return (
Expand All @@ -161,7 +135,9 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
title="Variables were not fetched"
actionLinks={
<React.Fragment>
<AlertActionLink onClick={(): Promise<QueryObserverResult<IVariableValues[] | null, Error>> => refetch()}>
<AlertActionLink
onClick={(): Promise<QueryObserverResult<IDashboardVariableValues[] | null, Error>> => refetch()}
>
Retry
</AlertActionLink>
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Card, ToolbarItem } from '@patternfly/react-core';
import React from 'react';

import { IOptionsAdditionalFields, IPluginTimes, Toolbar } from '@kobsio/plugin-core';
import { IDashboardVariableValues, IOptionsAdditionalFields, IPluginTimes, Toolbar } from '@kobsio/plugin-core';
import DashboardToolbarVariable from './DashboardToolbarVariable';
import { IVariableValues } from '../../utils/interfaces';

interface IDashboardToolbarProps {
variables: IVariableValues[];
setVariables: (variables: IVariableValues[]) => void;
variables: IDashboardVariableValues[];
setVariables: (variables: IDashboardVariableValues[]) => void;
times: IPluginTimes;
setTimes: (times: IPluginTimes) => void;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useState } from 'react';
import { Select, SelectGroup, SelectOption, SelectOptionObject, SelectVariant } from '@patternfly/react-core';

import { IVariableValues } from '../../utils/interfaces';
import { IDashboardVariableValues } from '@kobsio/plugin-core';

interface IDashboardToolbarVariableProps {
variable: IVariableValues;
variable: IDashboardVariableValues;
selectValue: (value: string) => void;
}

Expand Down
4 changes: 2 additions & 2 deletions plugins/dashboards/src/components/dashboards/Dashboards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { QueryObserverResult, useQuery } from 'react-query';
import React, { useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';

import { IDashboard, IPluginDefaults, IReference } from '@kobsio/plugin-core';
import { IDashboard, IDashboardReference, IPluginDefaults } from '@kobsio/plugin-core';
import Dashboard from './Dashboard';
import { IDashboardsOptions } from '../../utils/interfaces';
import { getInitialOptions } from '../../utils/dashboard';

interface IDashboardsProps {
defaults: IPluginDefaults;
references: IReference[];
references: IDashboardReference[];
setDetails?: (details: React.ReactNode) => void;
forceDefaultSpan: boolean;
}
Expand Down
Loading

0 comments on commit a9d15fb

Please sign in to comment.