CloudMonitor: Remove cloudMonitoringExperimentalUI feature flag (#55054)

* CloudMonitor: remove cloudMonitoringExperimentalUI

* fix: address typecheck errors

* fix: fix SLO import and width cleanup

* fix wrong metricType when switching datasources

* fix: remove legacy SLO and fix queryType check
This commit is contained in:
Adam Simpson 2022-09-15 10:12:26 -04:00 committed by GitHub
parent 13014dc0df
commit 92857ef331
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 345 additions and 2024 deletions

View File

@ -5846,30 +5846,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/Aggregation.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/Aggregation.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/AliasBy.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/GroupBy.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/MetricQueryEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Experimental/VisualMetricQueryEditor.tsx:5381": [
"public/app/plugins/datasource/cloud-monitoring/components/GroupBy.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [
@ -5880,10 +5857,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -76,7 +76,6 @@ export interface QueryResultMeta {
/**
* Legacy data source specific, should be moved to custom
* */
alignmentPeriod?: number; // used by cloud monitoring
searchWords?: string[]; // used by log models and loki
limit?: number; // used by log models and loki
json?: boolean; // used to keep track of old json doc values

View File

@ -57,7 +57,6 @@ export interface FeatureToggles {
canvasPanelNesting?: boolean;
scenes?: boolean;
useLegacyHeatmapPanel?: boolean;
cloudMonitoringExperimentalUI?: boolean;
disableSecretsCompatibility?: boolean;
logRequestsInstrumentedAsUnknown?: boolean;
dataConnectionsConsole?: boolean;

View File

@ -232,12 +232,6 @@ var (
Description: "Continue to use the angular/flot based heatmap panel",
State: FeatureStateStable,
},
{
Name: "cloudMonitoringExperimentalUI",
Description: "Use grafana-experimental UI in Cloud Monitoring",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "disableSecretsCompatibility",
Description: "Disable duplicated secret storage in legacy tables",

View File

@ -171,10 +171,6 @@ const (
// Continue to use the angular/flot based heatmap panel
FlagUseLegacyHeatmapPanel = "useLegacyHeatmapPanel"
// FlagCloudMonitoringExperimentalUI
// Use grafana-experimental UI in Cloud Monitoring
FlagCloudMonitoringExperimentalUI = "cloudMonitoringExperimentalUI"
// FlagDisableSecretsCompatibility
// Disable duplicated secret storage in legacy tables
FlagDisableSecretsCompatibility = "disableSecretsCompatibility"

View File

@ -1,13 +1,11 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { EditorField, Select } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { MetricDescriptor, MetricKind, ValueTypes } from '../types';
import { QueryEditorField } from '.';
export interface Props {
refId: string;
onChange: (metricDescriptor: string) => void;
@ -22,14 +20,9 @@ export const Aggregation: FC<Props> = (props) => {
const selected = useSelectedFromOptions(aggOptions, props);
return (
<QueryEditorField
labelWidth={18}
label="Group by function"
data-testid="cloud-monitoring-aggregation"
htmlFor={`${props.refId}-group-by-function`}
>
<EditorField label="Group by function" data-testid="cloud-monitoring-aggregation">
<Select
width={16}
width="auto"
onChange={({ value }) => props.onChange(value!)}
value={selected}
options={[
@ -46,7 +39,7 @@ export const Aggregation: FC<Props> = (props) => {
placeholder="Select Reducer"
inputId={`${props.refId}-group-by-function`}
/>
</QueryEditorField>
</EditorField>
);
};

View File

@ -1,11 +1,7 @@
import { debounce } from 'lodash';
import React, { FunctionComponent, useState } from 'react';
import { Input } from '@grafana/ui';
import { INPUT_WIDTH } from '../constants';
import { QueryEditorRow } from '.';
import { EditorField, Input } from '@grafana/ui';
export interface Props {
refId: string;
@ -24,8 +20,8 @@ export const AliasBy: FunctionComponent<Props> = ({ refId, value = '', onChange
};
return (
<QueryEditorRow label="Alias by" htmlFor={`${refId}-alias-by`}>
<Input id={`${refId}-alias-by`} width={INPUT_WIDTH} value={alias} onChange={onChange} />
</QueryEditorRow>
<EditorField label="Alias by">
<Input id={`${refId}-alias-by`} value={alias} onChange={onChange} />
</EditorField>
);
};

View File

@ -5,9 +5,9 @@ import { openMenu } from 'react-select-event';
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
import { createMockDatasource } from '../../__mocks__/cloudMonitoringDatasource';
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery';
import { MetricKind, ValueTypes } from '../../types';
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
import { MetricKind, ValueTypes } from '../types';
import { Alignment } from './Alignment';

View File

@ -1,12 +1,15 @@
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup } from '@grafana/ui';
import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../constants';
import { ALIGNMENT_PERIODS } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { alignmentPeriodLabel } from '../functions';
import { CustomMetaData, MetricQuery, SLOQuery } from '../types';
import { AlignmentFunction, PeriodSelect, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.';
import { AlignmentFunction } from './AlignmentFunction';
import { PeriodSelect } from './PeriodSelect';
export interface Props {
refId: string;
@ -25,29 +28,29 @@ export const Alignment: FC<Props> = ({
customMetaData,
datasource,
}) => {
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
return (
<QueryEditorRow
label="Alignment function"
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
fillComponent={<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />}
htmlFor={`${refId}-alignment-function`}
>
<AlignmentFunction
inputId={`${refId}-alignment-function`}
templateVariableOptions={templateVariableOptions}
query={query}
onChange={onChange}
/>
<QueryEditorField label="Alignment period" htmlFor={`${refId}-alignment-period`}>
<EditorFieldGroup>
<EditorField
label="Alignment function"
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
>
<AlignmentFunction
inputId={`${refId}-alignment-function`}
templateVariableOptions={templateVariableOptions}
query={query}
onChange={onChange}
/>
</EditorField>
<EditorField label="Alignment period" tooltip={alignmentLabel}>
<PeriodSelect
inputId={`${refId}-alignment-period`}
selectWidth={SELECT_WIDTH}
templateVariableOptions={templateVariableOptions}
current={query.alignmentPeriod}
onChange={(period) => onChange({ ...query, alignmentPeriod: period })}
aligmentPeriods={ALIGNMENT_PERIODS}
/>
</QueryEditorField>
</QueryEditorRow>
</EditorField>
</EditorFieldGroup>
);
};

View File

@ -3,7 +3,6 @@ import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { SELECT_WIDTH } from '../constants';
import { getAlignmentPickerData } from '../functions';
import { MetricQuery } from '../types';
@ -23,7 +22,6 @@ export const AlignmentFunction: FC<Props> = ({ inputId, query, templateVariableO
return (
<Select
width={SELECT_WIDTH}
onChange={({ value }) => onChange({ ...query, perSeriesAligner: value! })}
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
options={[
@ -39,6 +37,6 @@ export const AlignmentFunction: FC<Props> = ({ inputId, query, templateVariableO
]}
placeholder="Select Alignment"
inputId={inputId}
></Select>
/>
);
};

View File

@ -2,10 +2,8 @@ import React, { useState } from 'react';
import { useDebounce } from 'react-use';
import { QueryEditorProps, toOption } from '@grafana/data';
import { config } from '@grafana/runtime';
import { EditorField, EditorRows, Input } from '@grafana/ui';
import { INPUT_WIDTH } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import {
EditorMode,
@ -16,10 +14,9 @@ import {
AlignmentTypes,
} from '../types';
import { MetricQueryEditor as ExperimentalMetricQueryEditor } from './Experimental/MetricQueryEditor';
import { MetricQueryEditor } from './MetricQueryEditor';
import { AnnotationsHelp, QueryEditorRow } from './';
import { AnnotationsHelp } from './';
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
@ -80,44 +77,23 @@ export const AnnotationQueryEditor = (props: Props) => {
return (
<EditorRows>
{config.featureToggles.cloudMonitoringExperimentalUI ? (
<>
<ExperimentalMetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
customMetaData={customMetaData}
onChange={handleQueryChange}
onRunQuery={onRunQuery}
datasource={datasource}
query={metricQuery}
/>
<EditorField label="Title" htmlFor="annotation-query-title">
<Input id="annotation-query-title" value={title} onChange={handleTitleChange} />
</EditorField>
<EditorField label="Text" htmlFor="annotation-query-text">
<Input id="annotation-query-text" value={text} onChange={handleTextChange} />
</EditorField>
</>
) : (
<>
<MetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
customMetaData={customMetaData}
onChange={handleQueryChange}
onRunQuery={onRunQuery}
datasource={datasource}
query={metricQuery}
/>
<QueryEditorRow label="Title" htmlFor="annotation-query-title">
<Input id="annotation-query-title" value={title} width={INPUT_WIDTH} onChange={handleTitleChange} />
</QueryEditorRow>
<QueryEditorRow label="Text" htmlFor="annotation-query-text">
<Input id="annotation-query-text" value={text} width={INPUT_WIDTH} onChange={handleTextChange} />
</QueryEditorRow>
</>
)}
<>
<MetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
customMetaData={customMetaData}
onChange={handleQueryChange}
onRunQuery={onRunQuery}
datasource={datasource}
query={metricQuery}
/>
<EditorField label="Title" htmlFor="annotation-query-title">
<Input id="annotation-query-title" value={title} onChange={handleTitleChange} />
</EditorField>
<EditorField label="Text" htmlFor="annotation-query-text">
<Input id="annotation-query-text" value={text} onChange={handleTextChange} />
</EditorField>
</>
<AnnotationsHelp />
</EditorRows>
);

View File

@ -1,65 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { openMenu } from 'react-select-event';
import { TemplateSrvStub } from 'test/specs/helpers';
import { ValueTypes, MetricKind } from '../../types';
import { Aggregation, Props } from './Aggregation';
const props: Props = {
onChange: () => {},
// @ts-ignore
templateSrv: new TemplateSrvStub(),
metricDescriptor: {
valueType: '',
metricKind: '',
} as any,
crossSeriesReducer: '',
groupBys: [],
templateVariableOptions: [],
};
describe('Aggregation', () => {
it('renders correctly', () => {
render(<Aggregation {...props} />);
expect(screen.getByTestId('cloud-monitoring-aggregation')).toBeInTheDocument();
});
describe('options', () => {
describe('when DOUBLE and GAUGE is passed as props', () => {
const nextProps = {
...props,
metricDescriptor: {
valueType: ValueTypes.DOUBLE,
metricKind: MetricKind.GAUGE,
} as any,
};
it('should not have the reduce values', () => {
render(<Aggregation {...nextProps} />);
const label = screen.getByLabelText('Group by function');
openMenu(label);
expect(screen.queryByText('count true')).not.toBeInTheDocument();
expect(screen.queryByText('count false')).not.toBeInTheDocument();
});
});
describe('when MONEY and CUMULATIVE is passed as props', () => {
const nextProps = {
...props,
metricDescriptor: {
valueType: ValueTypes.MONEY,
metricKind: MetricKind.CUMULATIVE,
} as any,
};
it('should have the reduce values', () => {
render(<Aggregation {...nextProps} />);
const label = screen.getByLabelText('Group by function');
openMenu(label);
expect(screen.getByText('none')).toBeInTheDocument();
});
});
});
});

View File

@ -1,67 +0,0 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, Select } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../../functions';
import { MetricDescriptor, MetricKind, ValueTypes } from '../../types';
export interface Props {
refId: string;
onChange: (metricDescriptor: string) => void;
metricDescriptor?: MetricDescriptor;
crossSeriesReducer: string;
groupBys: string[];
templateVariableOptions: Array<SelectableValue<string>>;
}
export const Aggregation: FC<Props> = (props) => {
const aggOptions = useAggregationOptionsByMetric(props);
const selected = useSelectedFromOptions(aggOptions, props);
return (
<EditorField label="Group by function" data-testid="cloud-monitoring-aggregation">
<Select
width="auto"
onChange={({ value }) => props.onChange(value!)}
value={selected}
options={[
{
label: 'Template Variables',
options: props.templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
inputId={`${props.refId}-group-by-function`}
/>
</EditorField>
);
};
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
const valueType = metricDescriptor?.valueType;
const metricKind = metricDescriptor?.metricKind;
return useMemo(() => {
if (!valueType || !metricKind) {
return [];
}
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
...a,
label: a.text,
}));
}, [valueType, metricKind]);
};
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
return useMemo(() => {
const allOptions = [...aggOptions, ...props.templateVariableOptions];
return allOptions.find((s) => s.value === props.crossSeriesReducer);
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
};

View File

@ -1,29 +0,0 @@
import { debounce } from 'lodash';
import React, { FunctionComponent, useState } from 'react';
import { EditorField, Input } from '@grafana/ui';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
refId: string;
onChange: (alias: any) => void;
value?: string;
}
export const AliasBy: FunctionComponent<Props> = ({ refId, value = '', onChange }) => {
const [alias, setAlias] = useState(value ?? '');
const propagateOnChange = debounce(onChange, 1000);
onChange = (e: any) => {
setAlias(e.target.value);
propagateOnChange(e.target.value);
};
return (
<EditorField label="Alias by">
<Input id={`${refId}-alias-by`} width={SELECT_WIDTH} value={alias} onChange={onChange} />
</EditorField>
);
};

View File

@ -1,56 +0,0 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup } from '@grafana/ui';
import { ALIGNMENT_PERIODS } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { alignmentPeriodLabel } from '../../functions';
import { CustomMetaData, MetricQuery, SLOQuery } from '../../types';
import { AlignmentFunction } from './AlignmentFunction';
import { PeriodSelect } from './PeriodSelect';
export interface Props {
refId: string;
onChange: (query: MetricQuery | SLOQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const Alignment: FC<Props> = ({
refId,
templateVariableOptions,
onChange,
query,
customMetaData,
datasource,
}) => {
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
return (
<EditorFieldGroup>
<EditorField
label="Alignment function"
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
>
<AlignmentFunction
inputId={`${refId}-alignment-function`}
templateVariableOptions={templateVariableOptions}
query={query}
onChange={onChange}
/>
</EditorField>
<EditorField label="Alignment period" tooltip={alignmentLabel}>
<PeriodSelect
inputId={`${refId}-alignment-period`}
templateVariableOptions={templateVariableOptions}
current={query.alignmentPeriod}
onChange={(period) => onChange({ ...query, alignmentPeriod: period })}
aligmentPeriods={ALIGNMENT_PERIODS}
/>
</EditorField>
</EditorFieldGroup>
);
};

View File

@ -1,42 +0,0 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { getAlignmentPickerData } from '../../functions';
import { MetricQuery } from '../../types';
export interface Props {
inputId: string;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const AlignmentFunction: FC<Props> = ({ inputId, query, templateVariableOptions, onChange }) => {
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
const { perSeriesAligner, alignOptions } = useMemo(
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
[valueType, metricKind, psa, preprocessor]
);
return (
<Select
onChange={({ value }) => onChange({ ...query, perSeriesAligner: value! })}
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Alignment options',
expanded: true,
options: alignOptions,
},
]}
placeholder="Select Alignment"
inputId={inputId}
/>
);
};

View File

@ -1,39 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { select } from 'react-select-event';
import { GraphPeriod, Props } from './GraphPeriod';
const props: Props = {
onChange: jest.fn(),
refId: 'A',
variableOptionGroup: { options: [] },
};
describe('Graph Period', () => {
it('should enable graph_period by default', () => {
render(<GraphPeriod {...props} />);
expect(screen.getByLabelText('Graph period')).not.toBeDisabled();
});
it('should disable graph_period when toggled', async () => {
const onChange = jest.fn();
render(<GraphPeriod {...props} onChange={onChange} />);
const s = screen.getByTestId('A-switch-graph-period');
await userEvent.click(s);
expect(onChange).toHaveBeenCalledWith('disabled');
});
it('should set a different value when selected', async () => {
const onChange = jest.fn();
render(<GraphPeriod {...props} onChange={onChange} />);
const selectEl = screen.getByLabelText('Graph period');
expect(selectEl).toBeInTheDocument();
await select(selectEl, '1m', {
container: document.body,
});
expect(onChange).toHaveBeenCalledWith('1m');
});
});

View File

@ -1,48 +0,0 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorRow, HorizontalGroup, Switch } from '@grafana/ui';
import { GRAPH_PERIODS, SELECT_WIDTH } from '../../constants';
import { PeriodSelect } from '../index';
export interface Props {
refId: string;
onChange: (period: string) => void;
variableOptionGroup: SelectableValue<string>;
graphPeriod?: string;
}
export const GraphPeriod: FunctionComponent<Props> = ({ refId, onChange, graphPeriod, variableOptionGroup }) => {
return (
<EditorRow>
<EditorField
label="Graph period"
htmlFor={`${refId}-graph-period`}
tooltip={
<>
Set <code>graph_period</code> which forces a preferred period between points. Automatically set to the
current interval if left blank.
</>
}
>
<HorizontalGroup>
<Switch
data-testid={`${refId}-switch-graph-period`}
value={graphPeriod !== 'disabled'}
onChange={(e) => onChange(e.currentTarget.checked ? '' : 'disabled')}
/>
<PeriodSelect
inputId={`${refId}-graph-period`}
templateVariableOptions={variableOptionGroup.options}
current={graphPeriod}
onChange={onChange}
selectWidth={SELECT_WIDTH}
disabled={graphPeriod === 'disabled'}
aligmentPeriods={GRAPH_PERIODS}
/>
</HorizontalGroup>
</EditorField>
</EditorRow>
);
};

View File

@ -1,61 +0,0 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, MultiSelect } from '@grafana/ui';
import { SYSTEM_LABELS } from '../../constants';
import { labelsToGroupedOptions } from '../../functions';
import { MetricDescriptor, MetricQuery } from '../../types';
import { Aggregation } from './Aggregation';
export interface Props {
refId: string;
variableOptionGroup: SelectableValue<string>;
labels: string[];
metricDescriptor?: MetricDescriptor;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
}
export const GroupBy: FunctionComponent<Props> = ({
refId,
labels: groupBys = [],
query,
onChange,
variableOptionGroup,
metricDescriptor,
}) => {
const options = useMemo(
() => [variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...SYSTEM_LABELS])],
[groupBys, variableOptionGroup]
);
return (
<EditorFieldGroup>
<EditorField
label="Group by"
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series."
>
<MultiSelect
inputId={`${refId}-group-by`}
width="auto"
placeholder="Choose label"
options={options}
value={query.groupBys ?? []}
onChange={(options) => {
onChange({ ...query, groupBys: options.map((o) => o.value!) });
}}
/>
</EditorField>
<Aggregation
metricDescriptor={metricDescriptor}
templateVariableOptions={variableOptionGroup.options}
crossSeriesReducer={query.crossSeriesReducer}
groupBys={query.groupBys ?? []}
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
refId={refId}
/>
</EditorFieldGroup>
);
};

View File

@ -1,118 +0,0 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { AccessoryButton, EditorField, EditorList, EditorRow, HorizontalGroup, Select } from '@grafana/ui';
import { labelsToGroupedOptions, stringArrayToFilters } from '../../functions';
export interface Props {
labels: { [key: string]: string[] };
filters: string[];
onChange: (filters: string[]) => void;
variableOptionGroup: SelectableValue<string>;
}
interface Filter {
key: string;
operator: string;
value: string;
condition: string;
}
const DEFAULT_OPERATOR = '=';
const DEFAULT_CONDITION = 'AND';
const filtersToStringArray = (filters: Filter[]) =>
filters.flatMap(({ key, operator, value, condition }) => [key, operator, value, condition]).slice(0, -1);
const operators = ['=', '!=', '=~', '!=~'].map(toOption);
export const LabelFilter: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange: _onChange,
variableOptionGroup,
}) => {
const filters: Filter[] = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
const options = useMemo(
() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))],
[labels, variableOptionGroup]
);
const getOptions = ({ key = '', value = '' }: Partial<Filter>) => {
// Add the current key and value as options if they are manually entered
const keyPresent = options.some((op) => {
if (op.options) {
return options.some((opp) => opp.label === key);
}
return op.label === key;
});
if (!keyPresent) {
options.push({ label: key, value: key });
}
const valueOptions = labels.hasOwnProperty(key)
? [variableOptionGroup, ...labels[key].map(toOption)]
: [variableOptionGroup];
const valuePresent = valueOptions.some((op) => op.label === value);
if (!valuePresent) {
valueOptions.push({ label: value, value });
}
return { options, valueOptions };
};
const onChange = (items: Array<Partial<Filter>>) => {
const filters = items.map(({ key, operator, value, condition }) => ({
key: key || '',
operator: operator || DEFAULT_OPERATOR,
value: value || '',
condition: condition || DEFAULT_CONDITION,
}));
_onChange(filtersToStringArray(filters));
};
const renderItem = (item: Partial<Filter>, onChangeItem: (item: Filter) => void, onDeleteItem: () => void) => {
const { key = '', operator = DEFAULT_OPERATOR, value = '', condition = DEFAULT_CONDITION } = item;
const { options, valueOptions } = getOptions(item);
return (
<HorizontalGroup spacing="xs" width="auto">
<Select
aria-label="Filter label key"
formatCreateLabel={(v) => `Use label key: ${v}`}
allowCustomValue
value={key}
options={options}
onChange={({ value: key = '' }) => onChangeItem({ key, operator, value, condition })}
/>
<Select
value={operator}
options={operators}
onChange={({ value: operator = DEFAULT_OPERATOR }) => onChangeItem({ key, operator, value, condition })}
/>
<Select
aria-label="Filter label value"
placeholder="add filter value"
formatCreateLabel={(v) => `Use label value: ${v}`}
allowCustomValue
value={value}
options={valueOptions}
onChange={({ value = '' }) => onChangeItem({ key, operator, value, condition })}
/>
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDeleteItem} type="button" />
</HorizontalGroup>
);
};
return (
<EditorRow>
<EditorField
label="Filter"
tooltip="To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression."
>
<EditorList items={filters} renderItem={renderItem} onChange={onChange} />
</EditorField>
</EditorRow>
);
};

View File

@ -1,40 +0,0 @@
import { render, screen, act } from '@testing-library/react';
import React from 'react';
import { config } from '@grafana/runtime';
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
import { createMockDatasource } from '../../__mocks__/cloudMonitoringDatasource';
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery';
import { MetricQueryEditor, Props } from './MetricQueryEditor';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getTemplateSrv: () => new TemplateSrvMock({}),
}));
const props: Props = {
onChange: jest.fn(),
refId: 'refId',
customMetaData: {},
onRunQuery: jest.fn(),
datasource: createMockDatasource(),
variableOptionGroup: { options: [] },
query: createMockMetricQuery(),
};
describe('Cloud monitoring: Metric Query Editor', () => {
it('shoud render Project selector', async () => {
await act(async () => {
const originalValue = config.featureToggles.cloudMonitoringExperimentalUI;
config.featureToggles.cloudMonitoringExperimentalUI = true;
render(<MetricQueryEditor {...props} />);
expect(screen.getByLabelText('Project')).toBeInTheDocument();
config.featureToggles.cloudMonitoringExperimentalUI = originalValue;
});
});
});

