mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[UX] Migrate visitor breakdown chart to lens embeddable (#134684)
* Migrate visitor breakdown chart to lens embeddable * Remove visitor breakdown from APM Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a7032ceebc
commit
56512e1d41
14 changed files with 606 additions and 241 deletions
|
@ -1,103 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions';
|
||||
import { mergeProjection } from '../../projections/util/merge_projection';
|
||||
import { SetupUX } from './route';
|
||||
import {
|
||||
USER_AGENT_NAME,
|
||||
USER_AGENT_OS,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export async function getVisitorBreakdown({
|
||||
setup,
|
||||
urlQuery,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
setup: SetupUX;
|
||||
urlQuery?: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const projection = getRumPageLoadTransactionsProjection({
|
||||
setup,
|
||||
urlQuery,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
body: {
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
query: {
|
||||
bool: projection.body.query.bool,
|
||||
},
|
||||
aggs: {
|
||||
browsers: {
|
||||
terms: {
|
||||
field: USER_AGENT_NAME,
|
||||
size: 9,
|
||||
},
|
||||
},
|
||||
os: {
|
||||
terms: {
|
||||
field: USER_AGENT_OS,
|
||||
size: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.search('get_visitor_breakdown', params);
|
||||
const { browsers, os } = response.aggregations!;
|
||||
|
||||
const totalItems = response.hits.total.value;
|
||||
|
||||
const browserTotal = browsers.buckets.reduce(
|
||||
(prevVal, item) => prevVal + item.doc_count,
|
||||
0
|
||||
);
|
||||
|
||||
const osTotal = os.buckets.reduce(
|
||||
(prevVal, item) => prevVal + item.doc_count,
|
||||
0
|
||||
);
|
||||
|
||||
const browserItems = browsers.buckets.map((bucket) => ({
|
||||
count: bucket.doc_count,
|
||||
name: bucket.key as string,
|
||||
}));
|
||||
|
||||
if (totalItems > 0) {
|
||||
browserItems.push({
|
||||
count: totalItems - browserTotal,
|
||||
name: 'Others',
|
||||
});
|
||||
}
|
||||
|
||||
const osItems = os.buckets.map((bucket) => ({
|
||||
count: bucket.doc_count,
|
||||
name: bucket.key as string,
|
||||
}));
|
||||
|
||||
if (totalItems > 0) {
|
||||
osItems.push({
|
||||
count: totalItems - osTotal,
|
||||
name: 'Others',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
os: osItems,
|
||||
browsers: browserItems,
|
||||
};
|
||||
}
|
|
@ -10,7 +10,6 @@ import { setupRequest, Setup } from '../../lib/helpers/setup_request';
|
|||
import { getPageLoadDistribution } from './get_page_load_distribution';
|
||||
import { getPageViewTrends } from './get_page_view_trends';
|
||||
import { getPageLoadDistBreakdown } from './get_pl_dist_breakdown';
|
||||
import { getVisitorBreakdown } from './get_visitor_breakdown';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { rangeRt } from '../default_api_types';
|
||||
import { APMRouteHandlerResources } from '../typings';
|
||||
|
@ -152,33 +151,6 @@ const rumPageViewsTrendRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const rumVisitorsBreakdownRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/ux/visitor-breakdown',
|
||||
params: t.type({
|
||||
query: uxQueryRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
os: Array<{ count: number; name: string }>;
|
||||
browsers: Array<{ count: number; name: string }>;
|
||||
}> => {
|
||||
const setup = await setupUXRequest(resources);
|
||||
|
||||
const {
|
||||
query: { urlQuery, start, end },
|
||||
} = resources.params;
|
||||
|
||||
return getVisitorBreakdown({
|
||||
setup,
|
||||
urlQuery,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function decodeUiFilters(
|
||||
logger: Logger,
|
||||
uiFiltersEncoded?: string
|
||||
|
@ -212,5 +184,4 @@ export const rumRouteRepository = {
|
|||
...rumPageLoadDistributionRoute,
|
||||
...rumPageLoadDistBreakdownRoute,
|
||||
...rumPageViewsTrendRoute,
|
||||
...rumVisitorsBreakdownRoute,
|
||||
};
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from './url_ux_query.journey';
|
|||
export * from './ux_js_errors.journey';
|
||||
export * from './ux_client_metrics.journey';
|
||||
export * from './ux_long_task_metric_journey';
|
||||
export * from './ux_visitor_breakdown.journey';
|
||||
|
|
|
@ -75,3 +75,7 @@ export const getQuerystring = (params: object) => {
|
|||
|
||||
export const delay = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const byLensTestId = (id: string) => `[data-test-embeddable-id="${id}"]`;
|
||||
export const byLensDataLayerId = (id: string) =>
|
||||
`[data-ech-series-name="${id}"]`;
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { journey, step, before } from '@elastic/synthetics';
|
||||
import { UXDashboardDatePicker } from '../page_objects/date_picker';
|
||||
import { byLensTestId, loginToKibana, waitForLoadingToFinish } from './utils';
|
||||
|
||||
const osNameMetric = 'ux-visitor-breakdown-user_agent-os-name';
|
||||
const uaNameMetric = 'ux-visitor-breakdown-user_agent-name';
|
||||
|
||||
const chartIds = [osNameMetric, uaNameMetric];
|
||||
|
||||
journey('UX Visitor Breakdown', async ({ page, params }) => {
|
||||
before(async () => {
|
||||
await waitForLoadingToFinish({ page });
|
||||
});
|
||||
|
||||
const queryParams = {
|
||||
percentile: '50',
|
||||
rangeFrom: '2020-05-18T11:51:00.000Z',
|
||||
rangeTo: '2021-10-30T06:37:15.536Z',
|
||||
};
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
|
||||
const baseUrl = `${params.kibanaUrl}/app/ux`;
|
||||
|
||||
step('Go to UX Dashboard', async () => {
|
||||
await page.goto(`${baseUrl}?${queryString}`, {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
await loginToKibana({
|
||||
page,
|
||||
user: { username: 'elastic', password: 'changeme' },
|
||||
});
|
||||
});
|
||||
|
||||
step('Set date range', async () => {
|
||||
const datePickerPage = new UXDashboardDatePicker(page);
|
||||
await datePickerPage.setDefaultE2eRange();
|
||||
});
|
||||
|
||||
step('Confirm charts are visible', async () => {
|
||||
// Wait until chart data is loaded
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await Promise.all(
|
||||
chartIds.map(
|
||||
async (dataTestId) =>
|
||||
// lens embeddable injects its own test attribute
|
||||
await page.waitForSelector(byLensTestId(dataTestId))
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -21,7 +21,8 @@
|
|||
"alerts",
|
||||
"observability",
|
||||
"security",
|
||||
"maps"
|
||||
"maps",
|
||||
"lens"
|
||||
],
|
||||
"server": false,
|
||||
"ui": true,
|
||||
|
|
|
@ -106,7 +106,15 @@ export function UXAppRoot({
|
|||
appMountParameters,
|
||||
core,
|
||||
deps,
|
||||
corePlugins: { embeddable, inspector, maps, observability, data, dataViews },
|
||||
corePlugins: {
|
||||
embeddable,
|
||||
inspector,
|
||||
maps,
|
||||
observability,
|
||||
data,
|
||||
dataViews,
|
||||
lens,
|
||||
},
|
||||
}: {
|
||||
appMountParameters: AppMountParameters;
|
||||
core: CoreStart;
|
||||
|
@ -131,6 +139,7 @@ export function UXAppRoot({
|
|||
embeddable,
|
||||
data,
|
||||
dataViews,
|
||||
lens,
|
||||
}}
|
||||
>
|
||||
<i18nCore.Context>
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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",
|
||||
},
|
||||
],
|
||||
"state": Object {
|
||||
"datasourceStates": Object {
|
||||
"indexpattern": Object {
|
||||
"layers": Object {
|
||||
"layer1": Object {
|
||||
"columnOrder": Array [
|
||||
"col1",
|
||||
"col2",
|
||||
],
|
||||
"columns": Object {
|
||||
"col1": Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": true,
|
||||
"label": "Top 9 values of user_agent.os.name",
|
||||
"operationType": "terms",
|
||||
"params": Object {
|
||||
"orderBy": Object {
|
||||
"columnId": "col2",
|
||||
"type": "column",
|
||||
},
|
||||
"orderDirection": "desc",
|
||||
"otherBucket": true,
|
||||
"parentFormat": Object {
|
||||
"id": "terms",
|
||||
},
|
||||
"size": 9,
|
||||
},
|
||||
"scale": "ordinal",
|
||||
"sourceField": "user_agent.os.name",
|
||||
},
|
||||
"col2": Object {
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Count of records",
|
||||
"operationType": "count",
|
||||
"params": Object {
|
||||
"emptyAsNull": true,
|
||||
},
|
||||
"scale": "ratio",
|
||||
"sourceField": "___records___",
|
||||
},
|
||||
},
|
||||
"incompleteColumns": Object {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"filters": Array [
|
||||
Object {
|
||||
"meta": Object {},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"transaction.type": "page-load",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"terms": Object {
|
||||
"processor.event": Array [
|
||||
"transaction",
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.marks.navigationTiming.fetchStart",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"wildcard": Object {
|
||||
"url.full": "*elastic.co*",
|
||||
},
|
||||
},
|
||||
],
|
||||
"must_not": Array [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"query": Object {
|
||||
"language": "kuery",
|
||||
"query": "",
|
||||
},
|
||||
"visualization": Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"categoryDisplay": "default",
|
||||
"groups": Array [
|
||||
"col1",
|
||||
],
|
||||
"layerId": "layer1",
|
||||
"layerType": "data",
|
||||
"legendDisplay": "hide",
|
||||
"metric": "col2",
|
||||
"nestedLegend": false,
|
||||
"numberDisplay": "percent",
|
||||
"showValuesInLegend": true,
|
||||
},
|
||||
],
|
||||
"shape": "pie",
|
||||
},
|
||||
},
|
||||
"title": "ux-visitor-breakdown-user_agent.os.name",
|
||||
"visualizationType": "lnsPie",
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
getVisitorBreakdownLensAttributes,
|
||||
VisitorBreakdownChart,
|
||||
VisitorBreakdownMetric,
|
||||
} from './visitor_breakdown_chart';
|
||||
import { useKibanaServices } from '../../../../hooks/use_kibana_services';
|
||||
|
||||
jest.mock('../../../../hooks/use_kibana_services');
|
||||
|
||||
describe('VisitorBreakdownChart', () => {
|
||||
describe('getVisitorBreakdownLensAttributes', () => {
|
||||
test('generates expected lens attributes', () => {
|
||||
const props = {
|
||||
metric: VisitorBreakdownMetric.OS_BREAKDOWN,
|
||||
uiFilters: {
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
},
|
||||
urlQuery: 'elastic.co',
|
||||
dataView: 'Required',
|
||||
};
|
||||
|
||||
expect(getVisitorBreakdownLensAttributes(props)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
const mockEmbeddableComponent = jest.fn((_) => <></>);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useKibanaServices as jest.Mock).mockReturnValue({
|
||||
lens: {
|
||||
EmbeddableComponent: mockEmbeddableComponent,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('calls lens with original attributes', () => {
|
||||
const props = {
|
||||
start: '0',
|
||||
end: '5000',
|
||||
metric: VisitorBreakdownMetric.OS_BREAKDOWN,
|
||||
uiFilters: {
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
},
|
||||
urlQuery: 'elastic.co',
|
||||
dataView: 'Required',
|
||||
onFilter: (_m: VisitorBreakdownMetric, _e: any) => {},
|
||||
};
|
||||
|
||||
const { container: _ } = render(<VisitorBreakdownChart {...props} />);
|
||||
|
||||
expect(mockEmbeddableComponent).toHaveBeenCalledTimes(1);
|
||||
expect(mockEmbeddableComponent.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
timeRange: {
|
||||
from: props.start,
|
||||
to: props.end,
|
||||
},
|
||||
attributes: getVisitorBreakdownLensAttributes(props),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,90 +5,216 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
Chart,
|
||||
DARK_THEME,
|
||||
Datum,
|
||||
LIGHT_THEME,
|
||||
PartialTheme,
|
||||
Partition,
|
||||
PartitionLayout,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import styled from 'styled-components';
|
||||
CountIndexPatternColumn,
|
||||
PersistedIndexPatternLayer,
|
||||
PieVisualizationState,
|
||||
TermsIndexPatternColumn,
|
||||
TypedLensByValueInput,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { TRANSACTION_PAGE_LOAD } from '../../../../../common/transaction_types';
|
||||
import {
|
||||
EUI_CHARTS_THEME_DARK,
|
||||
EUI_CHARTS_THEME_LIGHT,
|
||||
} from '@elastic/eui/dist/eui_charts_theme';
|
||||
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
|
||||
import { ChartWrapper } from '../chart_wrapper';
|
||||
import { I18LABELS } from '../translations';
|
||||
PROCESSOR_EVENT,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { getEsFilter } from '../../../../services/data/get_es_filter';
|
||||
import { useKibanaServices } from '../../../../hooks/use_kibana_services';
|
||||
import type { UxUIFilters } from '../../../../../typings/ui_filters';
|
||||
import { ProcessorEvent } from '../../../../../common/processor_event';
|
||||
|
||||
const StyleChart = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
const BUCKET_SIZE = 9;
|
||||
|
||||
interface Props {
|
||||
options?: Array<{
|
||||
count: number;
|
||||
name: string;
|
||||
}>;
|
||||
loading: boolean;
|
||||
export enum VisitorBreakdownMetric {
|
||||
OS_BREAKDOWN = 'user_agent.os.name',
|
||||
UA_BREAKDOWN = 'user_agent.name',
|
||||
}
|
||||
|
||||
const theme: PartialTheme = {
|
||||
chartMargins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
legend: {
|
||||
verticalWidth: 100,
|
||||
},
|
||||
partition: {
|
||||
linkLabel: { maximumSection: Infinity, maxCount: 0 },
|
||||
outerSizeRatio: 1, // - 0.5 * Math.random(),
|
||||
circlePadding: 4,
|
||||
},
|
||||
};
|
||||
interface LensAttributes {
|
||||
metric: VisitorBreakdownMetric;
|
||||
uiFilters: UxUIFilters;
|
||||
urlQuery?: string;
|
||||
dataView: string;
|
||||
}
|
||||
|
||||
export function VisitorBreakdownChart({ loading, options }: Props) {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
type Props = {
|
||||
start: string;
|
||||
end: string;
|
||||
onFilter: (metric: VisitorBreakdownMetric, event: any) => void;
|
||||
} & LensAttributes;
|
||||
|
||||
const euiChartTheme = darkMode
|
||||
? EUI_CHARTS_THEME_DARK
|
||||
: EUI_CHARTS_THEME_LIGHT;
|
||||
export function VisitorBreakdownChart({
|
||||
start,
|
||||
end,
|
||||
onFilter,
|
||||
uiFilters,
|
||||
urlQuery,
|
||||
metric,
|
||||
dataView,
|
||||
}: Props) {
|
||||
const kibana = useKibanaServices();
|
||||
const LensEmbeddableComponent = kibana.lens.EmbeddableComponent;
|
||||
|
||||
const lensAttributes = useMemo(
|
||||
() =>
|
||||
getVisitorBreakdownLensAttributes({
|
||||
uiFilters,
|
||||
urlQuery,
|
||||
metric,
|
||||
dataView,
|
||||
}),
|
||||
[uiFilters, urlQuery, metric, dataView]
|
||||
);
|
||||
|
||||
const filterHandler = useCallback(
|
||||
(event) => {
|
||||
onFilter(metric, event);
|
||||
},
|
||||
[onFilter, metric]
|
||||
);
|
||||
|
||||
if (!LensEmbeddableComponent) {
|
||||
return <EuiText>No lens component</EuiText>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartWrapper loading={loading} height="245px" maxWidth="430px">
|
||||
<StyleChart>
|
||||
<Chart>
|
||||
<Settings
|
||||
showLegend
|
||||
baseTheme={darkMode ? DARK_THEME : LIGHT_THEME}
|
||||
theme={theme}
|
||||
/>
|
||||
<Partition
|
||||
id="spec_1"
|
||||
data={
|
||||
options?.length ? options : [{ count: 1, name: I18LABELS.noData }]
|
||||
}
|
||||
layout={PartitionLayout.sunburst}
|
||||
clockwiseSectors={false}
|
||||
valueAccessor={(d: Datum) => d.count as number}
|
||||
valueGetter="percent"
|
||||
percentFormatter={(d: number) =>
|
||||
`${Math.round((d + Number.EPSILON) * 100) / 100}%`
|
||||
}
|
||||
layers={[
|
||||
{
|
||||
groupByRollup: (d: Datum) => d.name,
|
||||
shape: {
|
||||
fillColor: (d) =>
|
||||
euiChartTheme.theme.colors?.vizColors?.[d.sortIndex]!,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Chart>
|
||||
</StyleChart>
|
||||
</ChartWrapper>
|
||||
<LensEmbeddableComponent
|
||||
id={`ux-visitor-breakdown-${metric.replaceAll('.', '-')}`}
|
||||
hidePanelTitles
|
||||
withDefaultActions
|
||||
style={{ minHeight: '250px', height: '100%' }}
|
||||
attributes={lensAttributes}
|
||||
timeRange={{
|
||||
from: start ?? '',
|
||||
to: end ?? '',
|
||||
}}
|
||||
viewMode={ViewMode.VIEW}
|
||||
onFilter={filterHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const visConfig: PieVisualizationState = {
|
||||
layers: [
|
||||
{
|
||||
layerId: 'layer1',
|
||||
groups: ['col1'],
|
||||
metric: 'col2',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'hide',
|
||||
numberDisplay: 'percent',
|
||||
showValuesInLegend: true,
|
||||
nestedLegend: false,
|
||||
layerType: 'data',
|
||||
},
|
||||
],
|
||||
shape: 'pie',
|
||||
};
|
||||
|
||||
export function getVisitorBreakdownLensAttributes({
|
||||
uiFilters,
|
||||
urlQuery,
|
||||
metric,
|
||||
dataView,
|
||||
}: LensAttributes): TypedLensByValueInput['attributes'] {
|
||||
const dataLayer: PersistedIndexPatternLayer = {
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: `Top ${BUCKET_SIZE} values of ${metric}`,
|
||||
dataType: 'string',
|
||||
operationType: 'terms',
|
||||
scale: 'ordinal',
|
||||
sourceField: metric,
|
||||
isBucketed: true,
|
||||
params: {
|
||||
size: BUCKET_SIZE,
|
||||
orderBy: {
|
||||
type: 'column',
|
||||
columnId: 'col2',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
parentFormat: {
|
||||
id: 'terms',
|
||||
},
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
col2: {
|
||||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
operationType: 'count',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: '___records___',
|
||||
params: {
|
||||
emptyAsNull: true,
|
||||
},
|
||||
} as CountIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
layer1: dataLayer,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
meta: {},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } },
|
||||
{
|
||||
terms: {
|
||||
[PROCESSOR_EVENT]: [ProcessorEvent.transaction],
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: 'transaction.marks.navigationTiming.fetchStart',
|
||||
},
|
||||
},
|
||||
...getEsFilter(uiFilters),
|
||||
...(urlQuery
|
||||
? [
|
||||
{
|
||||
wildcard: {
|
||||
'url.full': `*${urlQuery}*`,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
must_not: [...getEsFilter(uiFilters, true)],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: visConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,39 +5,83 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
|
||||
import { VisitorBreakdownChart } from '../charts/visitor_breakdown_chart';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
UxLocalUIFilterName,
|
||||
uxLocalUIFilterNames,
|
||||
} from '../../../../../common/ux_ui_filter';
|
||||
import {
|
||||
VisitorBreakdownChart,
|
||||
VisitorBreakdownMetric,
|
||||
} from '../charts/visitor_breakdown_chart';
|
||||
import { I18LABELS, VisitorBreakdownLabel } from '../translations';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
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';
|
||||
|
||||
type VisitorBreakdownFieldMap = Record<
|
||||
VisitorBreakdownMetric,
|
||||
UxLocalUIFilterName
|
||||
>;
|
||||
|
||||
const visitorBreakdownFieldMap: VisitorBreakdownFieldMap = {
|
||||
[VisitorBreakdownMetric.OS_BREAKDOWN]: 'os',
|
||||
[VisitorBreakdownMetric.UA_BREAKDOWN]: 'browser',
|
||||
};
|
||||
|
||||
const EuiLoadingEmbeddable = styled(EuiFlexGroup)`
|
||||
& {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const getInvertedFilterName = (filter: UxLocalUIFilterName, negate: boolean) =>
|
||||
negate ? filter : getExcludedName(filter);
|
||||
|
||||
export function VisitorBreakdown() {
|
||||
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
|
||||
|
||||
const { urlParams, uxUiFilters } = useLegacyUrlParams();
|
||||
const { start, end, searchTerm } = urlParams;
|
||||
// static dataView is required for lens
|
||||
const { dataView, loading } = useStaticDataView();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
const { serviceName } = uxUiFilters;
|
||||
const { filters, setFilterValue } = useLocalUIFilters({
|
||||
filterNames: uxLocalUIFilterNames.filter((name) =>
|
||||
['browser', 'browserExcluded', 'os', 'osExcluded'].includes(name)
|
||||
),
|
||||
});
|
||||
|
||||
if (start && end && serviceName) {
|
||||
return callApmApi('GET /internal/apm/ux/visitor-breakdown', {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uxUiFilters),
|
||||
urlQuery: searchTerm,
|
||||
},
|
||||
},
|
||||
});
|
||||
const onFilter = useCallback(
|
||||
(metric: VisitorBreakdownMetric, event: any) => {
|
||||
if (!visitorBreakdownFieldMap[metric]) {
|
||||
return;
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
|
||||
const filterValues = event?.data?.map((fdata: any) => fdata.value);
|
||||
const invertedField = getInvertedFilterName(
|
||||
visitorBreakdownFieldMap[metric],
|
||||
event?.negate ?? false
|
||||
);
|
||||
const invertedFieldValues =
|
||||
filters?.find((filter) => filter.name === invertedField)?.value ?? [];
|
||||
|
||||
setFilterValue(
|
||||
invertedField,
|
||||
invertedFieldValues.filter((value) => !filterValues.includes(value))
|
||||
);
|
||||
|
||||
setFilterValue(
|
||||
event?.negate
|
||||
? getExcludedName(visitorBreakdownFieldMap[metric])
|
||||
: visitorBreakdownFieldMap[metric],
|
||||
filterValues
|
||||
);
|
||||
},
|
||||
// `rangeId` acts as a cache buster for stable ranges like "Today"
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[end, start, uxUiFilters, searchTerm, rangeId]
|
||||
[filters, setFilterValue]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -52,20 +96,52 @@ export function VisitorBreakdown() {
|
|||
<h4>{I18LABELS.browser}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<VisitorBreakdownChart
|
||||
options={data?.browsers}
|
||||
loading={status !== 'success'}
|
||||
/>
|
||||
{!!loading ? (
|
||||
<EuiLoadingEmbeddable
|
||||
justifyContent="spaceAround"
|
||||
alignItems={'center'}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingChart size="l" mono />
|
||||
</EuiFlexItem>
|
||||
</EuiLoadingEmbeddable>
|
||||
) : (
|
||||
<VisitorBreakdownChart
|
||||
dataView={dataView?.id ?? ''}
|
||||
start={start ?? ''}
|
||||
end={end ?? ''}
|
||||
uiFilters={uxUiFilters}
|
||||
urlQuery={searchTerm}
|
||||
metric={VisitorBreakdownMetric.UA_BREAKDOWN}
|
||||
onFilter={onFilter}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{I18LABELS.operatingSystem}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<VisitorBreakdownChart
|
||||
options={data?.os}
|
||||
loading={status !== 'success'}
|
||||
/>
|
||||
{!!loading ? (
|
||||
<EuiLoadingEmbeddable
|
||||
justifyContent="spaceAround"
|
||||
alignItems={'center'}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingChart size="l" mono />
|
||||
</EuiFlexItem>
|
||||
</EuiLoadingEmbeddable>
|
||||
) : (
|
||||
<VisitorBreakdownChart
|
||||
dataView={dataView?.id ?? ''}
|
||||
start={start ?? ''}
|
||||
end={end ?? ''}
|
||||
uiFilters={uxUiFilters}
|
||||
urlQuery={searchTerm}
|
||||
metric={VisitorBreakdownMetric.OS_BREAKDOWN}
|
||||
onFilter={onFilter}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
|
|
22
x-pack/plugins/ux/public/hooks/use_static_data_view.ts
Normal file
22
x-pack/plugins/ux/public/hooks/use_static_data_view.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useKibanaServices } from './use_kibana_services';
|
||||
|
||||
export function useStaticDataView() {
|
||||
const { observability } = useKibanaServices();
|
||||
const { data, loading } = useFetcher(async () => {
|
||||
return observability.getAppDataView('ux');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dataView: data ?? undefined,
|
||||
loading,
|
||||
};
|
||||
}
|
|
@ -33,6 +33,7 @@ import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
|||
import { MapsStartApi } from '@kbn/maps-plugin/public';
|
||||
import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
|
||||
export type UxPluginSetup = void;
|
||||
export type UxPluginStart = void;
|
||||
|
@ -54,6 +55,7 @@ export interface ApmPluginStartDeps {
|
|||
inspector: InspectorPluginStart;
|
||||
observability: ObservabilityPublicStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
lens: LensPublicStart;
|
||||
}
|
||||
|
||||
async function getDataStartPlugin(core: CoreSetup) {
|
||||
|
@ -66,7 +68,6 @@ export class UxPlugin implements Plugin<UxPluginSetup, UxPluginStart> {
|
|||
|
||||
public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) {
|
||||
const pluginSetupDeps = plugins;
|
||||
|
||||
if (plugins.observability) {
|
||||
const getUxDataHelper = async () => {
|
||||
const { fetchUxOverviewDate, hasRumData, createCallApmApi } =
|
||||
|
|
|
@ -27,5 +27,6 @@
|
|||
{ "path": "../licensing/tsconfig.json" },
|
||||
{ "path": "../maps/tsconfig.json" },
|
||||
{ "path": "../observability/tsconfig.json" },
|
||||
{ "path": "../lens/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue