[APM] Replace environment dropdown with SuggestionsSelect

* Extend /suggestions api to accept range query

* Add new endpoint /suggestions_with_terms

* Use suggestionSelect for environment filter

* Clean up route

* UI tweaks for the EuiComboBox

* Make suggestion endpoint for service enrvironment explicit

- follow same naming convention for fields and values

* Fix deps

* Replace query_string with wildcard query

* Rename field to fieldName

* Introduce new enpoint has not defined environment

* Show Not defined option in environment filter

* get environment label for anomaly timeseries

* Fix types

* Select environment if there is only one available

* Fix tests

* move getPreferredServiceAnomalyTimeseries

* Remove redirect and get environment list from context

* Remove endpoint for not defined option

* Suggestions route fallbacks to use terms aggregation when service name is set

* Set default ranges (now-24h)

* Get environment list from context

* Fix types

* State should be handled by consuming component

* Use searchAggregatedTransactions

* Make ranges required for /suggestion endpoint

- the client set a default 24h-now if no URL is set

* Clean up files by separating the helper

* Fix debounce handler

- remove unused initial state

* Add option to run APM cypress tests in the flaky test runner

* Clean up environment context

- context will be responsible only for returning environment(s) info
- removing additional info not related to the context

* Rename event handlers

* Consuming component sets ranges for the suggestions select

* Immediately resolve the promise with empty response in case searchValue is empty

* Fix tests

* Get serviceName from useApmParams

* Fix type

* Initialize time ranges inside the components

* Remove redundant optimization

* Fix eslint import errors
This commit is contained in:
Katerina Patticha 2022-05-02 11:41:09 +02:00 committed by GitHub
parent 2ee62c42bc
commit abd615a15f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 382 additions and 189 deletions

View file

