mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
2a0a1e6498
commit
7a74bbf3f7
6 changed files with 135 additions and 91 deletions
|
@ -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": "",
|
||||
|
|
|
@ -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) => {},
|
||||
};
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -57,6 +57,7 @@ export function CsmSharedContextProvider({
|
|||
if (dataViewTitle) {
|
||||
return dataViews.create({
|
||||
title: dataViewTitle,
|
||||
timeFieldName: '@timestamp',
|
||||
});
|
||||
}
|
||||
}, [dataViewTitle, dataViews]);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue