CloudWatch: Fix logs insights deeplink (#59906)

* CloudWatch: Add arns to log insights button

* start test

* convert to function component

* add tests

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
This commit is contained in:
Shirley 2022-12-13 14:50:39 +01:00 committed by GitHub
parent 239888f229
commit db145774d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 154 additions and 62 deletions

View File

@ -8,7 +8,7 @@ import { CloudWatchAnnotationQueryRunner } from '../query-runner/CloudWatchAnnot
import { CloudWatchQuery } from '../types';
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
import { timeRange } from './timeRange';
import { TimeRangeMock } from './timeRange';
export function setupMockedAnnotationQueryRunner({ variables }: { variables?: CustomVariableModel[] }) {
let templateService = new TemplateSrv();
@ -25,7 +25,7 @@ export function setupMockedAnnotationQueryRunner({ variables }: { variables?: Cu
});
const request: DataQueryRequest<CloudWatchQuery> = {
range: timeRange,
range: TimeRangeMock,
rangeRaw: { from: '1483228800', to: '1483232400' },
targets: [],
requestId: '',
@ -37,5 +37,5 @@ export function setupMockedAnnotationQueryRunner({ variables }: { variables?: Cu
startTime: 0,
};
return { runner, fetchMock, templateService, request, timeRange };
return { runner, fetchMock, templateService, request, timeRange: TimeRangeMock };
}

View File

@ -8,7 +8,7 @@ import { CloudWatchMetricsQueryRunner } from '../query-runner/CloudWatchMetricsQ
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource';
import { timeRange } from './timeRange';
import { TimeRangeMock } from './timeRange';
export function setupMockedMetricsQueryRunner({
data = {
@ -44,7 +44,7 @@ export function setupMockedMetricsQueryRunner({
});
const request: DataQueryRequest<CloudWatchQuery> = {
range: timeRange,
range: TimeRangeMock,
rangeRaw: { from: '1483228800', to: '1483232400' },
targets: [],
requestId: '',
@ -56,5 +56,5 @@ export function setupMockedMetricsQueryRunner({
startTime: 0,
};
return { runner, fetchMock, templateService, instanceSettings, request, timeRange };
return { runner, fetchMock, templateService, instanceSettings, request, timeRange: TimeRangeMock };
}

View File

@ -0,0 +1,18 @@
import { DataQueryRequest } from '@grafana/data';
import { CloudWatchQuery } from '../types';
import { TimeRangeMock } from './timeRange';
export const RequestMock: DataQueryRequest<CloudWatchQuery> = {
range: TimeRangeMock,
rangeRaw: { from: TimeRangeMock.from, to: TimeRangeMock.to },
targets: [],
requestId: '',
interval: '',
intervalMs: 0,
scopedVars: {},
timezone: '',
app: '',
startTime: 0,
};

View File

@ -91,4 +91,5 @@ export const validLogsQuery: CloudWatchLogsQuery = {
id: '',
region: 'us-east-2',
refId: 'A',
expression: `fields @timestamp, @message | sort @timestamp desc | limit 25`,
};

View File

@ -3,4 +3,4 @@ import { dateTime, TimeRange } from '@grafana/data';
const start = 1483196400 * 1000;
const from = dateTime(start);
const to = dateTime(start + 3600 * 1000);
export const timeRange: TimeRange = { from, to, raw: { from, to } };
export const TimeRangeMock: TimeRange = { from, to, raw: { from, to } };

View File

@ -0,0 +1,87 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { LoadingState } from '@grafana/data';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
import { RequestMock } from '../__mocks__/Request';
import { validLogsQuery } from '../__mocks__/queries';
import { CloudWatchLogsQuery } from '../types';
import { CloudWatchLink } from './CloudWatchLink';
describe('CloudWatchLink', () => {
it('generates a link with log group names', async () => {
const ds = setupMockedDataSource();
const { rerender } = render(
<CloudWatchLink query={validLogsQuery} datasource={ds.datasource} panelData={undefined} />
);
await waitFor(() => {
expect(screen.getByText('CloudWatch Logs Insights').closest('a')).toHaveAttribute('href', '');
});
await act(async () => {
rerender(
<CloudWatchLink
query={validLogsQuery}
datasource={ds.datasource}
panelData={{
timeRange: RequestMock.range,
request: RequestMock,
state: LoadingState.Done,
series: [],
}}
/>
);
});
await waitFor(() => {
expect(screen.getByText('CloudWatch Logs Insights').closest('a')).toHaveAttribute(
'href',
"https://us-east-2.console.aws.amazon.com/cloudwatch/home?region=us-east-2#logs-insights:queryDetail=~(end~'2016-12-31T16*3a00*3a00.000Z~start~'2016-12-31T15*3a00*3a00.000Z~timeType~'ABSOLUTE~tz~'UTC~editorString~'fields*20*40timestamp*2c*20*40message*20*7c*20sort*20*40timestamp*20desc*20*7c*20limit*2025~isLiveTail~false~source~(~'group-A~'group-B))"
);
});
});
it('generates a link with log group names', async () => {
const ds = setupMockedDataSource();
const query: CloudWatchLogsQuery = {
...validLogsQuery,
logGroupNames: undefined,
logGroups: [
{ value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test1', text: '/aws/lambda/test1' },
{ value: 'arn:aws:logs:us-east-1:111111111111:log-group:/aws/lambda/test2', text: '/aws/lambda/test2' },
],
};
const { rerender } = render(<CloudWatchLink query={query} datasource={ds.datasource} panelData={undefined} />);
await waitFor(() => {
expect(screen.getByText('CloudWatch Logs Insights').closest('a')).toHaveAttribute('href', '');
});
await act(async () => {
rerender(
<CloudWatchLink
query={query}
datasource={ds.datasource}
panelData={{
timeRange: RequestMock.range,
request: RequestMock,
state: LoadingState.Done,
series: [],
}}
/>
);
});
await waitFor(() => {
expect(screen.getByText('CloudWatch Logs Insights').closest('a')).toHaveAttribute(
'href',
"https://us-east-2.console.aws.amazon.com/cloudwatch/home?region=us-east-2#logs-insights:queryDetail=~(end~'2016-12-31T16*3a00*3a00.000Z~start~'2016-12-31T15*3a00*3a00.000Z~timeType~'ABSOLUTE~tz~'UTC~editorString~'fields*20*40timestamp*2c*20*40message*20*7c*20sort*20*40timestamp*20desc*20*7c*20limit*2025~isLiveTail~false~source~(~'arn*3aaws*3alogs*3aus-east-1*3a111111111111*3alog-group*3a*2faws*2flambda*2ftest1~'arn*3aaws*3alogs*3aus-east-1*3a111111111111*3alog-group*3a*2faws*2flambda*2ftest2))"
);
});
});
});

View File

@ -1,9 +1,10 @@
import React, { Component } from 'react';
import React, { useEffect, useState } from 'react';
import { usePrevious } from 'react-use';
import { PanelData } from '@grafana/data';
import { Icon } from '@grafana/ui';
import { encodeUrl, AwsUrl } from '../aws_url';
import { AwsUrl, encodeUrl } from '../aws_url';
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchLogsQuery } from '../types';
@ -13,54 +14,39 @@ interface Props {
datasource: CloudWatchDatasource;
}
interface State {
href: string;
}
export function CloudWatchLink({ panelData, query, datasource }: Props) {
const [href, setHref] = useState('');
const prevPanelData = usePrevious<PanelData | undefined>(panelData);
export default class CloudWatchLink extends Component<Props, State> {
state: State = { href: '' };
useEffect(() => {
if (prevPanelData !== panelData && panelData?.request?.range) {
const arns = (query.logGroups ?? [])
.filter((group) => group?.value)
.map((group) => (group.value ?? '').replace(/:\*$/, '')); // remove `:*` from end of arn
const logGroupNames = query.logGroupNames;
let sources = arns?.length ? arns : logGroupNames;
async componentDidUpdate(prevProps: Props) {
const { panelData: panelDataNew } = this.props;
const { panelData: panelDataOld } = prevProps;
const range = panelData?.request?.range;
const start = range.from.toISOString();
const end = range.to.toISOString();
if (panelDataOld !== panelDataNew && panelDataNew?.request) {
const href = this.getExternalLink();
this.setState({ href });
const urlProps: AwsUrl = {
end,
start,
timeType: 'ABSOLUTE',
tz: 'UTC',
editorString: query.expression ?? '',
isLiveTail: false,
source: sources ?? [],
};
setHref(encodeUrl(urlProps, datasource.api.getActualRegion(query.region)));
}
}
}, [panelData, prevPanelData, datasource, query]);
getExternalLink(): string {
const { query, panelData, datasource } = this.props;
const range = panelData?.request?.range;
if (!range) {
return '';
}
const start = range.from.toISOString();
const end = range.to.toISOString();
const urlProps: AwsUrl = {
end,
start,
timeType: 'ABSOLUTE',
tz: 'UTC',
editorString: query.expression ?? '',
isLiveTail: false,
source: query.logGroupNames ?? [],
};
return encodeUrl(urlProps, datasource.api.getActualRegion(query.region));
}
render() {
const { href } = this.state;
return (
<a href={href} target="_blank" rel="noopener noreferrer">
<Icon name="share-alt" /> CloudWatch Logs Insights
</a>
);
}
return (
<a href={href} target="_blank" rel="noopener noreferrer">
<Icon name="share-alt" /> CloudWatch Logs Insights
</a>
);
}

View File

@ -9,7 +9,7 @@ import { InlineFormLabel } from '@grafana/ui';
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types';
import CloudWatchLink from './CloudWatchLink';
import { CloudWatchLink } from './CloudWatchLink';
import CloudWatchLogsQueryField from './LogsQueryField';
type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> & {

View File

@ -11,7 +11,7 @@ import {
} from './__mocks__/CloudWatchDataSource';
import { setupForLogs } from './__mocks__/logsTestContext';
import { validLogsQuery, validMetricSearchBuilderQuery } from './__mocks__/queries';
import { timeRange } from './__mocks__/timeRange';
import { TimeRangeMock } from './__mocks__/timeRange';
import { CloudWatchLogsQuery, CloudWatchMetricsQuery, CloudWatchQuery } from './types';
describe('datasource', () => {
@ -43,7 +43,7 @@ describe('datasource', () => {
requestId: '',
interval: '',
intervalMs: 0,
range: timeRange,
range: TimeRangeMock,
scopedVars: {},
timezone: '',
app: '',
@ -83,7 +83,7 @@ describe('datasource', () => {
requestId: '',
interval: '',
intervalMs: 0,
range: timeRange,
range: TimeRangeMock,
scopedVars: {},
timezone: '',
app: '',
@ -106,7 +106,7 @@ describe('datasource', () => {
requestId: '',
interval: '',
intervalMs: 0,
range: timeRange,
range: TimeRangeMock,
scopedVars: {},
timezone: '',
app: '',
@ -152,7 +152,7 @@ describe('datasource', () => {
requestId: '',
interval: '',
intervalMs: 0,
range: timeRange,
range: TimeRangeMock,
scopedVars: {},
timezone: '',
app: '',
@ -188,7 +188,7 @@ describe('datasource', () => {
requestId: '',
interval: '',
intervalMs: 0,
range: timeRange,
range: TimeRangeMock,
scopedVars: {},
timezone: '',
app: '',
@ -232,7 +232,7 @@ describe('datasource', () => {
requestId: '',
interval: '',
intervalMs: 0,
range: timeRange,
range: TimeRangeMock,
scopedVars: {},
timezone: '',
app: '',