mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
OpenTSDB: Convert the OpenTSDB Query Editor from Angular to React (#54677)
* add query editor to module * add metric section, add tests, update query type * remove use of any for betterer * fix test * add tooltip for alias * run runQuery on select change, fix metric select loading * add downsample row, differentiate for tsdb version * add tests for mteric section and downsample section * add filter section react component and tests * add tag section and tests * add rate section and tests * remove angular code * fix styling * remove comments * remove unused code
This commit is contained in:
parent
ad48cee2bb
commit
82d8015469
@ -6850,43 +6850,10 @@ exports[`better eslint`] = {
|
|||||||
"public/app/plugins/datasource/opentsdb/migrations.ts:5381": [
|
"public/app/plugins/datasource/opentsdb/migrations.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/opentsdb/query_ctrl.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "23"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/opentsdb/specs/datasource.test.ts:5381": [
|
"public/app/plugins/datasource/opentsdb/specs/datasource.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/opentsdb/types.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/postgres/datasource.test.ts:5381": [
|
"public/app/plugins/datasource/postgres/datasource.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { DownSample, DownSampleProps, testIds } from './DownSample';
|
||||||
|
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
const tsdbVersions = [
|
||||||
|
{ label: '<=2.1', value: 1 },
|
||||||
|
{ label: '==2.2', value: 2 },
|
||||||
|
{ label: '==2.3', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const setup = (tsdbVersion: number, propOverrides?: Object) => {
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: '',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
};
|
||||||
|
const props: DownSampleProps = {
|
||||||
|
query,
|
||||||
|
onChange: onChange,
|
||||||
|
onRunQuery: onRunQuery,
|
||||||
|
aggregators: ['avg'],
|
||||||
|
fillPolicies: ['none'],
|
||||||
|
tsdbVersion: tsdbVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return render(<DownSample {...props} />);
|
||||||
|
};
|
||||||
|
describe('DownSample', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render downsample section', () => {
|
||||||
|
setup(tsdbVersions[0].value);
|
||||||
|
expect(screen.getByTestId(testIds.section)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('downsample interval', () => {
|
||||||
|
it('should call runQuery on blur', () => {
|
||||||
|
setup(tsdbVersions[0].value);
|
||||||
|
fireEvent.click(screen.getByTestId('downsample-interval'));
|
||||||
|
fireEvent.blur(screen.getByTestId('downsample-interval'));
|
||||||
|
expect(onRunQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('aggregator select', () => {
|
||||||
|
it('should contain an aggregator', () => {
|
||||||
|
setup(tsdbVersions[0].value);
|
||||||
|
expect(screen.getByText('avg')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fillpolicies select', () => {
|
||||||
|
it('should contain an fillpolicy for versions >= 2.2', () => {
|
||||||
|
setup(tsdbVersions[1].value);
|
||||||
|
expect(screen.getByText('none')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not display fill policy for version >= 2', () => {
|
||||||
|
setup(tsdbVersions[0].value);
|
||||||
|
expect(screen.queryByText('none')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
103
public/app/plugins/datasource/opentsdb/components/DownSample.tsx
Normal file
103
public/app/plugins/datasource/opentsdb/components/DownSample.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { toOption } from '@grafana/data';
|
||||||
|
import { InlineLabel, Select, Input, InlineFormLabel, InlineSwitch } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { paddingRightClass } from './styles';
|
||||||
|
|
||||||
|
export interface DownSampleProps {
|
||||||
|
query: OpenTsdbQuery;
|
||||||
|
onChange: (query: OpenTsdbQuery) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
aggregators: string[];
|
||||||
|
fillPolicies: string[];
|
||||||
|
tsdbVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownSample({ query, onChange, onRunQuery, aggregators, fillPolicies, tsdbVersion }: DownSampleProps) {
|
||||||
|
const aggregatorOptions = aggregators.map((value: string) => toOption(value));
|
||||||
|
const fillPolicyOptions = fillPolicies.map((value: string) => toOption(value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline" data-testid={testIds.section}>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel
|
||||||
|
className="query-keyword"
|
||||||
|
width={8}
|
||||||
|
tooltip={
|
||||||
|
<div>
|
||||||
|
Leave interval blank for auto or for example use <code>1m</code>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Down sample
|
||||||
|
</InlineFormLabel>
|
||||||
|
<Input
|
||||||
|
width={25}
|
||||||
|
className={paddingRightClass}
|
||||||
|
data-testid={testIds.interval}
|
||||||
|
placeholder="interval"
|
||||||
|
value={query.downsampleInterval ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
onChange({ ...query, downsampleInterval: value });
|
||||||
|
}}
|
||||||
|
onBlur={() => onRunQuery()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel width={'auto'} className="query-keyword">
|
||||||
|
Aggregator
|
||||||
|
</InlineFormLabel>
|
||||||
|
<Select
|
||||||
|
className="gf-form-input"
|
||||||
|
value={query.downsampleAggregator ? toOption(query.downsampleAggregator) : undefined}
|
||||||
|
options={aggregatorOptions}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
onChange({ ...query, downsampleAggregator: value });
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{tsdbVersion >= 2 && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineLabel className="width-6 query-keyword">Fill</InlineLabel>
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-fillpolicy-select"
|
||||||
|
value={query.downsampleFillPolicy ? toOption(query.downsampleFillPolicy) : undefined}
|
||||||
|
options={fillPolicyOptions}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
onChange({ ...query, downsampleFillPolicy: value });
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel className="query-keyword">Disable downsampling</InlineFormLabel>
|
||||||
|
<InlineSwitch
|
||||||
|
value={query.disableDownsampling ?? false}
|
||||||
|
onChange={() => {
|
||||||
|
const disableDownsampling = query.disableDownsampling ?? false;
|
||||||
|
onChange({ ...query, disableDownsampling: !disableDownsampling });
|
||||||
|
onRunQuery();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
section: 'opentsdb-downsample',
|
||||||
|
interval: 'downsample-interval',
|
||||||
|
};
|
@ -0,0 +1,107 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { FilterSection, FilterSectionProps, testIds } from './FilterSection';
|
||||||
|
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
const setup = (propOverrides?: Object) => {
|
||||||
|
const suggestTagKeys = jest.fn();
|
||||||
|
const suggestTagValues = jest.fn();
|
||||||
|
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
filter: 'server1',
|
||||||
|
groupBy: true,
|
||||||
|
tagk: 'hostname',
|
||||||
|
type: 'iliteral_or',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: FilterSectionProps = {
|
||||||
|
query,
|
||||||
|
onChange: onChange,
|
||||||
|
onRunQuery: onRunQuery,
|
||||||
|
suggestTagKeys: suggestTagKeys,
|
||||||
|
filterTypes: ['literal_or'],
|
||||||
|
suggestTagValues: suggestTagValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return render(<FilterSection {...props} />);
|
||||||
|
};
|
||||||
|
describe('FilterSection', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render filter section', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId(testIds.section)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter editor', () => {
|
||||||
|
it('open the editor on clicking +', () => {
|
||||||
|
setup();
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.open));
|
||||||
|
expect(screen.getByText('Group by')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a list of filters', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId(testIds.list + '0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call runQuery on adding a filter', () => {
|
||||||
|
setup();
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.open));
|
||||||
|
fireEvent.click(screen.getByText('add filter'));
|
||||||
|
expect(onRunQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have an error if tags are present when adding a filter', () => {
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
tags: [{}],
|
||||||
|
};
|
||||||
|
setup({ query });
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.open));
|
||||||
|
fireEvent.click(screen.getByText('add filter'));
|
||||||
|
expect(screen.getByTestId(testIds.error)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a filter', () => {
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
filter: 'server1',
|
||||||
|
groupBy: true,
|
||||||
|
tagk: 'hostname',
|
||||||
|
type: 'iliteral_or',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
setup({ query });
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.remove));
|
||||||
|
expect(query.filters?.length === 0).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,246 @@
|
|||||||
|
import { size } from 'lodash';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { SelectableValue, toOption } from '@grafana/data';
|
||||||
|
import { InlineLabel, Select, InlineFormLabel, InlineSwitch, Icon } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { OpenTsdbFilter, OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
export interface FilterSectionProps {
|
||||||
|
query: OpenTsdbQuery;
|
||||||
|
onChange: (query: OpenTsdbQuery) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
suggestTagKeys: (query: OpenTsdbQuery) => Promise<string[]>;
|
||||||
|
filterTypes: string[];
|
||||||
|
suggestTagValues: () => Promise<SelectableValue[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterSection({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
onRunQuery,
|
||||||
|
suggestTagKeys,
|
||||||
|
filterTypes,
|
||||||
|
suggestTagValues,
|
||||||
|
}: FilterSectionProps) {
|
||||||
|
const [tagKeys, updTagKeys] = useState<Array<SelectableValue<string>>>();
|
||||||
|
const [keyIsLoading, updKeyIsLoading] = useState<boolean>();
|
||||||
|
|
||||||
|
const [tagValues, updTagValues] = useState<Array<SelectableValue<string>>>();
|
||||||
|
const [valueIsLoading, updValueIsLoading] = useState<boolean>();
|
||||||
|
|
||||||
|
const [addFilterMode, updAddFilterMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [curFilterType, updCurFilterType] = useState<string>('iliteral_or');
|
||||||
|
const [curFilterKey, updCurFilterKey] = useState<string>('');
|
||||||
|
const [curFilterValue, updCurFilterValue] = useState<string>('');
|
||||||
|
const [curFilterGroupBy, updCurFilterGroupBy] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<string>('');
|
||||||
|
|
||||||
|
const filterTypesOptions = filterTypes.map((value: string) => toOption(value));
|
||||||
|
|
||||||
|
function changeAddFilterMode() {
|
||||||
|
updAddFilterMode(!addFilterMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFilter() {
|
||||||
|
if (query.tags && size(query.tags) > 0) {
|
||||||
|
const err = 'Please remove tags to use filters, tags and filters are mutually exclusive.';
|
||||||
|
setErrors(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addFilterMode) {
|
||||||
|
updAddFilterMode(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the filter to the query
|
||||||
|
const currentFilter = {
|
||||||
|
type: curFilterType,
|
||||||
|
tagk: curFilterKey,
|
||||||
|
filter: curFilterValue,
|
||||||
|
groupBy: curFilterGroupBy,
|
||||||
|
};
|
||||||
|
|
||||||
|
// filters may be undefined
|
||||||
|
query.filters = query.filters ? query.filters.concat([currentFilter]) : [currentFilter];
|
||||||
|
|
||||||
|
// reset the inputs
|
||||||
|
updCurFilterType('literal_or');
|
||||||
|
updCurFilterKey('');
|
||||||
|
updCurFilterValue('');
|
||||||
|
updCurFilterGroupBy(false);
|
||||||
|
|
||||||
|
// fire the query
|
||||||
|
onChange(query);
|
||||||
|
onRunQuery();
|
||||||
|
|
||||||
|
// close the filter ditor
|
||||||
|
changeAddFilterMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFilter(index: number) {
|
||||||
|
query.filters?.splice(index, 1);
|
||||||
|
// fire the query
|
||||||
|
onChange(query);
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editFilter(fil: OpenTsdbFilter, idx: number) {
|
||||||
|
removeFilter(idx);
|
||||||
|
updCurFilterKey(fil.tagk);
|
||||||
|
updCurFilterValue(fil.filter);
|
||||||
|
updCurFilterType(fil.type);
|
||||||
|
updCurFilterGroupBy(fil.groupBy);
|
||||||
|
addFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are matching words split with space
|
||||||
|
const splitSeparator = ' ';
|
||||||
|
const customFilterOption = useCallback((option: SelectableValue<string>, searchQuery: string) => {
|
||||||
|
const label = option.value ?? '';
|
||||||
|
|
||||||
|
const searchWords = searchQuery.split(splitSeparator);
|
||||||
|
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline" data-testid={testIds.section}>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel
|
||||||
|
className="query-keyword"
|
||||||
|
width={8}
|
||||||
|
tooltip={<div>Filters does not work with tags, either of the two will work but not both.</div>}
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</InlineFormLabel>
|
||||||
|
{query.filters &&
|
||||||
|
query.filters.map((fil: OpenTsdbFilter, idx: number) => {
|
||||||
|
return (
|
||||||
|
<InlineFormLabel key={idx} width="auto" data-testid={testIds.list + idx}>
|
||||||
|
{fil.tagk} = {fil.type}({fil.filter}), groupBy = {'' + fil.groupBy}
|
||||||
|
<a onClick={() => editFilter(fil, idx)}>
|
||||||
|
<Icon name={'pen'} />
|
||||||
|
</a>
|
||||||
|
<a onClick={() => removeFilter(idx)} data-testid={testIds.remove}>
|
||||||
|
<Icon name={'times'} />
|
||||||
|
</a>
|
||||||
|
</InlineFormLabel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!addFilterMode && (
|
||||||
|
<label className="gf-form-label query-keyword">
|
||||||
|
<a onClick={changeAddFilterMode} data-testid={testIds.open}>
|
||||||
|
<Icon name={'plus'} />
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{addFilterMode && (
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-suggested-tagk-select"
|
||||||
|
className="gf-form-input"
|
||||||
|
value={curFilterKey ? toOption(curFilterKey) : undefined}
|
||||||
|
placeholder="key"
|
||||||
|
onOpenMenu={async () => {
|
||||||
|
updKeyIsLoading(true);
|
||||||
|
const tKs = await suggestTagKeys(query);
|
||||||
|
const tKsOptions = tKs.map((value: string) => toOption(value));
|
||||||
|
updTagKeys(tKsOptions);
|
||||||
|
updKeyIsLoading(false);
|
||||||
|
}}
|
||||||
|
isLoading={keyIsLoading}
|
||||||
|
options={tagKeys}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
updCurFilterKey(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineLabel className="width-4 query-keyword">Type</InlineLabel>
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-aggregator-select"
|
||||||
|
value={curFilterType ? toOption(curFilterType) : undefined}
|
||||||
|
options={filterTypesOptions}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
updCurFilterType(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-suggested-tagv-select"
|
||||||
|
className="gf-form-input"
|
||||||
|
value={curFilterValue ? toOption(curFilterValue) : undefined}
|
||||||
|
placeholder="filter"
|
||||||
|
allowCustomValue
|
||||||
|
filterOption={customFilterOption}
|
||||||
|
onOpenMenu={async () => {
|
||||||
|
if (!tagValues) {
|
||||||
|
updValueIsLoading(true);
|
||||||
|
const tVs = await suggestTagValues();
|
||||||
|
updTagValues(tVs);
|
||||||
|
updValueIsLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={valueIsLoading}
|
||||||
|
options={tagValues}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
updCurFilterValue(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InlineFormLabel width={5} className="query-keyword">
|
||||||
|
Group by
|
||||||
|
</InlineFormLabel>
|
||||||
|
<InlineSwitch
|
||||||
|
value={curFilterGroupBy}
|
||||||
|
onChange={() => {
|
||||||
|
// DO NOT RUN THE QUERY HERE
|
||||||
|
// OLD FUNCTIONALITY RAN THE QUERY
|
||||||
|
updCurFilterGroupBy(!curFilterGroupBy);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="gf-form">
|
||||||
|
{errors && (
|
||||||
|
<label className="gf-form-label" title={errors} data-testid={testIds.error}>
|
||||||
|
<Icon name={'exclamation-triangle'} color={'rgb(229, 189, 28)'} />
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="gf-form-label">
|
||||||
|
<a onClick={addFilter}>add filter</a>
|
||||||
|
<a onClick={changeAddFilterMode}>
|
||||||
|
<Icon name={'times'} />
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
section: 'opentsdb-filter',
|
||||||
|
open: 'opentsdb-filter-editor',
|
||||||
|
list: 'opentsdb-filter-list',
|
||||||
|
error: 'opentsdb-filter-error',
|
||||||
|
remove: 'opentsdb-filter-remove',
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { MetricSection, MetricSectionProps, testIds } from './MetricSection';
|
||||||
|
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
const setup = (propOverrides?: Object) => {
|
||||||
|
const suggestMetrics = jest.fn();
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
aggregator: 'avg',
|
||||||
|
alias: 'alias',
|
||||||
|
};
|
||||||
|
const props: MetricSectionProps = {
|
||||||
|
query,
|
||||||
|
onChange: onChange,
|
||||||
|
onRunQuery: onRunQuery,
|
||||||
|
suggestMetrics: suggestMetrics,
|
||||||
|
aggregators: ['avg'],
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return render(<MetricSection {...props} />);
|
||||||
|
};
|
||||||
|
describe('MetricSection', () => {
|
||||||
|
it('should render metrics section', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId(testIds.section)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
describe('metric aggregator', () => {
|
||||||
|
it('should render metrics select', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByText('cpu')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metric aggregator', () => {
|
||||||
|
it('should render the metrics aggregator', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByText('avg')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metric alias', () => {
|
||||||
|
it('should render the alias input', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId('metric-alias')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire OnRunQuery on blur', () => {
|
||||||
|
setup();
|
||||||
|
const alias = screen.getByTestId('metric-alias');
|
||||||
|
fireEvent.click(alias);
|
||||||
|
fireEvent.blur(alias);
|
||||||
|
expect(onRunQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,110 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { SelectableValue, toOption } from '@grafana/data';
|
||||||
|
import { Select, Input, InlineFormLabel } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
export interface MetricSectionProps {
|
||||||
|
query: OpenTsdbQuery;
|
||||||
|
onChange: (query: OpenTsdbQuery) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
suggestMetrics: () => Promise<SelectableValue[]>;
|
||||||
|
aggregators: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricSection({ query, onChange, onRunQuery, suggestMetrics, aggregators }: MetricSectionProps) {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
metrics?: Array<SelectableValue<string>>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// We are matching words split with space
|
||||||
|
const splitSeparator = ' ';
|
||||||
|
const customFilterOption = useCallback((option: SelectableValue<string>, searchQuery: string) => {
|
||||||
|
const label = option.value ?? '';
|
||||||
|
|
||||||
|
const searchWords = searchQuery.split(splitSeparator);
|
||||||
|
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const aggregatorOptions = aggregators.map((value: string) => toOption(value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline" data-testid={testIds.section}>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel width={8} className="query-keyword">
|
||||||
|
Metric
|
||||||
|
</InlineFormLabel>
|
||||||
|
<Select
|
||||||
|
width={25}
|
||||||
|
inputId="opentsdb-metric-select"
|
||||||
|
className="gf-form-input"
|
||||||
|
value={query.metric ? toOption(query.metric) : undefined}
|
||||||
|
placeholder="Metric name"
|
||||||
|
allowCustomValue
|
||||||
|
filterOption={customFilterOption}
|
||||||
|
onOpenMenu={async () => {
|
||||||
|
if (!state.metrics) {
|
||||||
|
setState({ isLoading: true });
|
||||||
|
const metrics = await suggestMetrics();
|
||||||
|
setState({ metrics, isLoading: undefined });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={state.isLoading}
|
||||||
|
options={state.metrics}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
onChange({ ...query, metric: value });
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel width={'auto'} className="query-keyword">
|
||||||
|
Aggregator
|
||||||
|
</InlineFormLabel>
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-aggregator-select"
|
||||||
|
className="gf-form-input"
|
||||||
|
value={query.aggregator ? toOption(query.aggregator) : undefined}
|
||||||
|
options={aggregatorOptions}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
onChange({ ...query, aggregator: value });
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form max-width-20">
|
||||||
|
<InlineFormLabel
|
||||||
|
className="query-keyword"
|
||||||
|
width={6}
|
||||||
|
tooltip={<div>Use patterns like $tag_tagname to replace part of the alias for a tag value</div>}
|
||||||
|
>
|
||||||
|
Alias
|
||||||
|
</InlineFormLabel>
|
||||||
|
<Input
|
||||||
|
data-testid={testIds.alias}
|
||||||
|
placeholder="series alias"
|
||||||
|
value={query.alias ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
onChange({ ...query, alias: value });
|
||||||
|
}}
|
||||||
|
onBlur={() => onRunQuery()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
section: 'opentsdb-metricsection',
|
||||||
|
alias: 'metric-alias',
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import OpenTsDatasource from '../datasource';
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { OpenTsdbQueryEditor, OpenTsdbQueryEditorProps, testIds } from './OpenTsdbQueryEditor';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: Object) => {
|
||||||
|
const getAggregators = jest.fn().mockResolvedValue([]);
|
||||||
|
const getFilterTypes = jest.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
|
const datasourceMock: unknown = {
|
||||||
|
getAggregators,
|
||||||
|
getFilterTypes,
|
||||||
|
tsdbVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasource: OpenTsDatasource = datasourceMock as OpenTsDatasource;
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const query: OpenTsdbQuery = { metric: '', refId: 'A' };
|
||||||
|
const props: OpenTsdbQueryEditorProps = {
|
||||||
|
datasource: datasource,
|
||||||
|
onRunQuery: onRunQuery,
|
||||||
|
onChange: onChange,
|
||||||
|
query,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return render(<OpenTsdbQueryEditor {...props} />);
|
||||||
|
};
|
||||||
|
describe('OpenTsdbQueryEditor', () => {
|
||||||
|
it('should render editor', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId(testIds.editor)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,160 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, QueryEditorProps, textUtil } from '@grafana/data';
|
||||||
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import OpenTsDatasource from '../datasource';
|
||||||
|
import { OpenTsdbOptions, OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { DownSample } from './DownSample';
|
||||||
|
import { FilterSection } from './FilterSection';
|
||||||
|
import { MetricSection } from './MetricSection';
|
||||||
|
import { RateSection } from './RateSection';
|
||||||
|
import { TagSection } from './TagSection';
|
||||||
|
|
||||||
|
export type OpenTsdbQueryEditorProps = QueryEditorProps<OpenTsDatasource, OpenTsdbQuery, OpenTsdbOptions>;
|
||||||
|
|
||||||
|
export function OpenTsdbQueryEditor({
|
||||||
|
datasource,
|
||||||
|
onRunQuery,
|
||||||
|
onChange,
|
||||||
|
query,
|
||||||
|
range,
|
||||||
|
queries,
|
||||||
|
}: OpenTsdbQueryEditorProps) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const [aggregators, setAggregators] = useState<string[]>([
|
||||||
|
'avg',
|
||||||
|
'sum',
|
||||||
|
'min',
|
||||||
|
'max',
|
||||||
|
'dev',
|
||||||
|
'zimsum',
|
||||||
|
'mimmin',
|
||||||
|
'mimmax',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const fillPolicies: string[] = ['none', 'nan', 'null', 'zero'];
|
||||||
|
|
||||||
|
const [filterTypes, setFilterTypes] = useState<string[]>([
|
||||||
|
'wildcard',
|
||||||
|
'iliteral_or',
|
||||||
|
'not_iliteral_or',
|
||||||
|
'not_literal_or',
|
||||||
|
'iwildcard',
|
||||||
|
'literal_or',
|
||||||
|
'regexp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tsdbVersion: number = datasource.tsdbVersion;
|
||||||
|
|
||||||
|
if (!query.aggregator) {
|
||||||
|
query.aggregator = 'sum';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.downsampleAggregator) {
|
||||||
|
query.downsampleAggregator = 'avg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.downsampleFillPolicy) {
|
||||||
|
query.downsampleFillPolicy = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource.getAggregators().then((aggs: string[]) => {
|
||||||
|
if (aggs.length !== 0) {
|
||||||
|
setAggregators(aggs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
datasource.getFilterTypes().then((filterTypes: string[]) => {
|
||||||
|
if (filterTypes.length !== 0) {
|
||||||
|
setFilterTypes(filterTypes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// previously called as an autocomplete on every input,
|
||||||
|
// in this we call it once on init and filter in the MetricSection component
|
||||||
|
async function suggestMetrics(): Promise<Array<{ value: string; description: string }>> {
|
||||||
|
return datasource.metricFindQuery('metrics()').then(getTextValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// previously called as an autocomplete on every input,
|
||||||
|
// in this we call it once on init and filter in the MetricSection component
|
||||||
|
async function suggestTagValues(): Promise<Array<{ value: string; description: string }>> {
|
||||||
|
return datasource.metricFindQuery('suggest_tagv()').then(getTextValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function suggestTagKeys(query: OpenTsdbQuery): Promise<string[]> {
|
||||||
|
return datasource.suggestTagKeys(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextValues(metrics: Array<{ text: string }>) {
|
||||||
|
return metrics.map((value: { text: string }) => {
|
||||||
|
return {
|
||||||
|
value: textUtil.escapeHtml(value.text),
|
||||||
|
description: value.text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container} data-testid={testIds.editor}>
|
||||||
|
<div className={styles.visualEditor}>
|
||||||
|
<MetricSection
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
suggestMetrics={suggestMetrics}
|
||||||
|
aggregators={aggregators}
|
||||||
|
/>
|
||||||
|
<DownSample
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
aggregators={aggregators}
|
||||||
|
fillPolicies={fillPolicies}
|
||||||
|
tsdbVersion={tsdbVersion}
|
||||||
|
/>
|
||||||
|
{tsdbVersion >= 2 && (
|
||||||
|
<FilterSection
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
filterTypes={filterTypes}
|
||||||
|
suggestTagValues={suggestTagValues}
|
||||||
|
suggestTagKeys={suggestTagKeys}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TagSection
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
suggestTagValues={suggestTagValues}
|
||||||
|
suggestTagKeys={suggestTagKeys}
|
||||||
|
tsdbVersion={tsdbVersion}
|
||||||
|
/>
|
||||||
|
<RateSection query={query} onChange={onChange} onRunQuery={onRunQuery} tsdbVersion={tsdbVersion} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
container: css`
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
visualEditor: css`
|
||||||
|
flex-grow: 1;
|
||||||
|
`,
|
||||||
|
toggleButton: css`
|
||||||
|
margin-left: ${theme.spacing(0.5)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
editor: 'opentsdb-editor',
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { RateSection, RateSectionProps, testIds } from './RateSection';
|
||||||
|
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
const tsdbVersions = [
|
||||||
|
{ label: '<=2.1', value: 1 },
|
||||||
|
{ label: '==2.2', value: 2 },
|
||||||
|
{ label: '==2.3', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const setup = (tsdbVersion: number, propOverrides?: Object) => {
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: '',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
};
|
||||||
|
const props: RateSectionProps = {
|
||||||
|
query,
|
||||||
|
onChange: onChange,
|
||||||
|
onRunQuery: onRunQuery,
|
||||||
|
tsdbVersion: tsdbVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return render(<RateSection {...props} />);
|
||||||
|
};
|
||||||
|
describe('RateSection', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the rate section', () => {
|
||||||
|
setup(tsdbVersions[0].value);
|
||||||
|
expect(screen.getByTestId(testIds.section)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rate components', () => {
|
||||||
|
it('should render the counter switch when rate is switched on', () => {
|
||||||
|
setup(tsdbVersions[0].value, { query: { shouldComputeRate: true } });
|
||||||
|
expect(screen.getByTestId(testIds.isCounter)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the max count input when rate & counter are switched on', () => {
|
||||||
|
setup(tsdbVersions[0].value, { query: { shouldComputeRate: true, isCounter: true } });
|
||||||
|
expect(screen.getByTestId(testIds.counterMax)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('explicit tags', () => {
|
||||||
|
it('should render explicit tags switch for tsdb versions > 2.2', () => {
|
||||||
|
setup(tsdbVersions[2].value);
|
||||||
|
expect(screen.getByText('Explicit tags')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render explicit tags switch for tsdb versions <= 2.2', () => {
|
||||||
|
setup(tsdbVersions[0].value);
|
||||||
|
expect(screen.queryByText('Explicit tags')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,107 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { InlineLabel, Input, InlineFormLabel, InlineSwitch } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
export interface RateSectionProps {
|
||||||
|
query: OpenTsdbQuery;
|
||||||
|
onChange: (query: OpenTsdbQuery) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
tsdbVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RateSection({ query, onChange, onRunQuery, tsdbVersion }: RateSectionProps) {
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline" data-testid={testIds.section}>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel className="query-keyword" width={8}>
|
||||||
|
Rate
|
||||||
|
</InlineFormLabel>
|
||||||
|
<InlineSwitch
|
||||||
|
data-testid={testIds.shouldComputeRate}
|
||||||
|
value={query.shouldComputeRate ?? false}
|
||||||
|
onChange={() => {
|
||||||
|
const shouldComputeRate = query.shouldComputeRate ?? false;
|
||||||
|
onChange({ ...query, shouldComputeRate: !shouldComputeRate });
|
||||||
|
onRunQuery();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{query.shouldComputeRate && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel className="query-keyword" width={'auto'}>
|
||||||
|
Counter
|
||||||
|
</InlineFormLabel>
|
||||||
|
<InlineSwitch
|
||||||
|
data-testid={testIds.isCounter}
|
||||||
|
value={query.isCounter ?? false}
|
||||||
|
onChange={() => {
|
||||||
|
const isCounter = query.isCounter ?? false;
|
||||||
|
onChange({ ...query, isCounter: !isCounter });
|
||||||
|
onRunQuery();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{query.shouldComputeRate && query.isCounter && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineLabel width={'auto'} className="query-keyword">
|
||||||
|
Counter max
|
||||||
|
</InlineLabel>
|
||||||
|
<Input
|
||||||
|
data-testid={testIds.counterMax}
|
||||||
|
placeholder="max value"
|
||||||
|
value={query.counterMax ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
onChange({ ...query, counterMax: value });
|
||||||
|
}}
|
||||||
|
onBlur={() => onRunQuery()}
|
||||||
|
/>
|
||||||
|
<InlineLabel width={'auto'} className="query-keyword">
|
||||||
|
Reset value
|
||||||
|
</InlineLabel>
|
||||||
|
<Input
|
||||||
|
data-testid={testIds.counterResetValue}
|
||||||
|
placeholder="reset value"
|
||||||
|
value={query.counterResetValue ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
onChange({ ...query, counterResetValue: value });
|
||||||
|
}}
|
||||||
|
onBlur={() => onRunQuery()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tsdbVersion > 2 && (
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel className="query-keyword" width={'auto'}>
|
||||||
|
Explicit tags
|
||||||
|
</InlineFormLabel>
|
||||||
|
<InlineSwitch
|
||||||
|
data-testid={testIds.explicitTags}
|
||||||
|
value={query.explicitTags ?? false}
|
||||||
|
onChange={() => {
|
||||||
|
const explicitTags = query.explicitTags ?? false;
|
||||||
|
onChange({ ...query, explicitTags: !explicitTags });
|
||||||
|
onRunQuery();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
section: 'opentsdb-rate',
|
||||||
|
shouldComputeRate: 'opentsdb-shouldComputeRate',
|
||||||
|
isCounter: 'opentsdb-is-counter',
|
||||||
|
counterMax: 'opentsdb-counter-max',
|
||||||
|
counterResetValue: 'opentsdb-counter-reset-value',
|
||||||
|
explicitTags: 'opentsdb-explicit-tags',
|
||||||
|
};
|
@ -0,0 +1,104 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
import { TagSection, TagSectionProps, testIds } from './TagSection';
|
||||||
|
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
const setup = (propOverrides?: Object) => {
|
||||||
|
const suggestTagKeys = jest.fn();
|
||||||
|
const suggestTagValues = jest.fn();
|
||||||
|
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
tags: {
|
||||||
|
tagKey: 'tagValue',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: TagSectionProps = {
|
||||||
|
query,
|
||||||
|
onChange: onChange,
|
||||||
|
onRunQuery: onRunQuery,
|
||||||
|
suggestTagKeys: suggestTagKeys,
|
||||||
|
suggestTagValues: suggestTagValues,
|
||||||
|
tsdbVersion: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return render(<TagSection {...props} />);
|
||||||
|
};
|
||||||
|
describe('Tag Section', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render tag section', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId(testIds.section)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tag editor', () => {
|
||||||
|
it('open the editor on clicking +', () => {
|
||||||
|
setup();
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.open));
|
||||||
|
expect(screen.getByText('add tag')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a list of tags', () => {
|
||||||
|
setup();
|
||||||
|
expect(screen.getByTestId(testIds.list + '0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call runQuery on adding a tag', () => {
|
||||||
|
setup();
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.open));
|
||||||
|
fireEvent.click(screen.getByText('add tag'));
|
||||||
|
expect(onRunQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have an error if filters are present when adding a tag', () => {
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
filter: 'server1',
|
||||||
|
groupBy: true,
|
||||||
|
tagk: 'hostname',
|
||||||
|
type: 'iliteral_or',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
setup({ query });
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.open));
|
||||||
|
fireEvent.click(screen.getByText('add tag'));
|
||||||
|
expect(screen.getByTestId(testIds.error)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a tag', () => {
|
||||||
|
const query: OpenTsdbQuery = {
|
||||||
|
metric: 'cpu',
|
||||||
|
refId: 'A',
|
||||||
|
downsampleAggregator: 'avg',
|
||||||
|
downsampleFillPolicy: 'none',
|
||||||
|
tags: {
|
||||||
|
tag: 'tagToRemove',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setup({ query });
|
||||||
|
fireEvent.click(screen.getByTestId(testIds.remove));
|
||||||
|
expect(Object.keys(query.tags).length === 0).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
219
public/app/plugins/datasource/opentsdb/components/TagSection.tsx
Normal file
219
public/app/plugins/datasource/opentsdb/components/TagSection.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { has, size } from 'lodash';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { SelectableValue, toOption } from '@grafana/data';
|
||||||
|
import { Select, InlineFormLabel, Icon } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
|
export interface TagSectionProps {
|
||||||
|
query: OpenTsdbQuery;
|
||||||
|
onChange: (query: OpenTsdbQuery) => void;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
suggestTagKeys: (query: OpenTsdbQuery) => Promise<string[]>;
|
||||||
|
suggestTagValues: () => Promise<SelectableValue[]>;
|
||||||
|
tsdbVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TagSection({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
onRunQuery,
|
||||||
|
suggestTagKeys,
|
||||||
|
suggestTagValues,
|
||||||
|
tsdbVersion,
|
||||||
|
}: TagSectionProps) {
|
||||||
|
const [tagKeys, updTagKeys] = useState<Array<SelectableValue<string>>>();
|
||||||
|
const [keyIsLoading, updKeyIsLoading] = useState<boolean>();
|
||||||
|
|
||||||
|
const [tagValues, updTagValues] = useState<Array<SelectableValue<string>>>();
|
||||||
|
const [valueIsLoading, updValueIsLoading] = useState<boolean>();
|
||||||
|
|
||||||
|
const [addTagMode, updAddTagMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [curTagKey, updCurTagKey] = useState<string | number>('');
|
||||||
|
const [curTagValue, updCurTagValue] = useState<string>('');
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<string>('');
|
||||||
|
|
||||||
|
function changeAddTagMode() {
|
||||||
|
updAddTagMode(!addTagMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag() {
|
||||||
|
if (query.filters && size(query.filters) > 0) {
|
||||||
|
const err = 'Please remove filters to use tags, tags and filters are mutually exclusive.';
|
||||||
|
setErrors(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addTagMode) {
|
||||||
|
updAddTagMode(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for duplicate tags
|
||||||
|
if (query.tags && has(query.tags, curTagKey)) {
|
||||||
|
const err = "Duplicate tag key '" + curTagKey + "'.";
|
||||||
|
setErrors(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tags may be undefined
|
||||||
|
if (!query.tags) {
|
||||||
|
query.tags = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// add tag to query
|
||||||
|
query.tags[curTagKey] = curTagValue;
|
||||||
|
|
||||||
|
// reset the inputs
|
||||||
|
updCurTagKey('');
|
||||||
|
updCurTagValue('');
|
||||||
|
|
||||||
|
// fire the query
|
||||||
|
onChange(query);
|
||||||
|
onRunQuery();
|
||||||
|
|
||||||
|
// close the tag ditor
|
||||||
|
changeAddTagMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTag(key: string | number) {
|
||||||
|
delete query.tags[key];
|
||||||
|
|
||||||
|
// fire off the query
|
||||||
|
onChange(query);
|
||||||
|
onRunQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editTag(key: string | number, value: string) {
|
||||||
|
removeTag(key);
|
||||||
|
updCurTagKey(key);
|
||||||
|
updCurTagValue(value);
|
||||||
|
addTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are matching words split with space
|
||||||
|
const splitSeparator = ' ';
|
||||||
|
const customTagOption = useCallback((option: SelectableValue<string>, searchQuery: string) => {
|
||||||
|
const label = option.value ?? '';
|
||||||
|
|
||||||
|
const searchWords = searchQuery.split(splitSeparator);
|
||||||
|
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-inline" data-testid={testIds.section}>
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineFormLabel
|
||||||
|
className="query-keyword"
|
||||||
|
width={8}
|
||||||
|
tooltip={tsdbVersion >= 2 ? <div>Please use filters, tags are deprecated in opentsdb 2.2</div> : undefined}
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
</InlineFormLabel>
|
||||||
|
{query.tags &&
|
||||||
|
Object.keys(query.tags).map((tagKey: string | number, idx: number) => {
|
||||||
|
const tagValue = query.tags[tagKey];
|
||||||
|
return (
|
||||||
|
<InlineFormLabel key={idx} width="auto" data-testid={testIds.list + idx}>
|
||||||
|
{tagKey}={tagValue}
|
||||||
|
<a onClick={() => editTag(tagKey, tagValue)}>
|
||||||
|
<Icon name={'pen'} />
|
||||||
|
</a>
|
||||||
|
<a onClick={() => removeTag(tagKey)} data-testid={testIds.remove}>
|
||||||
|
<Icon name={'times'} />
|
||||||
|
</a>
|
||||||
|
</InlineFormLabel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!addTagMode && (
|
||||||
|
<label className="gf-form-label query-keyword">
|
||||||
|
<a onClick={changeAddTagMode} data-testid={testIds.open}>
|
||||||
|
<Icon name={'plus'} />
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{addTagMode && (
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form">
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-suggested-tagk-select"
|
||||||
|
className="gf-form-input"
|
||||||
|
value={curTagKey ? toOption('' + curTagKey) : undefined}
|
||||||
|
placeholder="key"
|
||||||
|
onOpenMenu={async () => {
|
||||||
|
updKeyIsLoading(true);
|
||||||
|
const tKs = await suggestTagKeys(query);
|
||||||
|
const tKsOptions = tKs.map((value: string) => toOption(value));
|
||||||
|
updTagKeys(tKsOptions);
|
||||||
|
updKeyIsLoading(false);
|
||||||
|
}}
|
||||||
|
isLoading={keyIsLoading}
|
||||||
|
options={tagKeys}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
updCurTagKey(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<Select
|
||||||
|
inputId="opentsdb-suggested-tagv-select"
|
||||||
|
className="gf-form-input"
|
||||||
|
value={curTagValue ? toOption(curTagValue) : undefined}
|
||||||
|
placeholder="value"
|
||||||
|
allowCustomValue
|
||||||
|
filterOption={customTagOption}
|
||||||
|
onOpenMenu={async () => {
|
||||||
|
if (!tagValues) {
|
||||||
|
updValueIsLoading(true);
|
||||||
|
const tVs = await suggestTagValues();
|
||||||
|
updTagValues(tVs);
|
||||||
|
updValueIsLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={valueIsLoading}
|
||||||
|
options={tagValues}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
if (value) {
|
||||||
|
updCurTagValue(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
{errors && (
|
||||||
|
<label className="gf-form-label" title={errors} data-testid={testIds.error}>
|
||||||
|
<Icon name={'exclamation-triangle'} color={'rgb(229, 189, 28)'} />
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="gf-form-label">
|
||||||
|
<a onClick={addTag}>add tag</a>
|
||||||
|
<a onClick={changeAddTagMode}>
|
||||||
|
<Icon name={'times'} />
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<div className="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
section: 'opentsdb-tag',
|
||||||
|
open: 'opentsdb-tag-editor',
|
||||||
|
list: 'opentsdb-tag-list',
|
||||||
|
error: 'opentsdb-tag-error',
|
||||||
|
remove: 'opentsdb-tag-remove',
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const paddingRightClass = css({
|
||||||
|
paddingRight: '4px',
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import {
|
import {
|
||||||
clone,
|
clone,
|
||||||
|
cloneDeep,
|
||||||
compact,
|
compact,
|
||||||
each,
|
each,
|
||||||
every,
|
every,
|
||||||
@ -242,7 +242,8 @@ export default class OpenTsDatasource extends DataSourceApi<OpenTsdbQuery, OpenT
|
|||||||
return getBackendSrv().fetch(options);
|
return getBackendSrv().fetch(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestTagKeys(metric: string | number) {
|
suggestTagKeys(query: OpenTsdbQuery) {
|
||||||
|
const metric = query.metric ?? '';
|
||||||
return Promise.resolve(this.tagKeys[metric] || []);
|
return Promise.resolve(this.tagKeys[metric] || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,7 +538,8 @@ export default class OpenTsDatasource extends DataSourceApi<OpenTsdbQuery, OpenT
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target.filters && target.filters.length > 0) {
|
if (target.filters && target.filters.length > 0) {
|
||||||
query.filters = angular.copy(target.filters);
|
query.filters = cloneDeep(target.filters);
|
||||||
|
|
||||||
if (query.filters) {
|
if (query.filters) {
|
||||||
for (const filterKey in query.filters) {
|
for (const filterKey in query.filters) {
|
||||||
query.filters[filterKey].filter = this.templateSrv.replace(
|
query.filters[filterKey].filter = this.templateSrv.replace(
|
||||||
@ -548,7 +550,8 @@ export default class OpenTsDatasource extends DataSourceApi<OpenTsdbQuery, OpenT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
query.tags = angular.copy(target.tags);
|
query.tags = cloneDeep(target.tags);
|
||||||
|
|
||||||
if (query.tags) {
|
if (query.tags) {
|
||||||
for (const tagKey in query.tags) {
|
for (const tagKey in query.tags) {
|
||||||
query.tags[tagKey] = this.templateSrv.replace(query.tags[tagKey], options.scopedVars, 'pipe');
|
query.tags[tagKey] = this.templateSrv.replace(query.tags[tagKey], options.scopedVars, 'pipe');
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { DataSourcePlugin } from '@grafana/data';
|
import { DataSourcePlugin } from '@grafana/data';
|
||||||
|
|
||||||
import { ConfigEditor } from './components/ConfigEditor';
|
import { ConfigEditor } from './components/ConfigEditor';
|
||||||
|
import { OpenTsdbQueryEditor } from './components/OpenTsdbQueryEditor';
|
||||||
import OpenTsDatasource from './datasource';
|
import OpenTsDatasource from './datasource';
|
||||||
import { OpenTsQueryCtrl } from './query_ctrl';
|
|
||||||
|
|
||||||
export const plugin = new DataSourcePlugin(OpenTsDatasource)
|
export const plugin = new DataSourcePlugin(OpenTsDatasource)
|
||||||
.setQueryCtrl(OpenTsQueryCtrl)
|
.setQueryEditor(OpenTsdbQueryEditor)
|
||||||
.setConfigEditor(ConfigEditor);
|
.setConfigEditor(ConfigEditor);
|
||||||
|
@ -1,264 +0,0 @@
|
|||||||
<query-editor-row query-ctrl="ctrl" can-collapse="false">
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form max-width-25">
|
|
||||||
<label class="gf-form-label query-keyword width-8">
|
|
||||||
Metric
|
|
||||||
<label class="gf-form-label" bs-tooltip="ctrl.errors.metric" style="color: rgb(229, 189, 28)" ng-show="ctrl.errors.metric">
|
|
||||||
<icon name="'exclamation-triangle'"></icon>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.metric"
|
|
||||||
spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100
|
|
||||||
ng-blur="ctrl.targetBlur()">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword">
|
|
||||||
Aggregator
|
|
||||||
<a bs-tooltip="ctrl.errors.aggregator" style="color: rgb(229, 189, 28)" ng-show="ctrl.errors.aggregator">
|
|
||||||
<icon name="'exclamation-triangle'"></icon>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
<div class="gf-form-select-wrapper max-width-15">
|
|
||||||
<select aria-label="Metric aggregator"
|
|
||||||
ng-model="ctrl.target.aggregator" class="gf-form-input"
|
|
||||||
ng-options="agg for agg in ctrl.aggregators"
|
|
||||||
ng-change="ctrl.targetBlur()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form max-width-20">
|
|
||||||
<label class="gf-form-label query-keyword width-6">
|
|
||||||
Alias:
|
|
||||||
<info-popover mode="right-normal">
|
|
||||||
Use patterns like $tag_tagname to replace part of the alias for a tag value
|
|
||||||
</info-popover>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="gf-form-input"
|
|
||||||
ng-model="ctrl.target.alias"
|
|
||||||
spellcheck='false'
|
|
||||||
placeholder="series alias"
|
|
||||||
data-min-length=0 data-items=100
|
|
||||||
ng-blur="ctrl.targetBlur()"></input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form max-width-25">
|
|
||||||
<label class="gf-form-label query-keyword width-8">Down sample</label>
|
|
||||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon"
|
|
||||||
ng-model="ctrl.target.downsampleInterval"
|
|
||||||
ng-model-onblur
|
|
||||||
ng-change="ctrl.targetBlur()"
|
|
||||||
placeholder="interval"></input>
|
|
||||||
<info-popover mode="right-absolute">
|
|
||||||
blank for auto, or for example <code>1m</code>
|
|
||||||
</info-popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword">Aggregator</label>
|
|
||||||
<div class="gf-form-select-wrapper">
|
|
||||||
<select aria-label="Downsample aggregator"
|
|
||||||
ng-model="ctrl.target.downsampleAggregator" class="gf-form-input"
|
|
||||||
ng-options="agg for agg in ctrl.aggregators"
|
|
||||||
ng-change="ctrl.targetBlur()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form" ng-if="ctrl.tsdbVersion >= 2">
|
|
||||||
<label class="gf-form-label query-keyword width-6">Fill</label>
|
|
||||||
<div class="gf-form-select-wrapper">
|
|
||||||
<select ng-model="ctrl.target.downsampleFillPolicy" class="gf-form-input"
|
|
||||||
ng-options="agg for agg in ctrl.fillPolicies"
|
|
||||||
ng-change="ctrl.targetBlur()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<gf-form-switch class="gf-form"
|
|
||||||
label="Disable downsampling"
|
|
||||||
label-class="query-keyword"
|
|
||||||
checked="ctrl.target.disableDownsampling"
|
|
||||||
on-change="ctrl.targetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-if="ctrl.tsdbVersion >= 2">
|
|
||||||
<div class="gf-form">
|
|
||||||
|
|
||||||
<label class="gf-form-label query-keyword width-8">
|
|
||||||
Filters
|
|
||||||
<info-popover mode="right-normal">
|
|
||||||
Filters does not work with tags, either of the two will work but not both.
|
|
||||||
</info-popover>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div ng-repeat="fil in ctrl.target.filters track by $index" class="gf-form-label">
|
|
||||||
{{fil.tagk}} = {{fil.type}}({{fil.filter}}) , groupBy = {{fil.groupBy}}
|
|
||||||
<a ng-click="ctrl.editFilter(fil, $index)">
|
|
||||||
<icon name="'pen'"></icon>
|
|
||||||
</a>
|
|
||||||
<a ng-click="ctrl.removeFilter($index)">
|
|
||||||
<icon name="'times'"></icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<label class="gf-form-label query-keyword" ng-hide="ctrl.addFilterMode">
|
|
||||||
<a ng-click="ctrl.addFilter()">
|
|
||||||
<icon name="'plus'"></icon>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-show="ctrl.addFilterMode">
|
|
||||||
<div class="gf-form">
|
|
||||||
<input type="text" class="gf-form-input" spellcheck='false'
|
|
||||||
bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
|
|
||||||
ng-model="ctrl.target.currentFilterKey" placeholder="key">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword">Type</label>
|
|
||||||
<div class="gf-form-select-wrapper">
|
|
||||||
<select ng-model="ctrl.target.currentFilterType" class="gf-form-input" ng-options="filType for filType in ctrl.filterTypes">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
|
||||||
<input type="text" class="gf-form-input" spellcheck='false' bs-typeahead="ctrl.suggestTagValues" data-min-length=0 data-items=100 ng-model="ctrl.target.currentFilterValue" placeholder="filter">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<gf-form-switch class="gf-form"
|
|
||||||
label="Group by"
|
|
||||||
label-class="query-keyword"
|
|
||||||
checked="ctrl.target.currentFilterGroupBy"
|
|
||||||
on-change="ctrl.targetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.addFilterMode">
|
|
||||||
<label class="gf-form-label" ng-show="ctrl.errors.filters">
|
|
||||||
<a bs-tooltip="ctrl.errors.filters" style="color: rgb(229, 189, 28)" >
|
|
||||||
<icon name="'exclamation-triangle'"></icon>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<a ng-click="ctrl.addFilter()" ng-hide="ctrl.errors.filters">add filter</a>
|
|
||||||
<a ng-click="ctrl.closeAddFilterMode()">
|
|
||||||
<icon name="'times'"></icon>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-8">
|
|
||||||
Tags
|
|
||||||
<info-popover mode="right-normal" ng-if="ctrl.tsdbVersion >= 2">
|
|
||||||
Please use filters, tags are deprecated in opentsdb 2.2
|
|
||||||
</info-popover>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form" ng-repeat="(key, value) in ctrl.target.tags track by $index" class="gf-form">
|
|
||||||
<label class="gf-form-label">
|
|
||||||
{{key}} = {{value}}
|
|
||||||
<a ng-click="ctrl.editTag(key, value)">
|
|
||||||
<icon name="'pen'"></icon>
|
|
||||||
</a>
|
|
||||||
<a ng-click="ctrl.removeTag(key)">
|
|
||||||
<icon name="'times'"></icon>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form" ng-hide="ctrl.addTagMode">
|
|
||||||
<label class="gf-form-label query-keyword">
|
|
||||||
<a ng-click="ctrl.addTag()"><icon name="'plus'"></icon></a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.addTagMode">
|
|
||||||
<input type="text"
|
|
||||||
class="gf-form-input" spellcheck='false'
|
|
||||||
bs-typeahead="ctrl.suggestTagKeys" data-min-length=0 data-items=100
|
|
||||||
ng-model="ctrl.target.currentTagKey" placeholder="key">
|
|
||||||
</input>
|
|
||||||
|
|
||||||
<input type="text" class="gf-form-input"
|
|
||||||
spellcheck='false' bs-typeahead="ctrl.suggestTagValues"
|
|
||||||
data-min-length=0 data-items=100 ng-model="ctrl.target.currentTagValue" placeholder="value">
|
|
||||||
</input>
|
|
||||||
|
|
||||||
<label class="gf-form-label" ng-show="ctrl.errors.tags">
|
|
||||||
<a bs-tooltip="ctrl.errors.tags" style="color: rgb(229, 189, 28)" >
|
|
||||||
<icon name="'exclamation-triangle'"></icon>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label" >
|
|
||||||
<a ng-click="ctrl.addTag()" ng-hide="ctrl.errors.tags">add tag</a>
|
|
||||||
<a ng-click="ctrl.closeAddTagMode()"><icon name="'times'" size="'sm'"></icon></a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<gf-form-switch class="gf-form" label="Rate" label-class="width-8 query-keyword" checked="ctrl.target.shouldComputeRate" on-change="ctrl.targetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
|
|
||||||
<gf-form-switch ng-hide="!ctrl.target.shouldComputeRate"
|
|
||||||
class="gf-form" label="Counter" label-class="query-keyword" checked="ctrl.target.isCounter" on-change="ctrl.targetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="gf-form" ng-hide="!ctrl.target.isCounter || !ctrl.target.shouldComputeRate">
|
|
||||||
<label class="gf-form-label query-keyword">Counter Max</label>
|
|
||||||
<input type="text" class="gf-form-input"
|
|
||||||
ng-disabled="!ctrl.target.shouldComputeRate"
|
|
||||||
ng-model="ctrl.target.counterMax" spellcheck='false'
|
|
||||||
placeholder="max value" ng-model-onblur
|
|
||||||
ng-blur="ctrl.targetBlur()">
|
|
||||||
</input>
|
|
||||||
|
|
||||||
<label class="gf-form-label query-keyword">Reset Value</label>
|
|
||||||
<input type="text" class="tight-form-input input-small"
|
|
||||||
ng-disabled="!ctrl.target.shouldComputeRate"
|
|
||||||
ng-model="ctrl.target.counterResetValue" spellcheck='false'
|
|
||||||
placeholder="reset value" ng-model-onblur
|
|
||||||
ng-blur="ctrl.targetBlur()">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form" ng-if="ctrl.tsdbVersion > 2">
|
|
||||||
<gf-form-switch class="gf-form" label="Explicit tags" label-class="width-8 query-keyword" checked="ctrl.target.explicitTags" on-change="ctrl.targetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</query-editor-row>
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
|||||||
import { auto } from 'angular';
|
|
||||||
import { map, size, has } from 'lodash';
|
|
||||||
|
|
||||||
import { textUtil, rangeUtil } from '@grafana/data';
|
|
||||||
import { QueryCtrl } from 'app/plugins/sdk';
|
|
||||||
|
|
||||||
export class OpenTsQueryCtrl extends QueryCtrl {
|
|
||||||
static templateUrl = 'partials/query.editor.html';
|
|
||||||
aggregators: any;
|
|
||||||
fillPolicies: any;
|
|
||||||
filterTypes: any;
|
|
||||||
tsdbVersion: any;
|
|
||||||
aggregator: any;
|
|
||||||
downsampleInterval: any;
|
|
||||||
downsampleAggregator: any;
|
|
||||||
downsampleFillPolicy: any;
|
|
||||||
errors: any;
|
|
||||||
suggestMetrics: any;
|
|
||||||
suggestTagKeys: any;
|
|
||||||
suggestTagValues: any;
|
|
||||||
addTagMode = false;
|
|
||||||
addFilterMode = false;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope: any, $injector: auto.IInjectorService) {
|
|
||||||
super($scope, $injector);
|
|
||||||
|
|
||||||
this.errors = this.validateTarget();
|
|
||||||
this.aggregators = ['avg', 'sum', 'min', 'max', 'dev', 'zimsum', 'mimmin', 'mimmax'];
|
|
||||||
this.fillPolicies = ['none', 'nan', 'null', 'zero'];
|
|
||||||
this.filterTypes = [
|
|
||||||
'wildcard',
|
|
||||||
'iliteral_or',
|
|
||||||
'not_iliteral_or',
|
|
||||||
'not_literal_or',
|
|
||||||
'iwildcard',
|
|
||||||
'literal_or',
|
|
||||||
'regexp',
|
|
||||||
];
|
|
||||||
|
|
||||||
this.tsdbVersion = this.datasource.tsdbVersion;
|
|
||||||
|
|
||||||
if (!this.target.aggregator) {
|
|
||||||
this.target.aggregator = 'sum';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.target.downsampleAggregator) {
|
|
||||||
this.target.downsampleAggregator = 'avg';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.target.downsampleFillPolicy) {
|
|
||||||
this.target.downsampleFillPolicy = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.datasource.getAggregators().then((aggs: { length: number }) => {
|
|
||||||
if (aggs.length !== 0) {
|
|
||||||
this.aggregators = aggs;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.datasource.getFilterTypes().then((filterTypes: { length: number }) => {
|
|
||||||
if (filterTypes.length !== 0) {
|
|
||||||
this.filterTypes = filterTypes;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// needs to be defined here as it is called from typeahead
|
|
||||||
this.suggestMetrics = (query: string, callback: any) => {
|
|
||||||
this.datasource
|
|
||||||
.metricFindQuery('metrics(' + query + ')')
|
|
||||||
.then(this.getTextValues)
|
|
||||||
.then(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.suggestTagKeys = (query: any, callback: any) => {
|
|
||||||
this.datasource.suggestTagKeys(this.target.metric).then(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.suggestTagValues = (query: string, callback: any) => {
|
|
||||||
this.datasource
|
|
||||||
.metricFindQuery('suggest_tagv(' + query + ')')
|
|
||||||
.then(this.getTextValues)
|
|
||||||
.then(callback);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
targetBlur() {
|
|
||||||
this.errors = this.validateTarget();
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextValues(metricFindResult: any) {
|
|
||||||
return map(metricFindResult, (value) => {
|
|
||||||
return textUtil.escapeHtml(value.text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addTag() {
|
|
||||||
if (this.target.filters && this.target.filters.length > 0) {
|
|
||||||
this.errors.tags = 'Please remove filters to use tags, tags and filters are mutually exclusive.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.addTagMode) {
|
|
||||||
this.addTagMode = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.target.tags) {
|
|
||||||
this.target.tags = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errors = this.validateTarget();
|
|
||||||
|
|
||||||
if (!this.errors.tags) {
|
|
||||||
this.target.tags[this.target.currentTagKey] = this.target.currentTagValue;
|
|
||||||
this.target.currentTagKey = '';
|
|
||||||
this.target.currentTagValue = '';
|
|
||||||
this.targetBlur();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addTagMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTag(key: string | number) {
|
|
||||||
delete this.target.tags[key];
|
|
||||||
this.targetBlur();
|
|
||||||
}
|
|
||||||
|
|
||||||
editTag(key: string | number, value: any) {
|
|
||||||
this.removeTag(key);
|
|
||||||
this.target.currentTagKey = key;
|
|
||||||
this.target.currentTagValue = value;
|
|
||||||
this.addTag();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAddTagMode() {
|
|
||||||
this.addTagMode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addFilter() {
|
|
||||||
if (this.target.tags && size(this.target.tags) > 0) {
|
|
||||||
this.errors.filters = 'Please remove tags to use filters, tags and filters are mutually exclusive.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.addFilterMode) {
|
|
||||||
this.addFilterMode = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.target.filters) {
|
|
||||||
this.target.filters = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.target.currentFilterType) {
|
|
||||||
this.target.currentFilterType = 'iliteral_or';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.target.currentFilterGroupBy) {
|
|
||||||
this.target.currentFilterGroupBy = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errors = this.validateTarget();
|
|
||||||
|
|
||||||
if (!this.errors.filters) {
|
|
||||||
const currentFilter = {
|
|
||||||
type: this.target.currentFilterType,
|
|
||||||
tagk: this.target.currentFilterKey,
|
|
||||||
filter: this.target.currentFilterValue,
|
|
||||||
groupBy: this.target.currentFilterGroupBy,
|
|
||||||
};
|
|
||||||
this.target.filters.push(currentFilter);
|
|
||||||
this.target.currentFilterType = 'literal_or';
|
|
||||||
this.target.currentFilterKey = '';
|
|
||||||
this.target.currentFilterValue = '';
|
|
||||||
this.target.currentFilterGroupBy = false;
|
|
||||||
this.targetBlur();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addFilterMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFilter(index: number) {
|
|
||||||
this.target.filters.splice(index, 1);
|
|
||||||
this.targetBlur();
|
|
||||||
}
|
|
||||||
|
|
||||||
editFilter(fil: { tagk: any; filter: any; type: any; groupBy: any }, index: number) {
|
|
||||||
this.removeFilter(index);
|
|
||||||
this.target.currentFilterKey = fil.tagk;
|
|
||||||
this.target.currentFilterValue = fil.filter;
|
|
||||||
this.target.currentFilterType = fil.type;
|
|
||||||
this.target.currentFilterGroupBy = fil.groupBy;
|
|
||||||
this.addFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAddFilterMode() {
|
|
||||||
this.addFilterMode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateTarget() {
|
|
||||||
const errs: any = {};
|
|
||||||
|
|
||||||
if (this.target.shouldDownsample) {
|
|
||||||
try {
|
|
||||||
if (this.target.downsampleInterval) {
|
|
||||||
rangeUtil.describeInterval(this.target.downsampleInterval);
|
|
||||||
} else {
|
|
||||||
errs.downsampleInterval = "You must supply a downsample interval (e.g. '1m' or '1h').";
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
errs.downsampleInterval = err.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.target.tags && has(this.target.tags, this.target.currentTagKey)) {
|
|
||||||
errs.tags = "Duplicate tag key '" + this.target.currentTagKey + "'.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
import { OpenTsQueryCtrl } from '../query_ctrl';
|
|
||||||
|
|
||||||
describe('OpenTsQueryCtrl', () => {
|
|
||||||
const ctx = {
|
|
||||||
target: { target: '' },
|
|
||||||
datasource: {
|
|
||||||
tsdbVersion: '',
|
|
||||||
getAggregators: () => Promise.resolve([]),
|
|
||||||
getFilterTypes: () => Promise.resolve([]),
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
ctx.panelCtrl = {
|
|
||||||
panel: {
|
|
||||||
targets: [ctx.target],
|
|
||||||
},
|
|
||||||
refresh: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(OpenTsQueryCtrl.prototype, ctx);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx.ctrl = new OpenTsQueryCtrl({}, {} as any);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('init query_ctrl variables', () => {
|
|
||||||
it('filter types should be initialized', () => {
|
|
||||||
expect(ctx.ctrl.filterTypes.length).toBe(7);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('aggregators should be initialized', () => {
|
|
||||||
expect(ctx.ctrl.aggregators.length).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fill policy options should be initialized', () => {
|
|
||||||
expect(ctx.ctrl.fillPolicies.length).toBe(4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when adding filters and tags', () => {
|
|
||||||
it('addTagMode should be false when closed', () => {
|
|
||||||
ctx.ctrl.addTagMode = true;
|
|
||||||
ctx.ctrl.closeAddTagMode();
|
|
||||||
expect(ctx.ctrl.addTagMode).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('addFilterMode should be false when closed', () => {
|
|
||||||
ctx.ctrl.addFilterMode = true;
|
|
||||||
ctx.ctrl.closeAddFilterMode();
|
|
||||||
expect(ctx.ctrl.addFilterMode).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('removing a tag from the tags list', () => {
|
|
||||||
ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' };
|
|
||||||
ctx.ctrl.removeTag('tagk');
|
|
||||||
expect(Object.keys(ctx.ctrl.target.tags).length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('removing a filter from the filters list', () => {
|
|
||||||
ctx.ctrl.target.filters = [
|
|
||||||
{
|
|
||||||
tagk: 'tag_key',
|
|
||||||
filter: 'tag_value2',
|
|
||||||
type: 'wildcard',
|
|
||||||
groupBy: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
ctx.ctrl.removeFilter(0);
|
|
||||||
expect(ctx.ctrl.target.filters.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adding a filter when tags exist should generate error', () => {
|
|
||||||
ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' };
|
|
||||||
ctx.ctrl.addFilter();
|
|
||||||
expect(ctx.ctrl.errors.filters).toBe(
|
|
||||||
'Please remove tags to use filters, tags and filters are mutually exclusive.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adding a tag when filters exist should generate error', () => {
|
|
||||||
ctx.ctrl.target.filters = [
|
|
||||||
{
|
|
||||||
tagk: 'tag_key',
|
|
||||||
filter: 'tag_value2',
|
|
||||||
type: 'wildcard',
|
|
||||||
groupBy: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
ctx.ctrl.addTag();
|
|
||||||
expect(ctx.ctrl.errors.tags).toBe('Please remove filters to use tags, tags and filters are mutually exclusive.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,12 +1,36 @@
|
|||||||
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||||
|
|
||||||
export interface OpenTsdbQuery extends DataQuery {
|
export interface OpenTsdbQuery extends DataQuery {
|
||||||
metric?: any;
|
// migrating to react
|
||||||
|
// metrics section
|
||||||
|
metric?: string;
|
||||||
|
aggregator?: string;
|
||||||
|
alias?: string;
|
||||||
|
|
||||||
|
//downsample section
|
||||||
|
downsampleInterval?: string;
|
||||||
|
downsampleAggregator?: string;
|
||||||
|
downsampleFillPolicy?: string;
|
||||||
|
disableDownsampling?: boolean;
|
||||||
|
|
||||||
|
//filters
|
||||||
|
filters?: OpenTsdbFilter[];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
tags?: any;
|
||||||
|
|
||||||
// annotation attrs
|
// annotation attrs
|
||||||
fromAnnotations?: boolean;
|
fromAnnotations?: boolean;
|
||||||
isGlobal?: boolean;
|
isGlobal?: boolean;
|
||||||
target?: string;
|
target?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
// rate
|
||||||
|
shouldComputeRate?: boolean;
|
||||||
|
isCounter?: boolean;
|
||||||
|
counterMax?: string;
|
||||||
|
counterResetValue?: string;
|
||||||
|
explicitTags?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenTsdbOptions extends DataSourceJsonData {
|
export interface OpenTsdbOptions extends DataSourceJsonData {
|
||||||
@ -21,3 +45,10 @@ export type LegacyAnnotation = {
|
|||||||
target?: string;
|
target?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OpenTsdbFilter = {
|
||||||
|
type: string;
|
||||||
|
tagk: string;
|
||||||
|
filter: string;
|
||||||
|
groupBy: boolean;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user