mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Observability][Exploratory View] revert exploratory view multi-series (#107647)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
328c36dedc
commit
1649661ffd
123 changed files with 2595 additions and 3871 deletions
|
@ -22,6 +22,11 @@ interface UrlParam {
|
|||
username?: string;
|
||||
}
|
||||
|
||||
interface App {
|
||||
pathname?: string;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a config and a pathname to a url
|
||||
* @param {object} config A url config
|
||||
|
@ -41,11 +46,11 @@ interface UrlParam {
|
|||
* @return {string}
|
||||
*/
|
||||
|
||||
function getUrl(config: UrlParam, app: UrlParam) {
|
||||
function getUrl(config: UrlParam, app: App) {
|
||||
return url.format(_.assign({}, config, app));
|
||||
}
|
||||
|
||||
getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) {
|
||||
getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) {
|
||||
config = _.pickBy(config, function (val, param) {
|
||||
return param !== 'auth';
|
||||
});
|
||||
|
|
|
@ -202,13 +202,7 @@ export class CommonPageObject extends FtrService {
|
|||
|
||||
async navigateToApp(
|
||||
appName: string,
|
||||
{
|
||||
basePath = '',
|
||||
shouldLoginIfPrompted = true,
|
||||
hash = '',
|
||||
search = '',
|
||||
insertTimestamp = true,
|
||||
} = {}
|
||||
{ basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {}
|
||||
) {
|
||||
let appUrl: string;
|
||||
if (this.config.has(['apps', appName])) {
|
||||
|
@ -217,13 +211,11 @@ export class CommonPageObject extends FtrService {
|
|||
appUrl = getUrl.noAuth(this.config.get('servers.kibana'), {
|
||||
pathname: `${basePath}${appConfig.pathname}`,
|
||||
hash: hash || appConfig.hash,
|
||||
search,
|
||||
});
|
||||
} else {
|
||||
appUrl = getUrl.noAuth(this.config.get('servers.kibana'), {
|
||||
pathname: `${basePath}/app/${appName}`,
|
||||
hash,
|
||||
search,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import {
|
||||
createExploratoryViewUrl,
|
||||
HeaderMenuPortal,
|
||||
SeriesUrl,
|
||||
} from '../../../../../../observability/public';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { AppMountParameters } from '../../../../../../../../src/core/public';
|
||||
import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', {
|
||||
defaultMessage: 'Analyze data',
|
||||
|
@ -38,22 +38,15 @@ export function UXActionMenu({
|
|||
services: { http },
|
||||
} = useKibana();
|
||||
const { urlParams } = useUrlParams();
|
||||
const { rangeTo, rangeFrom, serviceName } = urlParams;
|
||||
const { rangeTo, rangeFrom } = urlParams;
|
||||
|
||||
const uxExploratoryViewLink = createExploratoryViewUrl(
|
||||
{
|
||||
reportType: 'kpi-over-time',
|
||||
allSeries: [
|
||||
{
|
||||
dataType: 'ux',
|
||||
name: `${serviceName}-page-views`,
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
[SERVICE_NAME]: serviceName ? [serviceName] : [],
|
||||
},
|
||||
selectedMetricField: 'Records',
|
||||
},
|
||||
],
|
||||
'ux-series': ({
|
||||
dataType: 'ux',
|
||||
isNew: true,
|
||||
time: { from: rangeFrom, to: rangeTo },
|
||||
} as unknown) as SeriesUrl,
|
||||
},
|
||||
http?.basePath.get()
|
||||
);
|
||||
|
@ -67,7 +60,6 @@ export function UXActionMenu({
|
|||
<EuiHeaderLinks gutterSize="xs">
|
||||
<EuiToolTip position="top" content={<p>{ANALYZE_MESSAGE}</p>}>
|
||||
<EuiHeaderLink
|
||||
data-test-subj="uxAnalyzeBtn"
|
||||
color="text"
|
||||
href={uxExploratoryViewLink}
|
||||
iconType="visBarVerticalStacked"
|
||||
|
|
|
@ -87,18 +87,15 @@ export function PageLoadDistribution() {
|
|||
|
||||
const exploratoryViewLink = createExploratoryViewUrl(
|
||||
{
|
||||
reportType: 'kpi-over-time',
|
||||
allSeries: [
|
||||
{
|
||||
name: `${serviceName}-page-views`,
|
||||
dataType: 'ux',
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
'service.name': serviceName as string[],
|
||||
},
|
||||
...(breakdown ? { breakdown: breakdown.fieldName } : {}),
|
||||
[`${serviceName}-page-views`]: {
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
'service.name': serviceName as string[],
|
||||
},
|
||||
],
|
||||
...(breakdown ? { breakdown: breakdown.fieldName } : {}),
|
||||
},
|
||||
},
|
||||
http?.basePath.get()
|
||||
);
|
||||
|
|
|
@ -62,18 +62,15 @@ export function PageViewsTrend() {
|
|||
|
||||
const exploratoryViewLink = createExploratoryViewUrl(
|
||||
{
|
||||
reportType: 'kpi-over-time',
|
||||
allSeries: [
|
||||
{
|
||||
name: `${serviceName}-page-views`,
|
||||
dataType: 'ux',
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
'service.name': serviceName as string[],
|
||||
},
|
||||
...(breakdown ? { breakdown: breakdown.fieldName } : {}),
|
||||
[`${serviceName}-page-views`]: {
|
||||
dataType: 'ux',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
'service.name': serviceName as string[],
|
||||
},
|
||||
],
|
||||
...(breakdown ? { breakdown: breakdown.fieldName } : {}),
|
||||
},
|
||||
},
|
||||
http?.basePath.get()
|
||||
);
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('AnalyzeDataButton', () => {
|
|||
render(<Example agentName="rum-js" />);
|
||||
|
||||
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
|
||||
'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
|
||||
'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => {
|
|||
render(<Example agentName="iOS/swift" />);
|
||||
|
||||
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
|
||||
'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
|
||||
'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => {
|
|||
render(<Example environment={undefined} />);
|
||||
|
||||
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
|
||||
'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
|
||||
'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => {
|
|||
render(<Example environment={ENVIRONMENT_NOT_DEFINED.value} />);
|
||||
|
||||
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
|
||||
'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
|
||||
'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -78,7 +78,7 @@ describe('AnalyzeDataButton', () => {
|
|||
render(<Example environment={ENVIRONMENT_ALL.value} />);
|
||||
|
||||
expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
|
||||
'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))'
|
||||
'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,10 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { createExploratoryViewUrl } from '../../../../../../observability/public';
|
||||
import {
|
||||
createExploratoryViewUrl,
|
||||
SeriesUrl,
|
||||
} from '../../../../../../observability/public';
|
||||
import { ALL_VALUES_SELECTED } from '../../../../../../observability/public';
|
||||
import {
|
||||
isIosAgentName,
|
||||
|
@ -18,7 +21,6 @@ import {
|
|||
import {
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import {
|
||||
ENVIRONMENT_ALL,
|
||||
|
@ -27,11 +29,13 @@ import {
|
|||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
|
||||
function getEnvironmentDefinition(environment: string) {
|
||||
function getEnvironmentDefinition(environment?: string) {
|
||||
switch (environment) {
|
||||
case ENVIRONMENT_ALL.value:
|
||||
return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] };
|
||||
case ENVIRONMENT_NOT_DEFINED.value:
|
||||
case undefined:
|
||||
return {};
|
||||
default:
|
||||
return { [SERVICE_ENVIRONMENT]: [environment] };
|
||||
}
|
||||
|
@ -47,26 +51,21 @@ export function AnalyzeDataButton() {
|
|||
|
||||
if (
|
||||
(isRumAgentName(agentName) || isIosAgentName(agentName)) &&
|
||||
rangeFrom &&
|
||||
canShowDashboard &&
|
||||
rangeTo
|
||||
canShowDashboard
|
||||
) {
|
||||
const href = createExploratoryViewUrl(
|
||||
{
|
||||
reportType: 'kpi-over-time',
|
||||
allSeries: [
|
||||
{
|
||||
name: `${serviceName}-response-latency`,
|
||||
selectedMetricField: TRANSACTION_DURATION,
|
||||
dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
|
||||
time: { from: rangeFrom, to: rangeTo },
|
||||
reportDefinitions: {
|
||||
[SERVICE_NAME]: [serviceName],
|
||||
...(environment ? getEnvironmentDefinition(environment) : {}),
|
||||
},
|
||||
operationType: 'average',
|
||||
'apm-series': {
|
||||
dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
|
||||
time: { from: rangeFrom, to: rangeTo },
|
||||
reportType: 'kpi-over-time',
|
||||
reportDefinitions: {
|
||||
[SERVICE_NAME]: [serviceName],
|
||||
...getEnvironmentDefinition(environment),
|
||||
},
|
||||
],
|
||||
operationType: 'average',
|
||||
isNew: true,
|
||||
} as SeriesUrl,
|
||||
},
|
||||
basepath
|
||||
);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
|
@ -21,10 +20,7 @@ export async function hasRumData({
|
|||
setup: Setup & Partial<SetupTimeRange>;
|
||||
}) {
|
||||
try {
|
||||
const {
|
||||
start = moment().subtract(24, 'h').valueOf(),
|
||||
end = moment().valueOf(),
|
||||
} = setup;
|
||||
const { start, end } = setup;
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
|
|
|
@ -6,8 +6,16 @@
|
|||
},
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "observability"],
|
||||
"optionalPlugins": ["home", "discover", "lens", "licensing", "usageCollection"],
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"observability"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"home",
|
||||
"lens",
|
||||
"licensing",
|
||||
"usageCollection"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"alerting",
|
||||
"cases",
|
||||
|
|
|
@ -1,32 +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 { EuiHeaderLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export function MobileAddData() {
|
||||
const kibana = useKibana();
|
||||
|
||||
return (
|
||||
<EuiHeaderLink
|
||||
aria-label={i18n.translate('xpack.observability.page_header.addMobileDataLink.label', {
|
||||
defaultMessage: 'Navigate to a tutorial about adding mobile APM data',
|
||||
})}
|
||||
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/apm')}
|
||||
color="primary"
|
||||
iconType="indexOpen"
|
||||
>
|
||||
{ADD_DATA_LABEL}
|
||||
</EuiHeaderLink>
|
||||
);
|
||||
}
|
||||
|
||||
const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', {
|
||||
defaultMessage: 'Add Mobile data',
|
||||
});
|
|
@ -1,32 +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 { EuiHeaderLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export function SyntheticsAddData() {
|
||||
const kibana = useKibana();
|
||||
|
||||
return (
|
||||
<EuiHeaderLink
|
||||
aria-label={i18n.translate('xpack.observability.page_header.addUptimeDataLink.label', {
|
||||
defaultMessage: 'Navigate to a tutorial about adding Uptime data',
|
||||
})}
|
||||
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/uptimeMonitors')}
|
||||
color="primary"
|
||||
iconType="indexOpen"
|
||||
>
|
||||
{ADD_DATA_LABEL}
|
||||
</EuiHeaderLink>
|
||||
);
|
||||
}
|
||||
|
||||
const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', {
|
||||
defaultMessage: 'Add synthetics data',
|
||||
});
|
|
@ -1,32 +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 { EuiHeaderLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export function UXAddData() {
|
||||
const kibana = useKibana();
|
||||
|
||||
return (
|
||||
<EuiHeaderLink
|
||||
aria-label={i18n.translate('xpack.observability.page_header.addUXDataLink.label', {
|
||||
defaultMessage: 'Navigate to a tutorial about adding user experience APM data',
|
||||
})}
|
||||
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/apm')}
|
||||
color="primary"
|
||||
iconType="indexOpen"
|
||||
>
|
||||
{ADD_DATA_LABEL}
|
||||
</EuiHeaderLink>
|
||||
);
|
||||
}
|
||||
|
||||
const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', {
|
||||
defaultMessage: 'Add UX data',
|
||||
});
|
|
@ -1,59 +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 { render } from '../../rtl_helpers';
|
||||
import { fireEvent, screen } from '@testing-library/dom';
|
||||
import React from 'react';
|
||||
import { sampleAttribute } from '../../configurations/test_data/sample_attribute';
|
||||
import * as pluginHook from '../../../../../hooks/use_plugin_context';
|
||||
import { TypedLensByValueInput } from '../../../../../../../lens/public';
|
||||
import { ExpViewActionMenuContent } from './action_menu';
|
||||
|
||||
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
|
||||
appMountParameters: {
|
||||
setHeaderActionMenu: jest.fn(),
|
||||
},
|
||||
} as any);
|
||||
|
||||
describe('Action Menu', function () {
|
||||
it('should be able to click open in lens', async function () {
|
||||
const { findByText, core } = render(
|
||||
<ExpViewActionMenuContent
|
||||
lensAttributes={sampleAttribute as TypedLensByValueInput['attributes']}
|
||||
timeRange={{ to: 'now', from: 'now-10m' }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Open in Lens')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(await findByText('Open in Lens'));
|
||||
|
||||
expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
|
||||
expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
|
||||
{
|
||||
id: '',
|
||||
attributes: sampleAttribute,
|
||||
timeRange: { to: 'now', from: 'now-10m' },
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to click save', async function () {
|
||||
const { findByText } = render(
|
||||
<ExpViewActionMenuContent
|
||||
lensAttributes={sampleAttribute as TypedLensByValueInput['attributes']}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Save')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(await findByText('Save'));
|
||||
|
||||
expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,92 +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 React, { useState } from 'react';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public';
|
||||
import { ObservabilityAppServices } from '../../../../../application/types';
|
||||
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export function ExpViewActionMenuContent({
|
||||
timeRange,
|
||||
lensAttributes,
|
||||
}: {
|
||||
timeRange?: { from: string; to: string };
|
||||
lensAttributes: TypedLensByValueInput['attributes'] | null;
|
||||
}) {
|
||||
const kServices = useKibana<ObservabilityAppServices>().services;
|
||||
|
||||
const { lens } = kServices;
|
||||
|
||||
const [isSaveOpen, setIsSaveOpen] = useState(false);
|
||||
|
||||
const LensSaveModalComponent = lens.SaveModalComponent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
style={{ paddingRight: 20 }}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="lensApp"
|
||||
fullWidth={false}
|
||||
isDisabled={!lens.canUseEditor() || lensAttributes === null}
|
||||
size="s"
|
||||
onClick={() => {
|
||||
if (lensAttributes) {
|
||||
lens.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange,
|
||||
attributes: lensAttributes,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.heading.openInLens', {
|
||||
defaultMessage: 'Open in Lens',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="save"
|
||||
fullWidth={false}
|
||||
isDisabled={!lens.canUseEditor() || lensAttributes === null}
|
||||
onClick={() => {
|
||||
if (lensAttributes) {
|
||||
setIsSaveOpen(true);
|
||||
}
|
||||
}}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{isSaveOpen && lensAttributes && (
|
||||
<LensSaveModalComponent
|
||||
initialInput={(lensAttributes as unknown) as LensEmbeddableInput}
|
||||
onClose={() => setIsSaveOpen(false)}
|
||||
// if we want to do anything after the viz is saved
|
||||
// right now there is no action, so an empty function
|
||||
onSave={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,26 +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 React from 'react';
|
||||
import { ExpViewActionMenuContent } from './action_menu';
|
||||
import HeaderMenuPortal from '../../../header_menu_portal';
|
||||
import { usePluginContext } from '../../../../../hooks/use_plugin_context';
|
||||
import { TypedLensByValueInput } from '../../../../../../../lens/public';
|
||||
|
||||
interface Props {
|
||||
timeRange?: { from: string; to: string };
|
||||
lensAttributes: TypedLensByValueInput['attributes'] | null;
|
||||
}
|
||||
export function ExpViewActionMenu(props: Props) {
|
||||
const { appMountParameters } = usePluginContext();
|
||||
|
||||
return (
|
||||
<HeaderMenuPortal setHeaderActionMenu={appMountParameters.setHeaderActionMenu}>
|
||||
<ExpViewActionMenuContent {...props} />
|
||||
</HeaderMenuPortal>
|
||||
);
|
||||
}
|
|
@ -10,19 +10,19 @@ import { isEmpty } from 'lodash';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LOADING_VIEW } from '../series_editor/series_editor';
|
||||
import { ReportViewType, SeriesUrl } from '../types';
|
||||
import { LOADING_VIEW } from '../series_builder/series_builder';
|
||||
import { SeriesUrl } from '../types';
|
||||
|
||||
export function EmptyView({
|
||||
loading,
|
||||
height,
|
||||
series,
|
||||
reportType,
|
||||
}: {
|
||||
loading: boolean;
|
||||
series?: SeriesUrl;
|
||||
reportType: ReportViewType;
|
||||
height: string;
|
||||
series: SeriesUrl;
|
||||
}) {
|
||||
const { dataType, reportDefinitions } = series ?? {};
|
||||
const { dataType, reportType, reportDefinitions } = series ?? {};
|
||||
|
||||
let emptyMessage = EMPTY_LABEL;
|
||||
|
||||
|
@ -45,7 +45,7 @@ export function EmptyView({
|
|||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Wrapper height={height}>
|
||||
{loading && (
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
|
@ -66,8 +66,9 @@ export function EmptyView({
|
|||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
const Wrapper = styled.div<{ height: string }>`
|
||||
text-align: center;
|
||||
height: ${(props) => props.height};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers';
|
||||
import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
|
||||
import { FilterLabel } from './filter_label';
|
||||
import * as useSeriesHook from '../hooks/use_series_filters';
|
||||
import { buildFilterLabel } from '../../filter_value_label/filter_value_label';
|
||||
|
@ -27,10 +27,9 @@ describe('FilterLabel', function () {
|
|||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={false}
|
||||
seriesId={0}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -52,10 +51,9 @@ describe('FilterLabel', function () {
|
|||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={false}
|
||||
seriesId={0}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={removeFilter}
|
||||
indexPattern={mockIndexPattern}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -76,10 +74,9 @@ describe('FilterLabel', function () {
|
|||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={false}
|
||||
seriesId={0}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={removeFilter}
|
||||
indexPattern={mockIndexPattern}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -103,10 +100,9 @@ describe('FilterLabel', function () {
|
|||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={true}
|
||||
seriesId={0}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,24 +9,21 @@ import React from 'react';
|
|||
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
import { useSeriesFilters } from '../hooks/use_series_filters';
|
||||
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
|
||||
import { SeriesUrl } from '../types';
|
||||
|
||||
interface Props {
|
||||
field: string;
|
||||
label: string;
|
||||
value: string | string[];
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
value: string;
|
||||
seriesId: string;
|
||||
negate: boolean;
|
||||
definitionFilter?: boolean;
|
||||
indexPattern: IndexPattern;
|
||||
removeFilter: (field: string, value: string | string[], notVal: boolean) => void;
|
||||
removeFilter: (field: string, value: string, notVal: boolean) => void;
|
||||
}
|
||||
|
||||
export function FilterLabel({
|
||||
label,
|
||||
seriesId,
|
||||
series,
|
||||
field,
|
||||
value,
|
||||
negate,
|
||||
|
@ -34,7 +31,7 @@ export function FilterLabel({
|
|||
removeFilter,
|
||||
definitionFilter,
|
||||
}: Props) {
|
||||
const { invertFilter } = useSeriesFilters({ seriesId, series });
|
||||
const { invertFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
return indexPattern ? (
|
||||
<FilterValueLabel
|
||||
|
|
|
@ -1,59 +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 React, { useState } from 'react';
|
||||
import { EuiColorPicker, EuiFormRow, EuiIcon, EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { ToolbarButton } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { SeriesUrl } from '../types';
|
||||
|
||||
export function SeriesColorPicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) {
|
||||
const theme = useTheme();
|
||||
|
||||
const { setSeries } = useSeriesStorage();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onChange = (colorN: string) => {
|
||||
setSeries(seriesId, { ...series, color: colorN });
|
||||
};
|
||||
|
||||
const color =
|
||||
series.color ?? ((theme.eui as unknown) as Record<string, string>)[`euiColorVis${seriesId}`];
|
||||
|
||||
const button = (
|
||||
<EuiToolTip content={EDIT_SERIES_COLOR_LABEL}>
|
||||
<ToolbarButton size="s" onClick={() => setIsOpen((prevState) => !prevState)} hasArrow={false}>
|
||||
<EuiIcon type="dot" size="l" color={color} />
|
||||
</ToolbarButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover button={button} isOpen={isOpen} closePopover={() => setIsOpen(false)}>
|
||||
<EuiFormRow label={PICK_A_COLOR_LABEL}>
|
||||
<EuiColorPicker onChange={onChange} color={color} />
|
||||
</EuiFormRow>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
const PICK_A_COLOR_LABEL = i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.pickColor',
|
||||
{
|
||||
defaultMessage: 'Pick a color',
|
||||
}
|
||||
);
|
||||
|
||||
const EDIT_SERIES_COLOR_LABEL = i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.editSeriesColor',
|
||||
{
|
||||
defaultMessage: 'Edit color for series',
|
||||
}
|
||||
);
|
|
@ -1,109 +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 moment from 'moment';
|
||||
import { EuiSuperDatePicker, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useHasData } from '../../../../../hooks/use_has_data';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges';
|
||||
import { parseTimeParts } from '../../series_viewer/columns/utils';
|
||||
import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { SeriesUrl } from '../../types';
|
||||
import { ReportTypes } from '../../configurations/constants';
|
||||
|
||||
export interface TimePickerTime {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface TimePickerQuickRange extends TimePickerTime {
|
||||
display: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
readonly?: boolean;
|
||||
}
|
||||
const readableUnit: Record<string, string> = {
|
||||
m: i18n.translate('xpack.observability.overview.exploratoryView.minutes', {
|
||||
defaultMessage: 'Minutes',
|
||||
}),
|
||||
h: i18n.translate('xpack.observability.overview.exploratoryView.hour', {
|
||||
defaultMessage: 'Hour',
|
||||
}),
|
||||
d: i18n.translate('xpack.observability.overview.exploratoryView.day', {
|
||||
defaultMessage: 'Day',
|
||||
}),
|
||||
};
|
||||
|
||||
export function SeriesDatePicker({ series, seriesId, readonly = true }: Props) {
|
||||
const { onRefreshTimeRange } = useHasData();
|
||||
|
||||
const commonlyUsedRanges = useQuickTimeRanges();
|
||||
|
||||
const { setSeries, reportType, allSeries, firstSeries } = useSeriesStorage();
|
||||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
onRefreshTimeRange();
|
||||
if (reportType === ReportTypes.KPI) {
|
||||
allSeries.forEach((currSeries, seriesIndex) => {
|
||||
setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } });
|
||||
});
|
||||
} else {
|
||||
setSeries(seriesId, { ...series, time: { from: start, to: end } });
|
||||
}
|
||||
}
|
||||
|
||||
const seriesTime = series.time ?? firstSeries!.time;
|
||||
|
||||
const dateFormat = useUiSetting<string>('dateFormat').replace('ss.SSS', 'ss');
|
||||
|
||||
if (readonly) {
|
||||
const timeParts = parseTimeParts(seriesTime?.from, seriesTime?.to);
|
||||
|
||||
if (timeParts) {
|
||||
const {
|
||||
timeTense: timeTenseDefault,
|
||||
timeUnits: timeUnitsDefault,
|
||||
timeValue: timeValueDefault,
|
||||
} = timeParts;
|
||||
|
||||
return (
|
||||
<EuiText color="subdued" size="s">{`${timeTenseDefault} ${timeValueDefault} ${
|
||||
readableUnit?.[timeUnitsDefault] ?? timeUnitsDefault
|
||||
}`}</EuiText>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.observability.overview.exploratoryView.dateRangeReadonly', {
|
||||
defaultMessage: '{start} to {end}',
|
||||
values: {
|
||||
start: moment(seriesTime.from).format(dateFormat),
|
||||
end: moment(seriesTime.to).format(dateFormat),
|
||||
},
|
||||
})}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={series?.time?.from}
|
||||
end={series?.time?.to}
|
||||
onTimeChange={onTimeChange}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
onRefresh={onTimeChange}
|
||||
showUpdateButton={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -94,19 +94,6 @@ export const DataViewLabels: Record<ReportViewType, string> = {
|
|||
'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
|
||||
};
|
||||
|
||||
export enum ReportTypes {
|
||||
KPI = 'kpi-over-time',
|
||||
DISTRIBUTION = 'data-distribution',
|
||||
CORE_WEB_VITAL = 'core-web-vitals',
|
||||
DEVICE_DISTRIBUTION = 'device-data-distribution',
|
||||
}
|
||||
|
||||
export enum DataTypes {
|
||||
SYNTHETICS = 'synthetics',
|
||||
UX = 'ux',
|
||||
MOBILE = 'mobile',
|
||||
}
|
||||
|
||||
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
|
||||
export const FILTER_RECORDS = 'FILTER_RECORDS';
|
||||
export const TERMS_COLUMN = 'TERMS_COLUMN';
|
||||
|
|
|
@ -8,12 +8,10 @@
|
|||
export enum URL_KEYS {
|
||||
DATA_TYPE = 'dt',
|
||||
OPERATION_TYPE = 'op',
|
||||
REPORT_TYPE = 'rt',
|
||||
SERIES_TYPE = 'st',
|
||||
BREAK_DOWN = 'bd',
|
||||
FILTERS = 'ft',
|
||||
REPORT_DEFINITIONS = 'rdf',
|
||||
SELECTED_METRIC = 'mt',
|
||||
HIDDEN = 'h',
|
||||
NAME = 'n',
|
||||
COLOR = 'c',
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config';
|
|||
import { getMobileKPIConfig } from './mobile/kpi_over_time_config';
|
||||
import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
|
||||
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
|
||||
import { DataTypes, ReportTypes } from './constants';
|
||||
|
||||
interface Props {
|
||||
reportType: ReportViewType;
|
||||
|
@ -25,24 +24,24 @@ interface Props {
|
|||
|
||||
export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
|
||||
switch (dataType) {
|
||||
case DataTypes.UX:
|
||||
if (reportType === ReportTypes.DISTRIBUTION) {
|
||||
case 'ux':
|
||||
if (reportType === 'data-distribution') {
|
||||
return getRumDistributionConfig({ indexPattern });
|
||||
}
|
||||
if (reportType === ReportTypes.CORE_WEB_VITAL) {
|
||||
if (reportType === 'core-web-vitals') {
|
||||
return getCoreWebVitalsConfig({ indexPattern });
|
||||
}
|
||||
return getKPITrendsLensConfig({ indexPattern });
|
||||
case DataTypes.SYNTHETICS:
|
||||
if (reportType === ReportTypes.DISTRIBUTION) {
|
||||
case 'synthetics':
|
||||
if (reportType === 'data-distribution') {
|
||||
return getSyntheticsDistributionConfig({ indexPattern });
|
||||
}
|
||||
return getSyntheticsKPIConfig({ indexPattern });
|
||||
case DataTypes.MOBILE:
|
||||
if (reportType === ReportTypes.DISTRIBUTION) {
|
||||
case 'mobile':
|
||||
if (reportType === 'data-distribution') {
|
||||
return getMobileKPIDistributionConfig({ indexPattern });
|
||||
}
|
||||
if (reportType === ReportTypes.DEVICE_DISTRIBUTION) {
|
||||
if (reportType === 'device-data-distribution') {
|
||||
return getMobileDeviceDistributionConfig({ indexPattern });
|
||||
}
|
||||
return getMobileKPIConfig({ indexPattern });
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from './constants/elasticsearch_fieldnames';
|
||||
import { buildExistsFilter, buildPhrasesFilter } from './utils';
|
||||
import { sampleAttributeKpi } from './test_data/sample_attribute_kpi';
|
||||
import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants';
|
||||
import { REPORT_METRIC_FIELD } from './constants';
|
||||
|
||||
describe('Lens Attribute', () => {
|
||||
mockAppIndexPattern();
|
||||
|
@ -38,9 +38,6 @@ describe('Lens Attribute', () => {
|
|||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: {},
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
color: 'green',
|
||||
name: 'test-series',
|
||||
selectedMetricField: TRANSACTION_DURATION,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -53,7 +50,7 @@ describe('Lens Attribute', () => {
|
|||
|
||||
it('should return expected json for kpi report type', function () {
|
||||
const seriesConfigKpi = getDefaultConfigs({
|
||||
reportType: ReportTypes.KPI,
|
||||
reportType: 'kpi-over-time',
|
||||
dataType: 'ux',
|
||||
indexPattern: mockIndexPattern,
|
||||
});
|
||||
|
@ -66,9 +63,6 @@ describe('Lens Attribute', () => {
|
|||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: { 'service.name': ['elastic-co'] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
color: 'green',
|
||||
name: 'test-series',
|
||||
selectedMetricField: RECORDS_FIELD,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -141,9 +135,6 @@ describe('Lens Attribute', () => {
|
|||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
color: 'green',
|
||||
name: 'test-series',
|
||||
selectedMetricField: TRANSACTION_DURATION,
|
||||
};
|
||||
|
||||
lnsAttr = new LensAttributes([layerConfig1]);
|
||||
|
@ -286,7 +277,7 @@ describe('Lens Attribute', () => {
|
|||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
isBucketed: false,
|
||||
label: 'test-series',
|
||||
label: 'Pages loaded',
|
||||
operationType: 'formula',
|
||||
params: {
|
||||
format: {
|
||||
|
@ -392,7 +383,7 @@ describe('Lens Attribute', () => {
|
|||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }],
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
},
|
||||
],
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
|
@ -412,9 +403,6 @@ describe('Lens Attribute', () => {
|
|||
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
|
||||
breakdown: USER_AGENT_NAME,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
color: 'green',
|
||||
name: 'test-series',
|
||||
selectedMetricField: TRANSACTION_DURATION,
|
||||
};
|
||||
|
||||
lnsAttr = new LensAttributes([layerConfig1]);
|
||||
|
@ -434,7 +422,7 @@ describe('Lens Attribute', () => {
|
|||
seriesType: 'line',
|
||||
splitAccessor: 'breakdown-column-layer0',
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }],
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -495,7 +483,7 @@ describe('Lens Attribute', () => {
|
|||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
isBucketed: false,
|
||||
label: 'test-series',
|
||||
label: 'Pages loaded',
|
||||
operationType: 'formula',
|
||||
params: {
|
||||
format: {
|
||||
|
@ -601,9 +589,6 @@ describe('Lens Attribute', () => {
|
|||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
color: 'green',
|
||||
name: 'test-series',
|
||||
selectedMetricField: TRANSACTION_DURATION,
|
||||
};
|
||||
|
||||
const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
import {
|
||||
CountIndexPatternColumn,
|
||||
DateHistogramIndexPatternColumn,
|
||||
|
@ -37,11 +36,10 @@ import {
|
|||
REPORT_METRIC_FIELD,
|
||||
RECORDS_FIELD,
|
||||
RECORDS_PERCENTAGE_FIELD,
|
||||
ReportTypes,
|
||||
} from './constants';
|
||||
import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types';
|
||||
import { PersistableFilter } from '../../../../../../lens/common';
|
||||
import { parseAbsoluteDate } from '../components/date_range_picker';
|
||||
import { parseAbsoluteDate } from '../series_date_picker/date_range_picker';
|
||||
import { getDistributionInPercentageColumn } from './lens_columns/overall_column';
|
||||
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
|
@ -75,6 +73,14 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF
|
|||
timeScale = currField?.timeScale;
|
||||
columnLabel = currField?.label;
|
||||
}
|
||||
} else if (metricOptions?.[0].field || metricOptions?.[0].id) {
|
||||
const firstMetricOption = metricOptions?.[0];
|
||||
|
||||
selectedMetricField = firstMetricOption.field || firstMetricOption.id;
|
||||
columnType = firstMetricOption.columnType;
|
||||
columnFilters = firstMetricOption.columnFilters;
|
||||
timeScale = firstMetricOption.timeScale;
|
||||
columnLabel = firstMetricOption.label;
|
||||
}
|
||||
|
||||
return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel };
|
||||
|
@ -89,9 +95,7 @@ export interface LayerConfig {
|
|||
reportDefinitions: URLReportDefinition;
|
||||
time: { to: string; from: string };
|
||||
indexPattern: IndexPattern;
|
||||
selectedMetricField: string;
|
||||
color: string;
|
||||
name: string;
|
||||
selectedMetricField?: string;
|
||||
}
|
||||
|
||||
export class LensAttributes {
|
||||
|
@ -467,15 +471,14 @@ export class LensAttributes {
|
|||
getLayerFilters(layerConfig: LayerConfig, totalLayers: number) {
|
||||
const {
|
||||
filters,
|
||||
time,
|
||||
time: { from, to },
|
||||
seriesConfig: { baseFilters: layerFilters, reportType },
|
||||
} = layerConfig;
|
||||
let baseFilters = '';
|
||||
|
||||
if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) {
|
||||
if (reportType !== 'kpi-over-time' && totalLayers > 1) {
|
||||
// for kpi over time, we don't need to add time range filters
|
||||
// since those are essentially plotted along the x-axis
|
||||
baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`;
|
||||
baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`;
|
||||
}
|
||||
|
||||
layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => {
|
||||
|
@ -531,11 +534,7 @@ export class LensAttributes {
|
|||
}
|
||||
|
||||
getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) {
|
||||
if (
|
||||
index === 0 ||
|
||||
mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI ||
|
||||
!layerConfig.time
|
||||
) {
|
||||
if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -547,14 +546,11 @@ export class LensAttributes {
|
|||
time: { from },
|
||||
} = layerConfig;
|
||||
|
||||
const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'));
|
||||
const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days');
|
||||
if (inDays > 1) {
|
||||
return inDays + 'd';
|
||||
}
|
||||
const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'));
|
||||
if (inHours === 0) {
|
||||
return null;
|
||||
}
|
||||
const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours');
|
||||
return inHours + 'h';
|
||||
}
|
||||
|
||||
|
@ -572,12 +568,6 @@ export class LensAttributes {
|
|||
|
||||
const { sourceField } = seriesConfig.xAxisColumn;
|
||||
|
||||
let label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label;
|
||||
|
||||
if (layerConfig.seriesConfig.reportType !== ReportTypes.CORE_WEB_VITAL && layerConfig.name) {
|
||||
label = layerConfig.name;
|
||||
}
|
||||
|
||||
layers[layerId] = {
|
||||
columnOrder: [
|
||||
`x-axis-column-${layerId}`,
|
||||
|
@ -591,7 +581,7 @@ export class LensAttributes {
|
|||
[`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId),
|
||||
[`y-axis-column-${layerId}`]: {
|
||||
...mainYAxis,
|
||||
label,
|
||||
label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label,
|
||||
filter: { query: columnFilter, language: 'kuery' },
|
||||
...(timeShift ? { timeShift } : {}),
|
||||
},
|
||||
|
@ -634,7 +624,7 @@ export class LensAttributes {
|
|||
seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType,
|
||||
palette: layerConfig.seriesConfig.palette,
|
||||
yConfig: layerConfig.seriesConfig.yConfig || [
|
||||
{ forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color },
|
||||
{ forAccessor: `y-axis-column-layer${index}` },
|
||||
],
|
||||
xAccessor: `x-axis-column-layer${index}`,
|
||||
...(layerConfig.breakdown &&
|
||||
|
@ -648,7 +638,7 @@ export class LensAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
getJSON(refresh?: number): TypedLensByValueInput['attributes'] {
|
||||
getJSON(): TypedLensByValueInput['attributes'] {
|
||||
const uniqueIndexPatternsIds = Array.from(
|
||||
new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)])
|
||||
);
|
||||
|
@ -657,7 +647,7 @@ export class LensAttributes {
|
|||
|
||||
return {
|
||||
title: 'Prefilled from exploratory view app',
|
||||
description: String(refresh),
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
...uniqueIndexPatternsIds.map((patternId) => ({
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants';
|
||||
import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants';
|
||||
import { buildPhraseFilter } from '../utils';
|
||||
import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames';
|
||||
import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels';
|
||||
|
@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields';
|
|||
|
||||
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
|
||||
return {
|
||||
reportType: ReportTypes.DEVICE_DISTRIBUTION,
|
||||
reportType: 'device-data-distribution',
|
||||
defaultSeriesType: 'bar',
|
||||
seriesTypes: ['bar', 'bar_horizontal'],
|
||||
xAxisColumn: {
|
||||
|
@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps)
|
|||
...MobileFields,
|
||||
[SERVICE_NAME]: MOBILE_APP,
|
||||
},
|
||||
definitionFields: [SERVICE_NAME],
|
||||
metricOptions: [
|
||||
{
|
||||
field: 'labels.device_id',
|
||||
id: 'labels.device_id',
|
||||
field: 'labels.device_id',
|
||||
label: NUMBER_OF_DEVICES,
|
||||
},
|
||||
],
|
||||
definitionFields: [SERVICE_NAME],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
|
||||
import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
|
||||
import { buildPhrasesFilter } from '../utils';
|
||||
import {
|
||||
METRIC_SYSTEM_CPU_USAGE,
|
||||
|
@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields';
|
|||
|
||||
export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
|
||||
return {
|
||||
reportType: ReportTypes.DISTRIBUTION,
|
||||
reportType: 'data-distribution',
|
||||
defaultSeriesType: 'bar',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
|
|
|
@ -6,13 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import {
|
||||
FieldLabels,
|
||||
OPERATION_COLUMN,
|
||||
RECORDS_FIELD,
|
||||
REPORT_METRIC_FIELD,
|
||||
ReportTypes,
|
||||
} from '../constants';
|
||||
import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
|
||||
import { buildPhrasesFilter } from '../utils';
|
||||
import {
|
||||
METRIC_SYSTEM_CPU_USAGE,
|
||||
|
@ -32,7 +26,7 @@ import { MobileFields } from './mobile_fields';
|
|||
|
||||
export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig {
|
||||
return {
|
||||
reportType: ReportTypes.KPI,
|
||||
reportType: 'kpi-over-time',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar', 'area'],
|
||||
xAxisColumn: {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers';
|
|||
import { getDefaultConfigs } from '../default_configs';
|
||||
import { LayerConfig, LensAttributes } from '../lens_attributes';
|
||||
import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv';
|
||||
import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
|
||||
import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
|
||||
|
||||
describe('Core web vital config test', function () {
|
||||
mockAppIndexPattern();
|
||||
|
@ -24,13 +24,10 @@ describe('Core web vital config test', function () {
|
|||
|
||||
const layerConfig: LayerConfig = {
|
||||
seriesConfig,
|
||||
color: 'green',
|
||||
name: 'test-series',
|
||||
breakdown: USER_AGENT_OS,
|
||||
indexPattern: mockIndexPattern,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
|
||||
selectedMetricField: LCP_FIELD,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
breakdown: USER_AGENT_OS,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
FieldLabels,
|
||||
FILTER_RECORDS,
|
||||
REPORT_METRIC_FIELD,
|
||||
ReportTypes,
|
||||
USE_BREAK_DOWN_COLUMN,
|
||||
} from '../constants';
|
||||
import { buildPhraseFilter } from '../utils';
|
||||
|
@ -39,7 +38,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon
|
|||
|
||||
return {
|
||||
defaultSeriesType: 'bar_horizontal_percentage_stacked',
|
||||
reportType: ReportTypes.CORE_WEB_VITAL,
|
||||
reportType: 'core-web-vitals',
|
||||
seriesTypes: ['bar_horizontal_percentage_stacked'],
|
||||
xAxisColumn: {
|
||||
sourceField: USE_BREAK_DOWN_COLUMN,
|
||||
|
@ -154,6 +153,5 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon
|
|||
{ color: statusPallete[1], forAccessor: 'y-axis-column-1' },
|
||||
{ color: statusPallete[2], forAccessor: 'y-axis-column-2' },
|
||||
],
|
||||
query: { query: 'transaction.type: "page-load"', language: 'kuery' },
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,12 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import {
|
||||
FieldLabels,
|
||||
REPORT_METRIC_FIELD,
|
||||
RECORDS_PERCENTAGE_FIELD,
|
||||
ReportTypes,
|
||||
} from '../constants';
|
||||
import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants';
|
||||
import { buildPhraseFilter } from '../utils';
|
||||
import {
|
||||
CLIENT_GEO_COUNTRY_NAME,
|
||||
|
@ -46,7 +41,7 @@ import {
|
|||
|
||||
export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig {
|
||||
return {
|
||||
reportType: ReportTypes.DISTRIBUTION,
|
||||
reportType: 'data-distribution',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: [],
|
||||
xAxisColumn: {
|
||||
|
|
|
@ -6,13 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import {
|
||||
FieldLabels,
|
||||
OPERATION_COLUMN,
|
||||
RECORDS_FIELD,
|
||||
REPORT_METRIC_FIELD,
|
||||
ReportTypes,
|
||||
} from '../constants';
|
||||
import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants';
|
||||
import { buildPhraseFilter } from '../utils';
|
||||
import {
|
||||
CLIENT_GEO_COUNTRY_NAME,
|
||||
|
@ -49,7 +43,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon
|
|||
return {
|
||||
defaultSeriesType: 'bar_stacked',
|
||||
seriesTypes: [],
|
||||
reportType: ReportTypes.KPI,
|
||||
reportType: 'kpi-over-time',
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
|
|
|
@ -6,12 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import {
|
||||
FieldLabels,
|
||||
REPORT_METRIC_FIELD,
|
||||
RECORDS_PERCENTAGE_FIELD,
|
||||
ReportTypes,
|
||||
} from '../constants';
|
||||
import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants';
|
||||
import {
|
||||
CLS_LABEL,
|
||||
DCL_LABEL,
|
||||
|
@ -35,7 +30,7 @@ export function getSyntheticsDistributionConfig({
|
|||
indexPattern,
|
||||
}: ConfigProps): SeriesConfig {
|
||||
return {
|
||||
reportType: ReportTypes.DISTRIBUTION,
|
||||
reportType: 'data-distribution',
|
||||
defaultSeriesType: series?.seriesType || 'line',
|
||||
seriesTypes: [],
|
||||
xAxisColumn: {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConfigProps, SeriesConfig } from '../../types';
|
||||
import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
|
||||
import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants';
|
||||
import {
|
||||
CLS_LABEL,
|
||||
DCL_LABEL,
|
||||
|
@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down';
|
|||
|
||||
export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig {
|
||||
return {
|
||||
reportType: ReportTypes.KPI,
|
||||
reportType: 'kpi-over-time',
|
||||
defaultSeriesType: 'bar_stacked',
|
||||
seriesTypes: [],
|
||||
xAxisColumn: {
|
||||
|
|
|
@ -5,18 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
export const sampleAttribute = {
|
||||
description: 'undefined',
|
||||
title: 'Prefilled from exploratory view app',
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
{
|
||||
id: 'apm-*',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'apm-*',
|
||||
name: 'indexpattern-datasource-layer-layer0',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
|
@ -34,23 +28,17 @@ export const sampleAttribute = {
|
|||
],
|
||||
columns: {
|
||||
'x-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [
|
||||
{
|
||||
from: 0,
|
||||
label: '',
|
||||
to: 1000,
|
||||
},
|
||||
],
|
||||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
label: 'Page load time',
|
||||
dataType: 'number',
|
||||
operationType: 'range',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: {
|
||||
type: 'histogram',
|
||||
ranges: [{ from: 0, to: 1000, label: '' }],
|
||||
maxBars: 'auto',
|
||||
},
|
||||
},
|
||||
'y-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
|
@ -60,7 +48,7 @@ export const sampleAttribute = {
|
|||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
isBucketed: false,
|
||||
label: 'test-series',
|
||||
label: 'Pages loaded',
|
||||
operationType: 'formula',
|
||||
params: {
|
||||
format: {
|
||||
|
@ -93,16 +81,16 @@ export const sampleAttribute = {
|
|||
'y-axis-column-layer0X1': {
|
||||
customLabel: true,
|
||||
dataType: 'number',
|
||||
filter: {
|
||||
language: 'kuery',
|
||||
query:
|
||||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
isBucketed: false,
|
||||
label: 'Part of count() / overall_sum(count())',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: {
|
||||
language: 'kuery',
|
||||
query:
|
||||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
},
|
||||
'y-axis-column-layer0X2': {
|
||||
customLabel: true,
|
||||
|
@ -153,51 +141,26 @@ export const sampleAttribute = {
|
|||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: 'transaction.duration.us < 60000000',
|
||||
},
|
||||
visualization: {
|
||||
axisTitlesVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
curveType: 'CURVE_MONOTONE_X',
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'Linear',
|
||||
gridlinesVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
curveType: 'CURVE_MONOTONE_X',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column-layer0'],
|
||||
layerId: 'layer0',
|
||||
seriesType: 'line',
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
yConfig: [
|
||||
{
|
||||
color: 'green',
|
||||
forAccessor: 'y-axis-column-layer0',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
isVisible: true,
|
||||
position: 'right',
|
||||
},
|
||||
preferredSeriesType: 'line',
|
||||
tickLabelsVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
valueLabels: 'hide',
|
||||
},
|
||||
query: { query: 'transaction.duration.us < 60000000', language: 'kuery' },
|
||||
filters: [],
|
||||
},
|
||||
title: 'Prefilled from exploratory view app',
|
||||
visualizationType: 'lnsXY',
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
export const sampleAttributeCoreWebVital = {
|
||||
description: 'undefined',
|
||||
description: '',
|
||||
references: [
|
||||
{
|
||||
id: 'apm-*',
|
||||
|
@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = {
|
|||
filters: [],
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: 'transaction.type: "page-load"',
|
||||
query: '',
|
||||
},
|
||||
visualization: {
|
||||
axisTitlesVisibilitySettings: {
|
||||
|
|
|
@ -5,18 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
export const sampleAttributeKpi = {
|
||||
description: 'undefined',
|
||||
title: 'Prefilled from exploratory view app',
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
{
|
||||
id: 'apm-*',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'apm-*',
|
||||
name: 'indexpattern-datasource-layer-layer0',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
|
@ -26,27 +20,25 @@ export const sampleAttributeKpi = {
|
|||
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
|
||||
columns: {
|
||||
'x-axis-column-layer0': {
|
||||
sourceField: '@timestamp',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
label: '@timestamp',
|
||||
operationType: 'date_histogram',
|
||||
params: {
|
||||
interval: 'auto',
|
||||
},
|
||||
params: { interval: 'auto' },
|
||||
scale: 'interval',
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
'y-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
filter: {
|
||||
language: 'kuery',
|
||||
query: 'transaction.type: page-load and processor.event: transaction',
|
||||
},
|
||||
isBucketed: false,
|
||||
label: 'test-series',
|
||||
label: 'Page views',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: {
|
||||
query: 'transaction.type: page-load and processor.event: transaction',
|
||||
language: 'kuery',
|
||||
},
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
|
@ -54,51 +46,26 @@ export const sampleAttributeKpi = {
|
|||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
},
|
||||
visualization: {
|
||||
axisTitlesVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
curveType: 'CURVE_MONOTONE_X',
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'Linear',
|
||||
gridlinesVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
curveType: 'CURVE_MONOTONE_X',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column-layer0'],
|
||||
layerId: 'layer0',
|
||||
seriesType: 'line',
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
yConfig: [
|
||||
{
|
||||
color: 'green',
|
||||
forAccessor: 'y-axis-column-layer0',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
isVisible: true,
|
||||
position: 'right',
|
||||
},
|
||||
preferredSeriesType: 'line',
|
||||
tickLabelsVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
valueLabels: 'hide',
|
||||
},
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [],
|
||||
},
|
||||
title: 'Prefilled from exploratory view app',
|
||||
visualizationType: 'lnsXY',
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import type { ReportViewType, SeriesUrl, UrlFilter } from '../types';
|
||||
import type { SeriesUrl, UrlFilter } from '../types';
|
||||
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
|
||||
import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public';
|
||||
|
@ -16,43 +16,40 @@ export function convertToShortUrl(series: SeriesUrl) {
|
|||
const {
|
||||
operationType,
|
||||
seriesType,
|
||||
reportType,
|
||||
breakdown,
|
||||
filters,
|
||||
reportDefinitions,
|
||||
dataType,
|
||||
selectedMetricField,
|
||||
hidden,
|
||||
name,
|
||||
color,
|
||||
...restSeries
|
||||
} = series;
|
||||
|
||||
return {
|
||||
[URL_KEYS.OPERATION_TYPE]: operationType,
|
||||
[URL_KEYS.REPORT_TYPE]: reportType,
|
||||
[URL_KEYS.SERIES_TYPE]: seriesType,
|
||||
[URL_KEYS.BREAK_DOWN]: breakdown,
|
||||
[URL_KEYS.FILTERS]: filters,
|
||||
[URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions,
|
||||
[URL_KEYS.DATA_TYPE]: dataType,
|
||||
[URL_KEYS.SELECTED_METRIC]: selectedMetricField,
|
||||
[URL_KEYS.HIDDEN]: hidden,
|
||||
[URL_KEYS.NAME]: name,
|
||||
[URL_KEYS.COLOR]: color,
|
||||
...restSeries,
|
||||
};
|
||||
}
|
||||
|
||||
export function createExploratoryViewUrl(
|
||||
{ reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries },
|
||||
baseHref = ''
|
||||
) {
|
||||
const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series));
|
||||
export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
|
||||
const allSeriesIds = Object.keys(allSeries);
|
||||
|
||||
const allShortSeries: AllShortSeries = {};
|
||||
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]);
|
||||
});
|
||||
|
||||
return (
|
||||
baseHref +
|
||||
`/app/observability/exploratory-view/configure#?reportType=${reportType}&sr=${rison.encode(
|
||||
(allShortSeries as unknown) as RisonValue
|
||||
)}`
|
||||
`/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,6 @@ import { render, mockCore, mockAppIndexPattern } from './rtl_helpers';
|
|||
import { ExploratoryView } from './exploratory_view';
|
||||
import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils';
|
||||
import * as obsvInd from './utils/observability_index_patterns';
|
||||
import * as pluginHook from '../../../hooks/use_plugin_context';
|
||||
|
||||
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
|
||||
appMountParameters: {
|
||||
setHeaderActionMenu: jest.fn(),
|
||||
},
|
||||
} as any);
|
||||
|
||||
describe('ExploratoryView', () => {
|
||||
mockAppIndexPattern();
|
||||
|
@ -48,18 +41,29 @@ describe('ExploratoryView', () => {
|
|||
it('renders exploratory view', async () => {
|
||||
render(<ExploratoryView />);
|
||||
|
||||
expect(await screen.findByText(/Preview/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Configure series/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Refresh/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: /Performance Distribution/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders lens component when there is series', async () => {
|
||||
render(<ExploratoryView />);
|
||||
const initSeries = {
|
||||
data: {
|
||||
'ux-series': {
|
||||
isNew: true,
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'data-distribution' as const,
|
||||
breakdown: 'user_agent .name',
|
||||
reportDefinitions: { 'service.name': ['elastic-co'] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(<ExploratoryView />, { initSeries });
|
||||
|
||||
expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument();
|
||||
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui';
|
||||
import { EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../plugin';
|
||||
import { ExploratoryViewHeader } from './header/header';
|
||||
|
@ -18,15 +16,40 @@ import { useSeriesStorage } from './hooks/use_series_storage';
|
|||
import { useLensAttributes } from './hooks/use_lens_attributes';
|
||||
import { TypedLensByValueInput } from '../../../../../lens/public';
|
||||
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
|
||||
import { SeriesViews } from './views/series_views';
|
||||
import { SeriesBuilder } from './series_builder/series_builder';
|
||||
import { SeriesUrl } from './types';
|
||||
import { LensEmbeddable } from './lens_embeddable';
|
||||
import { EmptyView } from './components/empty_view';
|
||||
|
||||
export type PanelId = 'seriesPanel' | 'chartPanel';
|
||||
export const combineTimeRanges = (
|
||||
allSeries: Record<string, SeriesUrl>,
|
||||
firstSeries?: SeriesUrl
|
||||
) => {
|
||||
let to: string = '';
|
||||
let from: string = '';
|
||||
if (firstSeries?.reportType === 'kpi-over-time') {
|
||||
return firstSeries.time;
|
||||
}
|
||||
Object.values(allSeries ?? {}).forEach((series) => {
|
||||
if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) {
|
||||
const seriesTo = new Date(series.time.to);
|
||||
const seriesFrom = new Date(series.time.from);
|
||||
if (!to || seriesTo > new Date(to)) {
|
||||
to = series.time.to;
|
||||
}
|
||||
if (!from || seriesFrom < new Date(from)) {
|
||||
from = series.time.from;
|
||||
}
|
||||
}
|
||||
});
|
||||
return { to, from };
|
||||
};
|
||||
|
||||
export function ExploratoryView({
|
||||
saveAttributes,
|
||||
multiSeries,
|
||||
}: {
|
||||
multiSeries?: boolean;
|
||||
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
|
||||
}) {
|
||||
const {
|
||||
|
@ -46,19 +69,20 @@ export function ExploratoryView({
|
|||
|
||||
const { loadIndexPattern, loading } = useAppIndexPatternContext();
|
||||
|
||||
const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage();
|
||||
const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage();
|
||||
|
||||
const lensAttributesT = useLensAttributes();
|
||||
|
||||
const setHeightOffset = () => {
|
||||
if (seriesBuilderRef?.current && wrapperRef.current) {
|
||||
const headerOffset = wrapperRef.current.getBoundingClientRect().top;
|
||||
setHeight(`calc(100vh - ${headerOffset + 40}px)`);
|
||||
const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height;
|
||||
setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
allSeries.forEach((seriesT) => {
|
||||
Object.values(allSeries).forEach((seriesT) => {
|
||||
loadIndexPattern({
|
||||
dataType: seriesT.dataType,
|
||||
});
|
||||
|
@ -72,104 +96,38 @@ export function ExploratoryView({
|
|||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]);
|
||||
}, [JSON.stringify(lensAttributesT ?? {})]);
|
||||
|
||||
useEffect(() => {
|
||||
setHeightOffset();
|
||||
});
|
||||
|
||||
const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>();
|
||||
|
||||
const [hiddenPanel, setHiddenPanel] = useState('');
|
||||
|
||||
const isPreview = !!useRouteMatch('/exploratory-view/preview');
|
||||
|
||||
const onCollapse = (panelId: string) => {
|
||||
setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId));
|
||||
};
|
||||
|
||||
const onChange = (panelId: PanelId) => {
|
||||
onCollapse(panelId);
|
||||
if (collapseFn.current) {
|
||||
collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{lens ? (
|
||||
<>
|
||||
<ExploratoryViewHeader
|
||||
lensAttributes={lensAttributes}
|
||||
seriesId={0}
|
||||
lastUpdated={lastUpdated}
|
||||
/>
|
||||
<ExploratoryViewHeader lensAttributes={lensAttributes} seriesId={firstSeriesId} />
|
||||
<LensWrapper ref={wrapperRef} height={height}>
|
||||
<EuiResizableContainer
|
||||
style={{ height: '100%' }}
|
||||
direction="vertical"
|
||||
onToggleCollapsed={onCollapse}
|
||||
>
|
||||
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
|
||||
collapseFn.current = (id, direction) => togglePanel?.(id, { direction });
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
initialSize={isPreview ? 70 : 40}
|
||||
minSize={isPreview ? '70%' : '30%'}
|
||||
mode={isPreview ? 'main' : 'collapsible'}
|
||||
id="chartPanel"
|
||||
>
|
||||
{lensAttributes ? (
|
||||
<LensEmbeddable
|
||||
setLastUpdated={setLastUpdated}
|
||||
lensAttributes={lensAttributes}
|
||||
/>
|
||||
) : (
|
||||
<EmptyView series={firstSeries} loading={loading} reportType={reportType} />
|
||||
)}
|
||||
</EuiResizablePanel>
|
||||
<EuiResizableButton />
|
||||
<EuiResizablePanel
|
||||
initialSize={isPreview ? 30 : 60}
|
||||
minSize="10%"
|
||||
mode={isPreview ? 'collapsible' : 'main'}
|
||||
id="seriesPanel"
|
||||
>
|
||||
{!isPreview &&
|
||||
(hiddenPanel === 'chartPanel' ? (
|
||||
<ShowChart onClick={() => onChange('chartPanel')} iconType="arrowDown">
|
||||
{SHOW_CHART_LABEL}
|
||||
</ShowChart>
|
||||
) : (
|
||||
<HideChart
|
||||
onClick={() => onChange('chartPanel')}
|
||||
iconType="arrowUp"
|
||||
color="text"
|
||||
>
|
||||
{HIDE_CHART_LABEL}
|
||||
</HideChart>
|
||||
))}
|
||||
<SeriesViews
|
||||
seriesBuilderRef={seriesBuilderRef}
|
||||
onSeriesPanelCollapse={onChange}
|
||||
/>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</EuiResizableContainer>
|
||||
{hiddenPanel === 'seriesPanel' && (
|
||||
<ShowPreview onClick={() => onChange('seriesPanel')} iconType="arrowUp">
|
||||
{PREVIEW_LABEL}
|
||||
</ShowPreview>
|
||||
{lensAttributes ? (
|
||||
<LensEmbeddable setLastUpdated={setLastUpdated} lensAttributes={lensAttributes} />
|
||||
) : (
|
||||
<EmptyView series={firstSeries} loading={loading} height={height} />
|
||||
)}
|
||||
</LensWrapper>
|
||||
<SeriesBuilder
|
||||
seriesBuilderRef={seriesBuilderRef}
|
||||
lastUpdated={lastUpdated}
|
||||
multiSeries={multiSeries}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EuiTitle>
|
||||
<h2>{LENS_NOT_AVAILABLE}</h2>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', {
|
||||
defaultMessage:
|
||||
'Lens app is not available, please enable Lens to use exploratory view.',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
)}
|
||||
</Wrapper>
|
||||
|
@ -189,39 +147,4 @@ const Wrapper = styled(EuiPanel)`
|
|||
margin: 0 auto;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ShowPreview = styled(EuiButtonEmpty)`
|
||||
position: absolute;
|
||||
bottom: 34px;
|
||||
`;
|
||||
const HideChart = styled(EuiButtonEmpty)`
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
right: 50px;
|
||||
`;
|
||||
const ShowChart = styled(EuiButtonEmpty)`
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 50px;
|
||||
`;
|
||||
|
||||
const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', {
|
||||
defaultMessage: 'Hide chart',
|
||||
});
|
||||
|
||||
const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', {
|
||||
defaultMessage: 'Show chart',
|
||||
});
|
||||
|
||||
const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', {
|
||||
defaultMessage: 'Preview',
|
||||
});
|
||||
|
||||
const LENS_NOT_AVAILABLE = i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.lensDisabled',
|
||||
{
|
||||
defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,22 +8,51 @@
|
|||
import React from 'react';
|
||||
import { render } from '../rtl_helpers';
|
||||
import { ExploratoryViewHeader } from './header';
|
||||
import * as pluginHook from '../../../../hooks/use_plugin_context';
|
||||
|
||||
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
|
||||
appMountParameters: {
|
||||
setHeaderActionMenu: jest.fn(),
|
||||
},
|
||||
} as any);
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
|
||||
describe('ExploratoryViewHeader', function () {
|
||||
it('should render properly', function () {
|
||||
const { getByText } = render(
|
||||
<ExploratoryViewHeader
|
||||
seriesId={0}
|
||||
seriesId={'dummy-series'}
|
||||
lensAttributes={{ title: 'Performance distribution' } as any}
|
||||
/>
|
||||
);
|
||||
getByText('Refresh');
|
||||
getByText('Open in Lens');
|
||||
});
|
||||
|
||||
it('should be able to click open in lens', function () {
|
||||
const initSeries = {
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { getByText, core } = render(
|
||||
<ExploratoryViewHeader
|
||||
seriesId={'dummy-series'}
|
||||
lensAttributes={{ title: 'Performance distribution' } as any}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
fireEvent.click(getByText('Open in Lens'));
|
||||
|
||||
expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
|
||||
expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
|
||||
{
|
||||
attributes: { title: 'Performance distribution' },
|
||||
id: '',
|
||||
timeRange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,37 +5,43 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { DataViewLabels } from '../configurations/constants';
|
||||
import { ObservabilityAppServices } from '../../../../application/types';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { LastUpdated } from './last_updated';
|
||||
import { combineTimeRanges } from '../lens_embeddable';
|
||||
import { ExpViewActionMenu } from '../components/action_menu';
|
||||
import { combineTimeRanges } from '../exploratory_view';
|
||||
|
||||
interface Props {
|
||||
seriesId?: number;
|
||||
lastUpdated?: number;
|
||||
seriesId: string;
|
||||
lensAttributes: TypedLensByValueInput['attributes'] | null;
|
||||
}
|
||||
|
||||
export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) {
|
||||
const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage();
|
||||
export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
||||
const kServices = useKibana<ObservabilityAppServices>().services;
|
||||
|
||||
const series = seriesId ? getSeries(seriesId) : undefined;
|
||||
const { lens } = kServices;
|
||||
|
||||
const timeRange = combineTimeRanges(reportType, allSeries, series);
|
||||
const { getSeries, allSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const [isSaveOpen, setIsSaveOpen] = useState(false);
|
||||
|
||||
const LensSaveModalComponent = lens.SaveModalComponent;
|
||||
|
||||
const timeRange = combineTimeRanges(allSeries, series);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExpViewActionMenu timeRange={timeRange} lensAttributes={lensAttributes} />
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h2>
|
||||
{DataViewLabels[reportType] ??
|
||||
{DataViewLabels[series.reportType] ??
|
||||
i18n.translate('xpack.observability.expView.heading.label', {
|
||||
defaultMessage: 'Analyze data',
|
||||
})}{' '}
|
||||
|
@ -51,18 +57,53 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }:
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LastUpdated lastUpdated={lastUpdated} />
|
||||
<EuiButton
|
||||
iconType="lensApp"
|
||||
fullWidth={false}
|
||||
isDisabled={!lens.canUseEditor() || lensAttributes === null}
|
||||
onClick={() => {
|
||||
if (lensAttributes) {
|
||||
lens.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange,
|
||||
attributes: lensAttributes,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.heading.openInLens', {
|
||||
defaultMessage: 'Open in Lens',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton iconType="refresh" onClick={() => setLastRefresh(Date.now())}>
|
||||
{REFRESH_LABEL}
|
||||
<EuiButton
|
||||
iconType="save"
|
||||
fullWidth={false}
|
||||
isDisabled={!lens.canUseEditor() || lensAttributes === null}
|
||||
onClick={() => {
|
||||
if (lensAttributes) {
|
||||
setIsSaveOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{isSaveOpen && lensAttributes && (
|
||||
<LensSaveModalComponent
|
||||
initialInput={(lensAttributes as unknown) as LensEmbeddableInput}
|
||||
onClose={() => setIsSaveOpen(false)}
|
||||
onSave={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', {
|
||||
defaultMessage: 'Refresh',
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ interface ProviderProps {
|
|||
}
|
||||
|
||||
type HasAppDataState = Record<AppDataType, boolean | null>;
|
||||
export type IndexPatternState = Record<AppDataType, IndexPattern>;
|
||||
type IndexPatternState = Record<AppDataType, IndexPattern>;
|
||||
type LoadingState = Record<AppDataType, boolean>;
|
||||
|
||||
export function IndexPatternContextProvider({ children }: ProviderProps) {
|
||||
|
|
|
@ -1,92 +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 { useCallback, useEffect, useState } from 'react';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { SeriesConfig, SeriesUrl } from '../types';
|
||||
import { useAppIndexPatternContext } from './use_app_index_pattern';
|
||||
import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils';
|
||||
import { getFiltersFromDefs } from './use_lens_attributes';
|
||||
import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
|
||||
|
||||
interface UseDiscoverLink {
|
||||
seriesConfig: SeriesConfig;
|
||||
series: SeriesUrl;
|
||||
}
|
||||
|
||||
export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => {
|
||||
const kServices = useKibana().services;
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
} = kServices;
|
||||
|
||||
const { indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
const urlGenerator = kServices.discover?.urlGenerator;
|
||||
const [discoverUrl, setDiscoverUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const indexPattern = indexPatterns?.[series.dataType];
|
||||
|
||||
const definitions = series.reportDefinitions ?? {};
|
||||
const filters = [...(seriesConfig?.baseFilters ?? [])];
|
||||
|
||||
const definitionFilters = getFiltersFromDefs(definitions);
|
||||
|
||||
definitionFilters.forEach(({ field, values = [] }) => {
|
||||
if (values.length > 1) {
|
||||
filters.push(buildPhrasesFilter(field, values, indexPattern)[0]);
|
||||
} else {
|
||||
filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const selectedMetricField = series.selectedMetricField;
|
||||
|
||||
if (
|
||||
selectedMetricField &&
|
||||
selectedMetricField !== RECORDS_FIELD &&
|
||||
selectedMetricField !== RECORDS_PERCENTAGE_FIELD
|
||||
) {
|
||||
filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]);
|
||||
}
|
||||
|
||||
const getDiscoverUrl = async () => {
|
||||
if (!urlGenerator?.createUrl) return;
|
||||
|
||||
const newUrl = await urlGenerator.createUrl({
|
||||
filters,
|
||||
indexPatternId: indexPattern?.id,
|
||||
});
|
||||
setDiscoverUrl(newUrl);
|
||||
};
|
||||
getDiscoverUrl();
|
||||
}, [
|
||||
indexPatterns,
|
||||
series.dataType,
|
||||
series.reportDefinitions,
|
||||
series.selectedMetricField,
|
||||
seriesConfig?.baseFilters,
|
||||
urlGenerator,
|
||||
]);
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (discoverUrl) {
|
||||
event.preventDefault();
|
||||
|
||||
return navigateToUrl(discoverUrl);
|
||||
}
|
||||
},
|
||||
[discoverUrl, navigateToUrl]
|
||||
);
|
||||
|
||||
return {
|
||||
href: discoverUrl,
|
||||
onClick,
|
||||
};
|
||||
};
|
|
@ -9,18 +9,12 @@ import { useMemo } from 'react';
|
|||
import { isEmpty } from 'lodash';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
|
||||
import {
|
||||
AllSeries,
|
||||
allSeriesKey,
|
||||
convertAllShortSeries,
|
||||
useSeriesStorage,
|
||||
} from './use_series_storage';
|
||||
import { useSeriesStorage } from './use_series_storage';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
|
||||
import { SeriesUrl, UrlFilter } from '../types';
|
||||
import { useAppIndexPatternContext } from './use_app_index_pattern';
|
||||
import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
|
||||
export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => {
|
||||
return Object.entries(reportDefinitions ?? {})
|
||||
|
@ -34,56 +28,41 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio
|
|||
};
|
||||
|
||||
export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
|
||||
const { storage, autoApply, allSeries, lastRefresh, reportType } = useSeriesStorage();
|
||||
const { allSeriesIds, allSeries } = useSeriesStorage();
|
||||
|
||||
const { indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return useMemo(() => {
|
||||
if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) {
|
||||
if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allSeriesT: AllSeries = autoApply
|
||||
? allSeries
|
||||
: convertAllShortSeries(storage.get(allSeriesKey) ?? []);
|
||||
|
||||
const layerConfigs: LayerConfig[] = [];
|
||||
|
||||
allSeriesT.forEach((series, seriesIndex) => {
|
||||
const indexPattern = indexPatterns?.[series?.dataType];
|
||||
|
||||
if (
|
||||
indexPattern &&
|
||||
!isEmpty(series.reportDefinitions) &&
|
||||
!series.hidden &&
|
||||
series.selectedMetricField
|
||||
) {
|
||||
allSeriesIds.forEach((seriesIdT) => {
|
||||
const seriesT = allSeries[seriesIdT];
|
||||
const indexPattern = indexPatterns?.[seriesT?.dataType];
|
||||
if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) {
|
||||
const seriesConfig = getDefaultConfigs({
|
||||
reportType,
|
||||
reportType: seriesT.reportType,
|
||||
dataType: seriesT.dataType,
|
||||
indexPattern,
|
||||
dataType: series.dataType,
|
||||
});
|
||||
|
||||
const filters: UrlFilter[] = (series.filters ?? []).concat(
|
||||
getFiltersFromDefs(series.reportDefinitions)
|
||||
const filters: UrlFilter[] = (seriesT.filters ?? []).concat(
|
||||
getFiltersFromDefs(seriesT.reportDefinitions)
|
||||
);
|
||||
|
||||
const color = `euiColorVis${seriesIndex}`;
|
||||
|
||||
layerConfigs.push({
|
||||
filters,
|
||||
indexPattern,
|
||||
seriesConfig,
|
||||
time: series.time,
|
||||
name: series.name,
|
||||
breakdown: series.breakdown,
|
||||
seriesType: series.seriesType,
|
||||
operationType: series.operationType,
|
||||
reportDefinitions: series.reportDefinitions ?? {},
|
||||
selectedMetricField: series.selectedMetricField,
|
||||
color: series.color ?? ((theme.eui as unknown) as Record<string, string>)[color],
|
||||
time: seriesT.time,
|
||||
breakdown: seriesT.breakdown,
|
||||
seriesType: seriesT.seriesType,
|
||||
operationType: seriesT.operationType,
|
||||
reportDefinitions: seriesT.reportDefinitions ?? {},
|
||||
selectedMetricField: seriesT.selectedMetricField,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -94,6 +73,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null
|
|||
|
||||
const lensAttributes = new LensAttributes(layerConfigs);
|
||||
|
||||
return lensAttributes.getJSON(lastRefresh);
|
||||
}, [indexPatterns, allSeries, reportType, autoApply, storage, theme, lastRefresh]);
|
||||
return lensAttributes.getJSON();
|
||||
}, [indexPatterns, allSeriesIds, allSeries]);
|
||||
};
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
*/
|
||||
|
||||
import { useSeriesStorage } from './use_series_storage';
|
||||
import { SeriesUrl, UrlFilter } from '../types';
|
||||
import { UrlFilter } from '../types';
|
||||
|
||||
export interface UpdateFilter {
|
||||
field: string;
|
||||
value: string | string[];
|
||||
value: string;
|
||||
negate?: boolean;
|
||||
}
|
||||
|
||||
export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const filters = series.filters ?? [];
|
||||
|
||||
|
@ -24,14 +26,10 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie
|
|||
.map((filter) => {
|
||||
if (filter.field === field) {
|
||||
if (negate) {
|
||||
const notValuesN = filter.notValues?.filter((val) =>
|
||||
value instanceof Array ? !value.includes(val) : val !== value
|
||||
);
|
||||
const notValuesN = filter.notValues?.filter((val) => val !== value);
|
||||
return { ...filter, notValues: notValuesN };
|
||||
} else {
|
||||
const valuesN = filter.values?.filter((val) =>
|
||||
value instanceof Array ? !value.includes(val) : val !== value
|
||||
);
|
||||
const valuesN = filter.values?.filter((val) => val !== value);
|
||||
return { ...filter, values: valuesN };
|
||||
}
|
||||
}
|
||||
|
@ -45,9 +43,9 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie
|
|||
const addFilter = ({ field, value, negate }: UpdateFilter) => {
|
||||
const currFilter: UrlFilter = { field };
|
||||
if (negate) {
|
||||
currFilter.notValues = value instanceof Array ? value : [value];
|
||||
currFilter.notValues = [value];
|
||||
} else {
|
||||
currFilter.values = value instanceof Array ? value : [value];
|
||||
currFilter.values = [value];
|
||||
}
|
||||
if (filters.length === 0) {
|
||||
setSeries(seriesId, { ...series, filters: [currFilter] });
|
||||
|
@ -67,26 +65,13 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie
|
|||
const currNotValues = currFilter.notValues ?? [];
|
||||
const currValues = currFilter.values ?? [];
|
||||
|
||||
const notValues = currNotValues.filter((val) =>
|
||||
value instanceof Array ? !value.includes(val) : val !== value
|
||||
);
|
||||
|
||||
const values = currValues.filter((val) =>
|
||||
value instanceof Array ? !value.includes(val) : val !== value
|
||||
);
|
||||
const notValues = currNotValues.filter((val) => val !== value);
|
||||
const values = currValues.filter((val) => val !== value);
|
||||
|
||||
if (negate) {
|
||||
if (value instanceof Array) {
|
||||
notValues.push(...value);
|
||||
} else {
|
||||
notValues.push(value);
|
||||
}
|
||||
notValues.push(value);
|
||||
} else {
|
||||
if (value instanceof Array) {
|
||||
values.push(...value);
|
||||
} else {
|
||||
values.push(value);
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
currFilter.notValues = notValues.length > 0 ? notValues : undefined;
|
||||
|
|
|
@ -6,39 +6,37 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Router } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage';
|
||||
import { getHistoryFromUrl } from '../rtl_helpers';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
const mockSingleSeries = [
|
||||
{
|
||||
name: 'performance-distribution',
|
||||
const mockSingleSeries = {
|
||||
'performance-distribution': {
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const mockMultipleSeries = [
|
||||
{
|
||||
name: 'performance-distribution',
|
||||
const mockMultipleSeries = {
|
||||
'performance-distribution': {
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
{
|
||||
name: 'kpi-over-time',
|
||||
'kpi-over-time': {
|
||||
reportType: 'kpi-over-time',
|
||||
dataType: 'synthetics',
|
||||
breakdown: 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
describe('userSeriesStorage', function () {
|
||||
describe('userSeries', function () {
|
||||
function setupTestComponent(seriesData: any) {
|
||||
const setData = jest.fn();
|
||||
|
||||
function TestComponent() {
|
||||
const data = useSeriesStorage();
|
||||
|
||||
|
@ -50,20 +48,11 @@ describe('userSeriesStorage', function () {
|
|||
}
|
||||
|
||||
render(
|
||||
<Router history={getHistoryFromUrl('/app/observability/exploratory-view/configure')}>
|
||||
<Route path={'/app/observability/exploratory-view/:mode'}>
|
||||
<UrlStorageContextProvider
|
||||
storage={{
|
||||
get: jest
|
||||
.fn()
|
||||
.mockImplementation((key: string) => (key === 'sr' ? seriesData : null)),
|
||||
set: jest.fn(),
|
||||
}}
|
||||
>
|
||||
<TestComponent />
|
||||
</UrlStorageContextProvider>
|
||||
</Route>
|
||||
</Router>
|
||||
<UrlStorageContextProvider
|
||||
storage={{ get: jest.fn().mockReturnValue(seriesData), set: jest.fn() }}
|
||||
>
|
||||
<TestComponent />
|
||||
</UrlStorageContextProvider>
|
||||
);
|
||||
|
||||
return setData;
|
||||
|
@ -74,20 +63,22 @@ describe('userSeriesStorage', function () {
|
|||
expect(setData).toHaveBeenCalledTimes(2);
|
||||
expect(setData).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
allSeries: [
|
||||
{
|
||||
name: 'performance-distribution',
|
||||
dataType: 'ux',
|
||||
allSeries: {
|
||||
'performance-distribution': {
|
||||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
],
|
||||
},
|
||||
allSeriesIds: ['performance-distribution'],
|
||||
firstSeries: {
|
||||
name: 'performance-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
firstSeriesId: 'performance-distribution',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -98,38 +89,42 @@ describe('userSeriesStorage', function () {
|
|||
expect(setData).toHaveBeenCalledTimes(2);
|
||||
expect(setData).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
allSeries: [
|
||||
{
|
||||
name: 'performance-distribution',
|
||||
dataType: 'ux',
|
||||
allSeries: {
|
||||
'performance-distribution': {
|
||||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
{
|
||||
name: 'kpi-over-time',
|
||||
'kpi-over-time': {
|
||||
reportType: 'kpi-over-time',
|
||||
dataType: 'synthetics',
|
||||
breakdown: 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
],
|
||||
},
|
||||
allSeriesIds: ['performance-distribution', 'kpi-over-time'],
|
||||
firstSeries: {
|
||||
name: 'performance-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
firstSeriesId: 'performance-distribution',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return expected result when there are no series', function () {
|
||||
const setData = setupTestComponent([]);
|
||||
const setData = setupTestComponent({});
|
||||
|
||||
expect(setData).toHaveBeenCalledTimes(1);
|
||||
expect(setData).toHaveBeenCalledTimes(2);
|
||||
expect(setData).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
allSeries: [],
|
||||
allSeries: {},
|
||||
allSeriesIds: [],
|
||||
firstSeries: undefined,
|
||||
firstSeriesId: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import {
|
||||
IKbnUrlStateStorage,
|
||||
ISessionStorageStateStorage,
|
||||
|
@ -23,19 +22,13 @@ import { OperationType, SeriesType } from '../../../../../../lens/public';
|
|||
import { URL_KEYS } from '../configurations/constants/url_constants';
|
||||
|
||||
export interface SeriesContextValue {
|
||||
firstSeries?: SeriesUrl;
|
||||
autoApply: boolean;
|
||||
lastRefresh: number;
|
||||
setLastRefresh: (val: number) => void;
|
||||
setAutoApply: (val: boolean) => void;
|
||||
applyChanges: () => void;
|
||||
firstSeries: SeriesUrl;
|
||||
firstSeriesId: string;
|
||||
allSeriesIds: string[];
|
||||
allSeries: AllSeries;
|
||||
setSeries: (seriesIndex: number, newValue: SeriesUrl) => void;
|
||||
getSeries: (seriesIndex: number) => SeriesUrl | undefined;
|
||||
removeSeries: (seriesIndex: number) => void;
|
||||
setReportType: (reportType: string) => void;
|
||||
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
|
||||
reportType: ReportViewType;
|
||||
setSeries: (seriesIdN: string, newValue: SeriesUrl) => void;
|
||||
getSeries: (seriesId: string) => SeriesUrl;
|
||||
removeSeries: (seriesId: string) => void;
|
||||
}
|
||||
export const UrlStorageContext = createContext<SeriesContextValue>({} as SeriesContextValue);
|
||||
|
||||
|
@ -43,112 +36,72 @@ interface ProviderProps {
|
|||
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
|
||||
}
|
||||
|
||||
export function convertAllShortSeries(allShortSeries: AllShortSeries) {
|
||||
return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries));
|
||||
}
|
||||
function convertAllShortSeries(allShortSeries: AllShortSeries) {
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
const allSeriesN: AllSeries = {};
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
|
||||
});
|
||||
|
||||
export const allSeriesKey = 'sr';
|
||||
const autoApplyKey = 'autoApply';
|
||||
const reportTypeKey = 'reportType';
|
||||
return allSeriesN;
|
||||
}
|
||||
|
||||
export function UrlStorageContextProvider({
|
||||
children,
|
||||
storage,
|
||||
}: ProviderProps & { children: JSX.Element }) {
|
||||
const allSeriesKey = 'sr';
|
||||
|
||||
const [allShortSeries, setAllShortSeries] = useState<AllShortSeries>(
|
||||
() => storage.get(allSeriesKey) ?? {}
|
||||
);
|
||||
const [allSeries, setAllSeries] = useState<AllSeries>(() =>
|
||||
convertAllShortSeries(storage.get(allSeriesKey) ?? [])
|
||||
convertAllShortSeries(storage.get(allSeriesKey) ?? {})
|
||||
);
|
||||
|
||||
const [autoApply, setAutoApply] = useState<boolean>(() => storage.get(autoApplyKey) ?? true);
|
||||
const [lastRefresh, setLastRefresh] = useState<number>(() => Date.now());
|
||||
|
||||
const [reportType, setReportType] = useState<string>(
|
||||
() => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? ''
|
||||
);
|
||||
|
||||
const [firstSeriesId, setFirstSeriesId] = useState('');
|
||||
const [firstSeries, setFirstSeries] = useState<SeriesUrl>();
|
||||
const isPreview = !!useRouteMatch('/exploratory-view/preview');
|
||||
|
||||
useEffect(() => {
|
||||
const allShortSeries = allSeries.map((series) => convertToShortUrl(series));
|
||||
|
||||
const firstSeriesT = allSeries?.[0];
|
||||
|
||||
setFirstSeries(firstSeriesT);
|
||||
|
||||
if (autoApply) {
|
||||
(storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
|
||||
}
|
||||
}, [allSeries, autoApply, storage]);
|
||||
|
||||
useEffect(() => {
|
||||
// needed for tab change
|
||||
const allShortSeries = allSeries.map((series) => convertToShortUrl(series));
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {});
|
||||
|
||||
setAllSeries(allSeriesN);
|
||||
setFirstSeriesId(allSeriesIds?.[0]);
|
||||
setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]);
|
||||
(storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
|
||||
(storage as IKbnUrlStateStorage).set(reportTypeKey, reportType);
|
||||
// this is only needed for tab change, so we will not add allSeries into dependencies
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isPreview, storage]);
|
||||
}, [allShortSeries, storage]);
|
||||
|
||||
const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => {
|
||||
setAllSeries((prevAllSeries) => {
|
||||
const newStateRest = prevAllSeries.map((series, index) => {
|
||||
if (index === seriesIndex) {
|
||||
return newValue;
|
||||
}
|
||||
return series;
|
||||
});
|
||||
|
||||
if (prevAllSeries.length === seriesIndex) {
|
||||
return [...newStateRest, newValue];
|
||||
}
|
||||
|
||||
return [...newStateRest];
|
||||
const setSeries = (seriesIdN: string, newValue: SeriesUrl) => {
|
||||
setAllShortSeries((prevState) => {
|
||||
prevState[seriesIdN] = convertToShortUrl(newValue);
|
||||
return { ...prevState };
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(storage as IKbnUrlStateStorage).set(reportTypeKey, reportType);
|
||||
}, [reportType, storage]);
|
||||
const removeSeries = (seriesIdN: string) => {
|
||||
setAllShortSeries((prevState) => {
|
||||
delete prevState[seriesIdN];
|
||||
return { ...prevState };
|
||||
});
|
||||
};
|
||||
|
||||
const removeSeries = useCallback((seriesIndex: number) => {
|
||||
setAllSeries((prevAllSeries) =>
|
||||
prevAllSeries.filter((seriesT, index) => index !== seriesIndex)
|
||||
);
|
||||
}, []);
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
|
||||
const getSeries = useCallback(
|
||||
(seriesIndex: number) => {
|
||||
return allSeries[seriesIndex];
|
||||
(seriesId?: string) => {
|
||||
return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl);
|
||||
},
|
||||
[allSeries]
|
||||
);
|
||||
|
||||
const applyChanges = useCallback(() => {
|
||||
const allShortSeries = allSeries.map((series) => convertToShortUrl(series));
|
||||
|
||||
(storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries);
|
||||
setLastRefresh(Date.now());
|
||||
}, [allSeries, storage]);
|
||||
|
||||
useEffect(() => {
|
||||
(storage as IKbnUrlStateStorage).set(autoApplyKey, autoApply);
|
||||
}, [autoApply, storage]);
|
||||
|
||||
const value = {
|
||||
autoApply,
|
||||
setAutoApply,
|
||||
applyChanges,
|
||||
storage,
|
||||
getSeries,
|
||||
setSeries,
|
||||
removeSeries,
|
||||
firstSeriesId,
|
||||
allSeries,
|
||||
lastRefresh,
|
||||
setLastRefresh,
|
||||
setReportType,
|
||||
reportType: storage.get(reportTypeKey) as ReportViewType,
|
||||
allSeriesIds,
|
||||
firstSeries: firstSeries!,
|
||||
};
|
||||
return <UrlStorageContext.Provider value={value}>{children}</UrlStorageContext.Provider>;
|
||||
|
@ -159,9 +112,10 @@ export function useSeriesStorage() {
|
|||
}
|
||||
|
||||
function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
|
||||
const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue;
|
||||
const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue;
|
||||
return {
|
||||
operationType: op,
|
||||
reportType: rt!,
|
||||
seriesType: st,
|
||||
breakdown: bd,
|
||||
filters: ft!,
|
||||
|
@ -169,31 +123,26 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
|
|||
reportDefinitions: rdf,
|
||||
dataType: dt!,
|
||||
selectedMetricField: mt,
|
||||
hidden: h,
|
||||
name: n,
|
||||
color: c,
|
||||
...restSeries,
|
||||
};
|
||||
}
|
||||
|
||||
interface ShortUrlSeries {
|
||||
[URL_KEYS.OPERATION_TYPE]?: OperationType;
|
||||
[URL_KEYS.REPORT_TYPE]?: ReportViewType;
|
||||
[URL_KEYS.DATA_TYPE]?: AppDataType;
|
||||
[URL_KEYS.SERIES_TYPE]?: SeriesType;
|
||||
[URL_KEYS.BREAK_DOWN]?: string;
|
||||
[URL_KEYS.FILTERS]?: UrlFilter[];
|
||||
[URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition;
|
||||
[URL_KEYS.SELECTED_METRIC]?: string;
|
||||
[URL_KEYS.HIDDEN]?: boolean;
|
||||
[URL_KEYS.NAME]: string;
|
||||
[URL_KEYS.COLOR]?: string;
|
||||
time?: {
|
||||
to: string;
|
||||
from: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AllShortSeries = ShortUrlSeries[];
|
||||
export type AllSeries = SeriesUrl[];
|
||||
export type AllShortSeries = Record<string, ShortUrlSeries>;
|
||||
export type AllSeries = Record<string, SeriesUrl>;
|
||||
|
||||
export const NEW_SERIES_KEY = 'new-series';
|
||||
export const NEW_SERIES_KEY = 'new-series-key';
|
||||
|
|
|
@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public';
|
|||
|
||||
export function ExploratoryViewPage({
|
||||
saveAttributes,
|
||||
multiSeries = false,
|
||||
useSessionStorage = false,
|
||||
}: {
|
||||
useSessionStorage?: boolean;
|
||||
multiSeries?: boolean;
|
||||
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
|
||||
}) {
|
||||
useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' });
|
||||
|
@ -59,7 +61,7 @@ export function ExploratoryViewPage({
|
|||
<Wrapper>
|
||||
<IndexPatternContextProvider>
|
||||
<UrlStorageContextProvider storage={kbnUrlStateStorage}>
|
||||
<ExploratoryView saveAttributes={saveAttributes} />
|
||||
<ExploratoryView saveAttributes={saveAttributes} multiSeries={multiSeries} />
|
||||
</UrlStorageContextProvider>
|
||||
</IndexPatternContextProvider>
|
||||
</Wrapper>
|
||||
|
|
|
@ -7,51 +7,16 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { combineTimeRanges } from './exploratory_view';
|
||||
import { TypedLensByValueInput } from '../../../../../lens/public';
|
||||
import { useSeriesStorage } from './hooks/use_series_storage';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../plugin';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ReportViewType, SeriesUrl } from './types';
|
||||
import { ReportTypes } from './configurations/constants';
|
||||
|
||||
interface Props {
|
||||
lensAttributes: TypedLensByValueInput['attributes'];
|
||||
setLastUpdated: Dispatch<SetStateAction<number | undefined>>;
|
||||
}
|
||||
export const combineTimeRanges = (
|
||||
reportType: ReportViewType,
|
||||
allSeries: SeriesUrl[],
|
||||
firstSeries?: SeriesUrl
|
||||
) => {
|
||||
let to: string = '';
|
||||
let from: string = '';
|
||||
|
||||
if (reportType === ReportTypes.KPI) {
|
||||
return firstSeries?.time;
|
||||
}
|
||||
|
||||
allSeries.forEach((series) => {
|
||||
if (
|
||||
series.dataType &&
|
||||
series.selectedMetricField &&
|
||||
!isEmpty(series.reportDefinitions) &&
|
||||
series.time
|
||||
) {
|
||||
const seriesTo = new Date(series.time.to);
|
||||
const seriesFrom = new Date(series.time.from);
|
||||
if (!to || seriesTo > new Date(to)) {
|
||||
to = series.time.to;
|
||||
}
|
||||
if (!from || seriesFrom < new Date(from)) {
|
||||
from = series.time.from;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { to, from };
|
||||
};
|
||||
|
||||
export function LensEmbeddable(props: Props) {
|
||||
const { lensAttributes, setLastUpdated } = props;
|
||||
|
@ -62,11 +27,9 @@ export function LensEmbeddable(props: Props) {
|
|||
|
||||
const LensComponent = lens?.EmbeddableComponent;
|
||||
|
||||
const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage();
|
||||
const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage();
|
||||
|
||||
const firstSeriesId = 0;
|
||||
|
||||
const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null;
|
||||
const timeRange = combineTimeRanges(allSeries, series);
|
||||
|
||||
const onLensLoad = useCallback(() => {
|
||||
setLastUpdated(Date.now());
|
||||
|
@ -74,9 +37,9 @@ export function LensEmbeddable(props: Props) {
|
|||
|
||||
const onBrushEnd = useCallback(
|
||||
({ range }: { range: number[] }) => {
|
||||
if (reportType !== 'data-distribution' && firstSeries) {
|
||||
if (series?.reportType !== 'data-distribution') {
|
||||
setSeries(firstSeriesId, {
|
||||
...firstSeries,
|
||||
...series,
|
||||
time: {
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
|
@ -90,30 +53,16 @@ export function LensEmbeddable(props: Props) {
|
|||
);
|
||||
}
|
||||
},
|
||||
[reportType, setSeries, firstSeries, notifications?.toasts]
|
||||
[notifications?.toasts, series, firstSeriesId, setSeries]
|
||||
);
|
||||
|
||||
if (timeRange === null || !firstSeries) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LensWrapper>
|
||||
<LensComponent
|
||||
id="exploratoryView"
|
||||
timeRange={timeRange}
|
||||
attributes={lensAttributes}
|
||||
onLoad={onLensLoad}
|
||||
onBrushEnd={onBrushEnd}
|
||||
/>
|
||||
</LensWrapper>
|
||||
<LensComponent
|
||||
id="exploratoryView"
|
||||
timeRange={timeRange}
|
||||
attributes={lensAttributes}
|
||||
onLoad={onLensLoad}
|
||||
onBrushEnd={onBrushEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const LensWrapper = styled.div`
|
||||
height: 100%;
|
||||
|
||||
&&& > div {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { ReactElement } from 'react';
|
|||
import { stringify } from 'query-string';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { render as reactTestLibRender, RenderOptions } from '@testing-library/react';
|
||||
import { Route, Router } from 'react-router-dom';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/
|
|||
import { lensPluginMock } from '../../../../../lens/public/mocks';
|
||||
import * as useAppIndexPatternHook from './hooks/use_app_index_pattern';
|
||||
import { IndexPatternContextProvider } from './hooks/use_app_index_pattern';
|
||||
import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage';
|
||||
import { AllSeries, UrlStorageContext } from './hooks/use_series_storage';
|
||||
|
||||
import * as fetcherHook from '../../../hooks/use_fetcher';
|
||||
import * as useSeriesFilterHook from './hooks/use_series_filters';
|
||||
|
@ -39,10 +39,9 @@ import {
|
|||
IndexPattern,
|
||||
IndexPatternsContract,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { AppDataType, SeriesUrl, UrlFilter } from './types';
|
||||
import { AppDataType, UrlFilter } from './types';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { ListItem } from '../../../hooks/use_values_list';
|
||||
import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames';
|
||||
|
||||
interface KibanaProps {
|
||||
services?: KibanaServices;
|
||||
|
@ -159,11 +158,9 @@ export function MockRouter<ExtraCore>({
|
|||
}: MockRouterProps<ExtraCore>) {
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Route path={'/app/observability/exploratory-view/:mode'}>
|
||||
<MockKibanaProvider core={core} kibanaProps={kibanaProps} history={history}>
|
||||
{children}
|
||||
</MockKibanaProvider>
|
||||
</Route>
|
||||
<MockKibanaProvider core={core} kibanaProps={kibanaProps} history={history}>
|
||||
{children}
|
||||
</MockKibanaProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
@ -176,7 +173,7 @@ export function render<ExtraCore>(
|
|||
core: customCore,
|
||||
kibanaProps,
|
||||
renderOptions,
|
||||
url = '/app/observability/exploratory-view/configure#?autoApply=!t',
|
||||
url,
|
||||
initSeries = {},
|
||||
}: RenderRouterOptions<ExtraCore> = {}
|
||||
) {
|
||||
|
@ -206,7 +203,7 @@ export function render<ExtraCore>(
|
|||
};
|
||||
}
|
||||
|
||||
export const getHistoryFromUrl = (url: Url) => {
|
||||
const getHistoryFromUrl = (url: Url) => {
|
||||
if (typeof url === 'string') {
|
||||
return createMemoryHistory({
|
||||
initialEntries: [url],
|
||||
|
@ -255,15 +252,6 @@ export const mockUseValuesList = (values?: ListItem[]) => {
|
|||
return { spy, onRefreshTimeRange };
|
||||
};
|
||||
|
||||
export const mockUxSeries = {
|
||||
name: 'performance-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
reportDefinitions: { 'service.name': ['elastic-co'] },
|
||||
selectedMetricField: TRANSACTION_DURATION,
|
||||
} as SeriesUrl;
|
||||
|
||||
function mockSeriesStorageContext({
|
||||
data,
|
||||
filters,
|
||||
|
@ -273,34 +261,34 @@ function mockSeriesStorageContext({
|
|||
filters?: UrlFilter[];
|
||||
breakdown?: string;
|
||||
}) {
|
||||
const testSeries = {
|
||||
...mockUxSeries,
|
||||
breakdown: breakdown || 'user_agent.name',
|
||||
...(filters ? { filters } : {}),
|
||||
const mockDataSeries = data || {
|
||||
'performance-distribution': {
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: breakdown || 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
...(filters ? { filters } : {}),
|
||||
},
|
||||
};
|
||||
const allSeriesIds = Object.keys(mockDataSeries);
|
||||
const firstSeriesId = allSeriesIds?.[0];
|
||||
|
||||
const mockDataSeries = data || [testSeries];
|
||||
const series = mockDataSeries[firstSeriesId];
|
||||
|
||||
const removeSeries = jest.fn();
|
||||
const setSeries = jest.fn();
|
||||
|
||||
const getSeries = jest.fn().mockReturnValue(testSeries);
|
||||
const getSeries = jest.fn().mockReturnValue(series);
|
||||
|
||||
return {
|
||||
firstSeriesId,
|
||||
allSeriesIds,
|
||||
removeSeries,
|
||||
setSeries,
|
||||
getSeries,
|
||||
autoApply: true,
|
||||
reportType: 'data-distribution',
|
||||
lastRefresh: Date.now(),
|
||||
setLastRefresh: jest.fn(),
|
||||
setAutoApply: jest.fn(),
|
||||
applyChanges: jest.fn(),
|
||||
firstSeries: mockDataSeries[0],
|
||||
firstSeries: mockDataSeries[firstSeriesId],
|
||||
allSeries: mockDataSeries,
|
||||
setReportType: jest.fn(),
|
||||
storage: { get: jest.fn() } as any,
|
||||
} as SeriesContextValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function mockUseSeriesFilter() {
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { mockUxSeries, render } from '../../rtl_helpers';
|
||||
import { render } from '../../rtl_helpers';
|
||||
import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types';
|
||||
|
||||
describe.skip('SeriesChartTypesSelect', function () {
|
||||
it('should render properly', async function () {
|
||||
render(<SeriesChartTypesSelect seriesId={0} defaultChartType={'line'} series={mockUxSeries} />);
|
||||
render(<SeriesChartTypesSelect seriesId={'series-id'} defaultChartType={'line'} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/chart type/i);
|
||||
|
@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () {
|
|||
|
||||
it('should call set series on change', async function () {
|
||||
const { setSeries } = render(
|
||||
<SeriesChartTypesSelect seriesId={0} defaultChartType={'line'} series={mockUxSeries} />
|
||||
<SeriesChartTypesSelect seriesId={'series-id'} defaultChartType={'line'} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
|
||||
import { SeriesUrl, useFetcher } from '../../../../..';
|
||||
import { useFetcher } from '../../../../..';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesType } from '../../../../../../../lens/public';
|
||||
|
||||
|
@ -20,14 +20,16 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.
|
|||
|
||||
export function SeriesChartTypesSelect({
|
||||
seriesId,
|
||||
series,
|
||||
seriesTypes,
|
||||
defaultChartType,
|
||||
}: {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
seriesTypes?: SeriesType[];
|
||||
defaultChartType: SeriesType;
|
||||
}) {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const seriesType = series?.seriesType ?? defaultChartType;
|
||||
|
||||
|
@ -40,15 +42,17 @@ export function SeriesChartTypesSelect({
|
|||
onChange={onChange}
|
||||
value={seriesType}
|
||||
excludeChartTypes={['bar_percentage_stacked']}
|
||||
includeChartTypes={[
|
||||
'bar',
|
||||
'bar_horizontal',
|
||||
'line',
|
||||
'area',
|
||||
'bar_stacked',
|
||||
'area_stacked',
|
||||
'bar_horizontal_percentage_stacked',
|
||||
]}
|
||||
includeChartTypes={
|
||||
seriesTypes || [
|
||||
'bar',
|
||||
'bar_horizontal',
|
||||
'line',
|
||||
'area',
|
||||
'bar_stacked',
|
||||
'area_stacked',
|
||||
'bar_horizontal_percentage_stacked',
|
||||
]
|
||||
}
|
||||
label={CHART_TYPE_LABEL}
|
||||
/>
|
||||
);
|
||||
|
@ -101,14 +105,14 @@ export function XYChartTypesSelect({
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow label={CHART_TYPE_LABEL} style={{ minWidth: 280 }}>
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
valueOfSelected={value}
|
||||
isLoading={loading}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
compressed
|
||||
prepend={CHART_TYPE_LABEL}
|
||||
valueOfSelected={value}
|
||||
isLoading={loading}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockAppIndexPattern, render } from '../../rtl_helpers';
|
||||
import { dataTypes, DataTypesCol } from './data_types_col';
|
||||
|
||||
describe('DataTypesCol', function () {
|
||||
const seriesId = 'test-series-id';
|
||||
|
||||
mockAppIndexPattern();
|
||||
|
||||
it('should render properly', function () {
|
||||
const { getByText } = render(<DataTypesCol seriesId={seriesId} />);
|
||||
|
||||
dataTypes.forEach(({ label }) => {
|
||||
getByText(label);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set series on change', function () {
|
||||
const { setSeries } = render(<DataTypesCol seriesId={seriesId} />);
|
||||
|
||||
fireEvent.click(screen.getByText(/user experience \(rum\)/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'ux',
|
||||
isNew: true,
|
||||
time: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set series on change on already selected', function () {
|
||||
const initSeries = {
|
||||
data: {
|
||||
[seriesId]: {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(<DataTypesCol seriesId={seriesId} />, { initSeries });
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /Synthetic Monitoring/i,
|
||||
});
|
||||
|
||||
expect(button.classList).toContain('euiButton--fill');
|
||||
});
|
||||
});
|
|
@ -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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { AppDataType } from '../../types';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
|
||||
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
|
||||
{ id: 'synthetics', label: 'Synthetic Monitoring' },
|
||||
{ id: 'ux', label: 'User Experience (RUM)' },
|
||||
{ id: 'mobile', label: 'Mobile Experience' },
|
||||
// { id: 'infra_logs', label: 'Logs' },
|
||||
// { id: 'infra_metrics', label: 'Metrics' },
|
||||
// { id: 'apm', label: 'APM' },
|
||||
];
|
||||
|
||||
export function DataTypesCol({ seriesId }: { seriesId: string }) {
|
||||
const { getSeries, setSeries, removeSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
const { loading } = useAppIndexPatternContext();
|
||||
|
||||
const onDataTypeChange = (dataType?: AppDataType) => {
|
||||
if (!dataType) {
|
||||
removeSeries(seriesId);
|
||||
} else {
|
||||
setSeries(seriesId || `${dataType}-series`, {
|
||||
dataType,
|
||||
isNew: true,
|
||||
time: series.time,
|
||||
} as any);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedDataType = series.dataType;
|
||||
|
||||
return (
|
||||
<FlexGroup direction="column" gutterSize="xs">
|
||||
{dataTypes.map(({ id: dataTypeId, label }) => (
|
||||
<EuiFlexItem key={dataTypeId}>
|
||||
<Button
|
||||
size="s"
|
||||
iconSide="right"
|
||||
iconType="arrowRight"
|
||||
color={selectedDataType === dataTypeId ? 'primary' : 'text'}
|
||||
fill={selectedDataType === dataTypeId}
|
||||
isDisabled={loading}
|
||||
isLoading={loading && selectedDataType === dataTypeId}
|
||||
onClick={() => {
|
||||
onDataTypeChange(dataTypeId);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</FlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const FlexGroup = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Button = styled(EuiButton)`
|
||||
will-change: transform;
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { SeriesDatePicker } from '../../series_date_picker';
|
||||
import { DateRangePicker } from '../../series_date_picker/date_range_picker';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
export function DatePickerCol({ seriesId }: Props) {
|
||||
const { firstSeriesId, getSeries } = useSeriesStorage();
|
||||
const { reportType } = getSeries(firstSeriesId);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
) : (
|
||||
<DateRangePicker seriesId={seriesId} />
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.euiSuperDatePicker__flexWrapper {
|
||||
width: 100%;
|
||||
> .euiFlexItem {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -7,66 +7,62 @@
|
|||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockUxSeries, render } from '../../rtl_helpers';
|
||||
import { render } from '../../rtl_helpers';
|
||||
import { OperationTypeSelect } from './operation_type_select';
|
||||
|
||||
describe('OperationTypeSelect', function () {
|
||||
it('should render properly', function () {
|
||||
render(<OperationTypeSelect seriesId={0} series={mockUxSeries} />);
|
||||
render(<OperationTypeSelect seriesId={'series-id'} />);
|
||||
|
||||
screen.getByText('Select an option: , is selected');
|
||||
});
|
||||
|
||||
it('should display selected value', function () {
|
||||
const initSeries = {
|
||||
data: [
|
||||
{
|
||||
name: 'performance-distribution',
|
||||
data: {
|
||||
'performance-distribution': {
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
operationType: 'median' as const,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
render(<OperationTypeSelect seriesId={0} series={initSeries.data[0]} />, {
|
||||
initSeries,
|
||||
});
|
||||
render(<OperationTypeSelect seriesId={'series-id'} />, { initSeries });
|
||||
|
||||
screen.getByText('Median');
|
||||
});
|
||||
|
||||
it('should call set series on change', function () {
|
||||
const initSeries = {
|
||||
data: [
|
||||
{
|
||||
name: 'performance-distribution',
|
||||
data: {
|
||||
'series-id': {
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
operationType: 'median' as const,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { setSeries } = render(<OperationTypeSelect seriesId={0} series={initSeries.data[0]} />, {
|
||||
initSeries,
|
||||
});
|
||||
const { setSeries } = render(<OperationTypeSelect seriesId={'series-id'} />, { initSeries });
|
||||
|
||||
fireEvent.click(screen.getByTestId('operationTypeSelect'));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith(0, {
|
||||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
operationType: 'median',
|
||||
dataType: 'ux',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
name: 'performance-distribution',
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('95th Percentile'));
|
||||
expect(setSeries).toHaveBeenCalledWith(0, {
|
||||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
operationType: '95th',
|
||||
dataType: 'ux',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
name: 'performance-distribution',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,18 +11,17 @@ import { EuiSuperSelect } from '@elastic/eui';
|
|||
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { OperationType } from '../../../../../../../lens/public';
|
||||
import { SeriesUrl } from '../../types';
|
||||
|
||||
export function OperationTypeSelect({
|
||||
seriesId,
|
||||
series,
|
||||
defaultOperationType,
|
||||
}: {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
defaultOperationType?: OperationType;
|
||||
}) {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const operationType = series?.operationType;
|
||||
|
||||
|
@ -84,7 +83,11 @@ export function OperationTypeSelect({
|
|||
return (
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
prepend={i18n.translate('xpack.observability.expView.operationType.label', {
|
||||
defaultMessage: 'Calculation',
|
||||
})}
|
||||
data-test-subj="operationTypeSelect"
|
||||
compressed
|
||||
valueOfSelected={operationType || defaultOperationType}
|
||||
options={options}
|
||||
onChange={onChange}
|
|
@ -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 { fireEvent, screen } from '@testing-library/react';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { mockIndexPattern, render } from '../../rtl_helpers';
|
||||
import { ReportBreakdowns } from './report_breakdowns';
|
||||
import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
|
||||
|
||||
describe('Series Builder ReportBreakdowns', function () {
|
||||
const seriesId = 'test-series-id';
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
indexPattern: mockIndexPattern,
|
||||
});
|
||||
|
||||
it('should render properly', function () {
|
||||
render(<ReportBreakdowns seriesConfig={dataViewSeries} seriesId={seriesId} />);
|
||||
|
||||
screen.getByText('Select an option: , is selected');
|
||||
screen.getAllByText('Browser family');
|
||||
});
|
||||
|
||||
it('should set new series breakdown on change', function () {
|
||||
const { setSeries } = render(
|
||||
<ReportBreakdowns seriesConfig={dataViewSeries} seriesId={seriesId} />
|
||||
);
|
||||
|
||||
const btn = screen.getByRole('button', {
|
||||
name: /select an option: Browser family , is selected/i,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
fireEvent.click(btn);
|
||||
|
||||
fireEvent.click(screen.getByText(/operating system/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
breakdown: USER_AGENT_OS,
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
it('should set undefined on new series on no select breakdown', function () {
|
||||
const { setSeries } = render(
|
||||
<ReportBreakdowns seriesConfig={dataViewSeries} seriesId={seriesId} />
|
||||
);
|
||||
|
||||
const btn = screen.getByRole('button', {
|
||||
name: /select an option: Browser family , is selected/i,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
fireEvent.click(btn);
|
||||
|
||||
fireEvent.click(screen.getByText(/no breakdown/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
breakdown: undefined,
|
||||
dataType: 'ux',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { Breakdowns } from '../../series_editor/columns/breakdowns';
|
||||
import { SeriesConfig } from '../../types';
|
||||
|
||||
export function ReportBreakdowns({
|
||||
seriesId,
|
||||
seriesConfig,
|
||||
}: {
|
||||
seriesConfig: SeriesConfig;
|
||||
seriesId: string;
|
||||
}) {
|
||||
return (
|
||||
<Breakdowns
|
||||
seriesConfig={seriesConfig}
|
||||
breakdowns={seriesConfig.breakdownFields ?? []}
|
||||
seriesId={seriesId}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -12,14 +12,14 @@ import {
|
|||
mockAppIndexPattern,
|
||||
mockIndexPattern,
|
||||
mockUseValuesList,
|
||||
mockUxSeries,
|
||||
render,
|
||||
} from '../../rtl_helpers';
|
||||
import { ReportDefinitionCol } from './report_definition_col';
|
||||
import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
|
||||
|
||||
describe('Series Builder ReportDefinitionCol', function () {
|
||||
mockAppIndexPattern();
|
||||
const seriesId = 0;
|
||||
const seriesId = 'test-series-id';
|
||||
|
||||
const seriesConfig = getDefaultConfigs({
|
||||
reportType: 'data-distribution',
|
||||
|
@ -27,24 +27,36 @@ describe('Series Builder ReportDefinitionCol', function () {
|
|||
dataType: 'ux',
|
||||
});
|
||||
|
||||
const initSeries = {
|
||||
data: {
|
||||
[seriesId]: {
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'data-distribution' as const,
|
||||
time: { from: 'now-30d', to: 'now' },
|
||||
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockUseValuesList([{ label: 'elastic-co', count: 10 }]);
|
||||
|
||||
it('renders', async () => {
|
||||
render(
|
||||
<ReportDefinitionCol seriesConfig={seriesConfig} seriesId={seriesId} series={mockUxSeries} />
|
||||
);
|
||||
it('should render properly', async function () {
|
||||
render(<ReportDefinitionCol seriesConfig={seriesConfig} seriesId={seriesId} />, {
|
||||
initSeries,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Web Application')).toBeInTheDocument();
|
||||
expect(screen.getByText('Environment')).toBeInTheDocument();
|
||||
expect(screen.getByText('Search Environment')).toBeInTheDocument();
|
||||
screen.getByText('Web Application');
|
||||
screen.getByText('Environment');
|
||||
screen.getByText('Select an option: Page load time, is selected');
|
||||
screen.getByText('Page load time');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render selected report definitions', async function () {
|
||||
render(
|
||||
<ReportDefinitionCol seriesConfig={seriesConfig} seriesId={seriesId} series={mockUxSeries} />
|
||||
);
|
||||
render(<ReportDefinitionCol seriesConfig={seriesConfig} seriesId={seriesId} />, {
|
||||
initSeries,
|
||||
});
|
||||
|
||||
expect(await screen.findByText('elastic-co')).toBeInTheDocument();
|
||||
|
||||
|
@ -53,7 +65,8 @@ describe('Series Builder ReportDefinitionCol', function () {
|
|||
|
||||
it('should be able to remove selected definition', async function () {
|
||||
const { setSeries } = render(
|
||||
<ReportDefinitionCol seriesConfig={seriesConfig} seriesId={seriesId} series={mockUxSeries} />
|
||||
<ReportDefinitionCol seriesConfig={seriesConfig} seriesId={seriesId} />,
|
||||
{ initSeries }
|
||||
);
|
||||
|
||||
expect(
|
||||
|
@ -67,14 +80,11 @@ describe('Series Builder ReportDefinitionCol', function () {
|
|||
fireEvent.click(removeBtn);
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'ux',
|
||||
name: 'performance-distribution',
|
||||
breakdown: 'user_agent.name',
|
||||
reportDefinitions: {},
|
||||
selectedMetricField: 'transaction.duration.us',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-30d', to: 'now' },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { ReportMetricOptions } from '../report_metric_options';
|
||||
import { SeriesConfig } from '../../types';
|
||||
import { SeriesChartTypesSelect } from './chart_types';
|
||||
import { OperationTypeSelect } from './operation_type_select';
|
||||
import { DatePickerCol } from './date_picker_col';
|
||||
import { parseCustomFieldName } from '../../configurations/lens_attributes';
|
||||
import { ReportDefinitionField } from './report_definition_field';
|
||||
|
||||
function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) {
|
||||
const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField);
|
||||
|
||||
return columnType;
|
||||
}
|
||||
|
||||
export function ReportDefinitionCol({
|
||||
seriesConfig,
|
||||
seriesId,
|
||||
}: {
|
||||
seriesConfig: SeriesConfig;
|
||||
seriesId: string;
|
||||
}) {
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {};
|
||||
|
||||
const {
|
||||
definitionFields,
|
||||
defaultSeriesType,
|
||||
hasOperationType,
|
||||
yAxisColumns,
|
||||
metricOptions,
|
||||
} = seriesConfig;
|
||||
|
||||
const onChange = (field: string, value?: string[]) => {
|
||||
if (!value?.[0]) {
|
||||
delete selectedReportDefinitions[field];
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
reportDefinitions: { ...selectedReportDefinitions },
|
||||
});
|
||||
} else {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
reportDefinitions: { ...selectedReportDefinitions, [field]: value },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const columnType = getColumnType(seriesConfig, selectedMetricField);
|
||||
|
||||
return (
|
||||
<FlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<DatePickerCol seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
{definitionFields.map((field) => (
|
||||
<EuiFlexItem key={field}>
|
||||
<ReportDefinitionField
|
||||
seriesId={seriesId}
|
||||
seriesConfig={seriesConfig}
|
||||
field={field}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{metricOptions && (
|
||||
<EuiFlexItem>
|
||||
<ReportMetricOptions options={metricOptions} seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{(hasOperationType || columnType === 'operation') && (
|
||||
<EuiFlexItem>
|
||||
<OperationTypeSelect
|
||||
seriesId={seriesId}
|
||||
defaultOperationType={yAxisColumns[0].operationType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<SeriesChartTypesSelect
|
||||
seriesId={seriesId}
|
||||
defaultChartType={defaultSeriesType}
|
||||
seriesTypes={seriesConfig.seriesTypes}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</FlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const FlexGroup = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
`;
|
|
@ -6,25 +6,30 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ExistsFilter } from '@kbn/es-query';
|
||||
import FieldValueSuggestions from '../../../field_value_suggestions';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch';
|
||||
import { PersistableFilter } from '../../../../../../../lens/common';
|
||||
import { buildPhrasesFilter } from '../../configurations/utils';
|
||||
import { SeriesConfig, SeriesUrl } from '../../types';
|
||||
import { SeriesConfig } from '../../types';
|
||||
import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
field: string;
|
||||
seriesConfig: SeriesConfig;
|
||||
onChange: (field: string, value?: string[]) => void;
|
||||
}
|
||||
|
||||
export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) {
|
||||
export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) {
|
||||
const { getSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
const { reportDefinitions: selectedReportDefinitions = {} } = series;
|
||||
|
@ -59,26 +64,23 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }:
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]);
|
||||
|
||||
if (!indexPattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldValueSuggestions
|
||||
label={labels[field]}
|
||||
sourceField={field}
|
||||
indexPatternTitle={indexPattern.title}
|
||||
selectedValue={selectedReportDefinitions?.[field]}
|
||||
onChange={(val?: string[]) => onChange(field, val)}
|
||||
filters={queryFilters}
|
||||
time={series.time}
|
||||
fullWidth={true}
|
||||
asCombobox={true}
|
||||
allowExclusions={false}
|
||||
allowAllValuesSelection={true}
|
||||
usePrependLabel={false}
|
||||
compressed={false}
|
||||
required={isEmpty(selectedReportDefinitions)}
|
||||
/>
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" alignItems="center" wrap>
|
||||
<EuiFlexItem>
|
||||
{indexPattern && (
|
||||
<FieldValueSuggestions
|
||||
label={labels[field]}
|
||||
sourceField={field}
|
||||
indexPatternTitle={indexPattern.title}
|
||||
selectedValue={selectedReportDefinitions?.[field]}
|
||||
onChange={(val?: string[]) => onChange(field, val)}
|
||||
filters={queryFilters}
|
||||
time={series.time}
|
||||
fullWidth={true}
|
||||
allowAllValuesSelection={true}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import { ReportFilters } from './report_filters';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { mockIndexPattern, render } from '../../rtl_helpers';
|
||||
|
||||
describe('Series Builder ReportFilters', function () {
|
||||
const seriesId = 'test-series-id';
|
||||
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'data-distribution',
|
||||
indexPattern: mockIndexPattern,
|
||||
dataType: 'ux',
|
||||
});
|
||||
|
||||
it('should render properly', function () {
|
||||
render(<ReportFilters seriesConfig={dataViewSeries} seriesId={seriesId} />);
|
||||
|
||||
screen.getByText('Add filter');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { SeriesFilter } from '../../series_editor/columns/series_filter';
|
||||
import { SeriesConfig } from '../../types';
|
||||
|
||||
export function ReportFilters({
|
||||
seriesConfig,
|
||||
seriesId,
|
||||
}: {
|
||||
seriesConfig: SeriesConfig;
|
||||
seriesId: string;
|
||||
}) {
|
||||
return (
|
||||
<SeriesFilter
|
||||
seriesConfig={seriesConfig}
|
||||
filterFields={seriesConfig.filterFields}
|
||||
baseFilters={seriesConfig.baseFilters}
|
||||
seriesId={seriesId}
|
||||
isNew={true}
|
||||
labels={seriesConfig.labels}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockAppIndexPattern, render } from '../../rtl_helpers';
|
||||
import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col';
|
||||
import { ReportTypes } from '../series_builder';
|
||||
import { DEFAULT_TIME } from '../../configurations/constants';
|
||||
|
||||
describe('ReportTypesCol', function () {
|
||||
const seriesId = 'performance-distribution';
|
||||
|
||||
mockAppIndexPattern();
|
||||
|
||||
it('should render properly', function () {
|
||||
render(<ReportTypesCol reportTypes={ReportTypes.ux} seriesId={seriesId} />);
|
||||
screen.getByText('Performance distribution');
|
||||
screen.getByText('KPI over time');
|
||||
});
|
||||
|
||||
it('should display empty message', function () {
|
||||
render(<ReportTypesCol reportTypes={[]} seriesId={seriesId} />);
|
||||
screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT);
|
||||
});
|
||||
|
||||
it('should set series on change', function () {
|
||||
const { setSeries } = render(
|
||||
<ReportTypesCol reportTypes={ReportTypes.synthetics} seriesId={seriesId} />
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText(/KPI over time/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'ux',
|
||||
selectedMetricField: undefined,
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should set selected as filled', function () {
|
||||
const initSeries = {
|
||||
data: {
|
||||
[seriesId]: {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
isNew: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { setSeries } = render(
|
||||
<ReportTypesCol reportTypes={ReportTypes.synthetics} seriesId={seriesId} />,
|
||||
{ initSeries }
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /KPI over time/i,
|
||||
});
|
||||
|
||||
expect(button.classList).toContain('euiButton--fill');
|
||||
fireEvent.click(button);
|
||||
|
||||
// undefined on click selected
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'synthetics',
|
||||
time: DEFAULT_TIME,
|
||||
isNew: true,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { map } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { ReportViewType, SeriesUrl } from '../../types';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { DEFAULT_TIME } from '../../configurations/constants';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { ReportTypeItem } from '../series_builder';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
reportTypes: ReportTypeItem[];
|
||||
}
|
||||
|
||||
export function ReportTypesCol({ seriesId, reportTypes }: Props) {
|
||||
const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage();
|
||||
|
||||
const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId);
|
||||
|
||||
const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType);
|
||||
|
||||
if (!restSeries.dataType) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.observability.expView.seriesBuilder.selectDataType"
|
||||
defaultMessage="No data type selected"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!loading && !hasData) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.observability.reportTypeCol.nodata"
|
||||
defaultMessage="No data available"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const disabledReportTypes: ReportViewType[] = map(
|
||||
reportTypes.filter(
|
||||
({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType
|
||||
),
|
||||
'reportType'
|
||||
);
|
||||
|
||||
return reportTypes?.length > 0 ? (
|
||||
<FlexGroup direction="column" gutterSize="xs">
|
||||
{reportTypes.map(({ reportType, label }) => (
|
||||
<EuiFlexItem key={reportType}>
|
||||
<Button
|
||||
fullWidth
|
||||
size="s"
|
||||
iconSide="right"
|
||||
iconType="arrowRight"
|
||||
color={selectedReportType === reportType ? 'primary' : 'text'}
|
||||
fill={selectedReportType === reportType}
|
||||
isDisabled={loading || disabledReportTypes.includes(reportType)}
|
||||
onClick={() => {
|
||||
if (reportType === selectedReportType) {
|
||||
setSeries(seriesId, {
|
||||
dataType: restSeries.dataType,
|
||||
time: DEFAULT_TIME,
|
||||
isNew: true,
|
||||
} as SeriesUrl);
|
||||
} else {
|
||||
setSeries(seriesId, {
|
||||
...restSeries,
|
||||
reportType,
|
||||
selectedMetricField: undefined,
|
||||
breakdown: undefined,
|
||||
time: restSeries?.time ?? DEFAULT_TIME,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</FlexGroup>
|
||||
) : (
|
||||
<EuiText color="subdued">{SELECTED_DATA_TYPE_FOR_REPORT}</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate(
|
||||
'xpack.observability.expView.reportType.noDataType',
|
||||
{ defaultMessage: 'No data type selected.' }
|
||||
);
|
||||
|
||||
const FlexGroup = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Button = styled(EuiButton)`
|
||||
will-change: transform;
|
||||
`;
|
|
@ -8,7 +8,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiIcon, EuiText } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
interface Props {
|
||||
lastUpdated?: number;
|
||||
|
@ -19,34 +18,20 @@ export function LastUpdated({ lastUpdated }: Props) {
|
|||
useEffect(() => {
|
||||
const interVal = setInterval(() => {
|
||||
setRefresh(Date.now());
|
||||
}, 5000);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interVal);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(Date.now());
|
||||
}, [lastUpdated]);
|
||||
|
||||
if (!lastUpdated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5;
|
||||
const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10;
|
||||
|
||||
return (
|
||||
<EuiText color={isDanger ? 'danger' : isWarning ? 'warning' : 'subdued'} size="s">
|
||||
<EuiIcon type="clock" />
|
||||
<FormattedMessage
|
||||
id="xpack.observability.expView.lastUpdated.label"
|
||||
defaultMessage="Last Updated: {updatedDate}"
|
||||
values={{
|
||||
updatedDate: moment(lastUpdated).from(refresh),
|
||||
}}
|
||||
/>
|
||||
<EuiText color="subdued" size="s">
|
||||
<EuiIcon type="clock" /> Last Updated: {moment(lastUpdated).from(refresh)}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { EuiSuperSelect } from '@elastic/eui';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { SeriesConfig } from '../types';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
defaultValue?: string;
|
||||
options: SeriesConfig['metricOptions'];
|
||||
}
|
||||
|
||||
export function ReportMetricOptions({ seriesId, options: opts }: Props) {
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
selectedMetricField: value,
|
||||
});
|
||||
};
|
||||
|
||||
const options = opts ?? [];
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
compressed
|
||||
prepend={'Metric'}
|
||||
options={options.map(({ label, field: fd, id }) => ({
|
||||
value: fd || id,
|
||||
inputDisplay: label,
|
||||
}))}
|
||||
valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* 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, { RefObject, useEffect, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
import { rgba } from 'polished';
|
||||
import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types';
|
||||
import { DataTypesCol } from './columns/data_types_col';
|
||||
import { ReportTypesCol } from './columns/report_types_col';
|
||||
import { ReportDefinitionCol } from './columns/report_definition_col';
|
||||
import { ReportFilters } from './columns/report_filters';
|
||||
import { ReportBreakdowns } from './columns/report_breakdowns';
|
||||
import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
import { SeriesEditor } from '../series_editor/series_editor';
|
||||
import { SeriesActions } from '../series_editor/columns/series_actions';
|
||||
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { LastUpdated } from './last_updated';
|
||||
import {
|
||||
CORE_WEB_VITALS_LABEL,
|
||||
DEVICE_DISTRIBUTION_LABEL,
|
||||
KPI_OVER_TIME_LABEL,
|
||||
PERF_DIST_LABEL,
|
||||
} from '../configurations/constants/labels';
|
||||
|
||||
export interface ReportTypeItem {
|
||||
id: string;
|
||||
reportType: ReportViewType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const ReportTypes: Record<AppDataType, ReportTypeItem[]> = {
|
||||
synthetics: [
|
||||
{ id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
|
||||
],
|
||||
ux: [
|
||||
{ id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
|
||||
{ id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
|
||||
],
|
||||
mobile: [
|
||||
{ id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
|
||||
{ id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
|
||||
],
|
||||
apm: [],
|
||||
infra_logs: [],
|
||||
infra_metrics: [],
|
||||
};
|
||||
|
||||
interface BuilderItem {
|
||||
id: string;
|
||||
series: SeriesUrl;
|
||||
seriesConfig?: SeriesConfig;
|
||||
}
|
||||
|
||||
export function SeriesBuilder({
|
||||
seriesBuilderRef,
|
||||
lastUpdated,
|
||||
multiSeries,
|
||||
}: {
|
||||
seriesBuilderRef: RefObject<HTMLDivElement>;
|
||||
lastUpdated?: number;
|
||||
multiSeries?: boolean;
|
||||
}) {
|
||||
const [editorItems, setEditorItems] = useState<BuilderItem[]>([]);
|
||||
const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage();
|
||||
|
||||
const { loading, indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
useEffect(() => {
|
||||
const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => {
|
||||
if (indexPatterns?.[dataType]) {
|
||||
return getDefaultConfigs({
|
||||
dataType,
|
||||
indexPattern: indexPatterns[dataType],
|
||||
reportType: reportType!,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const seriesToEdit: BuilderItem[] =
|
||||
allSeriesIds
|
||||
.filter((sId) => {
|
||||
return allSeries?.[sId]?.isNew;
|
||||
})
|
||||
.map((sId) => {
|
||||
const series = getSeries(sId);
|
||||
const seriesConfig = getDataViewSeries(series.dataType, series.reportType);
|
||||
|
||||
return { id: sId, series, seriesConfig };
|
||||
}) ?? [];
|
||||
const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }];
|
||||
setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries);
|
||||
}, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', {
|
||||
defaultMessage: 'Data Type',
|
||||
}),
|
||||
field: 'id',
|
||||
width: '15%',
|
||||
render: (seriesId: string) => <DataTypesCol seriesId={seriesId} />,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
|
||||
defaultMessage: 'Report',
|
||||
}),
|
||||
width: '15%',
|
||||
field: 'id',
|
||||
render: (seriesId: string, { series: { dataType } }: BuilderItem) => (
|
||||
<ReportTypesCol seriesId={seriesId} reportTypes={dataType ? ReportTypes[dataType] : []} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', {
|
||||
defaultMessage: 'Definition',
|
||||
}),
|
||||
width: '30%',
|
||||
field: 'id',
|
||||
render: (
|
||||
seriesId: string,
|
||||
{ series: { dataType, reportType }, seriesConfig }: BuilderItem
|
||||
) => {
|
||||
if (dataType && seriesConfig) {
|
||||
return loading ? (
|
||||
LOADING_VIEW
|
||||
) : reportType ? (
|
||||
<ReportDefinitionCol seriesId={seriesId} seriesConfig={seriesConfig} />
|
||||
) : (
|
||||
SELECT_REPORT_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
|
||||
reportType && seriesConfig ? (
|
||||
<ReportFilters seriesId={seriesId} seriesConfig={seriesConfig} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', {
|
||||
defaultMessage: 'Breakdowns',
|
||||
}),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
|
||||
reportType && seriesConfig ? (
|
||||
<ReportBreakdowns seriesId={seriesId} seriesConfig={seriesConfig} />
|
||||
) : null,
|
||||
},
|
||||
...(multiSeries
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
align: 'center' as const,
|
||||
width: '10%',
|
||||
field: 'id',
|
||||
render: (seriesId: string, item: BuilderItem) => (
|
||||
<SeriesActions seriesId={seriesId} editorMode={true} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const applySeries = () => {
|
||||
editorItems.forEach(({ series, id: seriesId }) => {
|
||||
const { reportType, reportDefinitions, isNew, ...restSeries } = series;
|
||||
|
||||
if (reportType && !isEmpty(reportDefinitions)) {
|
||||
const reportDefId = Object.values(reportDefinitions ?? {})[0];
|
||||
const newSeriesId = `${reportDefId}-${reportType}`;
|
||||
|
||||
const newSeriesN: SeriesUrl = {
|
||||
...restSeries,
|
||||
reportType,
|
||||
reportDefinitions,
|
||||
};
|
||||
|
||||
setSeries(newSeriesId, newSeriesN);
|
||||
removeSeries(seriesId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addSeries = () => {
|
||||
const prevSeries = allSeries?.[allSeriesIds?.[0]];
|
||||
setSeries(
|
||||
`${NEW_SERIES_KEY}-${editorItems.length + 1}`,
|
||||
prevSeries
|
||||
? ({ isNew: true, time: prevSeries.time } as SeriesUrl)
|
||||
: ({ isNew: true } as SeriesUrl)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper ref={seriesBuilderRef}>
|
||||
{multiSeries && (
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<LastUpdated lastUpdated={lastUpdated} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', {
|
||||
defaultMessage: 'Auto apply',
|
||||
})}
|
||||
checked={true}
|
||||
onChange={(e) => {}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={() => applySeries()} isDisabled={true} size="s">
|
||||
{i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
|
||||
defaultMessage: 'Apply changes',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton color="secondary" onClick={() => addSeries()} size="s">
|
||||
{i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
|
||||
defaultMessage: 'Add Series',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
<div>
|
||||
{multiSeries && <SeriesEditor />}
|
||||
{editorItems.length > 0 && (
|
||||
<EuiBasicTable
|
||||
items={editorItems}
|
||||
columns={columns}
|
||||
cellProps={{ style: { borderRight: '1px solid #d3dae6', verticalAlign: 'initial' } }}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = euiStyled.div`
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: clip;
|
||||
&::-webkit-scrollbar {
|
||||
height: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
width: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: content-box;
|
||||
background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
|
||||
border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LOADING_VIEW = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.loadingView',
|
||||
{
|
||||
defaultMessage: 'Loading view ...',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_REPORT_TYPE = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.selectReportType',
|
||||
{
|
||||
defaultMessage: 'No report type selected',
|
||||
}
|
||||
);
|
|
@ -6,48 +6,48 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
|
||||
import { Moment } from 'moment';
|
||||
import DateMath from '@elastic/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
|
||||
import DateMath from '@elastic/datemath';
|
||||
import { Moment } from 'moment';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { SeriesUrl } from '../types';
|
||||
import { ReportTypes } from '../configurations/constants';
|
||||
|
||||
export const parseAbsoluteDate = (date: string, options = {}) => {
|
||||
return DateMath.parse(date, options)!;
|
||||
};
|
||||
export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) {
|
||||
const { firstSeries, setSeries, reportType } = useSeriesStorage();
|
||||
export function DateRangePicker({ seriesId }: { seriesId: string }) {
|
||||
const { firstSeriesId, getSeries, setSeries } = useSeriesStorage();
|
||||
const dateFormat = useUiSetting<string>('dateFormat');
|
||||
|
||||
const seriesFrom = series.time?.from;
|
||||
const seriesTo = series.time?.to;
|
||||
const {
|
||||
time: { from, to },
|
||||
reportType,
|
||||
} = getSeries(firstSeriesId);
|
||||
|
||||
const { from: mainFrom, to: mainTo } = firstSeries!.time;
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!;
|
||||
const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!;
|
||||
const {
|
||||
time: { from: seriesFrom, to: seriesTo },
|
||||
} = series;
|
||||
|
||||
const getTotalDuration = () => {
|
||||
const mainStartDate = parseAbsoluteDate(mainTo)!;
|
||||
const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!;
|
||||
return mainEndDate.diff(mainStartDate, 'millisecond');
|
||||
};
|
||||
const startDate = parseAbsoluteDate(seriesFrom ?? from)!;
|
||||
const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!;
|
||||
|
||||
const onStartChange = (newStartDate: Moment) => {
|
||||
if (reportType === ReportTypes.KPI) {
|
||||
const totalDuration = getTotalDuration();
|
||||
const newFrom = newStartDate.toISOString();
|
||||
const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString();
|
||||
const onStartChange = (newDate: Moment) => {
|
||||
if (reportType === 'kpi-over-time') {
|
||||
const mainStartDate = parseAbsoluteDate(from)!;
|
||||
const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
|
||||
const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
|
||||
const newFrom = newDate.toISOString();
|
||||
const newTo = newDate.add(totalDuration, 'millisecond').toISOString();
|
||||
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
time: { from: newFrom, to: newTo },
|
||||
});
|
||||
} else {
|
||||
const newFrom = newStartDate.toISOString();
|
||||
const newFrom = newDate.toISOString();
|
||||
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
|
@ -55,19 +55,20 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onEndChange = (newEndDate: Moment) => {
|
||||
if (reportType === ReportTypes.KPI) {
|
||||
const totalDuration = getTotalDuration();
|
||||
const newTo = newEndDate.toISOString();
|
||||
const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString();
|
||||
const onEndChange = (newDate: Moment) => {
|
||||
if (reportType === 'kpi-over-time') {
|
||||
const mainStartDate = parseAbsoluteDate(from)!;
|
||||
const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!;
|
||||
const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond');
|
||||
const newTo = newDate.toISOString();
|
||||
const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString();
|
||||
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
time: { from: newFrom, to: newTo },
|
||||
});
|
||||
} else {
|
||||
const newTo = newEndDate.toISOString();
|
||||
const newTo = newDate.toISOString();
|
||||
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
|
@ -89,7 +90,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series
|
|||
aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
|
||||
defaultMessage: 'Start date',
|
||||
})}
|
||||
dateFormat={dateFormat.replace('ss.SSS', 'ss')}
|
||||
dateFormat={dateFormat}
|
||||
showTimeSelect
|
||||
/>
|
||||
}
|
||||
|
@ -103,7 +104,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series
|
|||
aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
|
||||
defaultMessage: 'End date',
|
||||
})}
|
||||
dateFormat={dateFormat.replace('ss.SSS', 'ss')}
|
||||
dateFormat={dateFormat}
|
||||
showTimeSelect
|
||||
/>
|
||||
}
|
|
@ -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 { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useHasData } from '../../../../hooks/use_has_data';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges';
|
||||
import { DEFAULT_TIME } from '../configurations/constants';
|
||||
|
||||
export interface TimePickerTime {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface TimePickerQuickRange extends TimePickerTime {
|
||||
display: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function SeriesDatePicker({ seriesId }: Props) {
|
||||
const { onRefreshTimeRange } = useHasData();
|
||||
|
||||
const commonlyUsedRanges = useQuickTimeRanges();
|
||||
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
onRefreshTimeRange();
|
||||
setSeries(seriesId, { ...series, time: { from: start, to: end } });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!series || !series.time) {
|
||||
setSeries(seriesId, { ...series, time: DEFAULT_TIME });
|
||||
}
|
||||
}, [series, seriesId, setSeries]);
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={series?.time?.from}
|
||||
end={series?.time?.to}
|
||||
onTimeChange={onTimeChange}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
onRefresh={onTimeChange}
|
||||
showUpdateButton={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -6,48 +6,67 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mockUseHasData, render } from '../../rtl_helpers';
|
||||
import { mockUseHasData, render } from '../rtl_helpers';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { SeriesDatePicker } from './index';
|
||||
import { DEFAULT_TIME } from '../configurations/constants';
|
||||
|
||||
describe('SeriesDatePicker', function () {
|
||||
it('should render properly', function () {
|
||||
const initSeries = {
|
||||
data: [
|
||||
{
|
||||
name: 'uptime-pings-histogram',
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'data-distribution' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const { getByText } = render(<SeriesDatePicker seriesId={0} series={initSeries.data[0]} />, {
|
||||
initSeries,
|
||||
});
|
||||
const { getByText } = render(<SeriesDatePicker seriesId={'series-id'} />, { initSeries });
|
||||
|
||||
getByText('Last 30 Minutes');
|
||||
getByText('Last 30 minutes');
|
||||
});
|
||||
|
||||
it('should set defaults', async function () {
|
||||
const initSeries = {
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'kpi-over-time' as const,
|
||||
dataType: 'synthetics' as const,
|
||||
breakdown: 'monitor.status',
|
||||
},
|
||||
},
|
||||
};
|
||||
const { setSeries: setSeries1 } = render(
|
||||
<SeriesDatePicker seriesId={'uptime-pings-histogram'} />,
|
||||
{ initSeries: initSeries as any }
|
||||
);
|
||||
expect(setSeries1).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
|
||||
breakdown: 'monitor.status',
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
time: DEFAULT_TIME,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set series data', async function () {
|
||||
const initSeries = {
|
||||
data: [
|
||||
{
|
||||
name: 'uptime-pings-histogram',
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { onRefreshTimeRange } = mockUseHasData();
|
||||
const { getByTestId, setSeries } = render(
|
||||
<SeriesDatePicker seriesId={0} series={initSeries.data[0]} readonly={false} />,
|
||||
{
|
||||
initSeries,
|
||||
}
|
||||
);
|
||||
const { getByTestId, setSeries } = render(<SeriesDatePicker seriesId={'series-id'} />, {
|
||||
initSeries,
|
||||
});
|
||||
|
||||
await waitFor(function () {
|
||||
fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton'));
|
||||
|
@ -57,10 +76,10 @@ describe('SeriesDatePicker', function () {
|
|||
|
||||
expect(onRefreshTimeRange).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith(0, {
|
||||
name: 'uptime-pings-histogram',
|
||||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
breakdown: 'monitor.status',
|
||||
dataType: 'synthetics',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now/d', to: 'now/d' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Breakdowns } from './columns/breakdowns';
|
||||
import { SeriesConfig } from '../types';
|
||||
import { ChartOptions } from './columns/chart_options';
|
||||
|
||||
interface Props {
|
||||
seriesConfig: SeriesConfig;
|
||||
seriesId: string;
|
||||
breakdownFields: string[];
|
||||
}
|
||||
export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup wrap>
|
||||
<EuiFlexItem>
|
||||
<Breakdowns seriesId={seriesId} breakdowns={breakdownFields} seriesConfig={seriesConfig} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ChartOptions seriesConfig={seriesConfig} seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { Breakdowns } from './breakdowns';
|
||||
import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
|
||||
import { mockIndexPattern, render } from '../../rtl_helpers';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames';
|
||||
|
||||
|
@ -20,7 +20,13 @@ describe('Breakdowns', function () {
|
|||
});
|
||||
|
||||
it('should render properly', async function () {
|
||||
render(<Breakdowns seriesId={0} seriesConfig={dataViewSeries} series={mockUxSeries} />);
|
||||
render(
|
||||
<Breakdowns
|
||||
seriesId={'series-id'}
|
||||
breakdowns={dataViewSeries.breakdownFields}
|
||||
seriesConfig={dataViewSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
screen.getAllByText('Browser family');
|
||||
});
|
||||
|
@ -30,9 +36,9 @@ describe('Breakdowns', function () {
|
|||
|
||||
const { setSeries } = render(
|
||||
<Breakdowns
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
breakdowns={dataViewSeries.breakdownFields}
|
||||
seriesConfig={dataViewSeries}
|
||||
series={{ ...mockUxSeries, breakdown: USER_AGENT_OS }}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
|
@ -43,14 +49,10 @@ describe('Breakdowns', function () {
|
|||
|
||||
fireEvent.click(screen.getByText('Browser family'));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith(0, {
|
||||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
name: 'performance-distribution',
|
||||
reportDefinitions: {
|
||||
'service.name': ['elastic-co'],
|
||||
},
|
||||
selectedMetricField: 'transaction.duration.us',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
|
@ -8,20 +8,20 @@
|
|||
import React from 'react';
|
||||
import { EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants';
|
||||
import { SeriesConfig, SeriesUrl } from '../../types';
|
||||
import { SeriesConfig } from '../../types';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
breakdowns: string[];
|
||||
seriesConfig: SeriesConfig;
|
||||
}
|
||||
|
||||
export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
const isPreview = !!useRouteMatch('/exploratory-view/preview');
|
||||
export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) {
|
||||
const { setSeries, getSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const selectedBreakdown = series.breakdown;
|
||||
const NO_BREAKDOWN = 'no_breakdown';
|
||||
|
@ -40,13 +40,9 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
if (!seriesConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN;
|
||||
|
||||
const items = seriesConfig.breakdownFields.map((breakdown) => ({
|
||||
const items = breakdowns.map((breakdown) => ({
|
||||
id: breakdown,
|
||||
label: seriesConfig.labels[breakdown],
|
||||
}));
|
||||
|
@ -54,12 +50,14 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
|
|||
if (!hasUseBreakdownColumn) {
|
||||
items.push({
|
||||
id: NO_BREAKDOWN,
|
||||
label: NO_BREAK_DOWN_LABEL,
|
||||
label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', {
|
||||
defaultMessage: 'No breakdown',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const options = items.map(({ id, label }) => ({
|
||||
inputDisplay: label,
|
||||
inputDisplay: id === NO_BREAKDOWN ? label : <strong>{label}</strong>,
|
||||
value: id,
|
||||
dropdownDisplay: label,
|
||||
}));
|
||||
|
@ -71,7 +69,7 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
|
|||
<div style={{ width: 200 }}>
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
compressed={isPreview}
|
||||
compressed
|
||||
options={options}
|
||||
valueOfSelected={valueOfSelected}
|
||||
onChange={(value) => onOptionChange(value)}
|
||||
|
@ -80,10 +78,3 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const NO_BREAK_DOWN_LABEL = i18n.translate(
|
||||
'xpack.observability.exp.breakDownFilter.noBreakdown',
|
||||
{
|
||||
defaultMessage: 'No breakdown',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { SeriesConfig } from '../../types';
|
||||
import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select';
|
||||
import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types';
|
||||
|
||||
interface Props {
|
||||
seriesConfig: SeriesConfig;
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function ChartOptions({ seriesConfig, seriesId }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SeriesChartTypesSelect
|
||||
seriesId={seriesId}
|
||||
defaultChartType={seriesConfig.seriesTypes[0]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{seriesConfig.hasOperationType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<OperationTypeSelect seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,39 +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 React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers';
|
||||
import { DataTypesLabels, DataTypesSelect } from './data_type_select';
|
||||
import { DataTypes } from '../../configurations/constants';
|
||||
|
||||
describe('DataTypeSelect', function () {
|
||||
const seriesId = 0;
|
||||
|
||||
mockAppIndexPattern();
|
||||
|
||||
it('should render properly', function () {
|
||||
render(<DataTypesSelect seriesId={seriesId} series={mockUxSeries} />);
|
||||
});
|
||||
|
||||
it('should set series on change', async function () {
|
||||
const { setSeries } = render(<DataTypesSelect seriesId={seriesId} series={mockUxSeries} />);
|
||||
|
||||
fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.UX]));
|
||||
fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS]));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'synthetics',
|
||||
name: 'synthetics-series-1',
|
||||
time: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,105 +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 React from 'react';
|
||||
import { EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { AppDataType, SeriesUrl } from '../../types';
|
||||
import { DataTypes, ReportTypes } from '../../configurations/constants';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
}
|
||||
|
||||
export const DataTypesLabels = {
|
||||
[DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', {
|
||||
defaultMessage: 'User experience (RUM)',
|
||||
}),
|
||||
|
||||
[DataTypes.SYNTHETICS]: i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.syntheticsLabel',
|
||||
{
|
||||
defaultMessage: 'Synthetics monitoring',
|
||||
}
|
||||
),
|
||||
|
||||
[DataTypes.MOBILE]: i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.mobileExperienceLabel',
|
||||
{
|
||||
defaultMessage: 'Mobile experience',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
|
||||
{
|
||||
id: DataTypes.SYNTHETICS,
|
||||
label: DataTypesLabels[DataTypes.SYNTHETICS],
|
||||
},
|
||||
{
|
||||
id: DataTypes.UX,
|
||||
label: DataTypesLabels[DataTypes.UX],
|
||||
},
|
||||
{
|
||||
id: DataTypes.MOBILE,
|
||||
label: DataTypesLabels[DataTypes.MOBILE],
|
||||
},
|
||||
];
|
||||
|
||||
const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE';
|
||||
|
||||
export function DataTypesSelect({ seriesId, series }: Props) {
|
||||
const { setSeries, reportType } = useSeriesStorage();
|
||||
|
||||
const onDataTypeChange = (dataType: AppDataType) => {
|
||||
if (String(dataType) !== SELECT_DATA_TYPE) {
|
||||
setSeries(seriesId, {
|
||||
dataType,
|
||||
time: series.time,
|
||||
name: `${dataType}-series-${seriesId + 1}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const options = dataTypes
|
||||
.filter(({ id }) => {
|
||||
if (reportType === ReportTypes.DEVICE_DISTRIBUTION) {
|
||||
return id === DataTypes.MOBILE;
|
||||
}
|
||||
if (reportType === ReportTypes.CORE_WEB_VITAL) {
|
||||
return id === DataTypes.UX;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(({ id, label }) => ({
|
||||
value: id,
|
||||
inputDisplay: label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
options={
|
||||
series.dataType
|
||||
? options
|
||||
: [{ value: SELECT_DATA_TYPE, inputDisplay: SELECT_DATA_TYPE_LABEL }, ...options]
|
||||
}
|
||||
valueOfSelected={series.dataType ?? SELECT_DATA_TYPE}
|
||||
onChange={(value) => onDataTypeChange(value as AppDataType)}
|
||||
style={{ minWidth: 220 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SELECT_DATA_TYPE_LABEL = i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.selectDataType',
|
||||
{
|
||||
defaultMessage: 'Select data type',
|
||||
}
|
||||
);
|
|
@ -6,84 +6,24 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { SeriesDatePicker } from '../../series_date_picker';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { DateRangePicker } from '../../components/date_range_picker';
|
||||
import { SeriesDatePicker } from '../../components/series_date_picker';
|
||||
import { AppDataType, SeriesUrl } from '../../types';
|
||||
import { ReportTypes } from '../../configurations/constants';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data';
|
||||
import { MobileAddData } from '../../../add_data_buttons/mobile_add_data';
|
||||
import { UXAddData } from '../../../add_data_buttons/ux_add_data';
|
||||
import { DateRangePicker } from '../../series_date_picker/date_range_picker';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
const AddDataComponents: Record<AppDataType, React.FC | null> = {
|
||||
mobile: MobileAddData,
|
||||
ux: UXAddData,
|
||||
synthetics: SyntheticsAddData,
|
||||
apm: null,
|
||||
infra_logs: null,
|
||||
infra_metrics: null,
|
||||
};
|
||||
|
||||
export function DatePickerCol({ seriesId, series }: Props) {
|
||||
const { reportType } = useSeriesStorage();
|
||||
|
||||
const { hasAppData } = useAppIndexPatternContext();
|
||||
|
||||
if (!series.dataType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const AddDataButton = AddDataComponents[series.dataType];
|
||||
if (hasAppData[series.dataType] === false && AddDataButton !== null) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<strong>
|
||||
{i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', {
|
||||
defaultMessage: 'No {dataType} data available.',
|
||||
values: {
|
||||
dataType: series.dataType,
|
||||
},
|
||||
})}
|
||||
</strong>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddDataButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!series.selectedMetricField) {
|
||||
return null;
|
||||
}
|
||||
export function DatePickerCol({ seriesId }: Props) {
|
||||
const { firstSeriesId, getSeries } = useSeriesStorage();
|
||||
const { reportType } = getSeries(firstSeriesId);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{seriesId === 0 || reportType !== ReportTypes.KPI ? (
|
||||
<SeriesDatePicker seriesId={seriesId} series={series} readonly={false} />
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
{firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
) : (
|
||||
<DateRangePicker seriesId={seriesId} series={series} />
|
||||
<DateRangePicker seriesId={seriesId} />
|
||||
)}
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
.euiSuperDatePicker__flexWrapper {
|
||||
width: 100%;
|
||||
> .euiFlexItem {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -8,24 +8,20 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { FilterExpanded } from './filter_expanded';
|
||||
import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames';
|
||||
|
||||
describe('FilterExpanded', function () {
|
||||
const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }];
|
||||
|
||||
const mockSeries = { ...mockUxSeries, filters };
|
||||
|
||||
it('render', async () => {
|
||||
const initSeries = { filters };
|
||||
it('should render properly', async function () {
|
||||
const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
|
||||
mockAppIndexPattern();
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={0}
|
||||
series={mockSeries}
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={jest.fn()}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
|
@ -37,14 +33,15 @@ describe('FilterExpanded', function () {
|
|||
});
|
||||
|
||||
it('should call go back on click', async function () {
|
||||
const initSeries = { filters };
|
||||
const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
|
||||
const goBack = jest.fn();
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={0}
|
||||
series={mockSeries}
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={goBack}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
|
@ -52,23 +49,28 @@ describe('FilterExpanded', function () {
|
|||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Browser Family'));
|
||||
|
||||
expect(goBack).toHaveBeenCalledTimes(1);
|
||||
expect(goBack).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls useValuesList on load', async () => {
|
||||
const initSeries = { filters };
|
||||
it('should call useValuesList on load', async function () {
|
||||
const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
|
||||
|
||||
const { spy } = mockUseValuesList([
|
||||
{ label: 'Chrome', count: 10 },
|
||||
{ label: 'Firefox', count: 5 },
|
||||
]);
|
||||
|
||||
const goBack = jest.fn();
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={0}
|
||||
series={mockSeries}
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={goBack}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
|
@ -85,8 +87,8 @@ describe('FilterExpanded', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('filters display values', async () => {
|
||||
const initSeries = { filters };
|
||||
it('should filter display values', async function () {
|
||||
const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
|
||||
|
||||
mockUseValuesList([
|
||||
{ label: 'Chrome', count: 10 },
|
||||
|
@ -95,20 +97,18 @@ describe('FilterExpanded', function () {
|
|||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={0}
|
||||
series={mockUxSeries}
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={jest.fn()}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
|
||||
expect(screen.getByText('Firefox')).toBeTruthy();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Browser Family'));
|
||||
|
||||
expect(screen.queryByText('Firefox')).toBeTruthy();
|
||||
|
||||
fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } });
|
||||
|
||||
expect(screen.queryByText('Firefox')).toBeFalsy();
|
|
@ -6,14 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiSpacer,
|
||||
EuiFilterGroup,
|
||||
EuiText,
|
||||
EuiPopover,
|
||||
EuiFilterButton,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -21,7 +14,8 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
|||
import { map } from 'lodash';
|
||||
import { ExistsFilter } from '@kbn/es-query';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesConfig, UrlFilter } from '../../types';
|
||||
import { FilterValueButton } from './filter_value_btn';
|
||||
import { useValuesList } from '../../../../../hooks/use_values_list';
|
||||
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
|
@ -29,33 +23,31 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc
|
|||
import { PersistableFilter } from '../../../../../../../lens/common';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
label: string;
|
||||
field: string;
|
||||
isNegated?: boolean;
|
||||
goBack: () => void;
|
||||
nestedField?: string;
|
||||
filters: SeriesConfig['baseFilters'];
|
||||
}
|
||||
|
||||
export interface NestedFilterOpen {
|
||||
value: string;
|
||||
negate: boolean;
|
||||
}
|
||||
|
||||
export function FilterExpanded({
|
||||
seriesId,
|
||||
series,
|
||||
field,
|
||||
label,
|
||||
goBack,
|
||||
nestedField,
|
||||
isNegated,
|
||||
filters: defaultFilters,
|
||||
}: Props) {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isNestedOpen, setIsNestedOpen] = useState<NestedFilterOpen>({ value: '', negate: false });
|
||||
const [isOpen, setIsOpen] = useState({ value: '', negate: false });
|
||||
|
||||
const { getSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const queryFilters: ESFilter[] = [];
|
||||
|
||||
|
@ -89,71 +81,62 @@ export function FilterExpanded({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiFilterButton onClick={() => setIsOpen((prevState) => !prevState)} iconType="arrowDown">
|
||||
{label}
|
||||
</EuiFilterButton>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
>
|
||||
<Wrapper>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isLoading={loading}
|
||||
value={value}
|
||||
onChange={(evt) => {
|
||||
setValue(evt.target.value);
|
||||
}}
|
||||
placeholder={i18n.translate('xpack.observability.filters.expanded.search', {
|
||||
defaultMessage: 'Search for {label}',
|
||||
values: { label },
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<ListWrapper>
|
||||
{displayValues.length === 0 && !loading && (
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.observability.filters.expanded.noFilter', {
|
||||
defaultMessage: 'No filters found.',
|
||||
})}
|
||||
</EuiText>
|
||||
)}
|
||||
{displayValues.map((opt) => (
|
||||
<Fragment key={opt}>
|
||||
<EuiFilterGroup fullWidth={true} color="primary">
|
||||
{isNegated !== false && (
|
||||
<FilterValueButton
|
||||
field={field}
|
||||
value={opt}
|
||||
allSelectedValues={currFilter?.notValues}
|
||||
negate={true}
|
||||
nestedField={nestedField}
|
||||
seriesId={seriesId}
|
||||
series={series}
|
||||
isNestedOpen={isNestedOpen}
|
||||
setIsNestedOpen={setIsNestedOpen}
|
||||
/>
|
||||
)}
|
||||
<Wrapper>
|
||||
<EuiButtonEmpty iconType="arrowLeft" color="text" onClick={() => goBack()}>
|
||||
{label}
|
||||
</EuiButtonEmpty>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isLoading={loading}
|
||||
value={value}
|
||||
onChange={(evt) => {
|
||||
setValue(evt.target.value);
|
||||
}}
|
||||
placeholder={i18n.translate('xpack.observability.filters.expanded.search', {
|
||||
defaultMessage: 'Search for {label}',
|
||||
values: { label },
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<ListWrapper>
|
||||
{displayValues.length === 0 && !loading && (
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.observability.filters.expanded.noFilter', {
|
||||
defaultMessage: 'No filters found.',
|
||||
})}
|
||||
</EuiText>
|
||||
)}
|
||||
{displayValues.map((opt) => (
|
||||
<Fragment key={opt}>
|
||||
<EuiFilterGroup fullWidth={true} color="primary">
|
||||
{isNegated !== false && (
|
||||
<FilterValueButton
|
||||
field={field}
|
||||
value={opt}
|
||||
allSelectedValues={currFilter?.values}
|
||||
allSelectedValues={currFilter?.notValues}
|
||||
negate={true}
|
||||
nestedField={nestedField}
|
||||
seriesId={seriesId}
|
||||
series={series}
|
||||
negate={false}
|
||||
isNestedOpen={isNestedOpen}
|
||||
setIsNestedOpen={setIsNestedOpen}
|
||||
isNestedOpen={isOpen}
|
||||
setIsNestedOpen={setIsOpen}
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
))}
|
||||
</ListWrapper>
|
||||
</Wrapper>
|
||||
</EuiPopover>
|
||||
)}
|
||||
<FilterValueButton
|
||||
field={field}
|
||||
value={opt}
|
||||
allSelectedValues={currFilter?.values}
|
||||
nestedField={nestedField}
|
||||
seriesId={seriesId}
|
||||
negate={false}
|
||||
isNestedOpen={isOpen}
|
||||
setIsNestedOpen={setIsOpen}
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
))}
|
||||
</ListWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { FilterValueButton } from './filter_value_btn';
|
||||
import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import {
|
||||
USER_AGENT_NAME,
|
||||
USER_AGENT_VERSION,
|
||||
|
@ -19,98 +19,84 @@ describe('FilterValueButton', function () {
|
|||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
screen.getByText('Chrome');
|
||||
});
|
||||
|
||||
it('should render display negate state', async function () {
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Chrome')).toBeInTheDocument();
|
||||
screen.getByText('Not Chrome');
|
||||
screen.getByTitle('Not Chrome');
|
||||
const btn = screen.getByRole('button');
|
||||
expect(btn.classList).toContain('euiButtonEmpty--danger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when negate is true', () => {
|
||||
it('displays negate stats', async () => {
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
it('should call set filter on click', async function () {
|
||||
const { setFilter, removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Not Chrome')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Not Chrome')).toBeInTheDocument();
|
||||
const btn = screen.getByRole('button');
|
||||
expect(btn.classList).toContain('euiButtonEmpty--danger');
|
||||
});
|
||||
});
|
||||
|
||||
it('calls setFilter on click', async () => {
|
||||
const { setFilter, removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
allSelectedValues={['Firefox']}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
allSelectedValues={['Firefox']}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Not Chrome'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(removeFilter).toHaveBeenCalledTimes(0);
|
||||
expect(setFilter).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(setFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: true,
|
||||
value: 'Chrome',
|
||||
});
|
||||
expect(removeFilter).toHaveBeenCalledTimes(0);
|
||||
expect(setFilter).toHaveBeenCalledTimes(1);
|
||||
expect(setFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: true,
|
||||
value: 'Chrome',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when selected', () => {
|
||||
it('removes the filter on click', async () => {
|
||||
const { removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
it('should remove filter on click if already selected', async function () {
|
||||
const { removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Chrome'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(removeFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: false,
|
||||
value: 'Chrome',
|
||||
});
|
||||
expect(removeFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: false,
|
||||
value: 'Chrome',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -121,13 +107,12 @@ describe('FilterValueButton', function () {
|
|||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -149,14 +134,13 @@ describe('FilterValueButton', function () {
|
|||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: 'Chrome', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -183,14 +167,13 @@ describe('FilterValueButton', function () {
|
|||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: 'Chrome', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -220,14 +203,13 @@ describe('FilterValueButton', function () {
|
|||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={setIsNestedOpen}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -247,14 +229,13 @@ describe('FilterValueButton', function () {
|
|||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={0}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: true }}
|
||||
setIsNestedOpen={setIsNestedOpen}
|
||||
negate={true}
|
||||
allSelectedValues={['Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
series={mockUxSeries}
|
||||
/>
|
||||
);
|
||||
|
|
@ -8,11 +8,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import React, { useMemo } from 'react';
|
||||
import { EuiFilterButton, hexToRgb } from '@elastic/eui';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { useSeriesFilters } from '../../hooks/use_series_filters';
|
||||
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import FieldValueSuggestions from '../../../field_value_suggestions';
|
||||
import { SeriesUrl } from '../../types';
|
||||
import { NestedFilterOpen } from './filter_expanded';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
|
@ -20,13 +19,12 @@ interface Props {
|
|||
allSelectedValues?: string[];
|
||||
negate: boolean;
|
||||
nestedField?: string;
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesId: string;
|
||||
isNestedOpen: {
|
||||
value: string;
|
||||
negate: boolean;
|
||||
};
|
||||
setIsNestedOpen: (val: NestedFilterOpen) => void;
|
||||
setIsNestedOpen: (val: { value: string; negate: boolean }) => void;
|
||||
}
|
||||
|
||||
export function FilterValueButton({
|
||||
|
@ -36,13 +34,16 @@ export function FilterValueButton({
|
|||
field,
|
||||
negate,
|
||||
seriesId,
|
||||
series,
|
||||
nestedField,
|
||||
allSelectedValues,
|
||||
}: Props) {
|
||||
const { getSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const { indexPatterns } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series });
|
||||
const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
const hasActiveFilters = (allSelectedValues ?? []).includes(value);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function RemoveSeries({ seriesId }: Props) {
|
||||
const { removeSeries } = useSeriesStorage();
|
||||
|
||||
const onClick = () => {
|
||||
removeSeries(seriesId);
|
||||
};
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.observability.expView.seriesEditor.removeSeries', {
|
||||
defaultMessage: 'Click to remove series',
|
||||
})}
|
||||
iconType="cross"
|
||||
color="danger"
|
||||
onClick={onClick}
|
||||
size="s"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,59 +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 React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesConfig, SeriesUrl } from '../../types';
|
||||
import { ReportDefinitionField } from './report_definition_field';
|
||||
|
||||
export function ReportDefinitionCol({
|
||||
seriesId,
|
||||
series,
|
||||
seriesConfig,
|
||||
}: {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesConfig: SeriesConfig;
|
||||
}) {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
|
||||
const { reportDefinitions: selectedReportDefinitions = {} } = series;
|
||||
|
||||
const { definitionFields } = seriesConfig;
|
||||
|
||||
const onChange = (field: string, value?: string[]) => {
|
||||
if (!value?.[0]) {
|
||||
delete selectedReportDefinitions[field];
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
reportDefinitions: { ...selectedReportDefinitions },
|
||||
});
|
||||
} else {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
reportDefinitions: { ...selectedReportDefinitions, [field]: value },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
{definitionFields.map((field) => (
|
||||
<EuiFlexItem key={field} grow={1}>
|
||||
<ReportDefinitionField
|
||||
seriesId={seriesId}
|
||||
series={series}
|
||||
seriesConfig={seriesConfig}
|
||||
field={field}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,64 +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 React from 'react';
|
||||
import { EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { ReportViewType } from '../../types';
|
||||
import {
|
||||
CORE_WEB_VITALS_LABEL,
|
||||
DEVICE_DISTRIBUTION_LABEL,
|
||||
KPI_OVER_TIME_LABEL,
|
||||
PERF_DIST_LABEL,
|
||||
} from '../../configurations/constants/labels';
|
||||
|
||||
const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE';
|
||||
|
||||
export const reportTypesList: Array<{
|
||||
reportType: ReportViewType | typeof SELECT_REPORT_TYPE;
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
reportType: SELECT_REPORT_TYPE,
|
||||
label: i18n.translate('xpack.observability.expView.reportType.selectLabel', {
|
||||
defaultMessage: 'Select report type',
|
||||
}),
|
||||
},
|
||||
{ reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
|
||||
{ reportType: 'data-distribution', label: PERF_DIST_LABEL },
|
||||
{ reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL },
|
||||
{ reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL },
|
||||
];
|
||||
|
||||
export function ReportTypesSelect() {
|
||||
const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage();
|
||||
|
||||
const onReportTypeChange = (reportType: ReportViewType) => {
|
||||
setReportType(reportType);
|
||||
};
|
||||
|
||||
const options = reportTypesList
|
||||
.filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true))
|
||||
.map(({ reportType, label }) => ({
|
||||
value: reportType,
|
||||
inputDisplay: reportType === SELECT_REPORT_TYPE ? label : <strong>{label}</strong>,
|
||||
dropdownDisplay: label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
options={options}
|
||||
valueOfSelected={selectedReportType ?? SELECT_REPORT_TYPE}
|
||||
onChange={(value) => onReportTypeChange(value as ReportViewType)}
|
||||
style={{ minWidth: 200 }}
|
||||
isInvalid={!selectedReportType && allSeries.length > 0}
|
||||
disabled={allSeries.length > 0}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { RemoveSeries } from './remove_series';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesUrl } from '../../types';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
editorMode?: boolean;
|
||||
}
|
||||
export function SeriesActions({ seriesId, editorMode = false }: Props) {
|
||||
const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage();
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const onEdit = () => {
|
||||
setSeries(seriesId, { ...series, isNew: true });
|
||||
};
|
||||
|
||||
const copySeries = () => {
|
||||
let copySeriesId: string = `${seriesId}-copy`;
|
||||
if (allSeriesIds.includes(copySeriesId)) {
|
||||
copySeriesId = copySeriesId + allSeriesIds.length;
|
||||
}
|
||||
setSeries(copySeriesId, series);
|
||||
};
|
||||
|
||||
const { reportType, reportDefinitions, isNew, ...restSeries } = series;
|
||||
const isSaveAble = reportType && !isEmpty(reportDefinitions);
|
||||
|
||||
const saveSeries = () => {
|
||||
if (isSaveAble) {
|
||||
const reportDefId = Object.values(reportDefinitions ?? {})[0];
|
||||
let newSeriesId = `${reportDefId}-${reportType}`;
|
||||
|
||||
if (allSeriesIds.includes(newSeriesId)) {
|
||||
newSeriesId = `${newSeriesId}-${allSeriesIds.length}`;
|
||||
}
|
||||
const newSeriesN: SeriesUrl = {
|
||||
...restSeries,
|
||||
reportType,
|
||||
reportDefinitions,
|
||||
};
|
||||
|
||||
setSeries(newSeriesId, newSeriesN);
|
||||
removeSeries(seriesId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center">
|
||||
{!editorMode && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="documentEdit"
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.edit', {
|
||||
defaultMessage: 'Edit series',
|
||||
})}
|
||||
size="s"
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{editorMode && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={'save'}
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.save', {
|
||||
defaultMessage: 'Save series',
|
||||
})}
|
||||
size="s"
|
||||
onClick={saveSeries}
|
||||
color="success"
|
||||
isDisabled={!isSaveAble}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{editorMode && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={'copy'}
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.clone', {
|
||||
defaultMessage: 'Copy series',
|
||||
})}
|
||||
size="s"
|
||||
onClick={copySeries}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveSeries seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { FilterExpanded } from './filter_expanded';
|
||||
import { SeriesConfig } from '../../types';
|
||||
import { FieldLabels } from '../../configurations/constants/constants';
|
||||
import { SelectedFilters } from '../selected_filters';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
filterFields: SeriesConfig['filterFields'];
|
||||
baseFilters: SeriesConfig['baseFilters'];
|
||||
seriesConfig: SeriesConfig;
|
||||
isNew?: boolean;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
label: string;
|
||||
field: string;
|
||||
nested?: string;
|
||||
isNegated?: boolean;
|
||||
}
|
||||
|
||||
export function SeriesFilter({
|
||||
seriesConfig,
|
||||
isNew,
|
||||
seriesId,
|
||||
filterFields = [],
|
||||
baseFilters,
|
||||
labels,
|
||||
}: Props) {
|
||||
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
|
||||
|
||||
const [selectedField, setSelectedField] = useState<Field | undefined>();
|
||||
|
||||
const options: Field[] = filterFields.map((field) => {
|
||||
if (typeof field === 'string') {
|
||||
return { label: labels?.[field] ?? FieldLabels[field], field };
|
||||
}
|
||||
|
||||
return {
|
||||
field: field.field,
|
||||
nested: field.nested,
|
||||
isNegated: field.isNegated,
|
||||
label: labels?.[field.field] ?? FieldLabels[field.field],
|
||||
};
|
||||
});
|
||||
|
||||
const { setSeries, getSeries } = useSeriesStorage();
|
||||
const urlSeries = getSeries(seriesId);
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
iconType="plus"
|
||||
onClick={() => {
|
||||
setIsPopoverVisible((prevState) => !prevState);
|
||||
}}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.seriesEditor.addFilter', {
|
||||
defaultMessage: 'Add filter',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const mainPanel = (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
{options.map((opt) => (
|
||||
<Fragment key={opt.label}>
|
||||
<EuiButton
|
||||
fullWidth={true}
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
onClick={() => {
|
||||
setSelectedField(opt);
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</EuiButton>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const childPanel = selectedField ? (
|
||||
<FilterExpanded
|
||||
seriesId={seriesId}
|
||||
field={selectedField.field}
|
||||
label={selectedField.label}
|
||||
nestedField={selectedField.nested}
|
||||
isNegated={selectedField.isNegated}
|
||||
goBack={() => {
|
||||
setSelectedField(undefined);
|
||||
}}
|
||||
filters={baseFilters}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const closePopover = () => {
|
||||
setIsPopoverVisible(false);
|
||||
setSelectedField(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup wrap direction="column" gutterSize="xs" alignItems="flexStart">
|
||||
<SelectedFilters seriesId={seriesId} seriesConfig={seriesConfig} isNew={isNew} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverVisible}
|
||||
closePopover={closePopover}
|
||||
anchorPosition={isNew ? 'leftCenter' : 'rightCenter'}
|
||||
>
|
||||
{!selectedField ? mainPanel : childPanel}
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
{(urlSeries.filters ?? []).length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
color="text"
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
setSeries(seriesId, { ...urlSeries, filters: undefined });
|
||||
}}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', {
|
||||
defaultMessage: 'Clear filters',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,77 +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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import { SeriesConfig, SeriesUrl } from '../types';
|
||||
import { ReportDefinitionCol } from './columns/report_definition_col';
|
||||
import { OperationTypeSelect } from './columns/operation_type_select';
|
||||
import { parseCustomFieldName } from '../configurations/lens_attributes';
|
||||
import { SeriesFilter } from '../series_viewer/columns/series_filter';
|
||||
|
||||
function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) {
|
||||
const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField);
|
||||
|
||||
return columnType;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesConfig: SeriesConfig;
|
||||
}
|
||||
export function ExpandedSeriesRow({ seriesId, series, seriesConfig }: Props) {
|
||||
if (!seriesConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { selectedMetricField } = series ?? {};
|
||||
|
||||
const { hasOperationType, yAxisColumns } = seriesConfig;
|
||||
|
||||
const columnType = getColumnType(seriesConfig, selectedMetricField);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<ReportDefinitionCol seriesId={seriesId} series={series} seriesConfig={seriesConfig} />
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem style={{ minWidth: 600 }}>
|
||||
<EuiFormRow label={FILTERS_LABEL} fullWidth>
|
||||
<SeriesFilter seriesConfig={seriesConfig} seriesId={seriesId} series={series} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{(hasOperationType || columnType === 'operation') && (
|
||||
<EuiFlexItem grow={false} style={{ minWidth: 200 }}>
|
||||
<EuiFormRow label={OPERATION_LABEL}>
|
||||
<OperationTypeSelect
|
||||
seriesId={seriesId}
|
||||
series={series}
|
||||
defaultOperationType={yAxisColumns[0].operationType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', {
|
||||
defaultMessage: 'Filters',
|
||||
});
|
||||
|
||||
const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', {
|
||||
defaultMessage: 'Operation',
|
||||
});
|
|
@ -1,101 +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 React from 'react';
|
||||
import { EuiSuperSelect, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { SeriesConfig, SeriesUrl } from '../types';
|
||||
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
defaultValue?: string;
|
||||
metricOptions: SeriesConfig['metricOptions'];
|
||||
}
|
||||
|
||||
const SELECT_REPORT_METRIC = 'SELECT_REPORT_METRIC';
|
||||
|
||||
export function ReportMetricOptions({ seriesId, series, metricOptions }: Props) {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
|
||||
const { indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
selectedMetricField: value,
|
||||
});
|
||||
};
|
||||
|
||||
if (!series.dataType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const indexPattern = indexPatterns?.[series.dataType];
|
||||
|
||||
const options = (metricOptions ?? []).map(({ label, field, id }) => {
|
||||
let disabled = false;
|
||||
|
||||
if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) {
|
||||
disabled = !Boolean(indexPattern?.getFieldByName(field));
|
||||
}
|
||||
return {
|
||||
disabled,
|
||||
value: field || id,
|
||||
dropdownDisplay: disabled ? (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.observability.expView.seriesEditor.selectReportMetric.noFieldData"
|
||||
defaultMessage="No data available for field {field}."
|
||||
values={{
|
||||
field: <strong>{field}</strong>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span>{label}</span>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
label
|
||||
),
|
||||
inputDisplay: label,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
options={
|
||||
series.selectedMetricField
|
||||
? options
|
||||
: [
|
||||
{
|
||||
value: SELECT_REPORT_METRIC,
|
||||
inputDisplay: SELECT_REPORT_METRIC_LABEL,
|
||||
disabled: false,
|
||||
},
|
||||
...options,
|
||||
]
|
||||
}
|
||||
valueOfSelected={series.selectedMetricField || SELECT_REPORT_METRIC}
|
||||
onChange={(value) => onChange(value)}
|
||||
style={{ minWidth: 220 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SELECT_REPORT_METRIC_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.seriesEditor.selectReportMetric',
|
||||
{
|
||||
defaultMessage: 'Select report metric',
|
||||
}
|
||||
);
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers';
|
||||
import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers';
|
||||
import { SelectedFilters } from './selected_filters';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
|
||||
|
@ -22,19 +22,11 @@ describe('SelectedFilters', function () {
|
|||
});
|
||||
|
||||
it('should render properly', async function () {
|
||||
const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }];
|
||||
const initSeries = { filters };
|
||||
const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] };
|
||||
|
||||
render(
|
||||
<SelectedFilters
|
||||
seriesId={0}
|
||||
seriesConfig={dataViewSeries}
|
||||
series={{ ...mockUxSeries, filters }}
|
||||
/>,
|
||||
{
|
||||
initSeries,
|
||||
}
|
||||
);
|
||||
render(<SelectedFilters seriesId={'series-id'} seriesConfig={dataViewSeries} />, {
|
||||
initSeries,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('Chrome');
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { FilterLabel } from '../components/filter_label';
|
||||
import { SeriesConfig, UrlFilter } from '../types';
|
||||
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { useSeriesFilters } from '../hooks/use_series_filters';
|
||||
import { getFiltersFromDefs } from '../hooks/use_lens_attributes';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
seriesConfig: SeriesConfig;
|
||||
isNew?: boolean;
|
||||
}
|
||||
export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) {
|
||||
const { getSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const { reportDefinitions = {} } = series;
|
||||
|
||||
const { labels } = seriesConfig;
|
||||
|
||||
const filters: UrlFilter[] = series.filters ?? [];
|
||||
|
||||
let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions);
|
||||
|
||||
// we don't want to display report definition filters in new series view
|
||||
if (isNew) {
|
||||
definitionFilters = [];
|
||||
}
|
||||
|
||||
const { removeFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup wrap gutterSize="xs">
|
||||
{filters.map(({ field, values, notValues }) => (
|
||||
<Fragment key={field}>
|
||||
{(values ?? []).map((val) => (
|
||||
<EuiFlexItem key={field + val} grow={false} style={{ maxWidth: 300 }}>
|
||||
<FilterLabel
|
||||
seriesId={seriesId}
|
||||
field={field}
|
||||
label={labels[field]}
|
||||
value={val}
|
||||
removeFilter={() => removeFilter({ field, value: val, negate: false })}
|
||||
negate={false}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{(notValues ?? []).map((val) => (
|
||||
<EuiFlexItem key={field + val} grow={false} style={{ maxWidth: 300 }}>
|
||||
<FilterLabel
|
||||
seriesId={seriesId}
|
||||
field={field}
|
||||
label={labels[field]}
|
||||
value={val}
|
||||
negate={true}
|
||||
removeFilter={() => removeFilter({ field, value: val, negate: true })}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{definitionFilters.map(({ field, values }) => (
|
||||
<Fragment key={field}>
|
||||
{(values ?? []).map((val) => (
|
||||
<EuiFlexItem key={field + val} grow={false}>
|
||||
<FilterLabel
|
||||
seriesId={seriesId}
|
||||
field={field}
|
||||
label={labels[field]}
|
||||
value={val}
|
||||
removeFilter={() => {
|
||||
// FIXME handle this use case
|
||||
}}
|
||||
negate={false}
|
||||
definitionFilter={true}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
}
|
|
@ -5,399 +5,134 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiSpacer,
|
||||
EuiFormRow,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { rgba } from 'polished';
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types';
|
||||
import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { SeriesFilter } from './columns/series_filter';
|
||||
import { SeriesConfig } from '../types';
|
||||
import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
import { SeriesActions } from '../series_viewer/columns/series_actions';
|
||||
import { SeriesInfo } from '../series_viewer/columns/series_info';
|
||||
import { DataTypesSelect } from './columns/data_type_select';
|
||||
import { DatePickerCol } from './columns/date_picker_col';
|
||||
import { ExpandedSeriesRow } from './expanded_series_row';
|
||||
import { SeriesName } from '../series_viewer/columns/series_name';
|
||||
import { ReportTypesSelect } from './columns/report_type_select';
|
||||
import { ViewActions } from '../views/view_actions';
|
||||
import { ReportMetricOptions } from './report_metric_options';
|
||||
import { Breakdowns } from '../series_viewer/columns/breakdowns';
|
||||
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { SeriesActions } from './columns/series_actions';
|
||||
import { ChartEditOptions } from './chart_edit_options';
|
||||
|
||||
export interface ReportTypeItem {
|
||||
id: string;
|
||||
reportType: ReportViewType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface BuilderItem {
|
||||
id: number;
|
||||
series: SeriesUrl;
|
||||
interface EditItem {
|
||||
seriesConfig: SeriesConfig;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ExpandedRowMap = Record<string, JSX.Element>;
|
||||
|
||||
export const getSeriesToEdit = ({
|
||||
indexPatterns,
|
||||
allSeries,
|
||||
reportType,
|
||||
}: {
|
||||
allSeries: SeriesContextValue['allSeries'];
|
||||
indexPatterns: IndexPatternState;
|
||||
reportType: ReportViewType;
|
||||
}): BuilderItem[] => {
|
||||
const getDataViewSeries = (dataType: AppDataType) => {
|
||||
if (indexPatterns?.[dataType]) {
|
||||
return getDefaultConfigs({
|
||||
dataType,
|
||||
reportType,
|
||||
indexPattern: indexPatterns[dataType],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return allSeries.map((series, seriesIndex) => {
|
||||
const seriesConfig = getDataViewSeries(series.dataType)!;
|
||||
|
||||
return { id: seriesIndex, series, seriesConfig };
|
||||
});
|
||||
};
|
||||
|
||||
export const SeriesEditor = React.memo(function () {
|
||||
const [editorItems, setEditorItems] = useState<BuilderItem[]>([]);
|
||||
|
||||
const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage();
|
||||
|
||||
const { loading, indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newExpandRows: ExpandedRowMap = {};
|
||||
|
||||
setEditorItems((prevState) => {
|
||||
const newEditorItems = getSeriesToEdit({
|
||||
reportType,
|
||||
allSeries,
|
||||
indexPatterns,
|
||||
});
|
||||
|
||||
newEditorItems.forEach(({ series, id, seriesConfig }) => {
|
||||
const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id);
|
||||
if (
|
||||
prevSeriesItem &&
|
||||
series.selectedMetricField &&
|
||||
prevSeriesItem.series.selectedMetricField !== series.selectedMetricField
|
||||
) {
|
||||
newExpandRows[id] = (
|
||||
<ExpandedSeriesRow seriesId={id} series={series} seriesConfig={seriesConfig} />
|
||||
);
|
||||
}
|
||||
});
|
||||
return [...newEditorItems];
|
||||
});
|
||||
|
||||
setItemIdToExpandedRowMap((prevState) => {
|
||||
return { ...prevState, ...newExpandRows };
|
||||
});
|
||||
}, [allSeries, getSeries, indexPatterns, loading, reportType]);
|
||||
|
||||
useEffect(() => {
|
||||
setItemIdToExpandedRowMap((prevState) => {
|
||||
const itemIdToExpandedRowMapValues = { ...prevState };
|
||||
|
||||
const newEditorItems = getSeriesToEdit({
|
||||
reportType,
|
||||
allSeries,
|
||||
indexPatterns,
|
||||
});
|
||||
|
||||
newEditorItems.forEach((item) => {
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
itemIdToExpandedRowMapValues[item.id] = (
|
||||
<ExpandedSeriesRow
|
||||
seriesId={item.id}
|
||||
series={item.series}
|
||||
seriesConfig={item.seriesConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
return itemIdToExpandedRowMapValues;
|
||||
});
|
||||
}, [allSeries, editorItems, indexPatterns, reportType]);
|
||||
|
||||
const toggleDetails = (item: BuilderItem) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
itemIdToExpandedRowMapValues[item.id] = (
|
||||
<ExpandedSeriesRow
|
||||
seriesId={item.id}
|
||||
series={item.series}
|
||||
seriesConfig={item.seriesConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
};
|
||||
export function SeriesEditor() {
|
||||
const { allSeries, allSeriesIds } = useSeriesStorage();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
align: 'left' as const,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
field: 'id',
|
||||
name: '',
|
||||
render: (id: number, item: BuilderItem) =>
|
||||
item.series.dataType && item.series.selectedMetricField ? (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
isDisabled={!item.series.dataType || !item.series.selectedMetricField}
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? COLLAPSE_LABEL : EXPAND_LABEL}
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
field: 'id',
|
||||
width: '40px',
|
||||
render: (seriesId: number, { seriesConfig, series }: BuilderItem) => (
|
||||
<SeriesInfo seriesId={seriesId} series={series} seriesConfig={seriesConfig} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.name', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
field: 'id',
|
||||
width: '20%',
|
||||
render: (seriesId: number, { series }: BuilderItem) => (
|
||||
<SeriesName seriesId={seriesId} series={series} />
|
||||
width: '15%',
|
||||
render: (seriesId: string) => (
|
||||
<EuiText>
|
||||
<EuiIcon type="dot" color="green" size="l" />{' '}
|
||||
{seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId}
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.dataType', {
|
||||
defaultMessage: 'Data type',
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
field: 'id',
|
||||
field: 'defaultFilters',
|
||||
width: '15%',
|
||||
render: (seriesId: number, { series }: BuilderItem) => (
|
||||
<DataTypesSelect seriesId={seriesId} series={series} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.reportMetric', {
|
||||
defaultMessage: 'Report metric',
|
||||
}),
|
||||
field: 'id',
|
||||
width: '15%',
|
||||
render: (seriesId: number, { seriesConfig, series }: BuilderItem) => (
|
||||
<ReportMetricOptions
|
||||
series={series}
|
||||
seriesId={seriesId}
|
||||
metricOptions={seriesConfig?.metricOptions}
|
||||
render: (seriesId: string, { seriesConfig, id }: EditItem) => (
|
||||
<SeriesFilter
|
||||
filterFields={seriesConfig.filterFields}
|
||||
seriesId={id}
|
||||
seriesConfig={seriesConfig}
|
||||
baseFilters={seriesConfig.baseFilters}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.time', {
|
||||
defaultMessage: 'Time',
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
|
||||
defaultMessage: 'Breakdowns',
|
||||
}),
|
||||
field: 'id',
|
||||
width: '27%',
|
||||
render: (seriesId: number, { series }: BuilderItem) => (
|
||||
<DatePickerCol seriesId={seriesId} series={series} />
|
||||
width: '25%',
|
||||
render: (seriesId: string, { seriesConfig, id }: EditItem) => (
|
||||
<ChartEditOptions
|
||||
seriesId={id}
|
||||
breakdownFields={seriesConfig.breakdownFields}
|
||||
seriesConfig={seriesConfig}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdownBy', {
|
||||
defaultMessage: 'Breakdown by',
|
||||
}),
|
||||
width: '10%',
|
||||
name: (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.expView.seriesEditor.time"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
render: (seriesId: number, { series, seriesConfig }: BuilderItem) => (
|
||||
<Breakdowns seriesConfig={seriesConfig} seriesId={seriesId} series={series} />
|
||||
),
|
||||
align: 'right' as const,
|
||||
render: (seriesId: string, item: EditItem) => <DatePickerCol seriesId={seriesId} />,
|
||||
},
|
||||
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', {
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
align: 'center' as const,
|
||||
width: '8%',
|
||||
width: '10%',
|
||||
field: 'id',
|
||||
render: (seriesId: number, { series, seriesConfig }: BuilderItem) => (
|
||||
<SeriesActions seriesId={seriesId} series={series} seriesConfig={seriesConfig} />
|
||||
),
|
||||
render: (seriesId: string, item: EditItem) => <SeriesActions seriesId={seriesId} />,
|
||||
},
|
||||
];
|
||||
|
||||
const getRowProps = (item: BuilderItem) => {
|
||||
const { dataType, reportDefinitions, selectedMetricField } = item.series;
|
||||
const { indexPatterns } = useAppIndexPatternContext();
|
||||
const items: EditItem[] = [];
|
||||
|
||||
return {
|
||||
className: classNames({
|
||||
isExpanded: itemIdToExpandedRowMap[item.id],
|
||||
isIncomplete: !dataType || isEmpty(reportDefinitions) || !selectedMetricField,
|
||||
}),
|
||||
// commenting this for now, since adding on click on row, blocks adding space
|
||||
// into text field for name column
|
||||
// ...(dataType && selectedMetricField
|
||||
// ? {
|
||||
// onClick: (evt: MouseEvent) => {
|
||||
// const targetElem = evt.target as HTMLElement;
|
||||
//
|
||||
// if (
|
||||
// targetElem.classList.contains('euiTableCellContent') &&
|
||||
// targetElem.tagName !== 'BUTTON'
|
||||
// ) {
|
||||
// toggleDetails(item);
|
||||
// }
|
||||
// evt.stopPropagation();
|
||||
// evt.preventDefault();
|
||||
// },
|
||||
// }
|
||||
// : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const resetView = () => {
|
||||
const totalSeries = allSeries.length;
|
||||
for (let i = totalSeries; i >= 0; i--) {
|
||||
removeSeries(i);
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
const series = allSeries[seriesKey];
|
||||
if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
|
||||
items.push({
|
||||
id: seriesKey,
|
||||
seriesConfig: getDefaultConfigs({
|
||||
indexPattern: indexPatterns[series.dataType],
|
||||
reportType: series.reportType,
|
||||
dataType: series.dataType,
|
||||
}),
|
||||
});
|
||||
}
|
||||
setEditorItems([]);
|
||||
setItemIdToExpandedRowMap({});
|
||||
};
|
||||
});
|
||||
|
||||
if (items.length === 0 && allSeriesIds.length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow label={REPORT_TYPE_LABEL} display="columnCompressed">
|
||||
<ReportTypesSelect />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
{reportType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={() => resetView()} color="text">
|
||||
{RESET_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<ViewActions />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{editorItems.length > 0 && (
|
||||
<EuiBasicTable
|
||||
loading={loading}
|
||||
items={editorItems}
|
||||
columns={columns}
|
||||
tableLayout="auto"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
itemId="id"
|
||||
rowProps={getRowProps}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
</div>
|
||||
</Wrapper>
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiBasicTable
|
||||
items={items}
|
||||
rowHeader="firstName"
|
||||
columns={columns}
|
||||
noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', {
|
||||
defaultMessage: 'No series found. Please add a series.',
|
||||
})}
|
||||
cellProps={{
|
||||
style: {
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
}}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const Wrapper = euiStyled.div`
|
||||
max-height: 50vh;
|
||||
&::-webkit-scrollbar {
|
||||
height: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
width: ${({ theme }) => theme.eui.euiScrollBar};
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: content-box;
|
||||
background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
|
||||
border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&&& {
|
||||
.euiTableRow-isExpandedRow .euiTableRowCell {
|
||||
border-top: none;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 2px solid #d3dae6;
|
||||
border-right: 2px solid rgb(211, 218, 230);
|
||||
border-left: 2px solid rgb(211, 218, 230);
|
||||
}
|
||||
|
||||
.isExpanded {
|
||||
border-right: 2px solid rgb(211, 218, 230);
|
||||
border-left: 2px solid rgb(211, 218, 230);
|
||||
.euiTableRowCell {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.isIncomplete .euiTableRowCell {
|
||||
background-color: rgba(254, 197, 20, 0.1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LOADING_VIEW = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.loadingView',
|
||||
{
|
||||
defaultMessage: 'Loading view ...',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_REPORT_TYPE = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.selectReportType',
|
||||
{
|
||||
defaultMessage: 'No report type selected',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', {
|
||||
defaultMessage: 'Reset',
|
||||
});
|
||||
|
||||
export const REPORT_TYPE_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.reportType',
|
||||
{
|
||||
defaultMessage: 'Report type',
|
||||
}
|
||||
);
|
||||
|
||||
const COLLAPSE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.collapse', {
|
||||
defaultMessage: 'Collapse',
|
||||
});
|
||||
|
||||
const EXPAND_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.expand', {
|
||||
defaultMessage: 'Exapnd',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,70 +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 React, { useState } from 'react';
|
||||
import { EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
useKibana,
|
||||
ToolbarButton,
|
||||
} from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
|
||||
import { SeriesUrl, useFetcher } from '../../../../..';
|
||||
import { SeriesConfig } from '../../types';
|
||||
import { SeriesChartTypesSelect } from '../../series_editor/columns/chart_types';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesConfig: SeriesConfig;
|
||||
}
|
||||
|
||||
export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) {
|
||||
const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType;
|
||||
|
||||
const {
|
||||
services: { lens },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
button={
|
||||
<EuiToolTip content={EDIT_CHART_TYPE_LABEL}>
|
||||
<ToolbarButton
|
||||
size="s"
|
||||
iconType={(data ?? []).find(({ id }) => id === seriesType)?.icon!}
|
||||
aria-label={CHART_TYPE_LABEL}
|
||||
onClick={() => setIsPopoverOpen((prevState) => !prevState)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
>
|
||||
<SeriesChartTypesSelect
|
||||
seriesId={seriesId}
|
||||
series={series}
|
||||
defaultChartType={seriesConfig.defaultSeriesType}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
const EDIT_CHART_TYPE_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.seriesEditor.editChartSeriesLabel',
|
||||
{
|
||||
defaultMessage: 'Edit chart type for series',
|
||||
}
|
||||
);
|
||||
|
||||
const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', {
|
||||
defaultMessage: 'Chart type',
|
||||
});
|
|
@ -1,51 +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 { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
}
|
||||
|
||||
export function RemoveSeries({ seriesId }: Props) {
|
||||
const { removeSeries, allSeries } = useSeriesStorage();
|
||||
|
||||
const onClick = () => {
|
||||
removeSeries(seriesId);
|
||||
};
|
||||
|
||||
const isDisabled = seriesId === 0 && allSeries.length > 1;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
isDisabled
|
||||
? i18n.translate('xpack.observability.expView.seriesEditor.removeSeriesDisabled', {
|
||||
defaultMessage:
|
||||
'Main series cannot be removed. Please remove all series below before you can remove this.',
|
||||
})
|
||||
: i18n.translate('xpack.observability.expView.seriesEditor.removeSeries', {
|
||||
defaultMessage: 'Remove series',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.observability.expView.seriesEditor.removeSeries', {
|
||||
defaultMessage: 'Remove series',
|
||||
})}
|
||||
iconType="trash"
|
||||
color="text"
|
||||
onClick={onClick}
|
||||
size="s"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -1,104 +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 React from 'react';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RemoveSeries } from './remove_series';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesConfig, SeriesUrl } from '../../types';
|
||||
import { useDiscoverLink } from '../../hooks/use_discover_link';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesConfig: SeriesConfig;
|
||||
}
|
||||
export function SeriesActions({ seriesId, series, seriesConfig }: Props) {
|
||||
const { setSeries, allSeries } = useSeriesStorage();
|
||||
|
||||
const { href: discoverHref } = useDiscoverLink({ series, seriesConfig });
|
||||
|
||||
const copySeries = () => {
|
||||
let copySeriesId: string = `${series.name}-copy`;
|
||||
if (allSeries.find(({ name }) => name === copySeriesId)) {
|
||||
copySeriesId = copySeriesId + allSeries.length;
|
||||
}
|
||||
setSeries(allSeries.length, { ...series, name: copySeriesId });
|
||||
};
|
||||
|
||||
const toggleSeries = () => {
|
||||
if (series.hidden) {
|
||||
setSeries(seriesId, { ...series, hidden: undefined });
|
||||
} else {
|
||||
setSeries(seriesId, { ...series, hidden: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.observability.seriesEditor.sampleDocuments', {
|
||||
defaultMessage: 'View sample documents in new tab',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="discoverApp"
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.sampleDocuments', {
|
||||
defaultMessage: 'View sample documents in new tab',
|
||||
})}
|
||||
size="s"
|
||||
color="text"
|
||||
target="_blank"
|
||||
href={discoverHref}
|
||||
isDisabled={!series.dataType || !series.selectedMetricField}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.observability.seriesEditor.hide', {
|
||||
defaultMessage: 'Hide series',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType={series.hidden ? 'eyeClosed' : 'eye'}
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.hide', {
|
||||
defaultMessage: 'Hide series',
|
||||
})}
|
||||
size="s"
|
||||
color="text"
|
||||
onClick={toggleSeries}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.observability.seriesEditor.clone', {
|
||||
defaultMessage: 'Copy series',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType={'copy'}
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.clone', {
|
||||
defaultMessage: 'Copy series',
|
||||
})}
|
||||
size="s"
|
||||
onClick={copySeries}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveSeries seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,69 +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 React from 'react';
|
||||
import { EuiFilterGroup, EuiSpacer } from '@elastic/eui';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { FilterExpanded } from './filter_expanded';
|
||||
import { SeriesConfig, SeriesUrl } from '../../types';
|
||||
import { FieldLabels } from '../../configurations/constants/constants';
|
||||
import { SelectedFilters } from '../selected_filters';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
seriesConfig: SeriesConfig;
|
||||
series: SeriesUrl;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
label: string;
|
||||
field: string;
|
||||
nested?: string;
|
||||
isNegated?: boolean;
|
||||
}
|
||||
|
||||
export function SeriesFilter({ series, seriesConfig, seriesId }: Props) {
|
||||
const isPreview = !!useRouteMatch('/exploratory-view/preview');
|
||||
|
||||
const options: Field[] = seriesConfig.filterFields.map((field) => {
|
||||
if (typeof field === 'string') {
|
||||
return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field };
|
||||
}
|
||||
|
||||
return {
|
||||
field: field.field,
|
||||
nested: field.nested,
|
||||
isNegated: field.isNegated,
|
||||
label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isPreview && (
|
||||
<>
|
||||
<EuiFilterGroup>
|
||||
{options.map((opt) => (
|
||||
<FilterExpanded
|
||||
series={series}
|
||||
key={opt.label}
|
||||
seriesId={seriesId}
|
||||
field={opt.field}
|
||||
label={opt.label}
|
||||
nestedField={opt.nested}
|
||||
isNegated={opt.isNegated}
|
||||
filters={seriesConfig.baseFilters}
|
||||
/>
|
||||
))}
|
||||
</EuiFilterGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
<SelectedFilters seriesId={seriesId} series={series} seriesConfig={seriesConfig} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,95 +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 React from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SeriesChartTypes } from './chart_types';
|
||||
import { SeriesConfig, SeriesUrl } from '../../types';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { SeriesColorPicker } from '../../components/series_color_picker';
|
||||
import { dataTypes } from '../../series_editor/columns/data_type_select';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
seriesConfig?: SeriesConfig;
|
||||
}
|
||||
|
||||
export function SeriesInfo({ seriesId, series, seriesConfig }: Props) {
|
||||
const isConfigure = !!useRouteMatch('/exploratory-view/configure');
|
||||
|
||||
const { dataType, reportDefinitions, selectedMetricField } = series;
|
||||
|
||||
const { loading } = useAppIndexPatternContext();
|
||||
|
||||
const isIncomplete =
|
||||
(!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading;
|
||||
|
||||
if (!seriesConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { definitionFields, labels } = seriesConfig;
|
||||
|
||||
const incompleteDefinition = isEmpty(reportDefinitions)
|
||||
? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', {
|
||||
defaultMessage: 'Missing {reportDefinition}',
|
||||
values: { reportDefinition: labels?.[definitionFields[0]] },
|
||||
})
|
||||
: '';
|
||||
|
||||
let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition;
|
||||
|
||||
if (!dataType) {
|
||||
incompleteMessage = MISSING_DATA_TYPE_LABEL;
|
||||
}
|
||||
|
||||
if (!isIncomplete && seriesConfig && isConfigure) {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SeriesChartTypes seriesId={seriesId} series={series} seriesConfig={seriesConfig} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SeriesColorPicker seriesId={seriesId} series={series} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isIncomplete && <EuiBadge color="warning">{incompleteMessage}</EuiBadge>}
|
||||
</EuiFlexItem>
|
||||
{!isConfigure && (
|
||||
<EuiFlexItem>
|
||||
<EuiBadgeGroup>
|
||||
<EuiBadge>{dataTypes.find(({ id }) => id === dataType)!.label}</EuiBadge>
|
||||
</EuiBadgeGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const MISSING_REPORT_METRIC_LABEL = i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.missingReportMetric',
|
||||
{
|
||||
defaultMessage: 'Missing report metric',
|
||||
}
|
||||
);
|
||||
|
||||
const MISSING_DATA_TYPE_LABEL = i18n.translate(
|
||||
'xpack.observability.overview.exploratoryView.missingDataType',
|
||||
{
|
||||
defaultMessage: 'Missing data type',
|
||||
}
|
||||
);
|
|
@ -1,38 +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 React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesUrl } from '../../types';
|
||||
|
||||
interface Props {
|
||||
seriesId: number;
|
||||
series: SeriesUrl;
|
||||
}
|
||||
|
||||
export function SeriesName({ series, seriesId }: Props) {
|
||||
const { setSeries } = useSeriesStorage();
|
||||
|
||||
const [value, setValue] = useState(series.name);
|
||||
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (value !== series.name) {
|
||||
setSeries(seriesId, { ...series, name: value });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue(series.name);
|
||||
}, [series.name]);
|
||||
|
||||
return <EuiFieldText value={value} onChange={onChange} fullWidth onBlur={onSave} />;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue