mirror of https://github.com/grafana/grafana
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 checkpull/55246/head
parent
13014dc0df
commit
92857ef331
@ -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(); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -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]); |
||||
}; |
@ -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> |
||||
); |
||||
}; |
@ -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> |
||||
); |
||||
}; |
@ -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} |
||||
/> |
||||
); |
||||
}; |
@ -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'); |
||||
}); |
||||
}); |
@ -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> |
||||
); |
||||
}; |
@ -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> |
||||
); |
||||
}; |
@ -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> |
||||
); |
||||
}; |
@ -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; |
||||
}); |
||||
}); |
||||
}); |
@ -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); |
@ -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}; |
||||
`;
|
@ -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 |
||||
/> |
||||
); |
||||
} |
@ -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]); |
||||
}; |
@ -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> |
||||
); |
||||
} |
@ -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; |
||||
}); |
||||
}); |
||||
}); |
@ -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); |
@ -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> |
||||
); |
||||
}; |
@ -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> |
||||
); |
||||
}; |
@ -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 })} /> |
||||
</> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
}; |
@ -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> |
||||
); |
||||
}; |
@ -1,3 +0,0 @@ |
||||
export { Service } from './Service'; |
||||
export { SLO } from './SLO'; |
||||
export { Selector } from './Selector'; |
Loading…
Reference in new issue