View File

@ -1,140 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorRows } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource';
import { getAlignmentPickerData } from '../../functions';
import {
AlignmentTypes,
CustomMetaData,
EditorMode,
MetricDescriptor,
MetricKind,
MetricQuery,
PreprocessorType,
SLOQuery,
ValueTypes,
} from '../../types';
import { MQLQueryEditor } from './../MQLQueryEditor';
import { GraphPeriod } from './GraphPeriod';
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
export interface Props {
refId: string;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onChange: (query: MetricQuery) => void;
onRunQuery: () => void;
query: MetricQuery;
datasource: CloudMonitoringDatasource;
}
interface State {
labels: any;
[key: string]: any;
}
export const defaultState: State = {
labels: {},
};
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => MetricQuery = (dataSource) => ({
editorMode: EditorMode.Visual,
projectName: dataSource.getDefaultProject(),
metricType: '',
metricKind: MetricKind.GAUGE,
valueType: '',
crossSeriesReducer: 'REDUCE_MEAN',
alignmentPeriod: 'cloud-monitoring-auto',
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
groupBys: [],
filters: [],
aliasBy: '',
query: '',
preprocessor: PreprocessorType.None,
});
function Editor({
refId,
query,
datasource,
onChange: onQueryChange,
onRunQuery,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const [state, setState] = useState<State>(defaultState);
const { projectName, metricType, groupBys, editorMode, crossSeriesReducer } = query;
useEffect(() => {
if (projectName && metricType) {
datasource
.getLabels(metricType, refId, projectName)
.then((labels) => setState((prevState) => ({ ...prevState, labels })));
}
}, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]);
const onChange = useCallback(
(metricQuery: MetricQuery | SLOQuery) => {
onQueryChange({ ...query, ...metricQuery });
onRunQuery();
},
[onQueryChange, onRunQuery, query]
);
const onMetricTypeChange = useCallback(
({ valueType, metricKind, type }: MetricDescriptor) => {
const preprocessor =
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
? PreprocessorType.None
: PreprocessorType.Rate;
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, state.perSeriesAligner, preprocessor);
onChange({
...query,
perSeriesAligner,
metricType: type,
valueType,
metricKind,
preprocessor,
});
},
[onChange, query, state]
);
return (
<EditorRows>
{editorMode === EditorMode.Visual && (
<VisualMetricQueryEditor
refId={refId}
labels={state.labels}
variableOptionGroup={variableOptionGroup}
customMetaData={customMetaData}
onMetricTypeChange={onMetricTypeChange}
onChange={onChange}
datasource={datasource}
query={query}
/>
)}
{editorMode === EditorMode.MQL && (
<>
<MQLQueryEditor
onChange={(q: string) => onQueryChange({ ...query, query: q })}
onRunQuery={onRunQuery}
query={query.query}
></MQLQueryEditor>
<GraphPeriod
onChange={(graphPeriod: string) => onQueryChange({ ...query, graphPeriod })}
graphPeriod={query.graphPeriod}
refId={refId}
variableOptionGroup={variableOptionGroup}
/>
</>
)}
</EditorRows>
);
}
export const MetricQueryEditor = React.memo(Editor);

