Cloudwatch: Break out resource requests from datasource file (#55358)

* break out resource requests from datasource file

* fix lint issues
This commit is contained in:
Erik Sundell 2022-09-20 07:50:54 +02:00 committed by GitHub
parent 4df41972f1
commit caba98590d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 298 additions and 303 deletions

View File

@ -56,7 +56,7 @@ exports[`no enzyme tests`] = {
"public/app/features/dimensions/editors/ThresholdsEditor/ThresholdsEditor.test.tsx:145048794": [
[0, 17, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:3543762762": [
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:2210656642": [
[1, 19, 13, "RegExp match", "2409514259"]
]
}`
@ -5905,12 +5905,18 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloud-monitoring/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloudwatch/__mocks__/LogsQueryRunner.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloudwatch/api.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"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@ -5953,15 +5959,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/datasource/cloudwatch/datasource.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.", "0"]
],
"public/app/plugins/datasource/cloudwatch/guards.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
@ -5985,9 +5983,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]

View File

@ -77,8 +77,8 @@ export function setupMockedDataSource({
const datasource = new CloudWatchDatasource(CloudWatchSettings, templateService, timeSrv);
datasource.getVariables = () => ['test'];
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.getRegions = jest.fn().mockResolvedValue([]);
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getRegions = jest.fn().mockResolvedValue([]);
datasource.logsQueryRunner.defaultLogGroups = [];
const fetchMock = jest.fn().mockReturnValue(of({}));
setBackendSrv({

View File

@ -1,6 +1,6 @@
import { of } from 'rxjs';
import { DataFrame } from '@grafana/data';
import { CustomVariableModel, DataFrame } from '@grafana/data';
import { BackendDataSourceResponse, getBackendSrv, setBackendSrv } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
@ -16,7 +16,7 @@ export function setupMockedLogsQueryRunner({
},
variables,
mockGetVariableName = true,
}: { data?: BackendDataSourceResponse; variables?: any; mockGetVariableName?: boolean } = {}) {
}: { data?: BackendDataSourceResponse; variables?: CustomVariableModel[]; mockGetVariableName?: boolean } = {}) {
let templateService = new TemplateSrv();
if (variables) {
templateService = setupMockedTemplateService(variables);

View File

@ -0,0 +1,111 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, BackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CloudWatchRequest } from './query-runner/CloudWatchRequest';
import { CloudWatchJsonData, Dimensions } from './types';
export class CloudWatchAPI extends CloudWatchRequest {
private backendSrv: BackendSrv;
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
super(instanceSettings, templateSrv);
this.backendSrv = getBackendSrv();
}
resourceRequest(subtype: string, parameters?: any): Promise<Array<{ text: any; label: any; value: any }>> {
return this.backendSrv.get(`/api/datasources/${this.instanceSettings.id}/resources/${subtype}`, parameters);
}
getRegions(): Promise<Array<{ label: string; value: string; text: string }>> {
return this.resourceRequest('regions').then((regions: any) => [
{ label: 'default', value: 'default', text: 'default' },
...regions,
]);
}
getNamespaces() {
return this.resourceRequest('namespaces');
}
async getMetrics(namespace: string | undefined, region?: string) {
if (!namespace) {
return [];
}
return this.resourceRequest('metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
});
}
async getAllMetrics(region: string): Promise<Array<{ metricName: string; namespace: string }>> {
const values = await this.resourceRequest('all-metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
});
return values.map((v) => ({ metricName: v.value, namespace: v.text }));
}
async getDimensionKeys(
namespace: string | undefined,
region: string,
dimensionFilters: Dimensions = {},
metricName = ''
) {
if (!namespace) {
return [];
}
return this.resourceRequest('dimension-keys', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
dimensionFilters: JSON.stringify(this.convertDimensionFormat(dimensionFilters, {})),
metricName,
});
}
async getDimensionValues(
region: string,
namespace: string | undefined,
metricName: string | undefined,
dimensionKey: string,
filterDimensions: {}
) {
if (!namespace || !metricName) {
return [];
}
const values = await this.resourceRequest('dimension-values', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
metricName: this.templateSrv.replace(metricName.trim()),
dimensionKey: this.templateSrv.replace(dimensionKey),
dimensions: JSON.stringify(this.convertDimensionFormat(filterDimensions, {})),
});
return values;
}
getEbsVolumeIds(region: string, instanceId: string) {
return this.resourceRequest('ebs-volume-ids', {
region: this.templateSrv.replace(this.getActualRegion(region)),
instanceId: this.templateSrv.replace(instanceId),
});
}
getEc2InstanceAttribute(region: string, attributeName: string, filters: any) {
return this.resourceRequest('ec2-instance-attribute', {
region: this.templateSrv.replace(this.getActualRegion(region)),
attributeName: this.templateSrv.replace(attributeName),
filters: JSON.stringify(this.convertMultiFilterFormat(filters, 'filter key')),
});
}
getResourceARNs(region: string, resourceType: string, tags: any) {
return this.resourceRequest('resource-arns', {
region: this.templateSrv.replace(this.getActualRegion(region)),
resourceType: this.templateSrv.replace(resourceType),
tags: JSON.stringify(this.convertMultiFilterFormat(tags, 'tag name')),
});
}
}

View File

@ -3,7 +3,7 @@ import { uniq } from 'lodash';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { CloudWatchDatasource } from '../../datasource';
import { CloudWatchAPI } from '../../api';
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
import { LinkedToken } from '../../monarch/LinkedToken';
import { TRIGGER_SUGGEST } from '../../monarch/commands';
@ -34,9 +34,9 @@ type CompletionItem = monacoTypes.languages.CompletionItem;
export class SQLCompletionItemProvider extends CompletionItemProvider {
region: string;
constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
super(datasource, templateSrv);
this.region = datasource.getActualRegion();
constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
super(api, templateSrv);
this.region = api.getActualRegion() ?? '';
this.getStatementPosition = getStatementPosition;
this.getSuggestionKinds = getSuggestionKinds;
this.tokenTypes = SQLTokenTypes;
@ -112,14 +112,14 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
const namespaceToken = getNamespaceToken(currentToken);
if (namespaceToken?.value) {
// if a namespace is specified, only suggest metrics for the namespace
const metrics = await this.datasource.getMetrics(
const metrics = await this.api.getMetrics(
this.templateSrv.replace(namespaceToken?.value.replace(/\"/g, '')),
this.templateSrv.replace(this.region)
);
metrics.map((m) => addSuggestion(m.value));
} else {
// If no namespace is specified in the query, just list all metrics
const metrics = await this.datasource.getAllMetrics(this.templateSrv.replace(this.region));
const metrics = await this.api.getAllMetrics(this.templateSrv.replace(this.region));
uniq(metrics.map((m) => m.metricName)).map((m) => addSuggestion(m, { insertText: m }));
}
}
@ -147,12 +147,12 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
let namespaces = [];
if (metricNameToken?.value) {
// if a metric is specified, only suggest namespaces that actually have that metric
const metrics = await this.datasource.getAllMetrics(this.region);
const metrics = await this.api.getAllMetrics(this.region);
const metricName = this.templateSrv.replace(metricNameToken.value);
namespaces = metrics.filter((m) => m.metricName === metricName).map((m) => m.namespace);
} else {
// if no metric is specified, just suggest all namespaces
const ns = await this.datasource.getNamespaces();
const ns = await this.api.getNamespaces();
namespaces = ns.map((n) => n.value);
}
namespaces.map((n) => addSuggestion(`"${n}"`, { insertText: `"${n}"` }));
@ -179,7 +179,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
dimensionFilter = (labelKeyTokens || []).reduce((acc, curr) => {
return { ...acc, [curr.value]: null };
}, {});
const keys = await this.datasource.getDimensionKeys(
const keys = await this.api.getDimensionKeys(
this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
this.templateSrv.replace(this.region),
dimensionFilter,
@ -199,7 +199,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
const metricNameToken = getMetricNameToken(currentToken);
const labelKey = currentToken?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken();
if (namespaceToken?.value && labelKey?.value && metricNameToken?.value) {
const values = await this.datasource.getDimensionValues(
const values = await this.api.getDimensionValues(
this.templateSrv.replace(this.region),
this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
this.templateSrv.replace(metricNameToken.value),
@ -266,12 +266,12 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
}
}
// always suggest template variables
this.templateVariables.map((v) => {
addSuggestion(v, {
this.templateSrv.getVariables().map((v) => {
const variable = `$${v.name}`;
addSuggestion(variable, {
range,
label: v,
insertText: v,
label: variable,
insertText: variable,
kind: monaco.languages.CompletionItemKind.Variable,
sortText: CompletionItemPriority.Low,
});

View File

@ -29,10 +29,10 @@ const q: CloudWatchQuery = {
alarmNamePrefix: '',
};
ds.datasource.getRegions = jest.fn().mockResolvedValue([]);
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]);
ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
const props: QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> = {
@ -51,7 +51,7 @@ describe('AnnotationQueryEditor', () => {
});
it('should return an error component in case CloudWatchQuery is not CloudWatchAnnotationQuery', async () => {
ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
render(
<AnnotationQueryEditor {...props} query={{ ...props.query, queryMode: 'Metrics' } as CloudWatchMetricsQuery} />
);
@ -59,7 +59,7 @@ describe('AnnotationQueryEditor', () => {
});
it('should not display wildcard option in dimension value dropdown', async () => {
ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
(props.query as CloudWatchAnnotationQuery).dimensions = { instanceId: 'instance-123' };
render(<AnnotationQueryEditor {...props} />);
const valueElement = screen.getByText('instance-123');

View File

@ -52,7 +52,7 @@ export default class CloudWatchLink extends Component<Props, State> {
source: query.logGroupNames ?? [],
};
return encodeUrl(urlProps, datasource.getActualRegion(query.region));
return encodeUrl(urlProps, datasource.api.getActualRegion(query.region));
}
render() {

View File

@ -12,12 +12,14 @@ import { ConfigEditor, Props } from './ConfigEditor';
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
loadDatasource: jest.fn().mockResolvedValue({
getRegions: jest.fn().mockResolvedValue([
{
label: 'ap-east-1',
value: 'ap-east-1',
},
]),
api: {
getRegions: jest.fn().mockResolvedValue([
{
label: 'ap-east-1',
value: 'ap-east-1',
},
]),
},
getActualRegion: jest.fn().mockReturnValue('ap-east-1'),
getVariables: jest.fn().mockReturnValue([]),
logsQueryRunner: {

View File

@ -63,7 +63,7 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
{...props}
loadRegions={
datasource &&
(() => datasource!.getRegions().then((r) => r.filter((r) => r.value !== 'default').map((v) => v.value)))
(() => datasource.api.getRegions().then((r) => r.filter((r) => r.value !== 'default').map((v) => v.value)))
}
>
<InlineField label="Namespaces of Custom Metrics" labelWidth={28} tooltip="Namespaces of Custom Metrics.">

View File

@ -10,9 +10,9 @@ const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
const q: CloudWatchMetricsQuery = {
id: '',

View File

@ -50,7 +50,7 @@ export const FilterItem: FunctionComponent<Props> = ({
return [];
}
return datasource
return datasource.api
.getDimensionValues(region, namespace, metricName, filter.key, dimensionsExcludingCurrentKey)
.then((result: Array<SelectableValue<string>>) => {
if (result.length && !disableExpressions) {

View File

@ -98,7 +98,7 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
}
setLoadingLogGroups(true);
return fetchLogGroupOptions(datasource.getActualRegion(region))
return fetchLogGroupOptions(datasource.getActualRegion(region) ?? '')
.then((logGroups) => {
const newSelectedLogGroups = intersection(
selectedLogGroups,

View File

@ -11,9 +11,9 @@ const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
const metricStat: MetricStat = {
region: 'us-east-2',
@ -121,8 +121,8 @@ describe('MetricStatEditor', () => {
};
beforeEach(() => {
propsNamespaceMetrics.datasource.getNamespaces = jest.fn().mockResolvedValue(namespaces);
propsNamespaceMetrics.datasource.getMetrics = jest.fn().mockResolvedValue(metrics);
propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces);
propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics);
onChange.mockClear();
onRunQuery.mockClear();
});
@ -148,7 +148,7 @@ describe('MetricStatEditor', () => {
});
it('should remove metricName from metricStat if it does not exist in new namespace', async () => {
propsNamespaceMetrics.datasource.getMetrics = jest
propsNamespaceMetrics.datasource.api.getMetrics = jest
.fn()
.mockImplementation((namespace: string, region: string) => {
let mockMetrics =

View File

@ -6,6 +6,7 @@ import { EditorField, EditorFieldGroup, EditorRow, EditorRows, EditorSwitch, Sel
import { Dimensions } from '..';
import { CloudWatchDatasource } from '../../datasource';
import { useDimensionKeys, useMetrics, useNamespaces } from '../../hooks';
import { standardStatistics } from '../../standardStatistics';
import { MetricStat } from '../../types';
import { appendTemplateVariables, toOption } from '../../utils/utils';
@ -46,7 +47,7 @@ export function MetricStatEditor({
if (!metricName) {
return metricStat;
}
await datasource.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
await datasource.api.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
if (!result.find((metric) => metric.value === metricName)) {
metricName = '';
}
@ -89,15 +90,15 @@ export function MetricStatEditor({
<Select
inputId={`${refId}-metric-stat-editor-select-statistic`}
allowCustomValue
value={toOption(metricStat.statistic ?? datasource.standardStatistics[0])}
value={toOption(metricStat.statistic ?? standardStatistics[0])}
options={appendTemplateVariables(
datasource,
datasource.standardStatistics.filter((s) => s !== metricStat.statistic).map(toOption)
standardStatistics.filter((s) => s !== metricStat.statistic).map(toOption)
)}
onChange={({ value: statistic }) => {
if (
!statistic ||
(!datasource.standardStatistics.includes(statistic) &&
(!standardStatistics.includes(statistic) &&
!/^p\d{2}(?:\.\d{1,2})?$/.test(statistic) &&
!statistic.startsWith('$'))
) {

View File

@ -46,10 +46,10 @@ const setup = () => {
const datasource = new CloudWatchDatasource(instanceSettings, templateSrv as any, {} as any);
datasource.metricFindQuery = async () => [{ value: 'test', label: 'test', text: 'test' }];
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.getMetrics = jest.fn().mockResolvedValue([]);
datasource.getRegions = jest.fn().mockResolvedValue([]);
datasource.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getRegions = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
const props: Props = {
query: {
@ -150,7 +150,7 @@ describe('QueryEditor', () => {
if (props.query.queryMode !== 'Metrics') {
fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`);
}
props.datasource.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
props.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
props.query.metricQueryType = MetricQueryType.Search;
props.query.metricEditorMode = MetricEditorMode.Builder;
props.query.dimensions = { instanceId: 'instance-123' };

