[UX] Use dynamic data view in visitor breakdown charts (#142992)

* Use ux dynamic data view in visitor breakdown

* Add default time field to data view

* Fix types and unit tests

* Memoize filter values to prevent lens re-rendering

* Fix unit tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Emilio Alvarez Piñeiro 2022-10-11 11:49:23 +02:00 committed by GitHub
parent 2a0a1e6498
commit 7a74bbf3f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 91 deletions

View file

@ -2,19 +2,13 @@
exports[`VisitorBreakdownChart getVisitorBreakdownLensAttributes generates expected lens attributes 1`] = `
Object {
"references": Array [
Object {
"id": "Required",
"name": "indexpattern-datasource-current-indexpattern",
"type": "index-pattern",
},
Object {
"id": "Required",
"name": "indexpattern-datasource-layer-layer1",
"type": "index-pattern",
},
],
"references": Array [],
"state": Object {
"adHocDataViews": Object {
"xxxx-xxxxxxxxxxx-xxxx": Object {
"id": "xxxx-xxxxxxxxxxx-xxxx",
},
},
"datasourceStates": Object {
"indexpattern": Object {
"layers": Object {
@ -95,6 +89,18 @@ Object {
},
},
],
"internalReferences": Array [
Object {
"id": "xxxx-xxxxxxxxxxx-xxxx",
"name": "indexpattern-datasource-current-indexpattern",
"type": "index-pattern",
},
Object {
"id": "xxxx-xxxxxxxxxxx-xxxx",
"name": "indexpattern-datasource-layer-layer1",
"type": "index-pattern",
},
],
"query": Object {
"language": "kuery",
"query": "",

View file

@ -13,8 +13,19 @@ import {
VisitorBreakdownMetric,
} from './visitor_breakdown_chart';
import { useKibanaServices } from '../../../../hooks/use_kibana_services';
import type { DataView } from '@kbn/data-views-plugin/public';
jest.mock('../../../../hooks/use_kibana_services');
jest.mock('uuid');
const mockDataView = {
id: 'mock-id',
title: 'mock-title',
timeFieldName: 'mock-time-field-name',
isPersisted: () => false,
getName: () => 'mock-data-view',
toSpec: () => ({}),
} as DataView;
describe('VisitorBreakdownChart', () => {
describe('getVisitorBreakdownLensAttributes', () => {
@ -25,7 +36,8 @@ describe('VisitorBreakdownChart', () => {
environment: 'ENVIRONMENT_ALL',
},
urlQuery: 'elastic.co',
dataView: 'Required',
dataView: mockDataView,
localDataViewId: 'xxxx-xxxxxxxxxxx-xxxx',
};
expect(getVisitorBreakdownLensAttributes(props)).toMatchSnapshot();
@ -33,6 +45,8 @@ describe('VisitorBreakdownChart', () => {
});
describe('component', () => {
const mockUuid = jest.requireMock('uuid');
mockUuid.v4 = jest.fn().mockReturnValue('xxxx-xxxxxxxxxxx-xxxx');
const mockEmbeddableComponent = jest.fn((_) => <></>);
beforeEach(() => {
@ -53,7 +67,8 @@ describe('VisitorBreakdownChart', () => {
environment: 'ENVIRONMENT_ALL',
},
urlQuery: 'elastic.co',
dataView: 'Required',
dataView: mockDataView,
localDataViewId: 'xxxx-xxxxxxxxxxx-xxxx',
onFilter: (_m: VisitorBreakdownMetric, _e: any) => {},
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import {
CountIndexPatternColumn,
@ -16,6 +16,8 @@ import {
} from '@kbn/lens-plugin/public';
import { EuiText } from '@elastic/eui';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { DataView } from '@kbn/data-views-plugin/public';
import uuid from 'uuid';
import { TRANSACTION_PAGE_LOAD } from '../../../../../common/transaction_types';
import {
PROCESSOR_EVENT,
@ -36,7 +38,7 @@ interface LensAttributes {
metric: VisitorBreakdownMetric;
uiFilters: UxUIFilters;
urlQuery?: string;
dataView: string;
dataView: DataView;
}
type Props = {
@ -56,6 +58,7 @@ export function VisitorBreakdownChart({
}: Props) {
const kibana = useKibanaServices();
const LensEmbeddableComponent = kibana.lens.EmbeddableComponent;
const [localDataViewId] = useState<string>(uuid.v4());
const lensAttributes = useMemo(
() =>
@ -64,8 +67,9 @@ export function VisitorBreakdownChart({
urlQuery,
metric,
dataView,
localDataViewId,
}),
[uiFilters, urlQuery, metric, dataView]
[uiFilters, urlQuery, metric, dataView, localDataViewId]
);
const filterHandler = useCallback(
@ -118,7 +122,13 @@ export function getVisitorBreakdownLensAttributes({
urlQuery,
metric,
dataView,
}: LensAttributes): TypedLensByValueInput['attributes'] {
localDataViewId,
}: LensAttributes & {
localDataViewId: string;
}): TypedLensByValueInput['attributes'] {
const localDataView = dataView.toSpec(false);
localDataView.id = localDataViewId;
const dataLayer: PersistedIndexPatternLayer = {
incompleteColumns: {},
columnOrder: ['col1', 'col2'],
@ -160,19 +170,23 @@ export function getVisitorBreakdownLensAttributes({
return {
visualizationType: 'lnsPie',
title: `ux-visitor-breakdown-${metric}`,
references: [
{
id: dataView,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: dataView,
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
],
references: [],
state: {
internalReferences: [
{
id: localDataView.id,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: localDataView.id,
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
],
adHocDataViews: {
[localDataView.id]: localDataView,
},
datasourceStates: {
indexpattern: {
layers: {

View file

@ -57,6 +57,7 @@ export function CsmSharedContextProvider({
if (dataViewTitle) {
return dataViews.create({
title: dataViewTitle,
timeFieldName: '@timestamp',
});
}
}, [dataViewTitle, dataViews]);

View file

@ -8,6 +8,7 @@
import { omit } from 'lodash';
import { useHistory } from 'react-router-dom';
import { fromQuery, toQuery } from '@kbn/observability-plugin/public';
import { useCallback, useMemo } from 'react';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
import { getExcludedName } from '../local_uifilters';
@ -28,55 +29,57 @@ export function useLocalUIFilters({
const history = useHistory();
const { uxUiFilters } = useLegacyUrlParams();
const setFilterValue = (name: UxLocalUIFilterName, value: string[]) => {
const search = omit(toQuery(history.location.search), name);
const setFilterValue = useCallback(
(name: UxLocalUIFilterName, value: string[]) => {
const search = omit(toQuery(history.location.search), name);
history.push({
...history.location,
search: fromQuery(
removeUndefinedProps({
...search,
[name]: value.length ? value.join(',') : undefined,
})
),
});
};
history.push({
...history.location,
search: fromQuery(
removeUndefinedProps({
...search,
[name]: value.length ? value.join(',') : undefined,
})
),
});
},
[history]
);
const invertFilter = (
name: UxLocalUIFilterName,
value: string,
negate: boolean
) => {
if (!negate) {
setFilterValue(
name,
(uxUiFilters?.[name] as string[]).filter((valT) => valT !== value)
);
const invertFilter = useCallback(
(name: UxLocalUIFilterName, value: string, negate: boolean) => {
if (!negate) {
setFilterValue(
name,
(uxUiFilters?.[name] as string[]).filter((valT) => valT !== value)
);
const excludedName = getExcludedName(name);
setFilterValue(excludedName, [
...(uxUiFilters?.[excludedName] ?? []),
value,
]);
} else {
const includeName = name.split('Excluded')[0] as UxLocalUIFilterName;
const excludedName = name;
const excludedName = getExcludedName(name);
setFilterValue(excludedName, [
...(uxUiFilters?.[excludedName] ?? []),
value,
]);
} else {
const includeName = name.split('Excluded')[0] as UxLocalUIFilterName;
const excludedName = name;
setFilterValue(
excludedName,
(uxUiFilters?.[excludedName] as string[]).filter(
(valT) => valT !== value
)
);
setFilterValue(
excludedName,
(uxUiFilters?.[excludedName] as string[]).filter(
(valT) => valT !== value
)
);
setFilterValue(includeName, [
...(uxUiFilters?.[includeName] ?? []),
value,
]);
}
};
setFilterValue(includeName, [
...(uxUiFilters?.[includeName] ?? []),
value,
]);
}
},
[setFilterValue, uxUiFilters]
);
const clearValues = () => {
const clearValues = useCallback(() => {
const search = omit(toQuery(history.location.search), [
...filterNames,
'searchTerm',
@ -87,13 +90,17 @@ export function useLocalUIFilters({
...history.location,
search: fromQuery(search),
});
};
}, [filterNames, history]);
const filters: UxLocalUIFilter[] = filterNames.map((name) => ({
value: (uxUiFilters[name] as string[]) ?? [],
...uxFiltersByName[name],
name,
}));
const filters: UxLocalUIFilter[] = useMemo(
() =>
filterNames.map((name) => ({
value: (uxUiFilters[name] as string[]) ?? [],
...uxFiltersByName[name],
name,
})),
[filterNames, uxUiFilters]
);
return {
filters,

View file

@ -19,9 +19,9 @@ import {
} from '../charts/visitor_breakdown_chart';
import { I18LABELS, VisitorBreakdownLabel } from '../translations';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useStaticDataView } from '../../../../hooks/use_static_data_view';
import { useLocalUIFilters } from '../hooks/use_local_uifilters';
import { getExcludedName } from '../local_uifilters';
import { useDataView } from '../local_uifilters/use_data_view';
type VisitorBreakdownFieldMap = Record<
VisitorBreakdownMetric,
@ -40,20 +40,21 @@ const EuiLoadingEmbeddable = styled(EuiFlexGroup)`
}
`;
const vistorBreakdownFilter = {
filterNames: uxLocalUIFilterNames.filter((name) =>
['browser', 'browserExcluded', 'os', 'osExcluded'].includes(name)
),
};
const getInvertedFilterName = (filter: UxLocalUIFilterName, negate: boolean) =>
negate ? filter : getExcludedName(filter);
export function VisitorBreakdown() {
const { urlParams, uxUiFilters } = useLegacyUrlParams();
const { start, end, searchTerm } = urlParams;
// static dataView is required for lens
const { dataView, loading } = useStaticDataView();
const { dataView } = useDataView();
const { filters, setFilterValue } = useLocalUIFilters({
filterNames: uxLocalUIFilterNames.filter((name) =>
['browser', 'browserExcluded', 'os', 'osExcluded'].includes(name)
),
});
const { filters, setFilterValue } = useLocalUIFilters(vistorBreakdownFilter);
const onFilter = useCallback(
(metric: VisitorBreakdownMetric, event: any) => {
@ -96,7 +97,7 @@ export function VisitorBreakdown() {
<h4>{I18LABELS.browser}</h4>
</EuiTitle>
<EuiSpacer size="s" />
{!!loading ? (
{!dataView?.id ? (
<EuiLoadingEmbeddable
justifyContent="spaceAround"
alignItems={'center'}
@ -107,7 +108,7 @@ export function VisitorBreakdown() {
</EuiLoadingEmbeddable>
) : (
<VisitorBreakdownChart
dataView={dataView?.id ?? ''}
dataView={dataView}
start={start ?? ''}
end={end ?? ''}
uiFilters={uxUiFilters}
@ -122,7 +123,7 @@ export function VisitorBreakdown() {
<h4>{I18LABELS.operatingSystem}</h4>
</EuiTitle>
<EuiSpacer size="s" />
{!!loading ? (
{!dataView?.id ? (
<EuiLoadingEmbeddable
justifyContent="spaceAround"
alignItems={'center'}
@ -133,7 +134,7 @@ export function VisitorBreakdown() {
</EuiLoadingEmbeddable>
) : (
<VisitorBreakdownChart
dataView={dataView?.id ?? ''}
dataView={dataView}
start={start ?? ''}
end={end ?? ''}
uiFilters={uxUiFilters}