diff --git a/.betterer.results b/.betterer.results
index e8f91a1b754..925d7652935 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -6850,43 +6850,10 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/opentsdb/migrations.ts:5381": [
[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": [
[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/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": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
diff --git a/public/app/plugins/datasource/opentsdb/components/DownSample.test.tsx b/public/app/plugins/datasource/opentsdb/components/DownSample.test.tsx
new file mode 100644
index 00000000000..3f903d10ec4
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/DownSample.test.tsx
@@ -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();
+};
+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();
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/opentsdb/components/DownSample.tsx b/public/app/plugins/datasource/opentsdb/components/DownSample.tsx
new file mode 100644
index 00000000000..ab022d66f48
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/DownSample.tsx
@@ -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 (
+
+
+
+ Leave interval blank for auto or for example use 1m
+
+ }
+ >
+ Down sample
+
+
{
+ const value = e.currentTarget.value;
+ onChange({ ...query, downsampleInterval: value });
+ }}
+ onBlur={() => onRunQuery()}
+ />
+
+
+
+ Aggregator
+
+
+ {tsdbVersion >= 2 && (
+
+ Fill
+
+ )}
+
+ Disable downsampling
+ {
+ const disableDownsampling = query.disableDownsampling ?? false;
+ onChange({ ...query, disableDownsampling: !disableDownsampling });
+ onRunQuery();
+ }}
+ />
+
+
+
+ );
+}
+
+export const testIds = {
+ section: 'opentsdb-downsample',
+ interval: 'downsample-interval',
+};
diff --git a/public/app/plugins/datasource/opentsdb/components/FilterSection.test.tsx b/public/app/plugins/datasource/opentsdb/components/FilterSection.test.tsx
new file mode 100644
index 00000000000..b3c60167b93
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/FilterSection.test.tsx
@@ -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();
+};
+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();
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/opentsdb/components/FilterSection.tsx b/public/app/plugins/datasource/opentsdb/components/FilterSection.tsx
new file mode 100644
index 00000000000..9bf1c4aa6b3
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/FilterSection.tsx
@@ -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;
+ filterTypes: string[];
+ suggestTagValues: () => Promise;
+}
+
+export function FilterSection({
+ query,
+ onChange,
+ onRunQuery,
+ suggestTagKeys,
+ filterTypes,
+ suggestTagValues,
+}: FilterSectionProps) {
+ const [tagKeys, updTagKeys] = useState>>();
+ const [keyIsLoading, updKeyIsLoading] = useState();
+
+ const [tagValues, updTagValues] = useState>>();
+ const [valueIsLoading, updValueIsLoading] = useState();
+
+ const [addFilterMode, updAddFilterMode] = useState(false);
+
+ const [curFilterType, updCurFilterType] = useState('iliteral_or');
+ const [curFilterKey, updCurFilterKey] = useState('');
+ const [curFilterValue, updCurFilterValue] = useState('');
+ const [curFilterGroupBy, updCurFilterGroupBy] = useState(false);
+
+ const [errors, setErrors] = useState('');
+
+ 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, searchQuery: string) => {
+ const label = option.value ?? '';
+
+ const searchWords = searchQuery.split(splitSeparator);
+ return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
+ }, []);
+
+ return (
+
+ {addFilterMode && (
+
+
+
+
+
+ Type
+
+
+
+
+
+
+ Group by
+
+
{
+ // DO NOT RUN THE QUERY HERE
+ // OLD FUNCTIONALITY RAN THE QUERY
+ updCurFilterGroupBy(!curFilterGroupBy);
+ }}
+ />
+
+
+ )}
+
+
+ );
+}
+
+export const testIds = {
+ section: 'opentsdb-filter',
+ open: 'opentsdb-filter-editor',
+ list: 'opentsdb-filter-list',
+ error: 'opentsdb-filter-error',
+ remove: 'opentsdb-filter-remove',
+};
diff --git a/public/app/plugins/datasource/opentsdb/components/MetricSection.test.tsx b/public/app/plugins/datasource/opentsdb/components/MetricSection.test.tsx
new file mode 100644
index 00000000000..3c40d6a2158
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/MetricSection.test.tsx
@@ -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();
+};
+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();
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/opentsdb/components/MetricSection.tsx b/public/app/plugins/datasource/opentsdb/components/MetricSection.tsx
new file mode 100644
index 00000000000..e90b7c8e96d
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/MetricSection.tsx
@@ -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;
+ aggregators: string[];
+}
+
+export function MetricSection({ query, onChange, onRunQuery, suggestMetrics, aggregators }: MetricSectionProps) {
+ const [state, setState] = useState<{
+ metrics?: Array>;
+ isLoading?: boolean;
+ }>({});
+
+ // We are matching words split with space
+ const splitSeparator = ' ';
+ const customFilterOption = useCallback((option: SelectableValue, 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 (
+
+
+
+ Metric
+
+
+
+
+ Aggregator
+
+
+
+ Use patterns like $tag_tagname to replace part of the alias for a tag value
}
+ >
+ Alias
+
+
{
+ const value = e.currentTarget.value;
+ onChange({ ...query, alias: value });
+ }}
+ onBlur={() => onRunQuery()}
+ />
+
+
+
+ );
+}
+
+export const testIds = {
+ section: 'opentsdb-metricsection',
+ alias: 'metric-alias',
+};
diff --git a/public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.test.tsx b/public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.test.tsx
new file mode 100644
index 00000000000..ca0e6107130
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.test.tsx
@@ -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();
+};
+describe('OpenTsdbQueryEditor', () => {
+ it('should render editor', () => {
+ setup();
+ expect(screen.getByTestId(testIds.editor)).toBeInTheDocument();
+ });
+});
diff --git a/public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.tsx b/public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.tsx
new file mode 100644
index 00000000000..cfb504f9946
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/OpenTsdbQueryEditor.tsx
@@ -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;
+
+export function OpenTsdbQueryEditor({
+ datasource,
+ onRunQuery,
+ onChange,
+ query,
+ range,
+ queries,
+}: OpenTsdbQueryEditorProps) {
+ const styles = useStyles2(getStyles);
+
+ const [aggregators, setAggregators] = useState([
+ 'avg',
+ 'sum',
+ 'min',
+ 'max',
+ 'dev',
+ 'zimsum',
+ 'mimmin',
+ 'mimmax',
+ ]);
+
+ const fillPolicies: string[] = ['none', 'nan', 'null', 'zero'];
+
+ const [filterTypes, setFilterTypes] = useState([
+ '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> {
+ 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> {
+ return datasource.metricFindQuery('suggest_tagv()').then(getTextValues);
+ }
+
+ async function suggestTagKeys(query: OpenTsdbQuery): Promise {
+ 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 (
+
+
+
+
+ {tsdbVersion >= 2 && (
+
+ )}
+
+
+
+
+ );
+}
+
+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',
+};
diff --git a/public/app/plugins/datasource/opentsdb/components/RateSection.test.tsx b/public/app/plugins/datasource/opentsdb/components/RateSection.test.tsx
new file mode 100644
index 00000000000..a5b8bf07ee0
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/RateSection.test.tsx
@@ -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();
+};
+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();
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/opentsdb/components/RateSection.tsx b/public/app/plugins/datasource/opentsdb/components/RateSection.tsx
new file mode 100644
index 00000000000..868969b39b3
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/RateSection.tsx
@@ -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 (
+
+
+
+ Rate
+
+ {
+ const shouldComputeRate = query.shouldComputeRate ?? false;
+ onChange({ ...query, shouldComputeRate: !shouldComputeRate });
+ onRunQuery();
+ }}
+ />
+
+ {query.shouldComputeRate && (
+
+
+ Counter
+
+ {
+ const isCounter = query.isCounter ?? false;
+ onChange({ ...query, isCounter: !isCounter });
+ onRunQuery();
+ }}
+ />
+
+ )}
+ {query.shouldComputeRate && query.isCounter && (
+
+
+ Counter max
+
+ {
+ const value = e.currentTarget.value;
+ onChange({ ...query, counterMax: value });
+ }}
+ onBlur={() => onRunQuery()}
+ />
+
+ Reset value
+
+ {
+ const value = e.currentTarget.value;
+ onChange({ ...query, counterResetValue: value });
+ }}
+ onBlur={() => onRunQuery()}
+ />
+
+ )}
+ {tsdbVersion > 2 && (
+
+
+ Explicit tags
+
+ {
+ const explicitTags = query.explicitTags ?? false;
+ onChange({ ...query, explicitTags: !explicitTags });
+ onRunQuery();
+ }}
+ />
+
+ )}
+
+
+ );
+}
+
+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',
+};
diff --git a/public/app/plugins/datasource/opentsdb/components/TagSection.test.tsx b/public/app/plugins/datasource/opentsdb/components/TagSection.test.tsx
new file mode 100644
index 00000000000..e399e52c8c2
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/TagSection.test.tsx
@@ -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();
+};
+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();
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/opentsdb/components/TagSection.tsx b/public/app/plugins/datasource/opentsdb/components/TagSection.tsx
new file mode 100644
index 00000000000..563cdbe8764
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/TagSection.tsx
@@ -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;
+ suggestTagValues: () => Promise;
+ tsdbVersion: number;
+}
+
+export function TagSection({
+ query,
+ onChange,
+ onRunQuery,
+ suggestTagKeys,
+ suggestTagValues,
+ tsdbVersion,
+}: TagSectionProps) {
+ const [tagKeys, updTagKeys] = useState>>();
+ const [keyIsLoading, updKeyIsLoading] = useState();
+
+ const [tagValues, updTagValues] = useState>>();
+ const [valueIsLoading, updValueIsLoading] = useState();
+
+ const [addTagMode, updAddTagMode] = useState(false);
+
+ const [curTagKey, updCurTagKey] = useState('');
+ const [curTagValue, updCurTagValue] = useState('');
+
+ const [errors, setErrors] = useState('');
+
+ 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, searchQuery: string) => {
+ const label = option.value ?? '';
+
+ const searchWords = searchQuery.split(splitSeparator);
+ return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
+ }, []);
+
+ return (
+
+
+ {addTagMode && (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export const testIds = {
+ section: 'opentsdb-tag',
+ open: 'opentsdb-tag-editor',
+ list: 'opentsdb-tag-list',
+ error: 'opentsdb-tag-error',
+ remove: 'opentsdb-tag-remove',
+};
diff --git a/public/app/plugins/datasource/opentsdb/components/styles.ts b/public/app/plugins/datasource/opentsdb/components/styles.ts
new file mode 100644
index 00000000000..c042d110665
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/components/styles.ts
@@ -0,0 +1,5 @@
+import { css } from '@emotion/css';
+
+export const paddingRightClass = css({
+ paddingRight: '4px',
+});
diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts
index f05d97b52e7..95d43902b71 100644
--- a/public/app/plugins/datasource/opentsdb/datasource.ts
+++ b/public/app/plugins/datasource/opentsdb/datasource.ts
@@ -1,6 +1,6 @@
-import angular from 'angular';
import {
clone,
+ cloneDeep,
compact,
each,
every,
@@ -242,7 +242,8 @@ export default class OpenTsDatasource extends DataSourceApi 0) {
- query.filters = angular.copy(target.filters);
+ query.filters = cloneDeep(target.filters);
+
if (query.filters) {
for (const filterKey in query.filters) {
query.filters[filterKey].filter = this.templateSrv.replace(
@@ -548,7 +550,8 @@ export default class OpenTsDatasource extends DataSourceApi
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/app/plugins/datasource/opentsdb/query_ctrl.ts b/public/app/plugins/datasource/opentsdb/query_ctrl.ts
deleted file mode 100644
index 7e71745f7c9..00000000000
--- a/public/app/plugins/datasource/opentsdb/query_ctrl.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts
deleted file mode 100644
index 17d0d48f11d..00000000000
--- a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts
+++ /dev/null
@@ -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.');
- });
- });
-});
diff --git a/public/app/plugins/datasource/opentsdb/types.ts b/public/app/plugins/datasource/opentsdb/types.ts
index 909feeb9cbe..4ae66b8d6d1 100644
--- a/public/app/plugins/datasource/opentsdb/types.ts
+++ b/public/app/plugins/datasource/opentsdb/types.ts
@@ -1,12 +1,36 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
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
fromAnnotations?: boolean;
isGlobal?: boolean;
target?: string;
name?: string;
+
+ // rate
+ shouldComputeRate?: boolean;
+ isCounter?: boolean;
+ counterMax?: string;
+ counterResetValue?: string;
+ explicitTags?: boolean;
}
export interface OpenTsdbOptions extends DataSourceJsonData {
@@ -21,3 +45,10 @@ export type LegacyAnnotation = {
target?: string;
name?: string;
};
+
+export type OpenTsdbFilter = {
+ type: string;
+ tagk: string;
+ filter: string;
+ groupBy: boolean;
+};