AzureMonitor: Update UI to experimental package (#52123)

* feat: make azure experimental the default

* feat: combine metrics query editor rows

fix: linter errors

* chore: remove test loop for DimensionFields test
This commit is contained in:
Adam Simpson 2022-07-14 09:07:31 -04:00 committed by GitHub
parent c8b5307c61
commit 5d199a40b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 751 additions and 1256 deletions

View File

@ -49,7 +49,6 @@ export interface FeatureToggles {
commandPalette?: boolean;
cloudWatchDynamicLabels?: boolean;
datasourceQueryMultiStatus?: boolean;
azureMonitorExperimentalUI?: boolean;
traceToMetrics?: boolean;
prometheusStreamingJSONParser?: boolean;
validateDashboardsOnSave?: boolean;

View File

@ -187,12 +187,6 @@ var (
Description: "Introduce HTTP 207 Multi Status for api/ds/query",
State: FeatureStateAlpha,
},
{
Name: "azureMonitorExperimentalUI",
Description: "Use grafana-experimental UI in Azure Monitor",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "traceToMetrics",
Description: "Enable trace to metrics links",

View File

@ -139,10 +139,6 @@ const (
// Introduce HTTP 207 Multi Status for api/ds/query
FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus"
// FlagAzureMonitorExperimentalUI
// Use grafana-experimental UI in Azure Monitor
FlagAzureMonitorExperimentalUI = "azureMonitorExperimentalUI"
// FlagTraceToMetrics
// Enable trace to metrics links
FlagTraceToMetrics = "traceToMetrics"

View File

@ -1,8 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { InlineFieldRow } from '@grafana/ui';
import Datasource from '../../datasource';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
@ -54,62 +52,34 @@ const ArgQueryEditor: React.FC<ArgQueryEditorProps> = ({
.catch((err) => setError(ERROR_SOURCE, err));
}, [datasource, onChange, query, setError]);
if (config.featureToggles.azureMonitorExperimentalUI) {
return (
<span data-testid="azure-monitor-arg-query-editor-with-experimental-ui">
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<SubscriptionField
multiSelect
subscriptions={subscriptions}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</EditorFieldGroup>
</EditorRow>
</EditorRows>
<QueryField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</span>
);
} else {
return (
<div data-testid="azure-monitor-arg-query-editor">
<InlineFieldRow>
<SubscriptionField
multiSelect
subscriptions={subscriptions}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</InlineFieldRow>
<QueryField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</div>
);
}
return (
<span data-testid="azure-monitor-arg-query-editor-with-experimental-ui">
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<SubscriptionField
multiSelect
subscriptions={subscriptions}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</EditorFieldGroup>
</EditorRow>
</EditorRows>
<QueryField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</span>
);
};
export default ArgQueryEditor;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { EditorField } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { InlineField } from '@grafana/ui';
import { Props as InlineFieldProps } from '@grafana/ui/src/components/Forms/InlineField';
@ -16,7 +15,7 @@ const DEFAULT_LABEL_WIDTH = 18;
export const Field = (props: Props) => {
const { labelWidth, inlineField, ...remainingProps } = props;
if (config.featureToggles.azureMonitorExperimentalUI && !inlineField) {
if (!inlineField) {
return <EditorField width={labelWidth || DEFAULT_LABEL_WIDTH} {...remainingProps} />;
} else {
return <InlineField labelWidth={labelWidth || DEFAULT_LABEL_WIDTH} {...remainingProps} />;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import Datasource from '../../datasource';
@ -35,81 +34,32 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
}) => {
const migrationError = useMigrations(datasource, query, onChange);
if (config.featureToggles.azureMonitorExperimentalUI) {
return (
<span data-testid="azure-monitor-logs-query-editor-with-experimental-ui">
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<ResourceField
query={query}
datasource={datasource}
inlineField={true}
labelWidth={10}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
setResource={setResource}
resourceUri={query.azureLogAnalytics?.resource}
queryType="logs"
/>
</EditorFieldGroup>
</EditorRow>
<QueryField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<EditorRow>
<EditorFieldGroup>
{!hideFormatAs && (
<FormatAsField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
)}
{migrationError && <Alert title={migrationError.title}>{migrationError.message}</Alert>}
</EditorFieldGroup>
</EditorRow>
</EditorRows>
</span>
);
} else {
return (
<div data-testid="azure-monitor-logs-query-editor">
<ResourceField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
setResource={setResource}
resourceUri={query.azureLogAnalytics?.resource}
queryType="logs"
/>
return (
<span data-testid="azure-monitor-logs-query-editor-with-experimental-ui">
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<ResourceField
query={query}
datasource={datasource}
inlineField={true}
labelWidth={10}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
setResource={setResource}
resourceUri={query.azureLogAnalytics?.resource}
queryType="logs"
/>
</EditorFieldGroup>
</EditorRow>
<QueryField
query={query}
datasource={datasource}
@ -118,22 +68,25 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
onQueryChange={onChange}
setError={setError}
/>
<EditorRow>
<EditorFieldGroup>
{!hideFormatAs && (
<FormatAsField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
)}
{!hideFormatAs && (
<FormatAsField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
)}
{migrationError && <Alert title={migrationError.title}>{migrationError.message}</Alert>}
</div>
);
}
{migrationError && <Alert title={migrationError.title}>{migrationError.message}</Alert>}
</EditorFieldGroup>
</EditorRow>
</EditorRows>
</span>
);
};
export default LogsQueryEditor;

View File

@ -44,7 +44,6 @@ const AggregationField: React.FC<AggregationFieldProps> = ({
value={query.azureMonitor?.aggregation}
onChange={handleChange}
options={options}
width={38}
isLoading={isLoading}
/>
</Field>

View File

@ -9,7 +9,6 @@ import createMockPanelData from '../../__mocks__/panelData';
import createMockQuery from '../../__mocks__/query';
import DimensionFields from './DimensionFields';
import NewDimensionFields from './NewDimensionFields';
import { appendDimensionFilter, setDimensionFilterValue } from './setQueryValue';
const variableOptionGroup = {
@ -18,365 +17,346 @@ const variableOptionGroup = {
};
const user = userEvent.setup();
const tests = [
{
component: DimensionFields,
label: 'Dimension Fields',
addDimension: async () => {
const addDimension = await screen.findByText('Add new dimension');
await user.click(addDimension);
},
},
{
component: NewDimensionFields,
label: 'Dimension Fields experimental UI',
addDimension: async () => {
const addDimension = await screen.findByLabelText('Add');
await user.click(addDimension);
},
},
];
describe(`Azure Monitor QueryEditor`, () => {
const mockDatasource = createMockDatasource();
for (const t of tests) {
describe(`Azure Monitor QueryEditor: ${t.label}`, () => {
const mockDatasource = createMockDatasource();
it('should render a dimension filter', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
const onQueryChange = jest.fn();
const dimensionOptions = [
{ label: 'Test Dimension 1', value: 'TestDimension1' },
{ label: 'Test Dimension 2', value: 'TestDimension2' },
];
const { rerender } = render(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
it('should render a dimension filter', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
const onQueryChange = jest.fn();
const dimensionOptions = [
{ label: 'Test Dimension 1', value: 'TestDimension1' },
{ label: 'Test Dimension 2', value: 'TestDimension2' },
];
const { rerender } = render(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const addDimension = await screen.findByLabelText('Add');
await user.click(addDimension);
await t.addDimension();
mockQuery = appendDimensionFilter(mockQuery);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }],
},
});
rerender(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const dimensionSelect = await screen.findByText('Field');
await selectOptionInTest(dimensionSelect, 'Test Dimension 1');
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
},
});
expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument();
expect(screen.queryByText('==')).toBeInTheDocument();
mockQuery = appendDimensionFilter(mockQuery);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }],
},
});
it('correctly filters out dimensions when selected', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockQuery.azureMonitor = {
rerender(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const dimensionSelect = await screen.findByText('Field');
await selectOptionInTest(dimensionSelect, 'Test Dimension 1');
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
};
const onQueryChange = jest.fn();
const dimensionOptions = [
{ label: 'Test Dimension 1', value: 'TestDimension1' },
{ label: 'Test Dimension 2', value: 'TestDimension2' },
];
const { rerender } = render(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
await t.addDimension();
mockQuery = appendDimensionFilter(mockQuery);
rerender(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const dimensionSelect = await screen.findByText('Field');
await user.click(dimensionSelect);
const options = await screen.findAllByLabelText('Select option');
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('Test Dimension 2');
},
});
expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument();
expect(screen.queryByText('==')).toBeInTheDocument();
});
it('correctly displays dimension labels', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockQuery.azureMonitor = {
it('correctly filters out dimensions when selected', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockQuery.azureMonitor = {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
};
const onQueryChange = jest.fn();
const dimensionOptions = [
{ label: 'Test Dimension 1', value: 'TestDimension1' },
{ label: 'Test Dimension 2', value: 'TestDimension2' },
];
const { rerender } = render(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const addDimension = await screen.findByLabelText('Add');
await user.click(addDimension);
mockQuery = appendDimensionFilter(mockQuery);
rerender(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const dimensionSelect = await screen.findByText('Field');
await user.click(dimensionSelect);
const options = await screen.findAllByLabelText('Select option');
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('Test Dimension 2');
});
it('correctly displays dimension labels', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockQuery.azureMonitor = {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
};
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
];
const onQueryChange = jest.fn();
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
render(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = await screen.findByText('Select value(s)');
await user.click(labelSelect);
const options = await screen.findAllByLabelText('Select option');
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('testlabel');
});
it('correctly updates dimension labels', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockQuery.azureMonitor = {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }],
};
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
];
const onQueryChange = jest.fn();
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
const { rerender } = render(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
await screen.findByText('testlabel');
const labelClear = await screen.findByLabelText('Remove testlabel');
await user.click(labelClear);
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
};
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
];
const onQueryChange = jest.fn();
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
render(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = await screen.findByText('Select value(s)');
await user.click(labelSelect);
const options = await screen.findAllByLabelText('Select option');
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('testlabel');
},
});
mockPanelData.series = [
...mockPanelData.series,
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel2' },
},
],
},
];
rerender(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = screen.getByLabelText('dimension-labels-select');
await openMenu(labelSelect);
const options = await screen.findAllByLabelText('Select option');
expect(options).toHaveLength(2);
expect(options[0]).toHaveTextContent('testlabel');
expect(options[1]).toHaveTextContent('testlabel2');
});
it('correctly updates dimension labels', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockQuery.azureMonitor = {
it('correctly selects multiple dimension labels', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel2' },
},
],
},
];
const onQueryChange = jest.fn();
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
mockQuery = appendDimensionFilter(mockQuery, 'TestDimension1');
const { rerender } = render(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = screen.getByLabelText('dimension-labels-select');
await user.click(labelSelect);
await openMenu(labelSelect);
screen.getByText('testlabel');
screen.getByText('testlabel2');
await selectOptionInTest(labelSelect, 'testlabel');
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel']);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }],
};
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
];
const onQueryChange = jest.fn();
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
const { rerender } = render(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
await screen.findByText('testlabel');
const labelClear = await screen.findByLabelText('Remove testlabel');
await user.click(labelClear);
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }],
},
});
mockPanelData.series = [
...mockPanelData.series,
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel2' },
},
],
},
];
rerender(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = screen.getByLabelText('dimension-labels-select');
await openMenu(labelSelect);
const options = await screen.findAllByLabelText('Select option');
expect(options).toHaveLength(2);
expect(options[0]).toHaveTextContent('testlabel');
expect(options[1]).toHaveTextContent('testlabel2');
},
});
it('correctly selects multiple dimension labels', async () => {
let mockQuery = createMockQuery();
const mockPanelData = createMockPanelData();
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel2' },
},
],
},
];
const onQueryChange = jest.fn();
const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }];
mockQuery = appendDimensionFilter(mockQuery, 'TestDimension1');
const { rerender } = render(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect = screen.getByLabelText('dimension-labels-select');
await user.click(labelSelect);
await openMenu(labelSelect);
screen.getByText('testlabel');
screen.getByText('testlabel2');
await selectOptionInTest(labelSelect, 'testlabel');
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel']);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }],
},
});
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
];
rerender(
<t.component
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect2 = screen.getByLabelText('dimension-labels-select');
await openMenu(labelSelect2);
const refreshedOptions = await screen.findAllByLabelText('Select options menu');
expect(refreshedOptions).toHaveLength(1);
expect(refreshedOptions[0]).toHaveTextContent('testlabel2');
await selectOptionInTest(labelSelect2, 'testlabel2');
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel', 'testlabel2']);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel', 'testlabel2'] }],
},
});
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel2' },
},
],
},
];
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
];
rerender(
<DimensionFields
data={mockPanelData}
subscriptionId="123"
query={mockQuery}
onQueryChange={onQueryChange}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
setError={() => {}}
dimensionOptions={dimensionOptions}
/>
);
const labelSelect2 = screen.getByLabelText('dimension-labels-select');
await openMenu(labelSelect2);
const refreshedOptions = await screen.findAllByLabelText('Select options menu');
expect(refreshedOptions).toHaveLength(1);
expect(refreshedOptions[0]).toHaveTextContent('testlabel2');
await selectOptionInTest(labelSelect2, 'testlabel2');
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel', 'testlabel2']);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel', 'testlabel2'] }],
},
});
mockPanelData.series = [
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel' },
},
],
},
{
...mockPanelData.series[0],
fields: [
{
...mockPanelData.series[0].fields[0],
name: 'Test Dimension 1',
labels: { testdimension1: 'testlabel2' },
},
],
},
];
});
}
});

