mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
9e712bb6fe
commit
60c8b8fecd
28 changed files with 500 additions and 112 deletions
|
@ -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,
|
||||
}: {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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'} />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
|
@ -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[];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -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
|
|
@ -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.",
|
||||
|
|
|
@ -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サーバートランザクションは、サーバーではなく呼び出し側が失敗の原因であるため、失敗と見なされません。",
|
||||
|
|
|
@ -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 服务器事务(客户端错误)不会视为失败,因为是调用方而不是服务器造成了失败。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue