diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx index ea464e2825d..c95c6f7e95a 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.tsx @@ -1,12 +1,11 @@ -import { defaults } from 'lodash'; +import deepEqual from 'fast-deep-equal'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useAsync } from 'react-use'; import { CoreApp, QueryEditorProps, TimeRange } from '@grafana/data'; import { ButtonCascader, CascaderOption } from '@grafana/ui'; -import { defaultGrafanaPyroscope, defaultPhlareQueryType, GrafanaPyroscope } from '../dataquery.gen'; -import { PhlareDataSource } from '../datasource'; +import { normalizeQuery, PhlareDataSource } from '../datasource'; import { BackendType, PhlareDataSourceOptions, ProfileTypeMessage, Query } from '../types'; import { EditorRow } from './EditorRow'; @@ -16,31 +15,18 @@ import { QueryOptions } from './QueryOptions'; export type Props = QueryEditorProps; -export const defaultQuery: Partial = { - ...defaultGrafanaPyroscope, - queryType: defaultPhlareQueryType, -}; - export function QueryEditor(props: Props) { - let query = normalizeQuery(props.query, props.app); + const { onChange, onRunQuery, datasource, query, range, app } = props; function handleRunQuery(value: string) { - props.onChange({ ...props.query, labelSelector: value }); - props.onRunQuery(); + onChange({ ...query, labelSelector: value }); + onRunQuery(); } - const { profileTypes, onProfileTypeChange, selectedProfileName } = useProfileTypes( - props.datasource, - props.query, - props.onChange, - props.datasource.backendType - ); - const { labels, getLabelValues, onLabelSelectorChange } = useLabels( - props.range, - props.datasource, - props.query, - props.onChange - ); + const { profileTypes, onProfileTypeChange, selectedProfileName } = useProfileTypes(datasource, query, onChange); + const { labels, getLabelValues, onLabelSelectorChange } = useLabels(range, datasource, query, onChange); + useNormalizeQuery(query, profileTypes, onChange, app); + const cascaderOptions = useCascaderOptions(profileTypes); return ( @@ -64,6 +50,42 @@ export function QueryEditor(props: Props) { ); } +function useNormalizeQuery( + query: Query, + profileTypes: ProfileTypeMessage[], + onChange: (value: Query) => void, + app?: CoreApp +) { + useEffect(() => { + const normalizedQuery = normalizeQuery(query, app); + // Query can be stored with some old type, or we can have query from different pyro datasource + const selectedProfile = query.profileTypeId && profileTypes.find((p) => p.id === query.profileTypeId); + if (profileTypes.length && !selectedProfile) { + normalizedQuery.profileTypeId = defaultProfileType(profileTypes); + } + // Makes sure we don't have an infinite loop updates because the normalization creates a new object + if (!deepEqual(query, normalizedQuery)) { + onChange(normalizedQuery); + } + }, [app, query, profileTypes, onChange]); +} + +function defaultProfileType(profileTypes: ProfileTypeMessage[]): string { + const cpuProfiles = profileTypes.filter((p) => p.id.indexOf('cpu') >= 0); + if (cpuProfiles.length) { + // Prefer cpu time profile if available instead of samples + const cpuTimeProfile = cpuProfiles.find((p) => p.id.indexOf('samples') === -1); + if (cpuTimeProfile) { + return cpuTimeProfile.id; + } + // Fallback to first cpu profile type + return cpuProfiles[0].id; + } + + // Fallback to first profile type from response data + return profileTypes[0].id; +} + function useLabels( range: TimeRange | undefined, datasource: PhlareDataSource, @@ -138,12 +160,7 @@ function useCascaderOptions(profileTypes: ProfileTypeMessage[]) { }, [profileTypes]); } -function useProfileTypes( - datasource: PhlareDataSource, - query: Query, - onChange: (value: Query) => void, - backendType: BackendType = 'phlare' -) { +function useProfileTypes(datasource: PhlareDataSource, query: Query, onChange: (value: Query) => void) { const [profileTypes, setProfileTypes] = useState([]); useEffect(() => { @@ -160,23 +177,20 @@ function useProfileTypes( } const id = selectedOptions[selectedOptions.length - 1].value; - - // Probably cannot happen but makes TS happy - if (typeof id !== 'string') { - throw new Error('id is not string'); - } - onChange({ ...query, profileTypeId: id }); }, [onChange, query] ); - const selectedProfileName = useProfileName(profileTypes, query.profileTypeId, backendType); - + const selectedProfileName = useProfileName(profileTypes, query.profileTypeId, datasource.backendType); return { profileTypes, onProfileTypeChange, selectedProfileName }; } -function useProfileName(profileTypes: ProfileTypeMessage[], profileTypeId: string, backendType: BackendType) { +function useProfileName( + profileTypes: ProfileTypeMessage[], + profileTypeId: string, + backendType: BackendType = 'phlare' +) { return useMemo(() => { if (!profileTypes) { return 'Loading'; @@ -192,13 +206,3 @@ function useProfileName(profileTypes: ProfileTypeMessage[], profileTypeId: strin return profile.label; }, [profileTypeId, profileTypes, backendType]); } - -export function normalizeQuery(query: Query, app?: CoreApp | string) { - let normalized = defaults(query, defaultQuery); - if (app !== CoreApp.Explore && normalized.queryType === 'both') { - // In dashboards and other places, we can't show both types of graphs at the same time. - // This will also be a default when having 'both' query and adding it from explore to dashboard - normalized.queryType = 'profile'; - } - return normalized; -} diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts index 37d4ae9e29f..3d5fe90b365 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts @@ -3,6 +3,7 @@ import { Observable, of } from 'rxjs'; import { AbstractQuery, + CoreApp, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, @@ -12,7 +13,7 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import { extractLabelMatchers, toPromLikeExpr } from '../prometheus/language_utils'; -import { normalizeQuery } from './QueryEditor/QueryEditor'; +import { defaultGrafanaPyroscope, defaultPhlareQueryType } from './dataquery.gen'; import { PhlareDataSourceOptions, Query, ProfileTypeMessage, BackendType } from './types'; export class PhlareDataSource extends DataSourceWithBackend { @@ -101,6 +102,25 @@ export class PhlareDataSource extends DataSourceWithBackend { + return defaultQuery; + } +} + +export const defaultQuery: Partial = { + ...defaultGrafanaPyroscope, + queryType: defaultPhlareQueryType, +}; + +export function normalizeQuery(query: Query, app?: CoreApp | string) { + let normalized = { ...query, ...defaultQuery }; + if (app !== CoreApp.Explore && normalized.queryType === 'both') { + // In dashboards and other places, we can't show both types of graphs at the same time. + // This will also be a default when having 'both' query and adding it from explore to dashboard + normalized.queryType = 'profile'; + } + return normalized; } const grammar: Grammar = {