View File

@ -1,12 +1,13 @@
import React, { useEffect, useMemo, useState } from 'react';
import { SelectableValue, DataFrame, PanelData } from '@grafana/data';
import { Button, Select, HorizontalGroup, VerticalGroup, MultiSelect } from '@grafana/ui';
import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data';
import { AccessoryButton, EditorList } from '@grafana/experimental';
import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui';
import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types';
import { Field } from '../Field';
import { appendDimensionFilter, removeDimensionFilter, setDimensionFilterValue } from './setQueryValue';
import { setDimensionFilters } from './setQueryValue';
interface DimensionFieldsProps extends AzureQueryEditorFieldProps {
dimensionOptions: AzureMonitorOption[];
@ -28,16 +29,14 @@ const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuer
const labels = fields
.map((fields) => fields.labels)
.flat()
.filter((item) => item!);
.filter((item): item is Labels => item !== null && item !== undefined);
for (const label of labels) {
// Labels only exist for series that have a dimension selected
if (label) {
for (const [dimension, value] of Object.entries(label)) {
if (labelsObj[dimension]) {
labelsObj[dimension].add(value);
} else {
labelsObj[dimension] = new Set([value]);
}
for (const [dimension, value] of Object.entries(label)) {
if (labelsObj[dimension]) {
labelsObj[dimension].add(value);
} else {
labelsObj[dimension] = new Set([value]);
}
}
}
@ -87,24 +86,14 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
return t;
}, [dimensionFilters, dimensionOptions]);
const addFilter = () => {
onQueryChange(appendDimensionFilter(query));
};
const removeFilter = (index: number) => {
onQueryChange(removeDimensionFilter(query, index));
};
const onFieldChange = <Key extends keyof AzureMetricDimension>(
filterIndex: number,
fieldName: Key,
value: AzureMetricDimension[Key]
item: Partial<AzureMetricDimension>,
value: AzureMetricDimension[Key],
onChange: (item: Partial<AzureMetricDimension>) => void
) => {
onQueryChange(setDimensionFilterValue(query, filterIndex, fieldName, value));
};
const onFilterInputChange = (index: number, v: SelectableValue<string> | null) => {
onFieldChange(index, 'filters', [v?.value ?? '']);
item[fieldName] = value;
onChange(item);
};
const getValidDimensionOptions = (selectedDimension: string) => {
@ -133,7 +122,6 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
}
return labelOptions;
};
const getValidOperators = (selectedOperator: string) => {
if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) {
return dimensionOperators;
@ -141,70 +129,76 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensio
return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])];
};
const onMultiSelectFilterChange = (index: number, v: Array<SelectableValue<string>>) => {
onFieldChange(
index,
'filters',
v.map((item) => item.value || '')
const changedFunc = (changed: Array<Partial<AzureMetricDimension>>) => {
const properData: AzureMetricDimension[] = changed.map((x) => {
return {
dimension: x.dimension ?? '',
operator: x.operator ?? 'eq',
filters: x.filters ?? [],
};
});
onQueryChange(setDimensionFilters(query, properData));
};
const renderFilters = (
item: Partial<AzureMetricDimension>,
onChange: (item: Partial<AzureMetricDimension>) => void,
onDelete: () => void
) => {
return (
<HorizontalGroup spacing="none">
<Select
menuShouldPortal
placeholder="Field"
value={item.dimension}
options={getValidDimensionOptions(item.dimension || '')}
onChange={(e) => onFieldChange('dimension', item, e.value ?? '', onChange)}
/>
<Select
menuShouldPortal
placeholder="Operation"
value={item.operator}
options={getValidOperators(item.operator || 'eq')}
onChange={(e) => onFieldChange('operator', item, e.value ?? '', onChange)}
allowCustomValue
/>
{item.operator === 'eq' || item.operator === 'ne' ? (
<MultiSelect
menuShouldPortal
placeholder="Select value(s)"
value={item.filters}
options={getValidMultiSelectOptions(item.filters, item.dimension ?? '')}
onChange={(e) =>
onFieldChange(
'filters',
item,
e.map((x) => x.value ?? ''),
onChange
)
}
aria-label={'dimension-labels-select'}
allowCustomValue
/>
) : (
// The API does not currently allow for multiple "starts with" clauses to be used.
<Select
menuShouldPortal
placeholder="Select value"
value={item.filters ? item.filters[0] : ''}
allowCustomValue
options={getValidFilterOptions(item.filters ? item.filters[0] : '', item.dimension ?? '')}
onChange={(e) => onFieldChange('filters', item, [e?.value ?? ''], onChange)}
isClearable
/>
)}
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
</HorizontalGroup>
);
};
return (
<Field label="Dimension">
<VerticalGroup spacing="xs">
{dimensionFilters.map((filter, index) => (
<HorizontalGroup key={index} spacing="xs">
<Select
placeholder="Field"
value={filter.dimension}
options={getValidDimensionOptions(filter.dimension)}
onChange={(v) => onFieldChange(index, 'dimension', v.value ?? '')}
width={38}
/>
<Select
menuShouldPortal
placeholder="Operation"
value={filter.operator}
options={getValidOperators(filter.operator)}
onChange={(v) => onFieldChange(index, 'operator', v.value ?? '')}
allowCustomValue
/>
{filter.operator === 'eq' || filter.operator === 'ne' ? (
<MultiSelect
menuShouldPortal
placeholder="Select value(s)"
value={filter.filters}
options={getValidMultiSelectOptions(filter.filters, filter.dimension)}
onChange={(v) => onMultiSelectFilterChange(index, v)}
aria-label={'dimension-labels-select'}
allowCustomValue
/>
) : (
// The API does not currently allow for multiple "starts with" clauses to be used.
<Select
menuShouldPortal
placeholder="Select value"
value={filter.filters ? filter.filters[0] : ''}
allowCustomValue
options={getValidFilterOptions(filter.filters ? filter.filters[0] : '', filter.dimension)}
onChange={(v) => onFilterInputChange(index, v)}
isClearable
/>
)}
<Button
variant="secondary"
size="md"
icon="trash-alt"
aria-label="Remove"
onClick={() => removeFilter(index)}
></Button>
</HorizontalGroup>
))}
<Button variant="secondary" size="md" onClick={addFilter}>
Add new dimension
</Button>
</VerticalGroup>
<Field label="Dimensions">
<EditorList items={dimensionFilters} onChange={changedFunc} renderItem={renderFilters} />
</Field>
);
};

View File

@ -34,7 +34,6 @@ const MetricNameField: React.FC<MetricNameProps> = ({ metricNames, query, variab
value={query.azureMonitor?.metricName ?? null}
onChange={handleChange}
options={options}
width={38}
allowCustomValue
/>
</Field>

View File

@ -46,7 +46,6 @@ const MetricNamespaceField: React.FC<MetricNamespaceFieldProps> = ({
value={query.azureMonitor?.metricNamespace}
onChange={handleChange}
options={options}
width={38}
allowCustomValue
/>
</Field>

View File

@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { config } from '@grafana/runtime';
import createMockDatasource from '../../__mocks__/datasource';
import { createMockInstanceSetttings } from '../../__mocks__/instanceSettings';
import createMockPanelData from '../../__mocks__/panelData';
@ -23,15 +21,6 @@ const variableOptionGroup = {
options: [],
};
const tests = [
{
id: 'azure-monitor-metrics-query-editor-with-resource-picker',
},
{
id: 'azure-monitor-metrics-query-editor-with-experimental-ui',
},
];
export function createMockResourcePickerData() {
const mockDatasource = new ResourcePickerData(createMockInstanceSetttings());
@ -46,215 +35,210 @@ export function createMockResourcePickerData() {
return mockDatasource;
}
for (const t of tests) {
describe(`MetricsQueryEditor: ${t.id}`, () => {
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
const mockPanelData = createMockPanelData();
describe('MetricsQueryEditor', () => {
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
const mockPanelData = createMockPanelData();
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = function () {};
config.featureToggles.azureMonitorExperimentalUI =
t.id === 'azure-monitor-metrics-query-editor-with-experimental-ui';
});
afterEach(() => {
window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
config.featureToggles.azureMonitorExperimentalUI = false;
});
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = function () {};
});
afterEach(() => {
window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
});
it('should render', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
it('should render', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
render(
<MetricsQueryEditor
data={mockPanelData}
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={() => {}}
setError={() => {}}
/>
);
render(
<MetricsQueryEditor
data={mockPanelData}
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={() => {}}
setError={() => {}}
/>
);
expect(await screen.findByTestId(t.id)).toBeInTheDocument();
});
expect(await screen.findByTestId('azure-monitor-metrics-query-editor-with-experimental-ui')).toBeInTheDocument();
});
it('should change resource when a resource is selected in the ResourcePicker', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.azureMonitor?.resourceUri;
const onChange = jest.fn();
it('should change resource when a resource is selected in the ResourcePicker', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.azureMonitor?.resourceUri;
const onChange = jest.fn();
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
expect(resourcePickerButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
resourcePickerButton.click();
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
expect(resourcePickerButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
expect(subscriptionButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument();
subscriptionButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
expect(subscriptionButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument();
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
expect(resourceGroupButton).toBeInTheDocument();
expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument();
resourceGroupButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
expect(resourceGroupButton).toBeInTheDocument();
expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument();
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('web-server');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
const checkbox = await screen.findByLabelText('web-server');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resourceUri:
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server',
}),
})
);
});
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resourceUri:
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server',
}),
})
);
});
it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
const onChange = jest.fn();
it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
const onChange = jest.fn();
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: /grafana/ });
const resourcePickerButton = await screen.findByRole('button', { name: /grafana/ });
expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument();
expect(screen.getByText('Metric A')).toBeInTheDocument();
expect(screen.getByText('Average')).toBeInTheDocument();
expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument();
expect(screen.getByText('Metric A')).toBeInTheDocument();
expect(screen.getByText('Average')).toBeInTheDocument();
expect(resourcePickerButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
resourcePickerButton.click();
expect(resourcePickerButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' });
expect(subscriptionButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument();
subscriptionButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' });
expect(subscriptionButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument();
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' });
expect(resourceGroupButton).toBeInTheDocument();
expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument();
resourceGroupButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' });
expect(resourceGroupButton).toBeInTheDocument();
expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument();
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('db-server');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
const checkbox = await screen.findByLabelText('db-server');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resourceUri:
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server',
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
dimensionFilters: [],
}),
})
);
});
it('should change the metric name when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const onChange = jest.fn();
const mockQuery = createMockQuery();
mockDatasource.azureMonitorDatasource.getMetricNames = jest.fn().mockResolvedValue([
{
value: 'metric-a',
text: 'Metric A',
},
{
value: 'metric-b',
text: 'Metric B',
},
]);
render(
<MetricsQueryEditor
data={mockPanelData}
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const metrics = await screen.findByLabelText('Metric');
expect(metrics).toBeInTheDocument();
await selectOptionInTest(metrics, 'Metric B');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
metricName: 'metric-b',
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resourceUri:
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server',
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
},
});
});
dimensionFilters: [],
}),
})
);
});
it('should change the aggregation type when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const onChange = jest.fn();
const mockQuery = createMockQuery();
it('should change the metric name when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const onChange = jest.fn();
const mockQuery = createMockQuery();
mockDatasource.azureMonitorDatasource.getMetricNames = jest.fn().mockResolvedValue([
{
value: 'metric-a',
text: 'Metric A',
},
{
value: 'metric-b',
text: 'Metric B',
},
]);
render(
<MetricsQueryEditor
data={mockPanelData}
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
render(
<MetricsQueryEditor
data={mockPanelData}
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const aggregation = await screen.findByLabelText('Aggregation');
expect(aggregation).toBeInTheDocument();
await selectOptionInTest(aggregation, 'Maximum');
const metrics = await screen.findByLabelText('Metric');
expect(metrics).toBeInTheDocument();
await selectOptionInTest(metrics, 'Metric B');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
aggregation: 'Maximum',
},
});
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
metricName: 'metric-b',
aggregation: undefined,
timeGrain: '',
},
});
});
}
it('should change the aggregation type when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const onChange = jest.fn();
const mockQuery = createMockQuery();
render(
<MetricsQueryEditor
data={mockPanelData}
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const aggregation = await screen.findByLabelText('Aggregation');
expect(aggregation).toBeInTheDocument();
await selectOptionInTest(aggregation, 'Maximum');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
aggregation: 'Maximum',
},
});
});
});

