Skip to content

Commit

Permalink
[App Search] Analytics - add reusable Layout & Header + basic subrout…
Browse files Browse the repository at this point in the history
…e view components (#88552)

* Add AnalyticsUnavailable component

* Add AnalyticsHeader component

+ update AnalyticsLogic to store allTags prop passed by API

+ convertTagsToSelectOptions util helper

* Add AnalyticsLayout that all subroutes will utilize

- Handles shared concerns of:
  - Loading state & unavailable state
  - Flash messages & log retention callout
  - Reusing the header component
  - Fetching data
  - Reloading data based on filter updates

* Add very basic subroute views that utilize AnalyticsLayout

* Update QueryDetail pages to set breadcrumbs

* Fix QueryDetail breadcrumbs to not 404 on the 'Query' crumb

Redirect to the /analytics overview since we don't actually have a /query_details overview

* [PR feedback] Enforce date range defaults on the client side

- instead of using coincidence to sync our client side default range & server default range

- tags remain ''/undefined as default

* [PR feedback] Add explicit query vs analytics view prop

- to help devs more quickly distinguish at a glance whether a view will fetch analytics data or query data

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
Constance and kibanamachine authored Jan 19, 2021
1 parent 6d1c010 commit 3168b7d
Show file tree
Hide file tree
Showing 30 changed files with 913 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 '../../../__mocks__/shallow_useeffect.mock';
import '../../../__mocks__/react_router_history.mock';
import { mockKibanaValues, setMockValues, setMockActions, rerender } from '../../../__mocks__';

import React from 'react';
import { useParams } from 'react-router-dom';
import { shallow } from 'enzyme';

import { Loading } from '../../../shared/loading';
import { FlashMessages } from '../../../shared/flash_messages';
import { LogRetentionCallout } from '../log_retention';
import { AnalyticsHeader, AnalyticsUnavailable } from './components';

import { AnalyticsLayout } from './analytics_layout';

describe('AnalyticsLayout', () => {
const { history } = mockKibanaValues;

const values = {
history,
dataLoading: false,
analyticsUnavailable: false,
};
const actions = {
loadAnalyticsData: jest.fn(),
loadQueryData: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
history.location.search = '';
setMockValues(values);
setMockActions(actions);
});

it('renders', () => {
const wrapper = shallow(
<AnalyticsLayout title="Hello">
<div data-test-subj="world">World!</div>
</AnalyticsLayout>
);

expect(wrapper.find(FlashMessages)).toHaveLength(1);
expect(wrapper.find(LogRetentionCallout)).toHaveLength(1);

expect(wrapper.find(AnalyticsHeader).prop('title')).toEqual('Hello');
expect(wrapper.find('[data-test-subj="world"]').text()).toEqual('World!');
});

it('renders a loading component if data is not done loading', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow(<AnalyticsLayout title="" />);

expect(wrapper.type()).toEqual(Loading);
});

it('renders an unavailable component if analytics are unavailable', () => {
setMockValues({ ...values, analyticsUnavailable: true });
const wrapper = shallow(<AnalyticsLayout title="" />);

expect(wrapper.type()).toEqual(AnalyticsUnavailable);
});

describe('data loading', () => {
it('loads query data for query details pages', () => {
(useParams as jest.Mock).mockReturnValueOnce({ query: 'test' });
shallow(<AnalyticsLayout isQueryView title="" />);

expect(actions.loadQueryData).toHaveBeenCalledWith('test');
});

it('loads analytics data for non query details pages', () => {
shallow(<AnalyticsLayout isAnalyticsView title="" />);

expect(actions.loadAnalyticsData).toHaveBeenCalled();
});

it('reloads data when search params are updated (by our AnalyticsHeader filters)', () => {
const wrapper = shallow(<AnalyticsLayout isAnalyticsView title="" />);
expect(actions.loadAnalyticsData).toHaveBeenCalledTimes(1);

history.location.search = '?tag=some-filter';
rerender(wrapper);

expect(actions.loadAnalyticsData).toHaveBeenCalledTimes(2);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useValues, useActions } from 'kea';

import { KibanaLogic } from '../../../shared/kibana';
import { FlashMessages } from '../../../shared/flash_messages';
import { Loading } from '../../../shared/loading';

import { LogRetentionCallout, LogRetentionOptions } from '../log_retention';

import { AnalyticsLogic } from './';
import { AnalyticsHeader, AnalyticsUnavailable } from './components';

interface Props {
title: string;
isQueryView?: boolean;
isAnalyticsView?: boolean;
}
export const AnalyticsLayout: React.FC<Props> = ({
title,
isQueryView,
isAnalyticsView,
children,
}) => {
const { history } = useValues(KibanaLogic);
const { query } = useParams() as { query: string };
const { dataLoading, analyticsUnavailable } = useValues(AnalyticsLogic);
const { loadAnalyticsData, loadQueryData } = useActions(AnalyticsLogic);

useEffect(() => {
if (isQueryView) loadQueryData(query);
if (isAnalyticsView) loadAnalyticsData();
}, [history.location.search]);

if (dataLoading) return <Loading />;
if (analyticsUnavailable) return <AnalyticsUnavailable />;

return (
<>
<AnalyticsHeader title={title} />
<FlashMessages />
<LogRetentionCallout type={LogRetentionOptions.Analytics} />
{children}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jest.mock('../engine', () => ({
EngineLogic: { values: { engineName: 'test-engine' } },
}));

import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants';
import { AnalyticsLogic } from './';

describe('AnalyticsLogic', () => {
Expand All @@ -27,6 +28,7 @@ describe('AnalyticsLogic', () => {
const DEFAULT_VALUES = {
dataLoading: true,
analyticsUnavailable: false,
allTags: [],
};

const MOCK_TOP_QUERIES = [
Expand Down Expand Up @@ -117,6 +119,7 @@ describe('AnalyticsLogic', () => {
...DEFAULT_VALUES,
dataLoading: false,
analyticsUnavailable: false,
allTags: ['some-tag'],
// TODO: more state will get set here in future PRs
});
});
Expand All @@ -131,6 +134,7 @@ describe('AnalyticsLogic', () => {
...DEFAULT_VALUES,
dataLoading: false,
analyticsUnavailable: false,
allTags: ['some-tag'],
// TODO: more state will get set here in future PRs
});
});
Expand Down Expand Up @@ -162,7 +166,11 @@ describe('AnalyticsLogic', () => {
expect(http.get).toHaveBeenCalledWith(
'/api/app_search/engines/test-engine/analytics/queries',
{
query: { size: 20 },
query: {
start: DEFAULT_START_DATE,
end: DEFAULT_END_DATE,
size: 20,
},
}
);
expect(AnalyticsLogic.actions.onAnalyticsDataLoad).toHaveBeenCalledWith(
Expand Down Expand Up @@ -239,7 +247,12 @@ describe('AnalyticsLogic', () => {

expect(http.get).toHaveBeenCalledWith(
'/api/app_search/engines/test-engine/analytics/queries/some-query',
expect.any(Object) // empty query obj
{
query: {
start: DEFAULT_START_DATE,
end: DEFAULT_END_DATE,
},
}
);
expect(AnalyticsLogic.actions.onQueryDataLoad).toHaveBeenCalledWith(MOCK_QUERY_RESPONSE);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HttpLogic } from '../../../shared/http';
import { flashAPIErrors } from '../../../shared/flash_messages';
import { EngineLogic } from '../engine';

import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants';
import { AnalyticsData, QueryDetails } from './types';

interface AnalyticsValues extends AnalyticsData, QueryDetails {
Expand Down Expand Up @@ -54,6 +55,13 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction
onQueryDataLoad: () => false,
},
],
allTags: [
[],
{
onAnalyticsDataLoad: (_, { allTags }) => allTags,
onQueryDataLoad: (_, { allTags }) => allTags,
},
],
}),
listeners: ({ actions }) => ({
loadAnalyticsData: async () => {
Expand All @@ -63,7 +71,12 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction

try {
const { start, end, tag } = queryString.parse(history.location.search);
const query = { start, end, tag, size: 20 };
const query = {
start: start || DEFAULT_START_DATE,
end: end || DEFAULT_END_DATE,
tag,
size: 20,
};
const url = `/api/app_search/engines/${engineName}/analytics/queries`;

const response = await http.get(url, { query });
Expand All @@ -85,7 +98,11 @@ export const AnalyticsLogic = kea<MakeLogicType<AnalyticsValues, AnalyticsAction

try {
const { start, end, tag } = queryString.parse(history.location.search);
const queryParams = { start, end, tag };
const queryParams = {
start: start || DEFAULT_START_DATE,
end: end || DEFAULT_END_DATE,
tag,
};
const url = `/api/app_search/engines/${engineName}/analytics/queries/${query}`;

const response = await http.get(url, { query: queryParams });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ describe('AnalyticsRouter', () => {
const wrapper = shallow(<AnalyticsRouter engineBreadcrumb={['Engines', 'some-engine']} />);

expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(8);
expect(wrapper.find(Route)).toHaveLength(9);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@
*/

import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Route, Switch, Redirect } from 'react-router-dom';

import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
import { NotFound } from '../../../shared/not_found';
import {
getEngineRoute,
ENGINE_PATH,
ENGINE_ANALYTICS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH,
ENGINE_ANALYTICS_RECENT_QUERIES_PATH,
ENGINE_ANALYTICS_QUERY_DETAILS_PATH,
ENGINE_ANALYTICS_QUERY_DETAIL_PATH,
} from '../../routes';
import {
Expand All @@ -29,40 +32,54 @@ import {
RECENT_QUERIES,
} from './constants';

import {
Analytics,
TopQueries,
TopQueriesNoResults,
TopQueriesNoClicks,
TopQueriesWithClicks,
RecentQueries,
QueryDetail,
} from './views';

interface Props {
engineBreadcrumb: string[];
engineBreadcrumb: BreadcrumbTrail;
}
export const AnalyticsRouter: React.FC<Props> = ({ engineBreadcrumb }) => {
const ANALYTICS_BREADCRUMB = [...engineBreadcrumb, ANALYTICS_TITLE];
const engineName = engineBreadcrumb[1];

return (
<Switch>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_PATH}>
<SetPageChrome trail={ANALYTICS_BREADCRUMB} />
TODO: Analytics overview
<Analytics />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES]} />
TODO: Top queries
<TopQueries />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_NO_RESULTS]} />
TODO: Top queries with no results
<TopQueriesNoResults />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_NO_CLICKS]} />
TODO: Top queries with no clicks
<TopQueriesNoClicks />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, TOP_QUERIES_WITH_CLICKS]} />
TODO: Top queries with clicks
<TopQueriesWithClicks />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_RECENT_QUERIES_PATH}>
<SetPageChrome trail={[...ANALYTICS_BREADCRUMB, RECENT_QUERIES]} />
TODO: Recent queries
<RecentQueries />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_QUERY_DETAIL_PATH}>
TODO: Query detail page
<QueryDetail breadcrumbs={ANALYTICS_BREADCRUMB} />
</Route>
<Route exact path={ENGINE_PATH + ENGINE_ANALYTICS_QUERY_DETAILS_PATH}>
<Redirect to={getEngineRoute(engineName) + ENGINE_ANALYTICS_PATH} />
</Route>
<Route>
<NotFound breadcrumbs={ANALYTICS_BREADCRUMB} product={APP_SEARCH_PLUGIN} />
Expand Down
Loading

0 comments on commit 3168b7d

Please sign in to comment.