Skip to content

Commit 9d65777

Browse files
fquezecanova
andauthored
Add a 'filter' button next to the tooltip label of markers. (#5626)
Co-authored-by: Nazım Can Altınova <[email protected]>
1 parent 41f4de3 commit 9d65777

File tree

14 files changed

+862
-60
lines changed

14 files changed

+862
-60
lines changed

locales/en-US/app.ftl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,16 @@ MarkerTable--duration = Duration
491491
MarkerTable--name = Name
492492
MarkerTable--details = Details
493493
494+
## MarkerTooltip
495+
## This is the component for Marker Tooltip panel.
496+
497+
# This is used as the tooltip for the filter button in marker tooltips.
498+
# Variables:
499+
# $filter (String) - Search string that will be used to filter the markers.
500+
MarkerTooltip--filter-button-tooltip =
501+
.title = Only show markers matching: “{ $filter }
502+
.aria-label = Only show markers matching: “{ $filter }
503+
494504
## MenuButtons
495505
## These strings are used for the buttons at the top of the profile viewer.
496506

src/components/shared/IdleSearchField.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ export class IdleSearchField extends PureComponent<Props, State> {
9191
_onFormSubmit(e: React.FormEvent<HTMLFormElement>) {
9292
e.preventDefault();
9393
}
94+
95+
override componentDidUpdate(prevProps: Props) {
96+
// When the defaultValue prop changes externally (e.g., from Redux),
97+
// getDerivedStateFromProps will update the state value. We need to sync
98+
// _previouslyNotifiedValue to match so that subsequent changes are detected
99+
// correctly by _notifyIfChanged.
100+
if (prevProps.defaultValue !== this.props.defaultValue) {
101+
this._previouslyNotifiedValue = this.state.value;
102+
}
103+
}
104+
94105
static getDerivedStateFromProps(props: Props, state: State) {
95106
if (props.defaultValue !== state.previousDefaultValue) {
96107
return {

src/components/tooltip/Marker.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,20 @@
1616
.marker-table-value th {
1717
text-align: start;
1818
}
19+
20+
.tooltipTitleFilterButton {
21+
width: 16px;
22+
height: 16px;
23+
flex: none;
24+
padding: 0;
25+
border: none;
26+
background-color: transparent;
27+
background-image: url(../../../res/img/svg/filter.svg);
28+
cursor: pointer;
29+
margin-inline-start: 6px;
30+
opacity: 0.6;
31+
}
32+
33+
.tooltipTitleFilterButton:hover {
34+
opacity: 1;
35+
}

src/components/tooltip/Marker.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import * as React from 'react';
66
import classNames from 'classnames';
7+
import { Localized } from '@fluent/react';
78
import {
89
formatMilliseconds,
910
formatTimestamp,
@@ -20,6 +21,7 @@ import {
2021
getProcessIdToNameMap,
2122
getThreadSelectorsFromThreadsKey,
2223
} from 'firefox-profiler/selectors';
24+
import { changeMarkersSearchString } from 'firefox-profiler/actions/profile-view';
2325

2426
import {
2527
TooltipNetworkMarkerPhases,
@@ -96,10 +98,15 @@ type StateProps = {
9698
readonly processIdToNameMap: Map<Pid, string>;
9799
readonly markerSchemaByName: MarkerSchemaByName;
98100
readonly getMarkerLabel: (param: MarkerIndex) => string;
101+
readonly getMarkerSearchTerm: (param: MarkerIndex) => string;
99102
readonly categories: CategoryList;
100103
};
101104

102-
type Props = ConnectedProps<OwnProps, StateProps, {}>;
105+
type DispatchProps = {
106+
readonly changeMarkersSearchString: typeof changeMarkersSearchString;
107+
};
108+
109+
type Props = ConnectedProps<OwnProps, StateProps, DispatchProps>;
103110

104111
// Maximum image size of a tooltip field.
105112
const MAXIMUM_IMAGE_SIZE = 350;
@@ -473,10 +480,12 @@ class MarkerTooltipContents extends React.PureComponent<Props> {
473480
return null;
474481
}
475482

476-
_renderTitle(): string {
477-
const { markerIndex, getMarkerLabel } = this.props;
478-
return getMarkerLabel(markerIndex);
479-
}
483+
_onFilterButtonClick = () => {
484+
const { markerIndex, getMarkerSearchTerm, changeMarkersSearchString } =
485+
this.props;
486+
const searchTerm = getMarkerSearchTerm(markerIndex);
487+
changeMarkersSearchString(searchTerm);
488+
};
480489

481490
/**
482491
* Often-times component logic is split out into several different components. This
@@ -501,13 +510,31 @@ class MarkerTooltipContents extends React.PureComponent<Props> {
501510
* a short list of rendering strategies, in the order they appear.
502511
*/
503512
override render() {
504-
const { className } = this.props;
513+
const { className, markerIndex, getMarkerLabel, getMarkerSearchTerm } =
514+
this.props;
515+
const markerLabel = getMarkerLabel(markerIndex);
516+
const searchTerm = getMarkerSearchTerm(markerIndex);
505517
return (
506518
<div className={classNames('tooltipMarker', className)}>
507519
<div className="tooltipHeader">
508520
<div className="tooltipOneLine">
509521
{this._maybeRenderMarkerDuration()}
510-
<div className="tooltipTitle">{this._renderTitle()}</div>
522+
<div className="tooltipTitle">
523+
<span className="tooltipTitleText">{markerLabel}</span>
524+
<Localized
525+
id="MarkerTooltip--filter-button-tooltip"
526+
vars={{ filter: searchTerm }}
527+
attrs={{ title: true, 'aria-label': true }}
528+
>
529+
<button
530+
className="tooltipTitleFilterButton"
531+
type="button"
532+
title={`Only show markers matching: “${searchTerm}”`}
533+
aria-label={`Only show markers matching: “${searchTerm}”`}
534+
onClick={this._onFilterButtonClick}
535+
/>
536+
</Localized>
537+
</div>
511538
</div>
512539
</div>
513540
<TooltipDetails>
@@ -522,7 +549,11 @@ class MarkerTooltipContents extends React.PureComponent<Props> {
522549
}
523550
}
524551

525-
export const TooltipMarker = explicitConnect<OwnProps, StateProps, {}>({
552+
export const TooltipMarker = explicitConnect<
553+
OwnProps,
554+
StateProps,
555+
DispatchProps
556+
>({
526557
mapStateToProps: (state, props) => {
527558
const selectors = getThreadSelectorsFromThreadsKey(props.threadsKey);
528559
return {
@@ -536,8 +567,10 @@ export const TooltipMarker = explicitConnect<OwnProps, StateProps, {}>({
536567
processIdToNameMap: getProcessIdToNameMap(state),
537568
markerSchemaByName: getMarkerSchemaByName(state),
538569
getMarkerLabel: selectors.getMarkerTooltipLabelGetter(state),
570+
getMarkerSearchTerm: selectors.getMarkerSearchTermGetter(state),
539571
categories: getCategories(state),
540572
};
541573
},
574+
mapDispatchToProps: { changeMarkersSearchString },
542575
component: MarkerTooltipContents,
543576
});

src/components/tooltip/Tooltip.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,22 @@
3333
}
3434

3535
.tooltipTitle {
36+
display: flex;
3637
overflow: hidden;
38+
min-width: 0;
3739
flex: 1;
40+
align-items: center;
3841
text-overflow: ellipsis;
3942
}
4043

44+
.tooltipTitleText {
45+
overflow: hidden;
46+
min-width: 0;
47+
flex: 1;
48+
text-overflow: ellipsis;
49+
white-space: nowrap;
50+
}
51+
4152
.tooltipSwatch {
4253
display: inline-block;
4354
width: 10px;

src/profile-logic/marker-schema.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
MarkerFormatType,
2121
MarkerSchema,
2222
MarkerSchemaByName,
23+
MarkerSchemaField,
2324
Marker,
2425
MarkerIndex,
2526
MarkerPayload,
@@ -366,6 +367,111 @@ export function getLabelGetter(
366367
};
367368
}
368369

370+
/**
371+
* Extract the value of the first field from a marker's tooltipLabel schema.
372+
* This is used for filtering markers by their primary identifying value.
373+
* Falls back to the marker name if no tooltipLabel is defined in the schema.
374+
*
375+
* This function should only be used behind a selector.
376+
*/
377+
export function getSearchTermGetter(
378+
getMarker: (markerIndex: MarkerIndex) => Marker,
379+
markerSchemaByName: MarkerSchemaByName,
380+
stringTable: StringTable
381+
): (markerIndex: MarkerIndex) => string {
382+
// Cache the search terms as they are created.
383+
const markerIndexToSearchTerm: Map<MarkerIndex, string> = new Map();
384+
385+
return (markerIndex: MarkerIndex) => {
386+
let searchTerm: string | undefined | null =
387+
markerIndexToSearchTerm.get(markerIndex);
388+
389+
if (searchTerm === undefined) {
390+
const marker = getMarker(markerIndex);
391+
const schemaName = marker.data ? marker.data.type : null;
392+
393+
if (schemaName) {
394+
const schema = markerSchemaByName[schemaName];
395+
if (schema?.tooltipLabel) {
396+
// Extract the first field from the tooltipLabel
397+
// e.g., "{marker.data.eventType} - DOMEvent" -> extract marker.data.eventType value
398+
searchTerm = parseFirstField(
399+
marker,
400+
schema,
401+
stringTable,
402+
schema.tooltipLabel
403+
);
404+
}
405+
}
406+
407+
// Fall back to the marker name if no schema, tooltipLabel, or extraction failed
408+
if (!searchTerm) {
409+
searchTerm = marker.name;
410+
}
411+
412+
// Cache this result.
413+
markerIndexToSearchTerm.set(markerIndex, searchTerm);
414+
}
415+
416+
return searchTerm;
417+
};
418+
}
419+
420+
/**
421+
* Parse the first field from a label string and extract its value from the marker.
422+
* Returns null if no field is found or if the field is not a data field (marker.data.*).
423+
*
424+
* e.g., "{marker.data.eventType} - DOMEvent" extracts the value of marker.data.eventType
425+
*/
426+
function parseFirstField(
427+
marker: Marker,
428+
markerSchema: MarkerSchema,
429+
stringTable: StringTable,
430+
label: string
431+
): string | null {
432+
// Split the label on the "{key}" capture groups.
433+
const splits = label.split(/{([^}]+)}/);
434+
435+
if (splits.length < 2) {
436+
// No fields in the label
437+
return null;
438+
}
439+
440+
// The first field is at index 1 (even indices are literal strings)
441+
const firstField = splits[1].trim();
442+
const keys = firstField.split('.');
443+
444+
// We only extract from marker.data.* fields (3 parts: marker, data, key)
445+
if (keys.length !== 3) {
446+
return null;
447+
}
448+
449+
const [markerStr, markerKey, payloadKey] = keys;
450+
if (markerStr !== 'marker' || markerKey !== 'data' || !payloadKey) {
451+
return null;
452+
}
453+
454+
// Access the payload data
455+
const field = markerSchema.fields?.find(
456+
(f: MarkerSchemaField) => f.key === payloadKey
457+
);
458+
if (!field) {
459+
return null;
460+
}
461+
462+
const payload = marker.data;
463+
if (!payload) {
464+
return null;
465+
}
466+
467+
const value = (payload as any)[payloadKey];
468+
if (value === undefined || value === null) {
469+
return null;
470+
}
471+
472+
return formatFromMarkerSchema(payload.type, field.format, value, stringTable);
473+
}
474+
369475
/**
370476
* This function formats a string from a marker type and a value.
371477
* If you wish to get markup instead, have a look at

src/selectors/per-thread/markers.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import * as MarkerData from '../../profile-logic/marker-data';
99
import * as MarkerTimingLogic from '../../profile-logic/marker-timing';
1010
import * as ProfileSelectors from '../profile';
1111
import { getRightClickedMarkerInfo } from '../right-clicked-marker';
12-
import { getLabelGetter } from '../../profile-logic/marker-schema';
12+
import {
13+
getLabelGetter,
14+
getSearchTermGetter,
15+
} from '../../profile-logic/marker-schema';
1316
import { getInclusiveSampleIndexRangeForSelection } from '../../profile-logic/profile-data';
1417

1518
import type { BasicThreadSelectorsPerThread } from './thread';
@@ -445,6 +448,20 @@ export function getMarkerSelectorsPerThread(
445448
getLabelGetter
446449
);
447450

451+
/**
452+
* This getter extracts the first field value from a marker's tooltipLabel schema
453+
* to use as a search term for filtering. Falls back to the marker name if no
454+
* tooltipLabel is defined.
455+
*/
456+
const getMarkerSearchTermGetter: Selector<
457+
(markerIndex: MarkerIndex) => string
458+
> = createSelector(
459+
getMarkerGetter,
460+
ProfileSelectors.getMarkerSchemaByName,
461+
ProfileSelectors.getStringTable,
462+
getSearchTermGetter
463+
);
464+
448465
/**
449466
* This organizes the result of the previous selector in rows to be nicely
450467
* displayed in the marker chart.
@@ -750,6 +767,7 @@ export function getMarkerSelectorsPerThread(
750767
getMarkerTooltipLabelGetter,
751768
getMarkerTableLabelGetter,
752769
getMarkerLabelToCopyGetter,
770+
getMarkerSearchTermGetter,
753771
getMarkerChartTimingAndBuckets,
754772
getCommittedRangeFilteredMarkerIndexes,
755773
getTimelineOverviewMarkerIndexes,

0 commit comments

Comments
 (0)