Skip to content

Commit 5b35a6d

Browse files
committed
Metric labels do not include hostname when two queries are used
This change adds a custom label input box and allows the user to customize the labels. It corresponding filter is created, these variables are available: * $filter_site: Site name * $filter_host_name: Host name * $filter_host_in_group: Group containing the host * $filter_service: Service name * $filter_service_in_group: Group containing the service $label is always available and contains the original label sent by Checmk. CMK-15138
1 parent 8c2a87a commit 5b35a6d

File tree

18 files changed

+1859
-1451
lines changed

18 files changed

+1859
-1451
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@vue/compiler-sfc": "3.2.47",
4242
"copy-webpack-plugin": "^11.0.0",
4343
"css-loader": "^6.7.3",
44+
"cypress": "13.6.0",
4445
"eslint": "^8.39.0",
4546
"eslint-plugin-jsdoc": "^43.0.7",
4647
"eslint-plugin-react": "^7.32.2",

src/RequestSpec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface RequestSpec {
3131
service_in_group: NegatableOption | undefined;
3232

3333
graph: string | undefined;
34+
35+
label: string | undefined;
3436
}
3537

3638
// subset of RequestSpec used with the Filters Component

src/backend/rest.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
FieldType,
99
MetricFindValue,
1010
MutableDataFrame,
11+
ScopedVars,
1112
TimeRange,
1213
dateTime,
1314
} from '@grafana/data';
@@ -16,7 +17,7 @@ import { Aggregation, GraphType, MetricFindQuery } from 'RequestSpec';
1617
import * as process from 'process';
1718

1819
import { CmkQuery } from '../types';
19-
import { createCmkContext, replaceVariables, toLiveStatusQuery, updateQuery } from '../utils';
20+
import { createCmkContext, replaceVariables, toLiveStatusQuery, updateMetricTitles, updateQuery } from '../utils';
2021
import { Backend, DatasourceOptions } from './types';
2122
import { validateRequestSpec } from './validate';
2223

@@ -26,18 +27,20 @@ type RestApiError = {
2627
title: string;
2728
};
2829

30+
export type MetricResponse = {
31+
color: string;
32+
data_points: number[];
33+
line_type: string;
34+
title: string;
35+
};
36+
2937
type RestApiGraphResponse = {
3038
time_range: {
3139
start: string;
3240
end: string;
3341
};
3442
step: number;
35-
metrics: Array<{
36-
color: string;
37-
data_points: number[];
38-
line_type: string;
39-
title: string;
40-
}>;
43+
metrics: MetricResponse[];
4144
};
4245

