mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch: add dimensions component to variable editor (#47596)
This commit is contained in:
parent
2d8d9bc137
commit
992c0604f9
@ -249,7 +249,7 @@
|
|||||||
"@grafana/aws-sdk": "0.0.35",
|
"@grafana/aws-sdk": "0.0.35",
|
||||||
"@grafana/data": "workspace:*",
|
"@grafana/data": "workspace:*",
|
||||||
"@grafana/e2e-selectors": "workspace:*",
|
"@grafana/e2e-selectors": "workspace:*",
|
||||||
"@grafana/experimental": "0.0.2-canary.22",
|
"@grafana/experimental": "^0.0.2-canary.25",
|
||||||
"@grafana/google-sdk": "0.0.3",
|
"@grafana/google-sdk": "0.0.3",
|
||||||
"@grafana/lezer-logql": "^0.0.11",
|
"@grafana/lezer-logql": "^0.0.11",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { EditorList } from '@grafana/experimental';
|
import { EditorList } from '@grafana/experimental';
|
||||||
import { CloudWatchDatasource } from '../../datasource';
|
import { CloudWatchDatasource } from '../../datasource';
|
||||||
import { CloudWatchMetricsQuery, Dimensions as DimensionsType } from '../../types';
|
import { Dimensions as DimensionsType, DimensionsQuery } from '../../types';
|
||||||
import { FilterItem } from './FilterItem';
|
import { FilterItem } from './FilterItem';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: CloudWatchMetricsQuery;
|
query: DimensionsQuery;
|
||||||
onChange: (dimensions: DimensionsType) => void;
|
onChange: (dimensions: DimensionsType) => void;
|
||||||
datasource: CloudWatchDatasource;
|
datasource: CloudWatchDatasource;
|
||||||
dimensionKeys: Array<SelectableValue<string>>;
|
dimensionKeys: Array<SelectableValue<string>>;
|
||||||
@ -43,8 +43,8 @@ const filterConditionsToDimensions = (filters: DimensionFilterCondition[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Dimensions: React.FC<Props> = ({ query, datasource, dimensionKeys, disableExpressions, onChange }) => {
|
export const Dimensions: React.FC<Props> = ({ query, datasource, dimensionKeys, disableExpressions, onChange }) => {
|
||||||
const dimensionFilters = useMemo(() => dimensionsToFilterConditions(query.dimensions), [query.dimensions]);
|
const [items, setItems] = useState<DimensionFilterCondition[]>([]);
|
||||||
const [items, setItems] = useState<DimensionFilterCondition[]>(dimensionFilters);
|
useEffect(() => setItems(dimensionsToFilterConditions(query.dimensions)), [query.dimensions]);
|
||||||
const onDimensionsChange = (newItems: Array<Partial<DimensionFilterCondition>>) => {
|
const onDimensionsChange = (newItems: Array<Partial<DimensionFilterCondition>>) => {
|
||||||
setItems(newItems);
|
setItems(newItems);
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export const Dimensions: React.FC<Props> = ({ query, datasource, dimensionKeys,
|
|||||||
|
|
||||||
function makeRenderFilter(
|
function makeRenderFilter(
|
||||||
datasource: CloudWatchDatasource,
|
datasource: CloudWatchDatasource,
|
||||||
query: CloudWatchMetricsQuery,
|
query: DimensionsQuery,
|
||||||
dimensionKeys: Array<SelectableValue<string>>,
|
dimensionKeys: Array<SelectableValue<string>>,
|
||||||
disableExpressions: boolean
|
disableExpressions: boolean
|
||||||
) {
|
) {
|
@ -5,12 +5,12 @@ import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
|
|||||||
import { InputGroup, AccessoryButton } from '@grafana/experimental';
|
import { InputGroup, AccessoryButton } from '@grafana/experimental';
|
||||||
import { Select, stylesFactory, useTheme2 } from '@grafana/ui';
|
import { Select, stylesFactory, useTheme2 } from '@grafana/ui';
|
||||||
import { CloudWatchDatasource } from '../../datasource';
|
import { CloudWatchDatasource } from '../../datasource';
|
||||||
import { CloudWatchMetricsQuery, Dimensions } from '../../types';
|
import { Dimensions, DimensionsQuery } from '../../types';
|
||||||
import { appendTemplateVariables } from '../../utils/utils';
|
import { appendTemplateVariables } from '../../utils/utils';
|
||||||
import { DimensionFilterCondition } from './Dimensions';
|
import { DimensionFilterCondition } from './Dimensions';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: CloudWatchMetricsQuery;
|
query: DimensionsQuery;
|
||||||
datasource: CloudWatchDatasource;
|
datasource: CloudWatchDatasource;
|
||||||
filter: DimensionFilterCondition;
|
filter: DimensionFilterCondition;
|
||||||
dimensionKeys: Array<SelectableValue<string>>;
|
dimensionKeys: Array<SelectableValue<string>>;
|
||||||
@ -96,7 +96,7 @@ export const FilterItem: FunctionComponent<Props> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} />
|
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -1,7 +1,7 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { select } from 'react-select-event';
|
import { select } from 'react-select-event';
|
||||||
import { VariableQueryType } from '../../types';
|
import { Dimensions, VariableQueryType } from '../../types';
|
||||||
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
|
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
|
||||||
import { VariableQueryEditor, Props } from './VariableQueryEditor';
|
import { VariableQueryEditor, Props } from './VariableQueryEditor';
|
||||||
|
|
||||||
@ -11,7 +11,6 @@ const defaultQuery = {
|
|||||||
region: '',
|
region: '',
|
||||||
metricName: '',
|
metricName: '',
|
||||||
dimensionKey: '',
|
dimensionKey: '',
|
||||||
dimensionFilters: '',
|
|
||||||
ec2Filters: '',
|
ec2Filters: '',
|
||||||
instanceID: '',
|
instanceID: '',
|
||||||
attributeName: '',
|
attributeName: '',
|
||||||
@ -37,26 +36,42 @@ ds.datasource.getMetrics = jest.fn().mockResolvedValue([
|
|||||||
{ label: 'i3', value: 'i3' },
|
{ label: 'i3', value: 'i3' },
|
||||||
{ label: 'j3', value: 'j3' },
|
{ label: 'j3', value: 'j3' },
|
||||||
]);
|
]);
|
||||||
ds.datasource.getDimensionKeys = jest.fn().mockImplementation((namespace: string, region: string) => {
|
ds.datasource.getDimensionKeys = jest
|
||||||
if (region === 'a1') {
|
.fn()
|
||||||
return Promise.resolve([
|
.mockImplementation((namespace: string, region: string, dimensionFilters?: Dimensions) => {
|
||||||
{ label: 'q4', value: 'q4' },
|
if (!!dimensionFilters) {
|
||||||
{ label: 'r4', value: 'r4' },
|
return Promise.resolve([
|
||||||
{ label: 's4', value: 's4' },
|
{ label: 's4', value: 's4' },
|
||||||
]);
|
{ label: 'v4', value: 'v4' },
|
||||||
}
|
]);
|
||||||
return Promise.resolve([{ label: 't4', value: 't4' }]);
|
}
|
||||||
});
|
if (region === 'a1') {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ label: 'q4', value: 'q4' },
|
||||||
|
{ label: 'r4', value: 'r4' },
|
||||||
|
{ label: 's4', value: 's4' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return Promise.resolve([{ label: 't4', value: 't4' }]);
|
||||||
|
});
|
||||||
|
ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([
|
||||||
|
{ label: 'foo', value: 'foo' },
|
||||||
|
{ label: 'bar', value: 'bar' },
|
||||||
|
]);
|
||||||
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
|
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
|
||||||
|
|
||||||
|
const onChange = jest.fn();
|
||||||
const defaultProps: Props = {
|
const defaultProps: Props = {
|
||||||
onChange: jest.fn(),
|
onChange: onChange,
|
||||||
query: defaultQuery,
|
query: defaultQuery,
|
||||||
datasource: ds.datasource,
|
datasource: ds.datasource,
|
||||||
onRunQuery: () => {},
|
onRunQuery: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('VariableEditor', () => {
|
describe('VariableEditor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
onChange.mockClear();
|
||||||
|
});
|
||||||
describe('and a new variable is created', () => {
|
describe('and a new variable is created', () => {
|
||||||
it('should trigger a query using the first query type in the array', async () => {
|
it('should trigger a query using the first query type in the array', async () => {
|
||||||
const props = defaultProps;
|
const props = defaultProps;
|
||||||
@ -100,6 +115,56 @@ describe('VariableEditor', () => {
|
|||||||
expect(metricSelect).not.toBeInTheDocument();
|
expect(metricSelect).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should parse dimensionFilters correctly', async () => {
|
||||||
|
const props = defaultProps;
|
||||||
|
props.query = {
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.DimensionValues,
|
||||||
|
namespace: 'z2',
|
||||||
|
region: 'a1',
|
||||||
|
metricName: 'i3',
|
||||||
|
dimensionKey: 's4',
|
||||||
|
dimensionFilters: { s4: 'foo' },
|
||||||
|
};
|
||||||
|
render(<VariableQueryEditor {...props} />);
|
||||||
|
|
||||||
|
const filterItem = screen.getByTestId('cloudwatch-dimensions-filter-item');
|
||||||
|
expect(filterItem).toBeInTheDocument();
|
||||||
|
expect(within(filterItem).getByText('s4')).toBeInTheDocument();
|
||||||
|
expect(within(filterItem).getByText('foo')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// change filter key
|
||||||
|
const keySelect = screen.getByRole('combobox', { name: 'Dimensions filter key' });
|
||||||
|
// confirms getDimensionKeys was called with filter and that the element uses keysForDimensionFilter
|
||||||
|
await select(keySelect, 'v4', {
|
||||||
|
container: document.body,
|
||||||
|
});
|
||||||
|
expect(ds.datasource.getDimensionKeys).toHaveBeenCalledWith('z2', 'a1', {}, '');
|
||||||
|
expect(onChange).toHaveBeenCalledWith({
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.DimensionValues,
|
||||||
|
namespace: 'z2',
|
||||||
|
region: 'a1',
|
||||||
|
metricName: 'i3',
|
||||||
|
dimensionKey: 's4',
|
||||||
|
dimensionFilters: { v4: undefined },
|
||||||
|
});
|
||||||
|
|
||||||
|
// set filter value
|
||||||
|
const valueSelect = screen.getByRole('combobox', { name: 'Dimensions filter value' });
|
||||||
|
await select(valueSelect, 'bar', {
|
||||||
|
container: document.body,
|
||||||
|
});
|
||||||
|
expect(onChange).toHaveBeenCalledWith({
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.DimensionValues,
|
||||||
|
namespace: 'z2',
|
||||||
|
region: 'a1',
|
||||||
|
metricName: 'i3',
|
||||||
|
dimensionKey: 's4',
|
||||||
|
dimensionFilters: { v4: 'bar' },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('and a different region is selected', () => {
|
describe('and a different region is selected', () => {
|
||||||
it('should clear invalid fields', async () => {
|
it('should clear invalid fields', async () => {
|
||||||
@ -111,6 +176,7 @@ describe('VariableEditor', () => {
|
|||||||
region: 'a1',
|
region: 'a1',
|
||||||
metricName: 'i3',
|
metricName: 'i3',
|
||||||
dimensionKey: 's4',
|
dimensionKey: 's4',
|
||||||
|
dimensionFilters: { s4: 'foo' },
|
||||||
};
|
};
|
||||||
render(<VariableQueryEditor {...props} />);
|
render(<VariableQueryEditor {...props} />);
|
||||||
|
|
||||||
@ -118,7 +184,6 @@ describe('VariableEditor', () => {
|
|||||||
expect(querySelect).toBeInTheDocument();
|
expect(querySelect).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
|
expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
|
||||||
const regionSelect = screen.getByRole('combobox', { name: 'Region' });
|
const regionSelect = screen.getByRole('combobox', { name: 'Region' });
|
||||||
regionSelect.click();
|
|
||||||
await select(regionSelect, 'b1', {
|
await select(regionSelect, 'b1', {
|
||||||
container: document.body,
|
container: document.body,
|
||||||
});
|
});
|
||||||
@ -133,8 +198,9 @@ describe('VariableEditor', () => {
|
|||||||
region: 'b1',
|
region: 'b1',
|
||||||
// metricName i3 exists in the new region and should not be removed
|
// metricName i3 exists in the new region and should not be removed
|
||||||
metricName: 'i3',
|
metricName: 'i3',
|
||||||
// dimensionKey s4 does not exist in the new region and should be removed
|
// dimensionKey s4 and valueDimension do not exist in the new region and should be removed
|
||||||
dimensionKey: '',
|
dimensionKey: '',
|
||||||
|
dimensionFilters: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,8 @@ import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../h
|
|||||||
import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
|
import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
|
||||||
import { migrateVariableQuery } from '../../migrations';
|
import { migrateVariableQuery } from '../../migrations';
|
||||||
import { VariableQueryField } from './VariableQueryField';
|
import { VariableQueryField } from './VariableQueryField';
|
||||||
|
import { Dimensions } from '..';
|
||||||
|
import { InlineField } from '@grafana/ui';
|
||||||
|
|
||||||
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData, VariableQuery>;
|
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData, VariableQuery>;
|
||||||
|
|
||||||
@ -25,11 +27,12 @@ const queryTypes: Array<{ value: string; label: string }> = [
|
|||||||
export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||||
const parsedQuery = migrateVariableQuery(query);
|
const parsedQuery = migrateVariableQuery(query);
|
||||||
|
|
||||||
const { region, namespace, metricName, dimensionKey } = parsedQuery;
|
const { region, namespace, metricName, dimensionKey, dimensionFilters } = parsedQuery;
|
||||||
const [regions, regionIsLoading] = useRegions(datasource);
|
const [regions, regionIsLoading] = useRegions(datasource);
|
||||||
const namespaces = useNamespaces(datasource);
|
const namespaces = useNamespaces(datasource);
|
||||||
const metrics = useMetrics(datasource, region, namespace);
|
const metrics = useMetrics(datasource, region, namespace);
|
||||||
const dimensionKeys = useDimensionKeys(datasource, region, namespace, metricName);
|
const dimensionKeys = useDimensionKeys(datasource, region, namespace, metricName);
|
||||||
|
const keysForDimensionFilter = useDimensionKeys(datasource, region, namespace, metricName, dimensionFilters ?? {});
|
||||||
|
|
||||||
const onRegionChange = async (region: string) => {
|
const onRegionChange = async (region: string) => {
|
||||||
const validatedQuery = await sanitizeQuery({
|
const validatedQuery = await sanitizeQuery({
|
||||||
@ -48,7 +51,10 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onQueryChange = (newQuery: VariableQuery) => {
|
const onQueryChange = (newQuery: VariableQuery) => {
|
||||||
onChange({ ...newQuery, refId: 'CloudWatchVariableQueryEditor-VariableQuery' });
|
onChange({
|
||||||
|
...newQuery,
|
||||||
|
refId: 'CloudWatchVariableQueryEditor-VariableQuery',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset dimensionValue parameters if namespace or region change
|
// Reset dimensionValue parameters if namespace or region change
|
||||||
@ -58,7 +64,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
await datasource.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
|
await datasource.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
|
||||||
if (!result.find((metric) => metric.value === metricName)) {
|
if (!result.find((metric) => metric.value === metricName)) {
|
||||||
metricName = '';
|
metricName = '';
|
||||||
dimensionFilters = '';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -66,7 +71,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
await datasource.getDimensionKeys(namespace, region).then((result: Array<SelectableValue<string>>) => {
|
await datasource.getDimensionKeys(namespace, region).then((result: Array<SelectableValue<string>>) => {
|
||||||
if (!result.find((key) => key.value === dimensionKey)) {
|
if (!result.find((key) => key.value === dimensionKey)) {
|
||||||
dimensionKey = '';
|
dimensionKey = '';
|
||||||
dimensionFilters = '';
|
dimensionFilters = {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -86,7 +91,6 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
VariableQueryType.DimensionKeys,
|
VariableQueryType.DimensionKeys,
|
||||||
VariableQueryType.DimensionValues,
|
VariableQueryType.DimensionValues,
|
||||||
].includes(parsedQuery.queryType);
|
].includes(parsedQuery.queryType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VariableQueryField
|
<VariableQueryField
|
||||||
@ -94,6 +98,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
options={queryTypes}
|
options={queryTypes}
|
||||||
onChange={(value: VariableQueryType) => onQueryChange({ ...parsedQuery, queryType: value })}
|
onChange={(value: VariableQueryType) => onQueryChange({ ...parsedQuery, queryType: value })}
|
||||||
label="Query Type"
|
label="Query Type"
|
||||||
|
inputId={`variable-query-type-${query.refId}`}
|
||||||
/>
|
/>
|
||||||
{hasRegionField && (
|
{hasRegionField && (
|
||||||
<VariableQueryField
|
<VariableQueryField
|
||||||
@ -102,6 +107,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
onChange={(value: string) => onRegionChange(value)}
|
onChange={(value: string) => onRegionChange(value)}
|
||||||
label="Region"
|
label="Region"
|
||||||
isLoading={regionIsLoading}
|
isLoading={regionIsLoading}
|
||||||
|
inputId={`variable-query-region-${query.refId}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasNamespaceField && (
|
{hasNamespaceField && (
|
||||||
@ -110,6 +116,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
options={namespaces}
|
options={namespaces}
|
||||||
onChange={(value: string) => onNamespaceChange(value)}
|
onChange={(value: string) => onNamespaceChange(value)}
|
||||||
label="Namespace"
|
label="Namespace"
|
||||||
|
inputId={`variable-query-namespace-${query.refId}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{parsedQuery.queryType === VariableQueryType.DimensionValues && (
|
{parsedQuery.queryType === VariableQueryType.DimensionValues && (
|
||||||
@ -119,20 +126,26 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
|||||||
options={metrics}
|
options={metrics}
|
||||||
onChange={(value: string) => onQueryChange({ ...parsedQuery, metricName: value })}
|
onChange={(value: string) => onQueryChange({ ...parsedQuery, metricName: value })}
|
||||||
label="Metric"
|
label="Metric"
|
||||||
|
inputId={`variable-query-metric-${query.refId}`}
|
||||||
/>
|
/>
|
||||||
<VariableQueryField
|
<VariableQueryField
|
||||||
value={dimensionKey || null}
|
value={dimensionKey || null}
|
||||||
options={dimensionKeys}
|
options={dimensionKeys}
|
||||||
onChange={(value: string) => onQueryChange({ ...parsedQuery, dimensionKey: value })}
|
onChange={(value: string) => onQueryChange({ ...parsedQuery, dimensionKey: value })}
|
||||||
label="Dimension Key"
|
label="Dimension Key"
|
||||||
|
inputId={`variable-query-dimension-key-${query.refId}`}
|
||||||
/>
|
/>
|
||||||
<VariableTextField
|
<InlineField label="Dimensions" labelWidth={20} tooltip="Dimensions to filter the returned values on">
|
||||||
value={query.dimensionFilters}
|
<Dimensions
|
||||||
tooltip='A JSON object representing dimensions and the values to filter on. Ex. { "filter_name1": [ "filter_value1" ], "filter_name2": [ "*" ] }'
|
query={{ ...parsedQuery, dimensions: parsedQuery.dimensionFilters }}
|
||||||
placeholder='{"key":["value"]}'
|
onChange={(dimensions) => {
|
||||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, dimensionFilters: value })}
|
onChange({ ...parsedQuery, dimensionFilters: dimensions });
|
||||||
label="Filters"
|
}}
|
||||||
/>
|
dimensionKeys={keysForDimensionFilter}
|
||||||
|
disableExpressions={true}
|
||||||
|
datasource={datasource}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{parsedQuery.queryType === VariableQueryType.EBSVolumeIDs && (
|
{parsedQuery.queryType === VariableQueryType.EBSVolumeIDs && (
|
||||||
|
@ -10,9 +10,9 @@ interface VariableQueryFieldProps<T> {
|
|||||||
options: SelectableValue[];
|
options: SelectableValue[];
|
||||||
value: T | null;
|
value: T | null;
|
||||||
label: string;
|
label: string;
|
||||||
|
inputId?: string;
|
||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
inputId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VariableQueryField = <T extends string | VariableQueryType>({
|
export const VariableQueryField = <T extends string | VariableQueryType>({
|
||||||
@ -22,9 +22,10 @@ export const VariableQueryField = <T extends string | VariableQueryType>({
|
|||||||
options,
|
options,
|
||||||
allowCustomValue = false,
|
allowCustomValue = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
inputId = label,
|
||||||
}: VariableQueryFieldProps<T>) => {
|
}: VariableQueryFieldProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<InlineField label={label} labelWidth={LABEL_WIDTH} htmlFor={'inline-field'}>
|
<InlineField label={label} labelWidth={LABEL_WIDTH} htmlFor={inputId}>
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
@ -34,7 +35,7 @@ export const VariableQueryField = <T extends string | VariableQueryType>({
|
|||||||
onChange={({ value }) => onChange(value!)}
|
onChange={({ value }) => onChange(value!)}
|
||||||
options={options}
|
options={options}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
inputId="inline-field"
|
inputId={inputId}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,6 @@ import { InlineField, Input } from '@grafana/ui';
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
|
|
||||||
const LABEL_WIDTH = 20;
|
const LABEL_WIDTH = 20;
|
||||||
const TEXT_WIDTH = 100;
|
|
||||||
|
|
||||||
interface VariableTextFieldProps {
|
interface VariableTextFieldProps {
|
||||||
onBlur: (value: string) => void;
|
onBlur: (value: string) => void;
|
||||||
@ -15,14 +14,13 @@ interface VariableTextFieldProps {
|
|||||||
export const VariableTextField: FC<VariableTextFieldProps> = ({ label, onBlur, placeholder, value, tooltip }) => {
|
export const VariableTextField: FC<VariableTextFieldProps> = ({ label, onBlur, placeholder, value, tooltip }) => {
|
||||||
const [localValue, setLocalValue] = useState(value);
|
const [localValue, setLocalValue] = useState(value);
|
||||||
return (
|
return (
|
||||||
<InlineField label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip}>
|
<InlineField label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip} grow>
|
||||||
<Input
|
<Input
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={localValue}
|
value={localValue}
|
||||||
onChange={(e) => setLocalValue(e.currentTarget.value)}
|
onChange={(e) => setLocalValue(e.currentTarget.value)}
|
||||||
onBlur={() => onBlur(localValue)}
|
onBlur={() => onBlur(localValue)}
|
||||||
width={TEXT_WIDTH}
|
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export { Dimensions } from './MetricStatEditor/Dimensions';
|
export { Dimensions } from './Dimensions/Dimensions';
|
||||||
export { QueryInlineField, QueryField } from './Forms';
|
export { QueryInlineField, QueryField } from './Forms';
|
||||||
export { Alias } from './Alias';
|
export { Alias } from './Alias';
|
||||||
export { PanelQueryEditor } from './PanelQueryEditor';
|
export { PanelQueryEditor } from './PanelQueryEditor';
|
||||||
|
@ -206,7 +206,7 @@ describe('migration', () => {
|
|||||||
expect(query.namespace).toBe('AWS/RDS');
|
expect(query.namespace).toBe('AWS/RDS');
|
||||||
expect(query.metricName).toBe('CPUUtilization');
|
expect(query.metricName).toBe('CPUUtilization');
|
||||||
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
||||||
expect(query.dimensionFilters).toBe('');
|
expect(query.dimensionFilters).toStrictEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('and filter param is defined by user', () => {
|
describe('and filter param is defined by user', () => {
|
||||||
@ -219,9 +219,18 @@ describe('migration', () => {
|
|||||||
expect(query.namespace).toBe('AWS/RDS');
|
expect(query.namespace).toBe('AWS/RDS');
|
||||||
expect(query.metricName).toBe('CPUUtilization');
|
expect(query.metricName).toBe('CPUUtilization');
|
||||||
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
||||||
expect(query.dimensionFilters).toBe('{"InstanceId":"$instance_id"}');
|
expect(query.dimensionFilters).toStrictEqual({ InstanceId: '$instance_id' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('when resource_arns query is used', () => {
|
||||||
|
it('should parse the query', () => {
|
||||||
|
const query = migrateVariableQuery('resource_arns(us-east-1,rds:db,{"environment":["$environment"]})');
|
||||||
|
expect(query.queryType).toBe(VariableQueryType.ResourceArns);
|
||||||
|
expect(query.region).toBe('us-east-1');
|
||||||
|
expect(query.resourceType).toBe('rds:db');
|
||||||
|
expect(query.tags).toBe('{"environment":["$environment"]}');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -80,7 +80,7 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
|
|||||||
region: '',
|
region: '',
|
||||||
metricName: '',
|
metricName: '',
|
||||||
dimensionKey: '',
|
dimensionKey: '',
|
||||||
dimensionFilters: '',
|
dimensionFilters: {},
|
||||||
ec2Filters: '',
|
ec2Filters: '',
|
||||||
instanceID: '',
|
instanceID: '',
|
||||||
attributeName: '',
|
attributeName: '',
|
||||||
@ -122,7 +122,14 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
|
|||||||
newQuery.namespace = dimensionValuesQuery[2];
|
newQuery.namespace = dimensionValuesQuery[2];
|
||||||
newQuery.metricName = dimensionValuesQuery[3];
|
newQuery.metricName = dimensionValuesQuery[3];
|
||||||
newQuery.dimensionKey = dimensionValuesQuery[4];
|
newQuery.dimensionKey = dimensionValuesQuery[4];
|
||||||
newQuery.dimensionFilters = dimensionValuesQuery[6] || '';
|
newQuery.dimensionFilters = {};
|
||||||
|
if (!!dimensionValuesQuery[6]) {
|
||||||
|
try {
|
||||||
|
newQuery.dimensionFilters = JSON.parse(dimensionValuesQuery[6]);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`unable to migrate poorly formed filters: ${dimensionValuesQuery[6]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
return newQuery;
|
return newQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +155,7 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
|
|||||||
newQuery.queryType = VariableQueryType.ResourceArns;
|
newQuery.queryType = VariableQueryType.ResourceArns;
|
||||||
newQuery.region = resourceARNsQuery[1];
|
newQuery.region = resourceARNsQuery[1];
|
||||||
newQuery.resourceType = resourceARNsQuery[2];
|
newQuery.resourceType = resourceARNsQuery[2];
|
||||||
newQuery.tags = JSON.parse(resourceARNsQuery[3]) || '';
|
newQuery.tags = resourceARNsQuery[3] || '';
|
||||||
return newQuery;
|
return newQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,13 @@ export interface SQLExpression {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DimensionsQuery extends DataQuery {
|
||||||
|
namespace: string;
|
||||||
|
region: string;
|
||||||
|
metricName?: string;
|
||||||
|
dimensions?: Dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CloudWatchMetricsQuery extends DataQuery {
|
export interface CloudWatchMetricsQuery extends DataQuery {
|
||||||
queryMode?: 'Metrics';
|
queryMode?: 'Metrics';
|
||||||
metricQueryType?: MetricQueryType;
|
metricQueryType?: MetricQueryType;
|
||||||
@ -386,7 +393,7 @@ export interface VariableQuery extends DataQuery {
|
|||||||
region: string;
|
region: string;
|
||||||
metricName: string;
|
metricName: string;
|
||||||
dimensionKey: string;
|
dimensionKey: string;
|
||||||
dimensionFilters: string;
|
dimensionFilters?: Dimensions;
|
||||||
ec2Filters: string;
|
ec2Filters: string;
|
||||||
instanceID: string;
|
instanceID: string;
|
||||||
attributeName: string;
|
attributeName: string;
|
||||||
|
172
public/app/plugins/datasource/cloudwatch/variables.test.ts
Normal file
172
public/app/plugins/datasource/cloudwatch/variables.test.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { VariableQuery, VariableQueryType } from './types';
|
||||||
|
import { CloudWatchVariableSupport } from './variables';
|
||||||
|
import { setupMockedDataSource } from './__mocks__/CloudWatchDataSource';
|
||||||
|
|
||||||
|
const defaultQuery: VariableQuery = {
|
||||||
|
queryType: VariableQueryType.Regions,
|
||||||
|
namespace: 'foo',
|
||||||
|
region: 'bar',
|
||||||
|
metricName: '',
|
||||||
|
dimensionKey: '',
|
||||||
|
ec2Filters: '',
|
||||||
|
instanceID: '',
|
||||||
|
attributeName: '',
|
||||||
|
resourceType: '',
|
||||||
|
tags: '',
|
||||||
|
refId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ds = setupMockedDataSource();
|
||||||
|
ds.datasource.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]);
|
||||||
|
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]);
|
||||||
|
ds.datasource.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]);
|
||||||
|
ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]);
|
||||||
|
const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]);
|
||||||
|
const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]);
|
||||||
|
const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]);
|
||||||
|
const getResourceARNs = jest.fn().mockResolvedValue([{ label: 'h', value: 'h' }]);
|
||||||
|
|
||||||
|
const variables = new CloudWatchVariableSupport(ds.datasource);
|
||||||
|
|
||||||
|
describe('variables', () => {
|
||||||
|
it('should run regions', async () => {
|
||||||
|
const result = await variables.execute({ ...defaultQuery });
|
||||||
|
expect(result).toEqual([{ text: 'a', value: 'a', expandable: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run namespaces', async () => {
|
||||||
|
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.Namespaces });
|
||||||
|
expect(result).toEqual([{ text: 'b', value: 'b', expandable: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run metrics', async () => {
|
||||||
|
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.Metrics });
|
||||||
|
expect(result).toEqual([{ text: 'c', value: 'c', expandable: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run dimension keys', async () => {
|
||||||
|
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.DimensionKeys });
|
||||||
|
expect(result).toEqual([{ text: 'd', value: 'd', expandable: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dimension values', () => {
|
||||||
|
const query = {
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.DimensionValues,
|
||||||
|
metricName: 'abc',
|
||||||
|
dimensionKey: 'efg',
|
||||||
|
dimensionFilters: { a: 'b' },
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
ds.datasource.getDimensionValues = getDimensionValues;
|
||||||
|
getDimensionValues.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if dimension key not set', async () => {
|
||||||
|
const result = await variables.execute({ ...query, dimensionKey: '' });
|
||||||
|
expect(getDimensionValues).not.toBeCalled();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if metric name not set', async () => {
|
||||||
|
const result = await variables.execute({ ...query, metricName: '' });
|
||||||
|
expect(getDimensionValues).not.toBeCalled();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
it('should run if values are set', async () => {
|
||||||
|
const result = await variables.execute(query);
|
||||||
|
expect(getDimensionValues).toBeCalledWith(
|
||||||
|
query.region,
|
||||||
|
query.namespace,
|
||||||
|
query.metricName,
|
||||||
|
query.dimensionKey,
|
||||||
|
query.dimensionFilters
|
||||||
|
);
|
||||||
|
expect(result).toEqual([{ text: 'e', value: 'e', expandable: true }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EBS volume ids', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
ds.datasource.getEbsVolumeIds = getEbsVolumeIds;
|
||||||
|
getEbsVolumeIds.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if instance id not set', async () => {
|
||||||
|
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.EBSVolumeIDs });
|
||||||
|
expect(getEbsVolumeIds).not.toBeCalled();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run if instance id set', async () => {
|
||||||
|
const result = await variables.execute({
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.EBSVolumeIDs,
|
||||||
|
instanceID: 'foo',
|
||||||
|
});
|
||||||
|
expect(getEbsVolumeIds).toBeCalledWith(defaultQuery.region, 'foo');
|
||||||
|
expect(result).toEqual([{ text: 'f', value: 'f', expandable: true }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EC2 instance attributes', () => {
|
||||||
|
const query = {
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||||
|
attributeName: 'abc',
|
||||||
|
ec2Filters: '{"a":["b"]}',
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
ds.datasource.getEc2InstanceAttribute = getEc2InstanceAttribute;
|
||||||
|
getEc2InstanceAttribute.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if instance id not set', async () => {
|
||||||
|
const result = await variables.execute({ ...query, attributeName: '' });
|
||||||
|
expect(getEc2InstanceAttribute).not.toBeCalled();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run if instance id set', async () => {
|
||||||
|
const result = await variables.execute(query);
|
||||||
|
expect(getEc2InstanceAttribute).toBeCalledWith(query.region, query.attributeName, { a: ['b'] });
|
||||||
|
expect(result).toEqual([{ text: 'g', value: 'g', expandable: true }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resource arns', () => {
|
||||||
|
const query = {
|
||||||
|
...defaultQuery,
|
||||||
|
queryType: VariableQueryType.ResourceArns,
|
||||||
|
resourceType: 'abc',
|
||||||
|
tags: '{"a":["b"]}',
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
ds.datasource.getResourceARNs = getResourceARNs;
|
||||||
|
getResourceARNs.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run if instance id not set', async () => {
|
||||||
|
const result = await variables.execute({ ...query, resourceType: '' });
|
||||||
|
expect(getResourceARNs).not.toBeCalled();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run if instance id set', async () => {
|
||||||
|
const result = await variables.execute(query);
|
||||||
|
expect(getResourceARNs).toBeCalledWith(query.region, query.resourceType, { a: ['b'] });
|
||||||
|
expect(result).toEqual([{ text: 'h', value: 'h', expandable: true }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run statistics', async () => {
|
||||||
|
const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.Statistics });
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ text: 'Average', value: 'Average', expandable: true },
|
||||||
|
{ text: 'Maximum', value: 'Maximum', expandable: true },
|
||||||
|
{ text: 'Minimum', value: 'Minimum', expandable: true },
|
||||||
|
{ text: 'Sum', value: 'Sum', expandable: true },
|
||||||
|
{ text: 'SampleCount', value: 'SampleCount', expandable: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -91,11 +91,13 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
|
|||||||
if (!dimensionKey || !metricName) {
|
if (!dimensionKey || !metricName) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
var filterJson = {};
|
const keys = await this.datasource.getDimensionValues(
|
||||||
if (dimensionFilters) {
|
region,
|
||||||
filterJson = JSON.parse(dimensionFilters);
|
namespace,
|
||||||
}
|
metricName,
|
||||||
const keys = await this.datasource.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson);
|
dimensionKey,
|
||||||
|
dimensionFilters ?? {}
|
||||||
|
);
|
||||||
return keys.map((s: { label: string; value: string }) => ({
|
return keys.map((s: { label: string; value: string }) => ({
|
||||||
text: s.label,
|
text: s.label,
|
||||||
value: s.value,
|
value: s.value,
|
||||||
@ -119,7 +121,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
|
|||||||
if (!attributeName) {
|
if (!attributeName) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
var filterJson = {};
|
let filterJson = {};
|
||||||
if (ec2Filters) {
|
if (ec2Filters) {
|
||||||
filterJson = JSON.parse(ec2Filters);
|
filterJson = JSON.parse(ec2Filters);
|
||||||
}
|
}
|
||||||
@ -135,7 +137,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
|
|||||||
if (!resourceType) {
|
if (!resourceType) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
var tagJson = {};
|
let tagJson = {};
|
||||||
if (tags) {
|
if (tags) {
|
||||||
tagJson = JSON.parse(tags);
|
tagJson = JSON.parse(tags);
|
||||||
}
|
}
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -4135,9 +4135,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@grafana/experimental@npm:0.0.2-canary.22":
|
"@grafana/experimental@npm:^0.0.2-canary.25":
|
||||||
version: 0.0.2-canary.22
|
version: 0.0.2-canary.25
|
||||||
resolution: "@grafana/experimental@npm:0.0.2-canary.22"
|
resolution: "@grafana/experimental@npm:0.0.2-canary.25"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/uuid": ^8.3.3
|
"@types/uuid": ^8.3.3
|
||||||
uuid: ^8.3.2
|
uuid: ^8.3.2
|
||||||
@ -4145,7 +4145,7 @@ __metadata:
|
|||||||
"@emotion/css": 11.1.3
|
"@emotion/css": 11.1.3
|
||||||
react: 17.0.1
|
react: 17.0.1
|
||||||
react-select: 5.2.1
|
react-select: 5.2.1
|
||||||
checksum: b9a64c0abc33798967c94e82e329925f75661eb23b4bbaf4d34fc0c95db1a535b95a240deb6e95fe08f2a2207859e599daf51b560813cf0e5c85468fa6d7a5cc
|
checksum: 20532d6a1ff1bb7a98db71728bc34474f87663431fef349a53f4673d12b6356336b9250bc85028693e7bbfeec8e468d9910837f4062cb49239d581dc974c55c9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -20424,7 +20424,7 @@ __metadata:
|
|||||||
"@grafana/e2e": "workspace:*"
|
"@grafana/e2e": "workspace:*"
|
||||||
"@grafana/e2e-selectors": "workspace:*"
|
"@grafana/e2e-selectors": "workspace:*"
|
||||||
"@grafana/eslint-config": 3.0.0
|
"@grafana/eslint-config": 3.0.0
|
||||||
"@grafana/experimental": 0.0.2-canary.22
|
"@grafana/experimental": ^0.0.2-canary.25
|
||||||
"@grafana/google-sdk": 0.0.3
|
"@grafana/google-sdk": 0.0.3
|
||||||
"@grafana/lezer-logql": ^0.0.11
|
"@grafana/lezer-logql": ^0.0.11
|
||||||
"@grafana/runtime": "workspace:*"
|
"@grafana/runtime": "workspace:*"
|
||||||
|
Loading…
Reference in New Issue
Block a user