[App Search] Add AnalyticsChart component to EnginesOverview (#87752)
* Set up Kibana charts plugin dependency - required for using shared colors/themes/etc. - see https://github.com/elastic/kibana/tree/master/src/plugins/charts * Add reusable AnalyticsChart component + util for converting data from our server API to data that Elastic Charts can use * Update EngineOverview to use AnalyticsChart + remove now-unnecessary endDate value (we don't really need it just IMO) * [PR feedback] Return type * [Self feedback] naming - remove pluralization
This commit is contained in:
parent
a1c2422261
commit
04739d851c
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(
|
||||
<AnalyticsChart height={300} lines={[{ id: 'test', data: MOCK_DATA }]} />
|
||||
);
|
||||
|
||||
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(
|
||||
<AnalyticsChart
|
||||
lines={[
|
||||
{ id: 'line 1', data: MOCK_DATA },
|
||||
{ id: 'line 2', data: MOCK_DATA },
|
||||
{ id: 'line 3', data: MOCK_DATA },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(LineSeries)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('formats x-axis dates correctly', () => {
|
||||
const wrapper = shallow(<AnalyticsChart lines={[{ id: 'test', data: MOCK_DATA }]} />);
|
||||
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(<AnalyticsChart lines={[{ id: 'test', data: MOCK_DATA }]} />);
|
||||
const dateFormatter: Function = (wrapper.find(Settings).prop('tooltip') as any).headerFormatter;
|
||||
|
||||
expect(dateFormatter({ value: '1970-12-03' })).toEqual('December 3, 1970');
|
||||
});
|
||||
});
|
|
@ -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<Props> = ({ height = 300, lines }) => {
|
||||
const { charts } = useValues(KibanaLogic);
|
||||
|
||||
return (
|
||||
<Chart size={{ height }}>
|
||||
<Settings
|
||||
theme={charts.theme.useChartsTheme()}
|
||||
baseTheme={charts.theme.useChartsBaseTheme()}
|
||||
tooltip={{
|
||||
headerFormatter: (tooltip) => moment(tooltip.value).format(TOOLTIP_DATE_FORMAT),
|
||||
}}
|
||||
/>
|
||||
{lines.map(({ id, data }) => (
|
||||
<LineSeries
|
||||
key={id}
|
||||
id={id}
|
||||
data={data}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
))}
|
||||
<Axis
|
||||
id="bottom-axis"
|
||||
position="bottom"
|
||||
tickFormat={(d) => moment(d).format(X_AXIS_DATE_FORMAT)}
|
||||
showGridLines
|
||||
/>
|
||||
<Axis id="left-axis" position="left" ticks={4} showGridLines />
|
||||
</Chart>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
*/
|
||||
|
||||
export { ANALYTICS_TITLE } from './constants';
|
||||
export { AnalyticsChart } from './components';
|
||||
export { convertToChartData } from './utils';
|
||||
|
|
|
@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}));
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<EuiFlexGroup>
|
||||
|
@ -58,12 +55,14 @@ export const TotalCharts: React.FC = () => {
|
|||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
TODO: Analytics chart
|
||||
{/* <EngineAnalytics
|
||||
data={[queriesPerDay]}
|
||||
startDate={new Date(startDate)}
|
||||
endDate={new Date(endDate)}
|
||||
/> */}
|
||||
<AnalyticsChart
|
||||
lines={[
|
||||
{
|
||||
id: TOTAL_QUERIES,
|
||||
data: convertToChartData({ startDate, data: queriesPerDay }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiFlexItem>
|
||||
|
@ -85,12 +84,14 @@ export const TotalCharts: React.FC = () => {
|
|||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
TODO: API Logs chart
|
||||
{/* <EngineAnalytics
|
||||
data={[operationsPerDay]}
|
||||
startDate={new Date(startDate)}
|
||||
endDate={new Date(endDate)}
|
||||
/> */}
|
||||
<AnalyticsChart
|
||||
lines={[
|
||||
{
|
||||
id: TOTAL_API_OPERATIONS,
|
||||
data: convertToChartData({ startDate, data: operationsPerDay }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<MakeLogicType<EngineOverviewValues, Engin
|
|||
setPolledData: (_, { startDate }) => startDate,
|
||||
},
|
||||
],
|
||||
endDate: [
|
||||
'',
|
||||
{
|
||||
setPolledData: (_, { endDate }) => endDate,
|
||||
},
|
||||
],
|
||||
queriesPerDay: [
|
||||
[],
|
||||
{
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -41,6 +41,7 @@ export const renderApp = (
|
|||
|
||||
const unmountKibanaLogic = mountKibanaLogic({
|
||||
config,
|
||||
charts: plugins.charts,
|
||||
cloud: plugins.cloud || {},
|
||||
history: params.history,
|
||||
navigateToUrl: core.application.navigateToUrl,
|
||||
|
|
|
@ -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<CloudSetup>;
|
||||
charts: ChartsPluginStart;
|
||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||
setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void;
|
||||
setDocTitle(title: string): void;
|
||||
|
@ -31,8 +33,9 @@ export const KibanaLogic = kea<MakeLogicType<KibanaValues>>({
|
|||
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 };
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue