diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index 36a3895c6161..daae8cf57f63 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -2,7 +2,7 @@ "id": "enterpriseSearch", "version": "kibana", "kibanaVersion": "kibana", - "requiredPlugins": ["features", "licensing"], + "requiredPlugins": ["features", "licensing", "charts"], "configPath": ["enterpriseSearch"], "optionalPlugins": ["usageCollection", "security", "home", "spaces", "cloud"], "server": true, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index 95843a243a3c..e1e20adbe575 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; import { mockHistory } from './'; export const mockKibanaValues = { config: { host: 'http://localhost:3002' }, - history: mockHistory, + charts: chartPluginMock.createStartContract(), cloud: { isCloudEnabled: false, cloudDeploymentUrl: 'https://cloud.elastic.co/deployments/some-id', }, + history: mockHistory, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx new file mode 100644 index 000000000000..4e071ac7982b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { mockKibanaValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { Chart, Settings, LineSeries, Axis } from '@elastic/charts'; + +import { AnalyticsChart } from './'; + +describe('AnalyticsChart', () => { + const MOCK_DATA = [ + { x: '1970-01-01', y: 0 }, + { x: '1970-01-02', y: 1 }, + { x: '1970-01-03', y: 5 }, + { x: '1970-01-04', y: 50 }, + { x: '1970-01-05', y: 25 }, + ]; + + beforeAll(() => { + jest.clearAllMocks(); + }); + + it('renders an Elastic line chart', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(Chart).prop('size')).toEqual({ height: 300 }); + expect(wrapper.find(Axis)).toHaveLength(2); + expect(mockKibanaValues.charts.theme.useChartsTheme).toHaveBeenCalled(); + expect(mockKibanaValues.charts.theme.useChartsBaseTheme).toHaveBeenCalled(); + + expect(wrapper.find(LineSeries)).toHaveLength(1); + expect(wrapper.find(LineSeries).prop('id')).toEqual('test'); + expect(wrapper.find(LineSeries).prop('data')).toEqual(MOCK_DATA); + }); + + it('renders multiple lines', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(LineSeries)).toHaveLength(3); + }); + + it('formats x-axis dates correctly', () => { + const wrapper = shallow(); + const dateFormatter: Function = wrapper.find('#bottom-axis').prop('tickFormat'); + + expect(dateFormatter('1970-02-28')).toEqual('2/28'); + }); + + it('formats tooltip dates correctly', () => { + const wrapper = shallow(); + const dateFormatter: Function = (wrapper.find(Settings).prop('tooltip') as any).headerFormatter; + + expect(dateFormatter({ value: '1970-12-03' })).toEqual('December 3, 1970'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx new file mode 100644 index 000000000000..02ad2dff9282 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx @@ -0,0 +1,61 @@ +/* + * 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 from 'react'; +import { useValues } from 'kea'; + +import moment from 'moment'; +import { Chart, Settings, LineSeries, CurveType, Axis } from '@elastic/charts'; + +import { KibanaLogic } from '../../../../shared/kibana'; + +import { X_AXIS_DATE_FORMAT, TOOLTIP_DATE_FORMAT } from '../constants'; + +interface ChartPoint { + x: string; // Date string + y: number; // # of clicks, queries, etc. +} +export type ChartData = ChartPoint[]; + +interface Props { + height?: number; + lines: Array<{ + id: string; + data: ChartData; + }>; +} +export const AnalyticsChart: React.FC = ({ height = 300, lines }) => { + const { charts } = useValues(KibanaLogic); + + return ( + + moment(tooltip.value).format(TOOLTIP_DATE_FORMAT), + }} + /> + {lines.map(({ id, data }) => ( + + ))} + moment(d).format(X_AXIS_DATE_FORMAT)} + showGridLines + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts new file mode 100644 index 000000000000..31aa998d86d9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { AnalyticsChart } from './analytics_chart'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts index 1e25582e3d56..9985753d0970 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts @@ -30,3 +30,8 @@ export const TOTAL_CLICKS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks', { defaultMessage: 'Total clicks' } ); + +// Moment date format conversions +export const SERVER_DATE_FORMAT = 'YYYY-MM-DD'; +export const TOOLTIP_DATE_FORMAT = 'MMMM D, YYYY'; +export const X_AXIS_DATE_FORMAT = 'M/D'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts index 7cddef645e83..3b201b38703d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts @@ -5,3 +5,5 @@ */ export { ANALYTICS_TITLE } from './constants'; +export { AnalyticsChart } from './components'; +export { convertToChartData } from './utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.test.ts new file mode 100644 index 000000000000..2f769dc44fda --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { convertToChartData } from './utils'; + +describe('convertToChartData', () => { + it('converts server-side analytics data into an array of objects that Elastic Charts can consume', () => { + expect( + convertToChartData({ + startDate: '1970-01-01', + data: [0, 1, 5, 50, 25], + }) + ).toEqual([ + { x: '1970-01-01', y: 0 }, + { x: '1970-01-02', y: 1 }, + { x: '1970-01-03', y: 5 }, + { x: '1970-01-04', y: 50 }, + { x: '1970-01-05', y: 25 }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts new file mode 100644 index 000000000000..a3aebc043e25 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; + +import { SERVER_DATE_FORMAT } from './constants'; +import { ChartData } from './components/analytics_chart'; + +interface ConvertToChartData { + data: number[]; + startDate: string; +} +export const convertToChartData = ({ data, startDate }: ConvertToChartData): ChartData => { + const date = moment(startDate, SERVER_DATE_FORMAT); + return data.map((y, index) => ({ + x: moment(date).add(index, 'days').format(SERVER_DATE_FORMAT), + y, + })); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx index 775a74921d0d..b1350b7e102e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; +import { AnalyticsChart } from '../../analytics'; import { TotalCharts } from './total_charts'; @@ -21,7 +22,6 @@ describe('TotalCharts', () => { setMockValues({ engineName: 'some-engine', startDate: '1970-01-01', - endDate: '1970-01-08', queriesPerDay: [0, 1, 2, 3, 5, 10, 50], operationsPerDay: [0, 0, 0, 0, 0, 0, 0], }); @@ -33,7 +33,7 @@ describe('TotalCharts', () => { expect(chart.find('h2').text()).toEqual('Total queries'); expect(chart.find(EuiButtonTo).prop('to')).toEqual('/engines/some-engine/analytics'); - // TODO: find chart component + expect(chart.find(AnalyticsChart)).toHaveLength(1); }); it('renders the total API operations chart', () => { @@ -41,6 +41,6 @@ describe('TotalCharts', () => { expect(chart.find('h2').text()).toEqual('Total API operations'); expect(chart.find(EuiButtonTo).prop('to')).toEqual('/engines/some-engine/api-logs'); - // TODO: find chart component + expect(chart.find(AnalyticsChart)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx index 214a6bd74aab..4ef4e08dee76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx @@ -24,6 +24,8 @@ import { ENGINE_ANALYTICS_PATH, ENGINE_API_LOGS_PATH, getEngineRoute } from '../ import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; import { VIEW_ANALYTICS, VIEW_API_LOGS, LAST_7_DAYS } from '../constants'; +import { AnalyticsChart, convertToChartData } from '../../analytics'; + import { EngineLogic } from '../../engine'; import { EngineOverviewLogic } from '../'; @@ -31,12 +33,7 @@ export const TotalCharts: React.FC = () => { const { engineName } = useValues(EngineLogic); const engineRoute = getEngineRoute(engineName); - const { - // startDate, - // endDate, - // queriesPerDay, - // operationsPerDay, - } = useValues(EngineOverviewLogic); + const { startDate, queriesPerDay, operationsPerDay } = useValues(EngineOverviewLogic); return ( @@ -58,12 +55,14 @@ export const TotalCharts: React.FC = () => { - TODO: Analytics chart - {/* */} + @@ -85,12 +84,14 @@ export const TotalCharts: React.FC = () => { - TODO: API Logs chart - {/* */} + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index 2063f706a474..61319245bba0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -28,7 +28,6 @@ describe('EngineOverviewLogic', () => { apiLogsUnavailable: true, documentCount: 10, startDate: '1970-01-30', - endDate: '1970-01-31', operationsPerDay: [0, 0, 0, 0, 0, 0, 0], queriesPerDay: [0, 0, 0, 0, 0, 25, 50], totalClicks: 50, @@ -40,7 +39,6 @@ describe('EngineOverviewLogic', () => { apiLogsUnavailable: false, documentCount: 0, startDate: '', - endDate: '', operationsPerDay: [], queriesPerDay: [], totalClicks: 0, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts index 3fc7ce8083e0..414159fff065 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts @@ -16,7 +16,6 @@ interface EngineOverviewApiData { apiLogsUnavailable: boolean; documentCount: number; startDate: string; - endDate: string; operationsPerDay: number[]; queriesPerDay: number[]; totalClicks: number; @@ -61,12 +60,6 @@ export const EngineOverviewLogic = kea startDate, }, ], - endDate: [ - '', - { - setPolledData: (_, { endDate }) => endDate, - }, - ], queriesPerDay: [ [], { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index d5b9513d0dbb..bbf42eae3708 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -9,6 +9,7 @@ import { getContext } from 'kea'; import { coreMock } from 'src/core/public/mocks'; import { licensingMock } from '../../../licensing/public/mocks'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { renderApp, renderHeaderActions } from './'; import { EnterpriseSearch } from './enterprise_search'; @@ -20,7 +21,10 @@ describe('renderApp', () => { const kibanaDeps = { params: coreMock.createAppMountParamters(), core: coreMock.createStart(), - plugins: { licensing: licensingMock.createStart() }, + plugins: { + licensing: licensingMock.createStart(), + charts: chartPluginMock.createStartContract(), + }, } as any; const pluginData = { config: {}, diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 1271015e40e5..6ebeb82ee326 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -41,6 +41,7 @@ export const renderApp = ( const unmountKibanaLogic = mountKibanaLogic({ config, + charts: plugins.charts, cloud: plugins.cloud || {}, history: params.history, navigateToUrl: core.application.navigateToUrl, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 7d3db4d36692..282b951e1514 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -9,6 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { FC } from 'react'; import { History } from 'history'; import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; +import { ChartsPluginStart } from 'src/plugins/charts/public'; import { CloudSetup } from '../../../../../cloud/public'; import { HttpLogic } from '../http'; @@ -18,6 +19,7 @@ interface KibanaLogicProps { config: { host?: string }; history: History; cloud: Partial; + charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setDocTitle(title: string): void; @@ -31,8 +33,9 @@ export const KibanaLogic = kea>({ path: ['enterprise_search', 'kibana_logic'], reducers: ({ props }) => ({ config: [props.config || {}, {}], - history: [props.history, {}], + charts: [props.charts, {}], cloud: [props.cloud || {}, {}], + history: [props.history, {}], navigateToUrl: [ (url: string, options?: CreateHrefOptions) => { const deps = { history: props.history, http: HttpLogic.values.http }; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 94e9ea88ea75..632bb425f203 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -18,6 +18,7 @@ import { } from '../../../../src/plugins/home/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { APP_SEARCH_PLUGIN, @@ -41,6 +42,7 @@ interface PluginsSetup { export interface PluginsStart { cloud?: CloudSetup; licensing: LicensingPluginStart; + charts: ChartsPluginStart; } export class EnterpriseSearchPlugin implements Plugin {