View File

@ -1,193 +0,0 @@
import { css } from '@emotion/css';
import { startCase, uniqBy } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorRow, getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource';
import { MetricDescriptor, MetricQuery } from '../../types';
import { Project } from './Project';
export interface Props {
refId: string;
onChange: (metricDescriptor: MetricDescriptor) => void;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
projectName: string;
metricType: string;
query: MetricQuery;
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
onProjectChange: (query: MetricQuery) => void;
}
export function Metrics(props: Props) {
const [metricDescriptors, setMetricDescriptors] = useState<MetricDescriptor[]>([]);
const [metricDescriptor, setMetricDescriptor] = useState<MetricDescriptor>();
const [metrics, setMetrics] = useState<Array<SelectableValue<string>>>([]);
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
const [service, setService] = useState<string>('');
const theme = useTheme2();
const selectStyles = getSelectStyles(theme);
const customStyle = useStyles2(getStyles);
const {
onProjectChange,
query,
refId,
metricType,
templateVariableOptions,
projectName,
datasource,
onChange,
children,
} = props;
const { templateSrv } = datasource;
const getSelectedMetricDescriptor = useCallback(
(metricDescriptors: MetricDescriptor[], metricType: string) => {
return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!;
},
[templateSrv]
);
useEffect(() => {
const getMetricsList = (metricDescriptors: MetricDescriptor[]) => {
const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
if (!selectedMetricDescriptor) {
return [];
}
const metricsByService = metricDescriptors
.filter((m) => m.service === selectedMetricDescriptor.service)
.map((m) => ({
service: m.service,
value: m.type,
label: m.displayName,
component: function optionComponent() {
return (
<div>
<div className={customStyle}>{m.type}</div>
<div className={selectStyles.optionDescription}>{m.description}</div>
</div>
);
},
}));
return metricsByService;
};
const loadMetricDescriptors = async () => {
if (projectName) {
const metricDescriptors = await datasource.getMetricTypes(projectName);
const services = getServicesList(metricDescriptors);
const metrics = getMetricsList(metricDescriptors);
const service = metrics.length > 0 ? metrics[0].service : '';
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
setMetricDescriptors(metricDescriptors);
setServices(services);
setMetrics(metrics);
setService(service);
setMetricDescriptor(metricDescriptor);
}
};
loadMetricDescriptors();
}, [datasource, getSelectedMetricDescriptor, metricType, projectName, customStyle, selectStyles.optionDescription]);
const onServiceChange = ({ value: service }: any) => {
const metrics = metricDescriptors
.filter((m: MetricDescriptor) => m.service === templateSrv.replace(service))
.map((m: MetricDescriptor) => ({
service: m.service,
value: m.type,
label: m.displayName,
description: m.description,
}));
if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) {
onMetricTypeChange(metrics[0]);
setService(service);
setMetrics(metrics);
} else {
setService(service);
setMetrics(metrics);
}
};
const onMetricTypeChange = ({ value }: SelectableValue<string>) => {
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!);
setMetricDescriptor(metricDescriptor);
onChange({ ...metricDescriptor, type: value! });
};
const getServicesList = (metricDescriptors: MetricDescriptor[]) => {
const services = metricDescriptors.map((m) => ({
value: m.service,
label: startCase(m.serviceShortName),
}));
return services.length > 0 ? uniqBy(services, (s) => s.value) : [];
};
return (
<>
<EditorRow>
<EditorFieldGroup>
<Project
refId={refId}
templateVariableOptions={templateVariableOptions}
projectName={projectName}
datasource={datasource}
onChange={(projectName) => {
onProjectChange({ ...query, projectName });
}}
/>
<EditorField label="Service" width="auto">
<Select
width="auto"
onChange={onServiceChange}
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
inputId={`${props.refId}-service`}
/>
</EditorField>
<EditorField label="Metric name" width="auto">
<Select
width="auto"
onChange={onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
inputId={`${props.refId}-select-metric`}
/>
</EditorField>
</EditorFieldGroup>
</EditorRow>
{children(metricDescriptor)}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => css`
label: grafana-select-option-description;
font-weight: normal;
font-style: italic;
color: ${theme.colors.text.secondary};
`;

View File

@ -1,59 +0,0 @@
import React, { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { periodOption } from '../../constants';
export interface Props {
inputId: string;
onChange: (period: string) => void;
templateVariableOptions: Array<SelectableValue<string>>;
aligmentPeriods: periodOption[];
selectWidth?: number;
category?: string;
disabled?: boolean;
current?: string;
}
export function PeriodSelect({
inputId,
templateVariableOptions,
onChange,
current,
disabled,
aligmentPeriods,
}: Props) {
const options = useMemo(
() =>
aligmentPeriods.map((ap) => ({
...ap,
label: ap.text,
})),
[aligmentPeriods]
);
const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]);
return (
<Select
width="auto"
onChange={({ value }) => onChange(value!)}
value={[...options, ...templateVariableOptions].find((s) => s.value === current)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: visibleOptions,
},
]}
placeholder="Select Period"
inputId={inputId}
disabled={disabled}
allowCustomValue
/>
);
}

