// Copyright 2022 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {Dispatch, SetStateAction, useCallback, useMemo, useRef, useState} from 'react'; import {RpcError} from '@protobuf-ts/runtime-rpc'; import {useQueryState as useNuqsQueryState} from 'nuqs'; import {ProfileTypesRequest, ProfileTypesResponse, QueryServiceClient} from '@parca/client'; import {DateTimeRange, IconButton, useGrpcMetadata, useParcaContext} from '@parca/components'; import {CloseIcon} from '@parca/icons'; import {Query} from '@parca/parser'; import {TEST_IDS, testId} from '@parca/test-utils'; import {millisToProtoTimestamp} from '@parca/utilities'; import { ProfileFilter, useProfileFilters, } from '../ProfileView/components/ProfileFilters/useProfileFilters'; import {QueryControls} from '../QueryControls'; import {LabelsQueryProvider, useLabelsQueryProvider} from '../contexts/LabelsQueryProvider'; import {UnifiedLabelsProvider} from '../contexts/UnifiedLabelsContext'; import {stringParam} from '../hooks/urlParsers'; import {useLabelNames} from '../hooks/useLabels'; import {useQueryState} from '../hooks/useQueryState'; import useGrpcQuery from '../useGrpcQuery'; import {MetricsGraphSection} from './MetricsGraphSection'; import {useAutoQuerySelector} from './useAutoQuerySelector'; export interface QuerySelection { expression: string; from: number; to: number; timeSelection: string; sumBy?: string[]; mergeFrom?: string; mergeTo?: string; } interface ProfileSelectorFeatures { showMetricsGraph: boolean; showSumBySelector?: boolean; showProfileTypeSelector?: boolean; disableProfileTypesDropdown?: boolean; } interface ProfileSelectorProps extends ProfileSelectorFeatures { queryClient: QueryServiceClient; closeProfile: () => void; enforcedProfileName: string; comparing: boolean; setDisplayHideMetricsGraphButton?: Dispatch>; suffix?: '_a' | '_b'; // For comparison mode onSearchHook?: () => void; } export interface IProfileTypesResult { loading: boolean; data?: ProfileTypesResponse; error?: RpcError; } export const useProfileTypes = ( client: QueryServiceClient, start?: number, end?: number ): IProfileTypesResult => { const metadata = useGrpcMetadata(); const metadataString = useMemo(() => JSON.stringify(metadata), [metadata]); const request: ProfileTypesRequest = {}; if (start != null && end != null) { request.start = millisToProtoTimestamp(start); request.end = millisToProtoTimestamp(end); } const {isLoading, data, error} = useGrpcQuery({ key: ['profileTypes', metadataString, start, end], queryFn: async abort => { const {response} = await client.profileTypes(request, { meta: metadata, abort, }); return response; }, }); return {loading: isLoading, data, error: error as RpcError}; }; const ProfileSelector = ({ queryClient, closeProfile, enforcedProfileName, comparing, showMetricsGraph = true, showSumBySelector = true, showProfileTypeSelector = true, setDisplayHideMetricsGraphButton, suffix, onSearchHook, }: ProfileSelectorProps): JSX.Element => { const {externalProfilerComponent, additionalMetricsGraph} = useParcaContext(); const [queryBrowserMode, setRawQueryBrowserMode] = useNuqsQueryState( 'query_browser_mode', stringParam.withOptions({history: 'replace'}) ); const setQueryBrowserMode = useCallback( (mode: string | null) => { void setRawQueryBrowserMode(mode); }, [setRawQueryBrowserMode] ); const profileFilterDefaults = externalProfilerComponent?.profileFilterDefaults as | ProfileFilter[] | undefined; const {forceApplyFilters} = useProfileFilters(); const handleProfileTypeChange = useCallback(() => { if (profileFilterDefaults != null && profileFilterDefaults.length > 0) { forceApplyFilters(profileFilterDefaults); } }, [forceApplyFilters, profileFilterDefaults]); // Use the new useQueryState hook - reads directly from URL params const { querySelection, draftSelection, setDraftExpression, setDraftTimeRange, setDraftSumBy, setDraftProfileName, setDraftMatchers, commitDraft, profileSelection, setProfileSelection, sumByLoading, draftParsedQuery, } = useQueryState({suffix, onProfileTypeChange: handleProfileTypeChange}); // Use draft state for local state instead of committed state const [timeRangeSelection, setTimeRangeSelection] = useState( DateTimeRange.fromRangeKey(draftSelection.timeSelection, draftSelection.from, draftSelection.to) ); // Sync local timeRangeSelection when URL state changes externally (e.g., "Switch to 1 minute" button) const [prevQueryTimeSelection, setPrevQueryTimeSelection] = useState( querySelection.timeSelection ); const [prevQueryFrom, setPrevQueryFrom] = useState(querySelection.from); const [prevQueryTo, setPrevQueryTo] = useState(querySelection.to); if ( prevQueryTimeSelection !== querySelection.timeSelection || prevQueryFrom !== querySelection.from || prevQueryTo !== querySelection.to ) { setPrevQueryTimeSelection(querySelection.timeSelection); setPrevQueryFrom(querySelection.from); setPrevQueryTo(querySelection.to); setTimeRangeSelection( DateTimeRange.fromRangeKey( querySelection.timeSelection, querySelection.from, querySelection.to ) ); } const [queryExpressionString, setQueryExpressionString] = useState(draftSelection.expression); const [advancedModeForQueryBrowser, setAdvancedModeForQueryBrowser] = useState( queryBrowserMode === 'advanced' ); // Handler to update draft when time range changes const handleTimeRangeChange = useCallback( (range: DateTimeRange) => { setTimeRangeSelection(range); setDraftTimeRange(range.getFromMs(), range.getToMs(), range.getRangeKey()); }, [setDraftTimeRange] ); const profileType = useMemo(() => { return Query.parse(queryExpressionString).profileType(); }, [queryExpressionString]); const from = timeRangeSelection.getFromMs(); const to = timeRangeSelection.getToMs(); const { loading: profileTypesLoading, data: profileTypesData, error, } = useProfileTypes(queryClient, from, to); const {result} = useLabelNames(queryClient, profileType.toString(), from, to); const labels = useMemo(() => { return result.response?.labelNames === undefined ? [] : result.response.labelNames; }, [result]); const [prevEnforcedProfileName, setPrevEnforcedProfileName] = useState(enforcedProfileName); const [prevQueryExpression, setPrevQueryExpression] = useState(querySelection.expression); if ( prevEnforcedProfileName !== enforcedProfileName || prevQueryExpression !== querySelection.expression ) { setPrevEnforcedProfileName(enforcedProfileName); setPrevQueryExpression(querySelection.expression); if (enforcedProfileName !== '') { const [q, changed] = Query.parse(querySelection.expression).setProfileName( enforcedProfileName ); if (changed) { setQueryExpressionString(q.toString()); } else { setQueryExpressionString(querySelection.expression); } } else { setQueryExpressionString(querySelection.expression); } } const enforcedProfileNameQuery = (): Query => { const pq = Query.parse(queryExpressionString); const [q] = pq.setProfileName(enforcedProfileName); return q; }; const query = enforcedProfileName !== '' ? enforcedProfileNameQuery() : Query.parse(queryExpressionString); const selectedProfileName = query.profileName(); const setQueryExpression = (updateTs = false): void => { if (onSearchHook != null) { onSearchHook(); } // When updateTs is true, re-evaluate the time range to current values if (updateTs) { // Force re-evaluation of time range (important for relative ranges like "last 15 minutes") const currentFrom = timeRangeSelection.getFromMs(true); const currentTo = timeRangeSelection.getToMs(true); const currentRangeKey = timeRangeSelection.getRangeKey(); // Commit with refreshed time range commitDraft({ from: currentFrom, to: currentTo, timeSelection: currentRangeKey, }); } else { // Commit the draft with existing values commitDraft(); } }; const setMatchersString = (matchers: string): void => { // Update draft state only setDraftMatchers(matchers); setQueryExpressionString(`${selectedProfileName}{${matchers}}`); }; const setProfileName = (profileName: string | undefined): void => { if (profileName === undefined) { return; } // Update draft state only setDraftProfileName(profileName); const [newQuery, changed] = query.setProfileName(profileName); if (changed) { const q = newQuery.toString(); setQueryExpressionString(q); } }; useAutoQuerySelector({ selectedProfileName, profileTypesData, setProfileName, setQueryExpression, querySelection, loading: sumByLoading, defaultProfileType: externalProfilerComponent?.defaultProfileType, }); const searchDisabled = queryExpressionString === undefined || queryExpressionString === '' || queryExpressionString === '{}'; const queryBrowserRef = useRef(null); const sumByRef = useRef(null); return ( <>
{additionalMetricsGraph?.({ querySelection, queryClient, suffix, timeRange: timeRangeSelection, onTimeRangeChange: handleTimeRangeChange, commitTimeRange: () => setQueryExpression(true), selectTimeRange: (range: DateTimeRange) => { commitDraft({ from: range.getFromMs(), to: range.getToMs(), timeSelection: range.getRangeKey(), }); }, })} {comparing && (
closeProfile()} icon={} {...testId(TEST_IDS.COMPARE_CLOSE_BUTTON)} />
)}
); }; export default ProfileSelector; const LabelsSource = ({children}: {children: React.ReactNode}): JSX.Element => { const { labelNames, labelValues, isLabelNamesLoading, isLabelValuesLoading, refetchLabelValues, refetchLabelNames, currentLabelName, setCurrentLabelName, suffix, } = useLabelsQueryProvider(); return ( {children} ); };