Merge pull request #11951 from backstage/search/track-result-clicks
[Search] Track `discover` event and result rank
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
It's now possible to pass result item components a `rank`, which is captured by the analytics API when a user clicks on a search result. To apply this change, update your `/packages/app/src/components/search/SearchPage.tsx` in the following way:
|
||||
|
||||
```diff
|
||||
// ...
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
- {results.map(({ type, document, highlight }) => {
|
||||
+ {results.map(({ type, document, highlight, rank }) => {
|
||||
switch (type) {
|
||||
case 'software-catalog':
|
||||
return (
|
||||
<CatalogSearchResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
+ rank={rank}
|
||||
/>
|
||||
);
|
||||
case 'techdocs':
|
||||
return (
|
||||
<TechDocsSearchResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
+ rank={rank}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
+ rank={rank}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</SearchResult>
|
||||
// ...
|
||||
```
|
||||
|
||||
If you have implemented a custom Search Modal or other custom search experience, you will want to make similar changes in those components.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-common': patch
|
||||
---
|
||||
|
||||
Added an optional `rank` attribute to the `Result` type. This represents the result rank (starting at 1) for a given result in a result set for a given search.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': patch
|
||||
'@backstage/plugin-search-backend-module-pg': patch
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
The provided search engine now adds a pagination-aware `rank` value to all results.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-search': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
In order to simplify analytics on top of the search experience in Backstage, the provided `<*ResultListItem />` component now captures a `discover` analytics event instead of a `click` event. This event includes the result rank as its `value` and, like a click, the URL/path clicked to as its `to` attribute.
|
||||
@@ -52,11 +52,12 @@ learn how to contribute the integration yourself!
|
||||
The following table summarizes events that, depending on the plugins you have
|
||||
installed, may be captured.
|
||||
|
||||
| Action | Provided By | Subject |
|
||||
| ---------- | -------------- | --------------------------------------------------- |
|
||||
| `navigate` | Backstage Core | The URL of the page that was navigated to |
|
||||
| `click` | Backstage Core | The text of the link that was clicked on |
|
||||
| `search` | Backstage Core | The search term entered in any search bar component |
|
||||
| Action | Subject | Other Notes |
|
||||
| ---------- | --------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `navigate` | The URL of the page that was navigated to | |
|
||||
| `click` | The text of the link that was clicked on | The `to` attribute represents the URL clicked to |
|
||||
| `search` | The search term entered in any search bar component | The `searchTypes` attribute holds `types` constraining the search |
|
||||
| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided |
|
||||
|
||||
If there is an event you'd like to see captured, please [open an
|
||||
issue][add-event] describing the event you want to see and the questions it
|
||||
|
||||
@@ -184,7 +184,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
{results.map(({ type, document, highlight }) => {
|
||||
{results.map(({ type, document, highlight, rank }) => {
|
||||
let resultItem;
|
||||
switch (type) {
|
||||
case 'software-catalog':
|
||||
@@ -194,6 +194,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -204,6 +205,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -213,6 +215,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ const SearchPage = () => {
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
{results.map(({ type, document, highlight }) => {
|
||||
{results.map(({ type, document, highlight, rank }) => {
|
||||
switch (type) {
|
||||
case 'software-catalog':
|
||||
return (
|
||||
@@ -141,6 +141,7 @@ const SearchPage = () => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
case 'techdocs':
|
||||
@@ -150,6 +151,7 @@ const SearchPage = () => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -158,6 +160,7 @@ const SearchPage = () => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-1
@@ -112,7 +112,7 @@ const SearchPage = () => {
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
{results.map(({ type, document, highlight }) => {
|
||||
{results.map(({ type, document, highlight, rank }) => {
|
||||
switch (type) {
|
||||
case 'software-catalog':
|
||||
return (
|
||||
@@ -120,6 +120,7 @@ const SearchPage = () => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
case 'techdocs':
|
||||
@@ -128,6 +129,7 @@ const SearchPage = () => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -136,6 +138,7 @@ const SearchPage = () => {
|
||||
key={document.location}
|
||||
result={document}
|
||||
highlight={highlight}
|
||||
rank={rank}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ export interface CatalogSearchResultListItemProps {
|
||||
// (undocumented)
|
||||
icon?: ReactNode;
|
||||
// (undocumented)
|
||||
rank?: number;
|
||||
// (undocumented)
|
||||
result: IndexableDocument;
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -25,6 +25,7 @@ import {
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { useAnalytics } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
IndexableDocument,
|
||||
ResultHighlight,
|
||||
@@ -51,6 +52,7 @@ export interface CatalogSearchResultListItemProps {
|
||||
icon?: ReactNode;
|
||||
result: IndexableDocument;
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -60,8 +62,16 @@ export function CatalogSearchResultListItem(
|
||||
const result = props.result as any;
|
||||
|
||||
const classes = useStyles();
|
||||
const analytics = useAnalytics();
|
||||
const handleClick = () => {
|
||||
analytics.captureEvent('discover', result.title, {
|
||||
attributes: { to: result.location },
|
||||
value: props.rank,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Link to={result.location}>
|
||||
<Link noTrack to={result.location} onClick={handleClick}>
|
||||
<ListItem alignItems="flex-start">
|
||||
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
|
||||
<div className={classes.flexContainer}>
|
||||
|
||||
+4
@@ -523,6 +523,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
.map((_, i) => ({
|
||||
type: 'mytype',
|
||||
document: { value: `${i}` },
|
||||
rank: i + 1,
|
||||
})),
|
||||
),
|
||||
});
|
||||
@@ -567,6 +568,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
.map((_, i) => ({
|
||||
type: 'mytype',
|
||||
document: { value: `${i}` },
|
||||
rank: i + 1,
|
||||
})),
|
||||
),
|
||||
nextPageCursor: 'MQ==',
|
||||
@@ -614,6 +616,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
.map((_, i) => ({
|
||||
type: 'mytype',
|
||||
document: { value: `${i}` },
|
||||
rank: i + 1,
|
||||
}))
|
||||
.slice(25),
|
||||
),
|
||||
@@ -667,6 +670,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
.map((_, i) => ({
|
||||
type: 'mytype',
|
||||
document: { value: `${i}` },
|
||||
rank: i + 1,
|
||||
highlight: {
|
||||
preTag: '<tag>',
|
||||
postTag: '</tag>',
|
||||
|
||||
+22
-19
@@ -334,27 +334,30 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
results: result.body.hits.hits.map((d: ElasticSearchResult) => {
|
||||
const resultItem: IndexableResult = {
|
||||
type: this.getTypeFromIndex(d._index),
|
||||
document: d._source,
|
||||
};
|
||||
|
||||
if (d.highlight) {
|
||||
resultItem.highlight = {
|
||||
preTag: this.highlightOptions.preTag as string,
|
||||
postTag: this.highlightOptions.postTag as string,
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(d.highlight).map(([field, fragments]) => [
|
||||
field,
|
||||
fragments.join(this.highlightOptions.fragmentDelimiter),
|
||||
]),
|
||||
),
|
||||
results: result.body.hits.hits.map(
|
||||
(d: ElasticSearchResult, index: number) => {
|
||||
const resultItem: IndexableResult = {
|
||||
type: this.getTypeFromIndex(d._index),
|
||||
document: d._source,
|
||||
rank: pageSize * page + index + 1,
|
||||
};
|
||||
}
|
||||
|
||||
return resultItem;
|
||||
}),
|
||||
if (d.highlight) {
|
||||
resultItem.highlight = {
|
||||
preTag: this.highlightOptions.preTag as string,
|
||||
postTag: this.highlightOptions.postTag as string,
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(d.highlight).map(([field, fragments]) => [
|
||||
field,
|
||||
fragments.join(this.highlightOptions.fragmentDelimiter),
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return resultItem;
|
||||
},
|
||||
),
|
||||
nextPageCursor,
|
||||
previousPageCursor,
|
||||
};
|
||||
|
||||
@@ -174,6 +174,7 @@ describe('PgSearchEngine', () => {
|
||||
location: 'location-1',
|
||||
},
|
||||
type: 'my-type',
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -215,6 +216,7 @@ describe('PgSearchEngine', () => {
|
||||
location: `location-${i}`,
|
||||
},
|
||||
type: 'my-type',
|
||||
rank: i + 1,
|
||||
})),
|
||||
nextPageCursor: 'MQ==',
|
||||
});
|
||||
@@ -257,6 +259,7 @@ describe('PgSearchEngine', () => {
|
||||
location: `location-${i}`,
|
||||
},
|
||||
type: 'my-type',
|
||||
rank: i + 1,
|
||||
}))
|
||||
.slice(25),
|
||||
previousPageCursor: 'MA==',
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SearchEngine } from '@backstage/plugin-search-backend-node';
|
||||
import {
|
||||
SearchQuery,
|
||||
IndexableResultSet,
|
||||
IndexableResult,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { PgSearchEngineIndexer } from './PgSearchEngineIndexer';
|
||||
import {
|
||||
@@ -104,10 +105,13 @@ export class PgSearchEngine implements SearchEngine {
|
||||
? encodePageCursor({ page: page - 1 })
|
||||
: undefined;
|
||||
|
||||
const results = pageRows.map(({ type, document }) => ({
|
||||
type,
|
||||
document,
|
||||
}));
|
||||
const results = pageRows.map(
|
||||
({ type, document }, index): IndexableResult => ({
|
||||
type,
|
||||
document,
|
||||
rank: page * pageSize + index + 1,
|
||||
}),
|
||||
);
|
||||
|
||||
return { results, nextPageCursor, previousPageCursor };
|
||||
}
|
||||
|
||||
@@ -431,6 +431,7 @@ describe('LunrSearchEngine', () => {
|
||||
location: 'test/location',
|
||||
},
|
||||
type: 'test-index',
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -469,6 +470,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
location: 'test/location',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -521,6 +523,7 @@ describe('LunrSearchEngine', () => {
|
||||
location: `${highlightTags.pre}test/location${highlightTags.post}`,
|
||||
},
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -559,6 +562,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
location: 'test/location',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -598,6 +602,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
location: 'test/location',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -637,6 +642,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'Hello World.',
|
||||
location: 'test/location',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -676,6 +682,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'Searching',
|
||||
location: 'test/location',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -722,6 +729,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
location: 'test/location2',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -777,6 +785,7 @@ describe('LunrSearchEngine', () => {
|
||||
location: 'test/location2',
|
||||
extraField: 'testExtraField',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -823,6 +832,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
location: 'test:location2',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -886,6 +896,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
title: 'testTitle',
|
||||
},
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
document: {
|
||||
@@ -893,6 +904,7 @@ describe('LunrSearchEngine', () => {
|
||||
text: 'testText',
|
||||
title: 'testTitle',
|
||||
},
|
||||
rank: 2,
|
||||
},
|
||||
],
|
||||
nextPageCursor: undefined,
|
||||
@@ -931,6 +943,7 @@ describe('LunrSearchEngine', () => {
|
||||
location: `test/location/${i}`,
|
||||
},
|
||||
type: 'test-index',
|
||||
rank: i + 1,
|
||||
})),
|
||||
nextPageCursor: 'MQ==',
|
||||
previousPageCursor: undefined,
|
||||
@@ -968,6 +981,7 @@ describe('LunrSearchEngine', () => {
|
||||
location: `test/location/${i}`,
|
||||
},
|
||||
type: 'test-index',
|
||||
rank: i + 1,
|
||||
}))
|
||||
.slice(25),
|
||||
nextPageCursor: undefined,
|
||||
|
||||
@@ -207,9 +207,10 @@ export class LunrSearchEngine implements SearchEngine {
|
||||
|
||||
// Translate results into IndexableResultSet
|
||||
const realResultSet: IndexableResultSet = {
|
||||
results: results.slice(offset, offset + pageSize).map(d => ({
|
||||
results: results.slice(offset, offset + pageSize).map((d, index) => ({
|
||||
type: d.type,
|
||||
document: this.docStore[d.result.ref],
|
||||
rank: page * pageSize + index + 1,
|
||||
highlight: {
|
||||
preTag: this.highlightPreTag,
|
||||
postTag: this.highlightPostTag,
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('AuthorizedSearchEngine', () => {
|
||||
.fill(0)
|
||||
.map((_, index) => ({
|
||||
type,
|
||||
rank: index + 1,
|
||||
document: {
|
||||
title: `${type}_doc_${index}`,
|
||||
authorization: withAuthorization
|
||||
@@ -261,9 +262,11 @@ describe('AuthorizedSearchEngine', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const expectedResult = { ...usersWithAuth[8], rank: 1 };
|
||||
|
||||
await expect(
|
||||
authorizedSearchEngine.query({ term: '' }, options),
|
||||
).resolves.toEqual({ results: [usersWithAuth[8]] });
|
||||
).resolves.toEqual({ results: [expectedResult] });
|
||||
|
||||
expect(mockedQuery).toHaveBeenCalledWith(
|
||||
{ term: '', types: ['users'] },
|
||||
@@ -275,6 +278,7 @@ describe('AuthorizedSearchEngine', () => {
|
||||
const searchResults = [
|
||||
{
|
||||
type: 'templates',
|
||||
rank: 1,
|
||||
document: {
|
||||
title: `doc_0_a`,
|
||||
authorization: { resourceRef: `template_doc_0` },
|
||||
@@ -282,6 +286,7 @@ describe('AuthorizedSearchEngine', () => {
|
||||
},
|
||||
{
|
||||
type: 'templates',
|
||||
rank: 2,
|
||||
document: {
|
||||
title: `doc_0_b`,
|
||||
authorization: { resourceRef: `template_doc_0` },
|
||||
@@ -419,7 +424,9 @@ describe('AuthorizedSearchEngine', () => {
|
||||
{ token: 'token' },
|
||||
);
|
||||
|
||||
const expectedResult = allDocuments.slice(0, 25);
|
||||
const expectedResult = allDocuments
|
||||
.slice(0, 25)
|
||||
.map((r, i) => ({ ...r, rank: i + 1 }));
|
||||
|
||||
const expectedFirstRequestCursor = 'MQ==';
|
||||
expect(result).toEqual({
|
||||
@@ -510,7 +517,8 @@ describe('AuthorizedSearchEngine', () => {
|
||||
|
||||
const expectedResult = allDocuments
|
||||
.filter(d => d.type !== typeServices)
|
||||
.slice(0, 25);
|
||||
.slice(0, 25)
|
||||
.map((d, i) => ({ ...d, rank: i + 1 }));
|
||||
|
||||
const expectedFirstRequestCursor = 'MQ==';
|
||||
expect(result).toEqual({
|
||||
@@ -587,8 +595,12 @@ describe('AuthorizedSearchEngine', () => {
|
||||
{ token: 'token' },
|
||||
);
|
||||
|
||||
const expectedResults = servicesWithAuth
|
||||
.slice(5)
|
||||
.map((r, i) => ({ ...r, rank: 25 + i + 1 }));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: servicesWithAuth.slice(5),
|
||||
results: expectedResults,
|
||||
previousPageCursor: encodePageCursor({ page: 0 }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,10 +189,15 @@ export class AuthorizedSearchEngine implements SearchEngine {
|
||||
);
|
||||
|
||||
return {
|
||||
results: filteredResults.slice(
|
||||
page * this.pageSize,
|
||||
(page + 1) * this.pageSize,
|
||||
),
|
||||
results: filteredResults
|
||||
.slice(page * this.pageSize, (page + 1) * this.pageSize)
|
||||
.map((result, index) => {
|
||||
// Overwrite any/all rank entries to avoid leaking knowledge of filtered results.
|
||||
return {
|
||||
...result,
|
||||
rank: page * this.pageSize + index + 1,
|
||||
};
|
||||
}),
|
||||
previousPageCursor:
|
||||
page === 0 ? undefined : encodePageCursor({ page: page - 1 }),
|
||||
nextPageCursor:
|
||||
|
||||
@@ -52,11 +52,9 @@ export type QueryTranslator = (query: SearchQuery) => unknown;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Result<TDocument extends SearchDocument> {
|
||||
// (undocumented)
|
||||
document: TDocument;
|
||||
// (undocumented)
|
||||
highlight?: ResultHighlight;
|
||||
// (undocumented)
|
||||
rank?: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,27 @@ export interface ResultHighlight {
|
||||
* @public
|
||||
*/
|
||||
export interface Result<TDocument extends SearchDocument> {
|
||||
/**
|
||||
* The "type" of the given document. See: {@link DocumentCollatorFactory."type"}
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* The raw value of the document, as indexed.
|
||||
*/
|
||||
document: TDocument;
|
||||
|
||||
/**
|
||||
* Optional result highlight. Useful for improving the search result
|
||||
* display/experience.
|
||||
*/
|
||||
highlight?: ResultHighlight;
|
||||
|
||||
/**
|
||||
* Optional result rank, where 1 is the first/top result returned. Useful for
|
||||
* understanding search effectiveness in analytics.
|
||||
*/
|
||||
rank?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,7 @@ export type DefaultResultListItemProps = {
|
||||
secondaryAction?: ReactNode;
|
||||
result: SearchDocument;
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
};
|
||||
|
||||
|
||||
+12
-2
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AnalyticsContext } from '@backstage/core-plugin-api';
|
||||
import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ResultHighlight,
|
||||
SearchDocument,
|
||||
@@ -40,6 +40,7 @@ export type DefaultResultListItemProps = {
|
||||
secondaryAction?: ReactNode;
|
||||
result: SearchDocument;
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
};
|
||||
|
||||
@@ -51,12 +52,21 @@ export type DefaultResultListItemProps = {
|
||||
export const DefaultResultListItemComponent = ({
|
||||
result,
|
||||
highlight,
|
||||
rank,
|
||||
icon,
|
||||
secondaryAction,
|
||||
lineClamp = 5,
|
||||
}: DefaultResultListItemProps) => {
|
||||
const analytics = useAnalytics();
|
||||
const handleClick = () => {
|
||||
analytics.captureEvent('discover', result.title, {
|
||||
attributes: { to: result.location },
|
||||
value: rank,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Link to={result.location}>
|
||||
<Link noTrack to={result.location} onClick={handleClick}>
|
||||
<ListItem alignItems="center">
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText
|
||||
|
||||
@@ -393,6 +393,7 @@ export type TechDocsSearchResultListItemProps = {
|
||||
icon?: ReactNode;
|
||||
result: any;
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
asListItem?: boolean;
|
||||
asLink?: boolean;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
|
||||
|
||||
@@ -45,6 +46,7 @@ export type TechDocsSearchResultListItemProps = {
|
||||
icon?: ReactNode;
|
||||
result: any;
|
||||
highlight?: ResultHighlight;
|
||||
rank?: number;
|
||||
lineClamp?: number;
|
||||
asListItem?: boolean;
|
||||
asLink?: boolean;
|
||||
@@ -62,6 +64,7 @@ export const TechDocsSearchResultListItem = (
|
||||
const {
|
||||
result,
|
||||
highlight,
|
||||
rank,
|
||||
lineClamp = 5,
|
||||
asListItem = true,
|
||||
asLink = true,
|
||||
@@ -69,6 +72,15 @@ export const TechDocsSearchResultListItem = (
|
||||
icon,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const analytics = useAnalytics();
|
||||
const handleClick = () => {
|
||||
analytics.captureEvent('discover', result.title, {
|
||||
attributes: { to: result.location },
|
||||
value: rank,
|
||||
});
|
||||
};
|
||||
|
||||
const TextItem = () => {
|
||||
const resultTitle = highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
@@ -138,7 +150,13 @@ export const TechDocsSearchResultListItem = (
|
||||
};
|
||||
|
||||
const LinkWrapper = ({ children }: PropsWithChildren<{}>) =>
|
||||
asLink ? <Link to={result.location}>{children}</Link> : <>{children}</>;
|
||||
asLink ? (
|
||||
<Link noTrack to={result.location} onClick={handleClick}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const ListItemWrapper = ({ children }: PropsWithChildren<{}>) =>
|
||||
asListItem ? (
|
||||
|
||||
Reference in New Issue
Block a user