[UX] Update url filter (#79497)

This commit is contained in:
Shahzad 2020-10-06 19:29:41 +02:00 committed by GitHub
parent 06f87bb838
commit d845922a1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 180 deletions

View file

@ -55,7 +55,7 @@ Then(`it should filter results based on query`, () => {
listOfUrls.should('have.length', 1);
const actualUrlsText = [
'http://opbeans-node:3000/customersPage views: 10Page load duration: 76 ms ',
'http://opbeans-node:3000/customersPage views: 10Page load duration: 76 ms',
];
cy.get('li.euiSelectableListItem')

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { EnvironmentFilter } from '../../../shared/EnvironmentFilter';
import { ServiceNameFilter } from '../URLFilter/ServiceNameFilter';
import { useFetcher } from '../../../../hooks/useFetcher';
import { RUM_AGENT_NAMES } from '../../../../../common/agent_name';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { UserPercentile } from '../UserPercentile';
export function MainFilters() {
const {
urlParams: { start, end },
} = useUrlParams();
const { data, status } = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/services',
params: {
query: {
start,
end,
uiFilters: JSON.stringify({ agentName: RUM_AGENT_NAMES }),
},
},
});
}
},
[start, end]
);
return (
<>
<EuiFlexItem grow={false}>
<ServiceNameFilter
loading={status !== 'success'}
serviceNames={data ?? []}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UserPercentile />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ maxWidth: 200 }}>
<EnvironmentFilter />
</EuiFlexItem>
</>
);
}

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { DatePicker } from '../../../shared/DatePicker';
export function RumHeader({ children }: { children: ReactNode }) {
return (
<>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>
<DatePicker />
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -8,9 +8,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { RumOverview } from '../RumDashboard';
import { RumHeader } from './RumHeader';
import { UserPercentile } from './UserPercentile';
import { CsmSharedContextProvider } from './CsmSharedContext';
import { MainFilters } from './Panels/MainFilters';
import { DatePicker } from '../../shared/DatePicker';
export const UX_LABEL = i18n.translate('xpack.apm.ux.title', {
defaultMessage: 'User Experience',
@ -19,18 +19,25 @@ export const UX_LABEL = i18n.translate('xpack.apm.ux.title', {
export function RumHome() {
return (
<CsmSharedContextProvider>
<RumHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={true}>
<EuiTitle size="l">
<h1>{UX_LABEL}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UserPercentile />
</EuiFlexItem>
</EuiFlexGroup>
</RumHeader>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="l">
<h1 className="eui-textNoWrap">{UX_LABEL}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
wrap
style={{ flexWrap: 'wrap-reverse' }}
justifyContent="flexEnd"
>
<MainFilters />
<EuiFlexItem grow={false}>
<DatePicker />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<RumOverview />
</CsmSharedContextProvider>
);

View file

@ -4,12 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiHorizontalRule,
EuiSelect,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
@ -66,22 +61,17 @@ function ServiceNameFilter({ loading, serviceNames }: Props) {
return (
<>
<EuiTitle size="xxxs" textTransform="uppercase">
<h4>
{i18n.translate('xpack.apm.localFilters.titles.serviceName', {
defaultMessage: 'Service name',
})}
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<EuiHorizontalRule margin="none" />
<EuiSpacer size="s" />
<EuiSelect
prepend={i18n.translate(
'xpack.apm.ux.localFilters.titles.webApplication',
{
defaultMessage: 'Web Application',
}
)}
isLoading={loading}
data-cy="serviceNameFilter"
options={options}
value={selectedServiceName}
compressed={true}
onChange={(event) => {
updateServiceName(event.target.value);
}}

View file

@ -4,9 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FormEvent, SetStateAction, useRef, useState } from 'react';
import React, {
FormEvent,
SetStateAction,
useRef,
useState,
KeyboardEvent,
} from 'react';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
@ -14,13 +19,41 @@ import {
EuiPopoverTitle,
EuiSelectable,
EuiSelectableMessage,
EuiPopoverFooter,
EuiButton,
EuiText,
EuiIcon,
EuiBadge,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { useEvent } from 'react-use';
import {
formatOptions,
selectableRenderOptions,
UrlOption,
} from './RenderOption';
import { I18LABELS } from '../../translations';
import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public';
const StyledRow = styled.div<{
darkMode: boolean;
}>`
text-align: center;
padding: 8px 0px;
background-color: ${(props) =>
props.darkMode
? euiDarkVars.euiPageBackgroundColor
: euiLightVars.euiPageBackgroundColor};
border-bottom: 1px solid
${(props) =>
props.darkMode
? euiDarkVars.euiColorLightestShade
: euiLightVars.euiColorLightestShade};
`;
interface Props {
data: {
@ -48,11 +81,23 @@ export function SelectableUrlList({
popoverIsOpen,
setPopoverIsOpen,
}: Props) {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
const [popoverRef, setPopoverRef] = useState<HTMLElement | null>(null);
const [searchRef, setSearchRef] = useState<HTMLInputElement | null>(null);
const titleRef = useRef<HTMLDivElement>(null);
const onEnterKey = (evt: KeyboardEvent<HTMLInputElement>) => {
if (evt.key.toLowerCase() === 'enter') {
onTermChange();
setPopoverIsOpen(false);
}
};
// @ts-ignore - not sure, why it's not working
useEvent('keydown', onEnterKey, searchRef);
const searchOnFocus = (e: React.FocusEvent<HTMLInputElement>) => {
setPopoverIsOpen(true);
};
@ -102,22 +147,10 @@ export function SelectableUrlList({
function PopOverTitle() {
return (
<EuiPopoverTitle>
<EuiFlexGroup ref={titleRef}>
<EuiFlexGroup ref={titleRef} gutterSize="xs">
<EuiFlexItem style={{ justifyContent: 'center' }}>
{loading ? <EuiLoadingSpinner /> : titleText}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
disabled={!searchValue}
onClick={() => {
onTermChange();
setPopoverIsOpen(false);
}}
>
{I18LABELS.matchThisQuery}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
);
@ -142,6 +175,7 @@ export function SelectableUrlList({
listProps={{
rowHeight: 68,
showIcons: true,
onFocusBadge: false,
}}
loadingMessage={loadingMessage}
emptyMessage={emptyMessage}
@ -158,7 +192,43 @@ export function SelectableUrlList({
>
<div style={{ width: 600, maxWidth: '100%' }}>
<PopOverTitle />
{searchValue && (
<StyledRow darkMode={darkMode}>
<EuiText size="s">
<FormattedMessage
id="xpack.apm.ux.url.hitEnter.include"
defaultMessage="Hit {icon} to include all urls matching {searchValue}"
values={{
searchValue: <strong>{searchValue}</strong>,
icon: (
<EuiBadge color="hollow">
Enter <EuiIcon type="returnKey" />
</EuiBadge>
),
}}
/>
</EuiText>
</StyledRow>
)}
{list}
<EuiPopoverFooter>
<EuiFlexGroup style={{ justifyContent: 'flex-end' }}>
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
onClick={() => {
onTermChange();
closePopover();
}}
>
{i18n.translate('xpack.apm.apply.label', {
defaultMessage: 'Apply',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverFooter>
</div>
</EuiPopover>
)}

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiTitle } from '@elastic/eui';
import useDebounce from 'react-use/lib/useDebounce';
import React, { useEffect, useState, FormEvent, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { EuiTitle } from '@elastic/eui';
import { useUrlParams } from '../../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../../hooks/useFetcher';
import { I18LABELS } from '../../translations';
@ -24,7 +24,9 @@ interface Props {
export function URLSearch({ onChange: onFilterChange }: Props) {
const history = useHistory();
const { uiFilters } = useUrlParams();
const { uiFilters, urlParams } = useUrlParams();
const { searchTerm } = urlParams;
const [popoverIsOpen, setPopoverIsOpen] = useState(false);
@ -84,6 +86,12 @@ export function URLSearch({ onChange: onFilterChange }: Props) {
setCheckedUrls(uiFilters.transactionUrl || []);
}, [uiFilters]);
useEffect(() => {
if (searchTerm && searchValue === '') {
updateSearchTerm('');
}
}, [searchValue, updateSearchTerm, searchTerm]);
const onChange = (updatedOptions: UrlOption[]) => {
const clickedItems = updatedOptions.filter(
(option) => option.checked === 'on'
@ -121,7 +129,7 @@ export function URLSearch({ onChange: onFilterChange }: Props) {
return (
<>
<EuiTitle size="xxs" textTransform="uppercase">
<EuiTitle size="xxxs" textTransform="uppercase">
<h4>{I18LABELS.url}</h4>
</EuiTitle>
<SelectableUrlList

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { useCallback } from 'react';
import { EuiSpacer, EuiBadge } from '@elastic/eui';
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { omit } from 'lodash';
import { URLSearch } from './URLSearch';
@ -16,18 +15,9 @@ import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { removeUndefinedProps } from '../../../../context/UrlParamsContext/helpers';
import { LocalUIFilterName } from '../../../../../common/ui_filter';
const removeSearchTermLabel = i18n.translate(
'xpack.apm.uiFilter.url.removeSearchTerm',
{ defaultMessage: 'Clear url query' }
);
export function URLFilter() {
const history = useHistory();
const {
urlParams: { searchTerm },
} = useUrlParams();
const setFilterValue = (name: LocalUIFilterName, value: string[]) => {
const search = omit(toQuery(history.location.search), name);
@ -42,20 +32,6 @@ export function URLFilter() {
});
};
const updateSearchTerm = useCallback(
(searchTermN?: string) => {
const newLocation = {
...history.location,
search: fromQuery({
...toQuery(history.location.search),
searchTerm: searchTermN,
}),
};
history.push(newLocation);
},
[history]
);
const name = 'transactionUrl';
const { uiFilters } = useUrlParams();
@ -65,44 +41,25 @@ export function URLFilter() {
return (
<span data-cy="csmUrlFilter">
<EuiSpacer size="s" />
<URLSearch
onChange={(value) => {
setFilterValue('transactionUrl', value);
}}
/>
<EuiSpacer size="s" />
{searchTerm && (
{filterValue.length > 0 && (
<>
<EuiBadge
onClick={() => {
updateSearchTerm();
}}
onClickAriaLabel={removeSearchTermLabel}
iconOnClick={() => {
updateSearchTerm();
}}
iconOnClickAriaLabel={removeSearchTermLabel}
iconType="cross"
iconSide="right"
>
*{searchTerm}*
</EuiBadge>
<EuiSpacer size="s" />
<UrlList
onRemove={(val) => {
setFilterValue(
name,
filterValue.filter((v) => val !== v)
);
}}
value={filterValue}
/>
</>
)}
{filterValue.length > 0 && (
<UrlList
onRemove={(val) => {
setFilterValue(
name,
filterValue.filter((v) => val !== v)
);
}}
value={filterValue}
/>
)}
<EuiSpacer size="m" />
</span>
);
}

View file

@ -44,8 +44,7 @@ export function UserPercentile() {
if (!percentile) {
updatePercentile(DEFAULT_P);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
});
const options = [
{

View file

@ -15,7 +15,7 @@ export function useUxQuery() {
const queryParams = useMemo(() => {
const { serviceName } = uiFilters;
if (start && end && serviceName) {
if (start && end && serviceName && percentile) {
return {
start,
end,

View file

@ -5,22 +5,13 @@
*/
import React, { useMemo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiSpacer,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { RumDashboard } from './RumDashboard';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useFetcher } from '../../../hooks/useFetcher';
import { RUM_AGENT_NAMES } from '../../../../common/agent_name';
import { EnvironmentFilter } from '../../shared/EnvironmentFilter';
import { URLFilter } from './URLFilter';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { ServiceNameFilter } from './URLFilter/ServiceNameFilter';
import { URLFilter } from './URLFilter';
export function RumOverview() {
useTrackPageview({ app: 'ux', path: 'home' });
@ -35,46 +26,14 @@ export function RumOverview() {
return config;
}, []);
const {
urlParams: { start, end },
} = useUrlParams();
const { data, status } = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/services',
params: {
query: {
start,
end,
uiFilters: JSON.stringify({ agentName: RUM_AGENT_NAMES }),
},
},
});
}
},
[start, end]
);
return (
<>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EnvironmentFilter />
<EuiSpacer />
<LocalUIFilters {...localUIFiltersConfig} showCount={true}>
<>
<ServiceNameFilter
loading={status !== 'success'}
serviceNames={data ?? []}
/>
<EuiSpacer size="xl" />
<URLFilter />
<EuiHorizontalRule margin="none" />{' '}
</>
<URLFilter />
<EuiSpacer size="s" />
</LocalUIFilters>
</EuiFlexItem>
<EuiFlexItem grow={7}>

View file

@ -92,7 +92,7 @@ export const I18LABELS = {
}
),
searchByUrl: i18n.translate('xpack.apm.rum.filters.searchByUrl', {
defaultMessage: 'Search by url',
defaultMessage: 'Search by URL',
}),
getSearchResultsLabel: (total: number) =>
i18n.translate('xpack.apm.rum.filters.searchResults', {
@ -108,9 +108,6 @@ export const I18LABELS = {
url: i18n.translate('xpack.apm.rum.filters.url', {
defaultMessage: 'Url',
}),
matchThisQuery: i18n.translate('xpack.apm.rum.filters.url.matchThisQuery', {
defaultMessage: 'Match this query',
}),
loadingResults: i18n.translate('xpack.apm.rum.filters.url.loadingResults', {
defaultMessage: 'Loading results',
}),