// 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 {Icon} from '@iconify/react'; import cx from 'classnames'; import {Button, Input, Select, type SelectItem} from '@parca/components'; import {TEST_IDS, testId} from '@parca/test-utils'; import {useProfileViewContext} from '../../context/ProfileViewContext'; import {getPresetByKey, getPresetsForProfileType, isPresetKey} from './filterPresets'; import {useProfileFilters, type ProfileFilter} from './useProfileFilters'; export const isFilterComplete = (filter: ProfileFilter): boolean => { // For preset filters, only need type and value if (filter.type != null && isPresetKey(filter.type)) { return filter.value !== '' && filter.type != null; } // For regular filters, need all fields return ( filter.value !== '' && filter.type != null && filter.field != null && filter.matchType != null ); }; const getFilterTypeItems = (currentProfileType?: string): SelectItem[] => [ { key: 'stack', element: { active: <>Stack Filter, expanded: ( <> Stack Filter
Filters entire call stacks ), }, }, { key: 'frame', element: { active: <>Frame Filter, expanded: ( <> Frame Filter
Filters individual frames ), }, }, ...getPresetsForProfileType(currentProfileType).map(preset => ({ key: preset.key, element: { active: <>{preset.name}, expanded: ( <> {preset.name}
{preset.description} ), }, })), ]; const fieldItems: SelectItem[] = [ { key: 'function_name', element: { active: <>Function, expanded: <>Function Name, }, }, { key: 'binary', element: { active: <>Binary, expanded: <>Binary/Executable Name, }, }, { key: 'system_name', element: { active: <>System Name, expanded: <>System Name, }, }, { key: 'filename', element: { active: <>Filename, expanded: <>Source Filename, }, }, { key: 'address', element: { active: <>Address, expanded: <>Memory Address, }, }, { key: 'line_number', element: { active: <>Line Number, expanded: <>Source Line Number, }, }, ]; const stringMatchTypeItems: SelectItem[] = [ { key: 'equal', element: { active: <>Equals, expanded: <>Equals, }, }, { key: 'not_equal', element: { active: <>Not Equals, expanded: <>Not Equals, }, }, { key: 'contains', element: { active: <>Contains, expanded: <>Contains, }, }, { key: 'not_contains', element: { active: <>Not Contains, expanded: <>Not Contains, }, }, { key: 'starts_with', element: { active: <>Starts With, expanded: <>Starts With, }, }, { key: 'not_starts_with', element: { active: <>Not Starts With, expanded: <>Not Starts With, }, }, ]; const numberMatchTypeItems: SelectItem[] = [ { key: 'equal', element: { active: <>Equals, expanded: <>Equals, }, }, { key: 'not_equal', element: { active: <>Not Equals, expanded: <>Not Equals, }, }, ]; export interface ProfileFiltersProps { readOnly?: boolean; } const ProfileFilters = ({readOnly = false}: ProfileFiltersProps = {}): JSX.Element => { const {profileSource} = useProfileViewContext(); const currentProfileType = profileSource?.ProfileType()?.toString(); const filterTypeItems = getFilterTypeItems(currentProfileType); const { localFilters, appliedFilters, hasUnsavedChanges, onApplyFilters, addFilter, removeFilter, updateFilter, resetFilters, } = useProfileFilters(); const handleKeyDown = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { e.preventDefault(); if (e.currentTarget.value.trim() === '') { return; } onApplyFilters(); } }; const filtersToRender = localFilters.length > 0 ? localFilters : appliedFilters ?? []; return (
{filtersToRender.map(filter => { const isNumberField = filter.field === 'address' || filter.field === 'line_number'; const matchTypeItems = isNumberField ? numberMatchTypeItems : stringMatchTypeItems; const isPresetFilter = filter.type != null && isPresetKey(filter.type); return (
{ const newField = key as ProfileFilter['field']; const isNewFieldNumber = newField === 'address' || newField === 'line_number'; const isCurrentFieldNumber = filter.field === 'address' || filter.field === 'line_number'; if (isNewFieldNumber !== isCurrentFieldNumber) { updateFilter(filter.id, { field: newField, matchType: 'equal', }); } else { updateFilter(filter.id, {field: newField}); } }} className={cx( 'rounded-none border-r-0 w-32 gap-0 focus:z-50 focus:relative focus:outline-1', readOnly ? '' : 'pr-1' )} hideCaretDropdown={readOnly} /> updateFilter(filter.id, {value: e.target.value})} onKeyDown={handleKeyDown} className="rounded-none w-36 text-sm focus:outline-1" {...testId(TEST_IDS.FILTER_VALUE_INPUT)} /> )} {!readOnly && ( )}
); })} {!readOnly && localFilters.length > 0 && ( )} {!readOnly && localFilters.length === 0 && (appliedFilters?.length ?? 0) === 0 && ( )}
{!readOnly && localFilters.length > 0 && ( )}
); }; export default ProfileFilters;