mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
2ee62c42bc
commit
abd615a15f
21 changed files with 382 additions and 189 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
) ?? [],
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "依存関係",
|
||||
|
|
|
@ -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": "依赖项",
|
||||
|
|
|
@ -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(`
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue