diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index e319c035e2d..6f8fbb1cf92 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -338,6 +338,7 @@ func (s *Service) buildQueryExecutors(req *backend.QueryDataRequest) ([]cloudMon IntervalMS: query.Interval.Milliseconds(), AliasBy: q.MetricQuery.AliasBy, timeRange: req.Queries[0].TimeRange, + GraphPeriod: q.MetricQuery.GraphPeriod, } } else { cmtsf.AliasBy = q.MetricQuery.AliasBy diff --git a/pkg/tsdb/cloudmonitoring/time_series_query.go b/pkg/tsdb/cloudmonitoring/time_series_query.go index f885432e315..addd562ece3 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query.go @@ -20,6 +20,21 @@ import ( "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) +func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) appendGraphPeriod(req *backend.QueryDataRequest) string { + // GraphPeriod needs to be explicitly disabled. + // If not set, the default behavior is to set an automatic value + if timeSeriesQuery.GraphPeriod != "disabled" { + graphPeriod := timeSeriesQuery.GraphPeriod + if graphPeriod == "auto" || graphPeriod == "" { + intervalCalculator := intervalv2.NewCalculator(intervalv2.CalculatorOptions{}) + interval := intervalCalculator.Calculate(req.Queries[0].TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) + graphPeriod = interval.Text + } + return fmt.Sprintf(" | graph_period %s", graphPeriod) + } + return "" +} + func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, tracer tracing.Tracer) (*backend.DataResponse, cloudMonitoringResponse, string, error) { dr := &backend.DataResponse{} @@ -35,13 +50,11 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, r slog.Info("No project name set on query, using project name from datasource", "projectName", projectName) } - intervalCalculator := intervalv2.NewCalculator(intervalv2.CalculatorOptions{}) - interval := intervalCalculator.Calculate(req.Queries[0].TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) - + timeSeriesQuery.Query += timeSeriesQuery.appendGraphPeriod(req) from := req.Queries[0].TimeRange.From to := req.Queries[0].TimeRange.To timeFormat := "2006/01/02-15:04:05" - timeSeriesQuery.Query += fmt.Sprintf(" | graph_period %s | within d'%s', d'%s'", interval.Text, from.UTC().Format(timeFormat), to.UTC().Format(timeFormat)) + timeSeriesQuery.Query += fmt.Sprintf(" | within d'%s', d'%s'", from.UTC().Format(timeFormat), to.UTC().Format(timeFormat)) buf, err := json.Marshal(map[string]interface{}{ "query": timeSeriesQuery.Query, diff --git a/pkg/tsdb/cloudmonitoring/time_series_query_test.go b/pkg/tsdb/cloudmonitoring/time_series_query_test.go index 5f1900f9e75..33912e9bf48 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query_test.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query_test.go @@ -101,4 +101,14 @@ func TestTimeSeriesQuery(t *testing.T) { require.True(t, ok) assert.Equal(t, "6724404429462225363", labels["resource.label.instance_id"]) }) + + t.Run("appends graph_period to the query", func(t *testing.T) { + query := &cloudMonitoringTimeSeriesQuery{} + assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), " | graph_period 10ms") + }) + + t.Run("skips graph_period if disabled", func(t *testing.T) { + query := &cloudMonitoringTimeSeriesQuery{GraphPeriod: "disabled"} + assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), "") + }) } diff --git a/pkg/tsdb/cloudmonitoring/types.go b/pkg/tsdb/cloudmonitoring/types.go index a2cce2fb8b1..4d5d4fdbd65 100644 --- a/pkg/tsdb/cloudmonitoring/types.go +++ b/pkg/tsdb/cloudmonitoring/types.go @@ -40,6 +40,7 @@ type ( IntervalMS int64 AliasBy string timeRange backend.TimeRange + GraphPeriod string } metricQuery struct { @@ -56,6 +57,7 @@ type ( Query string Preprocessor string PreprocessorType preprocessorType + GraphPeriod string } sloQuery struct { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx index 7d4226183df..edb9e8163a1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx @@ -2,11 +2,11 @@ import React, { FC } from 'react'; import { SelectableValue } from '@grafana/data'; -import { SELECT_WIDTH } from '../constants'; +import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../constants'; import CloudMonitoringDatasource from '../datasource'; import { CustomMetaData, MetricQuery, SLOQuery } from '../types'; -import { AlignmentFunction, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.'; +import { AlignmentFunction, PeriodSelect, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.'; export interface Props { refId: string; @@ -39,12 +39,13 @@ export const Alignment: FC = ({ onChange={onChange} /> - onChange({ ...query, alignmentPeriod: period })} + aligmentPeriods={ALIGNMENT_PERIODS} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx new file mode 100644 index 00000000000..0c5307889cc --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx @@ -0,0 +1,39 @@ +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(); + expect(screen.getByLabelText('Graph period')).not.toBeDisabled(); + }); + + it('should disable graph_period when toggled', async () => { + const onChange = jest.fn(); + render(); + 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(); + const selectEl = screen.getByLabelText('Graph period'); + expect(selectEl).toBeInTheDocument(); + + await select(selectEl, '1m', { + container: document.body, + }); + expect(onChange).toHaveBeenCalledWith('1m'); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx new file mode 100644 index 00000000000..e6081a1b43f --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx @@ -0,0 +1,47 @@ +import React, { FunctionComponent } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { Switch } from '@grafana/ui'; + +import { GRAPH_PERIODS, SELECT_WIDTH } from '../constants'; + +import { PeriodSelect, QueryEditorRow } from '.'; + +export interface Props { + refId: string; + onChange: (period: string) => void; + variableOptionGroup: SelectableValue; + graphPeriod?: string; +} + +export const GraphPeriod: FunctionComponent = ({ refId, onChange, graphPeriod, variableOptionGroup }) => { + return ( + <> + + Set graph_period which forces a preferred period between points. Automatically set to the + current interval if left blank. + + } + > + onChange(e.currentTarget.checked ? '' : 'disabled')} + /> + + + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx index d6ec09e9941..89d78b7111a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx @@ -16,6 +16,7 @@ import { ValueTypes, } from '../types'; +import { GraphPeriod } from './GraphPeriod'; import { MQLQueryEditor } from './MQLQueryEditor'; import { AliasBy, Project, VisualMetricQueryEditor } from '.'; @@ -128,11 +129,19 @@ function Editor({ )} {editorMode === EditorMode.MQL && ( - onQueryChange({ ...query, query: q })} - onRunQuery={onRunQuery} - query={query.query} - > + <> + onQueryChange({ ...query, query: q })} + onRunQuery={onRunQuery} + query={query.query} + > + onQueryChange({ ...query, graphPeriod })} + graphPeriod={query.graphPeriod} + refId={refId} + variableOptionGroup={variableOptionGroup} + /> + )} { +export interface Props { inputId: string; - onChange(query: TQuery): void; - query: TQuery; + onChange: (period: string) => void; templateVariableOptions: Array>; + aligmentPeriods: periodOption[]; selectWidth?: number; + category?: string; + disabled?: boolean; + current?: string; } -export function AlignmentPeriod({ +export function PeriodSelect({ inputId, templateVariableOptions, onChange, - query, + current, selectWidth, -}: Props) { + disabled, + aligmentPeriods, +}: Props) { const options = useMemo( () => - ALIGNMENT_PERIODS.map((ap) => ({ + aligmentPeriods.map((ap) => ({ ...ap, label: ap.text, })), - [] + [aligmentPeriods] ); const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]); return ( ); } diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx index f33fe4e826b..a61376d06d8 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; -import { AliasBy, AlignmentPeriod, AlignmentPeriodLabel, Project, QueryEditorRow } from '..'; -import { SELECT_WIDTH } from '../../constants'; +import { AliasBy, PeriodSelect, AlignmentPeriodLabel, Project, QueryEditorRow } from '..'; +import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../../constants'; import CloudMonitoringDatasource from '../../datasource'; import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types'; @@ -71,15 +71,13 @@ export function SLOQueryEditor({ > - onChange({ ...query, alignmentPeriod: period })} + aligmentPeriods={ALIGNMENT_PERIODS} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/index.ts b/public/app/plugins/datasource/cloud-monitoring/components/index.ts index 260ac778bdb..0b51aad1ead 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/index.ts +++ b/public/app/plugins/datasource/cloud-monitoring/components/index.ts @@ -5,7 +5,6 @@ export { Alignment } from './Alignment'; export { LabelFilter } from './LabelFilter'; export { AnnotationsHelp } from './AnnotationsHelp'; export { AlignmentFunction } from './AlignmentFunction'; -export { AlignmentPeriod } from './AlignmentPeriod'; export { AlignmentPeriodLabel } from './AlignmentPeriodLabel'; export { AliasBy } from './AliasBy'; export { Aggregation } from './Aggregation'; @@ -14,4 +13,5 @@ export { SLOQueryEditor } from './SLO/SLOQueryEditor'; export { MQLQueryEditor } from './MQLQueryEditor'; export { VariableQueryField, QueryEditorRow, QueryEditorField } from './Fields'; export { VisualMetricQueryEditor } from './VisualMetricQueryEditor'; +export { PeriodSelect } from './PeriodSelect'; export { Preprocessor } from './Preprocessor'; diff --git a/public/app/plugins/datasource/cloud-monitoring/constants.ts b/public/app/plugins/datasource/cloud-monitoring/constants.ts index df640dcaabf..25922d5cdd1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/constants.ts +++ b/public/app/plugins/datasource/cloud-monitoring/constants.ts @@ -226,7 +226,13 @@ export const AGGREGATIONS = [ }, ]; -export const ALIGNMENT_PERIODS = [ +export type periodOption = { + text: string; + value: string; + hidden?: boolean; +}; + +export const ALIGNMENT_PERIODS: periodOption[] = [ { text: 'grafana auto', value: 'grafana-auto' }, { text: 'stackdriver auto', value: 'stackdriver-auto', hidden: true }, { text: 'cloud monitoring auto', value: 'cloud-monitoring-auto' }, @@ -243,6 +249,21 @@ export const ALIGNMENT_PERIODS = [ { text: '1w', value: '+604800s' }, ]; +export const GRAPH_PERIODS: periodOption[] = [ + { text: 'auto', value: 'auto' }, + { text: '1m', value: '1m' }, + { text: '2m', value: '2m' }, + { text: '5m', value: '5m' }, + { text: '10m', value: '10m' }, + { text: '30m', value: '30m' }, + { text: '1h', value: '1h' }, + { text: '3h', value: '3h' }, + { text: '6h', value: '6h' }, + { text: '1d', value: '1d' }, + { text: '3d', value: '3d' }, + { text: '1w', value: '1w' }, +]; + export const SYSTEM_LABELS = [ 'metadata.system_labels.cloud_account', 'metadata.system_labels.name', diff --git a/public/app/plugins/datasource/cloud-monitoring/types.ts b/public/app/plugins/datasource/cloud-monitoring/types.ts index 7f456d4ec7b..8081d8c0ed0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/types.ts +++ b/public/app/plugins/datasource/cloud-monitoring/types.ts @@ -126,6 +126,8 @@ export interface MetricQuery extends BaseQuery { view?: string; query: string; preprocessor?: PreprocessorType; + // To disable the graphPeriod, it should explictly be set to 'disabled' + graphPeriod?: 'disabled' | string; } export interface SLOQuery extends BaseQuery {