// 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. /* eslint-disable react-hooks/refs */ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Icon} from '@iconify/react'; import cx from 'classnames'; import {useGrpcMetadata, useParcaContext} from '@parca/components'; import {TEST_IDS, testId} from '@parca/test-utils'; import {millisToProtoTimestamp, sanitizeLabelValue} from '@parca/utilities'; import CustomSelect, {SelectItem} from '../SimpleMatchers/Select'; import {useUnifiedLabels} from '../contexts/UnifiedLabelsContext'; import {useQueryState} from '../hooks/useQueryState'; interface Props { labelNames: string[]; } const PreSelectedMatchers: React.FC = ({labelNames}) => { 'use no memo'; const [labelValuesMap, setLabelValuesMap] = useState>({}); const [isLoading, setIsLoading] = useState>({}); const metadata = useGrpcMetadata(); const {queryServiceClient: parcaQueryClient} = useParcaContext(); const {suffix} = useUnifiedLabels(); const {draftSelection, setDraftMatchers, commitDraft, draftParsedQuery} = useQueryState({suffix}); const currentMatchers = draftParsedQuery?.matchersString(); const profileType = draftParsedQuery?.profileType().toString(); const start = draftSelection.from; const end = draftSelection.to; const parseCurrentMatchers = useCallback((matchersString: string): Record => { const matches = matchersString.match(/(\w+)="([^"]+)"/g); if (matches === null) return {}; return matches.reduce>( (acc, match) => { const [label, value] = match.split('='); if (label !== undefined) { acc[label] = value.replace(/"/g, ''); } return acc; }, // eslint-disable-next-line @typescript-eslint/consistent-type-assertions {} as Record ); }, []); const initialSelections = parseCurrentMatchers(currentMatchers ?? ''); const selectionsRef = useRef>(initialSelections); const commitDraftRef = useRef(commitDraft); const timeoutRef = useRef(null); useEffect(() => { commitDraftRef.current = commitDraft; }, [commitDraft]); useEffect(() => { selectionsRef.current = initialSelections; }, [initialSelections]); const fetchLabelValues = useCallback( async (labelName: string): Promise => { try { const response = await parcaQueryClient.values( { labelName, match: [], profileType, ...(start !== undefined && end !== undefined ? { start: millisToProtoTimestamp(start), end: millisToProtoTimestamp(end), } : {}), }, {meta: metadata} ).response; return sanitizeLabelValue(response.labelValues); } catch (error) { console.error('Error fetching label values:', error); return []; } }, [parcaQueryClient, metadata, profileType, start, end] ); const fetchAllLabelValues = useCallback(async (): Promise => { const newLabelValuesMap: Record = {}; const newIsLoading: Record = {}; for (const labelName of labelNames) { newIsLoading[labelName] = true; setIsLoading(prev => ({...prev, [labelName]: true})); const values = await fetchLabelValues(labelName); newLabelValuesMap[labelName] = values; newIsLoading[labelName] = false; } setLabelValuesMap(newLabelValuesMap); setIsLoading(newIsLoading); }, [labelNames, fetchLabelValues]); useEffect(() => { void fetchAllLabelValues(); }, [fetchAllLabelValues]); const updateMatcherString = useCallback(() => { const matcherParts = Object.entries(selectionsRef.current) .filter(([_, v]) => v !== null && v !== '') .map(([ln, v]) => `${ln}="${v as string}"`); const matcherString = matcherParts.join(','); setDraftMatchers(matcherString); if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { commitDraftRef.current(); }, 300); }, [setDraftMatchers]); const handleSelection = useCallback( (labelName: string, value: string | null): void => { selectionsRef.current = { ...selectionsRef.current, [labelName]: value, }; updateMatcherString(); }, [updateMatcherString] ); const handleReset = useCallback( (labelName: string): void => { handleSelection(labelName, null); }, [handleSelection] ); const transformValuesForSelect = useCallback((values: string[]): SelectItem[] => { return values.map(value => ({ key: value, element: {active: <>{value}, expanded: <>{value}}, })); }, []); return (
{labelNames.map(labelName => (
{labelName}
handleSelection(labelName, value)} selectedKey={selectionsRef.current[labelName] ?? undefined} className={cx( 'rounded-l-none border-l-0', selectionsRef.current[labelName] != null && 'border-r-0 rounded-r-none' )} loading={isLoading[labelName] ?? false} /> {selectionsRef.current[labelName] != null && ( )}
))}
); }; export default PreSelectedMatchers;