Skip to content

Commit

Permalink
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
Browse files Browse the repository at this point in the history
…range
  • Loading branch information
XavierM committed Dec 12, 2022
2 parents fa414eb + b80a23e commit 12464f2
Show file tree
Hide file tree
Showing 30 changed files with 513 additions and 91 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,8 @@
"@opentelemetry/semantic-conventions": "^1.4.0",
"@reduxjs/toolkit": "1.7.2",
"@slack/webhook": "^5.0.4",
"@tanstack/react-query": "^4.19.0",
"@tanstack/react-query-devtools": "^4.19.0",
"@tanstack/react-query": "^4.19.1",
"@tanstack/react-query-devtools": "^4.19.1",
"@turf/along": "6.0.1",
"@turf/area": "6.0.1",
"@turf/bbox": "6.0.1",
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/enterprise_search/common/types/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export interface AnalyticsCollection {
}

export type AnalyticsCollectionDocument = Omit<AnalyticsCollection, 'id'>;

export interface AnalyticsEventsIndexExists {
exists: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { mockHttpValues } from '../../../__mocks__/kea_logic';

import { nextTick } from '@kbn/test-jest-helpers';

import { checkAnalyticsEventsIndexExists } from './check_analytics_events_index_api_logic';

describe('FetchAnalyticsCollectionApiLogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});

describe('FetchAnalyticsCollectionsApiLogic', () => {
it('calls the analytics collections exists api', async () => {
const promise = Promise.resolve({ exists: true });
const indexName = 'eventsIndex';
http.get.mockReturnValue(promise);
const result = checkAnalyticsEventsIndexExists({ indexName });
await nextTick();
expect(http.get).toHaveBeenCalledWith(
`/internal/enterprise_search/analytics/events/${indexName}/exists`
);
await expect(result).resolves.toEqual({ exists: true });
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { AnalyticsEventsIndexExists } from '../../../../../common/types/analytics';
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';

export interface AnalyticsEventsIndexExistsApiLogicArgs {
indexName: string;
}

export type AnalyticsEventsIndexExistsApiLogicResponse = AnalyticsEventsIndexExists;

export const checkAnalyticsEventsIndexExists = async ({
indexName,
}: AnalyticsEventsIndexExistsApiLogicArgs): Promise<AnalyticsEventsIndexExistsApiLogicResponse> => {
const { http } = HttpLogic.values;
const route = `/internal/enterprise_search/analytics/events/${indexName}/exists`;
const response = await http.get<AnalyticsEventsIndexExists>(route);

return response;
};

export const AnalyticsEventsIndexExistsAPILogic = createApiLogic(
['analytics', 'analytics_events_index_exists_api_logic'],
checkAnalyticsEventsIndexExists
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,67 @@

import '../../../__mocks__/shallow_useeffect.mock';

import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';

import React from 'react';

import { shallow } from 'enzyme';

import { EuiEmptyPrompt } from '@elastic/eui';

import { AnalyticsCollection } from '../../../../../common/types/analytics';
import { EntSearchLogStream } from '../../../shared/log_stream';

import { AnalyticsCollectionEvents } from './analytics_collection_events';

describe('AnalyticsCollectionEvents', () => {
const analyticsCollections: AnalyticsCollection = {
const analyticsCollection: AnalyticsCollection = {
event_retention_day_length: 180,
events_datastream: 'logs-elastic_analytics.events-example',
id: '1',
name: 'example',
};

const mockActions = {
analyticsEventsIndexExists: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();

setMockActions(mockActions);
});

it('renders', () => {
setMockValues({
isPresent: true,
isLoading: false,
});
const expectedQuery = '_index: logs-elastic_analytics.events-example';

const wrapper = shallow(<AnalyticsCollectionEvents collection={analyticsCollections} />);
const wrapper = shallow(<AnalyticsCollectionEvents collection={analyticsCollection} />);
expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual(expectedQuery);
});

describe('empty state', () => {
it('renders when analytics events index is not present', () => {
setMockValues({
isPresent: false,
});

const wrapper = shallow(<AnalyticsCollectionEvents collection={analyticsCollection} />);

expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});

it('renders when analytics events index check is not performed yet', () => {
setMockValues({
isLoading: true,
});

const wrapper = shallow(<AnalyticsCollectionEvents collection={analyticsCollection} />);

expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@
* 2.0.
*/

import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';

import { useValues, useActions } from 'kea';

import { EuiEmptyPrompt, EuiButton, EuiLink, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';

import { ENTERPRISE_SEARCH_ANALYTICS_LOGS_SOURCE_ID } from '../../../../../common/constants';
import { AnalyticsCollection } from '../../../../../common/types/analytics';
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { KibanaLogic } from '../../../shared/kibana';

import { EntSearchLogStream } from '../../../shared/log_stream';
import { COLLECTION_VIEW_PATH } from '../../routes';

import { AnalyticsEventsIndexExistsLogic } from './analytics_events_index_exists_logic';

interface AnalyticsCollectionEventsProps {
collection: AnalyticsCollection;
Expand All @@ -23,53 +32,116 @@ const EVENTS_POLLING_INTERVAL = 30 * 1000;
export const AnalyticsCollectionEvents: React.FC<AnalyticsCollectionEventsProps> = ({
collection,
}) => {
// Since EntSearchLogStream component doesn't have a poll interval property
// but gets reloaded on every filter or query change, it was decided to introduce
// a mutable filters state and reset it every 30 seconds to trigger polling

const [filters, setFilters] = useState([]);
const { analyticsEventsIndexExists } = useActions(AnalyticsEventsIndexExistsLogic);
const { isLoading, isPresent } = useValues(AnalyticsEventsIndexExistsLogic);
const { navigateToUrl } = useValues(KibanaLogic);

useEffect(() => {
analyticsEventsIndexExists(collection.id);

const interval = setInterval(() => {
setFilters([]);
analyticsEventsIndexExists(collection.id);
}, EVENTS_POLLING_INTERVAL);

return () => clearInterval(interval);
}, []);

return (
<EntSearchLogStream
logView={{
type: 'log-view-reference',
logViewId: ENTERPRISE_SEARCH_ANALYTICS_LOGS_SOURCE_ID,
}}
columns={[
{
type: 'timestamp',
},
{
type: 'field',
header: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.eventName',
<>
{(isLoading || !isPresent) && (
<EuiEmptyPrompt
iconType="visLine"
title={
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.title"
defaultMessage="{title}"
values={{
title: (
<>
There are no analytics events for <strong>{collection.name}</strong> yet
</>
),
}}
/>
</h2>
</EuiTitle>
}
body={i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.body',
{
defaultMessage: 'Event name',
defaultMessage:
"Start tracking events by adding the behavioral analytics client to every page of your website or application that you'd like to track",
}
),
field: 'event.action',
},
{
type: 'field',
header: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid',
)}
actions={
<EuiButton
color="primary"
fill
onClick={() =>
navigateToUrl(
generateEncodedPath(COLLECTION_VIEW_PATH, {
id: collection.id,
section: 'integrate',
})
)
}
>
{i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.actions',
{
defaultMessage: 'View integration instructions',
}
)}
</EuiButton>
}
footer={
<EuiLink href="#" target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.footer',
{
defaultMessage: 'Visit the behavioral analytics documentation',
}
)}
</EuiLink>
}
/>
)}
{!isLoading && isPresent && (
<EntSearchLogStream
logView={{
type: 'log-view-reference',
logViewId: ENTERPRISE_SEARCH_ANALYTICS_LOGS_SOURCE_ID,
}}
columns={[
{
defaultMessage: 'User UUID',
}
),
field: 'labels.user_uuid',
},
]}
query={`_index: ${collection.events_datastream}`}
filters={filters}
/>
type: 'timestamp',
},
{
type: 'field',
header: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.eventName',
{
defaultMessage: 'Event name',
}
),
field: 'event.action',
},
{
type: 'field',
header: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid',
{
defaultMessage: 'User UUID',
}
),
field: 'labels.user_uuid',
},
]}
query={`_index: ${collection.events_datastream}`}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { LogicMounter } from '../../../__mocks__/kea_logic';

import { Status } from '../../../../../common/types/api';

import { AnalyticsEventsIndexExistsLogic } from './analytics_events_index_exists_logic';

describe('analyticsEventsIndexExistsLogic', () => {
const { mount } = new LogicMounter(AnalyticsEventsIndexExistsLogic);
const indexName = true;

beforeEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
mount();
});

const DEFAULT_VALUES = {
data: undefined,
isLoading: true,
isPresent: false,
status: Status.IDLE,
};

it('has expected default values', () => {
expect(AnalyticsEventsIndexExistsLogic.values).toEqual(DEFAULT_VALUES);
});

describe('selectors', () => {
it('updates when apiSuccess listener triggered', () => {
AnalyticsEventsIndexExistsLogic.actions.apiSuccess({ exists: indexName });

expect(AnalyticsEventsIndexExistsLogic.values).toEqual({
...DEFAULT_VALUES,
isLoading: false,
isPresent: true,
status: Status.SUCCESS,
data: { exists: indexName },
});
});
});
});
Loading

0 comments on commit 12464f2

Please sign in to comment.