View File

@ -1,10 +1,7 @@
import { css } from '@emotion/css';
import React from 'react';
import { PanelData } from '@grafana/data/src/types';
import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { InlineFieldRow, useStyles2 } from '@grafana/ui';
import type Datasource from '../../datasource';
import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types';
@ -16,7 +13,6 @@ import DimensionFields from './DimensionFields';
import LegendFormatField from './LegendFormatField';
import MetricNameField from './MetricNameField';
import MetricNamespaceField from './MetricNamespaceField';
import NewDimensionFields from './NewDimensionFields';
import TimeGrainField from './TimeGrainField';
import TopField from './TopField';
import { useMetricNames, useMetricNamespaces, useMetricMetadata } from './dataHooks';
@ -39,187 +35,94 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
onChange,
setError,
}) => {
const styles = useStyles2(getStyles);
const metricsMetadata = useMetricMetadata(query, datasource, onChange);
const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError);
const metricNames = useMetricNames(query, datasource, onChange, setError);
if (config.featureToggles.azureMonitorExperimentalUI) {
return (
<span data-testid="azure-monitor-metrics-query-editor-with-experimental-ui">
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<ResourceField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[ResourceRowType.Resource]}
setResource={setResource}
resourceUri={query.azureMonitor?.resourceUri}
queryType={'metrics'}
/>
<MetricNamespaceField
metricNamespaces={metricNamespaces}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<MetricNameField
metricNames={metricNames}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</EditorFieldGroup>
</EditorRow>
<EditorRow>
<EditorFieldGroup>
<AggregationField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
aggregationOptions={metricsMetadata?.aggOptions ?? []}
isLoading={metricsMetadata.isLoading}
/>
<TimeGrainField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
timeGrainOptions={metricsMetadata?.timeGrains ?? []}
/>
</EditorFieldGroup>
</EditorRow>
<EditorRow>
<EditorFieldGroup>
<NewDimensionFields
data={data}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
dimensionOptions={metricsMetadata?.dimensions ?? []}
/>
</EditorFieldGroup>
</EditorRow>
<EditorRow>
<EditorFieldGroup>
<TopField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<LegendFormatField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</EditorFieldGroup>
</EditorRow>
</EditorRows>
</span>
);
} else {
return (
<div data-testid="azure-monitor-metrics-query-editor-with-resource-picker">
<InlineFieldRow className={styles.row}>
<ResourceField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[ResourceRowType.Resource]}
setResource={setResource}
resourceUri={query.azureMonitor?.resourceUri}
queryType="metrics"
/>
</InlineFieldRow>
<InlineFieldRow className={styles.row}>
<MetricNamespaceField
metricNamespaces={metricNamespaces}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<MetricNameField
metricNames={metricNames}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</InlineFieldRow>
<InlineFieldRow className={styles.row}>
<AggregationField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
aggregationOptions={metricsMetadata?.aggOptions ?? []}
isLoading={metricsMetadata.isLoading}
/>
<TimeGrainField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
timeGrainOptions={metricsMetadata?.timeGrains ?? []}
/>
</InlineFieldRow>
<DimensionFields
data={data}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
dimensionOptions={metricsMetadata?.dimensions ?? []}
/>
<TopField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<LegendFormatField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</div>
);
}
return (
<span data-testid="azure-monitor-metrics-query-editor-with-experimental-ui">
<EditorRows>
<EditorRow>
<EditorFieldGroup>
<ResourceField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[ResourceRowType.Resource]}
setResource={setResource}
resourceUri={query.azureMonitor?.resourceUri}
queryType={'metrics'}
/>
<MetricNamespaceField
metricNamespaces={metricNamespaces}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<MetricNameField
metricNames={metricNames}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<AggregationField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
aggregationOptions={metricsMetadata?.aggOptions ?? []}
isLoading={metricsMetadata.isLoading}
/>
<TimeGrainField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
timeGrainOptions={metricsMetadata?.timeGrains ?? []}
/>
</EditorFieldGroup>
</EditorRow>
<EditorRow>
<EditorFieldGroup>
<DimensionFields
data={data}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
dimensionOptions={metricsMetadata?.dimensions ?? []}
/>
</EditorFieldGroup>
</EditorRow>
<EditorRow>
<EditorFieldGroup>
<TopField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<LegendFormatField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</EditorFieldGroup>
</EditorRow>
</EditorRows>
</span>
);
};
const getStyles = () => ({
row: css({
rowGap: 0,
}),
});
export default MetricsQueryEditor;

