Cloudwatch: Cleanup resource api (#61465)

Co-authored-by: Isabella Siu <isabella.siu@grafana.com>
This commit is contained in:
Erik Sundell 2023-01-17 20:27:53 +01:00 committed by GitHub
parent 23e05373a7
commit 8c826cd785
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 289 additions and 277 deletions

View File

@ -80,12 +80,12 @@ export function setupMockedDataSource({
const timeSrv = getTimeSrv();
const datasource = new CloudWatchDatasource(customInstanceSettings, templateService, timeSrv);
datasource.getVariables = () => ['test'];
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getRegions = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getAccounts = jest.fn().mockResolvedValue([]);
datasource.api.getLogGroups = jest.fn().mockResolvedValue([]);
datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.resources.getRegions = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
datasource.resources.getAccounts = jest.fn().mockResolvedValue([]);
datasource.resources.getLogGroups = jest.fn().mockResolvedValue([]);
const fetchMock = jest.fn().mockReturnValue(of({}));
setBackendSrv({
...getBackendSrv(),

View File

@ -3,11 +3,11 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariableModel } from 'app/features/variables/types';
import { CloudWatchAPI } from '../api';
import { ResourcesAPI } from '../resources/ResourcesAPI';
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
export function setupMockedAPI({
export function setupMockedResourcesAPI({
variables,
response,
getMock,
@ -20,7 +20,7 @@ export function setupMockedAPI({
let templateService = variables ? setupMockedTemplateService(variables) : new TemplateSrv();
const timeSrv = getTimeSrv();
const api = new CloudWatchAPI(CloudWatchSettings, templateService);
const api = new ResourcesAPI(CloudWatchSettings, templateService);
let resourceRequestMock = getMock ? getMock : jest.fn().mockReturnValue(response);
setBackendSrv({
...getBackendSrv(),

View File

@ -3,11 +3,11 @@ import { uniq } from 'lodash';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { CloudWatchAPI } from '../../api';
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
import { LinkedToken } from '../../monarch/LinkedToken';
import { TRIGGER_SUGGEST } from '../../monarch/commands';
import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types';
import { ResourcesAPI } from '../../resources/ResourcesAPI';
import {
BY,
FROM,
@ -34,9 +34,9 @@ type CompletionItem = monacoTypes.languages.CompletionItem;
export class SQLCompletionItemProvider extends CompletionItemProvider {
region: string;
constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
super(api, templateSrv);
this.region = api.getActualRegion() ?? '';
constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
super(resources, templateSrv);
this.region = resources.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.api.getMetrics({
const metrics = await this.resources.getMetrics({
namespace: namespaceToken?.value.replace(/\"/g, ''),
region: this.region,
});
metrics.forEach((m) => m.value && addSuggestion(m.value));
} else {
// If no namespace is specified in the query, just list all metrics
const metrics = await this.api.getAllMetrics({ region: this.region });
const metrics = await this.resources.getAllMetrics({ region: this.region });
uniq(metrics.map((m) => m.metricName)).forEach((m) => 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.api.getMetrics({ region: this.region });
const metrics = await this.resources.getMetrics({ region: 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.api.getNamespaces();
const ns = await this.resources.getNamespaces();
namespaces = ns.map((n) => n.value);
}
namespaces.map((n) => addSuggestion(`"${n}"`, { insertText: `"${n}"` }));
@ -179,7 +179,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider {
dimensionFilters = (labelKeyTokens || []).reduce((acc, curr) => {
return { ...acc, [curr.value]: null };
}, {});
const keys = await this.api.getDimensionKeys({
const keys = await this.resources.getDimensionKeys({
namespace: this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
region: this.templateSrv.replace(this.region),
metricName: metricNameToken?.value,
@ -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.api.getDimensionValues({
const values = await this.resources.getDimensionValues({
region: this.region,
namespace: namespaceToken.value.replace(/\"/g, ''),
metricName: metricNameToken.value,

View File

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

View File

@ -40,7 +40,7 @@ export function CloudWatchLink({ panelData, query, datasource }: Props) {
source: sources ?? [],
};
setHref(encodeUrl(urlProps, datasource.api.getActualRegion(query.region)));
setHref(encodeUrl(urlProps, datasource.resources.getActualRegion(query.region)));
}
}, [panelData, prevPanelData, datasource, query]);

View File

@ -99,7 +99,7 @@ describe('Render', () => {
putMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource }));
getMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource }));
loadDataSourceMock.mockResolvedValue(datasource);
datasource.api.getRegions = jest.fn().mockResolvedValue([
datasource.resources.getRegions = jest.fn().mockResolvedValue([
{
label: 'ap-east-1',
value: 'ap-east-1',

View File

@ -14,8 +14,8 @@ import { createWarningNotification } from 'app/core/copy/appNotification';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { store } from 'app/store/store';
import { SelectableResourceValue } from '../api';
import { CloudWatchDatasource } from '../datasource';
import { SelectableResourceValue } from '../resources/types';
import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types';
import { LogGroupsField } from './LogGroups/LogGroupsField';
@ -45,7 +45,7 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
loadRegions={
datasource &&
(async () => {
return datasource.api
return datasource.resources
.getRegions()
.then((regions) =>
regions.reduce(

View File

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

View File

@ -51,7 +51,7 @@ export const FilterItem: FunctionComponent<Props> = ({
return [];
}
return datasource.api
return datasource.resources
.getDimensionValues({
dimensionKey: filter.key,
dimensionFilters: dimensionsExcludingCurrentKey,

View File

@ -32,13 +32,13 @@ describe('LogGroupSelection', () => {
it('should call getLogGroups to get associated log group arns and then update props if rendered with legacy log group names', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
defaultProps.datasource.api.getLogGroups = jest
defaultProps.datasource.resources.getLogGroups = jest
.fn()
.mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]);
render(<LogGroupsField {...defaultProps} legacyLogGroupNames={['loggroupname']} />);
await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument());
expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledWith({
expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledWith({
region: defaultProps.region,
logGroupNamePrefix: 'loggroupname',
});
@ -48,14 +48,14 @@ describe('LogGroupSelection', () => {
it('should not call getLogGroups to get associated log group arns for template variables that were part of the legacy log group names array, only include them in the call to onChange', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
defaultProps.datasource = setupMockedDataSource({ variables: [logGroupNamesVariable] }).datasource;
defaultProps.datasource.api.getLogGroups = jest
defaultProps.datasource.resources.getLogGroups = jest
.fn()
.mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]);
render(<LogGroupsField {...defaultProps} legacyLogGroupNames={['loggroupname', logGroupNamesVariable.name]} />);
await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument());
expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledTimes(1);
expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledWith({
expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledTimes(1);
expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledWith({
region: defaultProps.region,
logGroupNamePrefix: 'loggroupname',
});
@ -67,12 +67,12 @@ describe('LogGroupSelection', () => {
it('should not call getLogGroups and update props if rendered with log groups', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
defaultProps.datasource.api.getLogGroups = jest
defaultProps.datasource.resources.getLogGroups = jest
.fn()
.mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]);
render(<LogGroupsField {...defaultProps} logGroups={[{ arn: 'arn', name: 'loggroupname' }]} />);
await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument());
expect(defaultProps.datasource.api.getLogGroups).not.toHaveBeenCalled();
expect(defaultProps.datasource.resources.getLogGroups).not.toHaveBeenCalled();
expect(defaultProps.onChange).not.toHaveBeenCalled();
});
});

View File

@ -3,7 +3,8 @@ import React, { useEffect, useState } from 'react';
import { CloudWatchDatasource } from '../../datasource';
import { useAccountOptions } from '../../hooks';
import { DescribeLogGroupsRequest, LogGroup } from '../../types';
import { DescribeLogGroupsRequest } from '../../resources/types';
import { LogGroup } from '../../types';
import { isTemplateVariable } from '../../utils/templateVariableUtils';
import { LogGroupsSelector } from './LogGroupsSelector';
@ -32,7 +33,7 @@ export const LogGroupsField = ({
maxNoOfVisibleLogGroups,
onBeforeOpen,
}: Props) => {
const accountState = useAccountOptions(datasource?.api, region);
const accountState = useAccountOptions(datasource?.resources, region);
const [loadingLogGroupsStarted, setLoadingLogGroupsStarted] = useState(false);
useEffect(() => {
@ -41,13 +42,15 @@ export const LogGroupsField = ({
setLoadingLogGroupsStarted(true);
// there's no need to migrate variables, they will be taken care of in the logs query runner
const variables = legacyLogGroupNames.filter((lgn) => isTemplateVariable(datasource.api.templateSrv, lgn));
const variables = legacyLogGroupNames.filter((lgn) => isTemplateVariable(datasource.resources.templateSrv, lgn));
const legacyLogGroupNameValues = legacyLogGroupNames.filter(
(lgn) => !isTemplateVariable(datasource.api.templateSrv, lgn)
(lgn) => !isTemplateVariable(datasource.resources.templateSrv, lgn)
);
Promise.all(
legacyLogGroupNameValues.map((lg) => datasource.api.getLogGroups({ region: region, logGroupNamePrefix: lg }))
legacyLogGroupNameValues.map((lg) =>
datasource.resources.getLogGroups({ region: region, logGroupNamePrefix: lg })
)
)
.then((results) => {
const logGroups = results.flatMap((r) =>
@ -70,7 +73,7 @@ export const LogGroupsField = ({
<div className={`gf-form gf-form--grow flex-grow-1 ${rowGap}`}>
<LogGroupsSelector
fetchLogGroups={async (params: Partial<DescribeLogGroupsRequest>) =>
datasource?.api.getLogGroups({ region: region, ...params }) ?? []
datasource?.resources.getLogGroups({ region: region, ...params }) ?? []
}
onChange={onChange}
accountOptions={accountState.value}

View File

@ -5,7 +5,7 @@ import lodash from 'lodash';
import React from 'react';
import selectEvent from 'react-select-event';
import { ResourceResponse, LogGroupResponse } from '../../types';
import { ResourceResponse, LogGroupResponse } from '../../resources/types';
import { LogGroupsSelector } from './LogGroupsSelector';

View File

@ -5,7 +5,8 @@ import { EditorField, Space } from '@grafana/experimental';
import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, Select, useStyles2 } from '@grafana/ui';
import Search from '../../Search';
import { DescribeLogGroupsRequest, LogGroup, LogGroupResponse, ResourceResponse } from '../../types';
import { DescribeLogGroupsRequest, ResourceResponse, LogGroupResponse } from '../../resources/types';
import { LogGroup } from '../../types';
import { Account, ALL_ACCOUNTS_OPTION } from '../Account';
import getStyles from '../styles';

View File

@ -15,9 +15,9 @@ const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]);
ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
ds.datasource.resources.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.api.getNamespaces = jest.fn().mockResolvedValue(namespaces);
propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics);
propsNamespaceMetrics.datasource.resources.getNamespaces = jest.fn().mockResolvedValue(namespaces);
propsNamespaceMetrics.datasource.resources.getMetrics = jest.fn().mockResolvedValue(metrics);
onChange.mockClear();
});
@ -146,7 +146,7 @@ describe('MetricStatEditor', () => {
});
it('should remove metricName from metricStat if it does not exist in new namespace', async () => {
propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockImplementation(({ namespace, region }) => {
propsNamespaceMetrics.datasource.resources.getMetrics = jest.fn().mockImplementation(({ namespace, region }) => {
let mockMetrics =
namespace === 'n1' && region === props.metricStat.region
? metrics
@ -202,8 +202,8 @@ describe('MetricStatEditor', () => {
it('should set value to "all" when its a monitoring account and no account id is defined in the query', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const onChange = jest.fn();
props.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true);
props.datasource.api.getAccounts = jest.fn().mockResolvedValue([
props.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true);
props.datasource.resources.getAccounts = jest.fn().mockResolvedValue([
{
value: '123456789',
label: 'test-account1',
@ -231,8 +231,8 @@ describe('MetricStatEditor', () => {
it('should unset value when no accounts were found and an account id is defined in the query', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const onChange = jest.fn();
props.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false);
props.datasource.api.getAccounts = jest.fn().mockResolvedValue([]);
props.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false);
props.datasource.resources.getAccounts = jest.fn().mockResolvedValue([]);
await act(async () => {
render(
<MetricStatEditor

View File

@ -31,10 +31,10 @@ export function MetricStatEditor({
const namespaces = useNamespaces(datasource);
const metrics = useMetrics(datasource, metricStat);
const dimensionKeys = useDimensionKeys(datasource, { ...metricStat, dimensionFilters: metricStat.dimensions });
const accountState = useAccountOptions(datasource.api, metricStat.region);
const accountState = useAccountOptions(datasource.resources, metricStat.region);
useEffect(() => {
datasource.api.isMonitoringAccount(metricStat.region).then((isMonitoringAccount) => {
datasource.resources.isMonitoringAccount(metricStat.region).then((isMonitoringAccount) => {
if (isMonitoringAccount && !accountState.loading && accountState.value?.length && !metricStat.accountId) {
onChange({ ...metricStat, accountId: 'all' });
}
@ -43,7 +43,7 @@ export function MetricStatEditor({
onChange({ ...metricStat, accountId: undefined });
}
});
}, [accountState, metricStat, onChange, datasource.api]);
}, [accountState, metricStat, onChange, datasource.resources]);
const onNamespaceChange = async (metricStat: MetricStat) => {
const validatedQuery = await validateMetricName(metricStat);
@ -55,7 +55,7 @@ export function MetricStatEditor({
if (!metricName) {
return metricStat;
}
await datasource.api.getMetrics({ namespace, region }).then((result: Array<SelectableValue<string>>) => {
await datasource.resources.getMetrics({ namespace, region }).then((result: Array<SelectableValue<string>>) => {
if (!result.find((metric) => metric.value === metricName)) {
metricName = '';
}

View File

@ -46,11 +46,11 @@ const setup = () => {
const datasource = new CloudWatchDatasource(instanceSettings, templateSrv as any, {} as any);
datasource.metricFindQuery = async () => [{ value: 'test', label: 'test', text: 'test' }];
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getRegions = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false);
datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
datasource.resources.getRegions = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false);
const props: Props = {
query: {
@ -93,7 +93,9 @@ describe('QueryEditor', () => {
if (props.query.queryMode !== 'Metrics') {
fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`);
}
props.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]);
props.datasource.resources.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

@ -165,9 +165,9 @@ describe('QueryEditor should render right editor', () => {
let datasourceMock: ReturnType<typeof setupMockedDataSource>;
beforeEach(() => {
datasourceMock = setupMockedDataSource();
datasourceMock.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true);
datasourceMock.datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasourceMock.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasourceMock.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true);
datasourceMock.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
datasourceMock.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]);
originalValue = config.featureToggles.cloudWatchCrossAccountQuerying;
});
afterEach(() => {

View File

@ -13,7 +13,7 @@ const originalFeatureToggleValue = config.featureToggles.cloudWatchCrossAccountQ
const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]);
ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([]);
describe('QueryHeader', () => {
afterEach(() => {
@ -22,14 +22,14 @@ describe('QueryHeader', () => {
describe('when changing region', () => {
const { datasource } = setupMockedDataSource();
datasource.api.getRegions = jest.fn().mockResolvedValue([
datasource.resources.getRegions = jest.fn().mockResolvedValue([
{ value: 'us-east-2', label: 'us-east-2' },
{ value: 'us-east-1', label: 'us-east-1' },
]);
it('should reset account id if new region is not monitoring account', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const onChange = jest.fn();
datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false);
datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false);
render(
<QueryHeader
datasource={datasource}
@ -53,7 +53,7 @@ describe('QueryHeader', () => {
it('should not reset account id if new region is a monitoring account', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const onChange = jest.fn();
datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true);
datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true);
render(
<QueryHeader
@ -78,7 +78,7 @@ describe('QueryHeader', () => {
it('should not call isMonitoringAccount if its a logs query', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const onChange = jest.fn();
datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true);
datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true);
render(
<QueryHeader
@ -93,13 +93,13 @@ describe('QueryHeader', () => {
await act(async () => {
await selectEvent.select(screen.getByLabelText(/Region/), 'us-east-2', { container: document.body });
});
expect(datasource.api.isMonitoringAccount).not.toHaveBeenCalledWith('us-east-2');
expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith('us-east-2');
});
it('should not call isMonitoringAccount if feature toggle is not enabled', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = false;
const onChange = jest.fn();
datasource.api.isMonitoringAccount = jest.fn();
datasource.resources.isMonitoringAccount = jest.fn();
render(
<QueryHeader
@ -114,7 +114,7 @@ describe('QueryHeader', () => {
await act(async () => {
await selectEvent.select(screen.getByLabelText(/Region/), 'us-east-2', { container: document.body });
});
expect(datasource.api.isMonitoringAccount).not.toHaveBeenCalledWith();
expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith();
});
});
});

View File

@ -32,7 +32,7 @@ const QueryHeader: React.FC<Props> = ({
onRunQuery,
}) => {
const { queryMode, region } = query;
const isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region);
const isMonitoringAccount = useIsMonitoringAccount(datasource.resources, query.region);
const [regions, regionIsLoading] = useRegions(datasource);
const onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => {
@ -46,7 +46,7 @@ const QueryHeader: React.FC<Props> = ({
};
const onRegionChange = async (region: string) => {
if (config.featureToggles.cloudWatchCrossAccountQuerying && isCloudWatchMetricsQuery(query)) {
const isMonitoringAccount = await datasource.api.isMonitoringAccount(region);
const isMonitoringAccount = await datasource.resources.isMonitoringAccount(region);
onChange({ ...query, region, accountId: isMonitoringAccount ? query.accountId : undefined });
} else {
onChange({ ...query, region });

View File

@ -22,10 +22,10 @@ export const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({
describe('Cloudwatch SQLBuilderEditor', () => {
beforeEach(() => {
datasource.api.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]);
datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([]);
});
const baseProps = {
@ -46,7 +46,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AWS/EC2')).toBeInTheDocument();
expect(screen.getByLabelText('With schema')).not.toBeChecked();
@ -67,7 +67,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AWS/EC2')).toBeInTheDocument();
expect(screen.getByLabelText('With schema')).toBeChecked();
@ -94,7 +94,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() =>
expect(datasource.api.getDimensionKeys).toHaveBeenCalledWith({
expect(datasource.resources.getDimensionKeys).toHaveBeenCalledWith({
namespace: 'AWS/EC2',
region: query.region,
dimensionFilters: { InstanceId: null },
@ -121,7 +121,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AVERAGE')).toBeInTheDocument();
expect(screen.getByText('CPUUtilization')).toBeInTheDocument();
@ -137,7 +137,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled());
expect(screen.getByText('AVG')).toBeInTheDocument();
const directionElement = screen.getByLabelText('Direction');
@ -149,7 +149,7 @@ describe('Cloudwatch SQLBuilderEditor', () => {
const query = makeSQLQuery({});
render(<SQLBuilderEditor {...baseProps} query={query} />);
await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled());
await waitFor(() => expect(datasource.resources.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.api.getNamespaces = jest.fn().mockResolvedValue(namespaces);
datasource.api.getMetrics = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]);
datasource.resources.getNamespaces = jest.fn().mockResolvedValue(namespaces);
datasource.resources.getMetrics = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([]);
onQueryChange.mockReset();
});
it('Should not reset metricName when selecting a namespace if metric exist in new namespace', async () => {
datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics);
datasource.resources.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.api.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => {
datasource.resources.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => {
let mockMetrics =
namespace === 'n1' && region === baseProps.query.region
? metrics

View File

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

View File

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

View File

@ -22,7 +22,7 @@ const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({
sql: sql,
});
datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]);
datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]);
describe('Cloudwatch SQLGroupBy', () => {
const baseProps = {

View File

@ -4,7 +4,8 @@ import React from 'react';
import { select } from 'react-select-event';
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
import { GetDimensionKeysRequest, VariableQueryType } from '../../types';
import { GetDimensionKeysRequest } from '../../resources/types';
import { VariableQueryType } from '../../types';
import { VariableQueryEditor, Props } from './VariableQueryEditor';
@ -22,22 +23,22 @@ const defaultQuery = {
const ds = setupMockedDataSource();
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([
ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([
{ label: 'a1', value: 'a1' },
{ label: 'b1', value: 'b1' },
{ label: 'c1', value: 'c1' },
]);
ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([
ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([
{ label: 'x2', value: 'x2' },
{ label: 'y2', value: 'y2' },
{ label: 'z2', value: 'z2' },
]);
ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([
ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([
{ label: 'h3', value: 'h3' },
{ label: 'i3', value: 'i3' },
{ label: 'j3', value: 'j3' },
]);
ds.datasource.api.getDimensionKeys = jest
ds.datasource.resources.getDimensionKeys = jest
.fn()
.mockImplementation(({ namespace: region, dimensionFilters }: GetDimensionKeysRequest) => {
if (!!dimensionFilters) {
@ -55,12 +56,12 @@ ds.datasource.api.getDimensionKeys = jest
}
return Promise.resolve([{ label: 't4', value: 't4' }]);
});
ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([
ds.datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]);
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
ds.datasource.api.getEc2InstanceAttribute = jest.fn().mockReturnValue([]);
ds.datasource.resources.getEc2InstanceAttribute = jest.fn().mockReturnValue([]);
const onChange = jest.fn();
const defaultProps: Props = {
@ -142,7 +143,7 @@ describe('VariableEditor', () => {
await select(keySelect, 'v4', {
container: document.body,
});
expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith({
expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({
namespace: 'z2',
region: 'a1',
metricName: 'i3',
@ -232,8 +233,8 @@ describe('VariableEditor', () => {
})
);
expect(ds.datasource.api.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
expect(ds.datasource.resources.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' });
expect(props.onChange).toHaveBeenCalledWith({
...defaultQuery,
refId: 'CloudWatchVariableQueryEditor-VariableQuery',

View File

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

View File

@ -280,7 +280,7 @@ describe('datasource', () => {
},
]),
}).datasource;
const allMetrics = await datasource.api.getAllMetrics({ region: 'us-east-2' });
const allMetrics = await datasource.resources.getAllMetrics({ region: 'us-east-2' });
expect(allMetrics[0].metricName).toEqual('CPUUtilization');
expect(allMetrics[0].namespace).toEqual('AWS/EC2');
expect(allMetrics[1].metricName).toEqual('CPUPercentage');

View File

@ -19,7 +19,6 @@ 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 { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries';
import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards';
@ -28,6 +27,7 @@ import { MetricMathCompletionItemProvider } from './metric-math/completion/Compl
import { CloudWatchAnnotationQueryRunner } from './query-runner/CloudWatchAnnotationQueryRunner';
import { CloudWatchLogsQueryRunner } from './query-runner/CloudWatchLogsQueryRunner';
import { CloudWatchMetricsQueryRunner } from './query-runner/CloudWatchMetricsQueryRunner';
import { ResourcesAPI } from './resources/ResourcesAPI';
import {
CloudWatchAnnotationQuery,
CloudWatchJsonData,
@ -52,7 +52,7 @@ export class CloudWatchDatasource
private metricsQueryRunner: CloudWatchMetricsQueryRunner;
private annotationQueryRunner: CloudWatchAnnotationQueryRunner;
logsQueryRunner: CloudWatchLogsQueryRunner;
api: CloudWatchAPI;
resources: ResourcesAPI;
constructor(
private instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>,
@ -61,14 +61,14 @@ export class CloudWatchDatasource
) {
super(instanceSettings);
this.defaultRegion = instanceSettings.jsonData.defaultRegion;
this.api = new CloudWatchAPI(instanceSettings, templateSrv);
this.resources = new ResourcesAPI(instanceSettings, templateSrv);
this.languageProvider = new CloudWatchLanguageProvider(this);
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.api, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.api, this.templateSrv);
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, 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.variables = new CloudWatchVariableSupport(this.resources);
this.annotations = CloudWatchAnnotationSupport;
}
@ -167,7 +167,7 @@ export class CloudWatchDatasource
// public
getVariables() {
return this.api.getVariables();
return this.resources.getVariables();
}
getActualRegion(region?: string) {

View File

@ -2,7 +2,6 @@ import { renderHook } from '@testing-library/react-hooks';
import { config } from '@grafana/runtime';
import { setupMockedAPI } from './__mocks__/API';
import {
accountIdVariable,
dimensionVariable,
@ -11,6 +10,7 @@ import {
regionVariable,
setupMockedDataSource,
} from './__mocks__/CloudWatchDataSource';
import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI';
import { useAccountOptions, useDimensionKeys, useIsMonitoringAccount, useMetrics } from './hooks';
const WAIT_OPTIONS = {
@ -26,7 +26,7 @@ describe('hooks', () => {
describe('useIsMonitoringAccount', () => {
it('should interpolate variables before calling api', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const { api } = setupMockedAPI({
const { api } = setupMockedResourcesAPI({
variables: [regionVariable],
});
const isMonitoringAccountMock = jest.fn().mockResolvedValue(true);
@ -44,7 +44,7 @@ describe('hooks', () => {
variables: [regionVariable, namespaceVariable, accountIdVariable],
});
const getMetricsMock = jest.fn().mockResolvedValue([]);
datasource.api.getMetrics = getMetricsMock;
datasource.resources.getMetrics = getMetricsMock;
const { waitForNextUpdate } = renderHook(() =>
useMetrics(datasource, {
@ -70,7 +70,7 @@ describe('hooks', () => {
variables: [regionVariable, namespaceVariable, accountIdVariable, metricVariable, dimensionVariable],
});
const getDimensionKeysMock = jest.fn().mockResolvedValue([]);
datasource.api.getDimensionKeys = getDimensionKeysMock;
datasource.resources.getDimensionKeys = getDimensionKeysMock;
const { waitForNextUpdate } = renderHook(() =>
useDimensionKeys(datasource, {
@ -100,7 +100,7 @@ describe('hooks', () => {
describe('useAccountOptions', () => {
it('does not call the api if the feature toggle is off', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = false;
const { api } = setupMockedAPI({
const { api } = setupMockedResourcesAPI({
variables: [regionVariable],
});
const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]);
@ -112,7 +112,7 @@ describe('hooks', () => {
it('interpolates region variables before calling the api', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const { api } = setupMockedAPI({
const { api } = setupMockedResourcesAPI({
variables: [regionVariable],
});
const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]);
@ -125,7 +125,7 @@ describe('hooks', () => {
it('returns properly formatted account options, and template variables', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
const { api } = setupMockedAPI({
const { api } = setupMockedResourcesAPI({
variables: [regionVariable],
});
const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]);

View File

@ -4,9 +4,9 @@ import { useAsyncFn, useDeepCompareEffect } from 'react-use';
import { SelectableValue, toOption } from '@grafana/data';
import { config } from '@grafana/runtime';
import { CloudWatchAPI } from './api';
import { CloudWatchDatasource } from './datasource';
import { GetDimensionKeysRequest, GetMetricsRequest } from './types';
import { ResourcesAPI } from './resources/ResourcesAPI';
import { GetMetricsRequest, GetDimensionKeysRequest } from './resources/types';
import { appendTemplateVariables } from './utils/utils';
export const useRegions = (datasource: CloudWatchDatasource): [Array<SelectableValue<string>>, boolean] => {
@ -21,7 +21,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array<SelectableV
options: datasource.getVariables().map(toOption),
};
datasource.api
datasource.resources
.getRegions()
.then((regions: Array<SelectableValue<string>>) => setRegions([...regions, variableOptionGroup]))
.finally(() => setRegionsIsLoading(false));
@ -33,7 +33,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array<SelectableV
export const useNamespaces = (datasource: CloudWatchDatasource) => {
const [namespaces, setNamespaces] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
datasource.api.getNamespaces().then((namespaces) => {
datasource.resources.getNamespaces().then((namespaces) => {
setNamespaces(appendTemplateVariables(datasource, namespaces));
});
}, [datasource]);
@ -56,7 +56,7 @@ export const useMetrics = (datasource: CloudWatchDatasource, { region, namespace
accountId = datasource.templateSrv.replace(accountId, {});
}
useEffect(() => {
datasource.api.getMetrics({ namespace, region, accountId }).then((result: Array<SelectableValue<string>>) => {
datasource.resources.getMetrics({ namespace, region, accountId }).then((result: Array<SelectableValue<string>>) => {
setMetrics(appendTemplateVariables(datasource, result));
});
}, [datasource, region, namespace, accountId]);
@ -87,12 +87,12 @@ export const useDimensionKeys = (
}
if (dimensionFilters) {
dimensionFilters = datasource.api.convertDimensionFormat(dimensionFilters, {});
dimensionFilters = datasource.resources.convertDimensionFormat(dimensionFilters, {});
}
// doing deep comparison to avoid making new api calls to list metrics unless dimension filter object props changes
useDeepCompareEffect(() => {
datasource.api
datasource.resources
.getDimensionKeys({ namespace, region, metricName, accountId, dimensionFilters })
.then((result: Array<SelectableValue<string>>) => {
setDimensionKeys(appendTemplateVariables(datasource, result));
@ -102,37 +102,37 @@ export const useDimensionKeys = (
return dimensionKeys;
};
export const useIsMonitoringAccount = (api: CloudWatchAPI, region: string) => {
export const useIsMonitoringAccount = (resources: ResourcesAPI, region: string) => {
const [isMonitoringAccount, setIsMonitoringAccount] = useState(false);
// we call this before the use effect to ensure dependency array below
// receives the interpolated value so that the effect is triggered when a variable is changed
if (region) {
region = api.templateSrv.replace(region, {});
region = resources.templateSrv.replace(region, {});
}
useEffect(() => {
if (config.featureToggles.cloudWatchCrossAccountQuerying) {
api.isMonitoringAccount(region).then((result) => setIsMonitoringAccount(result));
resources.isMonitoringAccount(region).then((result) => setIsMonitoringAccount(result));
}
}, [region, api]);
}, [region, resources]);
return isMonitoringAccount;
};
export const useAccountOptions = (
api: Pick<CloudWatchAPI, 'getAccounts' | 'templateSrv' | 'getVariables'> | undefined,
resources: Pick<ResourcesAPI, 'getAccounts' | 'templateSrv' | 'getVariables'> | undefined,
region: string
) => {
// we call this before the use effect to ensure dependency array below
// receives the interpolated value so that the effect is triggered when a variable is changed
if (region) {
region = api?.templateSrv.replace(region, {}) ?? '';
region = resources?.templateSrv.replace(region, {}) ?? '';
}
const fetchAccountOptions = async () => {
if (!config.featureToggles.cloudWatchCrossAccountQuerying) {
return Promise.resolve([]);
}
const accounts = (await api?.getAccounts({ region })) ?? [];
const accounts = (await resources?.getAccounts({ region })) ?? [];
if (accounts.length === 0) {
return [];
}
@ -143,7 +143,7 @@ export const useAccountOptions = (
description: a.id,
}));
const variableOptions = api?.getVariables().map(toOption) || [];
const variableOptions = resources?.getVariables().map(toOption) || [];
const variableOptionGroup: SelectableValue<string> = {
label: 'Template Variables',
@ -153,11 +153,11 @@ export const useAccountOptions = (
return [...options, variableOptionGroup];
};
const [state, doFetch] = useAsyncFn(fetchAccountOptions, [api, region]);
const [state, doFetch] = useAsyncFn(fetchAccountOptions, [resources, region]);
useEffect(() => {
doFetch();
}, [api, region, doFetch]);
}, [resources, region, doFetch]);
return state;
};

View File

@ -5,6 +5,7 @@ import { TypeaheadOutput } from '@grafana/ui';
import { CloudWatchDatasource } from './datasource';
import { CloudWatchLanguageProvider } from './language_provider';
import { ResourceResponse } from './resources/types';
import {
AGGREGATION_FUNCTIONS_STATS,
BOOLEAN_FUNCTIONS,
@ -15,7 +16,7 @@ import {
STRING_FUNCTIONS,
FIELD_AND_FILTER_FUNCTIONS,
} from './syntax';
import { LogGroupField, ResourceResponse } from './types';
import { LogGroupField } from './types';
const fields = ['field1', '@message'];
@ -109,7 +110,7 @@ async function runSuggestionTest(query: string, expectedItems: string[][]) {
function makeDatasource(): CloudWatchDatasource {
return {
api: {
resources: {
getLogGroupFields(): Promise<Array<ResourceResponse<LogGroupField>>> {
return Promise.resolve([{ value: { name: 'field1' } }, { value: { name: '@message' } }]);
},

View File

@ -136,7 +136,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
);
const results = await Promise.all(
interpolatedLogGroups.map((logGroupName) =>
this.datasource.api
this.datasource.resources
.getLogGroupFields({ logGroupName, region })
.then((fields) => fields.filter((f) => f).map((f) => f.value.name ?? ''))
)

View File

@ -4,7 +4,7 @@ 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 { CloudWatchAPI } from '../../api';
import { ResourcesAPI } from '../../resources/ResourcesAPI';
import cloudWatchMetricMathLanguageDefinition from '../definition';
import {
METRIC_MATH_FNS,
@ -20,7 +20,7 @@ const getSuggestions = async (value: string, position: monacoTypes.IPosition) =>
const setup = new MetricMathCompletionItemProvider(
{
getActualRegion: () => 'us-east-2',
} as CloudWatchAPI,
} as ResourcesAPI,
setupMockedTemplateService([])
);
const monaco = MonacoMock as Monaco;

View File

@ -1,11 +1,11 @@
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { CloudWatchAPI } from '../../api';
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
import { LinkedToken } from '../../monarch/LinkedToken';
import { TRIGGER_SUGGEST } from '../../monarch/commands';
import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types';
import { ResourcesAPI } from '../../resources/ResourcesAPI';
import {
METRIC_MATH_FNS,
METRIC_MATH_KEYWORDS,
@ -21,8 +21,8 @@ import { MetricMathTokenTypes } from './types';
type CompletionItem = monacoTypes.languages.CompletionItem;
export class MetricMathCompletionItemProvider extends CompletionItemProvider {
constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
super(api, templateSrv);
constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
super(resources, templateSrv);
this.getStatementPosition = getStatementPosition;
this.getSuggestionKinds = getSuggestionKinds;
this.tokenTypes = MetricMathTokenTypes;

View File

@ -1,7 +1,7 @@
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
import { CloudWatchAPI } from '../api';
import { ResourcesAPI } from '../resources/ResourcesAPI';
import { LinkedToken } from './LinkedToken';
import { linkedTokenBuilder } from './linkedTokenBuilder';
@ -18,12 +18,12 @@ CompletionItemProvider is an extendable class which needs to implement :
- getSuggestions
*/
export class CompletionItemProvider implements Completeable {
api: CloudWatchAPI;
resources: ResourcesAPI;
templateSrv: TemplateSrv;
tokenTypes: TokenTypes;
constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
this.api = api;
constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
this.resources = resources;
this.templateSrv = templateSrv;
this.templateSrv = templateSrv;

View File

@ -1,9 +1,9 @@
import { setupMockedAPI } from './__mocks__/API';
import { setupMockedResourcesAPI } from '../__mocks__/ResourcesAPI';
describe('api', () => {
describe('ResourcesAPI', () => {
describe('describeLogGroup', () => {
it('replaces region correctly in the query', async () => {
const { api, resourceRequestMock } = setupMockedAPI();
const { api, resourceRequestMock } = setupMockedResourcesAPI();
await api.getLogGroups({ region: 'default' });
expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1');
@ -30,7 +30,7 @@ describe('api', () => {
},
];
const { api } = setupMockedAPI({ response });
const { api } = setupMockedResourcesAPI({ response });
const expectedLogGroups = [
{
text: '/aws/containerinsights/dev303-workshop/application',
@ -58,7 +58,7 @@ describe('api', () => {
describe('memoization', () => {
it('should not initiate new api request in case a previous request had same args', async () => {
const getMock = jest.fn();
const { api, resourceRequestMock } = setupMockedAPI({ getMock });
const { api, resourceRequestMock } = setupMockedResourcesAPI({ getMock });
resourceRequestMock.mockResolvedValue([]);
await Promise.all([
api.getMetrics({ namespace: 'AWS/EC2', region: 'us-east-1' }),
@ -87,7 +87,7 @@ describe('api', () => {
},
},
]);
const { api } = setupMockedAPI({ getMock });
const { api } = setupMockedResourcesAPI({ getMock });
const allMetrics = await api.getAllMetrics({ region: 'us-east-2' });
expect(allMetrics).toEqual([
{ metricName: 'CPUUtilization', namespace: 'AWS/EC2' },
@ -110,7 +110,7 @@ describe('api', () => {
},
},
]);
const { api } = setupMockedAPI({ getMock });
const { api } = setupMockedResourcesAPI({ getMock });
const allMetrics = await api.getMetrics({ region: 'us-east-2', namespace: 'AWS/EC2' });
expect(allMetrics).toEqual([
{ label: 'CPUUtilization', value: 'CPUUtilization' },

View File

@ -4,28 +4,24 @@ import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CloudWatchRequest } from './query-runner/CloudWatchRequest';
import { CloudWatchRequest } from '../query-runner/CloudWatchRequest';
import { CloudWatchJsonData, LogGroupField, MultiFilters } from '../types';
import {
CloudWatchJsonData,
ResourceRequest,
Account,
ResourceResponse,
DescribeLogGroupsRequest,
LogGroupResponse,
GetLogGroupFieldsRequest,
GetMetricsRequest,
GetDimensionKeysRequest,
GetDimensionValuesRequest,
GetMetricsRequest,
LogGroupResponse,
MetricResponse,
MultiFilters,
Account,
ResourceRequest,
ResourceResponse,
GetLogGroupFieldsRequest,
LogGroupField,
SelectableResourceValue,
} from './types';
export interface SelectableResourceValue extends SelectableValue<string> {
text: string;
}
export class CloudWatchAPI extends CloudWatchRequest {
export class ResourcesAPI extends CloudWatchRequest {
private memoizedGetRequest;
constructor(instanceSettings: DataSourceInstanceSettings<CloudWatchJsonData>, templateSrv: TemplateSrv) {

View File

@ -0,0 +1,63 @@
import { SelectableValue } from '@grafana/data';
import { Dimensions } from '../types';
export interface ResourceResponse<T> {
accountId?: string;
value: T;
}
export interface ResourceRequest {
region: string;
accountId?: string;
}
export interface GetLogGroupFieldsRequest extends ResourceRequest {
arn?: string;
logGroupName: string;
}
export interface GetDimensionKeysRequest extends ResourceRequest {
metricName?: string;
namespace?: string;
dimensionFilters?: Dimensions;
}
export interface GetDimensionValuesRequest extends ResourceRequest {
dimensionKey: string;
namespace: string;
metricName?: string;
dimensionFilters?: Dimensions;
}
export interface GetMetricsRequest extends ResourceRequest {
namespace?: string;
}
export interface DescribeLogGroupsRequest extends ResourceRequest {
logGroupNamePrefix?: string;
logGroupPattern?: string;
limit?: number;
listAllLogGroups?: boolean;
}
export interface Account {
arn: string;
id: string;
label: string;
isMonitoringAccount: boolean;
}
export interface LogGroupResponse {
arn: string;
name: string;
}
export interface MetricResponse {
name: string;
namespace: string;
}
export interface SelectableResourceValue extends SelectableValue<string> {
text: string;
}

View File

@ -336,73 +336,6 @@ export interface LegacyAnnotationQuery extends MetricStat, DataQuery {
type: string;
}
export interface MetricResponse {
name: string;
namespace: string;
}
export interface ResourceRequest {
region: string;
accountId?: string;
}
export interface GetLogGroupFieldsRequest extends ResourceRequest {
/**
* The log group identifier
*/
arn?: string;
/**
* The name of the log group to search.
*/
logGroupName: string;
}
export interface GetDimensionKeysRequest extends ResourceRequest {
metricName?: string;
namespace?: string;
dimensionFilters?: Dimensions;
}
export interface GetDimensionValuesRequest extends ResourceRequest {
dimensionKey: string;
namespace: string;
metricName?: string;
dimensionFilters?: Dimensions;
}
export interface GetMetricsRequest extends ResourceRequest {
namespace?: string;
}
export interface DescribeLogGroupsRequest extends ResourceRequest {
logGroupNamePrefix?: string;
logGroupPattern?: string;
limit?: number;
listAllLogGroups?: boolean;
}
export interface Account {
arn: string;
id: string;
label: string;
isMonitoringAccount: boolean;
}
export interface LogGroupResponse {
arn: string;
name: string;
}
export interface MetricResponse {
name: string;
namespace: string;
}
export interface ResourceResponse<T> {
accountId?: string;
value: T;
}
export interface LogGroup {
arn: string;
name: string;

View File

@ -1,5 +1,5 @@
import { setupMockedAPI } from './__mocks__/API';
import { dimensionVariable, labelsVariable, setupMockedDataSource } from './__mocks__/CloudWatchDataSource';
import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI';
import { VariableQuery, VariableQueryType } from './types';
import { CloudWatchVariableSupport } from './variables';
@ -16,20 +16,20 @@ const defaultQuery: VariableQuery = {
};
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.api.getLogGroups = jest
mock.datasource.resources.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]);
mock.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
mock.datasource.resources.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
mock.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
mock.datasource.resources.getLogGroups = jest
.fn()
.mockResolvedValue([{ value: { arn: 'a', name: 'a' } }, { value: { arn: 'b', name: 'b' } }]);
mock.datasource.api.getAccounts = jest.fn().mockResolvedValue([]);
mock.datasource.resources.getAccounts = jest.fn().mockResolvedValue([]);
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(mock.datasource.api);
const variables = new CloudWatchVariableSupport(mock.datasource.resources);
describe('variables', () => {
it('should run regions', async () => {
@ -54,7 +54,7 @@ describe('variables', () => {
describe('accounts', () => {
it('should run accounts', async () => {
const { api } = setupMockedAPI();
const { api } = setupMockedResourcesAPI();
const getAccountMock = jest.fn().mockResolvedValue([]);
api.getAccounts = getAccountMock;
const variables = new CloudWatchVariableSupport(api);
@ -63,7 +63,7 @@ describe('variables', () => {
});
it('should map accounts to metric find value and insert "all" option', async () => {
const { api } = setupMockedAPI();
const { api } = setupMockedResourcesAPI();
api.getAccounts = jest.fn().mockResolvedValue([{ id: '123', label: 'Account1' }]);
const variables = new CloudWatchVariableSupport(api);
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.Accounts });
@ -83,7 +83,7 @@ describe('variables', () => {
dimensionFilters: { a: 'b' },
};
beforeEach(() => {
mock.datasource.api.getDimensionValues = getDimensionValues;
mock.datasource.resources.getDimensionValues = getDimensionValues;
getDimensionValues.mockClear();
});
@ -113,7 +113,7 @@ describe('variables', () => {
describe('EBS volume ids', () => {
beforeEach(() => {
mock.datasource.api.getEbsVolumeIds = getEbsVolumeIds;
mock.datasource.resources.getEbsVolumeIds = getEbsVolumeIds;
getEbsVolumeIds.mockClear();
});
@ -142,7 +142,7 @@ describe('variables', () => {
ec2Filters: { a: ['b'] },
};
beforeEach(() => {
mock.datasource.api.getEc2InstanceAttribute = getEc2InstanceAttribute;
mock.datasource.resources.getEc2InstanceAttribute = getEc2InstanceAttribute;
getEc2InstanceAttribute.mockClear();
});
@ -167,7 +167,7 @@ describe('variables', () => {
tags: { a: ['b'] },
};
beforeEach(() => {
mock.datasource.api.getResourceARNs = getResourceARNs;
mock.datasource.resources.getResourceARNs = getResourceARNs;
getResourceARNs.mockClear();
});

View File

@ -9,16 +9,16 @@ import {
SelectableValue,
} from '@grafana/data';
import { CloudWatchAPI } from './api';
import { ALL_ACCOUNTS_OPTION } from './components/Account';
import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
import { CloudWatchDatasource } from './datasource';
import { migrateVariableQuery } from './migrations/variableQueryMigrations';
import { ResourcesAPI } from './resources/ResourcesAPI';
import { standardStatistics } from './standardStatistics';
import { VariableQuery, VariableQueryType } from './types';
export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchDatasource, VariableQuery> {
constructor(private readonly api: CloudWatchAPI) {
constructor(private readonly resources: ResourcesAPI) {
super();
this.query = this.query.bind(this);
}
@ -62,7 +62,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
}
async handleLogGroupsQuery({ region, logGroupPrefix }: VariableQuery) {
return this.api
return this.resources
.getLogGroups({
region,
logGroupNamePrefix: logGroupPrefix,
@ -80,26 +80,30 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
}
async handleRegionsQuery() {
return this.api.getRegions().then((regions) => regions.map(selectableValueToMetricFindOption));
return this.resources.getRegions().then((regions) => regions.map(selectableValueToMetricFindOption));
}
async handleNamespacesQuery() {
return this.api.getNamespaces().then((namespaces) => namespaces.map(selectableValueToMetricFindOption));
return this.resources.getNamespaces().then((namespaces) => namespaces.map(selectableValueToMetricFindOption));
}
async handleMetricsQuery({ namespace, region }: VariableQuery) {
return this.api.getMetrics({ namespace, region }).then((metrics) => metrics.map(selectableValueToMetricFindOption));
return this.resources
.getMetrics({ namespace, region })
.then((metrics) => metrics.map(selectableValueToMetricFindOption));
}
async handleDimensionKeysQuery({ namespace, region }: VariableQuery) {
return this.api.getDimensionKeys({ namespace, region }).then((keys) => keys.map(selectableValueToMetricFindOption));
return this.resources
.getDimensionKeys({ namespace, region })
.then((keys) => keys.map(selectableValueToMetricFindOption));
}
async handleDimensionValuesQuery({ namespace, region, dimensionKey, metricName, dimensionFilters }: VariableQuery) {
if (!dimensionKey || !metricName) {
return [];
}
return this.api
return this.resources
.getDimensionValues({
region,
namespace,
@ -114,14 +118,14 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
if (!instanceID) {
return [];
}
return this.api.getEbsVolumeIds(region, instanceID).then((ids) => ids.map(selectableValueToMetricFindOption));
return this.resources.getEbsVolumeIds(region, instanceID).then((ids) => ids.map(selectableValueToMetricFindOption));
}
async handleEc2InstanceAttributeQuery({ region, attributeName, ec2Filters }: VariableQuery) {
if (!attributeName) {
return [];
}
return this.api
return this.resources
.getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {})
.then((values) => values.map(selectableValueToMetricFindOption));
}
@ -130,7 +134,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
if (!resourceType) {
return [];
}
const keys = await this.api.getResourceARNs(region, resourceType, tags ?? {});
const keys = await this.resources.getResourceARNs(region, resourceType, tags ?? {});
return keys.map(selectableValueToMetricFindOption);
}
@ -144,7 +148,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
allMetricFindValue: MetricFindValue = { text: 'All', value: ALL_ACCOUNTS_OPTION.value, expandable: true };
async handleAccountsQuery({ region }: VariableQuery) {
return this.api.getAccounts({ region }).then((accounts) => {
return this.resources.getAccounts({ region }).then((accounts) => {
const metricFindOptions = accounts.map((account) => ({
text: account.label,
value: account.id,