View File

@ -1,66 +0,0 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, RadioButtonGroup } from '@grafana/ui';
import { getAlignmentPickerData } from '../../functions';
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../../types';
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
export interface Props {
metricDescriptor?: MetricDescriptor;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
}
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
const options = useOptions(metricDescriptor);
return (
<EditorField
label="Pre-processing"
tooltip="Preprocessing options are displayed when the selected metric has a metric kind of delta or cumulative. The specific options available are determined by the metic's value type. If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series"
>
<RadioButtonGroup
onChange={(value: PreprocessorType) => {
const { valueType, metricKind, perSeriesAligner: psa } = query;
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value);
onChange({ ...query, preprocessor: value, perSeriesAligner });
}}
value={query.preprocessor ?? PreprocessorType.None}
options={options}
/>
</EditorField>
);
};
const useOptions = (metricDescriptor?: MetricDescriptor): Array<SelectableValue<PreprocessorType>> => {
const metricKind = metricDescriptor?.metricKind;
const valueType = metricDescriptor?.valueType;
return useMemo(() => {
if (!metricKind || metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION) {
return [NONE_OPTION];
}
const options = [
NONE_OPTION,
{
label: 'Rate',
value: PreprocessorType.Rate,
description: 'Data points are aligned and converted to a rate per time series',
},
];
return metricKind === MetricKind.CUMULATIVE
? [
...options,
{
label: 'Delta',
value: PreprocessorType.Delta,
description: 'Data points are aligned by their delta (difference) per time series',
},
]
: options;
}, [metricKind, valueType]);
};

View File

@ -1,48 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, Select } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource';
export interface Props {
refId: string;
datasource: CloudMonitoringDatasource;
onChange: (projectName: string) => void;
templateVariableOptions: Array<SelectableValue<string>>;
projectName: string;
}
export function Project({ refId, projectName, datasource, onChange, templateVariableOptions }: Props) {
const [projects, setProjects] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
datasource.getProjects().then((projects) => setProjects(projects));
}, [datasource]);
const projectsWithTemplateVariables = useMemo(
() => [
projects,
{
label: 'Template Variables',
options: templateVariableOptions,
},
...projects,
],
[projects, templateVariableOptions]
);
return (
<EditorField label="Project">
<Select
width="auto"
allowCustomValue
formatCreateLabel={(v) => `Use project: ${v}`}
onChange={({ value }) => onChange(value!)}
options={projectsWithTemplateVariables}
value={{ value: projectName, label: projectName }}
placeholder="Select Project"
inputId={`${refId}-project`}
/>
</EditorField>
);
}

View File

@ -1,40 +0,0 @@
import { render, screen, act } from '@testing-library/react';
import React from 'react';
import { config } from '@grafana/runtime';
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
import { createMockDatasource } from '../../__mocks__/cloudMonitoringDatasource';
import { createMockSLOQuery } from '../../__mocks__/cloudMonitoringQuery';
import { SLOQueryEditor, Props } from './SLOQueryEditor';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getTemplateSrv: () => new TemplateSrvMock({}),
}));
const props: Props = {
onChange: jest.fn(),
refId: 'refId',
customMetaData: {},
onRunQuery: jest.fn(),
datasource: createMockDatasource(),
variableOptionGroup: { options: [] },
query: createMockSLOQuery(),
};
describe('Cloud monitoring: SLO Query Editor', () => {
it('shoud render Service selector', async () => {
await act(async () => {
const originalValue = config.featureToggles.cloudMonitoringExperimentalUI;
config.featureToggles.cloudMonitoringExperimentalUI = true;
render(<SLOQueryEditor {...props} />);
expect(screen.getByLabelText('Service')).toBeInTheDocument();
config.featureToggles.cloudMonitoringExperimentalUI = originalValue;
});
});
});