View File

@ -1,206 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data';
import { AccessoryButton, EditorList } from '@grafana/experimental';
import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui';
import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types';
import { Field } from '../Field';
import { setDimensionFilters } from './setQueryValue';
interface DimensionFieldsProps extends AzureQueryEditorFieldProps {
dimensionOptions: AzureMonitorOption[];
}
interface DimensionLabels {
[key: string]: Set<string>;
}
const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuery) => {
const [dimensionLabels, setDimensionLabels] = useState<DimensionLabels>({});
useEffect(() => {
let labelsObj: DimensionLabels = {};
if (data?.series?.length) {
// Identify which series' in the dataframe are relevant to the current query
const series: DataFrame[] = data.series.flat().filter((series) => series.refId === query.refId);
const fields = series.flatMap((series) => series.fields);
// Retrieve labels for series fields
const labels = fields
.map((fields) => fields.labels)
.flat()
.filter((item): item is Labels => item !== null && item !== undefined);
for (const label of labels) {
// Labels only exist for series that have a dimension selected
for (const [dimension, value] of Object.entries(label)) {
if (labelsObj[dimension]) {
labelsObj[dimension].add(value);
} else {
labelsObj[dimension] = new Set([value]);
}
}
}
}
setDimensionLabels((prevLabels) => {
const newLabels: DimensionLabels = {};
const currentLabels = Object.keys(labelsObj);
if (currentLabels.length === 0) {
return prevLabels;
}
for (const label of currentLabels) {
if (prevLabels[label] && labelsObj[label].size < prevLabels[label].size) {
newLabels[label] = prevLabels[label];
} else {
newLabels[label] = labelsObj[label];
}
}
return newLabels;
});
}, [data?.series, query.refId]);
return dimensionLabels;
};
const NewDimensionFields: React.FC<DimensionFieldsProps> = ({ data, query, dimensionOptions, onQueryChange }) => {
const dimensionFilters = useMemo(
() => query.azureMonitor?.dimensionFilters ?? [],
[query.azureMonitor?.dimensionFilters]
);
const dimensionLabels = useDimensionLabels(data, query);
const dimensionOperators: Array<SelectableValue<string>> = [
{ label: '==', value: 'eq' },
{ label: '!=', value: 'ne' },
{ label: 'starts with', value: 'sw' },
];
const validDimensionOptions = useMemo(() => {
// We filter out any dimensions that have already been used in a filter as the API doesn't support having multiple filters with the same dimension name.
// The Azure portal also doesn't support this feature so it makes sense for consistency.
let t = dimensionOptions;
if (dimensionFilters.length) {
t = dimensionOptions.filter(
(val) => !dimensionFilters.some((dimensionFilter) => dimensionFilter.dimension === val.value)
);
}
return t;
}, [dimensionFilters, dimensionOptions]);
const onFieldChange = <Key extends keyof AzureMetricDimension>(
fieldName: Key,
item: Partial<AzureMetricDimension>,
value: AzureMetricDimension[Key],
onChange: (item: Partial<AzureMetricDimension>) => void
) => {
item[fieldName] = value;
onChange(item);
};
const getValidDimensionOptions = (selectedDimension: string) => {
return validDimensionOptions.concat(dimensionOptions.filter((item) => item.value === selectedDimension));
};
const getValidFilterOptions = (selectedFilter: string | undefined, dimension: string) => {
const dimensionFilters = Array.from(dimensionLabels[dimension.toLowerCase()] ?? []);
if (dimensionFilters.find((filter) => filter === selectedFilter)) {
return dimensionFilters.map((filter) => ({ value: filter, label: filter }));
}
return [...dimensionFilters, ...(selectedFilter && selectedFilter !== '*' ? [selectedFilter] : [])].map((item) => ({
value: item,
label: item,
}));
};
const getValidMultiSelectOptions = (selectedFilters: string[] | undefined, dimension: string) => {
const labelOptions = getValidFilterOptions(undefined, dimension);
if (selectedFilters) {
for (const filter of selectedFilters) {
if (!labelOptions.find((label) => label.value === filter)) {
labelOptions.push({ value: filter, label: filter });
}
}
}
return labelOptions;
};
const getValidOperators = (selectedOperator: string) => {
if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) {
return dimensionOperators;
}
return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])];
};
const changedFunc = (changed: Array<Partial<AzureMetricDimension>>) => {
const properData: AzureMetricDimension[] = changed.map((x) => {
return {
dimension: x.dimension ?? '',
operator: x.operator ?? 'eq',
filters: x.filters ?? [],
};
});
onQueryChange(setDimensionFilters(query, properData));
};
const renderFilters = (
item: Partial<AzureMetricDimension>,
onChange: (item: Partial<AzureMetricDimension>) => void,
onDelete: () => void
) => {
return (
<HorizontalGroup spacing="none">
<Select
menuShouldPortal
placeholder="Field"
value={item.dimension}
options={getValidDimensionOptions(item.dimension || '')}
onChange={(e) => onFieldChange('dimension', item, e.value ?? '', onChange)}
/>
<Select
menuShouldPortal
placeholder="Operation"
value={item.operator}
options={getValidOperators(item.operator || 'eq')}
onChange={(e) => onFieldChange('operator', item, e.value ?? '', onChange)}
allowCustomValue
/>
{item.operator === 'eq' || item.operator === 'ne' ? (
<MultiSelect
menuShouldPortal
placeholder="Select value(s)"
value={item.filters}
options={getValidMultiSelectOptions(item.filters, item.dimension ?? '')}
onChange={(e) =>
onFieldChange(
'filters',
item,
e.map((x) => x.value ?? ''),
onChange
)
}
aria-label={'dimension-labels-select'}
allowCustomValue
/>
) : (
// The API does not currently allow for multiple "starts with" clauses to be used.
<Select
menuShouldPortal
placeholder="Select value"
value={item.filters ? item.filters[0] : ''}
allowCustomValue
options={getValidFilterOptions(item.filters ? item.filters[0] : '', item.dimension ?? '')}
onChange={(e) => onFieldChange('filters', item, [e?.value ?? ''], onChange)}
isClearable
/>
)}
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
</HorizontalGroup>
);
};
return (
<Field label="Dimensions">
<EditorList items={dimensionFilters} onChange={changedFunc} renderItem={renderFilters} />
</Field>
);
};
export default NewDimensionFields;