View File

@ -10,7 +10,7 @@ import MetricsQueryHeader from './MetricsQueryHeader';
const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.getRegions = jest.fn().mockResolvedValue([]);
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]);
const query: CloudWatchMetricsQuery = {
id: '',
region: 'us-east-2',

View File

@ -9,7 +9,7 @@ import QueryHeader from './QueryHeader';
const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.getRegions = jest.fn().mockResolvedValue([]);
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]);
describe('QueryHeader', () => {
it('should display metric options for metrics', async () => {

View File

@ -22,10 +22,10 @@ const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({
describe('Cloudwatch SQLBuilderEditor', () => {
beforeEach(() => {
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.getMetrics = jest.fn().mockResolvedValue([]);
datasource.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.getDimensionValues = jest.fn().mockResolvedValue([]);
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]);
});
const baseProps = {
@ -47,7 +47,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AWS/EC2')).toBeInTheDocument();
expect(screen.getByLabelText('With schema')).not.toBeChecked();
@ -68,7 +68,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AWS/EC2')).toBeInTheDocument();
expect(screen.getByLabelText('With schema')).toBeChecked();
@ -95,7 +95,12 @@ describe('Cloudwatch SQLBuilderEditor', () => {
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() =>
expect(datasource.getDimensionKeys).toHaveBeenCalledWith('AWS/EC2', query.region, { InstanceId: null }, undefined)
expect(datasource.api.getDimensionKeys).toHaveBeenCalledWith(
'AWS/EC2',
query.region,
{ InstanceId: null },
undefined
)
);
expect(screen.getByText('AWS/EC2')).toBeInTheDocument();
expect(screen.getByLabelText('With schema')).toBeChecked();
@ -117,7 +122,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AVERAGE')).toBeInTheDocument();
expect(screen.getByText('CPUUtilization')).toBeInTheDocument();
@ -133,7 +138,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AVG')).toBeInTheDocument();
const directionElement = screen.getByLabelText('Direction');
@ -145,7 +150,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
const query = makeSQLQuery({});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
expect(screen.queryByText('AVG')).toBeNull();
const directionElement = screen.getByLabelText('Direction');

View File

@ -60,15 +60,15 @@ const metrics = [
describe('Cloudwatch SQLBuilderSelectRow', () => {
beforeEach(() => {
datasource.getNamespaces = jest.fn().mockResolvedValue(namespaces);
datasource.getMetrics = jest.fn().mockResolvedValue([]);
datasource.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.getDimensionValues = jest.fn().mockResolvedValue([]);
datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]);
onQueryChange.mockReset();
});
it('Should not reset metricName when selecting a namespace if metric exist in new namespace', async () => {
datasource.getMetrics = jest.fn().mockResolvedValue(metrics);
datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics);
await act(async () => {
render(<SQLBuilderSelectRow {...baseProps} />);
@ -103,7 +103,7 @@ describe('Cloudwatch SQLBuilderSelectRow', () => {
});
it('Should reset metricName when selecting a namespace if metric does not exist in new namespace', async () => {
datasource.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => {
datasource.api.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => {
let mockMetrics =
namespace === 'n1' && region === baseProps.query.region
? metrics

View File

@ -62,7 +62,7 @@ const SQLBuilderSelectRow: React.FC<SQLBuilderSelectRowProps> = ({ datasource, q
const validateMetricName = async (query: CloudWatchMetricsQuery) => {
let { region, sql } = query;
await datasource.getMetrics(query.namespace, region).then((result: Array<SelectableValue<string>>) => {
await datasource.api.getMetrics(query.namespace, region).then((result: Array<SelectableValue<string>>) => {
if (!result.some((metric) => metric.value === metricName)) {
sql = removeMetricName(query).sql;
}

View File

@ -108,7 +108,7 @@ const FilterItem: React.FC<FilterItemProps> = (props) => {
return [];
}
return datasource
return datasource.api
.getDimensionValues(query.region, namespace, metricName, filter.property.name, {})
.then((result: Array<SelectableValue<string>>) => {
return appendTemplateVariables(datasource, result);

View File

@ -22,22 +22,22 @@ const defaultQuery = {
const ds = setupMockedDataSource();
ds.datasource.getRegions = jest.fn().mockResolvedValue([
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([
{ label: 'a1', value: 'a1' },
{ label: 'b1', value: 'b1' },
{ label: 'c1', value: 'c1' },
]);
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([
ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([
{ label: 'x2', value: 'x2' },
{ label: 'y2', value: 'y2' },
{ label: 'z2', value: 'z2' },
]);
ds.datasource.getMetrics = jest.fn().mockResolvedValue([
ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([
{ label: 'h3', value: 'h3' },
{ label: 'i3', value: 'i3' },
{ label: 'j3', value: 'j3' },
]);
ds.datasource.getDimensionKeys = jest
ds.datasource.api.getDimensionKeys = jest
.fn()
.mockImplementation((_namespace: string, region: string, dimensionFilters?: Dimensions) => {
if (!!dimensionFilters) {
@ -55,12 +55,12 @@ ds.datasource.getDimensionKeys = jest
}
return Promise.resolve([{ label: 't4', value: 't4' }]);
});
ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([
ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]);
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
ds.datasource.getEc2InstanceAttribute = jest.fn().mockReturnValue([]);
ds.datasource.api.getEc2InstanceAttribute = jest.fn().mockReturnValue([]);
const onChange = jest.fn();
const defaultProps: Props = {
@ -141,7 +141,7 @@ describe('VariableEditor', () => {
await select(keySelect, 'v4', {
container: document.body,
});
expect(ds.datasource.getDimensionKeys).toHaveBeenCalledWith('z2', 'a1', {}, '');
expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith('z2', 'a1', {}, '');
expect(onChange).toHaveBeenCalledWith({
...defaultQuery,
queryType: VariableQueryType.DimensionValues,
@ -224,8 +224,8 @@ describe('VariableEditor', () => {
container: document.body,
});
expect(ds.datasource.getMetrics).toHaveBeenCalledWith('z2', 'b1');
expect(ds.datasource.getDimensionKeys).toHaveBeenCalledWith('z2', 'b1');
expect(ds.datasource.api.getMetrics).toHaveBeenCalledWith('z2', 'b1');
expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith('z2', 'b1');
expect(props.onChange).toHaveBeenCalledWith({
...defaultQuery,
refId: 'CloudWatchVariableQueryEditor-VariableQuery',

View File

@ -65,14 +65,14 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
const sanitizeQuery = async (query: VariableQuery) => {
let { metricName, dimensionKey, dimensionFilters, namespace, region } = query;
if (metricName) {
await datasource.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
await datasource.api.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
if (!result.find((metric) => metric.value === metricName)) {
metricName = '';
}
});
}
if (dimensionKey) {
await datasource.getDimensionKeys(namespace, region).then((result: Array<SelectableValue<string>>) => {
await datasource.api.getDimensionKeys(namespace, region).then((result: Array<SelectableValue<string>>) => {
if (!result.find((key) => key.value === dimensionKey)) {
dimensionKey = '';
dimensionFilters = {};

View File

@ -204,7 +204,7 @@ describe('datasource', () => {
describe('resource requests', () => {
it('should map resource response to metric response', async () => {
const datasource = setupMockedDataSource().datasource;
datasource.doMetricResourceRequest = jest.fn().mockResolvedValue([
datasource.api.resourceRequest = jest.fn().mockResolvedValue([
{
text: 'AWS/EC2',
value: 'CPUUtilization',
@ -214,7 +214,7 @@ describe('datasource', () => {
value: 'CPUPercentage',
},
]);
const allMetrics = await datasource.getAllMetrics('us-east-2');
const allMetrics = await datasource.api.getAllMetrics('us-east-2');
expect(allMetrics[0].metricName).toEqual('CPUUtilization');
expect(allMetrics[0].namespace).toEqual('AWS/EC2');
expect(allMetrics[1].metricName).toEqual('CPUPercentage');

View File

@ -18,6 +18,7 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr
import { RowContextOptions } from '../../../features/logs/components/LogRowContextProvider';
import { CloudWatchAnnotationSupport } from './annotationSupport';
import { CloudWatchAPI } from './api';
import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider';
import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards';
import { CloudWatchLanguageProvider } from './language_provider';
@ -31,7 +32,6 @@ import {
CloudWatchLogsQuery,
CloudWatchMetricsQuery,
CloudWatchQuery,
Dimensions,
} from './types';
import { CloudWatchVariableSupport } from './variables';
@ -39,18 +39,18 @@ export class CloudWatchDatasource
extends DataSourceWithBackend<CloudWatchQuery, CloudWatchJsonData>
implements DataSourceWithLogsContextSupport<CloudWatchLogsQuery>
{
defaultRegion: any;
defaultRegion?: string;
languageProvider: CloudWatchLanguageProvider;
sqlCompletionItemProvider: SQLCompletionItemProvider;
metricMathCompletionItemProvider: MetricMathCompletionItemProvider;
type = 'cloudwatch';
standardStatistics = ['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount'];
private metricsQueryRunner: CloudWatchMetricsQueryRunner;
private annotationQueryRunner: CloudWatchAnnotationQueryRunner;
// this member should be private too, but we need to fix https://github.com/grafana/grafana/issues/55243 to enable that
logsQueryRunner: CloudWatchLogsQueryRunner;
api: CloudWatchAPI;
constructor(
instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>,
@ -59,18 +59,17 @@ export class CloudWatchDatasource
) {
super(instanceSettings);
this.defaultRegion = instanceSettings.jsonData.defaultRegion;
this.api = new CloudWatchAPI(instanceSettings, templateSrv);
this.languageProvider = new CloudWatchLanguageProvider(this);
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv);
this.variables = new CloudWatchVariableSupport(this);
this.annotations = CloudWatchAnnotationSupport;
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.api, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.api, this.templateSrv);
this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv);
this.logsQueryRunner = new CloudWatchLogsQueryRunner(instanceSettings, templateSrv, timeSrv);
this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv);
this.variables = new CloudWatchVariableSupport(this.api, this.logsQueryRunner);
this.annotations = CloudWatchAnnotationSupport;
}
// datasource api
filterQuery(query: CloudWatchQuery) {
return query.hide !== true || (isCloudWatchMetricsQuery(query) && query.id !== '');
}
@ -79,18 +78,31 @@ export class CloudWatchDatasource
options = cloneDeep(options);
let queries = options.targets.filter(this.filterQuery);
const { logQueries, metricsQueries, annotationQueries } = getTargetsByQueryMode(queries);
const logQueries: CloudWatchLogsQuery[] = [];
const metricsQueries: CloudWatchMetricsQuery[] = [];
const annotationQueries: CloudWatchAnnotationQuery[] = [];
queries.forEach((query) => {
if (isCloudWatchAnnotationQuery(query)) {
annotationQueries.push(query);
} else if (isCloudWatchLogsQuery(query)) {
logQueries.push(query);
} else {
metricsQueries.push(query);
}
});
const dataQueryResponses: Array<Observable<DataQueryResponse>> = [];
if (logQueries.length > 0) {
if (logQueries.length) {
dataQueryResponses.push(this.logsQueryRunner.handleLogQueries(logQueries, options));
}
if (metricsQueries.length > 0) {
if (metricsQueries.length) {
dataQueryResponses.push(this.metricsQueryRunner.handleMetricQueries(metricsQueries, options));
}
if (annotationQueries.length > 0) {
if (annotationQueries.length) {
dataQueryResponses.push(this.annotationQueryRunner.handleAnnotationQuery(annotationQueries, options));
}
// No valid targets, return the empty result to save a round trip.
@ -111,8 +123,9 @@ export class CloudWatchDatasource
return queries.map((query) => ({
...query,
region: this.getActualRegion(
this.metricsQueryRunner.replaceVariableAndDisplayWarningIfMulti(query.region, scopedVars)
region: this.metricsQueryRunner.replaceVariableAndDisplayWarningIfMulti(
this.getActualRegion(query.region),
scopedVars
),
...(isCloudWatchMetricsQuery(query) &&
this.metricsQueryRunner.interpolateMetricsQueryVariables(query, scopedVars)),
@ -155,134 +168,10 @@ export class CloudWatchDatasource
return this.templateSrv.getVariables().map((v) => `$${v.name}`);
}
getDefaultRegion() {
return this.defaultRegion;
}
getActualRegion(region?: string) {
if (region === 'default' || region === undefined || region === '') {
return this.getDefaultRegion();
return this.defaultRegion;
}
return region;
}
doMetricResourceRequest(subtype: string, parameters?: any): Promise<Array<{ text: any; label: any; value: any }>> {
return this.getResource(subtype, parameters);
}
// resource requests
getRegions(): Promise<Array<{ label: string; value: string; text: string }>> {
return this.doMetricResourceRequest('regions').then((regions: any) => [
{ label: 'default', value: 'default', text: 'default' },
...regions,
]);
}
getNamespaces() {
return this.doMetricResourceRequest('namespaces');
}
async getMetrics(namespace: string | undefined, region?: string) {
if (!namespace) {
return [];
}
return this.doMetricResourceRequest('metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
});
}
async getAllMetrics(region: string): Promise<Array<{ metricName: string; namespace: string }>> {
const values = await this.doMetricResourceRequest('all-metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
});
return values.map((v) => ({ metricName: v.value, namespace: v.text }));
}
async getDimensionKeys(
namespace: string | undefined,
region: string,
dimensionFilters: Dimensions = {},
metricName = ''
) {
if (!namespace) {
return [];
}
return this.doMetricResourceRequest('dimension-keys', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
dimensionFilters: JSON.stringify(this.metricsQueryRunner.convertDimensionFormat(dimensionFilters, {})),
metricName,
});
}
async getDimensionValues(
region: string,
namespace: string | undefined,
metricName: string | undefined,
dimensionKey: string,
filterDimensions: {}
) {
if (!namespace || !metricName) {
return [];
}
const values = await this.doMetricResourceRequest('dimension-values', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
metricName: this.templateSrv.replace(metricName.trim()),
dimensionKey: this.templateSrv.replace(dimensionKey),
dimensions: JSON.stringify(this.metricsQueryRunner.convertDimensionFormat(filterDimensions, {})),
});
return values;
}
getEbsVolumeIds(region: string, instanceId: string) {
return this.doMetricResourceRequest('ebs-volume-ids', {
region: this.templateSrv.replace(this.getActualRegion(region)),
instanceId: this.templateSrv.replace(instanceId),
});
}
getEc2InstanceAttribute(region: string, attributeName: string, filters: any) {
return this.doMetricResourceRequest('ec2-instance-attribute', {
region: this.templateSrv.replace(this.getActualRegion(region)),
attributeName: this.templateSrv.replace(attributeName),
filters: JSON.stringify(this.metricsQueryRunner.convertMultiFilterFormat(filters, 'filter key')),
});
}
getResourceARNs(region: string, resourceType: string, tags: any) {
return this.doMetricResourceRequest('resource-arns', {
region: this.templateSrv.replace(this.getActualRegion(region)),
resourceType: this.templateSrv.replace(resourceType),
tags: JSON.stringify(this.metricsQueryRunner.convertMultiFilterFormat(tags, 'tag name')),
});
}
}
function getTargetsByQueryMode(targets: CloudWatchQuery[]) {
const logQueries: CloudWatchLogsQuery[] = [];
const metricsQueries: CloudWatchMetricsQuery[] = [];
const annotationQueries: CloudWatchAnnotationQuery[] = [];
targets.forEach((query) => {
if (isCloudWatchAnnotationQuery(query)) {
annotationQueries.push(query);
} else if (isCloudWatchLogsQuery(query)) {
logQueries.push(query);
} else {
metricsQueries.push(query);
}
});
return {
logQueries,
metricsQueries,
annotationQueries,
};
}

View File

@ -19,7 +19,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array<SelectableV
options: datasource.getVariables().map(toOption),
};
datasource
datasource.api
.getRegions()
.then((regions: Array<SelectableValue<string>>) => setRegions([...regions, variableOptionGroup]))
.finally(() => setRegionsIsLoading(false));
@ -31,7 +31,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array<SelectableV
export const useNamespaces = (datasource: CloudWatchDatasource) => {
const [namespaces, setNamespaces] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
datasource.getNamespaces().then((namespaces) => {
datasource.api.getNamespaces().then((namespaces) => {
setNamespaces(appendTemplateVariables(datasource, namespaces));
});
}, [datasource]);
@ -42,7 +42,7 @@ export const useNamespaces = (datasource: CloudWatchDatasource) => {
export const useMetrics = (datasource: CloudWatchDatasource, region: string, namespace: string | undefined) => {
const [metrics, setMetrics] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
datasource.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
datasource.api.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
setMetrics(appendTemplateVariables(datasource, result));
});
}, [datasource, region, namespace]);
@ -61,7 +61,7 @@ export const useDimensionKeys = (
// doing deep comparison to avoid making new api calls to list metrics unless dimension filter object props changes
useDeepCompareEffect(() => {
datasource
datasource.api
.getDimensionKeys(namespace, region, dimensionFilter, metricName)
.then((result: Array<SelectableValue<string>>) => {
setDimensionKeys(appendTemplateVariables(datasource, result));

View File

@ -1,10 +1,10 @@
import { getTemplateSrv } from '@grafana/runtime';
import { Monaco, monacoTypes } from '@grafana/ui';
import { setupMockedTemplateService } from '../../__mocks__/CloudWatchDataSource';
import * as MetricMathTestData from '../../__mocks__/metric-math-test-data';
import MonacoMock from '../../__mocks__/monarch/Monaco';
import TextModel from '../../__mocks__/monarch/TextModel';
import { CloudWatchDatasource } from '../../datasource';
import { CloudWatchAPI } from '../../api';
import cloudWatchMetricMathLanguageDefinition from '../definition';
import {
METRIC_MATH_FNS,
@ -19,10 +19,9 @@ import { MetricMathCompletionItemProvider } from './CompletionItemProvider';
const getSuggestions = async (value: string, position: monacoTypes.IPosition) => {
const setup = new MetricMathCompletionItemProvider(
{
getVariables: () => [],
getActualRegion: () => 'us-east-2',
} as any as CloudWatchDatasource,
getTemplateSrv()
} as CloudWatchAPI,
setupMockedTemplateService([])
);
const monaco = MonacoMock as Monaco;
const provider = setup.getCompletionProvider(monaco, cloudWatchMetricMathLanguageDefinition);

View File

@ -1,7 +1,7 @@
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { CloudWatchDatasource } from '../../datasource';
import { CloudWatchAPI } from '../../api';
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
import { LinkedToken } from '../../monarch/LinkedToken';
import { TRIGGER_SUGGEST } from '../../monarch/commands';
@ -21,8 +21,8 @@ import { MetricMathTokenTypes } from './types';
type CompletionItem = monacoTypes.languages.CompletionItem;
export class MetricMathCompletionItemProvider extends CompletionItemProvider {
constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
super(datasource, templateSrv);
constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
super(api, templateSrv);
this.getStatementPosition = getStatementPosition;
this.getSuggestionKinds = getSuggestionKinds;
this.tokenTypes = MetricMathTokenTypes;
@ -110,11 +110,12 @@ export class MetricMathCompletionItemProvider extends CompletionItemProvider {
}
// always suggest template variables
this.templateVariables.map((v) => {
addSuggestion(v, {
this.templateSrv.getVariables().map((v) => {
const variable = `$${v.name}`;
addSuggestion(variable, {
range,
label: v,
insertText: v,
label: variable,
insertText: variable,
kind: monaco.languages.CompletionItemKind.Variable,
sortText: CompletionItemPriority.Low,
});

View File

@ -1,7 +1,7 @@
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchAPI } from '../api';
import { LinkedToken } from './LinkedToken';
import { linkedTokenBuilder } from './linkedTokenBuilder';
@ -18,15 +18,13 @@ CompletionItemProvider is an extendable class which needs to implement :
- getSuggestions
*/
export class CompletionItemProvider implements Completeable {
templateVariables: string[];
datasource: CloudWatchDatasource;
api: CloudWatchAPI;
templateSrv: TemplateSrv;
tokenTypes: TokenTypes;
constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
this.datasource = datasource;
constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
this.api = api;
this.templateSrv = templateSrv;
this.templateVariables = this.datasource.getVariables();
this.templateSrv = templateSrv;
// implement with more specific tokens when extending this class

View File

@ -6,10 +6,10 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { CloudWatchAnnotationQuery, CloudWatchJsonData, CloudWatchQuery } from '../types';
import { CloudWatchQueryRunner } from './CloudWatchQueryRunner';
import { CloudWatchRequest } from './CloudWatchRequest';
// This class handles execution of CloudWatch annotation queries
export class CloudWatchAnnotationQueryRunner extends CloudWatchQueryRunner {
export class CloudWatchAnnotationQueryRunner extends CloudWatchRequest {
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {
super(instanceSettings, templateSrv);
}

View File

@ -51,13 +51,13 @@ import { addDataLinksToLogsResponse } from '../utils/datalinks';
import { runWithRetry } from '../utils/logsRetry';
import { increasingInterval } from '../utils/rxjs/increasingInterval';
import { CloudWatchQueryRunner } from './CloudWatchQueryRunner';
import { CloudWatchRequest } from './CloudWatchRequest';
export const LOG_IDENTIFIER_INTERNAL = '__log__grafana_internal__';
export const LOGSTREAM_IDENTIFIER_INTERNAL = '__logstream__grafana_internal__';
// This class handles execution of CloudWatch logs query data queries
export class CloudWatchLogsQueryRunner extends CloudWatchQueryRunner {
export class CloudWatchLogsQueryRunner extends CloudWatchRequest {
logsTimeout: string;
defaultLogGroups: string[];
logQueries: Record<string, { id: string; region: string; statsQuery: boolean }> = {};

View File

@ -34,7 +34,7 @@ import {
MetricRequest,
} from '../types';
import { CloudWatchQueryRunner } from './CloudWatchQueryRunner';
import { CloudWatchRequest } from './CloudWatchRequest';
const displayAlert = (datasourceName: string, region: string) =>
store.dispatch(
@ -47,9 +47,8 @@ const displayAlert = (datasourceName: string, region: string) =>
)
)
);
// This class handles execution of CloudWatch metrics query data queries
export class CloudWatchMetricsQueryRunner extends CloudWatchQueryRunner {
export class CloudWatchMetricsQueryRunner extends CloudWatchRequest {
debouncedAlert: (datasourceName: string, region: string) => void = memoizedDebounce(
displayAlert,
AppNotificationTimeout.Error

View File

@ -11,7 +11,7 @@ import { AppNotificationTimeout } from 'app/types';
import memoizedDebounce from '../memoizedDebounce';
import { CloudWatchJsonData, Dimensions, MetricRequest, MultiFilters, TSDBResponse } from '../types';
export abstract class CloudWatchQueryRunner {
export abstract class CloudWatchRequest {
templateSrv: TemplateSrv;
ref: DataSourceRef;
dsQueryEndpoint = '/api/ds/query';

View File

@ -0,0 +1 @@
export const standardStatistics = ['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount'];

View File

@ -14,18 +14,18 @@ const defaultQuery: VariableQuery = {
refId: '',
};
const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable] });
ds.datasource.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]);
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
ds.datasource.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
ds.datasource.logsQueryRunner.describeAllLogGroups = jest.fn().mockResolvedValue(['a', 'b']);
const mock = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable] });
mock.datasource.api.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]);
mock.datasource.api.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
mock.datasource.api.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
mock.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
mock.datasource.logsQueryRunner.describeAllLogGroups = jest.fn().mockResolvedValue(['a', 'b']);
const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]);
const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]);
const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]);
const getResourceARNs = jest.fn().mockResolvedValue([{ label: 'h', value: 'h' }]);
const variables = new CloudWatchVariableSupport(ds.datasource);
const variables = new CloudWatchVariableSupport(mock.datasource.api, mock.datasource.logsQueryRunner);
describe('variables', () => {
it('should run regions', async () => {
@ -57,7 +57,7 @@ describe('variables', () => {
dimensionFilters: { a: 'b' },
};
beforeEach(() => {
ds.datasource.getDimensionValues = getDimensionValues;
mock.datasource.api.getDimensionValues = getDimensionValues;
getDimensionValues.mockClear();
});
@ -87,7 +87,7 @@ describe('variables', () => {
describe('EBS volume ids', () => {
beforeEach(() => {
ds.datasource.getEbsVolumeIds = getEbsVolumeIds;
mock.datasource.api.getEbsVolumeIds = getEbsVolumeIds;
getEbsVolumeIds.mockClear();
});
@ -116,7 +116,7 @@ describe('variables', () => {
ec2Filters: { a: ['b'] },
};
beforeEach(() => {
ds.datasource.getEc2InstanceAttribute = getEc2InstanceAttribute;
mock.datasource.api.getEc2InstanceAttribute = getEc2InstanceAttribute;
getEc2InstanceAttribute.mockClear();
});
@ -141,7 +141,7 @@ describe('variables', () => {
tags: { a: ['b'] },
};
beforeEach(() => {
ds.datasource.getResourceARNs = getResourceARNs;
mock.datasource.api.getResourceARNs = getResourceARNs;
getResourceARNs.mockClear();
});

View File

@ -3,17 +3,17 @@ import { map } from 'rxjs/operators';
import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@grafana/data';
import { CloudWatchAPI } from './api';
import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
import { CloudWatchDatasource } from './datasource';
import { migrateVariableQuery } from './migrations/variableQueryMigrations';
import { CloudWatchLogsQueryRunner } from './query-runner/CloudWatchLogsQueryRunner';
import { standardStatistics } from './standardStatistics';
import { VariableQuery, VariableQueryType } from './types';
export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchDatasource, VariableQuery> {
private readonly datasource: CloudWatchDatasource;
constructor(datasource: CloudWatchDatasource) {
constructor(private readonly api: CloudWatchAPI, private readonly logsQueryRunner: CloudWatchLogsQueryRunner) {
super();
this.datasource = datasource;
this.query = this.query.bind(this);
}
@ -55,7 +55,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleLogGroupsQuery({ region, logGroupPrefix }: VariableQuery) {
const logGroups = await this.datasource.logsQueryRunner.describeAllLogGroups({
const logGroups: string[] = await this.logsQueryRunner.describeAllLogGroups({
region,
logGroupNamePrefix: logGroupPrefix,
});
@ -67,7 +67,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleRegionsQuery() {
const regions = await this.datasource.getRegions();
const regions = await this.api.getRegions();
return regions.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -76,7 +76,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleNamespacesQuery() {
const namespaces = await this.datasource.getNamespaces();
const namespaces = await this.api.getNamespaces();
return namespaces.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -85,7 +85,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleMetricsQuery({ namespace, region }: VariableQuery) {
const metrics = await this.datasource.getMetrics(namespace, region);
const metrics = await this.api.getMetrics(namespace, region);
return metrics.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -94,7 +94,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleDimensionKeysQuery({ namespace, region }: VariableQuery) {
const keys = await this.datasource.getDimensionKeys(namespace, region);
const keys = await this.api.getDimensionKeys(namespace, region);
return keys.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -106,13 +106,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
if (!dimensionKey || !metricName) {
return [];
}
const keys = await this.datasource.getDimensionValues(
region,
namespace,
metricName,
dimensionKey,
dimensionFilters ?? {}
);
const keys = await this.api.getDimensionValues(region, namespace, metricName, dimensionKey, dimensionFilters ?? {});
return keys.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -124,7 +118,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
if (!instanceID) {
return [];
}
const ids = await this.datasource.getEbsVolumeIds(region, instanceID);
const ids = await this.api.getEbsVolumeIds(region, instanceID);
return ids.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -136,7 +130,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
if (!attributeName) {
return [];
}
const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {});
const values = await this.api.getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {});
return values.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -148,7 +142,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
if (!resourceType) {
return [];
}
const keys = await this.datasource.getResourceARNs(region, resourceType, tags ?? {});
const keys = await this.api.getResourceARNs(region, resourceType, tags ?? {});
return keys.map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
@ -157,7 +151,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleStatisticsQuery() {
return this.datasource.standardStatistics.map((s: string) => ({
return standardStatistics.map((s: string) => ({
text: s,
value: s,
expandable: true,