View File

@ -1,88 +0,0 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorRow } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource';
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../../types';
import { AliasBy } from './AliasBy';
import { Alignment } from './Alignment';
import { GroupBy } from './GroupBy';
import { LabelFilter } from './LabelFilter';
import { Metrics } from './Metrics';
import { Preprocessor } from './Preprocessor';
export interface Props {
refId: string;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onMetricTypeChange: (query: MetricDescriptor) => void;
onChange: (query: MetricQuery | SLOQuery) => void;
query: MetricQuery;
datasource: CloudMonitoringDatasource;
labels: any;
}
function Editor({
refId,
query,
labels,
datasource,
onChange,
onMetricTypeChange,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
return (
<Metrics
refId={refId}
projectName={query.projectName}
metricType={query.metricType}
templateVariableOptions={variableOptionGroup.options}
datasource={datasource}
onChange={onMetricTypeChange}
onProjectChange={onChange}
query={query}
>
{(metric) => (
<>
<LabelFilter
labels={labels}
filters={query.filters!}
onChange={(filters: string[]) => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<EditorRow>
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
<GroupBy
refId={refId}
labels={Object.keys(labels)}
query={query}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
metricDescriptor={metric}
/>
<Alignment
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
customMetaData={customMetaData}
onChange={onChange}
/>
<AliasBy
refId={refId}
value={query.aliasBy}
onChange={(aliasBy) => {
onChange({ ...query, aliasBy });
}}
/>
</EditorRow>
</>
)}
</Metrics>
);
}
export const VisualMetricQueryEditor = React.memo(Editor);

View File

@ -1,11 +1,11 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Switch } from '@grafana/ui';
import { EditorField, EditorRow, HorizontalGroup, Switch } from '@grafana/ui';
import { GRAPH_PERIODS, SELECT_WIDTH } from '../constants';
import { GRAPH_PERIODS } from '../constants';
import { PeriodSelect, QueryEditorRow } from '.';
import { PeriodSelect } from './index';
export interface Props {
refId: string;
@ -16,8 +16,8 @@ export interface Props {
export const GraphPeriod: FunctionComponent<Props> = ({ refId, onChange, graphPeriod, variableOptionGroup }) => {
return (
<>
<QueryEditorRow
<EditorRow>
<EditorField
label="Graph period"
htmlFor={`${refId}-graph-period`}
tooltip={
@ -27,21 +27,22 @@ export const GraphPeriod: FunctionComponent<Props> = ({ refId, onChange, graphPe
</>
}
>
<Switch
data-testid={`${refId}-switch-graph-period`}
value={graphPeriod !== 'disabled'}
onChange={(e) => onChange(e.currentTarget.checked ? '' : 'disabled')}
/>
<PeriodSelect
inputId={`${refId}-graph-period`}
templateVariableOptions={variableOptionGroup.options}
current={graphPeriod}
onChange={onChange}
selectWidth={SELECT_WIDTH}
disabled={graphPeriod === 'disabled'}
aligmentPeriods={GRAPH_PERIODS}
/>
</QueryEditorRow>
</>
<HorizontalGroup>
<Switch
data-testid={`${refId}-switch-graph-period`}
value={graphPeriod !== 'disabled'}
onChange={(e) => onChange(e.currentTarget.checked ? '' : 'disabled')}
/>
<PeriodSelect
inputId={`${refId}-graph-period`}
templateVariableOptions={variableOptionGroup.options}
current={graphPeriod}
onChange={onChange}
disabled={graphPeriod === 'disabled'}
aligmentPeriods={GRAPH_PERIODS}
/>
</HorizontalGroup>
</EditorField>
</EditorRow>
);
};

View File

@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import { openMenu, select } from 'react-select-event';
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery';
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
import { GroupBy, Props } from './GroupBy';

View File

@ -1,13 +1,13 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { MultiSelect } from '@grafana/ui';
import { EditorField, EditorFieldGroup, MultiSelect } from '@grafana/ui';
import { INPUT_WIDTH, SYSTEM_LABELS } from '../constants';
import { SYSTEM_LABELS } from '../constants';
import { labelsToGroupedOptions } from '../functions';
import { MetricDescriptor, MetricQuery } from '../types';
import { Aggregation, QueryEditorRow } from '.';
import { Aggregation } from './Aggregation';
export interface Props {
refId: string;
@ -32,21 +32,22 @@ export const GroupBy: FunctionComponent<Props> = ({
);
return (
<QueryEditorRow
label="Group by"
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series."
htmlFor={`${refId}-group-by`}
>
<MultiSelect
inputId={`${refId}-group-by`}
width={INPUT_WIDTH}
placeholder="Choose label"
options={options}
value={query.groupBys ?? []}
onChange={(options) => {
onChange({ ...query, groupBys: options.map((o) => o.value!) });
}}
></MultiSelect>
<EditorFieldGroup>
<EditorField
label="Group by"
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series."
>
<MultiSelect
inputId={`${refId}-group-by`}
width="auto"
placeholder="Choose label"
options={options}
value={query.groupBys ?? []}
onChange={(options) => {
onChange({ ...query, groupBys: options.map((o) => o.value!) });
}}
/>
</EditorField>
<Aggregation
metricDescriptor={metricDescriptor}
templateVariableOptions={variableOptionGroup.options}
@ -54,7 +55,7 @@ export const GroupBy: FunctionComponent<Props> = ({
groupBys={query.groupBys ?? []}
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
refId={refId}
></Aggregation>
</QueryEditorRow>
/>
</EditorFieldGroup>
);
};

View File

@ -1,15 +1,9 @@
import { flatten } from 'lodash';
import React, { FunctionComponent, useCallback, useMemo } from 'react';
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui';
import { CustomControlProps } from '@grafana/ui/src/components/Select/types';
import { AccessoryButton, EditorField, EditorList, EditorRow, HorizontalGroup, Select } from '@grafana/ui';
import { SELECT_WIDTH } from '../constants';
import { labelsToGroupedOptions, stringArrayToFilters } from '../functions';
import { Filter } from '../types';
import { QueryEditorRow } from '.';
export interface Props {
labels: { [key: string]: string[] };
@ -18,137 +12,107 @@ export interface Props {
variableOptionGroup: SelectableValue<string>;
}
const operators = ['=', '!=', '=~', '!=~'];
interface Filter {
key: string;
operator: string;
value: string;
condition: string;
}
const FilterButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(
({ value, isOpen, invalid, ...rest }, ref) => {
return <Button {...rest} ref={ref} variant="secondary" icon="plus" aria-label="Add filter"></Button>;
}
);
FilterButton.displayName = 'FilterButton';
const DEFAULT_OPERATOR = '=';
const DEFAULT_CONDITION = 'AND';
const OperatorButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(({ value, ...rest }, ref) => {
return (
<Button {...rest} ref={ref} variant="secondary">
<span className="query-segment-operator">{value?.label}</span>
</Button>
);
});
OperatorButton.displayName = 'OperatorButton';
const filtersToStringArray = (filters: Filter[]) =>
filters.flatMap(({ key, operator, value, condition }) => [key, operator, value, condition]).slice(0, -1);
const operators = ['=', '!=', '=~', '!=~'].map(toOption);
export const LabelFilter: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
onChange: _onChange,
variableOptionGroup,
}) => {
const filters = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
const filters: Filter[] = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
const options = useMemo(
() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))],
[labels, variableOptionGroup]
);
const filtersToStringArray = useCallback((filters: Filter[]) => {
const strArr = flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition!]));
return strArr.slice(0, strArr.length - 1);
}, []);
const getOptions = ({ key = '', value = '' }: Partial<Filter>) => {
// Add the current key and value as options if they are manually entered
const keyPresent = options.some((op) => {
if (op.options) {
return options.some((opp) => opp.label === key);
}
return op.label === key;
});
if (!keyPresent) {
options.push({ label: key, value: key });
}
const valueOptions = labels.hasOwnProperty(key)
? [variableOptionGroup, ...labels[key].map(toOption)]
: [variableOptionGroup];
const valuePresent = valueOptions.some((op) => op.label === value);
if (!valuePresent) {
valueOptions.push({ label: value, value });
}
return { options, valueOptions };
};
const onChange = (items: Array<Partial<Filter>>) => {
const filters = items.map(({ key, operator, value, condition }) => ({
key: key || '',
operator: operator || DEFAULT_OPERATOR,
value: value || '',
condition: condition || DEFAULT_CONDITION,
}));
_onChange(filtersToStringArray(filters));
};
const renderItem = (item: Partial<Filter>, onChangeItem: (item: Filter) => void, onDeleteItem: () => void) => {
const { key = '', operator = DEFAULT_OPERATOR, value = '', condition = DEFAULT_CONDITION } = item;
const { options, valueOptions } = getOptions(item);
const AddFilter = () => {
return (
<Select
allowCustomValue
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key = '' }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' }]))
}
menuPlacement="bottom"
renderControl={FilterButton}
/>
<HorizontalGroup spacing="xs" width="auto">
<Select
aria-label="Filter label key"
formatCreateLabel={(v) => `Use label key: ${v}`}
allowCustomValue
value={key}
options={options}
onChange={({ value: key = '' }) => onChangeItem({ key, operator, value, condition })}
/>
<Select
value={operator}
options={operators}
onChange={({ value: operator = DEFAULT_OPERATOR }) => onChangeItem({ key, operator, value, condition })}
/>
<Select
aria-label="Filter label value"
placeholder="add filter value"
formatCreateLabel={(v) => `Use label value: ${v}`}
allowCustomValue
value={value}
options={valueOptions}
onChange={({ value = '' }) => onChangeItem({ key, operator, value, condition })}
/>
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDeleteItem} type="button" />
</HorizontalGroup>
);
};
return (
<QueryEditorRow
label="Filter"
tooltip={
'To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression.'
}
noFillEnd={filters.length > 1}
>
<VerticalGroup spacing="xs" width="auto">
{filters.map(({ key, operator, value, condition }, index) => {
// Add the current key and value as options if they are manually entered
const keyPresent = options.some((op) => {
if (op.options) {
return options.some((opp) => opp.label === key);
}
return op.label === key;
});
if (!keyPresent) {
options.push({ label: key, value: key });
}
const valueOptions = labels.hasOwnProperty(key)
? [variableOptionGroup, ...labels[key].map(toOption)]
: [variableOptionGroup];
const valuePresent = valueOptions.some((op) => {
return op.label === value;
});
if (!valuePresent) {
valueOptions.push({ label: value, value });
}
return (
<HorizontalGroup key={index} spacing="xs" width="auto">
<Select
aria-label="Filter label key"
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use label key: ${v}`}
value={key}
options={options}
onChange={({ value: key = '' }) => {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}}
/>
<Select
value={operator}
options={operators.map(toOption)}
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
menuPlacement="bottom"
renderControl={OperatorButton}
/>
<Select
aria-label="Filter label value"
width={SELECT_WIDTH}
formatCreateLabel={(v) => `Use label value: ${v}`}
allowCustomValue
value={value}
placeholder="add filter value"
options={valueOptions}
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
<Button
variant="secondary"
size="md"
icon="trash-alt"
aria-label="Remove"
onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))}
></Button>
{index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />}
</HorizontalGroup>
);
})}
{!filters.length && <AddFilter />}
</VerticalGroup>
</QueryEditorRow>
<EditorRow>
<EditorField
label="Filter"
tooltip="To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression."
>
<EditorList items={filters} renderItem={renderItem} onChange={onChange} />
</EditorField>
</EditorRow>
);
};

View File

@ -3,7 +3,7 @@ import React from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, Select } from '@grafana/ui';
import { LOOKBACK_PERIODS } from '../../constants';
import { LOOKBACK_PERIODS } from '../constants';
export interface Props {
refId: string;

View File

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorRows } from '@grafana/ui';
import CloudMonitoringDatasource from '../datasource';
import { getAlignmentPickerData } from '../functions';
@ -18,8 +19,7 @@ import {
import { GraphPeriod } from './GraphPeriod';
import { MQLQueryEditor } from './MQLQueryEditor';
import { AliasBy, Project, VisualMetricQueryEditor } from '.';
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
export interface Props {
refId: string;
@ -104,17 +104,7 @@ function Editor({
);
return (
<>
<Project
refId={refId}
templateVariableOptions={variableOptionGroup.options}
projectName={projectName}
datasource={datasource}
onChange={(projectName) => {
onChange({ ...query, projectName });
}}
/>
<EditorRows>
{editorMode === EditorMode.Visual && (
<VisualMetricQueryEditor
refId={refId}
@ -143,15 +133,7 @@ function Editor({
/>
</>
)}
<AliasBy
refId={refId}
value={query.aliasBy}
onChange={(aliasBy) => {
onChange({ ...query, aliasBy });
}}
/>
</>
</EditorRows>
);
}

View File

@ -2,9 +2,9 @@ import { render, screen, within } from '@testing-library/react';
import React from 'react';
import { openMenu, select } from 'react-select-event';
import { createMockDatasource } from '../../__mocks__/cloudMonitoringDatasource';
import { createMockMetricDescriptor } from '../../__mocks__/cloudMonitoringMetricDescriptor';
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery';
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
import { Metrics } from './Metrics';

View File

@ -3,53 +3,49 @@ import { startCase, uniqBy } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
import { EditorField, EditorFieldGroup, EditorRow, getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
import { INNER_LABEL_WIDTH, LABEL_WIDTH, SELECT_WIDTH } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { MetricDescriptor } from '../types';
import { MetricDescriptor, MetricQuery } from '../types';
import { QueryEditorField, QueryEditorRow } from '.';
import { Project } from './Project';
export interface Props {
refId: string;
onChange: (metricDescriptor: MetricDescriptor) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
projectName: string;
metricType: string;
query: MetricQuery;
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
}
interface State {
metricDescriptors: MetricDescriptor[];
metrics: any[];
services: any[];
service: string;
metric: string;
metricDescriptor?: MetricDescriptor;
projectName: string | null;
onProjectChange: (query: MetricQuery) => void;
}
export function Metrics(props: Props) {
const [state, setState] = useState<State>({
metricDescriptors: [],
metrics: [],
services: [],
service: '',
metric: '',
projectName: null,
});
const [metricDescriptors, setMetricDescriptors] = useState<MetricDescriptor[]>([]);
const [metricDescriptor, setMetricDescriptor] = useState<MetricDescriptor>();
const [metrics, setMetrics] = useState<Array<SelectableValue<string>>>([]);
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
const [service, setService] = useState<string>('');
const theme = useTheme2();
const selectStyles = getSelectStyles(theme);
const customStyle = useStyles2(getStyles);
const { services, service, metrics, metricDescriptors } = state;
const { metricType, templateVariableOptions, projectName, templateSrv, datasource, onChange, children } = props;
const {
onProjectChange,
query,
refId,
metricType,
templateVariableOptions,
projectName,
datasource,
onChange,
children,
} = props;
const { templateSrv } = datasource;
const getSelectedMetricDescriptor = useCallback(
(metricDescriptors: MetricDescriptor[], metricType: string) => {
@ -63,11 +59,8 @@ export function Metrics(props: Props) {
if (projectName) {
const metricDescriptors = await datasource.getMetricTypes(projectName);
const services = getServicesList(metricDescriptors);
setState((prevState) => ({
...prevState,
metricDescriptors,
services,
}));
setMetricDescriptors(metricDescriptors);
setServices(services);
}
};
loadMetricDescriptors();
@ -97,15 +90,13 @@ export function Metrics(props: Props) {
}));
return metricsByService;
};
const metrics = getMetricsList(metricDescriptors);
const service = metrics.length > 0 ? metrics[0].service : '';
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
setState((prevState) => ({
...prevState,
metricDescriptor,
metrics,
service: service,
}));
setMetricDescriptor(metricDescriptor);
setMetrics(metrics);
setService(service);
}, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]);
const onServiceChange = ({ value: service }: any) => {
@ -119,15 +110,18 @@ export function Metrics(props: Props) {
}));
if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) {
onMetricTypeChange(metrics[0], { service, metrics });
onMetricTypeChange(metrics[0]);
setService(service);
setMetrics(metrics);
} else {
setState({ ...state, service, metrics });
setService(service);
setMetrics(metrics);
}
};
const onMetricTypeChange = ({ value }: SelectableValue<string>, extra: any = {}) => {
const metricDescriptor = getSelectedMetricDescriptor(state.metricDescriptors, value!);
setState({ ...state, metricDescriptor, ...extra });
const onMetricTypeChange = ({ value }: SelectableValue<string>) => {
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!);
setMetricDescriptor(metricDescriptor);
onChange({ ...metricDescriptor, type: value! });
};
@ -142,42 +136,54 @@ export function Metrics(props: Props) {
return (
<>
<QueryEditorRow>
<QueryEditorField labelWidth={LABEL_WIDTH} label="Service" htmlFor={`${props.refId}-service`}>
<Select
width={SELECT_WIDTH}
onChange={onServiceChange}
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
inputId={`${props.refId}-service`}
></Select>
</QueryEditorField>
<QueryEditorField label="Metric name" labelWidth={INNER_LABEL_WIDTH} htmlFor={`${props.refId}-select-metric`}>
<Select
width={SELECT_WIDTH}
onChange={onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
inputId={`${props.refId}-select-metric`}
></Select>
</QueryEditorField>
</QueryEditorRow>
<EditorRow>
<EditorFieldGroup>
<Project
refId={refId}
templateVariableOptions={templateVariableOptions}
projectName={projectName}
datasource={datasource}
onChange={(projectName) => {
onProjectChange({ ...query, projectName });
}}
/>
{children(state.metricDescriptor)}
<EditorField label="Service" width="auto">
<Select
width="auto"
onChange={onServiceChange}
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
inputId={`${props.refId}-service`}
/>
</EditorField>
<EditorField label="Metric name" width="auto">
<Select
width="auto"
onChange={onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
inputId={`${props.refId}-select-metric`}
/>
</EditorField>
</EditorFieldGroup>
</EditorRow>
{children(metricDescriptor)}
</>
);
}

View File

@ -21,7 +21,6 @@ export function PeriodSelect({
templateVariableOptions,
onChange,
current,
selectWidth,
disabled,
aligmentPeriods,
}: Props) {
@ -37,7 +36,7 @@ export function PeriodSelect({
return (
<Select
width={selectWidth}
width="auto"
onChange={({ value }) => onChange(value!)}
value={[...options, ...templateVariableOptions].find((s) => s.value === current)}
options={[
@ -55,6 +54,6 @@ export function PeriodSelect({
inputId={inputId}
disabled={disabled}
allowCustomValue
></Select>
/>
);
}

View File

@ -4,9 +4,9 @@ import React from 'react';
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
import { createMockMetricDescriptor } from '../../__mocks__/cloudMonitoringMetricDescriptor';
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery';
import { MetricKind, ValueTypes } from '../../types';
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
import { MetricKind, ValueTypes } from '../types';
import { Preprocessor } from './Preprocessor';

View File

@ -1,13 +1,11 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { RadioButtonGroup } from '@grafana/ui';
import { EditorField, RadioButtonGroup } from '@grafana/ui';
import { getAlignmentPickerData } from '../functions';
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../types';
import { QueryEditorRow } from '.';
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
export interface Props {
@ -19,7 +17,7 @@ export interface Props {
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
const options = useOptions(metricDescriptor);
return (
<QueryEditorRow
<EditorField
label="Pre-processing"
tooltip="Preprocessing options are displayed when the selected metric has a metric kind of delta or cumulative. The specific options available are determined by the metic's value type. If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series"
>
@ -31,8 +29,8 @@ export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor
}}
value={query.preprocessor ?? PreprocessorType.None}
options={options}
></RadioButtonGroup>
</QueryEditorRow>
/>
</EditorField>
);
};

View File

@ -1,13 +1,10 @@
import React, { useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { EditorField, Select } from '@grafana/ui';
import { SELECT_WIDTH } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { QueryEditorRow } from '.';
export interface Props {
refId: string;
datasource: CloudMonitoringDatasource;
@ -35,9 +32,9 @@ export function Project({ refId, projectName, datasource, onChange, templateVari
);
return (
<QueryEditorRow label="Project" htmlFor={`${refId}-project`}>
<EditorField label="Project">
<Select
width={SELECT_WIDTH}
width="auto"
allowCustomValue
formatCreateLabel={(v) => `Use project: ${v}`}
onChange={({ value }) => onChange(value!)}
@ -46,6 +43,6 @@ export function Project({ refId, projectName, datasource, onChange, templateVari
placeholder="Select Project"
inputId={`${refId}-project`}
/>
</QueryEditorRow>
</EditorField>
);
}

View File

@ -1,21 +1,16 @@
import { css } from '@emotion/css';
import React, { PureComponent } from 'react';
import { QueryEditorProps, toOption } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Button, EditorRows, Select } from '@grafana/ui';
import { EditorRows } from '@grafana/ui';
import { QUERY_TYPES, SELECT_WIDTH } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { CloudMonitoringQuery, EditorMode, MetricQuery, QueryType, SLOQuery, CloudMonitoringOptions } from '../types';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, CloudMonitoringOptions } from '../types';
import { MetricQueryEditor as ExperimentalMetricQueryEditor } from './Experimental/MetricQueryEditor';
import { QueryHeader } from './Experimental/QueryHeader';
import { SLOQueryEditor as ExperimentalSLOQueryEditor } from './Experimental/SLOQueryEditor';
import { defaultQuery } from './MetricQueryEditor';
import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor';
import { QueryHeader } from './QueryHeader';
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
import { MetricQueryEditor, QueryEditorRow, SLOQueryEditor } from './';
import { MetricQueryEditor, SLOQueryEditor } from './';
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
@ -30,7 +25,7 @@ export class QueryEditor extends PureComponent<Props> {
this.props.query.metricQuery = metricQuery;
}
if (!this.props.query.hasOwnProperty('queryType')) {
if (![QueryType.METRICS, QueryType.SLO].includes(this.props.query.queryType)) {
this.props.query.queryType = QueryType.METRICS;
}
@ -58,7 +53,7 @@ export class QueryEditor extends PureComponent<Props> {
options: datasource.getVariables().map(toOption),
};
return config.featureToggles.cloudMonitoringExperimentalUI ? (
return (
<EditorRows>
<QueryHeader
query={query}
@ -67,69 +62,6 @@ export class QueryEditor extends PureComponent<Props> {
onChange={onChange}
onRunQuery={onRunQuery}
/>
{queryType === QueryType.METRICS && (
<ExperimentalMetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
customMetaData={customMetaData}
onChange={(metricQuery: MetricQuery) => {
this.props.onChange({ ...this.props.query, metricQuery });
}}
onRunQuery={onRunQuery}
datasource={datasource}
query={metricQuery}
/>
)}
{queryType === QueryType.SLO && (
<ExperimentalSLOQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
customMetaData={customMetaData}
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
onRunQuery={onRunQuery}
datasource={datasource}
query={sloQuery}
/>
)}
</EditorRows>
) : (
<EditorRows>
<QueryEditorRow
label="Query type"
fillComponent={
query.queryType !== QueryType.SLO && (
<Button
variant="secondary"
className={css`
margin-left: auto;
`}
icon="edit"
onClick={() =>
this.onQueryChange('metricQuery', {
...metricQuery,
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
})
}
>
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
</Button>
)
}
htmlFor={`${query.refId}-query-type`}
>
<Select
width={SELECT_WIDTH}
value={queryType}
options={QUERY_TYPES}
onChange={({ value }) => {
onChange({ ...query, sloQuery, queryType: value! });
onRunQuery();
}}
inputId={`${query.refId}-query-type`}
/>
</QueryEditorRow>
{queryType === QueryType.METRICS && (
<MetricQueryEditor
refId={query.refId}

View File

@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { openMenu, select } from 'react-select-event';
import { createMockQuery, createMockSLOQuery } from '../../__mocks__/cloudMonitoringQuery';
import { EditorMode, QueryType } from '../../types';
import { createMockQuery, createMockSLOQuery } from '../__mocks__/cloudMonitoringQuery';
import { EditorMode, QueryType } from '../types';
import { QueryHeader } from './QueryHeader';

View File

@ -2,8 +2,8 @@ import React from 'react';
import { EditorHeader, FlexItem, InlineSelect, RadioButtonGroup } from '@grafana/ui';
import { QUERY_TYPES } from '../../constants';
import { EditorMode, CloudMonitoringQuery, QueryType, SLOQuery, MetricQuery } from '../../types';
import { QUERY_TYPES } from '../constants';
import { EditorMode, CloudMonitoringQuery, QueryType, SLOQuery, MetricQuery } from '../types';
export interface QueryEditorHeaderProps {
query: CloudMonitoringQuery;

View File

@ -3,8 +3,8 @@ import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select, EditorField } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import CloudMonitoringDatasource from '../datasource';
import { SLOQuery } from '../types';
export interface Props {
refId: string;

View File

@ -1,53 +0,0 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { QueryEditorRow } from '..';
import { SELECT_WIDTH, LOOKBACK_PERIODS } from '../../constants';
export interface Props {
refId: string;
onChange: (lookbackPeriod: string) => void;
templateVariableOptions: Array<SelectableValue<string>>;
current?: string;
}
export const LookbackPeriodSelect: FunctionComponent<Props> = ({
refId,
current,
templateVariableOptions,
onChange,
}) => {
const options = LOOKBACK_PERIODS.map((lp) => ({
...lp,
label: lp.text,
}));
if (current && !options.find((op) => op.value === current)) {
options.push({ label: current, text: current, value: current, hidden: false });
}
const visibleOptions = options.filter((lp) => !lp.hidden);
return (
<QueryEditorRow label="Lookback period" htmlFor={`${refId}-lookback-period`}>
<Select
inputId={`${refId}-lookback-period`}
width={SELECT_WIDTH}
allowCustomValue
value={[...options, ...templateVariableOptions].find((s) => s.value === current)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Predefined periods',
expanded: true,
options: visibleOptions,
},
]}
onChange={({ value }) => onChange(value!)}
/>
</QueryEditorRow>
);
};

View File

@ -1,56 +0,0 @@
import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { QueryEditorRow } from '..';
import { SELECT_WIDTH } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
export interface Props {
refId: string;
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const SLO = ({ refId, query, templateVariableOptions, onChange, datasource }: Props) => {
const [slos, setSLOs] = useState<Array<SelectableValue<string>>>([]);
const { projectName, serviceId } = query;
useEffect(() => {
if (!projectName || !serviceId) {
return;
}
datasource.getServiceLevelObjectives(projectName, serviceId).then((sloIds: Array<SelectableValue<string>>) => {
setSLOs([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...sloIds,
]);
});
}, [datasource, projectName, serviceId, templateVariableOptions]);
return (
<QueryEditorRow label="SLO" htmlFor={`${refId}-slo`}>
<Select
inputId={`${refId}-slo`}
width={SELECT_WIDTH}
allowCustomValue
value={query?.sloId && { value: query?.sloId, label: query?.sloName || query?.sloId }}
placeholder="Select SLO"
options={slos}
onChange={async ({ value: sloId = '', label: sloName = '' }) => {
const slos = await datasource.getServiceLevelObjectives(projectName, serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, sloName, goal: slo?.goal });
}}
/>
</QueryEditorRow>
);
};

View File

@ -1,100 +0,0 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { AliasBy, PeriodSelect, AlignmentPeriodLabel, Project, QueryEditorRow } from '..';
import { ALIGNMENT_PERIODS, SELECT_WIDTH, SLO_BURN_RATE_SELECTOR_NAME } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types';
import { LookbackPeriodSelect } from './LookbackPeriodSelect';
import { Selector, Service, SLO } from '.';
export interface Props {
refId: string;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: CloudMonitoringDatasource;
}
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
projectName: dataSource.getDefaultProject(),
alignmentPeriod: 'cloud-monitoring-auto',
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
serviceName: '',
sloId: '',
sloName: '',
lookbackPeriod: '',
});
export function SLOQueryEditor({
refId,
query,
datasource,
onChange,
variableOptionGroup,
customMetaData,
}: React.PropsWithChildren<Props>) {
return (
<>
<Project
refId={refId}
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={(projectName) => onChange({ ...query, projectName })}
/>
<Service
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></Service>
<SLO
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></SLO>
<Selector
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></Selector>
{query.selectorName === SLO_BURN_RATE_SELECTOR_NAME && (
<LookbackPeriodSelect
refId={refId}
onChange={(lookbackPeriod) => onChange({ ...query, lookbackPeriod: lookbackPeriod })}
current={query.lookbackPeriod}
templateVariableOptions={variableOptionGroup.options}
/>
)}
<QueryEditorRow label="Alignment period" htmlFor={`${refId}-alignment-period`}>
<PeriodSelect
inputId={`${refId}-alignment-period`}
templateVariableOptions={variableOptionGroup.options}
selectWidth={SELECT_WIDTH}
current={query.alignmentPeriod}
onChange={(period) => onChange({ ...query, alignmentPeriod: period })}
aligmentPeriods={ALIGNMENT_PERIODS}
/>
<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />
</QueryEditorRow>
<AliasBy refId={refId} value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
</>
);
}

View File

@ -1,38 +0,0 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { QueryEditorRow } from '..';
import { SELECT_WIDTH, SELECTORS } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
export interface Props {
refId: string;
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const Selector = ({ refId, query, templateVariableOptions, onChange, datasource }: Props) => {
return (
<QueryEditorRow label="Selector" htmlFor={`${refId}-slo-selector`}>
<Select
inputId={`${refId}-slo-selector`}
width={SELECT_WIDTH}
allowCustomValue
value={[...SELECTORS, ...templateVariableOptions].find((s) => s.value === query?.selectorName ?? '')}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...SELECTORS,
]}
onChange={({ value: selectorName }) => onChange({ ...query, selectorName: selectorName ?? '' })}
/>
</QueryEditorRow>
);
};

View File

@ -1,54 +0,0 @@
import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { QueryEditorRow } from '..';
import { SELECT_WIDTH } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
export interface Props {
refId: string;
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const Service = ({ refId, query, templateVariableOptions, onChange, datasource }: Props) => {
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
const { projectName } = query;
useEffect(() => {
if (!projectName) {
return;
}
datasource.getSLOServices(projectName).then((services: Array<SelectableValue<string>>) => {
setServices([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]);
});
}, [datasource, projectName, templateVariableOptions]);
return (
<QueryEditorRow label="Service" htmlFor={`${refId}-slo-service`}>
<Select
inputId={`${refId}-slo-service`}
width={SELECT_WIDTH}
allowCustomValue
value={query?.serviceId && { value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
placeholder="Select service"
options={services}
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
onChange({ ...query, serviceId, serviceName, sloId: '' })
}
/>
</QueryEditorRow>
);
};

View File

@ -1,3 +0,0 @@
export { Service } from './Service';
export { SLO } from './SLO';
export { Selector } from './Selector';

View File

@ -3,10 +3,10 @@ import React, { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/ui';
import { ALIGNMENT_PERIODS, SLO_BURN_RATE_SELECTOR_NAME } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { alignmentPeriodLabel } from '../../functions';
import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types';
import { ALIGNMENT_PERIODS, SLO_BURN_RATE_SELECTOR_NAME } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { alignmentPeriodLabel } from '../functions';
import { AlignmentTypes, CustomMetaData, SLOQuery } from '../types';
import { AliasBy } from './AliasBy';
import { LookbackPeriodSelect } from './LookbackPeriodSelect';

View File

@ -3,9 +3,9 @@ import React from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, Select } from '@grafana/ui';
import { SELECTORS } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECTORS } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { SLOQuery } from '../types';
export interface Props {
refId: string;

View File

@ -3,8 +3,8 @@ import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, Select } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import CloudMonitoringDatasource from '../datasource';
import { SLOQuery } from '../types';
export interface Props {
refId: string;

View File

@ -1,11 +1,17 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorRow } from '@grafana/ui';
import CloudMonitoringDatasource from '../datasource';
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../types';
import { Alignment, GroupBy, LabelFilter, Metrics, Preprocessor } from '.';
import { AliasBy } from './AliasBy';
import { Alignment } from './Alignment';
import { GroupBy } from './GroupBy';
import { LabelFilter } from './LabelFilter';
import { Metrics } from './Metrics';
import { Preprocessor } from './Preprocessor';
export interface Props {
refId: string;
@ -31,12 +37,13 @@ function Editor({
return (
<Metrics
refId={refId}
templateSrv={datasource.templateSrv}
projectName={query.projectName}
metricType={query.metricType}
templateVariableOptions={variableOptionGroup.options}
datasource={datasource}
onChange={onMetricTypeChange}
onProjectChange={onChange}
query={query}
>
{(metric) => (
<>
@ -46,23 +53,32 @@ function Editor({
onChange={(filters: string[]) => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
<GroupBy
refId={refId}
labels={Object.keys(labels)}
query={query}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
metricDescriptor={metric}
/>
<Alignment
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
customMetaData={customMetaData}
onChange={onChange}
/>
<EditorRow>
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
<GroupBy
refId={refId}
labels={Object.keys(labels)}
query={query}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
metricDescriptor={metric}
/>
<Alignment
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
customMetaData={customMetaData}
onChange={onChange}
/>
<AliasBy
refId={refId}
value={query.aliasBy}
onChange={(aliasBy) => {
onChange({ ...query, aliasBy });
}}
/>
</EditorRow>
</>
)}
</Metrics>

View File

@ -9,7 +9,7 @@ export { AlignmentPeriodLabel } from './AlignmentPeriodLabel';
export { AliasBy } from './AliasBy';
export { Aggregation } from './Aggregation';
export { MetricQueryEditor } from './MetricQueryEditor';
export { SLOQueryEditor } from './SLO/SLOQueryEditor';
export { SLOQueryEditor } from './SLOQueryEditor';
export { MQLQueryEditor } from './MQLQueryEditor';
export { VariableQueryField, QueryEditorRow, QueryEditorField } from './Fields';
export { VisualMetricQueryEditor } from './VisualMetricQueryEditor';