4346
type CommonRequest = {
@@ -155,7 +158,7 @@ export default class RestApiBackend implements Backend {
155158
const promises = request.targets
156159
.filter((target) => !target.hide)
157160
.map((target) => {
158-
return this.getSingleGraph(request.range, target);
161+
return this.getSingleGraph(request.range, target, request.scopedVars);
159162
});
160163
return await Promise.all(promises).then((data) => ({ data }));
161164
}
@@ -250,7 +253,7 @@ export default class RestApiBackend implements Backend {
250253
return result;
251254
}
252255

253-
async getSingleGraph(range: TimeRange, query: CmkQuery): Promise<DataQueryResponseData> {
256+
async getSingleGraph(range: TimeRange, query: CmkQuery, scopedVars: ScopedVars = {}): Promise<DataQueryResponseData> {
254257
// it's not about a single graph line, but a single chart. grafana supports
255258
// to query multiple graphs in one request, but we have to unwind this, as
256259
// our api only supports a single chart/query per api call.
@@ -322,6 +325,8 @@ export default class RestApiBackend implements Backend {
322325

323326
const { time_range, step, metrics } = response.data;
324327

328+
updateMetricTitles(metrics, query, scopedVars);
329+
325330
const timeValues = [];
326331
let currentTime: DateTime = dateTime(time_range.start);
327332
const endTime: DateTime = dateTime(time_range.end);

src/backend/web.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import { DataQueryRequest, DataQueryResponse, FieldType, MetricFindValue, MutableDataFrame } from '@grafana/data';
1+
import {
2+
DataQueryRequest,
3+
DataQueryResponse,
4+
FieldType,
5+
MetricFindValue,
6+
MutableDataFrame,
7+
ScopedVars,
8+
} from '@grafana/data';
29
import { BackendSrvRequest, FetchError, FetchResponse, getBackendSrv } from '@grafana/runtime';
310
import { MetricFindQuery } from 'RequestSpec';
411
import { defaults, get, isUndefined, zip } from 'lodash';
512

613
import { CmkQuery, defaultQuery } from '../types';
7-
import { updateQuery } from '../utils';
14+
import { updateMetricTitles, updateQuery } from '../utils';
815
import {
916
WebAPiGetGraphResult,
1017
WebApiResponse,
@@ -89,7 +96,7 @@ export default class WebApiBackend implements Backend {
8996
.map((target) => {
9097
// TODO: check if the defaults call is still necessary.
9198
const query = defaults(target, defaultQuery);
92-
return this.getGraphQuery([from, to], query);
99+
return this.getGraphQuery([from, to], query, options.scopedVars);
93100
});
94101
return Promise.all(promises).then((data) => ({ data }));
95102
}
@@ -145,7 +152,11 @@ export default class WebApiBackend implements Backend {
145152
}
146153
}
147154

148-
async getGraphQuery(range: number[], query: CmkQuery): Promise<MutableDataFrame<unknown>> {
155+
async getGraphQuery(
156+
range: number[],
157+
query: CmkQuery,
158+
scopedVars: ScopedVars = {}
159+
): Promise<MutableDataFrame<unknown>> {
149160
updateQuery(query);
150161
const graph = get(query, 'requestSpec.graph');
151162
if (isUndefined(graph) || graph === '') {
@@ -173,8 +184,11 @@ export default class WebApiBackend implements Backend {
173184
if (response.result_code !== 0) {
174185
throw new Error(`${response.result}`);
175186
}
187+
176188
const { start_time, step, curves } = response.result;
177189

190+
updateMetricTitles(curves, query, scopedVars);
191+
178192
const frame = new MutableDataFrame({
179193
refId: query.refId,
180194
fields: [

src/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { DataQuery, DataSourceJsonData } from '@grafana/data';
1+
import { DataSourceJsonData } from '@grafana/data';
2+
import { DataQuery } from '@grafana/schema';
23

34
import { RequestSpec, defaultRequestSpec } from './RequestSpec';
45

@@ -78,3 +79,12 @@ export interface SecureJsonData {
7879
export interface ResponseDataAutocomplete {
7980
choices: Array<[string, string]>;
8081
}
82+
83+
export enum LabelVariableNames {
84+
ORIGINAL = '$label',
85+
SITE = '$filter_site',
86+
HOSTNAME = '$filter_host_name',
87+
HOST_IN_GROUP = '$filter_host_in_group',
88+
SERVICE = '$filter_service',
89+
SERVICE_IN_GROUP = '$filter_service_in_group',
90+
}

src/ui/QueryEditor.tsx

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { QueryEditorProps, SelectableValue } from '@grafana/data';
2-
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
2+
import { Button, Icon, InlineFieldRow, Toggletip, VerticalGroup } from '@grafana/ui';
33
import React from 'react';
44

55
import { DataSource } from '../DataSource';
66
import { Aggregation, GraphType, RequestSpec } from '../RequestSpec';
7-
import { CmkQuery, DataSourceOptions } from '../types';
7+
import { CmkQuery, DataSourceOptions, LabelVariableNames } from '../types';
88
import { aggregationToPresentation, updateQuery } from '../utils';
9-
import { CheckMkSelect } from './components';
9+
import { CheckMkSelect, GenericField } from './components';
1010
import { Filters } from './filters';
1111
import { labelForRequestSpecKey } from './utils';
1212

@@ -19,6 +19,8 @@ export const QueryEditor = (props: Props): JSX.Element => {
1919
const [qAggregation, setQAggregation] = React.useState(rs.aggregation || 'off');
2020
const [qGraphType, setQGraphType] = React.useState(rs.graph_type || 'predefined_graph');
2121
const [qGraph, setQGraph] = React.useState(rs.graph);
22+
const [qLabel, setQLabel] = React.useState(rs.label);
23+
2224
const filters: Partial<RequestSpec> = {
2325
// make sure to only include keys filters should change, otherwise they could
2426
// overwrite other fields!
@@ -43,6 +45,7 @@ export const QueryEditor = (props: Props): JSX.Element => {
4345
graph_type: qGraphType,
4446
graph: qGraph,
4547
aggregation: qAggregation,
48+
label: qLabel,
4649
};
4750

4851
// TODO: not sure if this is a dirty hack or a great solution:
@@ -109,6 +112,55 @@ export const QueryEditor = (props: Props): JSX.Element => {
109112
/>
110113
);
111114

115+
const LabelsTooltip = (
116+
<Toggletip
117+
content={
118+
<>
119+
In addition to the variables provided by Grafana, the following variables hold the values provided in the
120+
filters:
121+
<ul style={{ paddingLeft: '20px' }}>
122+
{[
123+
[LabelVariableNames.SITE, 'Site name'],
124+
[LabelVariableNames.HOSTNAME, 'Host name'],
125+
[LabelVariableNames.HOST_IN_GROUP, 'Group containing the host'],
126+
[LabelVariableNames.SERVICE, 'Service name'],
127+
[LabelVariableNames.SERVICE_IN_GROUP, 'Group containing the service'],
128+
].map((lbl) => (
129+
<li key={lbl[0]}>
130+
<strong>{lbl[0]}</strong>: {lbl[1]}
131+
</li>
132+
))}
133+
</ul>
134+
<br />
135+
Also, the original label is available as
136+
<ul style={{ paddingLeft: '20px' }}>
137+
<li>
138+
<strong>{LabelVariableNames.ORIGINAL}</strong>
139+
</li>
140+
</ul>
141+
</>
142+
}
143+
closeButton={false}
144+
placement="right-end"
145+
>
146+
<Button type="button" fill="text" size="sm" tooltip={'Variables support information'}>
147+
<Icon name="question-circle" />
148+
</Button>
149+
</Toggletip>
150+
);
151+
152+
const labelField = (
153+
<GenericField
154+
requestSpecKey="label"
155+
label={labelForRequestSpecKey('label', requestSpec)}
156+
value={qLabel}
157+
onChange={setQLabel}
158+
dataTestId="custom-label-field"
159+
placeholder={LabelVariableNames.ORIGINAL}
160+
suffix={LabelsTooltip}
161+
></GenericField>
162+
);
163+
112164
if (editionMode === 'RAW') {
113165
return (
114166
<VerticalGroup>
@@ -122,6 +174,7 @@ export const QueryEditor = (props: Props): JSX.Element => {
122174
/>
123175
{graphTypeSelect}
124176
{graphSelect}
177+
{labelField}
125178
</VerticalGroup>
126179
);
127180
} else {
@@ -139,6 +192,7 @@ export const QueryEditor = (props: Props): JSX.Element => {
139192
/>
140193
{graphTypeSelect}
141194
{graphSelect}
195+
{labelField}
142196
</VerticalGroup>
143197
);
144198
}

src/ui/components.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,67 @@ export const Filter = <T extends RequestSpecNegatableOptionKeys>(props: FilterPr
288288
);
289289
};
290290

291+
interface GenericFieldProps<Key extends RequestSpecStringKeys> extends CommonProps<RequestSpec[Key]> {
292+
requestSpecKey: Key;
293+
children?: React.ReactNode;
294+
width?: number;
295+
tooltip?: string;
296+
dataTestId?: string;
297+
placeholder?: string;
298+
prefix?: React.ReactNode;
299+
suffix?: React.ReactNode;
300+
}
301+
export const GenericField = <T extends RequestSpecStringKeys>(props: GenericFieldProps<T>) => {
302+
const {
303+
label,
304+
width = 32,
305+
onChange,
306+
value,
307+
requestSpecKey,
308+
children = null,
309+
tooltip,
310+
placeholder = 'none',
311+
prefix,
312+
suffix,
313+
} = props;
314+
const { dataTestId = `${requestSpecKey}-filter-input` } = props;
315+
316+
const [textValue, setTextValue] = React.useState(value !== undefined ? value : '');
317+
318+
const debouncedOnChange = React.useMemo(
319+
() =>
320+
debounce((newValue) => {
321+
onChange(newValue);
322+
}, 1000),
323+
[onChange]
324+
);
325+
326+
const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
327+
setTextValue(event.target.value); // update text input
328+
debouncedOnChange(event.target.value); // only the last debounce call comes through
329+
};
330+
331+
return (
332+
<HorizontalGroup>
333+
<InlineField label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip}>
334+
<>
335+
<Input
336+
width={width}
337+
type="text"
338+
value={textValue}
339+
onChange={onValueChange}
340+
placeholder={placeholder}
341+
data-test-id={dataTestId}
342+
prefix={prefix}
343+
suffix={suffix}
344+
/>
345+
{children}
346+
</>
347+
</InlineField>
348+
</HorizontalGroup>
349+
);
350+
};
351+
291352
const SingleTag = (props: {
292353
index: number;
293354
onChange: (newValue: TagValue) => void;

src/ui/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const labelForRequestSpecKey = (key: keyof RequestSpec, rq: Partial<Reque
1414
aggregation: 'Aggregation',
1515
graph_type: 'Graph type',
1616
graph: rq.graph_type === 'predefined_graph' ? 'Predefined graph' : 'Single metric',
17+
label: 'Custom label',
1718
};
1819
return table[key];
1920
};

src/utils.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { getTemplateSrv } from '@grafana/runtime';
33
import { isUndefined } from 'lodash';
44

55
import { Aggregation, FiltersRequestSpec, NegatableOption, RequestSpec, TagValue } from './RequestSpec';
6-
import { CmkQuery } from './types';
6+
import { MetricResponse } from './backend/rest';
7+
import { CmkQuery, LabelVariableNames } from './types';
78
import { Presentation } from './ui/autocomplete';
8-
import { requestSpecFromLegacy } from './webapi';
9+
import { WebApiCurve, requestSpecFromLegacy } from './webapi';
910

1011
export const titleCase = (str: string): string => str[0].toUpperCase() + str.slice(1).toLowerCase();
1112

@@ -276,3 +277,30 @@ export function toLiveStatusQuery(filter: Partial<FiltersRequestSpec>, table: 'h
276277
query: query,
277278
};
278279
}
280+
281+
type GrapResponse = WebApiCurve | MetricResponse;
282+
283+
export function updateMetricTitles(metrics: GrapResponse[], query: CmkQuery, scopedVars: ScopedVars = {}) {
284+
const titleTemplate = query.requestSpec?.label || LabelVariableNames.ORIGINAL;
285+
if (titleTemplate !== LabelVariableNames.ORIGINAL) {
286+
scopedVars = {
287+
...scopedVars,
288+
[LabelVariableNames.SITE.substring(1)]: { value: query.requestSpec?.site },
289+
[LabelVariableNames.HOSTNAME.substring(1)]: { value: query.requestSpec?.host_name },
290+
[LabelVariableNames.HOST_IN_GROUP.substring(1)]: { value: query.requestSpec?.host_in_group?.value },
291+
[LabelVariableNames.SERVICE.substring(1)]: { value: query.requestSpec?.service },
292+
[LabelVariableNames.SERVICE_IN_GROUP.substring(1)]: { value: query.requestSpec?.service_in_group?.value },
293+
};
294+
295+
Object.keys(scopedVars).forEach((key) => {
296+
if (scopedVars[key] === undefined) {
297+
delete scopedVars[key];
298+
}
299+
});
300+
301+
metrics.forEach((metric) => {
302+
scopedVars[LabelVariableNames.ORIGINAL.substring(1)] = { value: metric.title };
303+
metric.title = getTemplateSrv().replace(titleTemplate, scopedVars);
304+
});
305+
}
306+
}

0 commit comments

Comments
 (0)