mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 09:26:43 -06:00
CloudWatch: Factor LogGroupSelector into a separate component (#50829)
This commit is contained in:
parent
8ba8e1df83
commit
497310a9cb
@ -101,9 +101,6 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:227258837": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:1501504663": [
|
||||
[2, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [
|
||||
[0, 26, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
@ -3886,27 +3883,16 @@ exports[`no type assertions`] = {
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx:1350761016": [
|
||||
[102, 22, 34, "Do not use any type assertions.", "1692406735"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx:1998970825": [
|
||||
[53, 33, 28, "Do not use any type assertions.", "1081347704"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx:3592370976": [
|
||||
[52, 33, 28, "Do not use any type assertions.", "1081347704"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:1501504663": [
|
||||
[18, 70, 34, "Do not use any type assertions.", "1535357135"],
|
||||
[30, 15, 9, "Do not use any type assertions.", "3692209159"],
|
||||
[51, 10, 726, "Do not use any type assertions.", "1108484993"],
|
||||
[76, 15, 9, "Do not use any type assertions.", "3692209159"],
|
||||
[88, 11, 46, "Do not use any type assertions.", "4131145701"],
|
||||
[185, 10, 806, "Do not use any type assertions.", "1219250420"],
|
||||
[209, 15, 9, "Do not use any type assertions.", "3692209159"],
|
||||
[224, 31, 69, "Do not use any type assertions.", "323157160"],
|
||||
[232, 27, 69, "Do not use any type assertions.", "323157160"],
|
||||
[252, 15, 9, "Do not use any type assertions.", "3692209159"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:4231019726": [
|
||||
[12, 70, 34, "Do not use any type assertions.", "1535357135"],
|
||||
[24, 15, 9, "Do not use any type assertions.", "3692209159"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:1141751632": [
|
||||
[70, 7, 39, "Do not use any type assertions.", "1215991248"],
|
||||
[92, 14, 29, "Do not use any type assertions.", "115491437"],
|
||||
[207, 10, 28, "Do not use any type assertions.", "1081347704"],
|
||||
[250, 39, 57, "Do not use any type assertions.", "1866145996"],
|
||||
[343, 22, 28, "Do not use any type assertions.", "1081347704"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:719311531": [
|
||||
[62, 14, 29, "Do not use any type assertions.", "115491437"],
|
||||
[97, 39, 57, "Do not use any type assertions.", "1866145996"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx:2002367401": [
|
||||
[23, 27, 101, "Do not use any type assertions.", "1879489671"],
|
||||
@ -3919,9 +3905,9 @@ exports[`no type assertions`] = {
|
||||
[80, 20, 443, "Do not use any type assertions.", "2067842726"],
|
||||
[108, 20, 472, "Do not use any type assertions.", "4092163612"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx:869258856": [
|
||||
[41, 15, 78, "Do not use any type assertions.", "2265747900"],
|
||||
[52, 13, 63, "Do not use any type assertions.", "1673299780"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx:4263462895": [
|
||||
[34, 15, 78, "Do not use any type assertions.", "2265747900"],
|
||||
[42, 13, 63, "Do not use any type assertions.", "1673299780"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/datasource.test.ts:3643150425": [
|
||||
[30, 42, 53, "Do not use any type assertions.", "1293523870"],
|
||||
@ -10693,20 +10679,13 @@ exports[`no explicit any`] = {
|
||||
"public/app/plugins/datasource/cloudwatch/__mocks__/monarch/Monaco.ts:1596319131": [
|
||||
[54, 19, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:1501504663": [
|
||||
[18, 39, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[18, 47, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[30, 21, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[66, 38, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[74, 15, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[76, 21, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[207, 15, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[209, 21, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[252, 21, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:4231019726": [
|
||||
[12, 39, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[12, 47, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[24, 21, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:1141751632": [
|
||||
[82, 75, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[265, 67, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:719311531": [
|
||||
[52, 75, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/Alias.tsx:4073366699": [
|
||||
[6, 20, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
|
@ -0,0 +1,147 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import lodash from 'lodash'; // eslint-disable-line lodash/import-scope
|
||||
import React from 'react';
|
||||
import { openMenu, select } from 'react-select-event';
|
||||
|
||||
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
||||
import { DescribeLogGroupsRequest } from '../types';
|
||||
|
||||
import { LogGroupSelector, LogGroupSelectorProps } from './LogGroupSelector';
|
||||
|
||||
const ds = setupMockedDataSource();
|
||||
|
||||
describe('LogGroupSelector', () => {
|
||||
const onChange = jest.fn();
|
||||
const defaultProps: LogGroupSelectorProps = {
|
||||
region: 'region1',
|
||||
datasource: ds.datasource,
|
||||
selectedLogGroups: [],
|
||||
onChange,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('updates upstream query log groups on region change', async () => {
|
||||
ds.datasource.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
||||
if (params.region === 'region1') {
|
||||
return Promise.resolve(['log_group_1']);
|
||||
} else {
|
||||
return Promise.resolve(['log_group_2']);
|
||||
}
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedLogGroups: ['log_group_1'],
|
||||
};
|
||||
|
||||
const { rerender } = render(<LogGroupSelector {...props} />);
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
|
||||
expect(onChange).toHaveBeenLastCalledWith(['log_group_1']);
|
||||
expect(await screen.findByText('log_group_1')).toBeInTheDocument();
|
||||
|
||||
act(() => rerender(<LogGroupSelector {...props} region="region2" />));
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
|
||||
expect(onChange).toHaveBeenLastCalledWith([]);
|
||||
});
|
||||
|
||||
it('does not update upstream query log groups if saved is false', async () => {
|
||||
ds.datasource.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
||||
if (params.region === 'region1') {
|
||||
return Promise.resolve(['log_group_1']);
|
||||
} else {
|
||||
return Promise.resolve(['log_group_2']);
|
||||
}
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedLogGroups: ['log_group_1'],
|
||||
};
|
||||
|
||||
const { rerender } = render(<LogGroupSelector {...props} />);
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
|
||||
expect(onChange).toHaveBeenLastCalledWith(['log_group_1']);
|
||||
expect(await screen.findByText('log_group_1')).toBeInTheDocument();
|
||||
|
||||
act(() => rerender(<LogGroupSelector {...props} region="region2" saved={false} />));
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
|
||||
expect(onChange).toHaveBeenLastCalledWith(['log_group_1']);
|
||||
});
|
||||
|
||||
it('should merge results of remote log groups search with existing results', async () => {
|
||||
lodash.debounce = jest.fn().mockImplementation((fn) => fn);
|
||||
const allLogGroups = [
|
||||
'AmazingGroup',
|
||||
'AmazingGroup2',
|
||||
'AmazingGroup3',
|
||||
'BeautifulGroup',
|
||||
'BeautifulGroup2',
|
||||
'BeautifulGroup3',
|
||||
'CrazyGroup',
|
||||
'CrazyGroup2',
|
||||
'CrazyGroup3',
|
||||
'DeliciousGroup',
|
||||
'DeliciousGroup2',
|
||||
'DeliciousGroup3',
|
||||
|
||||
'VelvetGroup',
|
||||
'VelvetGroup2',
|
||||
'VelvetGroup3',
|
||||
'WaterGroup',
|
||||
'WaterGroup2',
|
||||
'WaterGroup3',
|
||||
];
|
||||
const testLimit = 10;
|
||||
|
||||
ds.datasource.describeLogGroups = jest.fn().mockImplementation(async (params: DescribeLogGroupsRequest) => {
|
||||
const theLogGroups = allLogGroups
|
||||
.filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
|
||||
.slice(0, Math.max(params.limit ?? testLimit, testLimit));
|
||||
return Promise.resolve(theLogGroups);
|
||||
});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
};
|
||||
render(<LogGroupSelector {...props} />);
|
||||
const multiselect = await screen.findByLabelText('Log Groups');
|
||||
|
||||
// Adds the 3 Water groups to the 10 loaded in initially
|
||||
await userEvent.type(multiselect, 'Water');
|
||||
// The 3 Water groups + the create option
|
||||
expect(screen.getAllByLabelText('Select option').length).toBe(4);
|
||||
await userEvent.clear(multiselect);
|
||||
expect(screen.getAllByLabelText('Select option').length).toBe(testLimit + 3);
|
||||
|
||||
// Adds the three Velvet groups to the previous 13
|
||||
await userEvent.type(multiselect, 'Velv');
|
||||
// The 3 Velvet groups + the create option
|
||||
expect(screen.getAllByLabelText('Select option').length).toBe(4);
|
||||
await userEvent.clear(multiselect);
|
||||
expect(screen.getAllByLabelText('Select option').length).toBe(testLimit + 6);
|
||||
});
|
||||
|
||||
it('should render template variables a selectable option', async () => {
|
||||
lodash.debounce = jest.fn().mockImplementation((fn) => fn);
|
||||
ds.datasource.describeLogGroups = jest.fn().mockResolvedValue([]);
|
||||
const onChange = jest.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onChange,
|
||||
};
|
||||
render(<LogGroupSelector {...props} />);
|
||||
|
||||
const logGroupSelector = await screen.findByLabelText('Log Groups');
|
||||
expect(logGroupSelector).toBeInTheDocument();
|
||||
|
||||
await openMenu(logGroupSelector);
|
||||
const templateVariableSelector = await screen.findByText('Template Variables');
|
||||
expect(templateVariableSelector).toBeInTheDocument();
|
||||
|
||||
userEvent.click(templateVariableSelector);
|
||||
await select(await screen.findByLabelText('Select option'), 'test');
|
||||
|
||||
expect(onChange).toBeCalledWith(['test']);
|
||||
});
|
||||
});
|
@ -0,0 +1,151 @@
|
||||
import { debounce, intersection, unionBy } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
import { MultiSelect } from '@grafana/ui';
|
||||
import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { appendTemplateVariables } from '../utils/utils';
|
||||
|
||||
const MAX_LOG_GROUPS = 20;
|
||||
const MAX_VISIBLE_LOG_GROUPS = 4;
|
||||
const DEBOUNCE_TIMER = 300;
|
||||
|
||||
export interface LogGroupSelectorProps {
|
||||
region: string;
|
||||
selectedLogGroups: string[];
|
||||
onChange: (logGroups: string[]) => void;
|
||||
|
||||
datasource?: CloudWatchDatasource;
|
||||
onRunQuery?: () => void;
|
||||
onOpenMenu?: () => Promise<void>;
|
||||
refId?: string;
|
||||
width?: number | 'auto';
|
||||
saved?: boolean;
|
||||
}
|
||||
|
||||
export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
|
||||
region,
|
||||
selectedLogGroups,
|
||||
onChange,
|
||||
datasource,
|
||||
onRunQuery,
|
||||
onOpenMenu,
|
||||
refId,
|
||||
width,
|
||||
saved = true,
|
||||
}) => {
|
||||
const [loadingLogGroups, setLoadingLogGroups] = useState(false);
|
||||
const [availableLogGroups, setAvailableLogGroups] = useState<Array<SelectableValue<string>>>([]);
|
||||
const logGroupOptions = useMemo(
|
||||
() => unionBy(availableLogGroups, selectedLogGroups?.map(toOption), 'value'),
|
||||
[availableLogGroups, selectedLogGroups]
|
||||
);
|
||||
|
||||
const fetchLogGroupOptions = useCallback(
|
||||
async (region: string, logGroupNamePrefix?: string) => {
|
||||
if (!datasource) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const logGroups: string[] = await datasource.describeLogGroups({
|
||||
refId,
|
||||
region,
|
||||
logGroupNamePrefix,
|
||||
});
|
||||
return logGroups.map(toOption);
|
||||
} catch (err) {
|
||||
dispatch(notifyApp(createErrorNotification(typeof err === 'string' ? err : JSON.stringify(err))));
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[datasource, refId]
|
||||
);
|
||||
|
||||
const onLogGroupSearch = async (searchTerm: string, region: string, actionMeta: InputActionMeta) => {
|
||||
if (actionMeta.action !== 'input-change' || !datasource) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to fetch matching log groups if the search term isn't valid
|
||||
// This is also useful for preventing searches when a user is typing out a log group with template vars
|
||||
// See https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_LogGroup.html for the source of the pattern below
|
||||
const logGroupNamePattern = /^[\.\-_/#A-Za-z0-9]+$/;
|
||||
if (!logGroupNamePattern.test(searchTerm)) {
|
||||
if (searchTerm !== '') {
|
||||
dispatch(notifyApp(createErrorNotification('Invalid Log Group name: ' + searchTerm)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingLogGroups(true);
|
||||
const matchingLogGroups = await fetchLogGroupOptions(region, searchTerm);
|
||||
setAvailableLogGroups(unionBy(availableLogGroups, matchingLogGroups, 'value'));
|
||||
setLoadingLogGroups(false);
|
||||
};
|
||||
|
||||
// Reset the log group options if the datasource or region change and are saved
|
||||
useEffect(() => {
|
||||
async function resetLogGroups() {
|
||||
// Don't call describeLogGroups if datasource or region is undefined
|
||||
if (!datasource || !datasource.getActualRegion(region)) {
|
||||
setAvailableLogGroups([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingLogGroups(true);
|
||||
return fetchLogGroupOptions(datasource.getActualRegion(region))
|
||||
.then((logGroups) => {
|
||||
const newSelectedLogGroups = intersection(
|
||||
selectedLogGroups,
|
||||
logGroups.map((l) => l.value || '')
|
||||
);
|
||||
onChange(newSelectedLogGroups);
|
||||
setAvailableLogGroups(logGroups);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingLogGroups(false);
|
||||
});
|
||||
}
|
||||
// Only reset if the current datasource is saved
|
||||
saved && resetLogGroups();
|
||||
// this hook shouldn't get called every time selectedLogGroups or onChange updates
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasource, region, saved]);
|
||||
|
||||
const onOpenLogGroupMenu = async () => {
|
||||
if (onOpenMenu) {
|
||||
await onOpenMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const onLogGroupSearchDebounced = debounce(onLogGroupSearch, DEBOUNCE_TIMER);
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
inputId="default-log-groups"
|
||||
aria-label="Log Groups"
|
||||
allowCustomValue
|
||||
options={datasource ? appendTemplateVariables(datasource, logGroupOptions) : logGroupOptions}
|
||||
value={selectedLogGroups}
|
||||
onChange={(v) => onChange(v.filter(({ value }) => value).map(({ value }) => value))}
|
||||
onBlur={onRunQuery}
|
||||
closeMenuOnSelect={false}
|
||||
isClearable
|
||||
isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS}
|
||||
placeholder="Choose Log Groups"
|
||||
maxVisibleValues={MAX_VISIBLE_LOG_GROUPS}
|
||||
noOptionsMessage="No log groups available"
|
||||
isLoading={loadingLogGroups}
|
||||
onOpenMenu={onOpenLogGroupMenu}
|
||||
onInputChange={(value, actionMeta) => {
|
||||
onLogGroupSearchDebounced(value, region, actionMeta);
|
||||
}}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
@ -13,7 +13,7 @@ import CloudWatchLink from './CloudWatchLink';
|
||||
import { CloudWatchLogsQueryField } from './LogsQueryField';
|
||||
|
||||
type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> & {
|
||||
allowCustomValue?: boolean;
|
||||
query: CloudWatchLogsQuery;
|
||||
};
|
||||
|
||||
const labelClass = css`
|
||||
@ -22,7 +22,7 @@ const labelClass = css`
|
||||
`;
|
||||
|
||||
export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) {
|
||||
const { query, data, datasource, onRunQuery, onChange, exploreId, allowCustomValue = false } = props;
|
||||
const { query, data, datasource, onRunQuery, onChange, exploreId } = props;
|
||||
|
||||
let absolute: AbsoluteTimeRange;
|
||||
if (data?.request?.range?.from) {
|
||||
@ -48,7 +48,6 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
|
||||
history={[]}
|
||||
data={data}
|
||||
absoluteRange={absolute}
|
||||
allowCustomValue={allowCustomValue}
|
||||
ExtraFieldElement={
|
||||
<InlineFormLabel className={`gf-form-label--btn ${labelClass}`} width="auto" tooltip="Link to Graph in AWS">
|
||||
<CloudWatchLink query={query as CloudWatchLogsQuery} panelData={data} datasource={datasource} />
|
||||
|
@ -1,16 +1,10 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { shallow } from 'enzyme';
|
||||
import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { openMenu, select } from 'react-select-event';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { ExploreId } from '../../../../types';
|
||||
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
||||
import { DescribeLogGroupsRequest } from '../types';
|
||||
|
||||
import { CloudWatchLogsQueryField } from './LogsQueryField';
|
||||
|
||||
@ -40,232 +34,4 @@ describe('CloudWatchLogsQueryField', () => {
|
||||
});
|
||||
expect(onRunQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates upstream query log groups on region change', async () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<CloudWatchLogsQueryField
|
||||
history={[]}
|
||||
absoluteRange={{ from: 1, to: 10 }}
|
||||
exploreId={ExploreId.left}
|
||||
datasource={
|
||||
{
|
||||
getRegions() {
|
||||
return Promise.resolve([
|
||||
{
|
||||
label: 'region1',
|
||||
value: 'region1',
|
||||
text: 'region1',
|
||||
},
|
||||
{
|
||||
label: 'region2',
|
||||
value: 'region2',
|
||||
text: 'region2',
|
||||
},
|
||||
]);
|
||||
},
|
||||
describeLogGroups(params: any) {
|
||||
if (params.region === 'region1') {
|
||||
return Promise.resolve(['log_group_1']);
|
||||
} else {
|
||||
return Promise.resolve(['log_group_2']);
|
||||
}
|
||||
},
|
||||
getVariables: jest.fn().mockReturnValue([]),
|
||||
} as any
|
||||
}
|
||||
query={{} as any}
|
||||
onRunQuery={() => {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
const getLogGroupSelect = () => wrapper.find({ label: 'Log Groups' }).props().inputEl;
|
||||
|
||||
getLogGroupSelect().props.onChange([{ value: 'log_group_1' }]);
|
||||
expect(getLogGroupSelect().props.value.length).toBe(1);
|
||||
expect(getLogGroupSelect().props.value[0].value).toBe('log_group_1');
|
||||
|
||||
// We select new region where the selected log group does not exist
|
||||
await (wrapper.instance() as CloudWatchLogsQueryField).onRegionChange('region2');
|
||||
|
||||
// We clear the select
|
||||
expect(getLogGroupSelect().props.value.length).toBe(0);
|
||||
// Make sure we correctly updated the upstream state
|
||||
expect(onChange).toHaveBeenLastCalledWith({ logGroupNames: [] });
|
||||
});
|
||||
|
||||
it('should merge results of remote log groups search with existing results', async () => {
|
||||
const allLogGroups = [
|
||||
'AmazingGroup',
|
||||
'AmazingGroup2',
|
||||
'AmazingGroup3',
|
||||
'BeautifulGroup',
|
||||
'BeautifulGroup2',
|
||||
'BeautifulGroup3',
|
||||
'CrazyGroup',
|
||||
'CrazyGroup2',
|
||||
'CrazyGroup3',
|
||||
'DeliciousGroup',
|
||||
'DeliciousGroup2',
|
||||
'DeliciousGroup3',
|
||||
'EnjoyableGroup',
|
||||
'EnjoyableGroup2',
|
||||
'EnjoyableGroup3',
|
||||
'FavouriteGroup',
|
||||
'FavouriteGroup2',
|
||||
'FavouriteGroup3',
|
||||
'GorgeousGroup',
|
||||
'GorgeousGroup2',
|
||||
'GorgeousGroup3',
|
||||
'HappyGroup',
|
||||
'HappyGroup2',
|
||||
'HappyGroup3',
|
||||
'IncredibleGroup',
|
||||
'IncredibleGroup2',
|
||||
'IncredibleGroup3',
|
||||
'JollyGroup',
|
||||
'JollyGroup2',
|
||||
'JollyGroup3',
|
||||
'KoolGroup',
|
||||
'KoolGroup2',
|
||||
'KoolGroup3',
|
||||
'LovelyGroup',
|
||||
'LovelyGroup2',
|
||||
'LovelyGroup3',
|
||||
'MagnificentGroup',
|
||||
'MagnificentGroup2',
|
||||
'MagnificentGroup3',
|
||||
'NiceGroup',
|
||||
'NiceGroup2',
|
||||
'NiceGroup3',
|
||||
'OddGroup',
|
||||
'OddGroup2',
|
||||
'OddGroup3',
|
||||
'PerfectGroup',
|
||||
'PerfectGroup2',
|
||||
'PerfectGroup3',
|
||||
'QuietGroup',
|
||||
'QuietGroup2',
|
||||
'QuietGroup3',
|
||||
'RestlessGroup',
|
||||
'RestlessGroup2',
|
||||
'RestlessGroup3',
|
||||
'SurpriseGroup',
|
||||
'SurpriseGroup2',
|
||||
'SurpriseGroup3',
|
||||
'TestingGroup',
|
||||
'TestingGroup2',
|
||||
'TestingGroup3',
|
||||
'UmbrellaGroup',
|
||||
'UmbrellaGroup2',
|
||||
'UmbrellaGroup3',
|
||||
'VelvetGroup',
|
||||
'VelvetGroup2',
|
||||
'VelvetGroup3',
|
||||
'WaterGroup',
|
||||
'WaterGroup2',
|
||||
'WaterGroup3',
|
||||
'XylophoneGroup',
|
||||
'XylophoneGroup2',
|
||||
'XylophoneGroup3',
|
||||
'YellowGroup',
|
||||
'YellowGroup2',
|
||||
'YellowGroup3',
|
||||
'ZebraGroup',
|
||||
'ZebraGroup2',
|
||||
'ZebraGroup3',
|
||||
];
|
||||
|
||||
const onChange = jest.fn();
|
||||
const wrapper = shallow<CloudWatchLogsQueryField>(
|
||||
<CloudWatchLogsQueryField
|
||||
history={[]}
|
||||
absoluteRange={{ from: 1, to: 10 }}
|
||||
exploreId={ExploreId.left}
|
||||
datasource={
|
||||
{
|
||||
getRegions() {
|
||||
return Promise.resolve([
|
||||
{
|
||||
label: 'region1',
|
||||
value: 'region1',
|
||||
text: 'region1',
|
||||
},
|
||||
{
|
||||
label: 'region2',
|
||||
value: 'region2',
|
||||
text: 'region2',
|
||||
},
|
||||
]);
|
||||
},
|
||||
describeLogGroups(params: DescribeLogGroupsRequest) {
|
||||
const theLogGroups = allLogGroups
|
||||
.filter((logGroupName) => logGroupName.startsWith(params.logGroupNamePrefix ?? ''))
|
||||
.slice(0, Math.max(params.limit ?? 50, 50));
|
||||
return Promise.resolve(theLogGroups);
|
||||
},
|
||||
getVariables: jest.fn().mockReturnValue([]),
|
||||
} as any
|
||||
}
|
||||
query={{} as any}
|
||||
onRunQuery={() => {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
const initialAvailableGroups = allLogGroups
|
||||
.slice(0, 50)
|
||||
.map((logGroupName) => ({ value: logGroupName, label: logGroupName }));
|
||||
wrapper.setState({
|
||||
availableLogGroups: initialAvailableGroups,
|
||||
});
|
||||
|
||||
await wrapper.instance().onLogGroupSearch('Water', 'default', { action: 'input-change' });
|
||||
|
||||
let nextAvailableGroups = (wrapper.state('availableLogGroups') as Array<SelectableValue<string>>).map(
|
||||
(logGroup) => logGroup.value
|
||||
);
|
||||
expect(nextAvailableGroups).toEqual(
|
||||
initialAvailableGroups.map((logGroup) => logGroup.value).concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3'])
|
||||
);
|
||||
|
||||
await wrapper.instance().onLogGroupSearch('Velv', 'default', { action: 'input-change' });
|
||||
nextAvailableGroups = (wrapper.state('availableLogGroups') as Array<SelectableValue<string>>).map(
|
||||
(logGroup) => logGroup.value
|
||||
);
|
||||
expect(nextAvailableGroups).toEqual(
|
||||
initialAvailableGroups
|
||||
.map((logGroup) => logGroup.value)
|
||||
.concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should render template variables a selectable option', async () => {
|
||||
const { datasource } = setupMockedDataSource();
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(
|
||||
<CloudWatchLogsQueryField
|
||||
history={[]}
|
||||
absoluteRange={{ from: 1, to: 10 }}
|
||||
exploreId={ExploreId.left}
|
||||
datasource={datasource}
|
||||
query={{} as any}
|
||||
onRunQuery={() => {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
const logGroupSelector = await screen.findByLabelText('Log Groups');
|
||||
expect(logGroupSelector).toBeInTheDocument();
|
||||
|
||||
await openMenu(logGroupSelector);
|
||||
const templateVariableSelector = await screen.findByText('Template Variables');
|
||||
expect(templateVariableSelector).toBeInTheDocument();
|
||||
|
||||
userEvent.click(templateVariableSelector);
|
||||
await select(await screen.findByLabelText('Select option'), 'test');
|
||||
|
||||
expect(await screen.findByText('test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,23 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce, intersectionBy, unionBy } from 'lodash';
|
||||
import { LanguageMap, languages as prismLanguages } from 'prismjs';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Editor, Node, Plugin } from 'slate';
|
||||
import { Node, Plugin } from 'slate';
|
||||
|
||||
import { AbsoluteTimeRange, QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
BracesPlugin,
|
||||
LegacyForms,
|
||||
MultiSelect,
|
||||
QueryField,
|
||||
SlatePrism,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
} from '@grafana/ui';
|
||||
import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data';
|
||||
import { BracesPlugin, LegacyForms, QueryField, SlatePrism, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
|
||||
import { ExploreId } from 'app/types';
|
||||
// Utils & Services
|
||||
// dom also includes Element polyfills
|
||||
@ -27,8 +14,8 @@ import { CloudWatchLanguageProvider } from '../language_provider';
|
||||
import syntax from '../syntax';
|
||||
import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types';
|
||||
import { getStatsGroups } from '../utils/query/getStatsGroups';
|
||||
import { appendTemplateVariables } from '../utils/utils';
|
||||
|
||||
import { LogGroupSelector } from './LogGroupSelector';
|
||||
import QueryHeader from './QueryHeader';
|
||||
|
||||
export interface CloudWatchLogsQueryFieldProps
|
||||
@ -37,23 +24,14 @@ export interface CloudWatchLogsQueryFieldProps
|
||||
onLabelsRefresh?: () => void;
|
||||
ExtraFieldElement?: ReactNode;
|
||||
exploreId: ExploreId;
|
||||
allowCustomValue?: boolean;
|
||||
query: CloudWatchLogsQuery;
|
||||
}
|
||||
|
||||
const containerClass = css`
|
||||
flex-grow: 1;
|
||||
min-height: 35px;
|
||||
`;
|
||||
|
||||
const rowGap = css`
|
||||
gap: 3px;
|
||||
`;
|
||||
|
||||
interface State {
|
||||
selectedLogGroups: Array<SelectableValue<string>>;
|
||||
availableLogGroups: Array<SelectableValue<string>>;
|
||||
loadingLogGroups: boolean;
|
||||
invalidLogGroups: boolean;
|
||||
hint:
|
||||
| {
|
||||
message: string;
|
||||
@ -67,14 +45,6 @@ interface State {
|
||||
|
||||
export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogsQueryFieldProps, State> {
|
||||
state: State = {
|
||||
selectedLogGroups:
|
||||
(this.props.query as CloudWatchLogsQuery).logGroupNames?.map((logGroup) => ({
|
||||
value: logGroup,
|
||||
label: logGroup,
|
||||
})) ?? [],
|
||||
availableLogGroups: [],
|
||||
invalidLogGroups: false,
|
||||
loadingLogGroups: false,
|
||||
hint: undefined,
|
||||
};
|
||||
|
||||
@ -95,154 +65,31 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
];
|
||||
}
|
||||
|
||||
fetchLogGroupOptions = async (region: string, logGroupNamePrefix?: string) => {
|
||||
try {
|
||||
const logGroups: string[] = await this.props.datasource.describeLogGroups({
|
||||
refId: this.props.query.refId,
|
||||
region,
|
||||
logGroupNamePrefix,
|
||||
});
|
||||
|
||||
return logGroups.map((logGroup) => ({
|
||||
value: logGroup,
|
||||
label: logGroup,
|
||||
}));
|
||||
} catch (err) {
|
||||
let errMessage = 'unknown error';
|
||||
if (typeof err !== 'string') {
|
||||
try {
|
||||
errMessage = JSON.stringify(err);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
errMessage = err;
|
||||
}
|
||||
dispatch(notifyApp(createErrorNotification(errMessage)));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
onLogGroupSearch = (searchTerm: string, region: string, actionMeta: InputActionMeta) => {
|
||||
if (actionMeta.action !== 'input-change') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// No need to fetch matching log groups if the search term isn't valid
|
||||
// This is also useful for preventing searches when a user is typing out a log group with template vars
|
||||
// See https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_LogGroup.html for the source of the pattern below
|
||||
const logGroupNamePattern = /^[\.\-_/#A-Za-z0-9]+$/;
|
||||
if (!logGroupNamePattern.test(searchTerm)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingLogGroups: true,
|
||||
});
|
||||
|
||||
return this.fetchLogGroupOptions(region, searchTerm)
|
||||
.then((matchingLogGroups) => {
|
||||
this.setState((state) => ({
|
||||
availableLogGroups: unionBy(state.availableLogGroups, matchingLogGroups, 'value'),
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({
|
||||
loadingLogGroups: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onLogGroupSearchDebounced = debounce(this.onLogGroupSearch, 300);
|
||||
|
||||
componentDidMount = () => {
|
||||
const { query, onChange } = this.props;
|
||||
|
||||
this.setState({
|
||||
loadingLogGroups: true,
|
||||
});
|
||||
|
||||
query.region &&
|
||||
this.fetchLogGroupOptions(query.region).then((logGroups) => {
|
||||
this.setState((state) => {
|
||||
const selectedLogGroups = state.selectedLogGroups;
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
logGroupNames: selectedLogGroups.map((group) => group.value!),
|
||||
};
|
||||
|
||||
onChange(nextQuery);
|
||||
}
|
||||
|
||||
return {
|
||||
loadingLogGroups: false,
|
||||
availableLogGroups: logGroups,
|
||||
selectedLogGroups,
|
||||
};
|
||||
});
|
||||
});
|
||||
if (onChange) {
|
||||
onChange({ ...query, logGroupNames: query.logGroupNames ?? [] });
|
||||
}
|
||||
};
|
||||
|
||||
onChangeQuery = (value: string) => {
|
||||
// Send text change to parent
|
||||
const { query, onChange } = this.props;
|
||||
const { selectedLogGroups } = this.state;
|
||||
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
expression: value,
|
||||
logGroupNames: selectedLogGroups?.map((logGroupName) => logGroupName.value!) ?? [],
|
||||
statsGroups: getStatsGroups(value),
|
||||
};
|
||||
onChange(nextQuery);
|
||||
}
|
||||
};
|
||||
|
||||
setSelectedLogGroups = (selectedLogGroups: Array<SelectableValue<string>>) => {
|
||||
this.setState({
|
||||
selectedLogGroups,
|
||||
});
|
||||
|
||||
const { onChange, query } = this.props;
|
||||
onChange?.({
|
||||
...(query as CloudWatchLogsQuery),
|
||||
logGroupNames: selectedLogGroups.map((logGroupName) => logGroupName.value!) ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
setCustomLogGroups = (v: string) => {
|
||||
const customLogGroup: SelectableValue<string> = { value: v, label: v };
|
||||
const selectedLogGroups = [...this.state.selectedLogGroups, customLogGroup];
|
||||
this.setSelectedLogGroups(selectedLogGroups);
|
||||
};
|
||||
|
||||
onRegionChange = async (v: string) => {
|
||||
this.setState({
|
||||
loadingLogGroups: true,
|
||||
});
|
||||
const logGroups = await this.fetchLogGroupOptions(v);
|
||||
this.setState((state) => {
|
||||
const selectedLogGroups = intersectionBy(state.selectedLogGroups, logGroups, 'value');
|
||||
const { onChange, query } = this.props;
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
logGroupNames: selectedLogGroups.map((group) => group.value!),
|
||||
};
|
||||
|
||||
onChange(nextQuery);
|
||||
}
|
||||
return {
|
||||
availableLogGroups: logGroups,
|
||||
selectedLogGroups: selectedLogGroups,
|
||||
loadingLogGroups: false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
const { datasource, query } = this.props;
|
||||
const { selectedLogGroups } = this.state;
|
||||
const { logGroupNames } = query;
|
||||
|
||||
if (!datasource.languageProvider) {
|
||||
return { suggestions: [] };
|
||||
@ -257,41 +104,20 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
{
|
||||
history,
|
||||
absoluteRange,
|
||||
logGroupNames: selectedLogGroups.map((logGroup) => logGroup.value!),
|
||||
logGroupNames,
|
||||
region: query.region,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onQueryFieldClick = (_event: Event, _editor: Editor, next: () => any) => {
|
||||
const { selectedLogGroups, loadingLogGroups } = this.state;
|
||||
|
||||
const queryFieldDisabled = loadingLogGroups || selectedLogGroups.length === 0;
|
||||
|
||||
if (queryFieldDisabled) {
|
||||
this.setState({
|
||||
invalidLogGroups: true,
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
onOpenLogGroupMenu = () => {
|
||||
this.setState({
|
||||
invalidLogGroups: false,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onRunQuery, onChange, ExtraFieldElement, data, query, datasource, allowCustomValue } = this.props;
|
||||
const { selectedLogGroups, availableLogGroups, loadingLogGroups, hint, invalidLogGroups } = this.state;
|
||||
const { onRunQuery, onChange, ExtraFieldElement, data, query, datasource } = this.props;
|
||||
const { region, refId, expression, logGroupNames } = query;
|
||||
const { hint } = this.state;
|
||||
|
||||
const showError = data && data.error && data.error.refId === query.refId;
|
||||
const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
|
||||
|
||||
const MAX_LOG_GROUPS = 20;
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryHeader
|
||||
@ -300,7 +126,6 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
sqlCodeEditorIsDirty={false}
|
||||
onRegionChange={this.onRegionChange}
|
||||
/>
|
||||
<div className={`gf-form gf-form--grow flex-grow-1 ${rowGap}`}>
|
||||
<LegacyForms.FormField
|
||||
@ -308,31 +133,15 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
labelWidth={6}
|
||||
className="flex-grow-1"
|
||||
inputEl={
|
||||
<MultiSelect
|
||||
aria-label="Log Groups"
|
||||
allowCustomValue={allowCustomValue}
|
||||
options={appendTemplateVariables(datasource, unionBy(availableLogGroups, selectedLogGroups, 'value'))}
|
||||
value={selectedLogGroups}
|
||||
onChange={(v) => {
|
||||
this.setSelectedLogGroups(v);
|
||||
}}
|
||||
onCreateOption={(v) => {
|
||||
this.setCustomLogGroups(v);
|
||||
}}
|
||||
onBlur={this.props.onRunQuery}
|
||||
className={containerClass}
|
||||
closeMenuOnSelect={false}
|
||||
isClearable={true}
|
||||
invalid={invalidLogGroups}
|
||||
isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS}
|
||||
placeholder="Choose Log Groups"
|
||||
maxVisibleValues={4}
|
||||
noOptionsMessage="No log groups available"
|
||||
isLoading={loadingLogGroups}
|
||||
onOpenMenu={this.onOpenLogGroupMenu}
|
||||
onInputChange={(value, actionMeta) => {
|
||||
this.onLogGroupSearchDebounced(value, query.region, actionMeta);
|
||||
<LogGroupSelector
|
||||
region={region}
|
||||
selectedLogGroups={logGroupNames ?? []}
|
||||
datasource={datasource}
|
||||
onChange={function (logGroups: string[]): void {
|
||||
onChange({ ...query, logGroupNames: logGroups });
|
||||
}}
|
||||
onRunQuery={onRunQuery}
|
||||
refId={refId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -341,15 +150,14 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
<div className="gf-form gf-form--grow flex-shrink-1">
|
||||
<QueryField
|
||||
additionalPlugins={this.plugins}
|
||||
query={(query as CloudWatchLogsQuery).expression ?? ''}
|
||||
query={expression ?? ''}
|
||||
onChange={this.onChangeQuery}
|
||||
onClick={this.onQueryFieldClick}
|
||||
onRunQuery={this.props.onRunQuery}
|
||||
onTypeahead={this.onTypeahead}
|
||||
cleanText={cleanText}
|
||||
placeholder="Enter a CloudWatch Logs Insights query (run with Shift+Enter)"
|
||||
portalOrigin="cloudwatch"
|
||||
disabled={loadingLogGroups || selectedLogGroups.length === 0}
|
||||
disabled={!logGroupNames || logGroupNames.length === 0}
|
||||
/>
|
||||
</div>
|
||||
{ExtraFieldElement}
|
||||
|
@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
|
||||
import { QueryEditorProps } from '@grafana/data';
|
||||
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { isCloudWatchMetricsQuery } from '../guards';
|
||||
import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../guards';
|
||||
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
|
||||
|
||||
import { MetricsQueryEditor } from '././MetricsQueryEditor/MetricsQueryEditor';
|
||||
@ -17,11 +17,8 @@ export class PanelQueryEditor extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCloudWatchMetricsQuery(query) ? (
|
||||
<MetricsQueryEditor {...this.props} query={query} />
|
||||
) : (
|
||||
<LogsQueryEditor {...this.props} allowCustomValue />
|
||||
)}
|
||||
{isCloudWatchMetricsQuery(query) && <MetricsQueryEditor {...this.props} query={query} />}
|
||||
{isCloudWatchLogsQuery(query) && <LogsQueryEditor {...this.props} query={query} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -24,14 +24,7 @@ const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
|
||||
{ label: 'CloudWatch Logs', value: 'Logs' },
|
||||
];
|
||||
|
||||
const QueryHeader: React.FC<QueryHeaderProps> = ({
|
||||
query,
|
||||
sqlCodeEditorIsDirty,
|
||||
datasource,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
onRegionChange,
|
||||
}) => {
|
||||
const QueryHeader: React.FC<QueryHeaderProps> = ({ query, sqlCodeEditorIsDirty, datasource, onChange, onRunQuery }) => {
|
||||
const { queryMode, region } = query;
|
||||
|
||||
const [regions, regionIsLoading] = useRegions(datasource);
|
||||
@ -47,9 +40,6 @@ const QueryHeader: React.FC<QueryHeaderProps> = ({
|
||||
};
|
||||
|
||||
const onRegion = async ({ value }: SelectableValue<string>) => {
|
||||
if (onRegionChange) {
|
||||
await onRegionChange(value ?? 'default');
|
||||
}
|
||||
onChange({
|
||||
...query,
|
||||
region: value,
|
||||
|
Loading…
Reference in New Issue
Block a user