View File

@ -58,7 +58,6 @@ const TimeGrainField: React.FC<TimeGrainFieldProps> = ({
value={query.azureMonitor?.timeGrain}
onChange={handleChange}
options={timeGrains}
width={38}
/>
</Field>
);

View File

@ -2,7 +2,6 @@ import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { config } from '@grafana/runtime';
import * as ui from '@grafana/ui';
import createMockDatasource from '../../__mocks__/datasource';
@ -30,7 +29,7 @@ describe('Azure Monitor QueryEditor', () => {
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
await waitFor(() =>
expect(screen.getByTestId('azure-monitor-metrics-query-editor-with-resource-picker')).toBeInTheDocument()
expect(screen.getByTestId('azure-monitor-metrics-query-editor-with-experimental-ui')).toBeInTheDocument()
);
});
@ -42,7 +41,9 @@ describe('Azure Monitor QueryEditor', () => {
};
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
await waitFor(() => expect(screen.queryByTestId('azure-monitor-logs-query-editor')).toBeInTheDocument());
await waitFor(() =>
expect(screen.queryByTestId('azure-monitor-logs-query-editor-with-experimental-ui')).toBeInTheDocument()
);
});
it('changes the query type when selected', async () => {
@ -52,7 +53,7 @@ describe('Azure Monitor QueryEditor', () => {
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={onChange} onRunQuery={() => {}} />);
await waitFor(() => expect(screen.getByTestId('azure-monitor-query-editor')).toBeInTheDocument());
const metrics = await screen.findByLabelText('Service');
const metrics = await screen.findByLabelText(/Service/);
await selectOptionInTest(metrics, 'Logs');
expect(onChange).toHaveBeenCalledWith({
@ -68,16 +69,12 @@ describe('Azure Monitor QueryEditor', () => {
<QueryEditor query={createMockQuery()} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />
);
await waitFor(() =>
expect(screen.getByTestId('azure-monitor-metrics-query-editor-with-resource-picker')).toBeInTheDocument()
expect(screen.getByTestId('azure-monitor-metrics-query-editor-with-experimental-ui')).toBeInTheDocument()
);
expect(screen.getByText('An error occurred while requesting metadata from Azure Monitor')).toBeInTheDocument();
});
it('should render the experimental QueryHeader when feature toggle is enabled', async () => {
const originalConfigValue = config.featureToggles.azureMonitorExperimentalUI;
config.featureToggles.azureMonitorExperimentalUI = true;
const mockDatasource = createMockDatasource();
const mockQuery = {
...createMockQuery(),
@ -87,19 +84,5 @@ describe('Azure Monitor QueryEditor', () => {
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
await waitFor(() => expect(screen.getByTestId('azure-monitor-experimental-header')).toBeInTheDocument());
config.featureToggles.azureMonitorExperimentalUI = originalConfigValue;
});
it('should not render the experimental QueryHeader when feature toggle is disabled', async () => {
const mockDatasource = createMockDatasource();
const mockQuery = {
...createMockQuery(),
queryType: AzureQueryType.AzureMonitor,
};
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
await waitFor(() => expect(screen.queryByTestId('azure-monitor-experimental-header')).not.toBeInTheDocument());
});
});

View File

@ -2,7 +2,6 @@ import { debounce } from 'lodash';
import React, { useCallback, useMemo } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Alert, CodeEditor } from '@grafana/ui';
import AzureMonitorDatasource from '../../datasource';
@ -20,7 +19,6 @@ import NewMetricsQueryEditor from '../MetricsQueryEditor/MetricsQueryEditor';
import { QueryHeader } from '../QueryHeader';
import { Space } from '../Space';
import QueryTypeField from './QueryTypeField';
import usePreparedQuery from './usePreparedQuery';
export type AzureMonitorQueryEditorProps = QueryEditorProps<
@ -57,10 +55,7 @@ const QueryEditor: React.FC<AzureMonitorQueryEditorProps> = ({
return (
<div data-testid="azure-monitor-query-editor">
{config.featureToggles.azureMonitorExperimentalUI && <QueryHeader query={query} onQueryChange={onQueryChange} />}
{!config.featureToggles.azureMonitorExperimentalUI && (
<QueryTypeField query={query} onQueryChange={onQueryChange} />
)}
<QueryHeader query={query} onQueryChange={onQueryChange} />
<EditorForQueryType
data={data}

View File

@ -1,45 +0,0 @@
import React, { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { AzureMonitorQuery, AzureQueryType } from '../../types';
import { Field } from '../Field';
interface QueryTypeFieldProps {
query: AzureMonitorQuery;
onQueryChange: (newQuery: AzureMonitorQuery) => void;
}
const QueryTypeField: React.FC<QueryTypeFieldProps> = ({ query, onQueryChange }) => {
const queryTypes: Array<{ value: AzureQueryType; label: string }> = [
{ value: AzureQueryType.AzureMonitor, label: 'Metrics' },
{ value: AzureQueryType.LogAnalytics, label: 'Logs' },
{ value: AzureQueryType.AzureResourceGraph, label: 'Azure Resource Graph' },
];
const handleChange = useCallback(
(change: SelectableValue<AzureQueryType>) => {
change.value &&
onQueryChange({
...query,
queryType: change.value,
});
},
[onQueryChange, query]
);
return (
<Field label="Service">
<Select
inputId="azure-monitor-query-type-field"
value={query.queryType}
options={queryTypes}
onChange={handleChange}
width={38}
/>
</Field>
);
};
export default QueryTypeField;