[APM] Implement Unified Search for APM (#153842)

## Summary

This PR brings Unified Search to APM
https://github.com/elastic/kibana/issues/152147

## Scope of Implementation
1. We are only adding the search capability for now.
2. Filters and Saved queries are not part of this scope

### Pending Items

- [x] Add Unit tests
- [x] Fix existing broken Unit tests
- [x] Fix existing broken Cy tests -
https://github.com/elastic/kibana/pull/154059
- [x] Replace the search bar for mobile
- [x] Work on feedback after deploying this branch
- [x] Add validation for Free Text. Awaiting -
https://github.com/elastic/kibana/issues/154239
- [x] Add logic to pass custom filters to Unified Search. Awaiting -
https://github.com/elastic/kibana/issues/154437

### Pages using Unified Search
- [x] Service Inventory
- [x] Service Map
- [x] Service Overview
- [x] Transactions Overview
- [x] Errors
- [x] Trace Overview
- [x] Dependencies Inventory
- [x] Agent Explorer
- [x] Storage Explorer

### Pages still using old Custom Implementation
- [ ] Trace Explorer - Out of scope for this PR
- [ ] Service Group - Changing this logic could take some additional
time as this would mean we allowing our SearchBar component to accept a
custom submit function which does not updates the URL, as in every other
implementation, we update the URL. I would mark this as a follow up
ticket/stretch goal - https://github.com/elastic/kibana/issues/154320
- [x] Alerts - ~~It uses a Custom Search bar built by Actionable Obs
team. Not sure if it's in this scope.~~ Seems they are already using US


## Things to consider
- [x] ~~What do we do with the old components - `KueryBar` and
`ApmDatePicker`. Should we delete them ?~~ The existing component will
stay as its still used in Certain places, see `Pages still using old
Custom Implementation` of this PR
- [x] Other implementation are supporting Free Text Search, hence this
one is too and is not checking for valid KQL. I hope my understanding is
correct here - If needed, then awaiting -
https://github.com/elastic/kibana/issues/154239
[Update] - We will add validations for free text to replicate the
previous behaviour which we had with our own custom implementation
- [ ] The UX of the search bar is a bit off when it comes to long
placeholders. May be we need a shorter text for placeholder -
@boriskirov ?
- [x] ~~When navigating from Service Inventory page to Traces or
Dependencies page, we are only persisting URL state for the selected
time range. Shouldn't KQL query be also part of it. If yes, that would a
stretch goal but good thing to consider.~~ @gbamparop @sqren - As
discussed during the demo, we will keep this functionality as it is,
which means while navigating to a different page, the search bar will be
reset, but the when navigating between tabs on the same page, search bar
will persist.
- [x] ~~On the Initial page load, the Unified Search Bar Input box does
not loads immediately. You only see the DateTimePicker and the button.
Once the component has completely loaded, only then the text box appear.
I see other pages like Log Streams brings up a Full Page loader unless
the component has loaded and then once certain things have loaded, they
remove the full page loader and start showing the search bar. Opinion
?~~ @boriskirov @gbamparop @sqren - Added a EUI Skeleton to handle this
issue.



https://user-images.githubusercontent.com/7416358/228291762-0ca55e9a-7de9-4312-aa58-f484441430ce.mov

---------

Co-authored-by: Katerina Patticha <kate@kpatticha.com>
This commit is contained in:
Achyut Jhunjhunwala 2023-04-21 14:16:19 +02:00 committed by GitHub
parent 9e712bb6fe
commit 60c8b8fecd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 500 additions and 112 deletions

View file

@ -13,14 +13,14 @@ import {
} from './es_fields/apm';
import { environmentQuery } from './utils/environment_query';
export const kueryBarPlaceholder = i18n.translate(
'xpack.apm.dependencies.kueryBarPlaceholder',
export const unifiedSearchBarPlaceholder = i18n.translate(
'xpack.apm.dependencies.unifiedSearchBarPlaceholder',
{
defaultMessage: `Search dependency metrics (e.g. span.destination.service.resource:elasticsearch)`,
}
);
export const getKueryBarBoolFilter = ({
export const getSearchBarBoolFilter = ({
dependencyName,
environment,
}: {

View file

@ -140,12 +140,6 @@ describe('Storage Explorer', () => {
});
});
it('when clicking the refresh button', () => {
cy.wait(mainAliasNames);
cy.contains('Refresh').click();
cy.wait(mainAliasNames);
});
it('when selecting a different time range and clicking the update button', () => {
cy.wait(mainAliasNames);
@ -155,9 +149,6 @@ describe('Storage Explorer', () => {
);
cy.contains('Update').click();
cy.wait(mainAliasNames);
cy.contains('Refresh').click();
cy.wait(mainAliasNames);
});
it('with the correct lifecycle phase when changing the lifecycle phase', () => {

View file

@ -17,6 +17,8 @@ describe('APM deep links', () => {
cy.contains('APM / Service groups');
cy.contains('APM / Traces');
cy.contains('APM / Service Map');
cy.contains('APM / Dependencies');
cy.contains('APM / Settings');
// navigates to home page
// Force click because welcome screen changes
@ -43,5 +45,15 @@ describe('APM deep links', () => {
// navigates to service maps
cy.contains('APM / Service Map').click({ force: true });
cy.url().should('include', '/apm/service-map');
cy.getByTestSubj('nav-search-input').type('APM');
// navigates to dependencies page
cy.contains('APM / Dependencies').click({ force: true });
cy.url().should('include', '/apm/dependencies/inventory');
cy.getByTestSubj('nav-search-input').type('APM');
// navigates to settings page
cy.contains('APM / Settings').click({ force: true });
cy.url().should('include', '/apm/settings/general-settings');
});
});

View file

@ -81,16 +81,16 @@ describe('Errors page', () => {
cy.contains('div', 'Error 1');
});
it('clicking on type adds a filter in the kuerybar', () => {
it('clicking on type adds a filter in the searchbar', () => {
cy.visitKibana(javaServiceErrorsPageHref);
cy.getByTestSubj('headerFilterKuerybar')
cy.getByTestSubj('apmUnifiedSearchBar')
.invoke('val')
.should('be.empty');
// `force: true` because Cypress says the element is 0x0
cy.contains('exception 0').click({
force: true,
});
cy.getByTestSubj('headerFilterKuerybar')
cy.getByTestSubj('apmUnifiedSearchBar')
.its('length')
.should('be.gt', 0);
cy.get('table')

View file

@ -45,7 +45,7 @@ describe('Service inventory - header filters', () => {
cy.contains('Services');
cy.contains('opbeans-node');
cy.contains('service 1');
cy.getByTestSubj('headerFilterKuerybar')
cy.getByTestSubj('apmUnifiedSearchBar')
.type(`service.name: "${specialServiceName}"`)
.type('{enter}');
cy.contains('service 1');

View file

@ -103,12 +103,6 @@ describe('Service inventory', () => {
});
});
it('when clicking the refresh button', () => {
cy.wait(mainAliasNames);
cy.contains('Refresh').click();
cy.wait(mainAliasNames);
});
it('when selecting a different time range and clicking the update button', () => {
cy.wait(mainAliasNames);
@ -118,9 +112,6 @@ describe('Service inventory', () => {
);
cy.contains('Update').click();
cy.wait(mainAliasNames);
cy.contains('Refresh').click();
cy.wait(mainAliasNames);
});
});

View file

@ -50,12 +50,12 @@ describe('Errors table', () => {
it('clicking on type adds a filter in the kuerybar and navigates to errors page', () => {
cy.visitKibana(serviceOverviewHref);
cy.getByTestSubj('headerFilterKuerybar').invoke('val').should('be.empty');
cy.getByTestSubj('apmUnifiedSearchBar').invoke('val').should('be.empty');
// `force: true` because Cypress says the element is 0x0
cy.contains('Exception').click({
force: true,
});
cy.getByTestSubj('headerFilterKuerybar').its('length').should('be.gt', 0);
cy.getByTestSubj('apmUnifiedSearchBar').its('length').should('be.gt', 0);
cy.get('table').find('td:contains("Exception")').should('have.length', 1);
});

View file

@ -117,7 +117,7 @@ describe('Service overview - header filters', () => {
});
});
describe('Filtering by kuerybar', () => {
describe('Filtering by searchbar', () => {
beforeEach(() => {
cy.loginAsViewerUser();
});
@ -129,13 +129,23 @@ describe('Service overview - header filters', () => {
})
);
cy.contains('opbeans-java');
cy.getByTestSubj('headerFilterKuerybar').type('transaction.n');
cy.getByTestSubj('apmUnifiedSearchBar').type('transaction.n');
cy.contains('transaction.name');
cy.getByTestSubj('suggestionContainer').find('li').first().click();
cy.getByTestSubj('headerFilterKuerybar').type(':');
cy.getByTestSubj('suggestionContainer').find('li').first().click();
cy.getByTestSubj('headerFilterKuerybar').type('{enter}');
cy.url().should('include', '&kuery=transaction.name');
cy.getByTestSubj(
'autocompleteSuggestion-field-transaction.name-'
).click();
cy.getByTestSubj('apmUnifiedSearchBar').type(':');
cy.getByTestSubj('autoCompleteSuggestionText').should('have.length', 1);
cy.getByTestSubj(
Cypress.$.escapeSelector(
'autocompleteSuggestion-value-"GET-/api/product"-'
)
).click();
cy.getByTestSubj('apmUnifiedSearchBar').type('{enter}');
cy.url().should(
'include',
'&kuery=transaction.name%20:%22GET%20%2Fapi%2Fproduct%22%20'
);
});
});
});

View file

@ -121,7 +121,7 @@ describe('Service overview: Time Comparison', () => {
'2021-10-20T00:00:00.000Z'
);
cy.getByTestSubj('superDatePickerApplyTimeButton').click();
cy.getByTestSubj('querySubmitButton').click();
cy.getByTestSubj('comparisonSelect').should('have.value', '864000000ms');
cy.getByTestSubj('comparisonSelect').should(

View file

@ -52,7 +52,7 @@ describe('Span links', () => {
cy.contains('2 Span links');
cy.getByTestSubj(
`spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}`
).realHover();
).trigger('mouseover');
cy.contains('2 Span links found');
cy.contains('2 incoming');
cy.contains('0 outgoing');
@ -66,7 +66,7 @@ describe('Span links', () => {
cy.contains('2 Span links');
cy.getByTestSubj(
`spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}`
).realHover();
).trigger('mouseover');
cy.contains('2 Span links found');
cy.contains('1 incoming');
cy.contains('1 outgoing');
@ -80,7 +80,7 @@ describe('Span links', () => {
cy.contains('2 Span links');
cy.getByTestSubj(
`spanLinksBadge_${ids.producerConsumerIds.transactionCId}`
).realHover();
).trigger('mouseover');
cy.contains('2 Span links found');
cy.contains('1 incoming');
cy.contains('1 outgoing');
@ -94,7 +94,7 @@ describe('Span links', () => {
cy.contains('1 Span link');
cy.getByTestSubj(
`spanLinksBadge_${ids.producerConsumerIds.spanCId}`
).realHover();
).trigger('mouseover');
cy.contains('1 Span link found');
cy.contains('1 incoming');
cy.contains('0 outgoing');
@ -108,7 +108,7 @@ describe('Span links', () => {
cy.contains('2 Span links');
cy.getByTestSubj(
`spanLinksBadge_${ids.producerMultipleIds.transactionDId}`
).realHover();
).trigger('mouseover');
cy.contains('2 Span links found');
cy.contains('0 incoming');
cy.contains('2 outgoing');
@ -122,7 +122,7 @@ describe('Span links', () => {
cy.contains('2 Span links');
cy.getByTestSubj(
`spanLinksBadge_${ids.producerMultipleIds.spanEId}`
).realHover();
).trigger('mouseover');
cy.contains('2 Span links found');
cy.contains('0 incoming');
cy.contains('2 outgoing');

View file

@ -8,27 +8,26 @@
import { EuiSpacer } from '@elastic/eui';
import React from 'react';
import {
getKueryBarBoolFilter,
kueryBarPlaceholder,
unifiedSearchBarPlaceholder,
getSearchBarBoolFilter,
} from '../../../../common/dependencies';
import { useApmParams } from '../../../hooks/use_apm_params';
import { SearchBar } from '../../shared/search_bar/search_bar';
import { DependenciesInventoryTable } from './dependencies_inventory_table';
import { useApmParams } from '../../../hooks/use_apm_params';
export function DependenciesInventory() {
const {
query: { environment },
} = useApmParams('/dependencies/inventory');
const kueryBarBoolFilter = getKueryBarBoolFilter({
const searchBarBoolFilter = getSearchBarBoolFilter({
environment,
});
return (
<>
<SearchBar
showTimeComparison
kueryBarPlaceholder={kueryBarPlaceholder}
kueryBarBoolFilter={kueryBarBoolFilter}
searchBarPlaceholder={unifiedSearchBarPlaceholder}
searchBarBoolFilter={searchBarBoolFilter}
/>
<EuiSpacer size="s" />
<DependenciesInventoryTable />

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
EuiFlexGroup,
EuiFlexGroupProps,
@ -14,30 +13,27 @@ import {
} from '@elastic/eui';
import React from 'react';
import { useBreakpoints } from '../../../hooks/use_breakpoints';
import { ApmDatePicker } from '../../shared/date_picker/apm_date_picker';
import { KueryBar } from '../../shared/kuery_bar';
import { TimeComparison } from '../../shared/time_comparison';
import { TransactionTypeSelect } from '../../shared/transaction_type_select';
import { MobileFilters } from './service_overview/filters';
import { UnifiedSearchBar } from '../../shared/unified_search_bar';
interface Props {
hidden?: boolean;
showKueryBar?: boolean;
showUnifiedSearchBar?: boolean;
showTimeComparison?: boolean;
showTransactionTypeSelector?: boolean;
showMobileFilters?: boolean;
kueryBarPlaceholder?: string;
kueryBarBoolFilter?: QueryDslQueryContainer[];
searchBarPlaceholder?: string;
}
export function MobileSearchBar({
hidden = false,
showKueryBar = true,
showUnifiedSearchBar = true,
showTimeComparison = false,
showTransactionTypeSelector = false,
showMobileFilters = false,
kueryBarBoolFilter,
kueryBarPlaceholder,
searchBarPlaceholder,
}: Props) {
const { isSmall, isMedium, isLarge, isXl, isXXXL } = useBreakpoints();
@ -66,17 +62,11 @@ export function MobileSearchBar({
</EuiFlexItem>
)}
{showKueryBar && (
{showUnifiedSearchBar && (
<EuiFlexItem>
<KueryBar
placeholder={kueryBarPlaceholder}
boolFilter={kueryBarBoolFilter}
/>
<UnifiedSearchBar placeholder={searchBarPlaceholder} />
</EuiFlexItem>
)}
<EuiFlexItem grow={isSmall}>
<ApmDatePicker />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
<EuiSpacer size={isSmall ? 's' : 'm'} />

View file

@ -40,7 +40,7 @@ import { DisabledPrompt } from './disabled_prompt';
function PromptContainer({ children }: { children: ReactNode }) {
return (
<>
<SearchBar showKueryBar={false} />
<SearchBar showUnifiedSearchBar={false} />
<EuiFlexGroup
alignItems="center"
justifyContent="spaceAround"

View file

@ -34,7 +34,8 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { ApmEnvironmentFilter } from '../../../shared/environment_filter';
import { KueryBar } from '../../../shared/kuery_bar';
import { UnifiedSearchBar } from '../../../shared/unified_search_bar';
import * as urlHelpers from '../../../shared/links/url_helpers';
import { SuggestionsSelect } from '../../../shared/suggestions_select';
import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge';
@ -168,7 +169,11 @@ export function AgentExplorer() {
</EuiFlexItem>
<EuiSpacer />
<EuiFlexItem grow={false}>
<KueryBar />
<UnifiedSearchBar
showDatePicker={false}
showSubmitButton={false}
isClearable={false}
/>
</EuiFlexItem>
<EuiSpacer size="xs" />
<EuiFlexItem>

View file

@ -33,7 +33,7 @@ export function page({
tabKey: React.ComponentProps<typeof MobileServiceTemplate>['selectedTabKey'];
element: React.ReactElement<any, any>;
searchBarOptions?: {
showKueryBar?: boolean;
showUnifiedSearchBar?: boolean;
showTransactionTypeSelector?: boolean;
showTimeComparison?: boolean;
showMobileFilters?: boolean;

View file

@ -48,7 +48,7 @@ function page({
tab: React.ComponentProps<typeof ApmServiceTemplate>['selectedTab'];
element: React.ReactElement<any, any>;
searchBarOptions?: {
showKueryBar?: boolean;
showUnifiedSearchBar?: boolean;
showTransactionTypeSelector?: boolean;
showTimeComparison?: boolean;
hidden?: boolean;
@ -320,7 +320,7 @@ export const serviceDetail = {
}),
element: <ServiceLogs />,
searchBarOptions: {
showKueryBar: false,
showUnifiedSearchBar: false,
},
}),
'/services/{serviceName}/infrastructure': {
@ -331,7 +331,7 @@ export const serviceDetail = {
}),
element: <InfraOverview />,
searchBarOptions: {
showKueryBar: false,
showUnifiedSearchBar: false,
},
}),
params: t.partial({

View file

@ -8,10 +8,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
getKueryBarBoolFilter,
kueryBarPlaceholder,
} from '../../../../common/dependencies';
import { unifiedSearchBarPlaceholder } from '../../../../common/dependencies';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
@ -29,7 +26,7 @@ interface Props {
export function DependencyDetailTemplate({ children }: Props) {
const {
query,
query: { dependencyName, rangeFrom, rangeTo, environment },
query: { dependencyName, rangeFrom, rangeTo },
} = useApmParams('/dependencies');
const router = useApmRouter();
@ -38,11 +35,6 @@ export function DependencyDetailTemplate({ children }: Props) {
const path = useApmRoutePath();
const kueryBarBoolFilter = getKueryBarBoolFilter({
environment,
dependencyName,
});
const dependencyMetadataFetch = useFetcher(
(callApmApi) => {
if (!start || !end) {
@ -113,8 +105,7 @@ export function DependencyDetailTemplate({ children }: Props) {
>
<SearchBar
showTimeComparison
kueryBarPlaceholder={kueryBarPlaceholder}
kueryBarBoolFilter={kueryBarBoolFilter}
searchBarPlaceholder={unifiedSearchBarPlaceholder}
/>
{children}
</ApmMainTemplate>

View file

@ -12,11 +12,11 @@ import {
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../common/es_fields/apm';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { UIProcessorEvent } from '../../../../common/processor_event';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { ApmUrlParams } from '../../../context/url_params_context/types';
} from '../../../common/es_fields/apm';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { UIProcessorEvent } from '../../../common/processor_event';
import { environmentQuery } from '../../../common/utils/environment_query';
import { ApmUrlParams } from '../../context/url_params_context/types';
export function getBoolFilter({
groupId,

View file

@ -18,9 +18,9 @@ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmDataView } from '../../../hooks/use_apm_data_view';
import { fromQuery, toQuery } from '../links/url_helpers';
import { getBoolFilter } from './get_bool_filter';
import { getBoolFilter } from '../get_bool_filter';
import { Typeahead } from './typeahead';
import { useProcessorEvent } from './use_processor_event';
import { useProcessorEvent } from '../../../hooks/use_processor_event';
interface State {
suggestions: QuerySuggestion[];

View file

@ -39,6 +39,20 @@ function setup({
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
dataViews: { get: async () => {} },
data: {
query: {
queryString: {
setQuery: () => {},
getQuery: () => {},
clearQuery: () => {},
},
timefilter: {
timefilter: {
setTime: () => {},
},
},
},
},
} as Partial<CoreStart>);
// mock transaction types

View file

@ -4,8 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
EuiFlexGroup,
EuiFlexGroupProps,
@ -13,30 +11,30 @@ import {
EuiSpacer,
} from '@elastic/eui';
import React from 'react';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isMobileAgentName } from '../../../../common/agent_name';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useBreakpoints } from '../../../hooks/use_breakpoints';
import { ApmDatePicker } from '../date_picker/apm_date_picker';
import { KueryBar } from '../kuery_bar';
import { TimeComparison } from '../time_comparison';
import { TransactionTypeSelect } from '../transaction_type_select';
import { UnifiedSearchBar } from '../unified_search_bar';
interface Props {
hidden?: boolean;
showKueryBar?: boolean;
showUnifiedSearchBar?: boolean;
showTimeComparison?: boolean;
showTransactionTypeSelector?: boolean;
kueryBarPlaceholder?: string;
kueryBarBoolFilter?: QueryDslQueryContainer[];
searchBarPlaceholder?: string;
searchBarBoolFilter?: QueryDslQueryContainer[];
}
export function SearchBar({
hidden = false,
showKueryBar = true,
showUnifiedSearchBar = true,
showTimeComparison = false,
showTransactionTypeSelector = false,
kueryBarBoolFilter,
kueryBarPlaceholder,
searchBarPlaceholder,
searchBarBoolFilter,
}: Props) {
const { agentName } = useApmServiceContext();
const isMobileAgent = isMobileAgentName(agentName);
@ -69,11 +67,11 @@ export function SearchBar({
</EuiFlexItem>
)}
{showKueryBar && (
{showUnifiedSearchBar && (
<EuiFlexItem>
<KueryBar
placeholder={kueryBarPlaceholder}
boolFilter={kueryBarBoolFilter}
<UnifiedSearchBar
placeholder={searchBarPlaceholder}
boolFilter={searchBarBoolFilter}
/>
</EuiFlexItem>
)}
@ -91,9 +89,6 @@ export function SearchBar({
<TimeComparison />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<ApmDatePicker />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -0,0 +1,248 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
Filter,
fromKueryExpression,
Query,
TimeRange,
toElasticsearchQuery,
} from '@kbn/es-query';
import { useHistory, useLocation } from 'react-router-dom';
import deepEqual from 'fast-deep-equal';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { EuiSkeletonRectangle } from '@elastic/eui';
import qs from 'query-string';
import { DataView, UI_SETTINGS } from '@kbn/data-plugin/common';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { UIProcessorEvent } from '../../../../common/processor_event';
import { TimePickerTimeDefaults } from '../date_picker/typings';
import { ApmPluginStartDeps } from '../../../plugin';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmDataView } from '../../../hooks/use_apm_data_view';
import { useProcessorEvent } from '../../../hooks/use_processor_event';
import { fromQuery, toQuery } from '../links/url_helpers';
import { useApmParams } from '../../../hooks/use_apm_params';
import { getBoolFilter } from '../get_bool_filter';
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
function useSearchBarParams(defaultKuery?: string) {
const { path, query } = useApmParams('/*');
const urlKuery = 'kuery' in query ? query.kuery : undefined;
const serviceName = 'serviceName' in path ? path.serviceName : undefined;
const groupId = 'groupId' in path ? path.groupId : undefined;
const environment = 'environment' in query ? query.environment : undefined;
return {
kuery: urlKuery
? {
query: defaultKuery || urlKuery,
language: 'kuery',
}
: undefined,
serviceName,
groupId,
environment,
};
}
function useUrlTimeRange(defaultTimeRange: TimeRange) {
const location = useLocation();
const query = qs.parse(location.search);
const isDateRangeSet = 'rangeFrom' in query && 'rangeTo' in query;
if (isDateRangeSet) {
return {
from: query.rangeFrom,
to: query.rangeTo,
};
}
return defaultTimeRange;
}
function getSearchBarPlaceholder(
searchbarPlaceholder?: string,
processorEvent?: UIProcessorEvent
) {
const examples = {
transaction: 'transaction.duration.us > 300000',
error: 'http.response.status_code >= 400',
metric: 'process.pid = "1234"',
defaults:
'transaction.duration.us > 300000 AND http.response.status_code >= 400',
};
const example = examples[processorEvent || 'defaults'];
return (
searchbarPlaceholder ??
i18n.translate('xpack.apm.unifiedSearchBar.placeholder', {
defaultMessage: `Search {event, select,
transaction {transactions}
metric {metrics}
error {errors}
other {transactions, errors and metrics}
} (E.g. {queryExample})`,
values: {
queryExample: example,
event: processorEvent,
},
})
);
}
function convertKueryToEsQuery(kuery: string, dataView: DataView) {
const ast = fromKueryExpression(kuery);
return toElasticsearchQuery(ast, dataView);
}
export function UnifiedSearchBar({
placeholder,
value,
showDatePicker = true,
showSubmitButton = true,
isClearable = true,
boolFilter,
}: {
placeholder?: string;
value?: string;
showDatePicker?: boolean;
showSubmitButton?: boolean;
isClearable?: boolean;
boolFilter?: QueryDslQueryContainer[];
}) {
const {
unifiedSearch: {
ui: { SearchBar },
},
core,
} = useApmPluginContext();
const { services } = useKibana<ApmPluginStartDeps>();
const {
data: {
query: { queryString: queryStringService, timefilter: timeFilterService },
},
} = services;
const { kuery, serviceName, environment, groupId } =
useSearchBarParams(value);
const timePickerTimeDefaults = core.uiSettings.get<TimePickerTimeDefaults>(
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
);
const urlTimeRange = useUrlTimeRange(timePickerTimeDefaults);
const [displaySearchBar, setDisplaySearchBar] = useState(false);
const syncSearchBarWithUrl = useCallback(() => {
// Sync Kuery params with Search Bar
if (kuery && !deepEqual(queryStringService.getQuery(), kuery)) {
queryStringService.setQuery(kuery);
}
// On page navigation the search bar persists the state where as the url is cleared, hence we need to clear the search bar
if (!kuery) {
queryStringService.clearQuery();
}
// Sync Time Range with Search Bar
timeFilterService.timefilter.setTime(urlTimeRange as TimeRange);
}, [kuery, queryStringService, timeFilterService, urlTimeRange]);
useEffect(() => {
syncSearchBarWithUrl();
}, [syncSearchBarWithUrl]);
const location = useLocation();
const history = useHistory();
const { dataView } = useApmDataView();
const { urlParams } = useLegacyUrlParams();
const processorEvent = useProcessorEvent();
const searchbarPlaceholder = getSearchBarPlaceholder(
placeholder,
processorEvent
);
useEffect(() => {
if (dataView) setDisplaySearchBar(true);
}, [dataView]);
const customFilters =
boolFilter ??
getBoolFilter({
groupId,
processorEvent,
serviceName,
environment,
urlParams,
});
const filtersForSearchBarSuggestions = customFilters.map((filter) => {
return {
query: filter,
} as Filter;
});
const handleSubmit = (payload: { dateRange: TimeRange; query?: Query }) => {
if (dataView == null) {
return;
}
const { dateRange, query } = payload;
const { from: rangeFrom, to: rangeTo } = dateRange;
try {
const res = convertKueryToEsQuery(
query?.query as string,
dataView as DataView
);
if (!res) {
return;
}
const existingQueryParams = toQuery(location.search);
const updatedQueryWithTime = {
...existingQueryParams,
rangeFrom,
rangeTo,
};
history.push({
...location,
search: fromQuery({
...updatedQueryWithTime,
kuery: query?.query,
}),
});
} catch (e) {
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
}
};
return (
<EuiSkeletonRectangle
isLoading={!displaySearchBar}
width="100%"
height="40px"
>
<SearchBar
appName={i18n.translate('xpack.apm.appName', {
defaultMessage: 'APM',
})}
iconType="search"
placeholder={searchbarPlaceholder}
useDefaultBehaviors={true}
indexPatterns={dataView ? [dataView] : undefined}
showQueryInput={true}
showQueryMenu={false}
showFilterBar={false}
showDatePicker={showDatePicker}
showSubmitButton={showSubmitButton}
displayStyle="inPage"
onQuerySubmit={handleSubmit}
isClearable={isClearable}
dataTestSubj="apmUnifiedSearchBar"
filtersForSuggestions={filtersForSearchBarSuggestions}
/>
</EuiSkeletonRectangle>
);
}

View file

@ -0,0 +1,138 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createMemoryHistory, MemoryHistory } from 'history';
import React from 'react';
import { Router, useLocation } from 'react-router-dom';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import * as useFetcherHook from '../../../hooks/use_fetcher';
import * as useApmDataViewHook from '../../../hooks/use_apm_data_view';
import * as useApmParamsHook from '../../../hooks/use_apm_params';
import * as useProcessorEventHook from '../../../hooks/use_processor_event';
import { fromQuery } from '../links/url_helpers';
import { CoreStart } from '@kbn/core/public';
import { UnifiedSearchBar } from '.';
import { UrlParams } from '../../../context/url_params_context/types';
import { mount } from 'enzyme';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
function setup({
urlParams,
history,
}: {
urlParams: UrlParams;
history: MemoryHistory;
}) {
history.replace({
pathname: '/services',
search: fromQuery(urlParams),
});
const setQuerySpy = jest.fn();
const getQuerySpy = jest.fn();
const clearQuerySpy = jest.fn();
const setTimeSpy = jest.fn();
const KibanaReactContext = createKibanaReactContext({
usageCollection: {
reportUiCounter: () => {},
},
dataViews: {
get: async () => {},
},
data: {
query: {
queryString: {
setQuery: setQuerySpy,
getQuery: getQuerySpy,
clearQuery: clearQuerySpy,
},
timefilter: {
timefilter: {
setTime: setTimeSpy,
},
},
},
},
} as Partial<CoreStart>);
// mock transaction types
jest
.spyOn(useApmDataViewHook, 'useApmDataView')
.mockReturnValue({ dataView: undefined });
jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any);
const wrapper = mount(
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper>
<Router history={history}>
<UnifiedSearchBar />
</Router>
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
);
return { wrapper, setQuerySpy, getQuerySpy, clearQuerySpy, setTimeSpy };
}
describe('when kuery is already present in the url, the search bar must reflect the same', () => {
let history: MemoryHistory;
beforeEach(() => {
history = createMemoryHistory();
jest.spyOn(history, 'push');
jest.spyOn(history, 'replace');
});
jest
.spyOn(useProcessorEventHook, 'useProcessorEvent')
.mockReturnValue(undefined);
const search = '?method=json';
const pathname = '/services';
(useLocation as jest.Mock).mockImplementationOnce(() => ({
search,
pathname,
}));
it('sets the searchbar value based on URL', () => {
const expectedQuery = {
query: 'service.name:"opbeans-android"',
language: 'kuery',
};
const expectedTimeRange = {
from: 'now-15m',
to: 'now',
};
const urlParams = {
kuery: expectedQuery.query,
rangeFrom: expectedTimeRange.from,
rangeTo: expectedTimeRange.to,
environment: 'ENVIRONMENT_ALL',
comparisonEnabled: true,
serviceGroup: '',
offset: '1d',
};
jest
.spyOn(useApmParamsHook, 'useApmParams')
.mockReturnValue({ query: urlParams, path: {} });
const { setQuerySpy, setTimeSpy } = setup({
history,
urlParams,
});
expect(setQuerySpy).toBeCalledWith(expectedQuery);
expect(setTimeSpy).toBeCalledWith(expectedTimeRange);
});
});

View file

@ -98,6 +98,12 @@ const mockCorePlugins = {
data: {},
};
const mockUnifiedSearch = {
ui: {
SearchBar: () => <div className="searchBar" />,
},
};
export const mockApmPluginContextValue = {
appMountParameters: coreMock.createAppMountParameters('/basepath'),
config: mockConfig,
@ -106,6 +112,7 @@ export const mockApmPluginContextValue = {
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
corePlugins: mockCorePlugins,
deps: {},
unifiedSearch: mockUnifiedSearch,
};
export function MockApmPluginContextWrapper({

View file

@ -7,7 +7,7 @@
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { useLocation } from 'react-router-dom';
import { UIProcessorEvent } from '../../../../common/processor_event';
import { UIProcessorEvent } from '../../common/processor_event';
/**
* Infer the processor.event to used based on the route path

View file

@ -7546,7 +7546,6 @@
"xpack.apm.dataView.autoCreateDisabled": "La création automatique des vues de données a été désactivée via l'option de configuration \"autoCreateApmDataView\"",
"xpack.apm.dataView.noApmData": "Aucune donnée APM",
"xpack.apm.dependecyOperationDetailView.header.backLinkLabel": "Toutes les opérations",
"xpack.apm.dependencies.kueryBarPlaceholder": "Rechercher dans les indicateurs de dépendance (par ex., span.destination.service.resource:elasticsearch)",
"xpack.apm.dependenciesInventory.dependencyTableColumn": "Dépendance",
"xpack.apm.dependenciesTable.columnErrorRate": "Taux de transactions ayant échoué",
"xpack.apm.dependenciesTable.columnErrorRateTip": "Le pourcentage de transactions ayant échoué pour le service sélectionné. Les transactions du serveur HTTP avec un code du statut 4xx (erreur du client) ne sont pas considérées comme des échecs, car l'appelant, et non le serveur, a provoqué l'échec.",

View file

@ -7547,7 +7547,6 @@
"xpack.apm.dataView.autoCreateDisabled": "データビューの自動作成は、「autoCreateApmDataView」構成オプションによって無効化されています",
"xpack.apm.dataView.noApmData": "APMデータがありません",
"xpack.apm.dependecyOperationDetailView.header.backLinkLabel": "すべての演算",
"xpack.apm.dependencies.kueryBarPlaceholder": "依存関係メトリックを検索span.destination.service.resource:elasticsearch",
"xpack.apm.dependenciesInventory.dependencyTableColumn": "依存関係",
"xpack.apm.dependenciesTable.columnErrorRate": "失敗したトランザクション率",
"xpack.apm.dependenciesTable.columnErrorRateTip": "選択したサービスの失敗したトランザクションの割合。4xxステータスコードクライアントエラーのHTTPサーバートランザクションは、サーバーではなく呼び出し側が失敗の原因であるため、失敗と見なされません。",

View file

@ -7546,7 +7546,6 @@
"xpack.apm.dataView.autoCreateDisabled": "已通过“autoCreateApmDataView”配置选项禁止自动创建数据视图",
"xpack.apm.dataView.noApmData": "无 APM 数据",
"xpack.apm.dependecyOperationDetailView.header.backLinkLabel": "所有操作",
"xpack.apm.dependencies.kueryBarPlaceholder": "搜索依赖项指标例如span.destination.service.resource:elasticsearch",
"xpack.apm.dependenciesInventory.dependencyTableColumn": "依赖项",
"xpack.apm.dependenciesTable.columnErrorRate": "失败事务率",
"xpack.apm.dependenciesTable.columnErrorRateTip": "选定服务的失败事务百分比。状态代码为 4xx 的 HTTP 服务器事务(客户端错误)不会视为失败,因为是调用方而不是服务器造成了失败。",