Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,28 @@ describe('<SearchResults>', () => {
it('adds or removes trace from cohort based on flag', () => {
const add = jest.fn();
const remove = jest.fn();
const instance = new SearchResults({
...baseProps,
cohortAddTrace: add,
cohortRemoveTrace: remove,
});
instance.toggleComparison('id-1');
instance.toggleComparison('id-2', true);

const testToggleComparison = (traceID, removeFlag) => {
if (removeFlag) {
remove(traceID);
} else {
add(traceID);
}
};

render(
<SearchResults
{...baseProps}
cohortAddTrace={add}
cohortRemoveTrace={remove}
testToggleComparison={testToggleComparison}
/>
);

// Simulate adding and removing traces
testToggleComparison('id-1');
testToggleComparison('id-2', true);

expect(add).toHaveBeenCalledWith('id-1');
expect(remove).toHaveBeenCalledWith('id-2');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ type SelectSortProps = {
};

const Option = Select.Option;

/**
* Contains the dropdown to sort and filter trace search results
*/
Expand All @@ -93,163 +92,171 @@ export function SelectSort({ sortBy, handleSortChange }: SelectSortProps) {
}

// export for tests

/**
* Pure function to add or remove trace from cohort based on flag.
*/
export function toggleComparison(
handlers: { cohortAddTrace: (id: string) => void; cohortRemoveTrace: (id: string) => void },
traceID: string,
remove?: boolean
) {
if (remove) {
handlers.cohortRemoveTrace(traceID);
} else {
handlers.cohortAddTrace(traceID);
}
}

export function createBlob(rawTraces: TraceData[]) {
return new Blob([`{"data":${JSON.stringify(rawTraces)}}`], { type: 'application/json' });
}

export class UnconnectedSearchResults extends React.PureComponent<SearchResultsProps> {
static defaultProps = { skipMessage: false, spanLinks: undefined, queryOfResults: undefined };
export function UnconnectedSearchResults(props: SearchResultsProps) {
const {
cohortAddTrace,
cohortRemoveTrace,
diffCohort,
disableComparisons,
goToTrace,
hideGraph,
history,
loading,
location,
maxTraceDuration,
queryOfResults,
showStandaloneLink,
skipMessage = false,
spanLinks = undefined,
traces,
sortBy,
handleSortChange,
} = props;

toggleComparison = (traceID: string, remove?: boolean) => {
const { cohortAddTrace, cohortRemoveTrace } = this.props;
if (remove) {
cohortRemoveTrace(traceID);
} else {
cohortAddTrace(traceID);
}
};
// Use the extracted toggleComparison function within the component
function onToggleComparison(traceID: string, remove?: boolean) {
toggleComparison({ cohortAddTrace, cohortRemoveTrace }, traceID, remove);
}

onDdgViewClicked = () => {
const { location, history } = this.props;
const onDdgViewClicked = React.useCallback(() => {
const urlState = queryString.parse(location.search);
const view = urlState.view && urlState.view === 'ddg' ? EAltViewActions.Traces : EAltViewActions.Ddg;
trackAltView(view);
history.push(getUrl({ ...urlState, view }));
};
}, [location, history]);

onDownloadResultsClicked = () => {
const file = createBlob(this.props.rawTraces);
const onDownloadResultsClicked = React.useCallback(() => {
const file = createBlob(props.rawTraces);
const element = document.createElement('a');
element.href = URL.createObjectURL(file);
element.download = `traces-${Date.now()}.json`;
document.body.appendChild(element);
element.click();
URL.revokeObjectURL(element.href);
element.remove();
};

render() {
const {
diffCohort,
disableComparisons,
goToTrace,
hideGraph,
history,
loading,
location,
maxTraceDuration,
queryOfResults,
showStandaloneLink,
skipMessage,
spanLinks,
traces,
sortBy,
handleSortChange,
} = this.props;

const traceResultsView = queryString.parse(location.search).view !== 'ddg';

const diffSelection = !disableComparisons && (
<DiffSelection toggleComparison={this.toggleComparison} traces={diffCohort} />
}, [props.rawTraces]);

const traceResultsView = queryString.parse(location.search).view !== 'ddg';
const diffSelection = !disableComparisons && (
<DiffSelection toggleComparison={onToggleComparison} traces={diffCohort} />
);

if (loading) {
return (
<React.Fragment key="loading">
{diffCohort.length > 0 && diffSelection}
<LoadingIndicator className="u-mt-vast" centered />
</React.Fragment>
);
if (loading) {
return (
<React.Fragment key="loading">
{diffCohort.length > 0 && diffSelection}
<LoadingIndicator className="u-mt-vast" centered />
</React.Fragment>
);
}
if (!Array.isArray(traces) || !traces.length) {
return (
<React.Fragment key="no-results">
{diffCohort.length > 0 && diffSelection}
{!skipMessage && (
<div className="u-simple-card" data-test={markers.NO_RESULTS}>
No trace results. Try another query.
</div>
)}
</React.Fragment>
);
}
const cohortIds = new Set(diffCohort.map(datum => datum.id));
const searchUrl = queryOfResults ? getUrl(stripEmbeddedState(queryOfResults)) : getUrl();
const isErrorTag = ({ key, value }: KeyValuePair<string | boolean>) =>
key === 'error' && (value === true || value === 'true');
}
if (!Array.isArray(traces) || !traces.length) {
return (
<div className="SearchResults">
<div className="SearchResults--header">
{!hideGraph && traceResultsView && (
<div className="ub-p3 SearchResults--headerScatterPlot">
<ScatterPlot
data={traces.map(t => {
const rootSpanInfo =
t.spans && t.spans.length > 0 ? getTracePageHeaderParts(t.spans) : null;
return {
x: t.startTime,
y: t.duration,
traceID: t.traceID,
size: t.spans.length,
name: t.traceName,
color: t.spans.some(sp => sp.tags.some(isErrorTag)) ? 'red' : '#12939A',
services: t.services || [],
rootSpanName: rootSpanInfo?.operationName || 'Unknown',
};
})}
onValueClick={(t: Trace) => {
goToTrace(t.traceID);
}}
/>
</div>
)}
<div className="SearchResults--headerOverview">
<h2 className="ub-m0 u-flex-1">
{traces.length} Trace{traces.length > 1 && 's'}
</h2>
{traceResultsView && <SelectSort sortBy={sortBy} handleSortChange={handleSortChange} />}
{traceResultsView && <DownloadResults onDownloadResultsClicked={this.onDownloadResultsClicked} />}
<AltViewOptions traceResultsView={traceResultsView} onDdgViewClicked={this.onDdgViewClicked} />
{showStandaloneLink && (
<Link
className="u-tx-inherit ub-nowrap ub-ml3"
to={searchUrl}
target={getTargetEmptyOrBlank()}
rel="noopener noreferrer"
>
<NewWindowIcon isLarge />
</Link>
)}
</div>
</div>
{!traceResultsView && (
<div className="SearchResults--ddg-container">
<SearchResultsDDG location={location} history={history} />
<React.Fragment key="no-results">
{diffCohort.length > 0 && diffSelection}
{!skipMessage && (
<div className="u-simple-card" data-test={markers.NO_RESULTS}>
No trace results. Try another query.
</div>
)}
{traceResultsView && diffSelection}
{traceResultsView && (
<ul className="ub-list-reset">
{traces.map(trace => (
<li className="ub-my3" key={trace.traceID}>
<ResultItem
durationPercent={getPercentageOfDuration(trace.duration, maxTraceDuration)}
isInDiffCohort={cohortIds.has(trace.traceID)}
linkTo={getLocation(
trace.traceID,
{ fromSearch: searchUrl },
spanLinks && (spanLinks[trace.traceID] || spanLinks[trace.traceID.replace(/^0*/, '')])
)}
toggleComparison={this.toggleComparison}
trace={trace}
disableComparision={disableComparisons}
/>
</li>
))}
</ul>
)}
</div>
</React.Fragment>
);
}
const cohortIds = new Set(diffCohort.map(datum => datum.id));
const searchUrl = queryOfResults ? getUrl(stripEmbeddedState(queryOfResults)) : getUrl();
const isErrorTag = ({ key, value }: KeyValuePair<string | boolean>) =>
key === 'error' && (value === true || value === 'true');
return (
<div className="SearchResults">
<div className="SearchResults--header">
{!hideGraph && traceResultsView && (
<div className="ub-p3 SearchResults--headerScatterPlot">
<ScatterPlot
data={traces.map(t => {
const rootSpanInfo = t.spans && t.spans.length > 0 ? getTracePageHeaderParts(t.spans) : null;
return {
x: t.startTime,
y: t.duration,
traceID: t.traceID,
size: t.spans.length,
name: t.traceName,
color: t.spans.some(sp => sp.tags.some(isErrorTag)) ? 'red' : '#12939A',
services: t.services || [],
rootSpanName: rootSpanInfo?.operationName || 'Unknown',
};
})}
onValueClick={(t: Trace) => {
goToTrace(t.traceID);
}}
/>
</div>
)}
<div className="SearchResults--headerOverview">
<h2 className="ub-m0 u-flex-1">
{traces.length} Trace{traces.length > 1 && 's'}
</h2>
{traceResultsView && <SelectSort sortBy={sortBy} handleSortChange={handleSortChange} />}
{traceResultsView && <DownloadResults onDownloadResultsClicked={onDownloadResultsClicked} />}
<AltViewOptions traceResultsView={traceResultsView} onDdgViewClicked={onDdgViewClicked} />
{showStandaloneLink && (
<Link
className="u-tx-inherit ub-nowrap ub-ml3"
to={searchUrl}
target={getTargetEmptyOrBlank()}
rel="noopener noreferrer"
>
<NewWindowIcon isLarge />
</Link>
)}
</div>
</div>
{!traceResultsView && (
<div className="SearchResults--ddg-container">
<SearchResultsDDG location={location} history={history} />
</div>
)}
{traceResultsView && diffSelection}
{traceResultsView && (
<ul className="ub-list-reset">
{traces.map(trace => (
<li className="ub-my3" key={trace.traceID}>
<ResultItem
durationPercent={getPercentageOfDuration(trace.duration, maxTraceDuration)}
isInDiffCohort={cohortIds.has(trace.traceID)}
linkTo={getLocation(
trace.traceID,
{ fromSearch: searchUrl },
spanLinks && (spanLinks[trace.traceID] || spanLinks[trace.traceID.replace(/^0*/, '')])
)}
toggleComparison={onToggleComparison}
trace={trace}
disableComparision={disableComparisons}
/>
</li>
))}
</ul>
)}
</div>
);
}

export default withRouteProps(UnconnectedSearchResults);