[Exploratory view] Refactor code for multi series (#101157)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
52d5b9d51d
commit
293dc95f8a
|
@ -47,10 +47,11 @@ export function UXActionMenu({
|
|||
|
||||
const uxExploratoryViewLink = createExploratoryViewUrl(
|
||||
{
|
||||
'ux-series': {
|
||||
'ux-series': ({
|
||||
dataType: 'ux',
|
||||
isNew: true,
|
||||
time: { from: rangeFrom, to: rangeTo },
|
||||
} as SeriesUrl,
|
||||
} as unknown) as SeriesUrl,
|
||||
},
|
||||
http?.basePath.get()
|
||||
);
|
||||
|
|
|
@ -89,7 +89,7 @@ export function PageLoadDistribution() {
|
|||
{
|
||||
[`${serviceName}-page-views`]: {
|
||||
dataType: 'ux',
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
'service.name': serviceName as string[],
|
||||
|
|
|
@ -64,7 +64,7 @@ export function PageViewsTrend() {
|
|||
{
|
||||
[`${serviceName}-page-views`]: {
|
||||
dataType: 'ux',
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: rangeFrom!, to: rangeTo! },
|
||||
reportDefinitions: {
|
||||
'service.name': serviceName as string[],
|
||||
|
|
|
@ -38,6 +38,12 @@ export function EmptyView({
|
|||
emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT;
|
||||
}
|
||||
|
||||
if (!series) {
|
||||
emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
|
||||
defaultMessage: 'No series found. Please add a series.',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper height={height}>
|
||||
{loading && (
|
||||
|
@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui
|
|||
export const CHOOSE_REPORT_DEFINITION = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.emptyReportDefinition',
|
||||
{
|
||||
defaultMessage: 'Select a report type to create a visualization.',
|
||||
defaultMessage: 'Select a report definition to create a visualization.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('FilterLabel', function () {
|
|||
negate={false}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -52,6 +53,7 @@ describe('FilterLabel', function () {
|
|||
negate={false}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={removeFilter}
|
||||
indexPattern={mockIndexPattern}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -74,6 +76,7 @@ describe('FilterLabel', function () {
|
|||
negate={false}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={removeFilter}
|
||||
indexPattern={mockIndexPattern}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -99,6 +102,7 @@ describe('FilterLabel', function () {
|
|||
negate={true}
|
||||
seriesId={'kpi-over-time'}
|
||||
removeFilter={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
import { useSeriesFilters } from '../hooks/use_series_filters';
|
||||
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
|
||||
|
||||
|
@ -17,6 +17,7 @@ interface Props {
|
|||
seriesId: string;
|
||||
negate: boolean;
|
||||
definitionFilter?: boolean;
|
||||
indexPattern: IndexPattern;
|
||||
removeFilter: (field: string, value: string, notVal: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -26,11 +27,10 @@ export function FilterLabel({
|
|||
field,
|
||||
value,
|
||||
negate,
|
||||
indexPattern,
|
||||
removeFilter,
|
||||
definitionFilter,
|
||||
}: Props) {
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
|
||||
const { invertFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
return indexPattern ? (
|
||||
|
|
|
@ -5,8 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReportViewTypeId } from '../../types';
|
||||
import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames';
|
||||
import { ReportViewType } from '../../types';
|
||||
import {
|
||||
CLS_FIELD,
|
||||
FCP_FIELD,
|
||||
FID_FIELD,
|
||||
LCP_FIELD,
|
||||
TBT_FIELD,
|
||||
TRANSACTION_TIME_TO_FIRST_BYTE,
|
||||
} from './elasticsearch_fieldnames';
|
||||
import {
|
||||
AGENT_HOST_LABEL,
|
||||
BROWSER_FAMILY_LABEL,
|
||||
|
@ -58,6 +65,7 @@ export const FieldLabels: Record<string, string> = {
|
|||
[TBT_FIELD]: TBT_LABEL,
|
||||
[FID_FIELD]: FID_LABEL,
|
||||
[CLS_FIELD]: CLS_LABEL,
|
||||
[TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time',
|
||||
|
||||
'monitor.id': MONITOR_ID_LABEL,
|
||||
'monitor.status': MONITOR_STATUS_LABEL,
|
||||
|
@ -77,11 +85,11 @@ export const FieldLabels: Record<string, string> = {
|
|||
'http.request.method': REQUEST_METHOD,
|
||||
};
|
||||
|
||||
export const DataViewLabels: Record<ReportViewTypeId, string> = {
|
||||
dist: PERF_DIST_LABEL,
|
||||
kpi: KPI_OVER_TIME_LABEL,
|
||||
cwv: CORE_WEB_VITALS_LABEL,
|
||||
mdd: DEVICE_DISTRIBUTION_LABEL,
|
||||
export const DataViewLabels: Record<ReportViewType, string> = {
|
||||
'data-distribution': PERF_DIST_LABEL,
|
||||
'kpi-over-time': KPI_OVER_TIME_LABEL,
|
||||
'core-web-vitals': CORE_WEB_VITALS_LABEL,
|
||||
'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
|
||||
};
|
||||
|
||||
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppDataType, ReportViewTypes } from '../types';
|
||||
import { AppDataType, ReportViewType } from '../types';
|
||||
import { getRumDistributionConfig } from './rum/data_distribution_config';
|
||||
import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config';
|
||||
import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config';
|
||||
|
@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
|
|||
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
|
||||
|
||||
interface Props {
|
||||
reportType: keyof typeof ReportViewTypes;
|
||||
reportType: ReportViewType;
|
||||
indexPattern: IndexPattern;
|
||||
dataType: AppDataType;
|
||||
}
|
||||
|
@ -25,23 +25,23 @@ interface Props {
|
|||
export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => {
|
||||
switch (dataType) {
|
||||
case 'ux':
|
||||
if (reportType === 'dist') {
|
||||
if (reportType === 'data-distribution') {
|
||||
return getRumDistributionConfig({ indexPattern });
|
||||
}
|
||||
if (reportType === 'cwv') {
|
||||
if (reportType === 'core-web-vitals') {
|
||||
return getCoreWebVitalsConfig({ indexPattern });
|
||||
}
|
||||
return getKPITrendsLensConfig({ indexPattern });
|
||||
case 'synthetics':
|
||||
if (reportType === 'dist') {
|
||||
if (reportType === 'data-distribution') {
|
||||
return getSyntheticsDistributionConfig({ indexPattern });
|
||||
}
|
||||
return getSyntheticsKPIConfig({ indexPattern });
|
||||
case 'mobile':
|
||||
if (reportType === 'dist') {
|
||||
if (reportType === 'data-distribution') {
|
||||
return getMobileKPIDistributionConfig({ indexPattern });
|
||||
}
|
||||
if (reportType === 'mdd') {
|
||||
if (reportType === 'device-data-distribution') {
|
||||
return getMobileDeviceDistributionConfig({ indexPattern });
|
||||
}
|
||||
return getMobileKPIConfig({ indexPattern });
|
||||
|
|
|
@ -5,25 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LensAttributes } from './lens_attributes';
|
||||
import { LayerConfig, LensAttributes } from './lens_attributes';
|
||||
import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers';
|
||||
import { getDefaultConfigs } from './default_configs';
|
||||
import { sampleAttribute } from './test_data/sample_attribute';
|
||||
import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
|
||||
import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames';
|
||||
import { buildExistsFilter, buildPhrasesFilter } from './utils';
|
||||
|
||||
describe('Lens Attribute', () => {
|
||||
mockAppIndexPattern();
|
||||
|
||||
const reportViewConfig = getDefaultConfigs({
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
indexPattern: mockIndexPattern,
|
||||
});
|
||||
|
||||
reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern));
|
||||
|
||||
let lnsAttr: LensAttributes;
|
||||
|
||||
const layerConfig: LayerConfig = {
|
||||
reportConfig: reportViewConfig,
|
||||
seriesType: 'line',
|
||||
operationType: 'count',
|
||||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: {},
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {});
|
||||
lnsAttr = new LensAttributes([layerConfig]);
|
||||
});
|
||||
|
||||
it('should return expected json', function () {
|
||||
|
@ -31,7 +43,7 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return main y axis', function () {
|
||||
expect(lnsAttr.getMainYAxis()).toEqual({
|
||||
expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
|
@ -42,7 +54,7 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return expected field type', function () {
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual(
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual(
|
||||
JSON.stringify({
|
||||
fieldMeta: {
|
||||
count: 0,
|
||||
|
@ -60,7 +72,7 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return expected field type for custom field with default value', function () {
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual(
|
||||
JSON.stringify({
|
||||
fieldMeta: {
|
||||
count: 0,
|
||||
|
@ -79,11 +91,18 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return expected field type for custom field with passed value', function () {
|
||||
lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {
|
||||
'performance.metric': [LCP_FIELD],
|
||||
});
|
||||
const layerConfig1: LayerConfig = {
|
||||
reportConfig: reportViewConfig,
|
||||
seriesType: 'line',
|
||||
operationType: 'count',
|
||||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
};
|
||||
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
|
||||
lnsAttr = new LensAttributes([layerConfig1]);
|
||||
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual(
|
||||
JSON.stringify({
|
||||
fieldMeta: {
|
||||
count: 0,
|
||||
|
@ -102,7 +121,7 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return expected number range column', function () {
|
||||
expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
|
||||
expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
|
@ -124,7 +143,7 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return expected number operation column', function () {
|
||||
expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({
|
||||
expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
|
@ -160,7 +179,7 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return main x axis', function () {
|
||||
expect(lnsAttr.getXAxis()).toEqual({
|
||||
expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
|
@ -182,38 +201,45 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
|
||||
it('should return first layer', function () {
|
||||
expect(lnsAttr.getLayer()).toEqual({
|
||||
columnOrder: ['x-axis-column', 'y-axis-column'],
|
||||
columns: {
|
||||
'x-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [
|
||||
{
|
||||
from: 0,
|
||||
label: '',
|
||||
to: 1000,
|
||||
},
|
||||
],
|
||||
type: 'histogram',
|
||||
expect(lnsAttr.getLayers()).toEqual({
|
||||
layer0: {
|
||||
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
|
||||
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',
|
||||
},
|
||||
'y-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: {
|
||||
language: 'kuery',
|
||||
query:
|
||||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
},
|
||||
'y-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -225,12 +251,12 @@ describe('Lens Attribute', () => {
|
|||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
accessors: ['y-axis-column-layer0'],
|
||||
layerId: 'layer0',
|
||||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
},
|
||||
],
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
|
@ -240,108 +266,52 @@ describe('Lens Attribute', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('ParseFilters function', function () {
|
||||
it('should parse default filters', function () {
|
||||
expect(lnsAttr.parseFilters()).toEqual([
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse default and ui filters', function () {
|
||||
lnsAttr = new LensAttributes(
|
||||
mockIndexPattern,
|
||||
reportViewConfig,
|
||||
'line',
|
||||
[
|
||||
{ field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] },
|
||||
{ field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] },
|
||||
],
|
||||
'count',
|
||||
{}
|
||||
);
|
||||
|
||||
expect(lnsAttr.parseFilters()).toEqual([
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
|
||||
{
|
||||
meta: {
|
||||
index: 'apm-*',
|
||||
key: 'service.name',
|
||||
params: ['elastic-co', 'kibana-front'],
|
||||
type: 'phrases',
|
||||
value: 'elastic-co, kibana-front',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'service.name': 'elastic-co',
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
'service.name': 'kibana-front',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'apm-*',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'user_agent.name': 'Firefox',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'apm-*',
|
||||
negate: true,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'user_agent.name': 'Chrome',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layer breakdowns', function () {
|
||||
it('should add breakdown column', function () {
|
||||
lnsAttr.addBreakdown(USER_AGENT_NAME);
|
||||
it('should return breakdown column', function () {
|
||||
const layerConfig1: LayerConfig = {
|
||||
reportConfig: reportViewConfig,
|
||||
seriesType: 'line',
|
||||
operationType: 'count',
|
||||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
|
||||
breakdown: USER_AGENT_NAME,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
};
|
||||
|
||||
lnsAttr = new LensAttributes([layerConfig1]);
|
||||
|
||||
lnsAttr.getBreakdownColumn({
|
||||
sourceField: USER_AGENT_NAME,
|
||||
layerId: 'layer0',
|
||||
indexPattern: mockIndexPattern,
|
||||
});
|
||||
|
||||
expect(lnsAttr.visualization.layers).toEqual([
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
accessors: ['y-axis-column-layer0'],
|
||||
layerId: 'layer0',
|
||||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
splitAccessor: 'break-down-column',
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
|
||||
splitAccessor: 'breakdown-column-layer0',
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(lnsAttr.layers.layer1).toEqual({
|
||||
columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'],
|
||||
expect(lnsAttr.layers.layer0).toEqual({
|
||||
columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'],
|
||||
columns: {
|
||||
'break-down-column': {
|
||||
'breakdown-column-layer0': {
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Top values of Browser family',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
missingBucket: false,
|
||||
orderBy: { columnId: 'y-axis-column', type: 'column' },
|
||||
orderBy: {
|
||||
columnId: 'y-axis-column-layer0',
|
||||
type: 'column',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
size: 10,
|
||||
|
@ -349,10 +319,10 @@ describe('Lens Attribute', () => {
|
|||
scale: 'ordinal',
|
||||
sourceField: 'user_agent.name',
|
||||
},
|
||||
'x-axis-column': {
|
||||
'x-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
label: 'Largest contentful paint',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
|
@ -360,62 +330,47 @@ describe('Lens Attribute', () => {
|
|||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
sourceField: 'transaction.marks.agent.largestContentfulPaint',
|
||||
},
|
||||
'y-axis-column': {
|
||||
'y-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: {
|
||||
language: 'kuery',
|
||||
query:
|
||||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove breakdown column', function () {
|
||||
lnsAttr.addBreakdown(USER_AGENT_NAME);
|
||||
describe('Layer Filters', function () {
|
||||
it('should return expected filters', function () {
|
||||
reportViewConfig.filters?.push(
|
||||
...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern)
|
||||
);
|
||||
|
||||
lnsAttr.removeBreakdown();
|
||||
const layerConfig1: LayerConfig = {
|
||||
reportConfig: reportViewConfig,
|
||||
seriesType: 'line',
|
||||
operationType: 'count',
|
||||
indexPattern: mockIndexPattern,
|
||||
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
};
|
||||
|
||||
expect(lnsAttr.visualization.layers).toEqual([
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
|
||||
},
|
||||
]);
|
||||
const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
|
||||
|
||||
expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']);
|
||||
|
||||
expect(lnsAttr.layers.layer1.columns).toEqual({
|
||||
'x-axis-column': {
|
||||
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',
|
||||
},
|
||||
'y-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
});
|
||||
expect(filters).toEqual(
|
||||
'@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,13 +27,12 @@ import {
|
|||
TermsIndexPatternColumn,
|
||||
CardinalityIndexPatternColumn,
|
||||
} from '../../../../../../lens/public';
|
||||
import {
|
||||
buildPhraseFilter,
|
||||
buildPhrasesFilter,
|
||||
IndexPattern,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { urlFiltersToKueryString } from '../utils/stringify_kueries';
|
||||
import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants';
|
||||
import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types';
|
||||
import { PersistableFilter } from '../../../../../../lens/common';
|
||||
import { parseAbsoluteDate } from '../series_date_picker/date_range_picker';
|
||||
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
return `indexpattern-datasource-layer-${layerId}`;
|
||||
|
@ -87,46 +86,50 @@ export const parseCustomFieldName = (
|
|||
return { fieldName, columnType, columnFilters, timeScale, columnLabel };
|
||||
};
|
||||
|
||||
export class LensAttributes {
|
||||
export interface LayerConfig {
|
||||
filters?: UrlFilter[];
|
||||
reportConfig: DataSeries;
|
||||
breakdown?: string;
|
||||
seriesType?: SeriesType;
|
||||
operationType?: OperationType;
|
||||
reportDefinitions: URLReportDefinition;
|
||||
time: { to: string; from: string };
|
||||
indexPattern: IndexPattern;
|
||||
}
|
||||
|
||||
export class LensAttributes {
|
||||
layers: Record<string, PersistedIndexPatternLayer>;
|
||||
visualization: XYState;
|
||||
filters: UrlFilter[];
|
||||
seriesType: SeriesType;
|
||||
reportViewConfig: DataSeries;
|
||||
reportDefinitions: URLReportDefinition;
|
||||
breakdownSource?: string;
|
||||
layerConfigs: LayerConfig[];
|
||||
|
||||
constructor(
|
||||
indexPattern: IndexPattern,
|
||||
reportViewConfig: DataSeries,
|
||||
seriesType?: SeriesType,
|
||||
filters?: UrlFilter[],
|
||||
operationType?: OperationType,
|
||||
reportDefinitions?: URLReportDefinition,
|
||||
breakdownSource?: string
|
||||
) {
|
||||
this.indexPattern = indexPattern;
|
||||
constructor(layerConfigs: LayerConfig[]) {
|
||||
this.layers = {};
|
||||
this.filters = filters ?? [];
|
||||
this.reportDefinitions = reportDefinitions ?? {};
|
||||
this.breakdownSource = breakdownSource;
|
||||
|
||||
if (operationType) {
|
||||
reportViewConfig.yAxisColumns.forEach((yAxisColumn) => {
|
||||
if (typeof yAxisColumn.operationType !== undefined) {
|
||||
yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType;
|
||||
this.reportViewConfig = reportViewConfig;
|
||||
this.layers.layer1 = this.getLayer();
|
||||
layerConfigs.forEach(({ reportConfig, operationType }) => {
|
||||
if (operationType) {
|
||||
reportConfig.yAxisColumns.forEach((yAxisColumn) => {
|
||||
if (typeof yAxisColumn.operationType !== undefined) {
|
||||
yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType'];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.layerConfigs = layerConfigs;
|
||||
this.layers = this.getLayers();
|
||||
this.visualization = this.getXyState();
|
||||
}
|
||||
|
||||
getBreakdownColumn(sourceField: string): TermsIndexPatternColumn {
|
||||
const fieldMeta = this.indexPattern.getFieldByName(sourceField);
|
||||
getBreakdownColumn({
|
||||
sourceField,
|
||||
layerId,
|
||||
indexPattern,
|
||||
}: {
|
||||
sourceField: string;
|
||||
layerId: string;
|
||||
indexPattern: IndexPattern;
|
||||
}): TermsIndexPatternColumn {
|
||||
const fieldMeta = indexPattern.getFieldByName(sourceField);
|
||||
|
||||
return {
|
||||
sourceField,
|
||||
|
@ -136,8 +139,8 @@ export class LensAttributes {
|
|||
scale: 'ordinal',
|
||||
isBucketed: true,
|
||||
params: {
|
||||
orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` },
|
||||
size: 10,
|
||||
orderBy: { type: 'column', columnId: 'y-axis-column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
missingBucket: false,
|
||||
|
@ -145,36 +148,14 @@ export class LensAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
addBreakdown(sourceField: string) {
|
||||
const { xAxisColumn } = this.reportViewConfig;
|
||||
if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) {
|
||||
// do nothing since this will be used a x axis source
|
||||
return;
|
||||
}
|
||||
this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField);
|
||||
|
||||
this.layers.layer1.columnOrder = [
|
||||
'x-axis-column',
|
||||
'break-down-column',
|
||||
'y-axis-column',
|
||||
...Object.keys(this.getChildYAxises()),
|
||||
];
|
||||
|
||||
this.visualization.layers[0].splitAccessor = 'break-down-column';
|
||||
}
|
||||
|
||||
removeBreakdown() {
|
||||
delete this.layers.layer1.columns['break-down-column'];
|
||||
|
||||
this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column'];
|
||||
|
||||
this.visualization.layers[0].splitAccessor = undefined;
|
||||
}
|
||||
|
||||
getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn {
|
||||
getNumberRangeColumn(
|
||||
sourceField: string,
|
||||
reportViewConfig: DataSeries,
|
||||
label?: string
|
||||
): RangeIndexPatternColumn {
|
||||
return {
|
||||
sourceField,
|
||||
label: this.reportViewConfig.labels[sourceField] ?? label,
|
||||
label: reportViewConfig.labels[sourceField] ?? label,
|
||||
dataType: 'number',
|
||||
operationType: 'range',
|
||||
isBucketed: true,
|
||||
|
@ -187,16 +168,36 @@ export class LensAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
getCardinalityColumn(sourceField: string, label?: string) {
|
||||
return this.getNumberOperationColumn(sourceField, 'unique_count', label);
|
||||
getCardinalityColumn({
|
||||
sourceField,
|
||||
label,
|
||||
reportViewConfig,
|
||||
}: {
|
||||
sourceField: string;
|
||||
label?: string;
|
||||
reportViewConfig: DataSeries;
|
||||
}) {
|
||||
return this.getNumberOperationColumn({
|
||||
sourceField,
|
||||
operationType: 'unique_count',
|
||||
label,
|
||||
reportViewConfig,
|
||||
});
|
||||
}
|
||||
|
||||
getNumberColumn(
|
||||
sourceField: string,
|
||||
columnType?: string,
|
||||
operationType?: string,
|
||||
label?: string
|
||||
) {
|
||||
getNumberColumn({
|
||||
reportViewConfig,
|
||||
label,
|
||||
sourceField,
|
||||
columnType,
|
||||
operationType,
|
||||
}: {
|
||||
sourceField: string;
|
||||
columnType?: string;
|
||||
operationType?: string;
|
||||
label?: string;
|
||||
reportViewConfig: DataSeries;
|
||||
}) {
|
||||
if (columnType === 'operation' || operationType) {
|
||||
if (
|
||||
operationType === 'median' ||
|
||||
|
@ -204,48 +205,58 @@ export class LensAttributes {
|
|||
operationType === 'sum' ||
|
||||
operationType === 'unique_count'
|
||||
) {
|
||||
return this.getNumberOperationColumn(sourceField, operationType, label);
|
||||
return this.getNumberOperationColumn({
|
||||
sourceField,
|
||||
operationType,
|
||||
label,
|
||||
reportViewConfig,
|
||||
});
|
||||
}
|
||||
if (operationType?.includes('th')) {
|
||||
return this.getPercentileNumberColumn(sourceField, operationType);
|
||||
return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!);
|
||||
}
|
||||
}
|
||||
return this.getNumberRangeColumn(sourceField, label);
|
||||
return this.getNumberRangeColumn(sourceField, reportViewConfig!, label);
|
||||
}
|
||||
|
||||
getNumberOperationColumn(
|
||||
sourceField: string,
|
||||
operationType: 'average' | 'median' | 'sum' | 'unique_count',
|
||||
label?: string
|
||||
):
|
||||
getNumberOperationColumn({
|
||||
sourceField,
|
||||
label,
|
||||
reportViewConfig,
|
||||
operationType,
|
||||
}: {
|
||||
sourceField: string;
|
||||
operationType: 'average' | 'median' | 'sum' | 'unique_count';
|
||||
label?: string;
|
||||
reportViewConfig: DataSeries;
|
||||
}):
|
||||
| AvgIndexPatternColumn
|
||||
| MedianIndexPatternColumn
|
||||
| SumIndexPatternColumn
|
||||
| CardinalityIndexPatternColumn {
|
||||
return {
|
||||
...buildNumberColumn(sourceField),
|
||||
label:
|
||||
label ||
|
||||
i18n.translate('xpack.observability.expView.columns.operation.label', {
|
||||
defaultMessage: '{operationType} of {sourceField}',
|
||||
values: {
|
||||
sourceField: this.reportViewConfig.labels[sourceField],
|
||||
operationType: capitalize(operationType),
|
||||
},
|
||||
}),
|
||||
label: i18n.translate('xpack.observability.expView.columns.operation.label', {
|
||||
defaultMessage: '{operationType} of {sourceField}',
|
||||
values: {
|
||||
sourceField: label || reportViewConfig.labels[sourceField],
|
||||
operationType: capitalize(operationType),
|
||||
},
|
||||
}),
|
||||
operationType,
|
||||
};
|
||||
}
|
||||
|
||||
getPercentileNumberColumn(
|
||||
sourceField: string,
|
||||
percentileValue: string
|
||||
percentileValue: string,
|
||||
reportViewConfig: DataSeries
|
||||
): PercentileIndexPatternColumn {
|
||||
return {
|
||||
...buildNumberColumn(sourceField),
|
||||
label: i18n.translate('xpack.observability.expView.columns.label', {
|
||||
defaultMessage: '{percentileValue} percentile of {sourceField}',
|
||||
values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue },
|
||||
values: { sourceField: reportViewConfig.labels[sourceField], percentileValue },
|
||||
}),
|
||||
operationType: 'percentile',
|
||||
params: { percentile: Number(percentileValue.split('th')[0]) },
|
||||
|
@ -268,7 +279,7 @@ export class LensAttributes {
|
|||
return {
|
||||
operationType: 'terms',
|
||||
sourceField,
|
||||
label: label || 'Top values of ' + sourceField,
|
||||
label: 'Top values of ' + label || sourceField,
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
scale: 'ordinal',
|
||||
|
@ -283,30 +294,45 @@ export class LensAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
getXAxis() {
|
||||
const { xAxisColumn } = this.reportViewConfig;
|
||||
getXAxis(layerConfig: LayerConfig, layerId: string) {
|
||||
const { xAxisColumn } = layerConfig.reportConfig;
|
||||
|
||||
if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) {
|
||||
return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]);
|
||||
return this.getBreakdownColumn({
|
||||
layerId,
|
||||
indexPattern: layerConfig.indexPattern,
|
||||
sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0],
|
||||
});
|
||||
}
|
||||
|
||||
return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label);
|
||||
return this.getColumnBasedOnType({
|
||||
layerConfig,
|
||||
label: xAxisColumn.label,
|
||||
sourceField: xAxisColumn.sourceField!,
|
||||
});
|
||||
}
|
||||
|
||||
getColumnBasedOnType(
|
||||
sourceField: string,
|
||||
operationType?: OperationType,
|
||||
label?: string,
|
||||
colIndex?: number
|
||||
) {
|
||||
getColumnBasedOnType({
|
||||
sourceField,
|
||||
label,
|
||||
layerConfig,
|
||||
operationType,
|
||||
colIndex,
|
||||
}: {
|
||||
sourceField: string;
|
||||
operationType?: OperationType;
|
||||
label?: string;
|
||||
layerConfig: LayerConfig;
|
||||
colIndex?: number;
|
||||
}) {
|
||||
const {
|
||||
fieldMeta,
|
||||
columnType,
|
||||
fieldName,
|
||||
columnFilters,
|
||||
timeScale,
|
||||
columnLabel,
|
||||
} = this.getFieldMeta(sourceField);
|
||||
timeScale,
|
||||
columnFilters,
|
||||
} = this.getFieldMeta(sourceField, layerConfig);
|
||||
const { type: fieldType } = fieldMeta ?? {};
|
||||
|
||||
if (columnType === TERMS_COLUMN) {
|
||||
|
@ -325,47 +351,76 @@ export class LensAttributes {
|
|||
return this.getDateHistogramColumn(fieldName);
|
||||
}
|
||||
if (fieldType === 'number') {
|
||||
return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label);
|
||||
return this.getNumberColumn({
|
||||
sourceField: fieldName,
|
||||
columnType,
|
||||
operationType,
|
||||
label: columnLabel || label,
|
||||
reportViewConfig: layerConfig.reportConfig,
|
||||
});
|
||||
}
|
||||
if (operationType === 'unique_count') {
|
||||
return this.getCardinalityColumn(fieldName, columnLabel || label);
|
||||
return this.getCardinalityColumn({
|
||||
sourceField: fieldName,
|
||||
label: columnLabel || label,
|
||||
reportViewConfig: layerConfig.reportConfig,
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME review my approach again
|
||||
return this.getDateHistogramColumn(fieldName);
|
||||
}
|
||||
|
||||
getCustomFieldName(sourceField: string) {
|
||||
return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions);
|
||||
getCustomFieldName({
|
||||
sourceField,
|
||||
layerConfig,
|
||||
}: {
|
||||
sourceField: string;
|
||||
layerConfig: LayerConfig;
|
||||
}) {
|
||||
return parseCustomFieldName(
|
||||
sourceField,
|
||||
layerConfig.reportConfig,
|
||||
layerConfig.reportDefinitions
|
||||
);
|
||||
}
|
||||
|
||||
getFieldMeta(sourceField: string) {
|
||||
getFieldMeta(sourceField: string, layerConfig: LayerConfig) {
|
||||
const {
|
||||
fieldName,
|
||||
columnType,
|
||||
columnLabel,
|
||||
columnFilters,
|
||||
timeScale,
|
||||
columnLabel,
|
||||
} = this.getCustomFieldName(sourceField);
|
||||
} = this.getCustomFieldName({
|
||||
sourceField,
|
||||
layerConfig,
|
||||
});
|
||||
|
||||
const fieldMeta = this.indexPattern.getFieldByName(fieldName);
|
||||
const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName);
|
||||
|
||||
return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel };
|
||||
return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale };
|
||||
}
|
||||
|
||||
getMainYAxis() {
|
||||
const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0];
|
||||
getMainYAxis(layerConfig: LayerConfig) {
|
||||
const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0];
|
||||
|
||||
if (sourceField === 'Records' || !sourceField) {
|
||||
return this.getRecordsColumn(label);
|
||||
}
|
||||
|
||||
return this.getColumnBasedOnType(sourceField!, operationType, label, 0);
|
||||
return this.getColumnBasedOnType({
|
||||
sourceField,
|
||||
operationType,
|
||||
label,
|
||||
layerConfig,
|
||||
colIndex: 0,
|
||||
});
|
||||
}
|
||||
|
||||
getChildYAxises() {
|
||||
getChildYAxises(layerConfig: LayerConfig) {
|
||||
const lensColumns: Record<string, FieldBasedIndexPatternColumn | SumIndexPatternColumn> = {};
|
||||
const yAxisColumns = this.reportViewConfig.yAxisColumns;
|
||||
const yAxisColumns = layerConfig.reportConfig.yAxisColumns;
|
||||
// 1 means there is only main y axis
|
||||
if (yAxisColumns.length === 1) {
|
||||
return lensColumns;
|
||||
|
@ -373,12 +428,13 @@ export class LensAttributes {
|
|||
for (let i = 1; i < yAxisColumns.length; i++) {
|
||||
const { sourceField, operationType, label } = yAxisColumns[i];
|
||||
|
||||
lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType(
|
||||
sourceField!,
|
||||
lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({
|
||||
sourceField: sourceField!,
|
||||
operationType,
|
||||
label,
|
||||
i
|
||||
);
|
||||
layerConfig,
|
||||
colIndex: i,
|
||||
});
|
||||
}
|
||||
return lensColumns;
|
||||
}
|
||||
|
@ -396,20 +452,139 @@ export class LensAttributes {
|
|||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: columnFilter,
|
||||
timeScale,
|
||||
...(timeScale ? { timeScale } : {}),
|
||||
} as CountIndexPatternColumn;
|
||||
}
|
||||
|
||||
getLayer() {
|
||||
return {
|
||||
columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())],
|
||||
columns: {
|
||||
'x-axis-column': this.getXAxis(),
|
||||
'y-axis-column': this.getMainYAxis(),
|
||||
...this.getChildYAxises(),
|
||||
},
|
||||
incompleteColumns: {},
|
||||
};
|
||||
getLayerFilters(layerConfig: LayerConfig, totalLayers: number) {
|
||||
const {
|
||||
filters,
|
||||
time: { from, to },
|
||||
reportConfig: { filters: layerFilters, reportType },
|
||||
} = layerConfig;
|
||||
let baseFilters = '';
|
||||
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 >= ${from} and @timestamp <= ${to}`;
|
||||
}
|
||||
|
||||
layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => {
|
||||
const qFilter = filter as PersistableFilter;
|
||||
if (qFilter.query?.match_phrase) {
|
||||
const fieldName = Object.keys(qFilter.query.match_phrase)[0];
|
||||
const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`;
|
||||
if (baseFilters.length > 0) {
|
||||
baseFilters += ` and ${kql}`;
|
||||
} else {
|
||||
baseFilters += kql;
|
||||
}
|
||||
}
|
||||
if (qFilter.query?.bool?.should) {
|
||||
const values: string[] = [];
|
||||
let fieldName = '';
|
||||
qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => {
|
||||
if (ft.match_phrase) {
|
||||
fieldName = Object.keys(ft.match_phrase)[0];
|
||||
values.push(ft.match_phrase[fieldName]);
|
||||
}
|
||||
});
|
||||
|
||||
const kueryString = `${fieldName}: (${values.join(' or ')})`;
|
||||
|
||||
if (baseFilters.length > 0) {
|
||||
baseFilters += ` and ${kueryString}`;
|
||||
} else {
|
||||
baseFilters += kueryString;
|
||||
}
|
||||
}
|
||||
const existFilter = filter as ExistsFilter;
|
||||
|
||||
if (existFilter.exists) {
|
||||
const fieldName = existFilter.exists.field;
|
||||
const kql = `${fieldName} : *`;
|
||||
if (baseFilters.length > 0) {
|
||||
baseFilters += ` and ${kql}`;
|
||||
} else {
|
||||
baseFilters += kql;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rFilters = urlFiltersToKueryString(filters ?? []);
|
||||
if (!baseFilters) {
|
||||
return rFilters;
|
||||
}
|
||||
if (!rFilters) {
|
||||
return baseFilters;
|
||||
}
|
||||
return `${rFilters} and ${baseFilters}`;
|
||||
}
|
||||
|
||||
getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) {
|
||||
if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
time: { from: mainFrom },
|
||||
} = mainLayerConfig;
|
||||
|
||||
const {
|
||||
time: { from },
|
||||
} = layerConfig;
|
||||
|
||||
const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days');
|
||||
if (inDays > 1) {
|
||||
return inDays + 'd';
|
||||
}
|
||||
const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours');
|
||||
return inHours + 'h';
|
||||
}
|
||||
|
||||
getLayers() {
|
||||
const layers: Record<string, PersistedIndexPatternLayer> = {};
|
||||
const layerConfigs = this.layerConfigs;
|
||||
|
||||
layerConfigs.forEach((layerConfig, index) => {
|
||||
const { breakdown } = layerConfig;
|
||||
|
||||
const layerId = `layer${index}`;
|
||||
const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length);
|
||||
const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index);
|
||||
const mainYAxis = this.getMainYAxis(layerConfig);
|
||||
layers[layerId] = {
|
||||
columnOrder: [
|
||||
`x-axis-column-${layerId}`,
|
||||
...(breakdown ? [`breakdown-column-${layerId}`] : []),
|
||||
`y-axis-column-${layerId}`,
|
||||
...Object.keys(this.getChildYAxises(layerConfig)),
|
||||
],
|
||||
columns: {
|
||||
[`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId),
|
||||
[`y-axis-column-${layerId}`]: {
|
||||
...mainYAxis,
|
||||
label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label,
|
||||
filter: { query: columnFilter, language: 'kuery' },
|
||||
...(timeShift ? { timeShift } : {}),
|
||||
},
|
||||
...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN
|
||||
? // do nothing since this will be used a x axis source
|
||||
{
|
||||
[`breakdown-column-${layerId}`]: this.getBreakdownColumn({
|
||||
layerId,
|
||||
sourceField: breakdown,
|
||||
indexPattern: layerConfig.indexPattern,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...this.getChildYAxises(layerConfig),
|
||||
},
|
||||
incompleteColumns: {},
|
||||
};
|
||||
});
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
getXyState(): XYState {
|
||||
|
@ -422,71 +597,48 @@ export class LensAttributes {
|
|||
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())],
|
||||
layerId: 'layer1',
|
||||
seriesType: this.seriesType ?? 'line',
|
||||
palette: this.reportViewConfig.palette,
|
||||
yConfig: this.reportViewConfig.yConfig || [
|
||||
{ forAccessor: 'y-axis-column', color: 'green' },
|
||||
],
|
||||
xAccessor: 'x-axis-column',
|
||||
},
|
||||
],
|
||||
...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}),
|
||||
layers: this.layerConfigs.map((layerConfig, index) => ({
|
||||
accessors: [
|
||||
`y-axis-column-layer${index}`,
|
||||
...Object.keys(this.getChildYAxises(layerConfig)),
|
||||
],
|
||||
layerId: `layer${index}`,
|
||||
seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType,
|
||||
palette: layerConfig.reportConfig.palette,
|
||||
yConfig: layerConfig.reportConfig.yConfig || [
|
||||
{ forAccessor: `y-axis-column-layer${index}` },
|
||||
],
|
||||
xAccessor: `x-axis-column-layer${index}`,
|
||||
...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}),
|
||||
})),
|
||||
...(this.layerConfigs[0].reportConfig.yTitle
|
||||
? { yTitle: this.layerConfigs[0].reportConfig.yTitle }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
parseFilters() {
|
||||
const defaultFilters = this.reportViewConfig.filters ?? [];
|
||||
const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : [];
|
||||
|
||||
this.filters.forEach(({ field, values = [], notValues = [] }) => {
|
||||
const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!;
|
||||
|
||||
if (values?.length > 0) {
|
||||
if (values?.length > 1) {
|
||||
const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern);
|
||||
parsedFilters.push(multiFilter);
|
||||
} else {
|
||||
const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern);
|
||||
parsedFilters.push(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (notValues?.length > 0) {
|
||||
if (notValues?.length > 1) {
|
||||
const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern);
|
||||
multiFilter.meta.negate = true;
|
||||
parsedFilters.push(multiFilter);
|
||||
} else {
|
||||
const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern);
|
||||
filter.meta.negate = true;
|
||||
parsedFilters.push(filter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return parsedFilters;
|
||||
}
|
||||
parseFilters() {}
|
||||
|
||||
getJSON(): TypedLensByValueInput['attributes'] {
|
||||
const uniqueIndexPatternsIds = Array.from(
|
||||
new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)])
|
||||
);
|
||||
|
||||
return {
|
||||
title: 'Prefilled from exploratory view app',
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
{
|
||||
id: this.indexPattern.id!,
|
||||
...uniqueIndexPatternsIds.map((patternId) => ({
|
||||
id: patternId!,
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: this.indexPattern.id!,
|
||||
name: getLayerReferenceName('layer1'),
|
||||
})),
|
||||
...this.layerConfigs.map(({ indexPattern }, index) => ({
|
||||
id: indexPattern.id!,
|
||||
name: getLayerReferenceName(`layer${index}`),
|
||||
type: 'index-pattern',
|
||||
},
|
||||
})),
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
|
@ -496,7 +648,7 @@ export class LensAttributes {
|
|||
},
|
||||
visualization: this.visualization,
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: this.parseFilters(),
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields';
|
|||
|
||||
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
reportType: 'mobile-device-distribution',
|
||||
reportType: 'device-data-distribution',
|
||||
defaultSeriesType: 'bar',
|
||||
seriesTypes: ['bar', 'bar_horizontal'],
|
||||
xAxisColumn: {
|
||||
|
|
|
@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants';
|
|||
import { buildExistsFilter } from '../utils';
|
||||
import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels';
|
||||
|
||||
export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
|
||||
export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
reportType: 'data-distribution',
|
||||
defaultSeriesType: 'line',
|
||||
defaultSeriesType: series?.seriesType || 'line',
|
||||
seriesTypes: [],
|
||||
xAxisColumn: {
|
||||
sourceField: 'performance.metric',
|
||||
|
|
|
@ -10,16 +10,16 @@ export const sampleAttribute = {
|
|||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' },
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' },
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
layer1: {
|
||||
columnOrder: ['x-axis-column', 'y-axis-column'],
|
||||
layer0: {
|
||||
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'],
|
||||
columns: {
|
||||
'x-axis-column': {
|
||||
'x-axis-column-layer0': {
|
||||
sourceField: 'transaction.duration.us',
|
||||
label: 'Page load time',
|
||||
dataType: 'number',
|
||||
|
@ -32,13 +32,18 @@ export const sampleAttribute = {
|
|||
maxBars: 'auto',
|
||||
},
|
||||
},
|
||||
'y-axis-column': {
|
||||
'y-axis-column-layer0': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: {
|
||||
language: 'kuery',
|
||||
query:
|
||||
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
|
||||
},
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
|
@ -57,18 +62,15 @@ export const sampleAttribute = {
|
|||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
accessors: ['y-axis-column-layer0'],
|
||||
layerId: 'layer0',
|
||||
seriesType: 'line',
|
||||
yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }],
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ forAccessor: 'y-axis-column-layer0' }],
|
||||
xAccessor: 'x-axis-column-layer0',
|
||||
},
|
||||
],
|
||||
},
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
|
||||
],
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import type { SeriesUrl, UrlFilter } from '../types';
|
||||
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
|
||||
import type { SeriesUrl } from '../types';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
|
||||
import { esFilters } from '../../../../../../../../src/plugins/data/public';
|
||||
import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public';
|
||||
import { URL_KEYS } from './constants/url_constants';
|
||||
import { PersistableFilter } from '../../../../../../lens/common';
|
||||
|
||||
export function convertToShortUrl(series: SeriesUrl) {
|
||||
const {
|
||||
|
@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
|
|||
}
|
||||
|
||||
export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) {
|
||||
const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
|
||||
const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
|
||||
if (fieldMeta) {
|
||||
return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)];
|
||||
}
|
||||
|
@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II
|
|||
}
|
||||
|
||||
export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) {
|
||||
const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
|
||||
const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
|
||||
if (fieldMeta) {
|
||||
return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)];
|
||||
}
|
||||
|
@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern:
|
|||
}
|
||||
|
||||
export function buildExistsFilter(field: string, indexPattern: IIndexPattern) {
|
||||
const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field);
|
||||
const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field);
|
||||
if (fieldMeta) {
|
||||
return [esFilters.buildExistsFilter(fieldMeta, indexPattern)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
type FiltersType = PersistableFilter[] | ExistsFilter[];
|
||||
|
||||
export function urlFilterToPersistedFilter({
|
||||
urlFilters,
|
||||
initFilters,
|
||||
indexPattern,
|
||||
}: {
|
||||
urlFilters: UrlFilter[];
|
||||
initFilters: FiltersType;
|
||||
indexPattern: IIndexPattern;
|
||||
}) {
|
||||
const parsedFilters: FiltersType = initFilters ? [...initFilters] : [];
|
||||
|
||||
urlFilters.forEach(({ field, values = [], notValues = [] }) => {
|
||||
if (values?.length > 0) {
|
||||
const filter = buildPhrasesFilter(field, values, indexPattern);
|
||||
parsedFilters.push(...filter);
|
||||
}
|
||||
|
||||
if (notValues?.length > 0) {
|
||||
const filter = buildPhrasesFilter(field, notValues, indexPattern)[0];
|
||||
filter.meta.negate = true;
|
||||
parsedFilters.push(filter);
|
||||
}
|
||||
});
|
||||
|
||||
return parsedFilters;
|
||||
}
|
||||
|
|
|
@ -51,8 +51,9 @@ describe('ExploratoryView', () => {
|
|||
const initSeries = {
|
||||
data: {
|
||||
'ux-series': {
|
||||
isNew: true,
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'dist' as const,
|
||||
reportType: 'data-distribution' as const,
|
||||
breakdown: 'user_agent .name',
|
||||
reportDefinitions: { 'service.name': ['elastic-co'] },
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../plugin';
|
||||
import { ExploratoryViewHeader } from './header/header';
|
||||
|
@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view';
|
|||
import { TypedLensByValueInput } from '../../../../../lens/public';
|
||||
import { useAppIndexPatternContext } from './hooks/use_app_index_pattern';
|
||||
import { SeriesBuilder } from './series_builder/series_builder';
|
||||
import { SeriesUrl } from './types';
|
||||
|
||||
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 {
|
||||
|
@ -33,6 +61,8 @@ export function ExploratoryView({
|
|||
const [height, setHeight] = useState<string>('100vh');
|
||||
const [seriesId, setSeriesId] = useState<string>('');
|
||||
|
||||
const [lastUpdated, setLastUpdated] = useState<number | undefined>();
|
||||
|
||||
const [lensAttributes, setLensAttributes] = useState<TypedLensByValueInput['attributes'] | null>(
|
||||
null
|
||||
);
|
||||
|
@ -47,9 +77,7 @@ export function ExploratoryView({
|
|||
setSeriesId(firstSeriesId);
|
||||
}, [allSeries, firstSeriesId]);
|
||||
|
||||
const lensAttributesT = useLensAttributes({
|
||||
seriesId,
|
||||
});
|
||||
const lensAttributesT = useLensAttributes();
|
||||
|
||||
const setHeightOffset = () => {
|
||||
if (seriesBuilderRef?.current && wrapperRef.current) {
|
||||
|
@ -60,10 +88,12 @@ export function ExploratoryView({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (series?.dataType) {
|
||||
loadIndexPattern({ dataType: series?.dataType });
|
||||
}
|
||||
}, [series?.dataType, loadIndexPattern]);
|
||||
Object.values(allSeries).forEach((seriesT) => {
|
||||
loadIndexPattern({
|
||||
dataType: seriesT.dataType,
|
||||
});
|
||||
});
|
||||
}, [allSeries, loadIndexPattern]);
|
||||
|
||||
useEffect(() => {
|
||||
setLensAttributes(lensAttributesT);
|
||||
|
@ -72,47 +102,62 @@ export function ExploratoryView({
|
|||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]);
|
||||
}, [JSON.stringify(lensAttributesT ?? {})]);
|
||||
|
||||
useEffect(() => {
|
||||
setHeightOffset();
|
||||
});
|
||||
|
||||
const timeRange = combineTimeRanges(allSeries, series);
|
||||
|
||||
const onLensLoad = useCallback(() => {
|
||||
setLastUpdated(Date.now());
|
||||
}, []);
|
||||
|
||||
const onBrushEnd = useCallback(
|
||||
({ range }: { range: number[] }) => {
|
||||
if (series?.reportType !== 'data-distribution') {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
time: {
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
notifications?.toasts.add(
|
||||
i18n.translate('xpack.observability.exploratoryView.noBrusing', {
|
||||
defaultMessage: 'Zoom by brush selection is only available on time series charts.',
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[notifications?.toasts, series, seriesId, setSeries]
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{lens ? (
|
||||
<>
|
||||
<ExploratoryViewHeader lensAttributes={lensAttributes} seriesId={seriesId} />
|
||||
<LensWrapper ref={wrapperRef} height={height}>
|
||||
{lensAttributes && seriesId && series?.reportType && series?.time ? (
|
||||
{lensAttributes && timeRange.to && timeRange.from ? (
|
||||
<LensComponent
|
||||
id="exploratoryView"
|
||||
timeRange={series?.time}
|
||||
timeRange={timeRange}
|
||||
attributes={lensAttributes}
|
||||
onBrushEnd={({ range }) => {
|
||||
if (series?.reportType !== 'dist') {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
time: {
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
notifications?.toasts.add(
|
||||
i18n.translate('xpack.observability.exploratoryView.noBrusing', {
|
||||
defaultMessage:
|
||||
'Zoom by brush selection is only available on time series charts.',
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
onLoad={onLensLoad}
|
||||
onBrushEnd={onBrushEnd}
|
||||
/>
|
||||
) : (
|
||||
<EmptyView series={series} loading={loading} height={height} />
|
||||
)}
|
||||
</LensWrapper>
|
||||
<SeriesBuilder seriesId={seriesId} seriesBuilderRef={seriesBuilderRef} />
|
||||
<SeriesBuilder
|
||||
seriesBuilderRef={seriesBuilderRef}
|
||||
lastUpdated={lastUpdated}
|
||||
multiSeries={multiSeries}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EuiTitle>
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () {
|
|||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ
|
|||
import { DataViewLabels } from '../configurations/constants';
|
||||
import { ObservabilityAppServices } from '../../../../application/types';
|
||||
import { useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { combineTimeRanges } from '../exploratory_view';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
|
@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
|||
|
||||
const { lens } = kServices;
|
||||
|
||||
const { getSeries } = useSeriesStorage();
|
||||
const { getSeries, allSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
|
@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
|||
|
||||
const LensSaveModalComponent = lens.SaveModalComponent;
|
||||
|
||||
const timeRange = combineTimeRanges(allSeries, series);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
|
@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
|||
lens.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange: series.time,
|
||||
timeRange,
|
||||
attributes: lensAttributes,
|
||||
},
|
||||
true
|
||||
|
|
|
@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler';
|
|||
|
||||
export interface IIndexPatternContext {
|
||||
loading: boolean;
|
||||
selectedApp: AppDataType;
|
||||
indexPatterns: IndexPatternState;
|
||||
hasAppData: HasAppDataState;
|
||||
loadIndexPattern: (params: { dataType: AppDataType }) => void;
|
||||
|
@ -29,10 +28,10 @@ interface ProviderProps {
|
|||
|
||||
type HasAppDataState = Record<AppDataType, boolean | null>;
|
||||
type IndexPatternState = Record<AppDataType, IndexPattern>;
|
||||
type LoadingState = Record<AppDataType, boolean>;
|
||||
|
||||
export function IndexPatternContextProvider({ children }: ProviderProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedApp, setSelectedApp] = useState<AppDataType>();
|
||||
const [loading, setLoading] = useState<LoadingState>({} as LoadingState);
|
||||
const [indexPatterns, setIndexPatterns] = useState<IndexPatternState>({} as IndexPatternState);
|
||||
const [hasAppData, setHasAppData] = useState<HasAppDataState>({
|
||||
infra_metrics: null,
|
||||
|
@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
|
|||
|
||||
const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback(
|
||||
async ({ dataType }) => {
|
||||
setSelectedApp(dataType);
|
||||
if (hasAppData[dataType] === null && !loading[dataType]) {
|
||||
setLoading((prevState) => ({ ...prevState, [dataType]: true }));
|
||||
|
||||
if (hasAppData[dataType] === null) {
|
||||
setLoading(true);
|
||||
try {
|
||||
let hasDataT = false;
|
||||
let indices: string | undefined = '';
|
||||
|
@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
|
|||
|
||||
setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern }));
|
||||
}
|
||||
setLoading(false);
|
||||
setLoading((prevState) => ({ ...prevState, [dataType]: false }));
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
setLoading((prevState) => ({ ...prevState, [dataType]: false }));
|
||||
}
|
||||
}
|
||||
},
|
||||
[data, hasAppData]
|
||||
[data, hasAppData, loading]
|
||||
);
|
||||
|
||||
return (
|
||||
<IndexPatternContext.Provider
|
||||
value={{
|
||||
loading,
|
||||
hasAppData,
|
||||
selectedApp,
|
||||
indexPatterns,
|
||||
loadIndexPattern,
|
||||
loading: !!Object.values(loading).find((loadingT) => loadingT),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export const useAppIndexPatternContext = () => {
|
||||
const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext(
|
||||
export const useAppIndexPatternContext = (dataType?: AppDataType) => {
|
||||
const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext(
|
||||
(IndexPatternContext as unknown) as Context<IIndexPatternContext>
|
||||
);
|
||||
|
||||
if (dataType && !indexPatterns?.[dataType] && !loading) {
|
||||
loadIndexPattern({ dataType });
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
hasAppData,
|
||||
selectedApp,
|
||||
loading,
|
||||
indexPattern: indexPatterns?.[selectedApp],
|
||||
hasData: hasAppData?.[selectedApp],
|
||||
indexPatterns,
|
||||
indexPattern: dataType ? indexPatterns?.[dataType] : undefined,
|
||||
hasData: dataType ? hasAppData?.[dataType] : undefined,
|
||||
loadIndexPattern,
|
||||
};
|
||||
}, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]);
|
||||
}, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]);
|
||||
};
|
||||
|
|
|
@ -8,17 +8,13 @@
|
|||
import { useMemo } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { LensAttributes } from '../configurations/lens_attributes';
|
||||
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
|
||||
import { useSeriesStorage } from './use_series_storage';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
|
||||
import { DataSeries, SeriesUrl, UrlFilter } from '../types';
|
||||
import { useAppIndexPatternContext } from './use_app_index_pattern';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export const getFiltersFromDefs = (
|
||||
reportDefinitions: SeriesUrl['reportDefinitions'],
|
||||
dataViewConfig: DataSeries
|
||||
|
@ -37,54 +33,51 @@ export const getFiltersFromDefs = (
|
|||
});
|
||||
};
|
||||
|
||||
export const useLensAttributes = ({
|
||||
seriesId,
|
||||
}: Props): TypedLensByValueInput['attributes'] | null => {
|
||||
const { getSeries } = useSeriesStorage();
|
||||
const series = getSeries(seriesId);
|
||||
const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } =
|
||||
series ?? {};
|
||||
export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
|
||||
const { allSeriesIds, allSeries } = useSeriesStorage();
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
const { indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!indexPattern || !reportType || isEmpty(reportDefinitions)) {
|
||||
if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataViewConfig = getDefaultConfigs({
|
||||
reportType,
|
||||
dataType,
|
||||
indexPattern,
|
||||
const layerConfigs: LayerConfig[] = [];
|
||||
|
||||
allSeriesIds.forEach((seriesIdT) => {
|
||||
const seriesT = allSeries[seriesIdT];
|
||||
const indexPattern = indexPatterns?.[seriesT?.dataType];
|
||||
if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) {
|
||||
const reportViewConfig = getDefaultConfigs({
|
||||
reportType: seriesT.reportType,
|
||||
dataType: seriesT.dataType,
|
||||
indexPattern,
|
||||
});
|
||||
|
||||
const filters: UrlFilter[] = (seriesT.filters ?? []).concat(
|
||||
getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig)
|
||||
);
|
||||
|
||||
layerConfigs.push({
|
||||
filters,
|
||||
indexPattern,
|
||||
reportConfig: reportViewConfig,
|
||||
breakdown: seriesT.breakdown,
|
||||
operationType: seriesT.operationType,
|
||||
seriesType: seriesT.seriesType,
|
||||
reportDefinitions: seriesT.reportDefinitions ?? {},
|
||||
time: seriesT.time,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const filters: UrlFilter[] = (series.filters ?? []).concat(
|
||||
getFiltersFromDefs(reportDefinitions, dataViewConfig)
|
||||
);
|
||||
|
||||
const lensAttributes = new LensAttributes(
|
||||
indexPattern,
|
||||
dataViewConfig,
|
||||
seriesType,
|
||||
filters,
|
||||
operationType,
|
||||
reportDefinitions,
|
||||
breakdown
|
||||
);
|
||||
|
||||
if (breakdown) {
|
||||
lensAttributes.addBreakdown(breakdown);
|
||||
if (layerConfigs.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lensAttributes = new LensAttributes(layerConfigs);
|
||||
|
||||
return lensAttributes.getJSON();
|
||||
}, [
|
||||
indexPattern,
|
||||
reportType,
|
||||
reportDefinitions,
|
||||
dataType,
|
||||
series.filters,
|
||||
seriesType,
|
||||
operationType,
|
||||
breakdown,
|
||||
]);
|
||||
}, [indexPatterns, allSeriesIds, allSeries]);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import type {
|
||||
AppDataType,
|
||||
ReportViewTypeId,
|
||||
ReportViewType,
|
||||
SeriesUrl,
|
||||
UrlFilter,
|
||||
URLReportDefinition,
|
||||
|
@ -36,6 +36,16 @@ interface ProviderProps {
|
|||
storage: IKbnUrlStateStorage | ISessionStorageStateStorage;
|
||||
}
|
||||
|
||||
function convertAllShortSeries(allShortSeries: AllShortSeries) {
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
const allSeriesN: AllSeries = {};
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
|
||||
});
|
||||
|
||||
return allSeriesN;
|
||||
}
|
||||
|
||||
export function UrlStorageContextProvider({
|
||||
children,
|
||||
storage,
|
||||
|
@ -45,15 +55,14 @@ export function UrlStorageContextProvider({
|
|||
const [allShortSeries, setAllShortSeries] = useState<AllShortSeries>(
|
||||
() => storage.get(allSeriesKey) ?? {}
|
||||
);
|
||||
const [allSeries, setAllSeries] = useState<AllSeries>({});
|
||||
const [allSeries, setAllSeries] = useState<AllSeries>(() =>
|
||||
convertAllShortSeries(storage.get(allSeriesKey) ?? {})
|
||||
);
|
||||
const [firstSeriesId, setFirstSeriesId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
const allSeriesN: AllSeries = {};
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
|
||||
});
|
||||
const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {});
|
||||
|
||||
setAllSeries(allSeriesN);
|
||||
setFirstSeriesId(allSeriesIds?.[0]);
|
||||
|
@ -68,8 +77,10 @@ export function UrlStorageContextProvider({
|
|||
};
|
||||
|
||||
const removeSeries = (seriesIdN: string) => {
|
||||
delete allShortSeries[seriesIdN];
|
||||
delete allSeries[seriesIdN];
|
||||
setAllShortSeries((prevState) => {
|
||||
delete prevState[seriesIdN];
|
||||
return { ...prevState };
|
||||
});
|
||||
};
|
||||
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
|
@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
|
|||
|
||||
interface ShortUrlSeries {
|
||||
[URL_KEYS.OPERATION_TYPE]?: OperationType;
|
||||
[URL_KEYS.REPORT_TYPE]?: ReportViewTypeId;
|
||||
[URL_KEYS.REPORT_TYPE]?: ReportViewType;
|
||||
[URL_KEYS.DATA_TYPE]?: AppDataType;
|
||||
[URL_KEYS.SERIES_TYPE]?: SeriesType;
|
||||
[URL_KEYS.BREAK_DOWN]?: string;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi
|
|||
import indexPatternData from './configurations/test_data/test_index_pattern.json';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
|
||||
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { UrlFilter } from './types';
|
||||
import {
|
||||
IndexPattern,
|
||||
IndexPatternsContract,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { AppDataType, UrlFilter } from './types';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { ListItem } from '../../../hooks/use_values_list';
|
||||
|
||||
|
@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => {
|
|||
const loadIndexPattern = jest.fn();
|
||||
const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({
|
||||
indexPattern: mockIndexPattern,
|
||||
selectedApp: 'ux',
|
||||
hasData: true,
|
||||
loading: false,
|
||||
hasAppData: { ux: true } as any,
|
||||
loadIndexPattern,
|
||||
indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record<AppDataType, IndexPattern>,
|
||||
});
|
||||
return { spy, loadIndexPattern };
|
||||
};
|
||||
|
@ -260,7 +263,7 @@ function mockSeriesStorageContext({
|
|||
}) {
|
||||
const mockDataSeries = data || {
|
||||
'performance-distribution': {
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
breakdown: breakdown || 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
|
|
|
@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({
|
|||
seriesTypes?: SeriesType[];
|
||||
defaultChartType: SeriesType;
|
||||
}) {
|
||||
const { getSeries, setSeries, allSeries } = useSeriesStorage();
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const seriesType = series?.seriesType ?? defaultChartType;
|
||||
|
||||
const onChange = (value: SeriesType) => {
|
||||
Object.keys(allSeries).forEach((seriesKey) => {
|
||||
const seriesN = allSeries[seriesKey];
|
||||
|
||||
setSeries(seriesKey, { ...seriesN, seriesType: value });
|
||||
});
|
||||
setSeries(seriesId, { ...series, seriesType: value });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -29,7 +29,14 @@ describe('DataTypesCol', function () {
|
|||
fireEvent.click(screen.getByText(/user experience \(rum\)/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' });
|
||||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'ux',
|
||||
isNew: true,
|
||||
time: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set series on change on already selected', function () {
|
||||
|
@ -37,7 +44,7 @@ describe('DataTypesCol', function () {
|
|||
data: {
|
||||
[seriesId]: {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
|
|
|
@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) {
|
|||
if (!dataType) {
|
||||
removeSeries(seriesId);
|
||||
} else {
|
||||
setSeries(seriesId || `${dataType}-series`, { dataType } as any);
|
||||
setSeries(seriesId || `${dataType}-series`, {
|
||||
dataType,
|
||||
isNew: true,
|
||||
time: series.time,
|
||||
} as any);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -8,14 +8,23 @@
|
|||
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>
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
{firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
) : (
|
||||
<DateRangePicker seriesId={seriesId} />
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () {
|
|||
data: {
|
||||
'performance-distribution': {
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
operationType: 'median' as const,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () {
|
|||
data: {
|
||||
'series-id': {
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
operationType: 'median' as const,
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
|
@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
operationType: 'median',
|
||||
dataType: 'ux',
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
|
||||
|
@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
operationType: '95th',
|
||||
dataType: 'ux',
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel
|
|||
describe('Series Builder ReportBreakdowns', function () {
|
||||
const seriesId = 'test-series-id';
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
dataType: 'ux',
|
||||
indexPattern: mockIndexPattern,
|
||||
});
|
||||
|
@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
breakdown: USER_AGENT_OS,
|
||||
dataType: 'ux',
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
|
@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
breakdown: undefined,
|
||||
dataType: 'ux',
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () {
|
|||
const seriesId = 'test-series-id';
|
||||
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
indexPattern: mockIndexPattern,
|
||||
dataType: 'ux',
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () {
|
|||
data: {
|
||||
[seriesId]: {
|
||||
dataType: 'ux' as const,
|
||||
reportType: 'dist' as const,
|
||||
reportType: 'data-distribution' as const,
|
||||
time: { from: 'now-30d', to: 'now' },
|
||||
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'ux',
|
||||
reportDefinitions: {},
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-30d', to: 'now' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { CustomReportField } from '../custom_report_field';
|
||||
import { DataSeries, URLReportDefinition } from '../../types';
|
||||
|
@ -36,8 +35,6 @@ export function ReportDefinitionCol({
|
|||
dataViewSeries: DataSeries;
|
||||
seriesId: string;
|
||||
}) {
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
|
||||
const { getSeries, setSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
@ -69,21 +66,20 @@ export function ReportDefinitionCol({
|
|||
<DatePickerCol seriesId={seriesId} />
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
{indexPattern &&
|
||||
reportDefinitions.map(({ field, custom, options }) => (
|
||||
<EuiFlexItem key={field}>
|
||||
{!custom ? (
|
||||
<ReportDefinitionField
|
||||
seriesId={seriesId}
|
||||
dataSeries={dataViewSeries}
|
||||
field={field}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<CustomReportField field={field} options={options} seriesId={seriesId} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{reportDefinitions.map(({ field, custom, options }) => (
|
||||
<EuiFlexItem key={field}>
|
||||
{!custom ? (
|
||||
<ReportDefinitionField
|
||||
seriesId={seriesId}
|
||||
dataSeries={dataViewSeries}
|
||||
field={field}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<CustomReportField field={field} options={options} seriesId={seriesId} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{(hasOperationType || columnType === 'operation') && (
|
||||
<EuiFlexItem>
|
||||
<OperationTypeSelect
|
||||
|
|
|
@ -29,7 +29,7 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }:
|
|||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
const { indexPattern } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
const { reportDefinitions: selectedReportDefinitions = {} } = series;
|
||||
|
||||
|
@ -49,7 +49,7 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }:
|
|||
|
||||
if (!isEmpty(selectedReportDefinitions)) {
|
||||
reportDefinitions.forEach(({ field: fieldT, custom }) => {
|
||||
if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) {
|
||||
if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) {
|
||||
const values = selectedReportDefinitions?.[fieldT];
|
||||
const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0];
|
||||
filtersN.push(valueFilter.query);
|
||||
|
@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }:
|
|||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" alignItems="center" wrap>
|
||||
<EuiFlexItem>
|
||||
<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}
|
||||
/>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () {
|
|||
const seriesId = 'test-series-id';
|
||||
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
indexPattern: mockIndexPattern,
|
||||
dataType: 'ux',
|
||||
});
|
||||
|
|
|
@ -11,10 +11,9 @@ 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';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_series_storage';
|
||||
|
||||
describe('ReportTypesCol', function () {
|
||||
const seriesId = 'test-series-id';
|
||||
const seriesId = 'performance-distribution';
|
||||
|
||||
mockAppIndexPattern();
|
||||
|
||||
|
@ -40,7 +39,7 @@ describe('ReportTypesCol', function () {
|
|||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
reportDefinitions: {},
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
|
@ -49,11 +48,12 @@ describe('ReportTypesCol', function () {
|
|||
it('should set selected as filled', function () {
|
||||
const initSeries = {
|
||||
data: {
|
||||
[NEW_SERIES_KEY]: {
|
||||
[seriesId]: {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
isNew: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -74,6 +74,7 @@ describe('ReportTypesCol', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith(seriesId, {
|
||||
dataType: 'synthetics',
|
||||
time: DEFAULT_TIME,
|
||||
isNew: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,27 +7,33 @@
|
|||
|
||||
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 { ReportViewTypeId, SeriesUrl } from '../../types';
|
||||
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, SELECT_DATA_TYPE } from '../series_builder';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
reportTypes: Array<{ id: ReportViewTypeId; label: string }>;
|
||||
reportTypes: ReportTypeItem[];
|
||||
}
|
||||
|
||||
export function ReportTypesCol({ seriesId, reportTypes }: Props) {
|
||||
const { setSeries, getSeries } = useSeriesStorage();
|
||||
const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage();
|
||||
|
||||
const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId);
|
||||
|
||||
const { loading, hasData, selectedApp } = useAppIndexPatternContext();
|
||||
const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType);
|
||||
|
||||
if (!loading && !hasData && selectedApp) {
|
||||
if (!restSeries.dataType) {
|
||||
return <span>{SELECT_DATA_TYPE}</span>;
|
||||
}
|
||||
|
||||
if (!loading && !hasData) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.observability.reportTypeCol.nodata"
|
||||
|
@ -36,9 +42,16 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const disabledReportTypes: ReportViewType[] = map(
|
||||
reportTypes.filter(
|
||||
({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType
|
||||
),
|
||||
'reportType'
|
||||
);
|
||||
|
||||
return reportTypes?.length > 0 ? (
|
||||
<FlexGroup direction="column" gutterSize="xs">
|
||||
{reportTypes.map(({ id: reportType, label }) => (
|
||||
{reportTypes.map(({ reportType, label }) => (
|
||||
<EuiFlexItem key={reportType}>
|
||||
<Button
|
||||
fullWidth
|
||||
|
@ -47,12 +60,13 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) {
|
|||
iconType="arrowRight"
|
||||
color={selectedReportType === reportType ? 'primary' : 'text'}
|
||||
fill={selectedReportType === reportType}
|
||||
isDisabled={loading}
|
||||
isDisabled={loading || disabledReportTypes.includes(reportType)}
|
||||
onClick={() => {
|
||||
if (reportType === selectedReportType) {
|
||||
setSeries(seriesId, {
|
||||
dataType: restSeries.dataType,
|
||||
time: DEFAULT_TIME,
|
||||
isNew: true,
|
||||
} as SeriesUrl);
|
||||
} else {
|
||||
setSeries(seriesId, {
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { EuiIcon, EuiText } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
|
||||
interface Props {
|
||||
lastUpdated?: number;
|
||||
}
|
||||
export function LastUpdated({ lastUpdated }: Props) {
|
||||
const [refresh, setRefresh] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
const interVal = setInterval(() => {
|
||||
setRefresh(Date.now());
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interVal);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!lastUpdated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiText color="subdued" size="s">
|
||||
<EuiIcon type="clock" /> Last Updated: {moment(lastUpdated).from(refresh)}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
|
@ -5,11 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { RefObject } from 'react';
|
||||
|
||||
import React, { RefObject, useEffect, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
} from '@elastic/eui';
|
||||
import { rgba } from 'polished';
|
||||
import { AppDataType, DataSeries, 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';
|
||||
|
@ -18,6 +26,10 @@ 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,
|
||||
|
@ -25,72 +37,94 @@ import {
|
|||
PERF_DIST_LABEL,
|
||||
} from '../configurations/constants/labels';
|
||||
|
||||
export const ReportTypes: Record<AppDataType, Array<{ id: ReportViewTypeId; label: string }>> = {
|
||||
export interface ReportTypeItem {
|
||||
id: string;
|
||||
reportType: ReportViewType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const ReportTypes: Record<AppDataType, ReportTypeItem[]> = {
|
||||
synthetics: [
|
||||
{ id: 'kpi', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', label: PERF_DIST_LABEL },
|
||||
{ id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL },
|
||||
],
|
||||
ux: [
|
||||
{ id: 'kpi', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', label: PERF_DIST_LABEL },
|
||||
{ id: 'cwv', label: CORE_WEB_VITALS_LABEL },
|
||||
{ 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', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', label: PERF_DIST_LABEL },
|
||||
{ id: 'mdd', label: DEVICE_DISTRIBUTION_LABEL },
|
||||
{ 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?: DataSeries;
|
||||
}
|
||||
|
||||
export function SeriesBuilder({
|
||||
seriesBuilderRef,
|
||||
seriesId,
|
||||
lastUpdated,
|
||||
multiSeries,
|
||||
}: {
|
||||
seriesId: string;
|
||||
seriesBuilderRef: RefObject<HTMLDivElement>;
|
||||
lastUpdated?: number;
|
||||
multiSeries?: boolean;
|
||||
}) {
|
||||
const { getSeries, setSeries, removeSeries } = useSeriesStorage();
|
||||
const [editorItems, setEditorItems] = useState<BuilderItem[]>([]);
|
||||
const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage();
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
const { loading, indexPatterns } = useAppIndexPatternContext();
|
||||
|
||||
const {
|
||||
dataType,
|
||||
seriesType,
|
||||
reportType,
|
||||
reportDefinitions = {},
|
||||
filters = [],
|
||||
operationType,
|
||||
breakdown,
|
||||
time,
|
||||
} = series;
|
||||
useEffect(() => {
|
||||
const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => {
|
||||
if (indexPatterns?.[dataType]) {
|
||||
return getDefaultConfigs({
|
||||
dataType,
|
||||
indexPattern: indexPatterns[dataType],
|
||||
reportType: reportType!,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { indexPattern, loading, hasData } = useAppIndexPatternContext();
|
||||
const seriesToEdit: BuilderItem[] =
|
||||
allSeriesIds
|
||||
.filter((sId) => {
|
||||
return allSeries?.[sId]?.isNew;
|
||||
})
|
||||
.map((sId) => {
|
||||
const series = getSeries(sId);
|
||||
const seriesConfig = getDataViewSeries(series.dataType, series.reportType);
|
||||
|
||||
const getDataViewSeries = () => {
|
||||
return getDefaultConfigs({
|
||||
dataType,
|
||||
indexPattern,
|
||||
reportType: 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: (val: string) => <DataTypesCol seriesId={seriesId} />,
|
||||
render: (seriesId: string) => <DataTypesCol seriesId={seriesId} />,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
|
||||
defaultMessage: 'Report',
|
||||
}),
|
||||
width: '15%',
|
||||
render: (val: string) => (
|
||||
field: 'id',
|
||||
render: (seriesId: string, { series: { dataType } }: BuilderItem) => (
|
||||
<ReportTypesCol seriesId={seriesId} reportTypes={dataType ? ReportTypes[dataType] : []} />
|
||||
),
|
||||
},
|
||||
|
@ -99,12 +133,16 @@ export function SeriesBuilder({
|
|||
defaultMessage: 'Definition',
|
||||
}),
|
||||
width: '30%',
|
||||
render: (val: string) => {
|
||||
if (dataType && hasData) {
|
||||
field: 'id',
|
||||
render: (
|
||||
seriesId: string,
|
||||
{ series: { dataType, reportType }, seriesConfig }: BuilderItem
|
||||
) => {
|
||||
if (dataType && seriesConfig) {
|
||||
return loading ? (
|
||||
LOADING_VIEW
|
||||
) : reportType ? (
|
||||
<ReportDefinitionCol seriesId={seriesId} dataViewSeries={getDataViewSeries()} />
|
||||
<ReportDefinitionCol seriesId={seriesId} dataViewSeries={seriesConfig} />
|
||||
) : (
|
||||
SELECT_REPORT_TYPE
|
||||
);
|
||||
|
@ -118,9 +156,10 @@ export function SeriesBuilder({
|
|||
defaultMessage: 'Filters',
|
||||
}),
|
||||
width: '20%',
|
||||
render: (val: string) =>
|
||||
reportType && indexPattern ? (
|
||||
<ReportFilters seriesId={seriesId} dataViewSeries={getDataViewSeries()} />
|
||||
field: 'id',
|
||||
render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
|
||||
reportType && seriesConfig ? (
|
||||
<ReportFilters seriesId={seriesId} dataViewSeries={seriesConfig} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
|
@ -129,53 +168,126 @@ export function SeriesBuilder({
|
|||
}),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
render: (val: string) =>
|
||||
reportType && indexPattern ? (
|
||||
<ReportBreakdowns seriesId={seriesId} dataViewSeries={getDataViewSeries()} />
|
||||
render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) =>
|
||||
reportType && seriesConfig ? (
|
||||
<ReportBreakdowns seriesId={seriesId} dataViewSeries={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} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
// TODO: Remove this if remain unused during multiple series view
|
||||
// @ts-expect-error
|
||||
const addSeries = () => {
|
||||
if (reportType) {
|
||||
const newSeriesId = `${
|
||||
reportDefinitions?.['service.name'] ||
|
||||
reportDefinitions?.['monitor.id'] ||
|
||||
ReportViewTypes[reportType]
|
||||
}`;
|
||||
const applySeries = () => {
|
||||
editorItems.forEach(({ series, id: seriesId }) => {
|
||||
const { reportType, reportDefinitions, isNew, ...restSeries } = series;
|
||||
|
||||
const newSeriesN: SeriesUrl = {
|
||||
dataType,
|
||||
time,
|
||||
filters,
|
||||
breakdown,
|
||||
reportType,
|
||||
seriesType,
|
||||
operationType,
|
||||
reportDefinitions,
|
||||
};
|
||||
if (reportType && !isEmpty(reportDefinitions)) {
|
||||
const reportDefId = Object.values(reportDefinitions ?? {})[0];
|
||||
const newSeriesId = `${reportDefId}-${reportType}`;
|
||||
|
||||
setSeries(newSeriesId, newSeriesN);
|
||||
removeSeries(NEW_SERIES_KEY);
|
||||
}
|
||||
const newSeriesN: SeriesUrl = {
|
||||
...restSeries,
|
||||
reportType,
|
||||
reportDefinitions,
|
||||
};
|
||||
|
||||
setSeries(newSeriesId, newSeriesN);
|
||||
removeSeries(seriesId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const items = [{ id: 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 (
|
||||
<div ref={seriesBuilderRef}>
|
||||
<EuiBasicTable
|
||||
items={items as any}
|
||||
columns={columns}
|
||||
cellProps={{ style: { borderRight: '1px solid #d3dae6', verticalAlign: 'initial' } }}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
</div>
|
||||
<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',
|
||||
{
|
||||
|
@ -189,3 +301,10 @@ export const SELECT_REPORT_TYPE = i18n.translate(
|
|||
defaultMessage: 'No report type selected',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_DATA_TYPE = i18n.translate(
|
||||
'xpack.observability.expView.seriesBuilder.selectDataType',
|
||||
{
|
||||
defaultMessage: 'No data type selected',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { 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';
|
||||
|
||||
export const parseAbsoluteDate = (date: string, options = {}) => {
|
||||
return DateMath.parse(date, options)!;
|
||||
};
|
||||
export function DateRangePicker({ seriesId }: { seriesId: string }) {
|
||||
const { firstSeriesId, getSeries, setSeries } = useSeriesStorage();
|
||||
const dateFormat = useUiSetting<string>('dateFormat');
|
||||
|
||||
const {
|
||||
time: { from, to },
|
||||
reportType,
|
||||
} = getSeries(firstSeriesId);
|
||||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const {
|
||||
time: { from: seriesFrom, to: seriesTo },
|
||||
} = series;
|
||||
|
||||
const startDate = parseAbsoluteDate(seriesFrom ?? from)!;
|
||||
const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!;
|
||||
|
||||
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 = newDate.toISOString();
|
||||
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
time: { from: newFrom, to: seriesTo },
|
||||
});
|
||||
}
|
||||
};
|
||||
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 = newDate.toISOString();
|
||||
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
time: { from: seriesFrom, to: newTo },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiDatePickerRange
|
||||
fullWidth
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
selected={startDate}
|
||||
onChange={onStartChange}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
isInvalid={startDate > endDate}
|
||||
aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
|
||||
defaultMessage: 'Start date',
|
||||
})}
|
||||
dateFormat={dateFormat}
|
||||
showTimeSelect
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<EuiDatePicker
|
||||
selected={endDate}
|
||||
onChange={onEndChange}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
isInvalid={startDate > endDate}
|
||||
aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
|
||||
defaultMessage: 'End date',
|
||||
})}
|
||||
dateFormat={dateFormat}
|
||||
showTimeSelect
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -43,7 +43,7 @@ export function SeriesDatePicker({ seriesId }: Props) {
|
|||
if (!series || !series.time) {
|
||||
setSeries(seriesId, { ...series, time: DEFAULT_TIME });
|
||||
}
|
||||
}, [seriesId, series, setSeries]);
|
||||
}, [series, seriesId, setSeries]);
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
|
|
|
@ -17,7 +17,7 @@ describe('SeriesDatePicker', function () {
|
|||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'dist' as const,
|
||||
reportType: 'data-distribution' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
},
|
||||
|
@ -32,7 +32,7 @@ describe('SeriesDatePicker', function () {
|
|||
const initSeries = {
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
dataType: 'synthetics' as const,
|
||||
breakdown: 'monitor.status',
|
||||
},
|
||||
|
@ -46,7 +46,7 @@ describe('SeriesDatePicker', function () {
|
|||
expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
|
||||
breakdown: 'monitor.status',
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
time: DEFAULT_TIME,
|
||||
});
|
||||
});
|
||||
|
@ -56,7 +56,7 @@ describe('SeriesDatePicker', function () {
|
|||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
dataType: 'synthetics' as const,
|
||||
reportType: 'kpi' as const,
|
||||
reportType: 'kpi-over-time' as const,
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
},
|
||||
|
@ -79,7 +79,7 @@ describe('SeriesDatePicker', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
breakdown: 'monitor.status',
|
||||
dataType: 'synthetics',
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: 'now/d', to: 'now/d' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -14,7 +14,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel
|
|||
|
||||
describe('Breakdowns', function () {
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
indexPattern: mockIndexPattern,
|
||||
dataType: 'ux',
|
||||
});
|
||||
|
@ -52,7 +52,7 @@ describe('Breakdowns', function () {
|
|||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
breakdown: 'user_agent.name',
|
||||
dataType: 'ux',
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -7,14 +7,23 @@
|
|||
|
||||
import React from 'react';
|
||||
import { SeriesDatePicker } from '../../series_date_picker';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { DateRangePicker } from '../../series_date_picker/date_range_picker';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
export function DatePickerCol({ seriesId }: Props) {
|
||||
const { firstSeriesId, getSeries } = useSeriesStorage();
|
||||
const { reportType } = getSeries(firstSeriesId);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
{firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? (
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
) : (
|
||||
<DateRangePicker seriesId={seriesId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,8 +41,6 @@ export function FilterExpanded({
|
|||
isNegated,
|
||||
filters: defaultFilters,
|
||||
}: Props) {
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const [isOpen, setIsOpen] = useState({ value: '', negate: false });
|
||||
|
@ -53,23 +51,25 @@ export function FilterExpanded({
|
|||
|
||||
const queryFilters: ESFilter[] = [];
|
||||
|
||||
const { indexPatterns } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => {
|
||||
if (qFilter.query) {
|
||||
queryFilters.push(qFilter.query);
|
||||
}
|
||||
const asExistFilter = qFilter as ExistsFilter;
|
||||
if (asExistFilter?.exists) {
|
||||
queryFilters.push(asExistFilter.exists as QueryDslQueryContainer);
|
||||
queryFilters.push({ exists: asExistFilter.exists } as QueryDslQueryContainer);
|
||||
}
|
||||
});
|
||||
|
||||
const { values, loading } = useValuesList({
|
||||
query: value,
|
||||
indexPatternTitle: indexPattern?.title,
|
||||
sourceField: field,
|
||||
time: series.time,
|
||||
keepHistory: true,
|
||||
filters: queryFilters,
|
||||
indexPatternTitle: indexPatterns[series.dataType]?.title,
|
||||
});
|
||||
|
||||
const filters = series?.filters ?? [];
|
||||
|
|
|
@ -139,7 +139,7 @@ describe('FilterValueButton', function () {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: [
|
||||
|
@ -170,7 +170,7 @@ describe('FilterValueButton', function () {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenCalledTimes(6);
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: [
|
||||
|
|
|
@ -41,7 +41,7 @@ export function FilterValueButton({
|
|||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
const { indexPatterns } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
|
@ -96,7 +96,6 @@ export function FilterValueButton({
|
|||
<FieldValueSuggestions
|
||||
button={button}
|
||||
label={'Version'}
|
||||
indexPatternTitle={indexPattern?.title}
|
||||
sourceField={nestedField}
|
||||
onChange={onNestedChange}
|
||||
filters={filters}
|
||||
|
@ -104,6 +103,7 @@ export function FilterValueButton({
|
|||
anchorPosition="rightCenter"
|
||||
time={series.time}
|
||||
asCombobox={false}
|
||||
indexPatternTitle={indexPatterns[series.dataType]?.title}
|
||||
/>
|
||||
) : (
|
||||
button
|
||||
|
|
|
@ -26,9 +26,9 @@ export function RemoveSeries({ seriesId }: Props) {
|
|||
defaultMessage: 'Click to remove series',
|
||||
})}
|
||||
iconType="cross"
|
||||
color="primary"
|
||||
color="danger"
|
||||
onClick={onClick}
|
||||
size="m"
|
||||
size="s"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,33 +8,93 @@
|
|||
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 { NEW_SERIES_KEY, useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { SeriesUrl } from '../../types';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
editorMode?: boolean;
|
||||
}
|
||||
export function SeriesActions({ seriesId }: Props) {
|
||||
const { getSeries, removeSeries, setSeries } = useSeriesStorage();
|
||||
export function SeriesActions({ seriesId, editorMode = false }: Props) {
|
||||
const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage();
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const onEdit = () => {
|
||||
removeSeries(seriesId);
|
||||
setSeries(NEW_SERIES_KEY, { ...series });
|
||||
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="xs" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={'documentEdit'}
|
||||
aria-label={i18n.translate('xpack.observability.seriesEditor.edit', {
|
||||
defaultMessage: 'Edit series',
|
||||
})}
|
||||
size="m"
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<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>
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('SelectedFilters', function () {
|
|||
mockAppIndexPattern();
|
||||
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'dist',
|
||||
reportType: 'data-distribution',
|
||||
indexPattern: mockIndexPattern,
|
||||
dataType: 'ux',
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
|
|||
|
||||
const { removeFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
const { indexPattern } = useAppIndexPatternContext(series.dataType);
|
||||
|
||||
return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
|
||||
<EuiFlexItem>
|
||||
|
@ -55,6 +55,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
|
|||
value={val}
|
||||
removeFilter={() => removeFilter({ field, value: val, negate: false })}
|
||||
negate={false}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
@ -67,6 +68,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
|
|||
value={val}
|
||||
negate={true}
|
||||
removeFilter={() => removeFilter({ field, value: val, negate: true })}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
@ -87,6 +89,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props)
|
|||
}}
|
||||
negate={false}
|
||||
definitionFilter={true}
|
||||
indexPattern={indexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
|
|
|
@ -24,7 +24,7 @@ interface EditItem {
|
|||
}
|
||||
|
||||
export function SeriesEditor() {
|
||||
const { allSeries, firstSeriesId } = useSeriesStorage();
|
||||
const { allSeries, allSeriesIds } = useSeriesStorage();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -33,80 +33,77 @@ export function SeriesEditor() {
|
|||
}),
|
||||
field: 'id',
|
||||
width: '15%',
|
||||
render: (val: string) => (
|
||||
render: (seriesId: string) => (
|
||||
<EuiText>
|
||||
<EuiIcon type="dot" color="green" size="l" />{' '}
|
||||
{val === NEW_SERIES_KEY ? 'series-preview' : val}
|
||||
{seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId}
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
...(firstSeriesId !== NEW_SERIES_KEY
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
field: 'defaultFilters',
|
||||
width: '15%',
|
||||
render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => (
|
||||
<SeriesFilter
|
||||
defaultFilters={defaultFilters}
|
||||
seriesId={id}
|
||||
series={seriesConfig}
|
||||
filters={seriesConfig.filters}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
|
||||
defaultMessage: 'Breakdowns',
|
||||
}),
|
||||
field: 'breakdowns',
|
||||
width: '25%',
|
||||
render: (val: string[], item: EditItem) => (
|
||||
<ChartEditOptions seriesId={item.id} breakdowns={val} series={item.seriesConfig} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.expView.seriesEditor.time"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
align: 'right' as const,
|
||||
render: (val: string, item: EditItem) => <DatePickerCol seriesId={item.id} />,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
align: 'center' as const,
|
||||
width: '10%',
|
||||
field: 'id',
|
||||
render: (val: string, item: EditItem) => <SeriesActions seriesId={item.id} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
field: 'defaultFilters',
|
||||
width: '15%',
|
||||
render: (seriesId: string, { seriesConfig, id }: EditItem) => (
|
||||
<SeriesFilter
|
||||
defaultFilters={seriesConfig.defaultFilters}
|
||||
seriesId={id}
|
||||
series={seriesConfig}
|
||||
filters={seriesConfig.filters}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
|
||||
defaultMessage: 'Breakdowns',
|
||||
}),
|
||||
field: 'id',
|
||||
width: '25%',
|
||||
render: (seriesId: string, { seriesConfig, id }: EditItem) => (
|
||||
<ChartEditOptions
|
||||
seriesId={id}
|
||||
breakdowns={seriesConfig.breakdowns}
|
||||
series={seriesConfig}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.expView.seriesEditor.time"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
align: 'right' as const,
|
||||
render: (seriesId: string, item: EditItem) => <DatePickerCol seriesId={seriesId} />,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
align: 'center' as const,
|
||||
width: '10%',
|
||||
field: 'id',
|
||||
render: (seriesId: string, item: EditItem) => <SeriesActions seriesId={seriesId} />,
|
||||
},
|
||||
];
|
||||
|
||||
const allSeriesKeys = Object.keys(allSeries);
|
||||
|
||||
const { indexPatterns } = useAppIndexPatternContext();
|
||||
const items: EditItem[] = [];
|
||||
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
|
||||
allSeriesKeys.forEach((seriesKey) => {
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
const series = allSeries[seriesKey];
|
||||
if (series.reportType && indexPattern) {
|
||||
if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) {
|
||||
items.push({
|
||||
id: seriesKey,
|
||||
seriesConfig: getDefaultConfigs({
|
||||
indexPattern,
|
||||
indexPattern: indexPatterns[series.dataType],
|
||||
reportType: series.reportType,
|
||||
dataType: series.dataType,
|
||||
}),
|
||||
|
@ -114,6 +111,10 @@ export function SeriesEditor() {
|
|||
}
|
||||
});
|
||||
|
||||
if (items.length === 0 && allSeriesIds.length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
|
@ -121,8 +122,7 @@ export function SeriesEditor() {
|
|||
items={items}
|
||||
rowHeader="firstName"
|
||||
columns={columns}
|
||||
rowProps={() => (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })}
|
||||
noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
|
||||
noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', {
|
||||
defaultMessage: 'No series found, please add a series.',
|
||||
})}
|
||||
cellProps={{
|
||||
|
|
|
@ -23,7 +23,7 @@ export const ReportViewTypes = {
|
|||
dist: 'data-distribution',
|
||||
kpi: 'kpi-over-time',
|
||||
cwv: 'core-web-vitals',
|
||||
mdd: 'mobile-device-distribution',
|
||||
mdd: 'device-data-distribution',
|
||||
} as const;
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
@ -56,7 +56,6 @@ export interface DataSeries {
|
|||
reportType: ReportViewType;
|
||||
xAxisColumn: Partial<LastValueIndexPatternColumn> | Partial<DateHistogramIndexPatternColumn>;
|
||||
yAxisColumns: Array<Partial<FieldBasedIndexPatternColumn>>;
|
||||
|
||||
breakdowns: string[];
|
||||
defaultSeriesType: SeriesType;
|
||||
defaultFilters: Array<string | { field: string; nested?: string; isNegated?: boolean }>;
|
||||
|
@ -80,10 +79,11 @@ export interface SeriesUrl {
|
|||
breakdown?: string;
|
||||
filters?: UrlFilter[];
|
||||
seriesType?: SeriesType;
|
||||
reportType: ReportViewTypeId;
|
||||
reportType: ReportViewType;
|
||||
operationType?: OperationType;
|
||||
dataType: AppDataType;
|
||||
reportDefinitions?: URLReportDefinition;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export interface UrlFilter {
|
||||
|
@ -94,6 +94,7 @@ export interface UrlFilter {
|
|||
|
||||
export interface ConfigProps {
|
||||
indexPattern: IIndexPattern;
|
||||
series?: SeriesUrl;
|
||||
}
|
||||
|
||||
export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile';
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { urlFiltersToKueryString } from './stringify_kueries';
|
||||
import { UrlFilter } from '../types';
|
||||
import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames';
|
||||
|
||||
describe('stringifyKueries', () => {
|
||||
let filters: UrlFilter[];
|
||||
beforeEach(() => {
|
||||
filters = [
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: ['Chrome', 'Firefox'],
|
||||
notValues: [],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('stringifies the current values', () => {
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"user_agent.name: (\\"Chrome\\" or \\"Firefox\\")"`
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly stringifies a single value', () => {
|
||||
filters = [
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: ['Chrome'],
|
||||
notValues: [],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"user_agent.name: (\\"Chrome\\")"`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty string for an empty array', () => {
|
||||
expect(urlFiltersToKueryString([])).toMatchInlineSnapshot(`""`);
|
||||
});
|
||||
|
||||
it('returns an empty string for an empty value', () => {
|
||||
filters = [
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: [],
|
||||
notValues: [],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(`""`);
|
||||
});
|
||||
|
||||
it('adds quotations if the value contains a space', () => {
|
||||
filters = [
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: ['Google Chrome'],
|
||||
notValues: [],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"user_agent.name: (\\"Google Chrome\\")"`
|
||||
);
|
||||
});
|
||||
|
||||
it('adds quotations inside parens if there are values containing spaces', () => {
|
||||
filters = [
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: ['Google Chrome'],
|
||||
notValues: ['Apple Safari'],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"user_agent.name: (\\"Google Chrome\\") and not (user_agent.name: (\\"Apple Safari\\"))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parens for values with greater than 2 items', () => {
|
||||
filters = [
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: ['Chrome', 'Firefox', 'Safari', 'Opera'],
|
||||
notValues: ['Safari'],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"user_agent.name: (\\"Chrome\\" or \\"Firefox\\" or \\"Safari\\" or \\"Opera\\") and not (user_agent.name: (\\"Safari\\"))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles colon characters in values', () => {
|
||||
filters = [
|
||||
{
|
||||
field: 'url',
|
||||
values: ['https://elastic.co', 'https://example.com'],
|
||||
notValues: [],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles precending empty array', () => {
|
||||
filters = [
|
||||
{
|
||||
field: 'url',
|
||||
values: ['https://elastic.co', 'https://example.com'],
|
||||
notValues: [],
|
||||
},
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: [],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles skipped empty arrays', () => {
|
||||
filters = [
|
||||
{
|
||||
field: 'url',
|
||||
values: ['https://elastic.co', 'https://example.com'],
|
||||
notValues: [],
|
||||
},
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
values: [],
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
values: ['https://elastic.co', 'https://example.com'],
|
||||
notValues: [],
|
||||
},
|
||||
];
|
||||
expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(
|
||||
`"url: (\\"https://elastic.co\\" or \\"https://example.com\\") and url: (\\"https://elastic.co\\" or \\"https://example.com\\")"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { UrlFilter } from '../types';
|
||||
|
||||
/**
|
||||
* Extract a map's keys to an array, then map those keys to a string per key.
|
||||
* The strings contain all of the values chosen for the given field (which is also the key value).
|
||||
* Reduce the list of query strings to a singular string, with AND operators between.
|
||||
*/
|
||||
export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => {
|
||||
let kueryString = '';
|
||||
urlFilters.forEach(({ field, values, notValues }) => {
|
||||
const valuesT = values?.map((val) => `"${val}"`);
|
||||
const notValuesT = notValues?.map((val) => `"${val}"`);
|
||||
|
||||
if (valuesT && valuesT?.length > 0) {
|
||||
if (kueryString.length > 0) {
|
||||
kueryString += ' and ';
|
||||
}
|
||||
kueryString += `${field}: (${valuesT.join(' or ')})`;
|
||||
}
|
||||
|
||||
if (notValuesT && notValuesT?.length > 0) {
|
||||
if (kueryString.length > 0) {
|
||||
kueryString += ' and ';
|
||||
}
|
||||
kueryString += `not (${field}: (${notValuesT.join(' or ')}))`;
|
||||
}
|
||||
});
|
||||
|
||||
return kueryString;
|
||||
};
|
|
@ -112,4 +112,18 @@ export const routes = {
|
|||
}),
|
||||
},
|
||||
},
|
||||
// enable this to test multi series architecture
|
||||
// '/exploratory-view/multi': {
|
||||
// handler: () => {
|
||||
// return <ExploratoryViewPage multiSeries={true} />;
|
||||
// },
|
||||
// params: {
|
||||
// query: t.partial({
|
||||
// rangeFrom: t.string,
|
||||
// rangeTo: t.string,
|
||||
// refreshPaused: jsonRt.pipe(t.boolean),
|
||||
// refreshInterval: jsonRt.pipe(t.number),
|
||||
// }),
|
||||
// },
|
||||
// },
|
||||
};
|
||||
|
|
|
@ -17256,7 +17256,6 @@
|
|||
"xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去",
|
||||
"xpack.observability.expView.seriesEditor.filters": "フィルター",
|
||||
"xpack.observability.expView.seriesEditor.name": "名前",
|
||||
"xpack.observability.expView.seriesEditor.notFound": "系列が見つかりません。系列を追加してください。",
|
||||
"xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します",
|
||||
"xpack.observability.expView.seriesEditor.time": "時間",
|
||||
"xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。",
|
||||
|
|
|
@ -17492,7 +17492,6 @@
|
|||
"xpack.observability.expView.seriesEditor.clearFilter": "清除筛选",
|
||||
"xpack.observability.expView.seriesEditor.filters": "筛选",
|
||||
"xpack.observability.expView.seriesEditor.name": "名称",
|
||||
"xpack.observability.expView.seriesEditor.notFound": "未找到序列,请添加序列。",
|
||||
"xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列",
|
||||
"xpack.observability.expView.seriesEditor.time": "时间",
|
||||
"xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。",
|
||||
|
|
|
@ -191,7 +191,7 @@ export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({
|
|||
{
|
||||
'pings-over-time': {
|
||||
dataType: 'synthetics',
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: dateRangeStart, to: dateRangeEnd },
|
||||
...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}),
|
||||
},
|
||||
|
|
|
@ -40,10 +40,11 @@ export function ActionMenuContent(): React.ReactElement {
|
|||
|
||||
const syntheticExploratoryViewLink = createExploratoryViewUrl(
|
||||
{
|
||||
'synthetics-series': {
|
||||
'synthetics-series': ({
|
||||
dataType: 'synthetics',
|
||||
isNew: true,
|
||||
time: { from: dateRangeStart, to: dateRangeEnd },
|
||||
} as SeriesUrl,
|
||||
} as unknown) as SeriesUrl,
|
||||
},
|
||||
basePath
|
||||
);
|
||||
|
|
|
@ -56,7 +56,7 @@ export const MonitorDuration: React.FC<MonitorIdParam> = ({ monitorId }) => {
|
|||
const exploratoryViewLink = createExploratoryViewUrl(
|
||||
{
|
||||
[`monitor-duration`]: {
|
||||
reportType: 'kpi',
|
||||
reportType: 'kpi-over-time',
|
||||
time: { from: dateRangeStart, to: dateRangeEnd },
|
||||
reportDefinitions: {
|
||||
'monitor.id': [monitorId] as string[],
|
||||
|
|
Loading…
Reference in a new issue