@ -33,13 +33,6 @@ export function getEnvironmentLabel(environment: string) {
return environment;
}
// #TODO Once we replace the select dropdown we can remove it
// EuiSelect > EuiSelectOption accepts text attribute
export const ENVIRONMENT_ALL_SELECT_OPTION = {
value: ENVIRONMENT_ALL_VALUE,
text: getEnvironmentLabel(ENVIRONMENT_ALL_VALUE),
};
export const ENVIRONMENT_ALL = {
value: ENVIRONMENT_ALL_VALUE,
label: getEnvironmentLabel(ENVIRONMENT_ALL_VALUE),
@ -47,7 +40,7 @@ export const ENVIRONMENT_ALL = {
export const ENVIRONMENT_NOT_DEFINED = {
value: ENVIRONMENT_NOT_DEFINED_VALUE,
text: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE),
label: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE),
};
export function getEnvironmentEsField(environment: string) {

View file

@ -20,7 +20,7 @@ const serviceInventoryHref = url.format({
query: timeRange,
});
const apiRequestsToIntercept = [
const mainApiRequestsToIntercept = [
{
endpoint: '/internal/apm/services?*',
aliasName: 'servicesRequest',
@ -31,7 +31,14 @@ const apiRequestsToIntercept = [
},
];
const aliasNames = apiRequestsToIntercept.map(
const secondaryApiRequestsToIntercept = [
{
endpoint: 'internal/apm/suggestions?*',
aliasName: 'suggestionsRequest',
},
];
const mainAliasNames = mainApiRequestsToIntercept.map(
({ aliasName }) => `@${aliasName}`
);
@ -77,43 +84,51 @@ describe('When navigating to the service inventory', () => {
describe.skip('Calls APIs', () => {
beforeEach(() => {
apiRequestsToIntercept.map(({ endpoint, aliasName }) => {
cy.intercept('GET', endpoint).as(aliasName);
});
[...mainApiRequestsToIntercept, ...secondaryApiRequestsToIntercept].map(
({ endpoint, aliasName }) => {
cy.intercept('GET', endpoint).as(aliasName);
}
);
cy.loginAsReadOnlyUser();
cy.visit(serviceInventoryHref);
});
it('with the correct environment when changing the environment', () => {
cy.wait(aliasNames);
cy.get('[data-test-subj="environmentFilter"]').select('production');
cy.wait(mainAliasNames);
cy.get('[data-test-subj="environmentFilter"]').type('pro');
cy.expectAPIsToHaveBeenCalledWith({
apisIntercepted: aliasNames,
apisIntercepted: ['@suggestionsRequest'],
value: 'fieldValue=pro',
});
cy.contains('button', 'production').click();
cy.expectAPIsToHaveBeenCalledWith({
apisIntercepted: mainAliasNames,
value: 'environment=production',
});
});
it('when clicking the refresh button', () => {
cy.wait(aliasNames);
cy.wait(mainAliasNames);
cy.contains('Refresh').click();
cy.wait(aliasNames);
cy.wait(mainAliasNames);
});
it('when selecting a different time range and clicking the update button', () => {
cy.wait(aliasNames);
cy.wait(mainAliasNames);
cy.selectAbsoluteTimeRange(
moment(timeRange.rangeFrom).subtract(5, 'm').toISOString(),
moment(timeRange.rangeTo).subtract(5, 'm').toISOString()
);
cy.contains('Update').click();
cy.wait(aliasNames);
cy.wait(mainAliasNames);
cy.contains('Refresh').click();
cy.wait(aliasNames);
cy.wait(mainAliasNames);
});
});
});

View file

@ -216,7 +216,18 @@ describe('Service Overview', () => {
it('with the correct environment when changing the environment', () => {
cy.wait(aliasNames, { requestTimeout: 10000 });
cy.get('[data-test-subj="environmentFilter"]').select('production');
cy.intercept('GET', 'internal/apm/suggestions?*').as(
'suggestionsRequest'
);
cy.get('[data-test-subj="environmentFilter"]').type('pro').click();
cy.expectAPIsToHaveBeenCalledWith({
apisIntercepted: ['@suggestionsRequest'],
value: 'fieldValue=pro',
});
cy.contains('button', 'production').click();
cy.expectAPIsToHaveBeenCalledWith({
apisIntercepted: aliasNames,

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { EuiFieldNumber } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
@ -38,16 +38,21 @@ export function ServiceField({
})}
>
<SuggestionsSelect
allOption={allowAll ? ENVIRONMENT_ALL : undefined}
customOptionText={i18n.translate('xpack.apm.selectCustomOptionText', {
defaultMessage: 'Add \\{searchValue\\} as a new option',
})}
customOptions={allowAll ? [ENVIRONMENT_ALL] : undefined}
customOptionText={i18n.translate(
'xpack.apm.serviceNamesSelectCustomOptionText',
{
defaultMessage: 'Add \\{searchValue\\} as a new service name',
}
)}
defaultValue={currentValue}
field={SERVICE_NAME}
fieldName={SERVICE_NAME}
onChange={onChange}
placeholder={i18n.translate('xpack.apm.serviceNamesSelectPlaceholder', {
defaultMessage: 'Select service name',
})}
start={moment().subtract(24, 'h').toISOString()}
end={moment().toISOString()}
/>
</PopoverExpression>
);
@ -68,16 +73,21 @@ export function EnvironmentField({
})}
>
<SuggestionsSelect
allOption={ENVIRONMENT_ALL}
customOptionText={i18n.translate('xpack.apm.selectCustomOptionText', {
defaultMessage: 'Add \\{searchValue\\} as a new option',
})}
customOptions={[ENVIRONMENT_ALL]}
customOptionText={i18n.translate(
'xpack.apm.environmentsSelectCustomOptionText',
{
defaultMessage: 'Add \\{searchValue\\} as a new environment',
}
)}
defaultValue={getEnvironmentLabel(currentValue)}
field={SERVICE_ENVIRONMENT}
fieldName={SERVICE_ENVIRONMENT}
onChange={onChange}
placeholder={i18n.translate('xpack.apm.environmentsSelectPlaceholder', {
defaultMessage: 'Select environment',
})}
start={moment().subtract(24, 'h').toISOString()}
end={moment().toISOString()}
/>
</PopoverExpression>
);
@ -96,12 +106,15 @@ export function TransactionTypeField({
return (
<PopoverExpression value={currentValue || allOptionText} title={label}>
<SuggestionsSelect
allOption={ENVIRONMENT_ALL}
customOptionText={i18n.translate('xpack.apm.selectCustomOptionText', {
defaultMessage: 'Add \\{searchValue\\} as a new option',
})}
customOptions={[ENVIRONMENT_ALL]}
customOptionText={i18n.translate(
'xpack.apm.transactionTypesSelectCustomOptionText',
{
defaultMessage: 'Add \\{searchValue\\} as a new transaction type',
}
)}
defaultValue={currentValue}
field={TRANSACTION_TYPE}
fieldName={TRANSACTION_TYPE}
onChange={onChange}
placeholder={i18n.translate(
'xpack.apm.transactionTypesSelectPlaceholder',
@ -109,6 +122,8 @@ export function TransactionTypeField({
defaultMessage: 'Select transaction type',
}
)}
start={moment().subtract(24, 'h').toISOString()}
end={moment().toISOString()}
/>
</PopoverExpression>
);

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
@ -13,7 +13,7 @@ import { ENVIRONMENT_ALL } from '../../../../../../../common/environment_filter_
interface Props {
title: string;
field: string;
fieldName: string;
description: string;
fieldLabel: string;
value?: string;
@ -24,7 +24,7 @@ interface Props {
export function FormRowSuggestionsSelect({
title,
field,
fieldName,
description,
fieldLabel,
value,
@ -40,9 +40,9 @@ export function FormRowSuggestionsSelect({
>
<EuiFormRow label={fieldLabel}>
<SuggestionsSelect
allOption={allowAll ? ENVIRONMENT_ALL : undefined}
customOptions={allowAll ? [ENVIRONMENT_ALL] : undefined}
defaultValue={value}
field={field}
fieldName={fieldName}
onChange={onChange}
isClearable={false}
placeholder={i18n.translate(
@ -50,6 +50,8 @@ export function FormRowSuggestionsSelect({
{ defaultMessage: 'Select Option' }
)}
dataTestSubj={dataTestSubj}
start={moment().subtract(24, 'h').toISOString()}
end={moment().toISOString()}
/>
</EuiFormRow>
</EuiDescribedFormGroup>

View file

@ -97,7 +97,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
'xpack.apm.agentConfig.servicePage.service.fieldLabel',
{ defaultMessage: 'Service name' }
)}
field={SERVICE_NAME}
fieldName={SERVICE_NAME}
value={newConfig.service.name}
onChange={(name) => {
setNewConfig((prev) => ({

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import {
EuiButtonEmpty,
EuiFlexGroup,
@ -119,7 +119,7 @@ export function FiltersSection({
<EuiFlexItem>
<SuggestionsSelect
dataTestSubj={`${key}.value`}
field={key}
fieldName={key}
placeholder={i18n.translate(
'xpack.apm.settings.customLink.flyOut.filters.defaultOption.value',
{ defaultMessage: 'Value' }
@ -129,6 +129,8 @@ export function FiltersSection({
}
defaultValue={value}
isInvalid={!isEmpty(key) && isEmpty(value)}
start={moment().subtract(24, 'h').toISOString()}
end={moment().toISOString()}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -5,18 +5,11 @@
* 2.0.
*/
import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
import React from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import {
ENVIRONMENT_ALL_SELECT_OPTION,
ENVIRONMENT_NOT_DEFINED,
} from '../../../../common/environment_filter_values';
import { fromQuery, toQuery } from '../links/url_helpers';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { Environment } from '../../../../common/environment_rt';
import { EnvironmentSelect } from '../environment_select';
import { useEnvironmentsContext } from '../../../context/environments_context/use_environments_context';
function updateEnvironmentUrl(
@ -34,77 +27,19 @@ function updateEnvironmentUrl(
});
}
const SEPARATOR_OPTION = {
text: `- ${i18n.translate(
'xpack.apm.filter.environment.selectEnvironmentLabel',
{ defaultMessage: 'Select environment' }
)} -`,
disabled: true,
};
function getOptions(environments: string[]) {
const environmentOptions = environments
.filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value)
.map((environment) => ({
value: environment,
text: environment,
}));
return [
ENVIRONMENT_ALL_SELECT_OPTION,
...(environments.includes(ENVIRONMENT_NOT_DEFINED.value)
? [ENVIRONMENT_NOT_DEFINED]
: []),
...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []),
...environmentOptions,
];
}
export function ApmEnvironmentFilter() {
const { status, environments, environment } = useEnvironmentsContext();
return (
<EnvironmentFilter
status={status}
environment={environment}
environments={environments}
/>
);
}
export function EnvironmentFilter({
environment,
environments,
status,
}: {
environment: Environment;
environments: Environment[];
status: FETCH_STATUS;
}) {
const { environment, environments, status } = useEnvironmentsContext();
const history = useHistory();
const location = useLocation();
// Set the min-width so we don't see as much collapsing of the select during
// the loading state. 200px is what is looks like if "production" is
// the contents.
const minWidth = 200;
const options = getOptions(environments);
return (
<EuiSelect
fullWidth
prepend={i18n.translate('xpack.apm.filter.environment.label', {
defaultMessage: 'Environment',
})}
options={options}
value={environment}
onChange={(event) => {
updateEnvironmentUrl(history, location, event.target.value);
}}
isLoading={status === FETCH_STATUS.LOADING}
style={{ minWidth }}
data-test-subj="environmentFilter"
<EnvironmentSelect
status={status}
environment={environment}
availableEnvironments={environments}
onChange={(changeValue: string) =>
updateEnvironmentUrl(history, location, changeValue)
}
/>
);
}

View file

@ -0,0 +1,124 @@
/*
* 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 { isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
import React, { useMemo, useState } from 'react';
import { debounce } from 'lodash';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import {
getEnvironmentLabel,
ENVIRONMENT_NOT_DEFINED,
ENVIRONMENT_ALL,
} from '../../../../common/environment_filter_values';
import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useApmParams } from '../../../hooks/use_apm_params';
import { Environment } from '../../../../common/environment_rt';
function getEnvironmentOptions(environments: Environment[]) {
const environmentOptions = environments
.filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value)
.map((environment) => ({
value: environment,
label: environment,
}));
return [
ENVIRONMENT_ALL,
...(environments.includes(ENVIRONMENT_NOT_DEFINED.value)
? [ENVIRONMENT_NOT_DEFINED]
: []),
...environmentOptions,
];
}
export function EnvironmentSelect({
environment,
availableEnvironments,
status,
onChange,
}: {
environment: Environment;
availableEnvironments: Environment[];
status: FETCH_STATUS;
onChange: (value: string) => void;
}) {
const [searchValue, setSearchValue] = useState('');
const {
path: { serviceName },
query: { rangeFrom, rangeTo },
} = useApmParams('/services/{serviceName}/*');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const selectedOptions: Array<EuiComboBoxOptionOption<string>> = [
{
value: environment,
label: getEnvironmentLabel(environment),
},
];
const onSelect = (changedOptions: Array<EuiComboBoxOptionOption<string>>) => {
if (changedOptions.length === 1 && changedOptions[0].value) {
onChange(changedOptions[0].value);
}
};
const { data, status: searchStatus } = useFetcher(
(callApmApi) => {
return isEmpty(searchValue)
? Promise.resolve({ terms: [] })
: callApmApi('GET /internal/apm/suggestions', {
params: {
query: {
fieldName: SERVICE_ENVIRONMENT,
fieldValue: searchValue,
serviceName,
start,
end,
},
},
});
},
[searchValue, start, end, serviceName]
);
const terms = data?.terms ?? [];
const options: Array<EuiComboBoxOptionOption<string>> = [
...(searchValue === ''
? getEnvironmentOptions(availableEnvironments)
: terms.map((name) => {
return { label: name, value: name };
})),
];
const onSearch = useMemo(() => debounce(setSearchValue, 300), []);
return (
<EuiComboBox
data-test-subj="environmentFilter"
async
isClearable={false}
style={{ minWidth: '256px' }}
placeholder={i18n.translate('xpack.apm.filter.environment.placeholder', {
defaultMessage: 'Select environment',
})}
prepend={i18n.translate('xpack.apm.filter.environment.label', {
defaultMessage: 'Environment',
})}
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selectedOptions}
onChange={(changedOptions) => onSelect(changedOptions)}
onSearchChange={onSearch}
isLoading={
status === FETCH_STATUS.LOADING || searchStatus === FETCH_STATUS.LOADING
}
/>
);
}

View file

@ -11,27 +11,33 @@ import React, { useCallback, useState } from 'react';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
interface SuggestionsSelectProps {
allOption?: EuiComboBoxOptionOption<string>;
customOptions?: Array<EuiComboBoxOptionOption<string>>;
customOptionText?: string;
defaultValue?: string;
field: string;
fieldName: string;
start: string;
end: string;
onChange: (value?: string) => void;
isClearable?: boolean;
isInvalid?: boolean;
placeholder: string;
dataTestSubj?: string;
prepend?: string;
}
export function SuggestionsSelect({
allOption,
customOptions,
customOptionText,
defaultValue,
field,
fieldName,
start,
end,
onChange,
placeholder,
isInvalid,
dataTestSubj,
isClearable = true,
prepend,
}: SuggestionsSelectProps) {
let defaultOption: EuiComboBoxOptionOption<string> | undefined;
@ -48,11 +54,16 @@ export function SuggestionsSelect({
(callApmApi) => {
return callApmApi('GET /internal/apm/suggestions', {
params: {
query: { field, string: searchValue },
query: {
fieldName,
fieldValue: searchValue,
start,
end,
},
},
});
},
[field, searchValue],
[fieldName, searchValue, start, end],
{ preservePreviousData: false }
);
@ -85,11 +96,7 @@ export function SuggestionsSelect({
const terms = data?.terms ?? [];
const options: Array<EuiComboBoxOptionOption<string>> = [
...(allOption &&
(searchValue === '' ||
searchValue.toLowerCase() === allOption.label.toLowerCase())
? [allOption]
: []),
...(customOptions ? customOptions : []),
...terms.map((name) => {
return { label: name, value: name };
}),
@ -111,6 +118,7 @@ export function SuggestionsSelect({
style={{ minWidth: '256px' }}
onCreateOption={handleCreateOption}
data-test-subj={dataTestSubj}
prepend={prepend}
/>
);
}

View file

@ -60,11 +60,13 @@ export const Example: Story<Args> = ({
}) => {
return (
<SuggestionsSelect
allOption={allOption}
customOptions={[allOption]}
customOptionText={customOptionText}
field={field}
fieldName={field}
onChange={() => {}}
placeholder={placeholder}
start={'2022-04-13T10:29:28.541Z'}
end={'2021-04-13T10:29:28.541Z'}
/>
);
};

View file

@ -8,9 +8,9 @@ import React from 'react';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { Environment } from '../../../common/environment_rt';
import { useApmParams } from '../../hooks/use_apm_params';
import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher';
import { FETCH_STATUS } from '../../hooks/use_fetcher';
import { useTimeRange } from '../../hooks/use_time_range';
import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher';
export const EnvironmentsContext = React.createContext<{
environment: Environment;
@ -48,9 +48,9 @@ export function EnvironmentsContextProvider({
return (
<EnvironmentsContext.Provider
value={{
environment,
environments,
status,
environment,
}}
>
{children}

View file

@ -5,23 +5,7 @@
* 2.0.
*/
import { useMemo } from 'react';
import { useFetcher } from './use_fetcher';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
} from '../../common/environment_filter_values';
function getEnvironmentOptions(environments: string[]) {
const environmentOptions = environments
.filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value)
.map((environment) => ({
value: environment,
text: environment,
}));
return [ENVIRONMENT_ALL, ...environmentOptions];
}
const INITIAL_DATA = { environments: [] };
@ -51,10 +35,5 @@ export function useEnvironmentsFetcher({
[start, end, serviceName]
);
const environmentOptions = useMemo(
() => getEnvironmentOptions(data.environments),
[data?.environments]
);
return { environments: data.environments, status, environmentOptions };
return { environments: data.environments, status };
}

View file

@ -4,23 +4,26 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ProcessorEvent } from '../../../common/processor_event';
import { getProcessorEventForTransactions } from '../../lib/helpers/transactions';
import { Setup } from '../../lib/helpers/setup_request';
export async function getSuggestions({
field,
fieldName,
fieldValue,
searchAggregatedTransactions,
setup,
size,
string,
start,
end,
}: {
field: string;
fieldName: string;
fieldValue: string;
searchAggregatedTransactions: boolean;
setup: Setup;
size: number;
string: string;
start: number;
end: number;
}) {
const { apmEventClient } = setup;
@ -34,9 +37,18 @@ export async function getSuggestions({
},
body: {
case_insensitive: true,
field,
field: fieldName,
size,
string,
string: fieldValue,
index_filter: {
range: {
['@timestamp']: {
gte: start,
lte: end,
format: 'epoch_millis',
},
},
},
},
});

View file

@ -0,0 +1,75 @@
/*
* 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 { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
import { ProcessorEvent } from '../../../common/processor_event';
import { getProcessorEventForTransactions } from '../../lib/helpers/transactions';
import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames';
import { Setup } from '../../lib/helpers/setup_request';
export async function getSuggestionsWithTermsAggregation({
fieldName,
fieldValue,
searchAggregatedTransactions,
serviceName,
setup,
size,
start,
end,
}: {
fieldName: string;
fieldValue: string;
searchAggregatedTransactions: boolean;
serviceName: string;
setup: Setup;
size: number;
start: number;
end: number;
}) {
const { apmEventClient } = setup;
const response = await apmEventClient.search(
'get_suggestions_with_terms_aggregation',
{
apm: {
events: [
getProcessorEventForTransactions(searchAggregatedTransactions),
ProcessorEvent.error,
ProcessorEvent.metric,
],
},
body: {
timeout: '1500ms',
size: 0,
query: {
bool: {
filter: [
...termQuery(SERVICE_NAME, serviceName),
...rangeQuery(start, end),
{
wildcard: {
[fieldName]: `*${fieldValue}*`,
},
},
],
},
},
aggs: {
items: {
terms: { field: fieldName, size },
},
},
},
}
);
return {
terms:
response.aggregations?.items.buckets.map(
(bucket) => bucket.key as string
) ?? [],
};
}

View file

@ -8,20 +8,29 @@
import * as t from 'io-ts';
import { maxSuggestions } from '@kbn/observability-plugin/common';
import { getSuggestions } from './get_suggestions';
import { getSuggestionsWithTermsAggregation } from './get_suggestions_with_terms_aggregation';
import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions';
import { setupRequest } from '../../lib/helpers/setup_request';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import { rangeRt } from '../default_api_types';
const suggestionsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/suggestions',
params: t.partial({
query: t.type({ field: t.string, string: t.string }),
params: t.type({
query: t.intersection([
t.type({
fieldName: t.string,
fieldValue: t.string,
}),
rangeRt,
t.partial({ serviceName: t.string }),
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources): Promise<{ terms: string[] }> => {
const setup = await setupRequest(resources);
const { context, params } = resources;
const { field, string } = params.query;
const { fieldName, fieldValue, serviceName, start, end } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
apmEventClient: setup.apmEventClient,
config: setup.config,
@ -31,16 +40,32 @@ const suggestionsRoute = createApmServerRoute({
const size = await coreContext.uiSettings.client.get<number>(
maxSuggestions
);
const suggestions = await getSuggestions({
field,
searchAggregatedTransactions,
setup,
size,
string,
});
const suggestions = serviceName
? await getSuggestionsWithTermsAggregation({
fieldName,
fieldValue,
searchAggregatedTransactions,
serviceName,
setup,
size,
start,
end,
})
: await getSuggestions({
fieldName,
fieldValue,
searchAggregatedTransactions,
setup,
size,
start,
end,
});
return suggestions;
},
});
export const suggestionsRouteRepository = suggestionsRoute;
export const suggestionsRouteRepository = {
...suggestionsRoute,
};

View file

@ -7493,7 +7493,6 @@
"xpack.apm.filter.environment.allLabel": "Tous",
"xpack.apm.filter.environment.label": "Environnement",
"xpack.apm.filter.environment.notDefinedLabel": "Non défini",
"xpack.apm.filter.environment.selectEnvironmentLabel": "Sélectionner l'environnement",
"xpack.apm.fleet_integration.settings.advancedOptionsLavel": "Options avancées",
"xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText": "Noms d'agents autorisés pour l'accès anonyme.",
"xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel": "Agents autorisés",
@ -7720,7 +7719,6 @@
"xpack.apm.propertiesTable.tabs.metadataLabel": "Métadonnées",
"xpack.apm.propertiesTable.tabs.timelineLabel": "Chronologie",
"xpack.apm.searchInput.filter": "Filtrer…",
"xpack.apm.selectCustomOptionText": "Ajouter \\{searchValue\\} en tant que nouvelle option",
"xpack.apm.selectPlaceholder": "Sélectionner une option :",
"xpack.apm.serviceDependencies.breakdownChartTitle": "Temps consacré par dépendance",
"xpack.apm.serviceDetails.dependenciesTabLabel": "Dépendances",

View file

@ -7481,7 +7481,6 @@
"xpack.apm.filter.environment.allLabel": "すべて",
"xpack.apm.filter.environment.label": "環境",
"xpack.apm.filter.environment.notDefinedLabel": "未定義",
"xpack.apm.filter.environment.selectEnvironmentLabel": "環境を選択",
"xpack.apm.fleet_integration.settings.advancedOptionsLavel": "高度なオプション",
"xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText": "匿名アクセスの許可されたエージェント名。",
"xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel": "許可されたエージェント",
@ -7706,7 +7705,6 @@
"xpack.apm.propertiesTable.tabs.metadataLabel": "メタデータ",
"xpack.apm.propertiesTable.tabs.timelineLabel": "Timeline",
"xpack.apm.searchInput.filter": "フィルター...",
"xpack.apm.selectCustomOptionText": "\\{searchValue\\}を新しいオプションとして追加",
"xpack.apm.selectPlaceholder": "オプションを選択:",
"xpack.apm.serviceDependencies.breakdownChartTitle": "依存関係にかかった時間",
"xpack.apm.serviceDetails.dependenciesTabLabel": "依存関係",

View file

@ -7498,7 +7498,6 @@
"xpack.apm.filter.environment.allLabel": "全部",
"xpack.apm.filter.environment.label": "环境",
"xpack.apm.filter.environment.notDefinedLabel": "未定义",
"xpack.apm.filter.environment.selectEnvironmentLabel": "选择环境",
"xpack.apm.fleet_integration.settings.advancedOptionsLavel": "高级选项",
"xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText": "允许进行匿名访问的代理名称。",
"xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel": "允许的代理",
@ -7725,7 +7724,6 @@
"xpack.apm.propertiesTable.tabs.metadataLabel": "元数据",
"xpack.apm.propertiesTable.tabs.timelineLabel": "时间线",
"xpack.apm.searchInput.filter": "筛选...",
"xpack.apm.selectCustomOptionText": "将 \\{searchValue\\} 添加为新选项",
"xpack.apm.selectPlaceholder": "选择选项:",
"xpack.apm.serviceDependencies.breakdownChartTitle": "依赖项花费的时间",
"xpack.apm.serviceDetails.dependenciesTabLabel": "依赖项",

View file

@ -9,12 +9,14 @@ import {
SERVICE_NAME,
TRANSACTION_TYPE,
} from '@kbn/apm-plugin/common/elasticsearch_fieldnames';
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function suggestionsTests({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const archiveName = 'apm_8.0.0';
const { start, end } = archives_metadata[archiveName];
registry.when(
'suggestions when data is loaded',
@ -25,7 +27,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) {
it('returns all environments', async () => {
const { body } = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/suggestions',
params: { query: { field: SERVICE_ENVIRONMENT, string: '' } },
params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: '', start, end } },
});
expectSnapshot(body).toMatchInline(`
@ -43,7 +45,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) {
it('returns items matching the string parameter', async () => {
const { body } = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/suggestions',
params: { query: { field: SERVICE_ENVIRONMENT, string: 'pr' } },
params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: 'pr', start, end } },
});
expectSnapshot(body).toMatchInline(`
@ -62,7 +64,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) {
it('returns all services', async () => {
const { body } = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/suggestions',
params: { query: { field: SERVICE_NAME, string: '' } },
params: { query: { fieldName: SERVICE_NAME, fieldValue: '', start, end } },
});
expectSnapshot(body).toMatchInline(`
@ -86,7 +88,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) {
it('returns items matching the string parameter', async () => {
const { body } = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/suggestions',
params: { query: { field: SERVICE_NAME, string: 'aud' } },
params: { query: { fieldName: SERVICE_NAME, fieldValue: 'aud', start, end } },
});
expectSnapshot(body).toMatchInline(`
@ -105,7 +107,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) {
it('returns all transaction types', async () => {
const { body } = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/suggestions',
params: { query: { field: TRANSACTION_TYPE, string: '' } },
params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: '', start, end } },
});
expectSnapshot(body).toMatchInline(`
@ -125,7 +127,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) {
it('returns items matching the string parameter', async () => {
const { body } = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/suggestions',
params: { query: { field: TRANSACTION_TYPE, string: 'w' } },
params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: 'w', start, end } },
});
expectSnapshot(body).toMatchInline(`

View file

@ -70,7 +70,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await retry.try(async () => {
const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer');
const apmMainContainerTextItems = apmMainContainerText[0].split('\n');
expect(apmMainContainerTextItems).to.not.contain('No services found');
expect(apmMainContainerTextItems).to.contain('opbeans-go');