mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] Support metric trendlines (#141851)
This commit is contained in:
parent
836b60da22
commit
66041ca2c2
84 changed files with 4708 additions and 1194 deletions
|
@ -74,7 +74,7 @@ pageLoadAssetSize:
|
|||
kibanaUsageCollection: 16463
|
||||
kibanaUtils: 79713
|
||||
kubernetesSecurity: 77234
|
||||
lens: 36500
|
||||
lens: 37000
|
||||
licenseManagement: 41817
|
||||
licensing: 29004
|
||||
lists: 22900
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
*/
|
||||
|
||||
export const EXPRESSION_METRIC_NAME = 'metricVis';
|
||||
export const EXPRESSION_METRIC_TRENDLINE_NAME = 'metricTrendline';
|
||||
|
||||
export const DEFAULT_TRENDLINE_NAME = 'default';
|
||||
|
||||
export const LabelPosition = {
|
||||
BOTTOM: 'bottom',
|
||||
|
|
|
@ -0,0 +1,353 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`metric trendline function builds trends with breakdown 1`] = `
|
||||
Object {
|
||||
"css": Array [
|
||||
Object {
|
||||
"x": 1664121600000,
|
||||
"y": 3264,
|
||||
},
|
||||
Object {
|
||||
"x": 1664123400000,
|
||||
"y": 7215,
|
||||
},
|
||||
Object {
|
||||
"x": 1664125200000,
|
||||
"y": 9601,
|
||||
},
|
||||
Object {
|
||||
"x": 1664127000000,
|
||||
"y": 8458,
|
||||
},
|
||||
],
|
||||
"deb": Array [
|
||||
Object {
|
||||
"x": 1664121600000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664123400000,
|
||||
"y": 9680,
|
||||
},
|
||||
Object {
|
||||
"x": 1664125200000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664127000000,
|
||||
"y": NaN,
|
||||
},
|
||||
],
|
||||
"gz": Array [
|
||||
Object {
|
||||
"x": 1664121600000,
|
||||
"y": 3116.5,
|
||||
},
|
||||
Object {
|
||||
"x": 1664123400000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664125200000,
|
||||
"y": 4148,
|
||||
},
|
||||
Object {
|
||||
"x": 1664127000000,
|
||||
"y": NaN,
|
||||
},
|
||||
],
|
||||
"rpm": Array [
|
||||
Object {
|
||||
"x": 1664121600000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664123400000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664125200000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664127000000,
|
||||
"y": NaN,
|
||||
},
|
||||
],
|
||||
"zip": Array [
|
||||
Object {
|
||||
"x": 1664121600000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664123400000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664125200000,
|
||||
"y": 5037,
|
||||
},
|
||||
Object {
|
||||
"x": 1664127000000,
|
||||
"y": NaN,
|
||||
},
|
||||
Object {
|
||||
"x": 1664128800000,
|
||||
"y": NaN,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`metric trendline function builds trends without breakdown 1`] = `
|
||||
Object {
|
||||
"default": Array [
|
||||
Object {
|
||||
"x": 1664121600000,
|
||||
"y": null,
|
||||
},
|
||||
Object {
|
||||
"x": 1664123400000,
|
||||
"y": null,
|
||||
},
|
||||
Object {
|
||||
"x": 1664125200000,
|
||||
"y": null,
|
||||
},
|
||||
Object {
|
||||
"x": 1664127000000,
|
||||
"y": null,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`metric trendline function creates inspector information 1`] = `
|
||||
Object {
|
||||
"columns": Array [
|
||||
Object {
|
||||
"id": "breakdown",
|
||||
"meta": Object {
|
||||
"dimensionName": "Split group",
|
||||
"field": "extension.keyword",
|
||||
"index": "kibana_sample_data_logs",
|
||||
"params": Object {
|
||||
"id": "terms",
|
||||
"params": Object {
|
||||
"id": "string",
|
||||
"missingBucketLabel": "(missing value)",
|
||||
"otherBucketLabel": "Other",
|
||||
},
|
||||
},
|
||||
"source": "esaggs",
|
||||
"sourceParams": Object {
|
||||
"enabled": true,
|
||||
"hasPrecisionError": false,
|
||||
"id": "0",
|
||||
"indexPatternId": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"params": Object {
|
||||
"excludeIsRegex": false,
|
||||
"field": "extension.keyword",
|
||||
"includeIsRegex": false,
|
||||
"missingBucket": false,
|
||||
"missingBucketLabel": "(missing value)",
|
||||
"order": "desc",
|
||||
"orderBy": "2",
|
||||
"otherBucket": true,
|
||||
"otherBucketLabel": "Other",
|
||||
"size": 5,
|
||||
},
|
||||
"schema": "segment",
|
||||
"type": "terms",
|
||||
},
|
||||
"type": "string",
|
||||
},
|
||||
"name": "Top 5 values of extension.keyword",
|
||||
},
|
||||
Object {
|
||||
"id": "time",
|
||||
"meta": Object {
|
||||
"dimensionName": "Time field",
|
||||
"field": "timestamp",
|
||||
"index": "kibana_sample_data_logs",
|
||||
"params": Object {
|
||||
"id": "date",
|
||||
"params": Object {
|
||||
"pattern": "HH:mm",
|
||||
},
|
||||
},
|
||||
"source": "esaggs",
|
||||
"sourceParams": Object {
|
||||
"appliedTimeRange": Object {
|
||||
"from": "2022-09-25T16:00:00.000Z",
|
||||
"to": "2022-09-26T16:12:41.742Z",
|
||||
},
|
||||
"enabled": true,
|
||||
"hasPrecisionError": false,
|
||||
"id": "1",
|
||||
"indexPatternId": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"params": Object {
|
||||
"drop_partials": false,
|
||||
"extendToTimeRange": true,
|
||||
"extended_bounds": Object {},
|
||||
"field": "timestamp",
|
||||
"interval": "auto",
|
||||
"min_doc_count": 0,
|
||||
"scaleMetricValues": false,
|
||||
"timeRange": Object {
|
||||
"from": "2022-09-25T16:00:00.000Z",
|
||||
"to": "2022-09-26T16:12:41.742Z",
|
||||
},
|
||||
"useNormalizedEsInterval": true,
|
||||
"used_interval": "30m",
|
||||
},
|
||||
"schema": "segment",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
"type": "date",
|
||||
},
|
||||
"name": "timestamp per 30 minutes",
|
||||
},
|
||||
Object {
|
||||
"id": "metric",
|
||||
"meta": Object {
|
||||
"dimensionName": "Metric",
|
||||
"field": "bytes",
|
||||
"index": "kibana_sample_data_logs",
|
||||
"params": Object {
|
||||
"id": "number",
|
||||
},
|
||||
"source": "esaggs",
|
||||
"sourceParams": Object {
|
||||
"enabled": true,
|
||||
"hasPrecisionError": false,
|
||||
"id": "2",
|
||||
"indexPatternId": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"params": Object {
|
||||
"field": "bytes",
|
||||
},
|
||||
"schema": "metric",
|
||||
"type": "median",
|
||||
},
|
||||
"type": "number",
|
||||
},
|
||||
"name": "Median of byts",
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"source": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"statistics": Object {
|
||||
"totalCount": 236,
|
||||
},
|
||||
"type": "esaggs",
|
||||
},
|
||||
"rows": Array [
|
||||
Object {
|
||||
"breakdown": "rpm",
|
||||
"metric": null,
|
||||
"time": 1664121600000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "rpm",
|
||||
"metric": null,
|
||||
"time": 1664123400000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "rpm",
|
||||
"metric": null,
|
||||
"time": 1664125200000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "rpm",
|
||||
"metric": null,
|
||||
"time": 1664127000000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "deb",
|
||||
"metric": null,
|
||||
"time": 1664121600000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "deb",
|
||||
"metric": 9680,
|
||||
"time": 1664123400000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "deb",
|
||||
"metric": null,
|
||||
"time": 1664125200000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "deb",
|
||||
"metric": null,
|
||||
"time": 1664127000000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "zip",
|
||||
"metric": null,
|
||||
"time": 1664121600000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "zip",
|
||||
"metric": null,
|
||||
"time": 1664123400000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "zip",
|
||||
"metric": 5037,
|
||||
"time": 1664125200000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "zip",
|
||||
"metric": null,
|
||||
"time": 1664127000000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "zip",
|
||||
"metric": null,
|
||||
"time": 1664128800000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "css",
|
||||
"metric": 3264,
|
||||
"time": 1664121600000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "css",
|
||||
"metric": 7215,
|
||||
"time": 1664123400000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "css",
|
||||
"metric": 9601,
|
||||
"time": 1664125200000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "css",
|
||||
"metric": 8458,
|
||||
"time": 1664127000000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "gz",
|
||||
"metric": 3116.5,
|
||||
"time": 1664121600000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "gz",
|
||||
"metric": null,
|
||||
"time": 1664123400000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "gz",
|
||||
"metric": 4148,
|
||||
"time": 1664125200000,
|
||||
},
|
||||
Object {
|
||||
"breakdown": "gz",
|
||||
"metric": null,
|
||||
"time": 1664127000000,
|
||||
},
|
||||
],
|
||||
"type": "datatable",
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,397 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common';
|
||||
import { Adapters } from '@kbn/inspector-plugin/common';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { TrendlineArguments } from '../types';
|
||||
import { metricTrendlineFunction } from './metric_trendline_function';
|
||||
|
||||
const fakeContext = {} as ExecutionContext<Adapters, SerializableRecord>;
|
||||
const fakeInput = {} as Datatable;
|
||||
const metricTrendline = (args: TrendlineArguments) =>
|
||||
metricTrendlineFunction().fn(fakeInput, args, fakeContext);
|
||||
|
||||
describe('metric trendline function', () => {
|
||||
const tableWithBreakdown: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: 'breakdown',
|
||||
name: 'Top 5 values of extension.keyword',
|
||||
meta: {
|
||||
type: 'string',
|
||||
field: 'extension.keyword',
|
||||
index: 'kibana_sample_data_logs',
|
||||
params: {
|
||||
id: 'terms',
|
||||
params: {
|
||||
id: 'string',
|
||||
otherBucketLabel: 'Other',
|
||||
missingBucketLabel: '(missing value)',
|
||||
},
|
||||
},
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
hasPrecisionError: false,
|
||||
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
id: '0',
|
||||
enabled: true,
|
||||
type: 'terms',
|
||||
params: {
|
||||
field: 'extension.keyword',
|
||||
orderBy: '2',
|
||||
order: 'desc',
|
||||
size: 5,
|
||||
otherBucket: true,
|
||||
otherBucketLabel: 'Other',
|
||||
missingBucket: false,
|
||||
missingBucketLabel: '(missing value)',
|
||||
includeIsRegex: false,
|
||||
excludeIsRegex: false,
|
||||
},
|
||||
schema: 'segment',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'time',
|
||||
name: 'timestamp per 30 minutes',
|
||||
meta: {
|
||||
type: 'date',
|
||||
field: 'timestamp',
|
||||
index: 'kibana_sample_data_logs',
|
||||
params: {
|
||||
id: 'date',
|
||||
params: {
|
||||
pattern: 'HH:mm',
|
||||
},
|
||||
},
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
hasPrecisionError: false,
|
||||
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
appliedTimeRange: {
|
||||
from: '2022-09-25T16:00:00.000Z',
|
||||
to: '2022-09-26T16:12:41.742Z',
|
||||
},
|
||||
id: '1',
|
||||
enabled: true,
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
field: 'timestamp',
|
||||
timeRange: {
|
||||
from: '2022-09-25T16:00:00.000Z',
|
||||
to: '2022-09-26T16:12:41.742Z',
|
||||
},
|
||||
useNormalizedEsInterval: true,
|
||||
extendToTimeRange: true,
|
||||
scaleMetricValues: false,
|
||||
interval: 'auto',
|
||||
used_interval: '30m',
|
||||
drop_partials: false,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {},
|
||||
},
|
||||
schema: 'segment',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'metric',
|
||||
name: 'Median of byts',
|
||||
meta: {
|
||||
type: 'number',
|
||||
field: 'bytes',
|
||||
index: 'kibana_sample_data_logs',
|
||||
params: {
|
||||
id: 'number',
|
||||
},
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
hasPrecisionError: false,
|
||||
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
id: '2',
|
||||
enabled: true,
|
||||
type: 'median',
|
||||
params: {
|
||||
field: 'bytes',
|
||||
},
|
||||
schema: 'metric',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
breakdown: 'rpm',
|
||||
time: 1664121600000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'rpm',
|
||||
time: 1664123400000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'rpm',
|
||||
time: 1664125200000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'rpm',
|
||||
time: 1664127000000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'deb',
|
||||
time: 1664121600000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'deb',
|
||||
time: 1664123400000,
|
||||
metric: 9680,
|
||||
},
|
||||
{
|
||||
breakdown: 'deb',
|
||||
time: 1664125200000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'deb',
|
||||
time: 1664127000000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'zip',
|
||||
time: 1664121600000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'zip',
|
||||
time: 1664123400000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'zip',
|
||||
time: 1664125200000,
|
||||
metric: 5037,
|
||||
},
|
||||
{
|
||||
breakdown: 'zip',
|
||||
time: 1664127000000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'zip',
|
||||
time: 1664128800000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'css',
|
||||
time: 1664121600000,
|
||||
metric: 3264,
|
||||
},
|
||||
{
|
||||
breakdown: 'css',
|
||||
time: 1664123400000,
|
||||
metric: 7215,
|
||||
},
|
||||
{
|
||||
breakdown: 'css',
|
||||
time: 1664125200000,
|
||||
metric: 9601,
|
||||
},
|
||||
{
|
||||
breakdown: 'css',
|
||||
time: 1664127000000,
|
||||
metric: 8458,
|
||||
},
|
||||
{
|
||||
breakdown: 'gz',
|
||||
time: 1664121600000,
|
||||
metric: 3116.5,
|
||||
},
|
||||
{
|
||||
breakdown: 'gz',
|
||||
time: 1664123400000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
breakdown: 'gz',
|
||||
time: 1664125200000,
|
||||
metric: 4148,
|
||||
},
|
||||
{
|
||||
breakdown: 'gz',
|
||||
time: 1664127000000,
|
||||
metric: null,
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
type: 'esaggs',
|
||||
source: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
statistics: {
|
||||
totalCount: 236,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tableWithoutBreakdown: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: 'time',
|
||||
name: 'timestamp per 30 minutes',
|
||||
meta: {
|
||||
type: 'date',
|
||||
field: 'timestamp',
|
||||
index: 'kibana_sample_data_logs',
|
||||
params: {
|
||||
id: 'date',
|
||||
params: {
|
||||
pattern: 'HH:mm',
|
||||
},
|
||||
},
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
hasPrecisionError: false,
|
||||
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
appliedTimeRange: {
|
||||
from: '2022-09-25T16:00:00.000Z',
|
||||
to: '2022-09-26T16:12:41.742Z',
|
||||
},
|
||||
id: '1',
|
||||
enabled: true,
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
field: 'timestamp',
|
||||
timeRange: {
|
||||
from: '2022-09-25T16:00:00.000Z',
|
||||
to: '2022-09-26T16:12:41.742Z',
|
||||
},
|
||||
useNormalizedEsInterval: true,
|
||||
extendToTimeRange: true,
|
||||
scaleMetricValues: false,
|
||||
interval: 'auto',
|
||||
used_interval: '30m',
|
||||
drop_partials: false,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {},
|
||||
},
|
||||
schema: 'segment',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'metric',
|
||||
name: 'Median of byts',
|
||||
meta: {
|
||||
type: 'number',
|
||||
field: 'bytes',
|
||||
index: 'kibana_sample_data_logs',
|
||||
params: {
|
||||
id: 'number',
|
||||
},
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
hasPrecisionError: false,
|
||||
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
id: '2',
|
||||
enabled: true,
|
||||
type: 'median',
|
||||
params: {
|
||||
field: 'bytes',
|
||||
},
|
||||
schema: 'metric',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
time: 1664121600000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
time: 1664123400000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
time: 1664125200000,
|
||||
metric: null,
|
||||
},
|
||||
{
|
||||
time: 1664127000000,
|
||||
metric: null,
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
type: 'esaggs',
|
||||
source: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
statistics: {
|
||||
totalCount: 236,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it.each(['metric', 'time', 'breakdown'])('checks %s accessor', (accessor) => {
|
||||
const table = {
|
||||
...tableWithBreakdown,
|
||||
columns: tableWithBreakdown.columns.filter((column) => column.id !== accessor),
|
||||
};
|
||||
const args = {
|
||||
table,
|
||||
metric: 'metric',
|
||||
timeField: 'time',
|
||||
breakdownBy: 'breakdown',
|
||||
inspectorTableId: '',
|
||||
};
|
||||
|
||||
expect(() => metricTrendline(args)).toThrow();
|
||||
});
|
||||
|
||||
it('checks accessors', () => {});
|
||||
|
||||
it('builds trends with breakdown', () => {
|
||||
const { trends } = metricTrendline({
|
||||
table: tableWithBreakdown,
|
||||
metric: 'metric',
|
||||
timeField: 'time',
|
||||
breakdownBy: 'breakdown',
|
||||
inspectorTableId: '',
|
||||
});
|
||||
expect(trends).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('builds trends without breakdown', () => {
|
||||
const { trends } = metricTrendline({
|
||||
table: tableWithoutBreakdown,
|
||||
metric: 'metric',
|
||||
timeField: 'time',
|
||||
inspectorTableId: '',
|
||||
});
|
||||
expect(trends).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('creates inspector information', () => {
|
||||
const tableId = 'my-id';
|
||||
|
||||
const { inspectorTable, inspectorTableId } = metricTrendline({
|
||||
table: tableWithBreakdown,
|
||||
metric: 'metric',
|
||||
timeField: 'time',
|
||||
breakdownBy: 'breakdown',
|
||||
inspectorTableId: tableId,
|
||||
});
|
||||
|
||||
expect(inspectorTableId).toBe(tableId);
|
||||
expect(inspectorTable).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
validateAccessor,
|
||||
getColumnByAccessor,
|
||||
prepareLogTable,
|
||||
Dimension,
|
||||
} from '@kbn/visualizations-plugin/common/utils';
|
||||
import { DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { MetricWTrend } from '@elastic/charts';
|
||||
import type { TrendlineExpressionFunctionDefinition } from '../types';
|
||||
import { DEFAULT_TRENDLINE_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
|
||||
|
||||
export const metricTrendlineFunction = (): TrendlineExpressionFunctionDefinition => ({
|
||||
name: EXPRESSION_METRIC_TRENDLINE_NAME,
|
||||
inputTypes: ['datatable'],
|
||||
type: EXPRESSION_METRIC_TRENDLINE_NAME,
|
||||
help: i18n.translate('expressionMetricVis.trendline.function.help', {
|
||||
defaultMessage: 'Metric visualization',
|
||||
}),
|
||||
args: {
|
||||
metric: {
|
||||
types: ['vis_dimension', 'string'],
|
||||
help: i18n.translate('expressionMetricVis.trendline.function.metric.help', {
|
||||
defaultMessage: 'The primary metric.',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
timeField: {
|
||||
types: ['vis_dimension', 'string'],
|
||||
help: i18n.translate('expressionMetricVis.trendline.function.timeField.help', {
|
||||
defaultMessage: 'The time field for the trend line',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
breakdownBy: {
|
||||
types: ['vis_dimension', 'string'],
|
||||
help: i18n.translate('expressionMetricVis.trendline.function.breakdownBy.help', {
|
||||
defaultMessage: 'The dimension containing the labels for sub-categories.',
|
||||
}),
|
||||
},
|
||||
table: {
|
||||
types: ['datatable'],
|
||||
help: i18n.translate('expressionMetricVis.trendline.function.table.help', {
|
||||
defaultMessage: 'A data table',
|
||||
}),
|
||||
multi: false,
|
||||
},
|
||||
inspectorTableId: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionMetricVis.trendline.function.inspectorTableId.help', {
|
||||
defaultMessage: 'An ID for the inspector table',
|
||||
}),
|
||||
multi: false,
|
||||
default: 'trendline',
|
||||
},
|
||||
},
|
||||
fn(input, args, handlers) {
|
||||
const table = args.table;
|
||||
validateAccessor(args.metric, table.columns);
|
||||
validateAccessor(args.timeField, table.columns);
|
||||
validateAccessor(args.breakdownBy, table.columns);
|
||||
|
||||
const argsTable: Dimension[] = [
|
||||
[
|
||||
[args.metric],
|
||||
i18n.translate('expressionMetricVis.function.dimension.metric', {
|
||||
defaultMessage: 'Metric',
|
||||
}),
|
||||
],
|
||||
[
|
||||
[args.timeField],
|
||||
i18n.translate('expressionMetricVis.function.dimension.timeField', {
|
||||
defaultMessage: 'Time field',
|
||||
}),
|
||||
],
|
||||
];
|
||||
|
||||
if (args.breakdownBy) {
|
||||
argsTable.push([
|
||||
[args.breakdownBy],
|
||||
i18n.translate('expressionMetricVis.function.dimension.splitGroup', {
|
||||
defaultMessage: 'Split group',
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
const inspectorTable = prepareLogTable(table, argsTable, true);
|
||||
|
||||
const metricColId = getColumnByAccessor(args.metric, table.columns)?.id;
|
||||
const timeColId = getColumnByAccessor(args.timeField, table.columns)?.id;
|
||||
|
||||
if (!metricColId || !timeColId) {
|
||||
throw new Error("Metric trendline - couldn't find metric or time column!");
|
||||
}
|
||||
|
||||
const trends: Record<string, MetricWTrend['trend']> = {};
|
||||
|
||||
if (!args.breakdownBy) {
|
||||
trends[DEFAULT_TRENDLINE_NAME] = table.rows.map((row) => ({
|
||||
x: row[timeColId],
|
||||
y: row[metricColId],
|
||||
}));
|
||||
} else {
|
||||
const breakdownByColId = getColumnByAccessor(args.breakdownBy, table.columns)?.id;
|
||||
|
||||
if (!breakdownByColId) {
|
||||
throw new Error("Metric trendline - couldn't find breakdown column!");
|
||||
}
|
||||
|
||||
const rowsByBreakdown: Record<string, DatatableRow[]> = {};
|
||||
table.rows.forEach((row) => {
|
||||
const breakdownTerm = row[breakdownByColId];
|
||||
if (!(breakdownTerm in rowsByBreakdown)) {
|
||||
rowsByBreakdown[breakdownTerm] = [];
|
||||
}
|
||||
rowsByBreakdown[breakdownTerm].push(row);
|
||||
});
|
||||
|
||||
for (const breakdownTerm in rowsByBreakdown) {
|
||||
if (!rowsByBreakdown.hasOwnProperty(breakdownTerm)) continue;
|
||||
trends[breakdownTerm] = rowsByBreakdown[breakdownTerm].map((row) => ({
|
||||
x: row[timeColId] !== null ? row[timeColId] : NaN,
|
||||
y: row[metricColId] !== null ? row[metricColId] : NaN,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: EXPRESSION_METRIC_TRENDLINE_NAME,
|
||||
trends,
|
||||
inspectorTable,
|
||||
inspectorTableId: args.inspectorTableId,
|
||||
};
|
||||
},
|
||||
});
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { LayoutDirection } from '@elastic/charts';
|
||||
import { visType } from '../types';
|
||||
import { MetricVisExpressionFunctionDefinition } from '../types';
|
||||
import { EXPRESSION_METRIC_NAME } from '../constants';
|
||||
import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
|
||||
|
||||
export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
|
||||
name: EXPRESSION_METRIC_NAME,
|
||||
|
@ -51,6 +51,12 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
|
|||
defaultMessage: 'The dimension containing the labels for sub-categories.',
|
||||
}),
|
||||
},
|
||||
trendline: {
|
||||
types: [EXPRESSION_METRIC_TRENDLINE_NAME],
|
||||
help: i18n.translate('expressionMetricVis.function.trendline.help', {
|
||||
defaultMessage: 'An optional trendline configuration',
|
||||
}),
|
||||
},
|
||||
subtitle: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionMetricVis.function.subtitle.help', {
|
||||
|
@ -98,6 +104,14 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
|
|||
'Specifies the minimum number of tiles in the metric grid regardless of the input data.',
|
||||
}),
|
||||
},
|
||||
inspectorTableId: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressionMetricVis.function.inspectorTableId.help', {
|
||||
defaultMessage: 'An ID for the inspector table',
|
||||
}),
|
||||
multi: false,
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
fn(input, args, handlers) {
|
||||
validateAccessor(args.metric, input.columns);
|
||||
|
@ -146,7 +160,14 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
|
|||
}
|
||||
|
||||
const logTable = prepareLogTable(input, argsTable, true);
|
||||
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
|
||||
handlers.inspectorAdapters.tables.logDatatable(args.inspectorTableId, logTable);
|
||||
|
||||
if (args.trendline?.inspectorTable && args.trendline.inspectorTableId) {
|
||||
handlers.inspectorAdapters.tables.logDatatable(
|
||||
args.trendline?.inspectorTableId,
|
||||
args.trendline?.inspectorTable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -164,6 +185,7 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
|
|||
progressDirection: args.progressDirection,
|
||||
maxCols: args.maxCols,
|
||||
minTiles: args.minTiles,
|
||||
trends: args.trendline?.trends,
|
||||
},
|
||||
dimensions: {
|
||||
metric: args.metric,
|
||||
|
|
|
@ -22,4 +22,4 @@ export type {
|
|||
|
||||
export { metricVisFunction } from './expression_functions';
|
||||
|
||||
export { EXPRESSION_METRIC_NAME } from './constants';
|
||||
export { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from './constants';
|
||||
|
|
|
@ -7,22 +7,23 @@
|
|||
*/
|
||||
|
||||
import type { PaletteOutput } from '@kbn/coloring';
|
||||
import { LayoutDirection } from '@elastic/charts';
|
||||
import { LayoutDirection, MetricWTrend } from '@elastic/charts';
|
||||
import {
|
||||
Datatable,
|
||||
ExpressionFunctionDefinition,
|
||||
ExpressionValueRender,
|
||||
} from '@kbn/expressions-plugin/common';
|
||||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
|
||||
import { ExpressionValueVisDimension, prepareLogTable } from '@kbn/visualizations-plugin/common';
|
||||
import { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import { VisParams, visType } from './expression_renderers';
|
||||
import { EXPRESSION_METRIC_NAME } from '../constants';
|
||||
import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
|
||||
|
||||
export interface MetricArguments {
|
||||
metric: ExpressionValueVisDimension | string;
|
||||
secondaryMetric?: ExpressionValueVisDimension | string;
|
||||
max?: ExpressionValueVisDimension | string;
|
||||
breakdownBy?: ExpressionValueVisDimension | string;
|
||||
trendline?: TrendlineResult;
|
||||
subtitle?: string;
|
||||
secondaryPrefix?: string;
|
||||
progressDirection: LayoutDirection;
|
||||
|
@ -30,6 +31,7 @@ export interface MetricArguments {
|
|||
palette?: PaletteOutput<CustomPaletteState>;
|
||||
maxCols: number;
|
||||
minTiles?: number;
|
||||
inspectorTableId: string;
|
||||
}
|
||||
|
||||
export type MetricInput = Datatable;
|
||||
|
@ -46,3 +48,25 @@ export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition
|
|||
MetricArguments,
|
||||
ExpressionValueRender<MetricVisRenderConfig>
|
||||
>;
|
||||
|
||||
export interface TrendlineArguments {
|
||||
metric: ExpressionValueVisDimension | string;
|
||||
timeField: ExpressionValueVisDimension | string;
|
||||
breakdownBy?: ExpressionValueVisDimension | string;
|
||||
table: Datatable;
|
||||
inspectorTableId: string;
|
||||
}
|
||||
|
||||
export interface TrendlineResult {
|
||||
type: typeof EXPRESSION_METRIC_TRENDLINE_NAME;
|
||||
trends: Record<string, MetricWTrend['trend']>;
|
||||
inspectorTable: ReturnType<typeof prepareLogTable>;
|
||||
inspectorTableId: string;
|
||||
}
|
||||
|
||||
export type TrendlineExpressionFunctionDefinition = ExpressionFunctionDefinition<
|
||||
typeof EXPRESSION_METRIC_TRENDLINE_NAME,
|
||||
Datatable,
|
||||
TrendlineArguments,
|
||||
TrendlineResult
|
||||
>;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
|
||||
import { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import { LayoutDirection } from '@elastic/charts';
|
||||
import { TrendlineResult } from './expression_functions';
|
||||
|
||||
export const visType = 'metric';
|
||||
|
||||
|
@ -27,6 +28,7 @@ export interface MetricVisParam {
|
|||
progressDirection: LayoutDirection;
|
||||
maxCols: number;
|
||||
minTiles?: number;
|
||||
trends?: TrendlineResult['trends'];
|
||||
}
|
||||
|
||||
export interface VisParams {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
MetricElementEvent,
|
||||
MetricWNumber,
|
||||
MetricWProgress,
|
||||
MetricWTrend,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
|
@ -25,6 +26,8 @@ import { HtmlAttributes } from 'csstype';
|
|||
import { CustomPaletteState } from '@kbn/charts-plugin/common/expressions/palette/types';
|
||||
import { DimensionsVisParam } from '../../common';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DEFAULT_TRENDLINE_NAME } from '../../common/constants';
|
||||
import faker from 'faker';
|
||||
|
||||
const mockDeserialize = jest.fn((params) => {
|
||||
const converter =
|
||||
|
@ -367,6 +370,40 @@ describe('MetricVisComponent', function () {
|
|||
(getConfig(basePriceColumnId, 'horizontal') as MetricWProgress).progressBarDirection
|
||||
).toBe('horizontal');
|
||||
});
|
||||
|
||||
it('should configure trendline if provided', () => {
|
||||
const trends = {
|
||||
[DEFAULT_TRENDLINE_NAME]: [
|
||||
{ x: 1, y: 2 },
|
||||
{ x: 3, y: 4 },
|
||||
{ x: 5, y: 6 },
|
||||
{ x: 7, y: 8 },
|
||||
],
|
||||
};
|
||||
|
||||
const tileConfig = shallow(
|
||||
<MetricVis
|
||||
config={{
|
||||
...config,
|
||||
metric: {
|
||||
...config.metric,
|
||||
trends,
|
||||
},
|
||||
dimensions: {
|
||||
...config.dimensions,
|
||||
breakdownBy: undefined,
|
||||
},
|
||||
}}
|
||||
data={table}
|
||||
{...defaultProps}
|
||||
/>
|
||||
)
|
||||
.find(Metric)
|
||||
.props().data![0][0]! as MetricWTrend;
|
||||
|
||||
expect(tileConfig.trend).toEqual(trends[DEFAULT_TRENDLINE_NAME]);
|
||||
expect(tileConfig.trendShape).toEqual('area');
|
||||
});
|
||||
});
|
||||
|
||||
describe('metric grid', () => {
|
||||
|
@ -701,6 +738,74 @@ describe('MetricVisComponent', function () {
|
|||
]
|
||||
`);
|
||||
});
|
||||
it('should configure trendlines if provided', () => {
|
||||
const trends: Record<string, MetricWTrend['trend']> = {
|
||||
Friday: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
Wednesday: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
Saturday: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
Sunday: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
Thursday: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
Other: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
// this one shouldn't show up!
|
||||
[DEFAULT_TRENDLINE_NAME]: [
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
{ x: faker.random.number(), y: faker.random.number() },
|
||||
],
|
||||
};
|
||||
|
||||
const data = shallow(
|
||||
<MetricVis
|
||||
config={{
|
||||
...config,
|
||||
metric: {
|
||||
...config.metric,
|
||||
trends,
|
||||
},
|
||||
}}
|
||||
data={table}
|
||||
{...defaultProps}
|
||||
/>
|
||||
)
|
||||
.find(Metric)
|
||||
.props().data![0] as MetricWTrend[];
|
||||
|
||||
data?.forEach((tileConfig) => {
|
||||
expect(tileConfig.trend).toEqual(trends[tileConfig.title!]);
|
||||
expect(tileConfig.trendShape).toEqual('area');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders with no data', () => {
|
||||
const component = shallow(
|
||||
|
@ -816,6 +921,44 @@ describe('MetricVisComponent', function () {
|
|||
expect(renderCompleteSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should convert null values to NaN', () => {
|
||||
const metricId = faker.random.word();
|
||||
|
||||
const tableWNull: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: metricId,
|
||||
name: metricId,
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [{ [metricId]: null }],
|
||||
};
|
||||
|
||||
const metricConfig = shallow(
|
||||
<MetricVis
|
||||
config={{
|
||||
metric: {
|
||||
progressDirection: 'vertical',
|
||||
maxCols: 5,
|
||||
},
|
||||
dimensions: {
|
||||
metric: metricId,
|
||||
},
|
||||
}}
|
||||
data={tableWNull}
|
||||
{...defaultProps}
|
||||
/>
|
||||
)
|
||||
.find(Metric)
|
||||
.props().data![0][0]! as MetricWNumber;
|
||||
|
||||
expect(metricConfig.value).toBeNaN();
|
||||
});
|
||||
|
||||
describe('filter events', () => {
|
||||
const fireEventSpy = jest.fn();
|
||||
|
||||
|
|
|
@ -18,13 +18,14 @@ import {
|
|||
isMetricElementEvent,
|
||||
RenderChangeListener,
|
||||
Settings,
|
||||
MetricWTrend,
|
||||
MetricWNumber,
|
||||
} from '@elastic/charts';
|
||||
import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
|
||||
import type {
|
||||
Datatable,
|
||||
DatatableColumn,
|
||||
DatatableRow,
|
||||
IInterpreterRenderHandlers,
|
||||
RenderMode,
|
||||
} from '@kbn/expressions-plugin/common';
|
||||
|
@ -35,6 +36,7 @@ import { CUSTOM_PALETTE } from '@kbn/coloring';
|
|||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { useResizeObserver } from '@elastic/eui';
|
||||
import { DEFAULT_TRENDLINE_NAME } from '../../common/constants';
|
||||
import { VisParams } from '../../common';
|
||||
import {
|
||||
getPaletteService,
|
||||
|
@ -222,28 +224,20 @@ export const MetricVis = ({
|
|||
.getConverterFor('text');
|
||||
}
|
||||
|
||||
let getProgressBarConfig = (_row: DatatableRow): Partial<MetricWProgress> => ({});
|
||||
|
||||
const maxColId = config.dimensions.max
|
||||
? getColumnByAccessor(config.dimensions.max, data.columns)?.id
|
||||
: undefined;
|
||||
if (maxColId) {
|
||||
getProgressBarConfig = (_row: DatatableRow): Partial<MetricWProgress> => ({
|
||||
domainMax: _row[maxColId],
|
||||
progressBarDirection: config.metric.progressDirection,
|
||||
});
|
||||
}
|
||||
|
||||
const metricConfigs: MetricSpec['data'][number] = (
|
||||
breakdownByColumn ? data.rows : data.rows.slice(0, 1)
|
||||
).map((row, rowIdx) => {
|
||||
const value = row[primaryMetricColumn.id];
|
||||
const value: number = row[primaryMetricColumn.id] !== null ? row[primaryMetricColumn.id] : NaN;
|
||||
const title = breakdownByColumn
|
||||
? formatBreakdownValue(row[breakdownByColumn.id])
|
||||
: primaryMetricColumn.name;
|
||||
const subtitle = breakdownByColumn ? primaryMetricColumn.name : config.metric.subtitle;
|
||||
const secondaryPrefix = config.metric.secondaryPrefix ?? secondaryMetricColumn?.name;
|
||||
return {
|
||||
const baseMetric: MetricWNumber = {
|
||||
value,
|
||||
valueFormatter: formatPrimaryMetric,
|
||||
title,
|
||||
|
@ -272,8 +266,39 @@ export const MetricVis = ({
|
|||
rowIdx
|
||||
) ?? defaultColor
|
||||
: config.metric.color ?? defaultColor,
|
||||
...getProgressBarConfig(row),
|
||||
};
|
||||
|
||||
const trendId = breakdownByColumn ? row[breakdownByColumn.id] : DEFAULT_TRENDLINE_NAME;
|
||||
if (config.metric.trends && config.metric.trends[trendId]) {
|
||||
const metricWTrend: MetricWTrend = {
|
||||
...baseMetric,
|
||||
trend: config.metric.trends[trendId],
|
||||
trendShape: 'area',
|
||||
trendA11yTitle: i18n.translate('expressionMetricVis.trendA11yTitle', {
|
||||
defaultMessage: '{dataTitle} over time.',
|
||||
values: {
|
||||
dataTitle: primaryMetricColumn.name,
|
||||
},
|
||||
}),
|
||||
trendA11yDescription: i18n.translate('expressionMetricVis.trendA11yDescription', {
|
||||
defaultMessage: 'A line chart showing the trend of the primary metric over time.',
|
||||
}),
|
||||
};
|
||||
|
||||
return metricWTrend;
|
||||
}
|
||||
|
||||
if (maxColId && config.metric.progressDirection) {
|
||||
const metricWProgress: MetricWProgress = {
|
||||
...baseMetric,
|
||||
domainMax: row[maxColId],
|
||||
progressBarDirection: config.metric.progressDirection,
|
||||
};
|
||||
|
||||
return metricWProgress;
|
||||
}
|
||||
|
||||
return baseMetric;
|
||||
});
|
||||
|
||||
if (config.metric.minTiles) {
|
||||
|
|
|
@ -13,3 +13,4 @@ export function plugin() {
|
|||
}
|
||||
|
||||
export { getDataBoundsForPalette } from './utils';
|
||||
export { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../common';
|
||||
|
|
|
@ -17,6 +17,7 @@ import { setFormatService, setPaletteService } from './services';
|
|||
import { getMetricVisRenderer } from './expression_renderers';
|
||||
import { setThemeService } from './services/theme_service';
|
||||
import { setUiSettingsService } from './services/ui_settings';
|
||||
import { metricTrendlineFunction } from '../common/expression_functions/metric_trendline_function';
|
||||
|
||||
/** @internal */
|
||||
export interface ExpressionMetricPluginSetup {
|
||||
|
@ -45,6 +46,7 @@ export class ExpressionMetricPlugin implements Plugin {
|
|||
});
|
||||
|
||||
expressions.registerFunction(metricVisFunction);
|
||||
expressions.registerFunction(metricTrendlineFunction);
|
||||
expressions.registerRenderer(getMetricVisRenderer({ getStartDeps }));
|
||||
|
||||
setUiSettingsService(core.uiSettings);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { ColorSchemas } from '@kbn/charts-plugin/common';
|
||||
import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import { CollapseFunction } from '@kbn/visualizations-plugin/common';
|
||||
import { GaugeVisParams } from '../../types';
|
||||
import { getConfiguration } from './goal';
|
||||
|
||||
|
@ -69,7 +70,10 @@ describe('getConfiguration', () => {
|
|||
buckets,
|
||||
maxAccessor,
|
||||
columnsWithoutReferenced: [],
|
||||
bucketCollapseFn: { [collapseFn]: [breakdownByAccessor] },
|
||||
bucketCollapseFn: { [collapseFn]: [breakdownByAccessor] } as Record<
|
||||
CollapseFunction,
|
||||
string[]
|
||||
>,
|
||||
})
|
||||
).toEqual({
|
||||
breakdownByAccessor,
|
||||
|
@ -78,6 +82,7 @@ describe('getConfiguration', () => {
|
|||
layerType: 'data',
|
||||
maxAccessor,
|
||||
metricAccessor,
|
||||
showBar: true,
|
||||
palette,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
*/
|
||||
|
||||
import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import { Column, MetricVisConfiguration } from '@kbn/visualizations-plugin/common';
|
||||
import {
|
||||
CollapseFunction,
|
||||
Column,
|
||||
MetricVisConfiguration,
|
||||
} from '@kbn/visualizations-plugin/common';
|
||||
import { GaugeVisParams } from '../../types';
|
||||
|
||||
export const getConfiguration = (
|
||||
|
@ -28,15 +32,15 @@ export const getConfiguration = (
|
|||
};
|
||||
maxAccessor: string;
|
||||
columnsWithoutReferenced: Column[];
|
||||
bucketCollapseFn?: Record<string, string[]>;
|
||||
bucketCollapseFn?: Record<CollapseFunction, string[]>;
|
||||
}
|
||||
): MetricVisConfiguration => {
|
||||
const [metricAccessor] = metrics;
|
||||
const [breakdownByAccessor] = buckets.all;
|
||||
const collapseFn = bucketCollapseFn
|
||||
? Object.keys(bucketCollapseFn).find((key) =>
|
||||
bucketCollapseFn[key].includes(breakdownByAccessor)
|
||||
)
|
||||
? (Object.keys(bucketCollapseFn).find((key) =>
|
||||
bucketCollapseFn[key as CollapseFunction].includes(breakdownByAccessor)
|
||||
) as CollapseFunction)
|
||||
: undefined;
|
||||
return {
|
||||
layerId,
|
||||
|
@ -45,6 +49,7 @@ export const getConfiguration = (
|
|||
metricAccessor,
|
||||
breakdownByAccessor,
|
||||
maxAccessor,
|
||||
showBar: Boolean(maxAccessor),
|
||||
collapseFn,
|
||||
subtitle: gauge.labels.show && gauge.style.subText ? gauge.style.subText : undefined,
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { ColorSchemas } from '@kbn/charts-plugin/common';
|
||||
import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import { CollapseFunction } from '@kbn/visualizations-plugin/common';
|
||||
import { getConfiguration } from '.';
|
||||
import { VisParams } from '../../types';
|
||||
|
||||
|
@ -50,7 +51,7 @@ describe('getConfiguration', () => {
|
|||
metrics: [metric],
|
||||
buckets: { all: [bucket], customBuckets: { metric: bucket } },
|
||||
columnsWithoutReferenced: [],
|
||||
bucketCollapseFn: { [collapseFn]: [bucket] },
|
||||
bucketCollapseFn: { [collapseFn]: [bucket] } as Record<CollapseFunction, string[]>,
|
||||
})
|
||||
).toEqual({
|
||||
breakdownByAccessor: bucket,
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
*/
|
||||
|
||||
import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import { Column, MetricVisConfiguration } from '@kbn/visualizations-plugin/common';
|
||||
import {
|
||||
CollapseFunction,
|
||||
Column,
|
||||
MetricVisConfiguration,
|
||||
} from '@kbn/visualizations-plugin/common';
|
||||
import { VisParams } from '../../types';
|
||||
|
||||
export const getConfiguration = (
|
||||
|
@ -26,15 +30,15 @@ export const getConfiguration = (
|
|||
customBuckets: Record<string, string>;
|
||||
};
|
||||
columnsWithoutReferenced: Column[];
|
||||
bucketCollapseFn?: Record<string, string[]>;
|
||||
bucketCollapseFn?: Record<CollapseFunction, string[]>;
|
||||
}
|
||||
): MetricVisConfiguration => {
|
||||
const [metricAccessor] = metrics;
|
||||
const [breakdownByAccessor] = buckets.all;
|
||||
const collapseFn = bucketCollapseFn
|
||||
? Object.keys(bucketCollapseFn).find((key) =>
|
||||
bucketCollapseFn[key].includes(breakdownByAccessor)
|
||||
)
|
||||
? (Object.keys(bucketCollapseFn).find((key) =>
|
||||
bucketCollapseFn[key as CollapseFunction].includes(breakdownByAccessor)
|
||||
) as CollapseFunction)
|
||||
: undefined;
|
||||
return {
|
||||
layerId,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { AggTypes } from '../../../common';
|
||||
import { getConfiguration } from '.';
|
||||
import { CollapseFunction } from '@kbn/visualizations-plugin/common';
|
||||
|
||||
const params = {
|
||||
perPage: 20,
|
||||
|
@ -48,7 +49,7 @@ describe('getConfiguration', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
bucketCollapseFn: { sum: ['bucket-1'] },
|
||||
bucketCollapseFn: { sum: ['bucket-1'] } as Record<CollapseFunction, string[]>,
|
||||
})
|
||||
).toEqual({
|
||||
columns: [
|
||||
|
|
|
@ -6,19 +6,26 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Column, PagingState, TableVisConfiguration } from '@kbn/visualizations-plugin/common';
|
||||
import {
|
||||
CollapseFunction,
|
||||
Column,
|
||||
PagingState,
|
||||
TableVisConfiguration,
|
||||
} from '@kbn/visualizations-plugin/common';
|
||||
import { TableVisParams } from '../../../common';
|
||||
|
||||
const getColumns = (
|
||||
params: TableVisParams,
|
||||
metrics: string[],
|
||||
columns: Column[],
|
||||
bucketCollapseFn?: Record<string, string[]>
|
||||
bucketCollapseFn?: Record<CollapseFunction, string[]>
|
||||
) => {
|
||||
const { showTotal, totalFunc } = params;
|
||||
return columns.map(({ columnId }) => {
|
||||
const collapseFn = bucketCollapseFn
|
||||
? Object.keys(bucketCollapseFn).find((key) => bucketCollapseFn[key].includes(columnId))
|
||||
? (Object.keys(bucketCollapseFn).find((key) =>
|
||||
bucketCollapseFn[key as CollapseFunction].includes(columnId)
|
||||
) as CollapseFunction)
|
||||
: undefined;
|
||||
return {
|
||||
columnId,
|
||||
|
@ -61,7 +68,7 @@ export const getConfiguration = (
|
|||
customBuckets: Record<string, string>;
|
||||
};
|
||||
columnsWithoutReferenced: Column[];
|
||||
bucketCollapseFn?: Record<string, string[]>;
|
||||
bucketCollapseFn?: Record<CollapseFunction, string[]>;
|
||||
}
|
||||
): TableVisConfiguration => {
|
||||
return {
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { CollapseFunction } from '@kbn/visualizations-plugin/common';
|
||||
import type { Metric } from '../../../../common/types';
|
||||
|
||||
const functionMap: Partial<Record<string, string>> = {
|
||||
const functionMap: Partial<Record<string, CollapseFunction>> = {
|
||||
mean: 'avg',
|
||||
min: 'min',
|
||||
max: 'max',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { METRIC_TYPES } from '@kbn/data-plugin/public';
|
||||
import { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common';
|
||||
import { CollapseFunction, Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common';
|
||||
import {
|
||||
convertToLensModule,
|
||||
getVisSchemas,
|
||||
|
@ -25,7 +25,7 @@ export interface Layer {
|
|||
columnOrder: never[];
|
||||
seriesIdsMap: Record<string, string>;
|
||||
isReferenceLineLayer: boolean;
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
}
|
||||
|
||||
const SIBBLING_PIPELINE_AGGS: string[] = [
|
||||
|
@ -175,9 +175,11 @@ export const convertToLens: ConvertXYToLensVisualization = async (vis, timefilte
|
|||
}
|
||||
});
|
||||
const collapseFn = l.bucketCollapseFn
|
||||
? Object.keys(l.bucketCollapseFn).find((key) =>
|
||||
l.bucketCollapseFn[key].includes(l.buckets.customBuckets[l.metrics[0]])
|
||||
)
|
||||
? (Object.keys(l.bucketCollapseFn).find((key) =>
|
||||
l.bucketCollapseFn[key as CollapseFunction].includes(
|
||||
l.buckets.customBuckets[l.metrics[0]]
|
||||
)
|
||||
) as CollapseFunction)
|
||||
: undefined;
|
||||
return {
|
||||
indexPatternId,
|
||||
|
|
|
@ -134,3 +134,5 @@ export const GaugeColorModes = {
|
|||
PALETTE: 'palette',
|
||||
NONE: 'none',
|
||||
} as const;
|
||||
|
||||
export const CollapseFunctions = ['sum', 'avg', 'min', 'max'] as const;
|
||||
|
|
|
@ -26,8 +26,11 @@ import {
|
|||
GaugeLabelMajorModes,
|
||||
GaugeColorModes,
|
||||
GaugeCentralMajorModes,
|
||||
CollapseFunctions,
|
||||
} from '../constants';
|
||||
|
||||
export type CollapseFunction = typeof CollapseFunctions[number];
|
||||
|
||||
export type FillType = $Values<typeof FillTypes>;
|
||||
export type SeriesType = $Values<typeof SeriesTypes>;
|
||||
export type YAxisMode = $Values<typeof YAxisModes>;
|
||||
|
@ -72,7 +75,7 @@ export interface XYDataLayerConfig {
|
|||
yConfig?: YConfig[];
|
||||
splitAccessor?: string;
|
||||
palette?: PaletteOutput;
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
xScaleType?: 'time' | 'linear' | 'ordinal';
|
||||
isHistogram?: boolean;
|
||||
columnToLabel?: string;
|
||||
|
@ -179,7 +182,7 @@ export interface ColumnState {
|
|||
columnId: string;
|
||||
summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max';
|
||||
alignment?: 'left' | 'right' | 'center';
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
}
|
||||
|
||||
export interface TableVisConfiguration {
|
||||
|
@ -203,10 +206,11 @@ export interface MetricVisConfiguration {
|
|||
breakdownByAccessor?: string;
|
||||
// the dimensions can optionally be single numbers
|
||||
// computed by collapsing all rows
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
subtitle?: string;
|
||||
secondaryPrefix?: string;
|
||||
progressDirection?: LayoutDirection;
|
||||
showBar?: boolean;
|
||||
color?: string;
|
||||
palette?: PaletteOutput<CustomPaletteParams>;
|
||||
maxCols?: number;
|
||||
|
@ -218,7 +222,7 @@ export interface PartitionLayerState {
|
|||
primaryGroups: string[];
|
||||
secondaryGroups?: string[];
|
||||
metric?: string;
|
||||
collapseFns?: Record<string, string>;
|
||||
collapseFns?: Record<string, CollapseFunction>;
|
||||
numberDisplay: NumberDisplayType;
|
||||
categoryDisplay: CategoryDisplayType;
|
||||
legendDisplay: LegendDisplayType;
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
*/
|
||||
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { CollapseFunctions } from './constants';
|
||||
import type { SupportedMetric } from './lib/convert/supported_metrics';
|
||||
import type { Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types';
|
||||
import type { CollapseFunction, Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types';
|
||||
|
||||
export const isAnnotationsLayer = (
|
||||
layer: Pick<XYLayerConfig, 'layerType'>
|
||||
|
@ -31,3 +32,6 @@ export const isFieldValid = (
|
|||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isCollapseFunction = (candidate: string | undefined): candidate is CollapseFunction =>
|
||||
Boolean(candidate && CollapseFunctions.includes(candidate as CollapseFunction));
|
||||
|
|
|
@ -8,7 +8,13 @@
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/public';
|
||||
import { AggBasedColumn, SchemaConfig, SupportedAggregation } from '../../common';
|
||||
import {
|
||||
AggBasedColumn,
|
||||
CollapseFunction,
|
||||
isCollapseFunction,
|
||||
SchemaConfig,
|
||||
SupportedAggregation,
|
||||
} from '../../common';
|
||||
import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets';
|
||||
import { isSiblingPipeline } from '../../common/convert_to_lens/lib/utils';
|
||||
import { BucketColumn } from '../../common/convert_to_lens/lib';
|
||||
|
@ -30,7 +36,7 @@ export const getBucketCollapseFn = (
|
|||
customBucketsMap: Record<string, string>,
|
||||
metricColumns: AggBasedColumn[]
|
||||
) => {
|
||||
const collapseFnMap: Record<string, string[]> = {
|
||||
const collapseFnMap: Record<CollapseFunction, string[]> = {
|
||||
min: [],
|
||||
max: [],
|
||||
sum: [],
|
||||
|
@ -45,8 +51,11 @@ export const getBucketCollapseFn = (
|
|||
const collapseFn = metrics
|
||||
.find((m) => m.aggId === metricColumn.meta.aggId)
|
||||
?.aggType.split('_')[0];
|
||||
if (collapseFn) {
|
||||
collapseFnMap[collapseFn].push(bucket.columnId);
|
||||
|
||||
if (isCollapseFunction(collapseFn)) {
|
||||
if (collapseFn) {
|
||||
collapseFnMap[collapseFn].push(bucket.columnId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -217,6 +217,27 @@ export class InspectorService extends FtrService {
|
|||
await this.openInspectorView('Requests');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check how many tables are being shown in the inspector.
|
||||
* @returns
|
||||
*/
|
||||
public async getNumberOfTables(): Promise<number> {
|
||||
const chooserDataTestId = 'inspectorTableChooser';
|
||||
const menuDataTestId = 'inspectorTableChooserMenuPanel';
|
||||
|
||||
if (!(await this.testSubjects.exists(chooserDataTestId))) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return await this.retry.try(async () => {
|
||||
await this.testSubjects.click(chooserDataTestId);
|
||||
const menu = await this.testSubjects.find(menuDataTestId);
|
||||
return (
|
||||
await menu.findAllByCssSelector(`[data-test-subj="${menuDataTestId}"] .euiContextMenuItem`)
|
||||
).length;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected option value from combobox
|
||||
*/
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import type { CollapseExpressionFunction } from './types';
|
||||
|
||||
type CollapseFunction = 'sum' | 'avg' | 'min' | 'max';
|
||||
export type CollapseFunction = 'sum' | 'avg' | 'min' | 'max';
|
||||
export interface CollapseArgs {
|
||||
by?: string[];
|
||||
metric?: string[];
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
|
|||
import type { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import type { ExpressionFunctionDefinition, DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import type { SortingHint } from '../..';
|
||||
import { CollapseFunction } from '../collapse';
|
||||
|
||||
export type LensGridDirection = 'none' | Direction;
|
||||
|
||||
|
@ -43,7 +44,7 @@ export interface ColumnState {
|
|||
colorMode?: 'none' | 'cell' | 'text';
|
||||
summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max';
|
||||
summaryLabel?: string;
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
}
|
||||
|
||||
export type DatatableColumnResult = ColumnState & { type: 'lens_datatable_column' };
|
||||
|
|
13
x-pack/plugins/lens/common/layer_types.ts
Normal file
13
x-pack/plugins/lens/common/layer_types.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const layerTypes = {
|
||||
DATA: 'data',
|
||||
REFERENCELINE: 'referenceLine',
|
||||
ANNOTATIONS: 'annotations',
|
||||
METRIC_TRENDLINE: 'metricTrendline',
|
||||
} as const;
|
|
@ -11,9 +11,10 @@ import type { $Values } from '@kbn/utility-types';
|
|||
import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { ColorMode } from '@kbn/charts-plugin/common';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/common';
|
||||
import type { LegendSize } from '@kbn/visualizations-plugin/common';
|
||||
import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants';
|
||||
import { layerTypes } from './layer_types';
|
||||
import { CollapseFunction } from './expressions';
|
||||
|
||||
export type { OriginalColumn } from './expressions/map_to_columns';
|
||||
|
||||
|
@ -39,7 +40,7 @@ export interface PersistableFilter extends Filter {
|
|||
|
||||
export type SortingHint = 'version';
|
||||
|
||||
export type LayerType = typeof LayerTypes[keyof typeof LayerTypes];
|
||||
export type LayerType = typeof layerTypes[keyof typeof layerTypes];
|
||||
|
||||
export type ValueLabelConfig = 'hide' | 'show';
|
||||
|
||||
|
@ -59,7 +60,7 @@ export interface SharedPieLayerState {
|
|||
primaryGroups: string[];
|
||||
secondaryGroups?: string[];
|
||||
metric?: string;
|
||||
collapseFns?: Record<string, string>;
|
||||
collapseFns?: Record<string, CollapseFunction>;
|
||||
numberDisplay: NumberDisplayType;
|
||||
categoryDisplay: CategoryDisplayType;
|
||||
legendDisplay: LegendDisplayType;
|
||||
|
|
|
@ -75,6 +75,7 @@ describe('getLayerMetaInfo', () => {
|
|||
isStaticValue: false,
|
||||
sortingHint: undefined,
|
||||
hasTimeShift: true,
|
||||
hasReducedTimeRange: true,
|
||||
})),
|
||||
getTableSpec: jest.fn(),
|
||||
getVisualDefaults: jest.fn(),
|
||||
|
@ -82,6 +83,7 @@ describe('getLayerMetaInfo', () => {
|
|||
getMaxPossibleNumValues: jest.fn(),
|
||||
getFilters: jest.fn(),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
|
||||
expect(
|
||||
|
@ -101,6 +103,7 @@ describe('getLayerMetaInfo', () => {
|
|||
getMaxPossibleNumValues: jest.fn(),
|
||||
getFilters: jest.fn(() => ({ error: 'filters error' })),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
|
||||
expect(
|
||||
|
@ -128,6 +131,7 @@ describe('getLayerMetaInfo', () => {
|
|||
getMaxPossibleNumValues: jest.fn(),
|
||||
getFilters: jest.fn(() => ({ error: 'filters error' })),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
|
||||
// both capabilities should be enabled to enable discover
|
||||
|
@ -180,6 +184,7 @@ describe('getLayerMetaInfo', () => {
|
|||
},
|
||||
disabled: { kuery: [], lucene: [] },
|
||||
})),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
|
||||
const { error, meta } = getLayerMetaInfo(
|
||||
|
|
|
@ -88,7 +88,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
target: mockedDndOperations.notFiltering,
|
||||
state,
|
||||
setState,
|
||||
dimensionGroups: [],
|
||||
targetLayerDimensionGroups: [],
|
||||
indexPatterns: mockDataViews(),
|
||||
};
|
||||
|
||||
|
@ -285,7 +285,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
onDrop({
|
||||
...defaultProps,
|
||||
source: mockedDraggedField,
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups: dimensionGroups,
|
||||
dropType: 'field_add',
|
||||
target: {
|
||||
...defaultProps.target,
|
||||
|
@ -768,8 +768,9 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...state.layers,
|
||||
first: {
|
||||
...state.layers.first,
|
||||
columnOrder: ['col2'],
|
||||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
col1: state.layers.first.columns.col1,
|
||||
col2: state.layers.first.columns.col1,
|
||||
},
|
||||
},
|
||||
|
@ -803,10 +804,10 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['col1', 'col3'],
|
||||
columnOrder: ['col1', 'col2', 'col3'],
|
||||
columns: {
|
||||
col1: testState.layers.first.columns.col2,
|
||||
col2: testState.layers.first.columns.col2,
|
||||
col3: testState.layers.first.columns.col3,
|
||||
},
|
||||
},
|
||||
|
@ -879,7 +880,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
source: mockedDndOperations.bucket,
|
||||
state: testState,
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups: dimensionGroups,
|
||||
dropType: 'move_compatible',
|
||||
});
|
||||
|
||||
|
@ -890,11 +891,11 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['newCol', 'col1', 'col3', 'col4'],
|
||||
columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'],
|
||||
columns: {
|
||||
newCol: testState.layers.first.columns.col2,
|
||||
col1: testState.layers.first.columns.col1,
|
||||
col2: testState.layers.first.columns.col2,
|
||||
col3: testState.layers.first.columns.col3,
|
||||
col4: testState.layers.first.columns.col4,
|
||||
},
|
||||
|
@ -918,7 +919,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
source: mockedDndOperations.bucket,
|
||||
state: testState,
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups: dimensionGroups,
|
||||
dropType: 'duplicate_compatible',
|
||||
});
|
||||
|
||||
|
@ -957,7 +958,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
source: mockedDndOperations.bucket2,
|
||||
state: testState,
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -972,11 +973,11 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['col1', 'col2', 'col4'],
|
||||
columnOrder: ['col1', 'col2', 'col3', 'col4'],
|
||||
columns: {
|
||||
col1: testState.layers.first.columns.col3,
|
||||
col2: testState.layers.first.columns.col2,
|
||||
col3: testState.layers.first.columns.col3,
|
||||
col4: testState.layers.first.columns.col4,
|
||||
},
|
||||
},
|
||||
|
@ -1028,7 +1029,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1043,11 +1044,11 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['col1', 'col2', 'col4', 'col5', 'col6'],
|
||||
columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'],
|
||||
columns: {
|
||||
col1: testState.layers.first.columns.col3,
|
||||
col2: testState.layers.first.columns.col2,
|
||||
col3: testState.layers.first.columns.col3,
|
||||
col4: testState.layers.first.columns.col4,
|
||||
col5: expect.objectContaining({
|
||||
dataType: 'number',
|
||||
|
@ -1085,7 +1086,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
columnId: 'col1',
|
||||
groupId: 'a',
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1128,7 +1129,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
columnId: 'newCol',
|
||||
groupId: 'b',
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1142,9 +1143,9 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['col2', 'col3', 'newCol', 'col4'],
|
||||
columnOrder: ['col1', 'col2', 'col3', 'newCol', 'col4'],
|
||||
columns: {
|
||||
col1: testState.layers.first.columns.col1,
|
||||
newCol: testState.layers.first.columns.col1,
|
||||
col2: testState.layers.first.columns.col2,
|
||||
col3: testState.layers.first.columns.col3,
|
||||
|
@ -1171,7 +1172,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
source: mockedDndOperations.metric,
|
||||
state: testState,
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1214,7 +1215,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
groupId: 'a',
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
// a and b are ordered in reverse visually, but nesting order keeps them in place for column order
|
||||
{ ...dimensionGroups[1], nestingOrder: 1 },
|
||||
{ ...dimensionGroups[0], nestingOrder: 0 },
|
||||
|
@ -1264,7 +1265,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
columnId: 'newCol',
|
||||
groupId: 'a',
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1278,7 +1279,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
columnOrder: ['col1', 'newCol', 'col2', 'col3'],
|
||||
columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'],
|
||||
columns: {
|
||||
col1: testState.layers.first.columns.col1,
|
||||
newCol: expect.objectContaining({
|
||||
|
@ -1287,6 +1288,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
}),
|
||||
col2: testState.layers.first.columns.col2,
|
||||
col3: testState.layers.first.columns.col3,
|
||||
col4: testState.layers.first.columns.col4,
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
|
@ -1311,7 +1313,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
columnId: 'newCol',
|
||||
groupId: 'a',
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1359,7 +1361,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
columnId: 'col2',
|
||||
groupId: 'b',
|
||||
},
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1373,7 +1375,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...testState.layers,
|
||||
first: {
|
||||
...testState.layers.first,
|
||||
columnOrder: ['col1', 'col2', 'col3'],
|
||||
columnOrder: ['col1', 'col2', 'col3', 'col4'],
|
||||
columns: {
|
||||
col1: testState.layers.first.columns.col1,
|
||||
col2: {
|
||||
|
@ -1392,6 +1394,13 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
},
|
||||
col3: testState.layers.first.columns.col3,
|
||||
col4: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Median of bytes',
|
||||
operationType: 'median',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
|
@ -1416,7 +1425,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
source: mockedDndOperations.metricC,
|
||||
dropType: 'swap_compatible',
|
||||
state: testState,
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1459,7 +1468,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
dropType: 'swap_incompatible',
|
||||
source: mockedDndOperations.metricC,
|
||||
state: testState,
|
||||
dimensionGroups: [
|
||||
targetLayerDimensionGroups: [
|
||||
{ ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] },
|
||||
{ ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] },
|
||||
{ ...dimensionGroups[2] },
|
||||
|
@ -1575,7 +1584,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
layerId: 'second',
|
||||
indexPatternId: 'indexPattern1',
|
||||
},
|
||||
dimensionGroups: defaultDimensionGroups,
|
||||
targetLayerDimensionGroups: defaultDimensionGroups,
|
||||
dropType: 'move_compatible',
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
|
@ -1592,10 +1601,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: {
|
||||
...mockedLayers.emptyLayer(),
|
||||
incompleteColumns: {},
|
||||
},
|
||||
second: {
|
||||
columnOrder: ['col2', 'col3', 'col4', 'newCol', 'col5'],
|
||||
columns: {
|
||||
|
@ -1677,10 +1682,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: {
|
||||
...mockedLayers.emptyLayer(),
|
||||
incompleteColumns: {},
|
||||
},
|
||||
second: {
|
||||
columnOrder: ['col2', 'col3', 'col4', 'col5'],
|
||||
columns: {
|
||||
|
@ -1781,10 +1782,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: {
|
||||
...mockedLayers.emptyLayer(),
|
||||
incompleteColumns: {},
|
||||
},
|
||||
second: {
|
||||
columnOrder: ['col2', 'col3', 'col4', 'col5'],
|
||||
columns: {
|
||||
|
@ -1826,10 +1823,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: {
|
||||
...mockedLayers.emptyLayer(),
|
||||
incompleteColumns: {},
|
||||
},
|
||||
second: {
|
||||
columnOrder: ['col2', 'col3', 'col4', 'col5', 'newCol'],
|
||||
columns: {
|
||||
|
@ -1993,9 +1986,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
expect(props.setState).toBeCalledTimes(1);
|
||||
expect(props.setState).toHaveBeenCalledWith({
|
||||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} },
|
||||
layers: expect.objectContaining({
|
||||
second: {
|
||||
...props.state.layers.second,
|
||||
incompleteColumns: {},
|
||||
|
@ -2021,7 +2012,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
it('combine_incompatible: allows dropping to combine to multiterms', () => {
|
||||
|
@ -2058,9 +2049,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
expect(props.setState).toBeCalledTimes(1);
|
||||
expect(props.setState).toHaveBeenCalledWith({
|
||||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} },
|
||||
layers: expect.objectContaining({
|
||||
second: {
|
||||
...props.state.layers.second,
|
||||
incompleteColumns: {},
|
||||
|
@ -2086,7 +2075,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2094,7 +2083,7 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
let props: DatasourceDimensionDropHandlerProps<FormBasedPrivateState>;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
dimensionGroups: defaultDimensionGroups,
|
||||
targetLayerDimensionGroups: defaultDimensionGroups,
|
||||
setState: jest.fn(),
|
||||
dropType: 'move_compatible',
|
||||
|
||||
|
@ -2181,10 +2170,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: {
|
||||
...mockedLayers.emptyLayer(),
|
||||
incompleteColumns: {},
|
||||
},
|
||||
second: {
|
||||
columnOrder: ['second', 'secondX0', 'newColumnX0', 'newColumn'],
|
||||
columns: {
|
||||
|
@ -2240,10 +2225,6 @@ describe('FormBasedDimensionEditorPanel: onDrop', () => {
|
|||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
first: {
|
||||
...mockedLayers.emptyLayer(),
|
||||
incompleteColumns: {},
|
||||
},
|
||||
second: {
|
||||
columnOrder: ['second', 'secondX0'],
|
||||
columns: {
|
||||
|
|
|
@ -17,14 +17,12 @@ import {
|
|||
} from '../../../../types';
|
||||
import {
|
||||
insertOrReplaceColumn,
|
||||
deleteColumn,
|
||||
getColumnOrder,
|
||||
reorderByGroups,
|
||||
copyColumn,
|
||||
hasOperationSupportForMultipleFields,
|
||||
getOperationHelperForMultipleFields,
|
||||
replaceColumn,
|
||||
deleteColumnInLayers,
|
||||
} from '../../operations';
|
||||
import { mergeLayer, mergeLayers } from '../../state_helpers';
|
||||
import { getNewOperation, getField } from './get_drop_props';
|
||||
|
@ -39,7 +37,7 @@ interface DropHandlerProps<T = DataViewDragDropOperation> {
|
|||
forceRender?: boolean;
|
||||
}
|
||||
>;
|
||||
dimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
targetLayerDimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
dropType?: DropType;
|
||||
source: T;
|
||||
target: DataViewDragDropOperation;
|
||||
|
@ -89,16 +87,24 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<FormBasedPriva
|
|||
return onReorder(operationProps);
|
||||
}
|
||||
|
||||
if (['move_compatible', 'replace_compatible'].includes(dropType)) {
|
||||
return onMoveCompatible(operationProps, true);
|
||||
}
|
||||
if (['duplicate_compatible', 'replace_duplicate_compatible'].includes(dropType)) {
|
||||
if (
|
||||
[
|
||||
'duplicate_compatible',
|
||||
'replace_duplicate_compatible',
|
||||
'move_compatible',
|
||||
'replace_compatible',
|
||||
].includes(dropType)
|
||||
) {
|
||||
return onMoveCompatible(operationProps);
|
||||
}
|
||||
if (['move_incompatible', 'replace_incompatible'].includes(dropType)) {
|
||||
return onMoveIncompatible(operationProps, true);
|
||||
}
|
||||
if (['duplicate_incompatible', 'replace_duplicate_incompatible'].includes(dropType)) {
|
||||
if (
|
||||
[
|
||||
'duplicate_incompatible',
|
||||
'replace_duplicate_incompatible',
|
||||
'move_incompatible',
|
||||
'replace_incompatible',
|
||||
].includes(dropType)
|
||||
) {
|
||||
return onMoveIncompatible(operationProps);
|
||||
}
|
||||
if (dropType === 'swap_compatible') {
|
||||
|
@ -116,9 +122,9 @@ const isFieldDropType = (dropType: DropType) =>
|
|||
['field_add', 'field_replace', 'field_combine'].includes(dropType);
|
||||
|
||||
function onFieldDrop(props: DropHandlerProps<DraggedField>, shouldAddField?: boolean) {
|
||||
const { setState, state, source, target, dimensionGroups, indexPatterns } = props;
|
||||
const { setState, state, source, target, targetLayerDimensionGroups, indexPatterns } = props;
|
||||
|
||||
const prioritizedOperation = dimensionGroups.find(
|
||||
const prioritizedOperation = targetLayerDimensionGroups.find(
|
||||
(g) => g.groupId === target.groupId
|
||||
)?.prioritizedOperation;
|
||||
|
||||
|
@ -168,7 +174,7 @@ function onFieldDrop(props: DropHandlerProps<DraggedField>, shouldAddField?: boo
|
|||
indexPattern,
|
||||
op: newOperation,
|
||||
field,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
targetGroup: target.groupId,
|
||||
shouldCombineField: shouldAddField,
|
||||
initialParams,
|
||||
|
@ -177,45 +183,42 @@ function onFieldDrop(props: DropHandlerProps<DraggedField>, shouldAddField?: boo
|
|||
return true;
|
||||
}
|
||||
|
||||
function onMoveCompatible(
|
||||
{ setState, state, source, target, dimensionGroups }: DropHandlerProps<DataViewDragDropOperation>,
|
||||
shouldDeleteSource?: boolean
|
||||
) {
|
||||
const modifiedLayers = copyColumn({
|
||||
function onMoveCompatible({
|
||||
setState,
|
||||
state,
|
||||
source,
|
||||
target,
|
||||
targetLayerDimensionGroups,
|
||||
}: DropHandlerProps<DataViewDragDropOperation>) {
|
||||
let modifiedLayers = copyColumn({
|
||||
layers: state.layers,
|
||||
target,
|
||||
source,
|
||||
shouldDeleteSource,
|
||||
});
|
||||
|
||||
if (target.layerId === source.layerId) {
|
||||
const updatedColumnOrder = reorderByGroups(
|
||||
dimensionGroups,
|
||||
getColumnOrder(modifiedLayers[target.layerId]),
|
||||
target.groupId,
|
||||
target.columnId
|
||||
);
|
||||
const updatedColumnOrder = reorderByGroups(
|
||||
targetLayerDimensionGroups,
|
||||
getColumnOrder(modifiedLayers[target.layerId]),
|
||||
target.groupId,
|
||||
target.columnId
|
||||
);
|
||||
|
||||
const newLayer = {
|
||||
modifiedLayers = {
|
||||
...modifiedLayers,
|
||||
[target.layerId]: {
|
||||
...modifiedLayers[target.layerId],
|
||||
columnOrder: updatedColumnOrder,
|
||||
columns: modifiedLayers[target.layerId].columns,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Time to replace
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId: target.layerId,
|
||||
newLayer,
|
||||
})
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
setState(mergeLayers({ state, newLayers: modifiedLayers }));
|
||||
|
||||
return true;
|
||||
}
|
||||
setState(
|
||||
mergeLayers({
|
||||
state,
|
||||
newLayers: modifiedLayers,
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onReorder({
|
||||
|
@ -250,17 +253,14 @@ function onReorder({
|
|||
return true;
|
||||
}
|
||||
|
||||
function onMoveIncompatible(
|
||||
{
|
||||
setState,
|
||||
state,
|
||||
source,
|
||||
dimensionGroups,
|
||||
target,
|
||||
indexPatterns,
|
||||
}: DropHandlerProps<DataViewDragDropOperation>,
|
||||
shouldDeleteSource?: boolean
|
||||
) {
|
||||
function onMoveIncompatible({
|
||||
setState,
|
||||
state,
|
||||
source,
|
||||
targetLayerDimensionGroups,
|
||||
target,
|
||||
indexPatterns,
|
||||
}: DropHandlerProps<DataViewDragDropOperation>) {
|
||||
const targetLayer = state.layers[target.layerId];
|
||||
const targetColumn = targetLayer.columns[target.columnId] || null;
|
||||
const sourceLayer = state.layers[source.layerId];
|
||||
|
@ -272,22 +272,14 @@ function onMoveIncompatible(
|
|||
return false;
|
||||
}
|
||||
|
||||
const outputSourceLayer = shouldDeleteSource
|
||||
? deleteColumn({
|
||||
layer: sourceLayer,
|
||||
columnId: source.columnId,
|
||||
indexPattern,
|
||||
})
|
||||
: sourceLayer;
|
||||
|
||||
if (target.layerId === source.layerId) {
|
||||
const newLayer = insertOrReplaceColumn({
|
||||
layer: outputSourceLayer,
|
||||
layer: sourceLayer,
|
||||
columnId: target.columnId,
|
||||
indexPattern,
|
||||
op: newOperation,
|
||||
field: sourceField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
targetGroup: target.groupId,
|
||||
shouldResetLabel: true,
|
||||
});
|
||||
|
@ -306,7 +298,7 @@ function onMoveIncompatible(
|
|||
indexPattern,
|
||||
op: newOperation,
|
||||
field: sourceField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
targetGroup: target.groupId,
|
||||
shouldResetLabel: true,
|
||||
});
|
||||
|
@ -314,7 +306,7 @@ function onMoveIncompatible(
|
|||
mergeLayers({
|
||||
state,
|
||||
newLayers: {
|
||||
[source.layerId]: outputSourceLayer,
|
||||
[source.layerId]: sourceLayer,
|
||||
[target.layerId]: outputTargetLayer,
|
||||
},
|
||||
})
|
||||
|
@ -327,7 +319,7 @@ function onSwapIncompatible({
|
|||
setState,
|
||||
state,
|
||||
source,
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups,
|
||||
target,
|
||||
indexPatterns,
|
||||
}: DropHandlerProps<DragDropOperation>) {
|
||||
|
@ -354,7 +346,7 @@ function onSwapIncompatible({
|
|||
indexPattern,
|
||||
op: newOperationForSource,
|
||||
field: sourceField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
shouldResetLabel: true,
|
||||
});
|
||||
|
||||
|
@ -365,7 +357,7 @@ function onSwapIncompatible({
|
|||
indexPattern,
|
||||
op: newOperationForTarget,
|
||||
field: targetField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
targetGroup: source.groupId,
|
||||
shouldResetLabel: true,
|
||||
});
|
||||
|
@ -384,7 +376,7 @@ function onSwapIncompatible({
|
|||
indexPattern,
|
||||
op: newOperationForTarget,
|
||||
field: targetField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
targetGroup: source.groupId,
|
||||
shouldResetLabel: true,
|
||||
});
|
||||
|
@ -413,7 +405,7 @@ function onSwapCompatible({
|
|||
setState,
|
||||
state,
|
||||
source,
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups,
|
||||
target,
|
||||
}: DropHandlerProps<DataViewDragDropOperation>) {
|
||||
if (target.layerId === source.layerId) {
|
||||
|
@ -426,7 +418,7 @@ function onSwapCompatible({
|
|||
|
||||
let updatedColumnOrder = swapColumnOrder(layer.columnOrder, source.columnId, target.columnId);
|
||||
updatedColumnOrder = reorderByGroups(
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups,
|
||||
updatedColumnOrder,
|
||||
target.groupId,
|
||||
target.columnId
|
||||
|
@ -445,6 +437,7 @@ function onSwapCompatible({
|
|||
|
||||
return true;
|
||||
} else {
|
||||
// TODO why not reorderByGroups for both columns? Are they already in that order?
|
||||
const newTargetLayer = copyColumn({
|
||||
layers: state.layers,
|
||||
target,
|
||||
|
@ -478,7 +471,7 @@ function onCombine({
|
|||
setState,
|
||||
source,
|
||||
target,
|
||||
dimensionGroups,
|
||||
targetLayerDimensionGroups,
|
||||
indexPatterns,
|
||||
}: DropHandlerProps<DataViewDragDropOperation>) {
|
||||
const targetLayer = state.layers[target.layerId];
|
||||
|
@ -509,16 +502,14 @@ function onCombine({
|
|||
indexPattern,
|
||||
op: targetColumn.operationType,
|
||||
field: targetField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
visualizationGroups: targetLayerDimensionGroups,
|
||||
targetGroup: target.groupId,
|
||||
initialParams,
|
||||
shouldCombineField: true,
|
||||
});
|
||||
|
||||
const newLayers = deleteColumnInLayers({
|
||||
layers: { ...state.layers, [target.layerId]: outputTargetLayer },
|
||||
source,
|
||||
});
|
||||
setState(mergeLayers({ state, newLayers }));
|
||||
setState(
|
||||
mergeLayers({ state, newLayers: { ...state.layers, [target.layerId]: outputTargetLayer } })
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1632,7 +1632,7 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
currentIndexPatternId: '1',
|
||||
};
|
||||
expect(FormBasedDatasource.insertLayer(state, 'newLayer')).toEqual({
|
||||
expect(FormBasedDatasource.insertLayer(state, 'newLayer', ['link-to-id'])).toEqual({
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
|
@ -1640,6 +1640,7 @@ describe('IndexPattern Data Source', () => {
|
|||
indexPatternId: '1',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
linkToLayers: ['link-to-id'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1867,6 +1868,7 @@ describe('IndexPattern Data Source', () => {
|
|||
isBucketed: true,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
} as OperationDescriptor);
|
||||
});
|
||||
|
||||
|
@ -2722,6 +2724,43 @@ describe('IndexPattern Data Source', () => {
|
|||
expect(publicAPI.getMaxPossibleNumValues('non-existant')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('hasDefaultTimeField', () => {
|
||||
const indexPatternWithDefaultTimeField = {
|
||||
id: '1',
|
||||
title: 'my-fake-index-pattern',
|
||||
timeFieldName: 'timestamp',
|
||||
hasRestrictions: false,
|
||||
fields: fieldsOne,
|
||||
getFieldByName: getFieldByNameFactory(fieldsOne),
|
||||
spec: {},
|
||||
isPersisted: true,
|
||||
};
|
||||
|
||||
const indexPatternWithoutDefaultTimeField = {
|
||||
...indexPatternWithDefaultTimeField,
|
||||
timeFieldName: '',
|
||||
};
|
||||
|
||||
expect(
|
||||
FormBasedDatasource.getPublicAPI({
|
||||
state: baseState,
|
||||
layerId: 'first',
|
||||
indexPatterns: {
|
||||
1: indexPatternWithDefaultTimeField,
|
||||
},
|
||||
}).hasDefaultTimeField()
|
||||
).toBe(true);
|
||||
expect(
|
||||
FormBasedDatasource.getPublicAPI({
|
||||
state: baseState,
|
||||
layerId: 'first',
|
||||
indexPatterns: {
|
||||
1: indexPatternWithoutDefaultTimeField,
|
||||
},
|
||||
}).hasDefaultTimeField()
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getErrorMessages', () => {
|
||||
|
@ -3218,6 +3257,7 @@ describe('IndexPattern Data Source', () => {
|
|||
FormBasedDatasource.initializeDimension!(state, 'first', indexPatterns, {
|
||||
columnId: 'newStatic',
|
||||
groupId: 'a',
|
||||
visualizationGroups: [],
|
||||
})
|
||||
).toBe(state);
|
||||
});
|
||||
|
@ -3246,6 +3286,7 @@ describe('IndexPattern Data Source', () => {
|
|||
columnId: 'newStatic',
|
||||
groupId: 'a',
|
||||
staticValue: 0, // use a falsy value to check also this corner case
|
||||
visualizationGroups: [],
|
||||
})
|
||||
).toEqual({
|
||||
...state,
|
||||
|
@ -3272,6 +3313,272 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a new date histogram column if autoTimeField is passed', () => {
|
||||
const state = {
|
||||
currentIndexPatternId: '1',
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['metric'],
|
||||
columns: {
|
||||
metric: {
|
||||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: '___records___',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as FormBasedPrivateState;
|
||||
expect(
|
||||
FormBasedDatasource.initializeDimension!(state, 'first', indexPatterns, {
|
||||
columnId: 'newTime',
|
||||
groupId: 'a',
|
||||
autoTimeField: true,
|
||||
visualizationGroups: [],
|
||||
})
|
||||
).toEqual({
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
first: {
|
||||
...state.layers.first,
|
||||
incompleteColumns: {},
|
||||
columnOrder: ['newTime', 'metric'],
|
||||
columns: {
|
||||
...state.layers.first.columns,
|
||||
newTime: {
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
label: 'timestampLabel',
|
||||
operationType: 'date_histogram',
|
||||
params: { dropPartials: false, includeEmptyRows: true, interval: 'auto' },
|
||||
scale: 'interval',
|
||||
sourceField: 'timestamp',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#syncColumns', () => {
|
||||
it('copies linked columns', () => {
|
||||
const links: Parameters<Datasource['syncColumns']>[0]['links'] = [
|
||||
{
|
||||
from: {
|
||||
columnId: 'col1',
|
||||
layerId: 'first',
|
||||
groupId: 'foo',
|
||||
},
|
||||
to: {
|
||||
columnId: 'col1',
|
||||
layerId: 'second',
|
||||
groupId: 'foo',
|
||||
},
|
||||
},
|
||||
{
|
||||
from: {
|
||||
columnId: 'col2',
|
||||
layerId: 'first',
|
||||
groupId: 'foo',
|
||||
},
|
||||
to: {
|
||||
columnId: 'new-col',
|
||||
layerId: 'second',
|
||||
groupId: 'foo',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const newState = FormBasedDatasource.syncColumns({
|
||||
state: {
|
||||
currentIndexPatternId: 'foo',
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: 'foo',
|
||||
columnOrder: [],
|
||||
columns: {
|
||||
col1: {
|
||||
operationType: 'sum',
|
||||
label: '',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'field1',
|
||||
customLabel: false,
|
||||
timeScale: 'd',
|
||||
} as SumIndexPatternColumn,
|
||||
col2: {
|
||||
sourceField: 'field2',
|
||||
operationType: 'count',
|
||||
customLabel: false,
|
||||
timeScale: 'h',
|
||||
} as CountIndexPatternColumn,
|
||||
},
|
||||
},
|
||||
second: {
|
||||
indexPatternId: 'foo',
|
||||
columnOrder: [],
|
||||
columns: {
|
||||
col1: {
|
||||
sourceField: 'field1',
|
||||
operationType: 'count',
|
||||
customLabel: false,
|
||||
timeScale: 'd',
|
||||
} as CountIndexPatternColumn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
links,
|
||||
indexPatterns,
|
||||
getDimensionGroups: () => [],
|
||||
});
|
||||
|
||||
expect(newState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"currentIndexPatternId": "foo",
|
||||
"layers": Object {
|
||||
"first": Object {
|
||||
"columnOrder": Array [],
|
||||
"columns": Object {
|
||||
"col1": Object {
|
||||
"customLabel": false,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "",
|
||||
"operationType": "sum",
|
||||
"sourceField": "field1",
|
||||
"timeScale": "d",
|
||||
},
|
||||
"col2": Object {
|
||||
"customLabel": false,
|
||||
"operationType": "count",
|
||||
"sourceField": "field2",
|
||||
"timeScale": "h",
|
||||
},
|
||||
},
|
||||
"indexPatternId": "foo",
|
||||
},
|
||||
"second": Object {
|
||||
"columnOrder": Array [
|
||||
"col1",
|
||||
"new-col",
|
||||
],
|
||||
"columns": Object {
|
||||
"col1": Object {
|
||||
"customLabel": false,
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "",
|
||||
"operationType": "sum",
|
||||
"sourceField": "field1",
|
||||
"timeScale": "d",
|
||||
},
|
||||
"new-col": Object {
|
||||
"customLabel": false,
|
||||
"operationType": "count",
|
||||
"sourceField": "field2",
|
||||
"timeScale": "h",
|
||||
},
|
||||
},
|
||||
"indexPatternId": "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('updates terms order by references', () => {
|
||||
const links: Parameters<Datasource['syncColumns']>[0]['links'] = [
|
||||
{
|
||||
from: {
|
||||
columnId: 'col1FirstLayer',
|
||||
layerId: 'first',
|
||||
groupId: 'foo',
|
||||
},
|
||||
to: {
|
||||
columnId: 'col1SecondLayer',
|
||||
layerId: 'second',
|
||||
groupId: 'foo',
|
||||
},
|
||||
},
|
||||
{
|
||||
from: {
|
||||
columnId: 'col2',
|
||||
layerId: 'first',
|
||||
groupId: 'foo',
|
||||
},
|
||||
to: {
|
||||
columnId: 'new-col',
|
||||
layerId: 'second',
|
||||
groupId: 'foo',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const newState = FormBasedDatasource.syncColumns({
|
||||
state: {
|
||||
currentIndexPatternId: 'foo',
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: 'foo',
|
||||
columnOrder: [],
|
||||
columns: {
|
||||
col1FirstLayer: {
|
||||
operationType: 'sum',
|
||||
label: '',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'field1',
|
||||
customLabel: false,
|
||||
timeScale: 'd',
|
||||
} as SumIndexPatternColumn,
|
||||
col2: {
|
||||
operationType: 'terms',
|
||||
sourceField: 'field2',
|
||||
label: '',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
params: {
|
||||
orderBy: {
|
||||
columnId: 'col1FirstLayer',
|
||||
type: 'column',
|
||||
},
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
},
|
||||
},
|
||||
second: {
|
||||
indexPatternId: 'foo',
|
||||
columnOrder: [],
|
||||
columns: {
|
||||
col1SecondLayer: {
|
||||
sourceField: 'field1',
|
||||
operationType: 'count',
|
||||
customLabel: false,
|
||||
timeScale: 'd',
|
||||
} as CountIndexPatternColumn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
links,
|
||||
indexPatterns,
|
||||
getDimensionGroups: () => [],
|
||||
});
|
||||
|
||||
expect(
|
||||
(newState.layers.second.columns['new-col'] as TermsIndexPatternColumn).params.orderBy
|
||||
).toEqual({
|
||||
type: 'column',
|
||||
columnId: 'col1SecondLayer',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEqual', () => {
|
||||
|
|
|
@ -80,9 +80,14 @@ import {
|
|||
operationDefinitionMap,
|
||||
TermsIndexPatternColumn,
|
||||
} from './operations';
|
||||
import { getReferenceRoot } from './operations/layer_helpers';
|
||||
import { FormBasedPrivateState, FormBasedPersistedState } from './types';
|
||||
import { mergeLayer } from './state_helpers';
|
||||
import {
|
||||
copyColumn,
|
||||
getColumnOrder,
|
||||
getReferenceRoot,
|
||||
reorderByGroups,
|
||||
} from './operations/layer_helpers';
|
||||
import { FormBasedPrivateState, FormBasedPersistedState, DataViewDragDropOperation } from './types';
|
||||
import { mergeLayer, mergeLayers } from './state_helpers';
|
||||
import { Datasource, VisualizeEditorContext } from '../../types';
|
||||
import { deleteColumn, isReferenced } from './operations';
|
||||
import { GeoFieldWorkspacePanel } from '../../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel';
|
||||
|
@ -91,6 +96,7 @@ import { getStateTimeShiftWarningMessages } from './time_shift_utils';
|
|||
import { getPrecisionErrorWarningMessages } from './utils';
|
||||
import { DOCUMENT_FIELD_NAME } from '../../../common/constants';
|
||||
import { isColumnOfType } from './operations/definitions/helpers';
|
||||
import { FormBasedLayer } from '../..';
|
||||
export type { OperationType, GenericIndexPatternColumn } from './operations';
|
||||
export { deleteColumn } from './operations';
|
||||
|
||||
|
@ -99,7 +105,7 @@ export function columnToOperation(
|
|||
uniqueLabel?: string,
|
||||
dataView?: IndexPattern
|
||||
): OperationDescriptor {
|
||||
const { dataType, label, isBucketed, scale, operationType, timeShift } = column;
|
||||
const { dataType, label, isBucketed, scale, operationType, timeShift, reducedTimeRange } = column;
|
||||
const fieldTypes =
|
||||
'sourceField' in column ? dataView?.getFieldByName(column.sourceField)?.esTypes : undefined;
|
||||
return {
|
||||
|
@ -113,6 +119,7 @@ export function columnToOperation(
|
|||
? 'version'
|
||||
: undefined,
|
||||
hasTimeShift: Boolean(timeShift),
|
||||
hasReducedTimeRange: Boolean(reducedTimeRange),
|
||||
interval: isColumnOfType<DateHistogramIndexPatternColumn>('date_histogram', column)
|
||||
? column.params.interval
|
||||
: undefined,
|
||||
|
@ -180,12 +187,16 @@ export function getFormBasedDatasource({
|
|||
return extractReferences(state);
|
||||
},
|
||||
|
||||
insertLayer(state: FormBasedPrivateState, newLayerId: string) {
|
||||
insertLayer(
|
||||
state: FormBasedPrivateState,
|
||||
newLayerId: string,
|
||||
linkToLayers: string[] | undefined
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[newLayerId]: blankLayer(state.currentIndexPatternId),
|
||||
[newLayerId]: blankLayer(state.currentIndexPatternId, linkToLayers),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -219,7 +230,7 @@ export function getFormBasedDatasource({
|
|||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: blankLayer(state.currentIndexPatternId),
|
||||
[layerId]: blankLayer(state.currentIndexPatternId, state.layers[layerId].linkToLayers),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -241,26 +252,123 @@ export function getFormBasedDatasource({
|
|||
});
|
||||
},
|
||||
|
||||
initializeDimension(state, layerId, indexPatterns, { columnId, groupId, staticValue }) {
|
||||
initializeDimension(
|
||||
state,
|
||||
layerId,
|
||||
indexPatterns,
|
||||
{ columnId, groupId, staticValue, autoTimeField, visualizationGroups }
|
||||
) {
|
||||
const indexPattern = indexPatterns[state.layers[layerId]?.indexPatternId];
|
||||
if (staticValue == null) {
|
||||
return state;
|
||||
let ret = state;
|
||||
|
||||
if (staticValue != null) {
|
||||
ret = mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: insertNewColumn({
|
||||
layer: state.layers[layerId],
|
||||
op: 'static_value',
|
||||
columnId,
|
||||
field: undefined,
|
||||
indexPattern,
|
||||
visualizationGroups,
|
||||
initialParams: { params: { value: staticValue } },
|
||||
targetGroup: groupId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: insertNewColumn({
|
||||
layer: state.layers[layerId],
|
||||
op: 'static_value',
|
||||
columnId,
|
||||
field: undefined,
|
||||
indexPattern,
|
||||
visualizationGroups: [],
|
||||
initialParams: { params: { value: staticValue } },
|
||||
targetGroup: groupId,
|
||||
}),
|
||||
if (autoTimeField && indexPattern.timeFieldName) {
|
||||
ret = mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: insertNewColumn({
|
||||
layer: state.layers[layerId],
|
||||
op: 'date_histogram',
|
||||
columnId,
|
||||
field: indexPattern.fields.find((field) => field.name === indexPattern.timeFieldName),
|
||||
indexPattern,
|
||||
visualizationGroups,
|
||||
targetGroup: groupId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
syncColumns({ state, links, indexPatterns, getDimensionGroups }) {
|
||||
let modifiedLayers: Record<string, FormBasedLayer> = state.layers;
|
||||
|
||||
links.forEach((link) => {
|
||||
const source: DataViewDragDropOperation = {
|
||||
...link.from,
|
||||
dataView: indexPatterns[modifiedLayers[link.from.layerId]?.indexPatternId],
|
||||
filterOperations: () => true,
|
||||
};
|
||||
|
||||
const target: DataViewDragDropOperation = {
|
||||
...link.to,
|
||||
dataView: indexPatterns[modifiedLayers[link.to.layerId]?.indexPatternId],
|
||||
filterOperations: () => true,
|
||||
};
|
||||
|
||||
modifiedLayers = copyColumn({
|
||||
layers: modifiedLayers,
|
||||
target,
|
||||
source,
|
||||
});
|
||||
|
||||
const updatedColumnOrder = reorderByGroups(
|
||||
getDimensionGroups(target.layerId),
|
||||
getColumnOrder(modifiedLayers[target.layerId]),
|
||||
target.groupId,
|
||||
target.columnId
|
||||
);
|
||||
|
||||
modifiedLayers = {
|
||||
...modifiedLayers,
|
||||
[target.layerId]: {
|
||||
...modifiedLayers[target.layerId],
|
||||
columnOrder: updatedColumnOrder,
|
||||
columns: modifiedLayers[target.layerId].columns,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const newState = mergeLayers({
|
||||
state,
|
||||
newLayers: modifiedLayers,
|
||||
});
|
||||
|
||||
links
|
||||
.filter((link) =>
|
||||
isColumnOfType<TermsIndexPatternColumn>(
|
||||
'terms',
|
||||
newState.layers[link.from.layerId].columns[link.from.columnId]
|
||||
)
|
||||
)
|
||||
.forEach(({ from, to }) => {
|
||||
const fromColumn = newState.layers[from.layerId].columns[
|
||||
from.columnId
|
||||
] as TermsIndexPatternColumn;
|
||||
if (fromColumn.params.orderBy.type === 'column') {
|
||||
const fromOrderByColumnId = fromColumn.params.orderBy.columnId;
|
||||
const orderByColumnLink = links.find(
|
||||
({ from: { columnId } }) => columnId === fromOrderByColumnId
|
||||
);
|
||||
|
||||
if (orderByColumnLink) {
|
||||
// order the synced column by the dimension which is linked to the column that the original column was ordered by
|
||||
const toColumn = newState.layers[to.layerId].columns[
|
||||
to.columnId
|
||||
] as TermsIndexPatternColumn;
|
||||
toColumn.params.orderBy = { type: 'column', columnId: orderByColumnLink.to.columnId };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
getSelectedFields(state) {
|
||||
|
@ -497,9 +605,18 @@ export function getFormBasedDatasource({
|
|||
onRefreshIndexPattern,
|
||||
onIndexPatternChange(state, indexPatterns, indexPatternId, layerId) {
|
||||
if (layerId) {
|
||||
const layersToChange = [
|
||||
layerId,
|
||||
...Object.entries(state.layers)
|
||||
.map(([possiblyLinkedId, layer]) =>
|
||||
layer.linkToLayers?.includes(layerId) ? possiblyLinkedId : ''
|
||||
)
|
||||
.filter(Boolean),
|
||||
];
|
||||
|
||||
return changeLayerIndexPattern({
|
||||
indexPatternId,
|
||||
layerId,
|
||||
layerIds: layersToChange,
|
||||
state,
|
||||
replaceIfPossible: true,
|
||||
storage,
|
||||
|
@ -620,6 +737,7 @@ export function getFormBasedDatasource({
|
|||
}
|
||||
return null;
|
||||
},
|
||||
hasDefaultTimeField: () => Boolean(indexPatterns[layer.indexPatternId].timeFieldName),
|
||||
};
|
||||
},
|
||||
getDatasourceSuggestionsForField(state, draggedField, filterLayers, indexPatterns) {
|
||||
|
@ -799,9 +917,10 @@ export function getFormBasedDatasource({
|
|||
return formBasedDatasource;
|
||||
}
|
||||
|
||||
function blankLayer(indexPatternId: string) {
|
||||
function blankLayer(indexPatternId: string, linkToLayers?: string[]): FormBasedLayer {
|
||||
return {
|
||||
indexPatternId,
|
||||
linkToLayers,
|
||||
columns: {},
|
||||
columnOrder: [],
|
||||
};
|
||||
|
|
|
@ -1283,6 +1283,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1294,6 +1295,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1373,6 +1375,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1384,6 +1387,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2198,6 +2202,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2222,6 +2227,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2272,6 +2278,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
|
@ -2284,6 +2291,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -2349,6 +2357,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ordinal',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -2361,6 +2370,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
|
@ -2373,6 +2383,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -2459,6 +2470,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ordinal',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -2471,6 +2483,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
|
@ -2483,6 +2496,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -2592,6 +2606,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ordinal',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -2604,6 +2619,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
|
@ -2616,6 +2632,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -3123,6 +3140,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -3134,6 +3152,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -3201,6 +3220,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
|
@ -3213,6 +3233,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -3225,6 +3246,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -3291,6 +3313,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
|
@ -3303,6 +3326,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -3315,6 +3339,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -401,7 +401,7 @@ describe('loader', () => {
|
|||
});
|
||||
|
||||
describe('changeLayerIndexPattern', () => {
|
||||
it('loads the index pattern and then changes the specified layer', async () => {
|
||||
it('loads the index pattern and then changes the specified layers', async () => {
|
||||
const state: FormBasedPrivateState = {
|
||||
currentIndexPatternId: '1',
|
||||
layers: {
|
||||
|
@ -434,7 +434,7 @@ describe('loader', () => {
|
|||
const newState = changeLayerIndexPattern({
|
||||
state,
|
||||
indexPatternId: '2',
|
||||
layerId: 'l1',
|
||||
layerIds: ['l0', 'l1'],
|
||||
storage,
|
||||
indexPatterns: sampleIndexPatterns,
|
||||
replaceIfPossible: true,
|
||||
|
@ -444,9 +444,9 @@ describe('loader', () => {
|
|||
currentIndexPatternId: '2',
|
||||
layers: {
|
||||
l0: {
|
||||
columnOrder: ['col1'],
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
indexPatternId: '1',
|
||||
indexPatternId: '2',
|
||||
},
|
||||
l1: {
|
||||
columnOrder: ['col2'],
|
||||
|
|
|
@ -253,25 +253,34 @@ export function triggerActionOnIndexPatternChange({
|
|||
export function changeLayerIndexPattern({
|
||||
indexPatternId,
|
||||
indexPatterns,
|
||||
layerId,
|
||||
layerIds,
|
||||
state,
|
||||
replaceIfPossible,
|
||||
storage,
|
||||
}: {
|
||||
indexPatternId: string;
|
||||
layerId: string;
|
||||
layerIds: string[];
|
||||
state: FormBasedPrivateState;
|
||||
replaceIfPossible?: boolean;
|
||||
storage: IStorageWrapper;
|
||||
indexPatterns: Record<string, IndexPattern>;
|
||||
}) {
|
||||
setLastUsedIndexPatternId(storage, indexPatternId);
|
||||
|
||||
const newLayers = {
|
||||
...state.layers,
|
||||
};
|
||||
|
||||
layerIds.forEach((layerId) => {
|
||||
newLayers[layerId] = updateLayerIndexPattern(
|
||||
state.layers[layerId],
|
||||
indexPatterns[indexPatternId]
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: updateLayerIndexPattern(state.layers[layerId], indexPatterns[indexPatternId]),
|
||||
},
|
||||
layers: newLayers,
|
||||
currentIndexPatternId: replaceIfPossible ? indexPatternId : state.currentIndexPatternId,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -74,34 +74,8 @@ interface ColumnCopy {
|
|||
shouldDeleteSource?: boolean;
|
||||
}
|
||||
|
||||
export const deleteColumnInLayers = ({
|
||||
layers,
|
||||
source,
|
||||
}: {
|
||||
layers: Record<string, FormBasedLayer>;
|
||||
source: DataViewDragDropOperation;
|
||||
}) => ({
|
||||
...layers,
|
||||
[source.layerId]: deleteColumn({
|
||||
layer: layers[source.layerId],
|
||||
columnId: source.columnId,
|
||||
indexPattern: source.dataView,
|
||||
}),
|
||||
});
|
||||
|
||||
export function copyColumn({
|
||||
layers,
|
||||
source,
|
||||
target,
|
||||
shouldDeleteSource,
|
||||
}: ColumnCopy): Record<string, FormBasedLayer> {
|
||||
const outputLayers = createCopiedColumn(layers, target, source);
|
||||
return shouldDeleteSource
|
||||
? deleteColumnInLayers({
|
||||
layers: outputLayers,
|
||||
source,
|
||||
})
|
||||
: outputLayers;
|
||||
export function copyColumn({ layers, source, target }: ColumnCopy): Record<string, FormBasedLayer> {
|
||||
return createCopiedColumn(layers, target, source);
|
||||
}
|
||||
|
||||
function createCopiedColumn(
|
||||
|
|
|
@ -51,6 +51,7 @@ export interface FormBasedLayer {
|
|||
columns: Record<string, GenericIndexPatternColumn>;
|
||||
// Each layer is tied to the index pattern that created it
|
||||
indexPatternId: string;
|
||||
linkToLayers?: string[];
|
||||
// Partial columns represent the temporary invalid states
|
||||
incompleteColumns?: Record<string, IncompleteColumn>;
|
||||
}
|
||||
|
|
|
@ -728,6 +728,7 @@ describe('IndexPattern Data Source', () => {
|
|||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -209,6 +209,12 @@ export function getTextBasedDatasource({
|
|||
initialContext: context,
|
||||
};
|
||||
},
|
||||
|
||||
syncColumns({ state }) {
|
||||
// TODO implement this for real
|
||||
return state;
|
||||
},
|
||||
|
||||
onRefreshIndexPattern() {},
|
||||
|
||||
getUsedDataViews: (state) => {
|
||||
|
@ -581,6 +587,7 @@ export function getTextBasedDatasource({
|
|||
label: columnLabelMap[columnId] ?? column?.fieldName,
|
||||
isBucketed: Boolean(column?.meta?.type !== 'number'),
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -606,6 +613,7 @@ export function getTextBasedDatasource({
|
|||
},
|
||||
};
|
||||
},
|
||||
hasDefaultTimeField: () => false,
|
||||
};
|
||||
},
|
||||
getDatasourceSuggestionsForField(state, draggedField) {
|
||||
|
|
|
@ -44,10 +44,12 @@ export function AddLayerButton({
|
|||
if (!visualization.appendLayer || !visualizationState) {
|
||||
return null;
|
||||
}
|
||||
return visualization.getSupportedLayers?.(visualizationState, layersMeta);
|
||||
return visualization
|
||||
.getSupportedLayers?.(visualizationState, layersMeta)
|
||||
?.filter(({ canAddViaMenu: hideFromMenu }) => !hideFromMenu);
|
||||
}, [visualization, visualizationState, layersMeta]);
|
||||
|
||||
if (supportedLayers == null) {
|
||||
if (supportedLayers == null || !supportedLayers.length) {
|
||||
return null;
|
||||
}
|
||||
if (supportedLayers.length === 1) {
|
||||
|
|
|
@ -147,11 +147,23 @@ export interface OnVisDropProps<T> {
|
|||
group?: VisualizationDimensionGroupConfig;
|
||||
}
|
||||
|
||||
export function shouldRemoveSource(source: DragDropIdentifier, dropType: DropType) {
|
||||
return (
|
||||
isOperation(source) &&
|
||||
(dropType === 'move_compatible' ||
|
||||
dropType === 'move_incompatible' ||
|
||||
dropType === 'combine_incompatible' ||
|
||||
dropType === 'combine_compatible' ||
|
||||
dropType === 'replace_compatible' ||
|
||||
dropType === 'replace_incompatible')
|
||||
);
|
||||
}
|
||||
|
||||
export function onDropForVisualization<T, P = unknown>(
|
||||
props: OnVisDropProps<T>,
|
||||
activeVisualization: Visualization<T, P>
|
||||
) {
|
||||
const { prevState, target, frame, dropType, source, group } = props;
|
||||
const { prevState, target, frame, source, group } = props;
|
||||
const { layerId, columnId, groupId } = target;
|
||||
|
||||
const previousColumn =
|
||||
|
@ -166,21 +178,5 @@ export function onDropForVisualization<T, P = unknown>(
|
|||
frame,
|
||||
});
|
||||
|
||||
if (
|
||||
isOperation(source) &&
|
||||
(dropType === 'move_compatible' ||
|
||||
dropType === 'move_incompatible' ||
|
||||
dropType === 'combine_incompatible' ||
|
||||
dropType === 'combine_compatible' ||
|
||||
dropType === 'replace_compatible' ||
|
||||
dropType === 'replace_incompatible')
|
||||
) {
|
||||
return activeVisualization.removeDimension({
|
||||
columnId: source?.columnId,
|
||||
layerId: source?.layerId,
|
||||
prevState: newVisState,
|
||||
frame,
|
||||
});
|
||||
}
|
||||
return newVisState;
|
||||
}
|
||||
|
|
|
@ -374,6 +374,16 @@ describe('ConfigPanel', () => {
|
|||
columnId: 'myColumn',
|
||||
groupId: 'testGroup',
|
||||
staticValue: 100,
|
||||
visualizationGroups: [
|
||||
expect.objectContaining({
|
||||
accessors: [],
|
||||
dataTestSubj: 'mockVisA',
|
||||
groupId: 'a',
|
||||
groupLabel: 'a',
|
||||
layerId: 'layer1',
|
||||
supportsMoreColumns: true,
|
||||
}),
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -410,6 +420,16 @@ describe('ConfigPanel', () => {
|
|||
groupId: 'a',
|
||||
columnId: 'newId',
|
||||
staticValue: 100,
|
||||
visualizationGroups: [
|
||||
expect.objectContaining({
|
||||
accessors: [],
|
||||
dataTestSubj: 'mockVisA',
|
||||
groupId: 'a',
|
||||
groupLabel: 'a',
|
||||
layerId: 'layer1',
|
||||
supportsMoreColumns: true,
|
||||
}),
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -13,7 +13,8 @@ import {
|
|||
UPDATE_FILTER_REFERENCES_ACTION,
|
||||
UPDATE_FILTER_REFERENCES_TRIGGER,
|
||||
} from '@kbn/unified-search-plugin/public';
|
||||
import { changeIndexPattern } from '../../../state_management/lens_slice';
|
||||
import { LayerType } from '../../../../common';
|
||||
import { changeIndexPattern, removeDimension } from '../../../state_management/lens_slice';
|
||||
import { Visualization } from '../../../types';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { generateId } from '../../../id_generator';
|
||||
|
@ -24,7 +25,7 @@ import {
|
|||
useLensDispatch,
|
||||
removeOrClearLayer,
|
||||
cloneLayer,
|
||||
addLayer,
|
||||
addLayer as addLayerAction,
|
||||
updateState,
|
||||
updateDatasourceState,
|
||||
updateVisualizationState,
|
||||
|
@ -78,18 +79,20 @@ export function LayerPanels(
|
|||
[activeVisualization, dispatchLens]
|
||||
);
|
||||
const updateDatasource = useMemo(
|
||||
() => (datasourceId: string | undefined, newState: unknown) => {
|
||||
if (datasourceId) {
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater: (prevState: unknown) =>
|
||||
typeof newState === 'function' ? newState(prevState) : newState,
|
||||
datasourceId,
|
||||
clearStagedPreview: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
() =>
|
||||
(datasourceId: string | undefined, newState: unknown, dontSyncLinkedDimensions?: boolean) => {
|
||||
if (datasourceId) {
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater: (prevState: unknown) =>
|
||||
typeof newState === 'function' ? newState(prevState) : newState,
|
||||
datasourceId,
|
||||
clearStagedPreview: false,
|
||||
dontSyncLinkedDimensions,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatchLens]
|
||||
);
|
||||
const updateDatasourceAsync = useMemo(
|
||||
|
@ -157,6 +160,48 @@ export function LayerPanels(
|
|||
[dispatchLens]
|
||||
);
|
||||
|
||||
const onRemoveLayer = useCallback(
|
||||
(layerToRemoveId: string) => {
|
||||
const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerToRemoveId];
|
||||
const datasourceId = datasourcePublicAPI?.datasourceId;
|
||||
|
||||
if (datasourceId) {
|
||||
const layerDatasource = datasourceMap[datasourceId];
|
||||
const layerDatasourceState = datasourceStates?.[datasourceId]?.state;
|
||||
const trigger = props.uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER);
|
||||
const action = props.uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION);
|
||||
|
||||
action?.execute({
|
||||
trigger,
|
||||
fromDataView: layerDatasource.getUsedDataView(layerDatasourceState, layerToRemoveId),
|
||||
usedDataViews: layerDatasource
|
||||
.getLayers(layerDatasourceState)
|
||||
.map((layer) => layerDatasource.getUsedDataView(layerDatasourceState, layer)),
|
||||
defaultDataView: layerDatasource.getUsedDataView(layerDatasourceState),
|
||||
} as ActionExecutionContext);
|
||||
}
|
||||
|
||||
dispatchLens(
|
||||
removeOrClearLayer({
|
||||
visualizationId: activeVisualization.id,
|
||||
layerId: layerToRemoveId,
|
||||
layerIds,
|
||||
})
|
||||
);
|
||||
removeLayerRef(layerToRemoveId);
|
||||
},
|
||||
[
|
||||
activeVisualization.id,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
dispatchLens,
|
||||
layerIds,
|
||||
props.framePublicAPI.datasourceLayers,
|
||||
props.uiActions,
|
||||
removeLayerRef,
|
||||
]
|
||||
);
|
||||
|
||||
const onChangeIndexPattern = useCallback(
|
||||
async ({
|
||||
indexPatternId,
|
||||
|
@ -183,100 +228,98 @@ export function LayerPanels(
|
|||
})
|
||||
);
|
||||
},
|
||||
[dispatchLens, props.framePublicAPI.dataViews, props.indexPatternService]
|
||||
[dispatchLens, props.framePublicAPI.dataViews.indexPatterns, props.indexPatternService]
|
||||
);
|
||||
|
||||
const addLayer = (layerType: LayerType) => {
|
||||
const layerId = generateId();
|
||||
dispatchLens(addLayerAction({ layerId, layerType }));
|
||||
setNextFocusedLayerId(layerId);
|
||||
};
|
||||
|
||||
const hideAddLayerButton = query && isOfAggregateQueryType(query);
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{layerIds.map((layerId, layerIndex) => (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
activeVisualization={activeVisualization}
|
||||
registerNewLayerRef={registerNewLayerRef}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerIndex={layerIndex}
|
||||
visualizationState={visualization.state}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateDatasourceAsync={updateDatasourceAsync}
|
||||
onChangeIndexPattern={onChangeIndexPattern}
|
||||
updateAll={updateAll}
|
||||
isOnlyLayer={
|
||||
getRemoveOperation(
|
||||
activeVisualization,
|
||||
visualization.state,
|
||||
layerId,
|
||||
layerIds.length
|
||||
) === 'clear'
|
||||
}
|
||||
onEmptyDimensionAdd={(columnId, { groupId }) => {
|
||||
// avoid state update if the datasource does not support initializeDimension
|
||||
if (
|
||||
activeDatasourceId != null &&
|
||||
datasourceMap[activeDatasourceId]?.initializeDimension
|
||||
) {
|
||||
dispatchLens(
|
||||
setLayerDefaultDimension({
|
||||
{layerIds.map((layerId, layerIndex) => {
|
||||
const { hidden, groups } = activeVisualization.getConfiguration({
|
||||
layerId,
|
||||
frame: props.framePublicAPI,
|
||||
state: visualization.state,
|
||||
});
|
||||
|
||||
return (
|
||||
!hidden && (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
dimensionGroups={groups}
|
||||
activeVisualization={activeVisualization}
|
||||
registerNewLayerRef={registerNewLayerRef}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerIndex={layerIndex}
|
||||
visualizationState={visualization.state}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateDatasourceAsync={updateDatasourceAsync}
|
||||
onChangeIndexPattern={(args) => {
|
||||
onChangeIndexPattern(args);
|
||||
const layersToRemove =
|
||||
activeVisualization.getLayersToRemoveOnIndexPatternChange?.(
|
||||
visualization.state
|
||||
) ?? [];
|
||||
layersToRemove.forEach((id) => onRemoveLayer(id));
|
||||
}}
|
||||
updateAll={updateAll}
|
||||
addLayer={addLayer}
|
||||
isOnlyLayer={
|
||||
getRemoveOperation(
|
||||
activeVisualization,
|
||||
visualization.state,
|
||||
layerId,
|
||||
columnId,
|
||||
groupId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
onCloneLayer={() => {
|
||||
dispatchLens(
|
||||
cloneLayer({
|
||||
layerId,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onRemoveLayer={() => {
|
||||
const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerId];
|
||||
const datasourceId = datasourcePublicAPI?.datasourceId;
|
||||
|
||||
if (datasourceId) {
|
||||
const layerDatasource = datasourceMap[datasourceId];
|
||||
const layerDatasourceState = datasourceStates?.[datasourceId]?.state;
|
||||
const trigger = props.uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER);
|
||||
const action = props.uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION);
|
||||
|
||||
action?.execute({
|
||||
trigger,
|
||||
fromDataView: layerDatasource.getUsedDataView(layerDatasourceState, layerId),
|
||||
usedDataViews: layerDatasource
|
||||
.getLayers(layerDatasourceState)
|
||||
.map((layer) => layerDatasource.getUsedDataView(layerDatasourceState, layer)),
|
||||
defaultDataView: layerDatasource.getUsedDataView(layerDatasourceState),
|
||||
} as ActionExecutionContext);
|
||||
}
|
||||
|
||||
dispatchLens(
|
||||
removeOrClearLayer({
|
||||
visualizationId: activeVisualization.id,
|
||||
layerId,
|
||||
layerIds,
|
||||
})
|
||||
);
|
||||
removeLayerRef(layerId);
|
||||
}}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
indexPatternService={indexPatternService}
|
||||
/>
|
||||
))}
|
||||
layerIds.length
|
||||
) === 'clear'
|
||||
}
|
||||
onEmptyDimensionAdd={(columnId, { groupId }) => {
|
||||
// avoid state update if the datasource does not support initializeDimension
|
||||
if (
|
||||
activeDatasourceId != null &&
|
||||
datasourceMap[activeDatasourceId]?.initializeDimension
|
||||
) {
|
||||
dispatchLens(
|
||||
setLayerDefaultDimension({
|
||||
layerId,
|
||||
columnId,
|
||||
groupId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
onCloneLayer={() => {
|
||||
dispatchLens(
|
||||
cloneLayer({
|
||||
layerId,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onRemoveLayer={onRemoveLayer}
|
||||
onRemoveDimension={(dimensionProps) => {
|
||||
const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerId];
|
||||
const datasourceId = datasourcePublicAPI?.datasourceId;
|
||||
dispatchLens(removeDimension({ ...dimensionProps, datasourceId }));
|
||||
}}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
indexPatternService={indexPatternService}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
{!hideAddLayerButton && (
|
||||
<AddLayerButton
|
||||
visualization={activeVisualization}
|
||||
visualizationState={visualization.state}
|
||||
layersMeta={props.framePublicAPI}
|
||||
onAddLayerClick={(layerType) => {
|
||||
const layerId = generateId();
|
||||
dispatchLens(addLayer({ layerId, layerType }));
|
||||
setNextFocusedLayerId(layerId);
|
||||
}}
|
||||
onAddLayerClick={(layerType) => addLayer(layerType)}
|
||||
/>
|
||||
)}
|
||||
</EuiForm>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { FramePublicAPI, Visualization } from '../../../types';
|
||||
import { FramePublicAPI, Visualization, VisualizationConfigProps } from '../../../types';
|
||||
import { LayerPanel } from './layer_panel';
|
||||
import { ChildDragDropProvider, DragDrop } from '../../../drag_drop';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
@ -89,6 +89,7 @@ describe('LayerPanel', () => {
|
|||
return {
|
||||
layerId: 'first',
|
||||
activeVisualization: mockVisualization,
|
||||
dimensionGroups: mockVisualization.getConfiguration({} as VisualizationConfigProps).groups,
|
||||
datasourceMap: {
|
||||
testDatasource: mockDatasource,
|
||||
},
|
||||
|
@ -99,8 +100,10 @@ describe('LayerPanel', () => {
|
|||
updateAll: jest.fn(),
|
||||
framePublicAPI: frame,
|
||||
isOnlyLayer: true,
|
||||
addLayer: jest.fn(),
|
||||
onRemoveLayer: jest.fn(),
|
||||
onCloneLayer: jest.fn(),
|
||||
onRemoveDimension: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
core: coreMock.createStart(),
|
||||
layerIndex: 0,
|
||||
|
@ -473,9 +476,6 @@ describe('LayerPanel', () => {
|
|||
});
|
||||
|
||||
it('should remove the dimension when the datasource marks it as removed', async () => {
|
||||
const updateAll = jest.fn();
|
||||
const updateDatasource = jest.fn();
|
||||
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
|
@ -489,37 +489,31 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
updateDatasource={updateDatasource}
|
||||
updateAll={updateAll}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
layers: [
|
||||
{
|
||||
indexPatternId: '1',
|
||||
columns: {
|
||||
y: {
|
||||
operationType: 'moving_average',
|
||||
references: ['ref'],
|
||||
},
|
||||
const props = getDefaultProps();
|
||||
const { instance } = await mountWithProvider(<LayerPanel {...props} />, {
|
||||
preloadedState: {
|
||||
datasourceStates: {
|
||||
testDatasource: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
layers: [
|
||||
{
|
||||
indexPatternId: '1',
|
||||
columns: {
|
||||
y: {
|
||||
operationType: 'moving_average',
|
||||
references: ['ref'],
|
||||
},
|
||||
columnOrder: ['y'],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
columnOrder: ['y'],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').last().simulate('click');
|
||||
|
@ -548,12 +542,10 @@ describe('LayerPanel', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
expect(updateAll).toHaveBeenCalled();
|
||||
expect(mockVisualization.removeDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: 'y',
|
||||
})
|
||||
);
|
||||
expect(props.onRemoveDimension).toHaveBeenCalledWith({
|
||||
layerId: props.layerId,
|
||||
columnId: 'y',
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep the DimensionContainer open when configuring a new dimension', async () => {
|
||||
|
@ -985,7 +977,6 @@ describe('LayerPanel', () => {
|
|||
it('should call onDrop and update visualization when replacing between compatible groups', async () => {
|
||||
const mockVis = {
|
||||
...mockVisualization,
|
||||
removeDimension: jest.fn(),
|
||||
setDimension: jest.fn(() => 'modifiedState'),
|
||||
};
|
||||
mockVis.getConfiguration.mockReturnValue({
|
||||
|
@ -1019,11 +1010,13 @@ describe('LayerPanel', () => {
|
|||
|
||||
mockDatasource.onDrop.mockReturnValue(true);
|
||||
const updateVisualization = jest.fn();
|
||||
const mockOnRemoveDimension = jest.fn();
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
onRemoveDimension={mockOnRemoveDimension}
|
||||
updateVisualization={updateVisualization}
|
||||
activeVisualization={mockVis}
|
||||
/>
|
||||
|
@ -1047,19 +1040,15 @@ describe('LayerPanel', () => {
|
|||
prevState: 'state',
|
||||
})
|
||||
);
|
||||
expect(mockVis.removeDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: 'a',
|
||||
layerId: 'first',
|
||||
prevState: 'modifiedState',
|
||||
})
|
||||
);
|
||||
expect(mockOnRemoveDimension).toHaveBeenCalledWith({
|
||||
columnId: 'a',
|
||||
layerId: 'first',
|
||||
});
|
||||
expect(updateVisualization).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should call onDrop and update visualization when replacing between compatible groups2', async () => {
|
||||
const mockVis = {
|
||||
...mockVisualization,
|
||||
removeDimension: jest.fn(),
|
||||
setDimension: jest.fn(() => 'modifiedState'),
|
||||
onDrop: jest.fn(() => 'modifiedState'),
|
||||
};
|
||||
|
@ -1096,11 +1085,13 @@ describe('LayerPanel', () => {
|
|||
|
||||
mockDatasource.onDrop.mockReturnValue(true);
|
||||
const updateVisualization = jest.fn();
|
||||
const mockOnRemoveDimension = jest.fn();
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
onRemoveDimension={mockOnRemoveDimension}
|
||||
updateVisualization={updateVisualization}
|
||||
activeVisualization={mockVis}
|
||||
/>
|
||||
|
@ -1132,9 +1123,130 @@ describe('LayerPanel', () => {
|
|||
mockVis
|
||||
);
|
||||
expect(mockVis.setDimension).not.toHaveBeenCalled();
|
||||
expect(mockVis.removeDimension).not.toHaveBeenCalled();
|
||||
expect(mockOnRemoveDimension).toHaveBeenCalledWith({
|
||||
columnId: 'a',
|
||||
layerId: 'first',
|
||||
});
|
||||
expect(updateVisualization).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not change visualization state if datasource drop failed', async () => {
|
||||
const mockVis = {
|
||||
...mockVisualization,
|
||||
setDimension: jest.fn(() => 'modifiedState'),
|
||||
};
|
||||
|
||||
mockVis.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [{ columnId: 'a' }, { columnId: 'b' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
{
|
||||
groupLabel: 'B',
|
||||
groupId: 'b',
|
||||
accessors: [{ columnId: 'c' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const draggingOperation = {
|
||||
layerId: 'first',
|
||||
columnId: 'a',
|
||||
groupId: 'a',
|
||||
id: 'a',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
mockDatasource.onDrop.mockReturnValue(false);
|
||||
const updateVisualization = jest.fn();
|
||||
const mockOnRemoveDimension = jest.fn();
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
onRemoveDimension={mockOnRemoveDimension}
|
||||
updateVisualization={updateVisualization}
|
||||
activeVisualization={mockVis}
|
||||
/>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible');
|
||||
});
|
||||
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dropType: 'replace_compatible',
|
||||
source: draggingOperation,
|
||||
})
|
||||
);
|
||||
expect(updateVisualization).not.toHaveBeenCalled();
|
||||
expect(mockOnRemoveDimension).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not remove source if drop type doesn't require it", async () => {
|
||||
const mockVis = {
|
||||
...mockVisualization,
|
||||
setDimension: jest.fn(() => 'modifiedState'),
|
||||
};
|
||||
|
||||
mockVis.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [{ columnId: 'a' }, { columnId: 'b' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
{
|
||||
groupLabel: 'B',
|
||||
groupId: 'b',
|
||||
accessors: [{ columnId: 'c' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const draggingOperation = {
|
||||
layerId: 'first',
|
||||
columnId: 'a',
|
||||
groupId: 'a',
|
||||
id: 'a',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
|
||||
mockDatasource.onDrop.mockReturnValue(true);
|
||||
|
||||
const mockOnRemoveDimension = jest.fn();
|
||||
|
||||
const { instance } = await mountWithProvider(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel
|
||||
{...getDefaultProps()}
|
||||
onRemoveDimension={mockOnRemoveDimension}
|
||||
activeVisualization={mockVis}
|
||||
/>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
act(() => {
|
||||
instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'duplicate_compatible');
|
||||
});
|
||||
|
||||
expect(mockOnRemoveDimension).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('add a new dimension', () => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LayerType } from '../../../../common';
|
||||
import { LayerActions } from './layer_actions';
|
||||
import { IndexPatternServiceAPI } from '../../../data_views_service/service';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
DragDropOperation,
|
||||
DropType,
|
||||
isOperation,
|
||||
VisualizationDimensionGroupConfig,
|
||||
} from '../../../types';
|
||||
import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
|
@ -42,7 +44,7 @@ import {
|
|||
selectResolvedDateRange,
|
||||
selectDatasourceStates,
|
||||
} from '../../../state_management';
|
||||
import { onDropForVisualization } from './buttons/drop_targets_utils';
|
||||
import { onDropForVisualization, shouldRemoveSource } from './buttons/drop_targets_utils';
|
||||
import { getSharedActions } from './layer_actions/layer_actions';
|
||||
|
||||
const initialActiveDimensionState = {
|
||||
|
@ -52,19 +54,26 @@ const initialActiveDimensionState = {
|
|||
export function LayerPanel(
|
||||
props: Exclude<LayerPanelProps, 'state' | 'setState'> & {
|
||||
activeVisualization: Visualization;
|
||||
dimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
layerId: string;
|
||||
layerIndex: number;
|
||||
isOnlyLayer: boolean;
|
||||
addLayer: (layerType: LayerType) => void;
|
||||
updateVisualization: StateSetter<unknown>;
|
||||
updateDatasource: (datasourceId: string | undefined, newState: unknown) => void;
|
||||
updateDatasource: (
|
||||
datasourceId: string | undefined,
|
||||
newState: unknown,
|
||||
dontSyncLinkedDimensions?: boolean
|
||||
) => void;
|
||||
updateDatasourceAsync: (datasourceId: string | undefined, newState: unknown) => void;
|
||||
updateAll: (
|
||||
datasourceId: string | undefined,
|
||||
newDatasourcestate: unknown,
|
||||
newVisualizationState: unknown
|
||||
) => void;
|
||||
onRemoveLayer: () => void;
|
||||
onRemoveLayer: (layerId: string) => void;
|
||||
onCloneLayer: () => void;
|
||||
onRemoveDimension: (props: { columnId: string; layerId: string }) => void;
|
||||
registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void;
|
||||
toggleFullscreen: () => void;
|
||||
onEmptyDimensionAdd: (columnId: string, group: { groupId: string }) => void;
|
||||
|
@ -86,6 +95,7 @@ export function LayerPanel(
|
|||
framePublicAPI,
|
||||
layerId,
|
||||
isOnlyLayer,
|
||||
dimensionGroups,
|
||||
onRemoveLayer,
|
||||
onCloneLayer,
|
||||
registerNewLayerRef,
|
||||
|
@ -138,26 +148,15 @@ export function LayerPanel(
|
|||
dateRange,
|
||||
};
|
||||
|
||||
const { groups } = useMemo(
|
||||
() => activeVisualization.getConfiguration(layerVisualizationConfigProps),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
layerVisualizationConfigProps.frame,
|
||||
layerVisualizationConfigProps.state,
|
||||
layerId,
|
||||
activeVisualization,
|
||||
]
|
||||
);
|
||||
|
||||
const columnLabelMap =
|
||||
!layerDatasource && activeVisualization.getUniqueLabels
|
||||
? activeVisualization.getUniqueLabels(props.visualizationState)
|
||||
: layerDatasource?.uniqueLabels?.(layerDatasourceConfigProps?.state);
|
||||
|
||||
const isEmptyLayer = !groups.some((d) => d.accessors.length > 0);
|
||||
const isEmptyLayer = !dimensionGroups.some((d) => d.accessors.length > 0);
|
||||
const { activeId, activeGroup } = activeDimension;
|
||||
|
||||
const allAccessors = groups.flatMap((group) =>
|
||||
const allAccessors = dimensionGroups.flatMap((group) =>
|
||||
group.accessors.map((accessor) => accessor.columnId)
|
||||
);
|
||||
|
||||
|
@ -188,16 +187,17 @@ export function LayerPanel(
|
|||
layerDatasource?.onDrop({
|
||||
state: layerDatasourceState,
|
||||
setState: (newState: unknown) => {
|
||||
updateDatasource(datasourceId, newState);
|
||||
// we don't sync linked dimension here because that would trigger an onDrop routine within an onDrop routine
|
||||
updateDatasource(datasourceId, newState, true);
|
||||
},
|
||||
source,
|
||||
target: {
|
||||
...(target as unknown as DragDropOperation),
|
||||
filterOperations:
|
||||
groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations ||
|
||||
Boolean,
|
||||
dimensionGroups.find(({ groupId: gId }) => gId === target.groupId)
|
||||
?.filterOperations || Boolean,
|
||||
},
|
||||
dimensionGroups: groups,
|
||||
targetLayerDimensionGroups: dimensionGroups,
|
||||
dropType,
|
||||
indexPatterns: framePublicAPI.dataViews.indexPatterns,
|
||||
})
|
||||
|
@ -214,24 +214,31 @@ export function LayerPanel(
|
|||
target,
|
||||
source,
|
||||
dropType,
|
||||
group: groups.find(({ groupId: gId }) => gId === target.groupId),
|
||||
group: dimensionGroups.find(({ groupId: gId }) => gId === target.groupId),
|
||||
},
|
||||
activeVisualization
|
||||
)
|
||||
);
|
||||
|
||||
if (isOperation(source) && shouldRemoveSource(source, dropType)) {
|
||||
props.onRemoveDimension({
|
||||
columnId: source.columnId,
|
||||
layerId: source.layerId,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [
|
||||
layerDatasource,
|
||||
setNextFocusedButtonId,
|
||||
layerDatasourceState,
|
||||
groups,
|
||||
dimensionGroups,
|
||||
framePublicAPI,
|
||||
updateDatasource,
|
||||
datasourceId,
|
||||
activeVisualization,
|
||||
updateVisualization,
|
||||
props.visualizationState,
|
||||
framePublicAPI,
|
||||
props,
|
||||
]);
|
||||
|
||||
const isDimensionPanelOpen = Boolean(activeId);
|
||||
|
@ -260,16 +267,8 @@ export function LayerPanel(
|
|||
// The datasource can indicate that the previously-valid column is no longer
|
||||
// complete, which clears the visualization. This keeps the flyout open and reuses
|
||||
// the previous columnId
|
||||
updateAll(
|
||||
datasourceId,
|
||||
newState,
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: activeId,
|
||||
prevState: visualizationState,
|
||||
frame: framePublicAPI,
|
||||
})
|
||||
);
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
props.onRemoveDimension({ layerId, columnId: activeId });
|
||||
}
|
||||
} else if (isDimensionComplete) {
|
||||
updateAll(
|
||||
|
@ -327,7 +326,7 @@ export function LayerPanel(
|
|||
isOnlyLayer,
|
||||
isTextBasedLanguage,
|
||||
onCloneLayer,
|
||||
onRemoveLayer,
|
||||
onRemoveLayer: () => onRemoveLayer(layerId),
|
||||
}),
|
||||
].filter((i) => i.isCompatible),
|
||||
[
|
||||
|
@ -402,7 +401,7 @@ export function LayerPanel(
|
|||
)}
|
||||
</header>
|
||||
|
||||
{groups.map((group, groupIndex) => {
|
||||
{dimensionGroups.map((group, groupIndex) => {
|
||||
let errorText: string = '';
|
||||
|
||||
if (!isEmptyLayer) {
|
||||
|
@ -529,32 +528,7 @@ export function LayerPanel(
|
|||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
if (datasourceId && layerDatasource) {
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
layerDatasource.removeColumn({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: layerDatasourceState,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
}),
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: props.visualizationState,
|
||||
frame: framePublicAPI,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
props.updateVisualization(
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: props.visualizationState,
|
||||
frame: framePublicAPI,
|
||||
})
|
||||
);
|
||||
}
|
||||
props.onRemoveDimension({ columnId: id, layerId });
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
invalid={
|
||||
|
@ -694,7 +668,7 @@ export function LayerPanel(
|
|||
groupId: activeGroup.groupId,
|
||||
hideGrouping: activeGroup.hideGrouping,
|
||||
filterOperations: activeGroup.filterOperations,
|
||||
dimensionGroups: groups,
|
||||
dimensionGroups,
|
||||
toggleFullscreen,
|
||||
isFullscreen,
|
||||
setState: updateDataLayerState,
|
||||
|
@ -723,7 +697,10 @@ export function LayerPanel(
|
|||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
datasource,
|
||||
setState: props.updateVisualization,
|
||||
addLayer: props.addLayer,
|
||||
removeLayer: props.onRemoveLayer,
|
||||
panelRef,
|
||||
}}
|
||||
/>
|
||||
|
@ -735,7 +712,10 @@ export function LayerPanel(
|
|||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
datasource,
|
||||
setState: props.updateVisualization,
|
||||
addLayer: props.addLayer,
|
||||
removeLayer: props.onRemoveLayer,
|
||||
panelRef,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('Data Panel Wrapper', () => {
|
|||
activeDatasource: {
|
||||
renderDataPanel,
|
||||
getUsedDataViews: jest.fn(),
|
||||
getLayers: jest.fn(() => []),
|
||||
} as unknown as Datasource,
|
||||
};
|
||||
|
||||
|
|
|
@ -306,7 +306,7 @@ describe('editor_frame', () => {
|
|||
setDatasourceState(updatedState);
|
||||
});
|
||||
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(3);
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
state: updatedState,
|
||||
|
@ -377,6 +377,7 @@ describe('editor_frame', () => {
|
|||
getFilters: jest.fn(),
|
||||
getMaxPossibleNumValues: jest.fn(),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
};
|
||||
mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI);
|
||||
|
||||
|
@ -386,7 +387,7 @@ describe('editor_frame', () => {
|
|||
setDatasourceState({});
|
||||
});
|
||||
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(3);
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
frame: expect.objectContaining({
|
||||
|
@ -711,7 +712,7 @@ describe('editor_frame', () => {
|
|||
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
|
||||
});
|
||||
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1);
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
|
||||
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
state: suggestionVisState,
|
||||
|
|
|
@ -571,6 +571,7 @@ describe('suggestion helpers', () => {
|
|||
getFilters: jest.fn(),
|
||||
getMaxPossibleNumValues: jest.fn(),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
},
|
||||
},
|
||||
{ activeId: 'testVis', state: {} },
|
||||
|
@ -609,6 +610,7 @@ describe('suggestion helpers', () => {
|
|||
getFilters: jest.fn(),
|
||||
getMaxPossibleNumValues: jest.fn(),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
},
|
||||
};
|
||||
defaultParams[3] = {
|
||||
|
@ -672,6 +674,7 @@ describe('suggestion helpers', () => {
|
|||
getFilters: jest.fn(),
|
||||
getMaxPossibleNumValues: jest.fn(),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
},
|
||||
};
|
||||
mockVisualization1.getSuggestions.mockReturnValue([]);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { LensPlugin } from './plugin';
|
||||
|
||||
export type {
|
||||
|
@ -38,6 +37,7 @@ export type {
|
|||
PieVisualizationState,
|
||||
PieLayerState,
|
||||
SharedPieLayerState,
|
||||
LayerType,
|
||||
} from '../common/types';
|
||||
|
||||
export type { DatatableVisualizationState } from './visualizations/datatable/visualization';
|
||||
|
@ -80,7 +80,6 @@ export type {
|
|||
export type {
|
||||
XYArgs,
|
||||
XYRender,
|
||||
LayerType,
|
||||
LineStyle,
|
||||
FillStyle,
|
||||
YScaleType,
|
||||
|
@ -105,8 +104,7 @@ export type {
|
|||
|
||||
export type { LensEmbeddableInput, LensSavedObjectAttributes, Embeddable } from './embeddable';
|
||||
|
||||
/** @deprecated Please use LayerTypes from @kbn/expression-xy-plugin **/
|
||||
export const layerTypes = LayerTypes;
|
||||
export { layerTypes } from '../common/layer_types';
|
||||
|
||||
export type { LensPublicStart, LensPublicSetup } from './plugin';
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
getFilters: jest.fn(),
|
||||
getMaxPossibleNumValues: jest.fn(),
|
||||
isTextBasedLanguage: jest.fn(() => false),
|
||||
hasDefaultTimeField: jest.fn(() => true),
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -53,6 +54,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
getDropProps: jest.fn(),
|
||||
onDrop: jest.fn(),
|
||||
createEmptyLayer: jest.fn(),
|
||||
syncColumns: jest.fn(),
|
||||
|
||||
// this is an additional property which doesn't exist on real datasources
|
||||
// but can be used to validate whether specific API mock functions are called
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiFormRow, EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { CollapseFunction } from '../../common/expressions';
|
||||
|
||||
const options = [
|
||||
{ text: i18n.translate('xpack.lens.collapse.none', { defaultMessage: 'None' }), value: '' },
|
||||
|
@ -22,7 +23,7 @@ export function CollapseSetting({
|
|||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onChange: (value: CollapseFunction) => void;
|
||||
}) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
|
@ -52,7 +53,7 @@ export function CollapseSetting({
|
|||
options={options}
|
||||
value={value}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange(e.target.value);
|
||||
onChange(e.target.value as CollapseFunction);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -44,6 +44,7 @@ export const {
|
|||
cloneLayer,
|
||||
addLayer,
|
||||
setLayerDefaultDimension,
|
||||
removeDimension,
|
||||
} = lensActions;
|
||||
|
||||
export const makeConfigureStore = (
|
||||
|
|
|
@ -20,12 +20,20 @@ import {
|
|||
LensRootStore,
|
||||
selectTriggerApplyChanges,
|
||||
selectChangesApplied,
|
||||
removeDimension,
|
||||
} from '.';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { makeLensStore, defaultState, mockStoreDeps } from '../mocks';
|
||||
import { DatasourceMap, VisualizationMap } from '../types';
|
||||
import {
|
||||
Datasource,
|
||||
DatasourceMap,
|
||||
Visualization,
|
||||
VisualizationDimensionGroupConfig,
|
||||
VisualizationMap,
|
||||
} from '../types';
|
||||
import { applyChanges, disableAutoApply, enableAutoApply, setChangesApplied } from './lens_slice';
|
||||
import { LensAppState } from './types';
|
||||
import { DataViewsState, LensAppState } from './types';
|
||||
import { layerTypes } from '../../common/layer_types';
|
||||
|
||||
describe('lensSlice', () => {
|
||||
let store: EnhancedStore<{ lens: LensAppState }>;
|
||||
|
@ -108,7 +116,7 @@ describe('lensSlice', () => {
|
|||
})
|
||||
);
|
||||
|
||||
expect(store.getState().lens.visualization.state).toBe(newVisState);
|
||||
expect(store.getState().lens.visualization.state).toEqual(newVisState);
|
||||
});
|
||||
it('should update the datasource state with passed in reducer', () => {
|
||||
const datasourceUpdater = jest.fn(() => ({ changed: true }));
|
||||
|
@ -278,7 +286,12 @@ describe('lensSlice', () => {
|
|||
),
|
||||
removeLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
||||
insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||
insertLayer: (layerIds: unknown, layerId: string, layersToLinkTo: string[]) => [
|
||||
...(layerIds as string[]),
|
||||
layerId,
|
||||
...layersToLinkTo,
|
||||
],
|
||||
getCurrentIndexPatternId: jest.fn(() => 'indexPattern1'),
|
||||
getUsedDataView: jest.fn(() => 'indexPattern1'),
|
||||
};
|
||||
};
|
||||
|
@ -296,8 +309,10 @@ describe('lensSlice', () => {
|
|||
testDatasource: testDatasource('testDatasource'),
|
||||
testDatasource2: testDatasource('testDatasource2'),
|
||||
};
|
||||
|
||||
const activeVisId = 'testVis';
|
||||
const visualizationMap = {
|
||||
testVis: {
|
||||
[activeVisId]: {
|
||||
clearLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).map((id: string) =>
|
||||
id === layerId ? `vis_clear_${layerId}` : id
|
||||
|
@ -305,9 +320,10 @@ describe('lensSlice', () => {
|
|||
removeLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
||||
getLayerIds: (layerIds: unknown) => layerIds as string[],
|
||||
getLayersToLinkTo: (state, newLayerId) => ['linked-layer-id'],
|
||||
appendLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||
getSupportedLayers: jest.fn(() => [{ type: LayerTypes.DATA, label: 'Data Layer' }]),
|
||||
},
|
||||
getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]),
|
||||
} as Partial<Visualization>,
|
||||
};
|
||||
|
||||
let customStore: LensRootStore;
|
||||
|
@ -317,12 +333,12 @@ describe('lensSlice', () => {
|
|||
activeDatasourceId: 'testDatasource',
|
||||
datasourceStates,
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
activeId: activeVisId,
|
||||
state: ['layer1', 'layer2'],
|
||||
},
|
||||
stagedPreview: {
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
activeId: activeVisId,
|
||||
state: ['layer1', 'layer2'],
|
||||
},
|
||||
datasourceStates,
|
||||
|
@ -345,11 +361,112 @@ describe('lensSlice', () => {
|
|||
const state = customStore.getState().lens;
|
||||
|
||||
expect(state.visualization.state).toEqual(['layer1', 'layer2', 'foo']);
|
||||
expect(state.datasourceStates.testDatasource.state).toEqual(['layer1', 'foo']);
|
||||
expect(state.datasourceStates.testDatasource.state).toEqual([
|
||||
'layer1',
|
||||
'foo',
|
||||
'linked-layer-id',
|
||||
]);
|
||||
expect(state.datasourceStates.testDatasource2.state).toEqual(['layer2']);
|
||||
expect(state.stagedPreview).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('addLayer: syncs linked dimensions', () => {
|
||||
const activeVisualization = visualizationMap[activeVisId];
|
||||
|
||||
activeVisualization.getLinkedDimensions = jest.fn(() => [
|
||||
{
|
||||
from: {
|
||||
layerId: 'from-layer',
|
||||
columnId: 'from-column',
|
||||
groupId: 'from-group',
|
||||
},
|
||||
to: {
|
||||
layerId: 'from-layer',
|
||||
columnId: 'from-column',
|
||||
groupId: 'from-group',
|
||||
},
|
||||
},
|
||||
]);
|
||||
activeVisualization.getConfiguration = jest.fn(() => ({
|
||||
groups: [{ groupId: 'to-group' } as VisualizationDimensionGroupConfig],
|
||||
}));
|
||||
activeVisualization.onDrop = jest.fn(({ prevState }) => prevState);
|
||||
(datasourceMap.testDatasource as unknown as Datasource).syncColumns = jest.fn(
|
||||
({ state }) => state
|
||||
);
|
||||
|
||||
customStore.dispatch(
|
||||
addLayer({
|
||||
layerId: 'foo',
|
||||
layerType: layerTypes.DATA,
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
(
|
||||
(datasourceMap.testDatasource as unknown as Datasource).syncColumns as jest.Mock<
|
||||
Datasource['syncColumns']
|
||||
>
|
||||
).mock.calls[0][0]
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"getDimensionGroups": [Function],
|
||||
"indexPatterns": Object {},
|
||||
"links": Array [
|
||||
Object {
|
||||
"from": Object {
|
||||
"columnId": "from-column",
|
||||
"groupId": "from-group",
|
||||
"layerId": "from-layer",
|
||||
},
|
||||
"to": Object {
|
||||
"columnId": "from-column",
|
||||
"groupId": "from-group",
|
||||
"layerId": "from-layer",
|
||||
},
|
||||
},
|
||||
],
|
||||
"state": Array [
|
||||
"layer1",
|
||||
"foo",
|
||||
"linked-layer-id",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(activeVisualization.onDrop).toHaveBeenCalledTimes(1);
|
||||
expect({
|
||||
...(activeVisualization.onDrop as jest.Mock<Visualization['onDrop']>).mock.calls[0][0],
|
||||
frame: undefined,
|
||||
}).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"dropType": "duplicate_compatible",
|
||||
"frame": undefined,
|
||||
"group": undefined,
|
||||
"prevState": Array [
|
||||
"layer1",
|
||||
"layer2",
|
||||
"foo",
|
||||
],
|
||||
"source": Object {
|
||||
"columnId": "from-column",
|
||||
"groupId": "from-group",
|
||||
"humanData": Object {
|
||||
"label": "",
|
||||
},
|
||||
"id": "from-column",
|
||||
"layerId": "from-layer",
|
||||
},
|
||||
"target": Object {
|
||||
"columnId": "from-column",
|
||||
"filterOperations": [Function],
|
||||
"groupId": "from-group",
|
||||
"layerId": "from-layer",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('removeLayer: should remove the layer if it is not the only layer', () => {
|
||||
customStore.dispatch(
|
||||
removeOrClearLayer({
|
||||
|
@ -366,5 +483,155 @@ describe('lensSlice', () => {
|
|||
expect(state.stagedPreview).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removing a dimension', () => {
|
||||
const colToRemove = 'col-id';
|
||||
const otherCol = 'other-col-id';
|
||||
const datasourceId = 'testDatasource';
|
||||
|
||||
interface DatasourceState {
|
||||
cols: string[];
|
||||
}
|
||||
|
||||
const datasourceStates = {
|
||||
[datasourceId]: {
|
||||
isLoading: false,
|
||||
state: {
|
||||
cols: [colToRemove, otherCol],
|
||||
} as DatasourceState,
|
||||
},
|
||||
};
|
||||
|
||||
const datasourceMap = {
|
||||
[datasourceId]: {
|
||||
id: datasourceId,
|
||||
removeColumn: jest.fn(({ prevState: state, columnId }) => ({
|
||||
...(state as DatasourceState),
|
||||
cols: (state as DatasourceState).cols.filter((id) => id !== columnId),
|
||||
})),
|
||||
getLayers: () => [],
|
||||
} as Partial<Datasource>,
|
||||
};
|
||||
|
||||
const activeVisId = 'testVis';
|
||||
|
||||
const visualizationMap = {
|
||||
[activeVisId]: {
|
||||
removeDimension: jest.fn(({ prevState, columnId }) =>
|
||||
(prevState as string[]).filter((id) => id !== columnId)
|
||||
),
|
||||
} as Partial<Visualization>,
|
||||
};
|
||||
|
||||
const visualizationState = [colToRemove, otherCol];
|
||||
|
||||
const dataViews = { indexPatterns: {} } as DataViewsState;
|
||||
|
||||
const layerId = 'some-layer-id';
|
||||
|
||||
let customStore: LensRootStore;
|
||||
beforeEach(() => {
|
||||
customStore = makeLensStore({
|
||||
preloadedState: {
|
||||
activeDatasourceId: datasourceId,
|
||||
datasourceStates,
|
||||
visualization: {
|
||||
activeId: activeVisId,
|
||||
state: visualizationState,
|
||||
},
|
||||
dataViews,
|
||||
} as Partial<LensAppState>,
|
||||
storeDeps: mockStoreDeps({
|
||||
visualizationMap: visualizationMap as unknown as VisualizationMap,
|
||||
datasourceMap: datasourceMap as unknown as DatasourceMap,
|
||||
}),
|
||||
}).store;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('removes a dimension', () => {
|
||||
customStore.dispatch(
|
||||
removeDimension({
|
||||
layerId,
|
||||
columnId: colToRemove,
|
||||
datasourceId,
|
||||
})
|
||||
);
|
||||
|
||||
const state = customStore.getState().lens;
|
||||
|
||||
expect(datasourceMap[datasourceId].removeColumn).toHaveBeenCalledWith({
|
||||
layerId,
|
||||
columnId: colToRemove,
|
||||
prevState: datasourceStates[datasourceId].state,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
});
|
||||
expect(visualizationMap[activeVisId].removeDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
layerId,
|
||||
columnId: colToRemove,
|
||||
prevState: visualizationState,
|
||||
})
|
||||
);
|
||||
expect(state.visualization.state).toEqual([otherCol]);
|
||||
expect((state.datasourceStates[datasourceId].state as DatasourceState).cols).toEqual([
|
||||
otherCol,
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes a dimension without touching the datasource', () => {
|
||||
customStore.dispatch(
|
||||
removeDimension({
|
||||
layerId,
|
||||
columnId: colToRemove,
|
||||
datasourceId: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const state = customStore.getState().lens;
|
||||
|
||||
expect(datasourceMap[datasourceId].removeColumn).not.toHaveBeenCalled();
|
||||
|
||||
expect(visualizationMap[activeVisId].removeDimension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
layerId,
|
||||
columnId: colToRemove,
|
||||
prevState: visualizationState,
|
||||
})
|
||||
);
|
||||
expect(state.visualization.state).toEqual([otherCol]);
|
||||
});
|
||||
|
||||
it('removes linked dimensions', () => {
|
||||
visualizationMap[activeVisId].getLinkedDimensions = jest.fn(() => [
|
||||
{
|
||||
from: {
|
||||
columnId: colToRemove,
|
||||
layerId,
|
||||
groupId: '',
|
||||
},
|
||||
to: {
|
||||
columnId: otherCol,
|
||||
layerId,
|
||||
groupId: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
customStore.dispatch(
|
||||
removeDimension({
|
||||
layerId,
|
||||
columnId: colToRemove,
|
||||
datasourceId,
|
||||
})
|
||||
);
|
||||
|
||||
const state = customStore.getState().lens;
|
||||
|
||||
expect(state.visualization.state).toEqual([]);
|
||||
expect((state.datasourceStates[datasourceId].state as DatasourceState).cols).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,13 @@ import { Query } from '@kbn/es-query';
|
|||
import { History } from 'history';
|
||||
import { LensEmbeddableInput } from '..';
|
||||
import { TableInspectorAdapter } from '../editor_frame_service/types';
|
||||
import type { VisualizeEditorContext, Suggestion, IndexPattern } from '../types';
|
||||
import type {
|
||||
VisualizeEditorContext,
|
||||
Suggestion,
|
||||
IndexPattern,
|
||||
VisualizationMap,
|
||||
DatasourceMap,
|
||||
} from '../types';
|
||||
import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils';
|
||||
import type { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types';
|
||||
import type { Datasource, Visualization } from '../types';
|
||||
|
@ -21,7 +27,8 @@ import type { LayerType } from '../../common/types';
|
|||
import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer';
|
||||
import { getVisualizeFieldSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers';
|
||||
import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types';
|
||||
import { selectFramePublicAPI } from './selectors';
|
||||
import { selectDataViews, selectFramePublicAPI } from './selectors';
|
||||
import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils';
|
||||
|
||||
export const initialState: LensAppState = {
|
||||
persistedDoc: undefined,
|
||||
|
@ -106,6 +113,7 @@ export const updateDatasourceState = createAction<{
|
|||
updater: unknown | ((prevState: unknown) => unknown);
|
||||
datasourceId: string;
|
||||
clearStagedPreview?: boolean;
|
||||
dontSyncLinkedDimensions?: boolean;
|
||||
}>('lens/updateDatasourceState');
|
||||
export const updateVisualizationState = createAction<{
|
||||
visualizationId: string;
|
||||
|
@ -200,6 +208,11 @@ export const changeIndexPattern = createAction<{
|
|||
layerId?: string;
|
||||
dataViews: Partial<DataViewsState>;
|
||||
}>('lens/changeIndexPattern');
|
||||
export const removeDimension = createAction<{
|
||||
layerId: string;
|
||||
columnId: string;
|
||||
datasourceId?: string;
|
||||
}>('lens/removeDimension');
|
||||
|
||||
export const lensActions = {
|
||||
setState,
|
||||
|
@ -231,6 +244,8 @@ export const lensActions = {
|
|||
updateIndexPatterns,
|
||||
replaceIndexpattern,
|
||||
changeIndexPattern,
|
||||
removeDimension,
|
||||
syncLinkedDimensions,
|
||||
};
|
||||
|
||||
export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
||||
|
@ -286,7 +301,31 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
};
|
||||
}
|
||||
) => {
|
||||
const newState = updater(current(state) as LensAppState);
|
||||
let newState: LensAppState = updater(current(state) as LensAppState);
|
||||
|
||||
if (newState.activeDatasourceId) {
|
||||
const { datasourceState, visualizationState } = syncLinkedDimensions(
|
||||
newState,
|
||||
visualizationMap,
|
||||
datasourceMap
|
||||
);
|
||||
|
||||
newState = {
|
||||
...newState,
|
||||
visualization: {
|
||||
...newState.visualization,
|
||||
state: visualizationState,
|
||||
},
|
||||
datasourceStates: {
|
||||
...newState.datasourceStates,
|
||||
[newState.activeDatasourceId]: {
|
||||
...newState.datasourceStates[newState.activeDatasourceId],
|
||||
state: datasourceState,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...newState,
|
||||
stagedPreview: undefined,
|
||||
|
@ -545,22 +584,49 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
updater: unknown | ((prevState: unknown) => unknown);
|
||||
datasourceId: string;
|
||||
clearStagedPreview?: boolean;
|
||||
dontSyncLinkedDimensions: boolean;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
const currentState = current(state);
|
||||
|
||||
const newAppState: LensAppState = {
|
||||
...currentState,
|
||||
datasourceStates: {
|
||||
...state.datasourceStates,
|
||||
...currentState.datasourceStates,
|
||||
[payload.datasourceId]: {
|
||||
state:
|
||||
typeof payload.updater === 'function'
|
||||
? payload.updater(current(state).datasourceStates[payload.datasourceId].state)
|
||||
? payload.updater(currentState.datasourceStates[payload.datasourceId].state)
|
||||
: payload.updater,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
stagedPreview: payload.clearStagedPreview ? undefined : state.stagedPreview,
|
||||
stagedPreview: payload.clearStagedPreview ? undefined : currentState.stagedPreview,
|
||||
};
|
||||
|
||||
if (payload.dontSyncLinkedDimensions) {
|
||||
return newAppState;
|
||||
}
|
||||
|
||||
const {
|
||||
datasourceState: syncedDatasourceState,
|
||||
visualizationState: syncedVisualizationState,
|
||||
} = syncLinkedDimensions(newAppState, visualizationMap, datasourceMap, payload.datasourceId);
|
||||
|
||||
return {
|
||||
...newAppState,
|
||||
visualization: {
|
||||
...newAppState.visualization,
|
||||
state: syncedVisualizationState,
|
||||
},
|
||||
datasourceStates: {
|
||||
...newAppState.datasourceStates,
|
||||
[payload.datasourceId]: {
|
||||
state: syncedDatasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
[updateVisualizationState.type]: (
|
||||
|
@ -583,13 +649,21 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
if (state.visualization.activeId !== payload.visualizationId) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
visualization: {
|
||||
...state.visualization,
|
||||
state: payload.newState,
|
||||
},
|
||||
};
|
||||
|
||||
state.visualization.state = payload.newState;
|
||||
|
||||
if (!state.activeDatasourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO - consolidate into applySyncLinkedDimensions
|
||||
const {
|
||||
datasourceState: syncedDatasourceState,
|
||||
visualizationState: syncedVisualizationState,
|
||||
} = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
|
||||
|
||||
state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState;
|
||||
state.visualization.state = syncedVisualizationState;
|
||||
},
|
||||
|
||||
[switchVisualization.type]: (
|
||||
|
@ -945,11 +1019,15 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
.getSupportedLayers(visualizationState, framePublicAPI)
|
||||
.find(({ type }) => type === layerType) || {};
|
||||
|
||||
const layersToLinkTo =
|
||||
activeVisualization.getLayersToLinkTo?.(visualizationState, layerId) ?? [];
|
||||
|
||||
const datasourceState =
|
||||
!noDatasource && activeDatasource
|
||||
? activeDatasource.insertLayer(
|
||||
state.datasourceStates[state.activeDatasourceId].state,
|
||||
layerId
|
||||
layerId,
|
||||
layersToLinkTo
|
||||
)
|
||||
: state.datasourceStates[state.activeDatasourceId].state;
|
||||
|
||||
|
@ -966,6 +1044,14 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
state.visualization.state = activeVisualizationState;
|
||||
state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
|
||||
state.stagedPreview = undefined;
|
||||
|
||||
const {
|
||||
datasourceState: syncedDatasourceState,
|
||||
visualizationState: syncedVisualizationState,
|
||||
} = syncLinkedDimensions(current(state), visualizationMap, datasourceMap);
|
||||
|
||||
state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState;
|
||||
state.visualization.state = syncedVisualizationState;
|
||||
},
|
||||
[setLayerDefaultDimension.type]: (
|
||||
state,
|
||||
|
@ -1001,6 +1087,72 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
state.visualization.state = activeVisualizationState;
|
||||
state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState;
|
||||
},
|
||||
[removeDimension.type]: (
|
||||
state,
|
||||
{
|
||||
payload: { layerId, columnId, datasourceId },
|
||||
}: {
|
||||
payload: {
|
||||
layerId: string;
|
||||
columnId: string;
|
||||
datasourceId?: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
if (!state.visualization.activeId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const activeVisualization = visualizationMap[state.visualization.activeId];
|
||||
|
||||
const links = activeVisualization.getLinkedDimensions?.(state.visualization.state);
|
||||
|
||||
const linkedDimensions = links
|
||||
?.filter(({ from: { columnId: fromId } }) => columnId === fromId)
|
||||
?.map(({ to }) => to);
|
||||
|
||||
const datasource = datasourceId ? datasourceMap[datasourceId] : undefined;
|
||||
|
||||
const frame = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
|
||||
|
||||
const remove = (dimensionProps: { layerId: string; columnId: string }) => {
|
||||
if (datasource && datasourceId) {
|
||||
let datasourceState;
|
||||
try {
|
||||
datasourceState = current(state.datasourceStates[datasourceId].state);
|
||||
} catch {
|
||||
datasourceState = state.datasourceStates[datasourceId].state;
|
||||
}
|
||||
state.datasourceStates[datasourceId].state = datasource?.removeColumn({
|
||||
layerId: dimensionProps.layerId,
|
||||
columnId: dimensionProps.columnId,
|
||||
prevState: datasourceState,
|
||||
indexPatterns: frame.dataViews.indexPatterns,
|
||||
});
|
||||
}
|
||||
|
||||
let visualizationState;
|
||||
try {
|
||||
visualizationState = current(state.visualization.state);
|
||||
} catch {
|
||||
visualizationState = state.visualization.state;
|
||||
}
|
||||
state.visualization.state = activeVisualization.removeDimension({
|
||||
layerId: dimensionProps.layerId,
|
||||
columnId: dimensionProps.columnId,
|
||||
prevState: visualizationState,
|
||||
frame,
|
||||
});
|
||||
};
|
||||
|
||||
remove({ layerId, columnId });
|
||||
|
||||
linkedDimensions?.forEach(
|
||||
(linkedDimension) =>
|
||||
linkedDimension.columnId && // if there's no columnId, there's no dimension to remove
|
||||
remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId })
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1053,6 +1205,11 @@ function addInitialValueIfAvailable({
|
|||
{
|
||||
...info,
|
||||
columnId: columnId || info.columnId,
|
||||
visualizationGroups: activeVisualization.getConfiguration({
|
||||
layerId,
|
||||
frame: framePublicAPI,
|
||||
state: activeVisualizationState,
|
||||
}).groups,
|
||||
}
|
||||
),
|
||||
activeVisualizationState,
|
||||
|
@ -1071,3 +1228,76 @@ function addInitialValueIfAvailable({
|
|||
activeVisualizationState: visualizationState,
|
||||
};
|
||||
}
|
||||
|
||||
function syncLinkedDimensions(
|
||||
state: LensAppState,
|
||||
visualizationMap: VisualizationMap,
|
||||
datasourceMap: DatasourceMap,
|
||||
_datasourceId?: string
|
||||
) {
|
||||
const datasourceId = _datasourceId ?? state.activeDatasourceId;
|
||||
|
||||
if (!datasourceId) {
|
||||
return { datasourceState: null, visualizationState: state.visualization.state };
|
||||
}
|
||||
|
||||
const indexPatterns = selectDataViews({ lens: state }).indexPatterns;
|
||||
|
||||
let datasourceState: unknown = state.datasourceStates[datasourceId].state;
|
||||
let visualizationState: unknown = state.visualization.state;
|
||||
|
||||
const activeVisualization = visualizationMap[state.visualization.activeId!]; // TODO - double check the safety of this coercion
|
||||
const linkedDimensions = activeVisualization.getLinkedDimensions?.(visualizationState);
|
||||
const frame = selectFramePublicAPI({ lens: state }, datasourceMap);
|
||||
|
||||
const getDimensionGroups = (layerId: string) =>
|
||||
activeVisualization.getConfiguration({
|
||||
state: visualizationState,
|
||||
layerId,
|
||||
frame,
|
||||
}).groups;
|
||||
|
||||
if (linkedDimensions) {
|
||||
const idAssuredLinks = linkedDimensions.map((link) => ({
|
||||
...link,
|
||||
to: { ...link.to, columnId: link.to.columnId ?? generateId() },
|
||||
}));
|
||||
|
||||
datasourceState = datasourceMap[datasourceId].syncColumns({
|
||||
state: datasourceState,
|
||||
links: idAssuredLinks,
|
||||
getDimensionGroups,
|
||||
indexPatterns,
|
||||
});
|
||||
|
||||
idAssuredLinks.forEach(({ from, to }) => {
|
||||
const dropSource = {
|
||||
...from,
|
||||
id: from.columnId,
|
||||
// don't need to worry about accessibility here
|
||||
humanData: { label: '' },
|
||||
};
|
||||
|
||||
const dropTarget = {
|
||||
...to,
|
||||
filterOperations: () => true,
|
||||
};
|
||||
|
||||
visualizationState = (activeVisualization.onDrop || onDropForVisualization)?.(
|
||||
{
|
||||
prevState: visualizationState,
|
||||
frame,
|
||||
target: dropTarget,
|
||||
source: dropSource,
|
||||
dropType: 'duplicate_compatible',
|
||||
group: getDimensionGroups(to.layerId).find(
|
||||
({ groupId }) => groupId === dropTarget.groupId
|
||||
),
|
||||
},
|
||||
activeVisualization
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return { datasourceState, visualizationState };
|
||||
}
|
||||
|
|
|
@ -233,6 +233,15 @@ export interface GetDropPropsArgs<T = unknown> {
|
|||
indexPatterns: IndexPatternMap;
|
||||
}
|
||||
|
||||
interface DimensionLink {
|
||||
from: { columnId: string; groupId: string; layerId: string };
|
||||
to: {
|
||||
columnId?: string;
|
||||
groupId: string;
|
||||
layerId: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the datasource registry
|
||||
*/
|
||||
|
@ -254,7 +263,7 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] };
|
||||
getUnifiedSearchErrors?: (state: T) => Error[];
|
||||
|
||||
insertLayer: (state: T, newLayerId: string) => T;
|
||||
insertLayer: (state: T, newLayerId: string, linkToLayers?: string[]) => T;
|
||||
createEmptyLayer: (indexPatternId: string) => T;
|
||||
removeLayer: (state: T, layerId: string) => T;
|
||||
clearLayer: (state: T, layerId: string) => T;
|
||||
|
@ -278,10 +287,18 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
value: {
|
||||
columnId: string;
|
||||
groupId: string;
|
||||
visualizationGroups: VisualizationDimensionGroupConfig[];
|
||||
staticValue?: unknown;
|
||||
autoTimeField?: boolean;
|
||||
}
|
||||
) => T;
|
||||
|
||||
syncColumns: (args: {
|
||||
state: T;
|
||||
links: Array<DimensionLink & { to: { columnId: string } }>;
|
||||
getDimensionGroups: (layerId: string) => VisualizationDimensionGroupConfig[];
|
||||
indexPatterns: IndexPatternMap;
|
||||
}) => T;
|
||||
getSelectedFields?: (state: T) => string[];
|
||||
|
||||
renderDataPanel: (
|
||||
|
@ -489,6 +506,7 @@ export interface DatasourcePublicAPI {
|
|||
* or 6 if the "Other" bucket is enabled)
|
||||
*/
|
||||
getMaxPossibleNumValues: (columnId: string) => number | null;
|
||||
hasDefaultTimeField: () => boolean;
|
||||
}
|
||||
|
||||
export interface DatasourceDataPanelProps<T = unknown> {
|
||||
|
@ -619,7 +637,7 @@ export interface DatasourceDimensionDropProps<T> {
|
|||
forceRender?: boolean;
|
||||
}
|
||||
>;
|
||||
dimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
targetLayerDimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
}
|
||||
|
||||
export type DatasourceDimensionDropHandlerProps<S> = DatasourceDimensionDropProps<S> & {
|
||||
|
@ -675,6 +693,7 @@ export interface OperationMetadata {
|
|||
*/
|
||||
export interface OperationDescriptor extends Operation {
|
||||
hasTimeShift: boolean;
|
||||
hasReducedTimeRange: boolean;
|
||||
}
|
||||
|
||||
export interface VisualizationConfigProps<T = unknown> {
|
||||
|
@ -699,7 +718,10 @@ export interface VisualizationToolbarProps<T = unknown> {
|
|||
export type VisualizationDimensionEditorProps<T = unknown> = VisualizationConfigProps<T> & {
|
||||
groupId: string;
|
||||
accessor: string;
|
||||
datasource: DatasourcePublicAPI | undefined;
|
||||
setState(newState: T | ((currState: T) => T)): void;
|
||||
addLayer: (layerType: LayerType) => void;
|
||||
removeLayer: (layerId: string) => void;
|
||||
panelRef: MutableRefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
|
@ -977,7 +999,9 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
columnId: string;
|
||||
groupId: string;
|
||||
staticValue?: unknown;
|
||||
autoTimeField?: boolean;
|
||||
}>;
|
||||
canAddViaMenu?: boolean;
|
||||
}>;
|
||||
/**
|
||||
* returns a list of custom actions supported by the visualization layer.
|
||||
|
@ -990,6 +1014,17 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
) => LayerAction[];
|
||||
/** returns the type string of the given layer */
|
||||
getLayerType: (layerId: string, state?: T) => LayerType | undefined;
|
||||
|
||||
/**
|
||||
* Get the layers this one should be linked to (currently that means just keeping the data view in sync)
|
||||
*/
|
||||
getLayersToLinkTo?: (state: T, newLayerId: string) => string[];
|
||||
|
||||
/**
|
||||
* Returns a set of dimensions that should be kept in sync
|
||||
*/
|
||||
getLinkedDimensions?: (state: T) => DimensionLink[];
|
||||
|
||||
/* returns the type of removal operation to perform for the specific layer in the current state */
|
||||
getRemoveOperation?: (state: T, layerId: string) => 'remove' | 'clear';
|
||||
|
||||
|
@ -997,6 +1032,7 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
* For consistency across different visualizations, the dimension configuration UI is standardized
|
||||
*/
|
||||
getConfiguration: (props: VisualizationConfigProps<T>) => {
|
||||
hidden?: boolean;
|
||||
groups: VisualizationDimensionGroupConfig[];
|
||||
};
|
||||
|
||||
|
@ -1148,6 +1184,7 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
*/
|
||||
onIndexPatternChange?: (state: T, indexPatternId: string, layerId?: string) => T;
|
||||
onIndexPatternRename?: (state: T, oldIndexPatternId: string, newIndexPatternId: string) => T;
|
||||
getLayersToRemoveOnIndexPatternChange?: (state: T) => string[];
|
||||
/**
|
||||
* Gets custom display options for showing the visualization.
|
||||
*/
|
||||
|
|
|
@ -74,6 +74,9 @@ describe('data table dimension editor', () => {
|
|||
setState,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
panelRef: React.createRef(),
|
||||
addLayer: jest.fn(),
|
||||
removeLayer: jest.fn(),
|
||||
datasource: {} as DatasourcePublicAPI,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ export function TableDimensionEditor(
|
|||
{props.groupId === 'rows' && (
|
||||
<CollapseSetting
|
||||
value={column.collapseFn || ''}
|
||||
onChange={(collapseFn: string) => {
|
||||
onChange={(collapseFn) => {
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { collapseFn }),
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import React from 'react';
|
||||
import { EuiComboBox, EuiFieldText } from '@elastic/eui';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types';
|
||||
import {
|
||||
DatasourcePublicAPI,
|
||||
FramePublicAPI,
|
||||
VisualizationDimensionEditorProps,
|
||||
} from '../../../types';
|
||||
import { DatatableVisualizationState } from '../visualization';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
@ -67,6 +71,9 @@ describe('data table dimension editor additional section', () => {
|
|||
setState,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
panelRef: React.createRef(),
|
||||
addLayer: jest.fn(),
|
||||
removeLayer: jest.fn(),
|
||||
datasource: {} as DatasourcePublicAPI,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -526,6 +526,7 @@ describe('Datatable Visualization', () => {
|
|||
label: 'label',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
|
||||
const expression = datatableVisualization.toExpression(
|
||||
|
@ -577,6 +578,7 @@ describe('Datatable Visualization', () => {
|
|||
label: 'label',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
|
||||
const expression = datatableVisualization.toExpression(
|
||||
|
@ -712,6 +714,7 @@ describe('Datatable Visualization', () => {
|
|||
label: 'label',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
|
||||
const error = datatableVisualization.getErrorMessages({
|
||||
|
@ -737,6 +740,7 @@ describe('Datatable Visualization', () => {
|
|||
label: 'label',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
|
||||
const error = datatableVisualization.getErrorMessages({
|
||||
|
|
|
@ -24,6 +24,7 @@ import { act } from 'react-dom/test-utils';
|
|||
import { PalettePanelContainer } from '../../shared_components';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { LegacyMetricState } from '../../../common/types';
|
||||
import { DatasourcePublicAPI } from '../..';
|
||||
|
||||
// mocking random id generator function
|
||||
jest.mock('@elastic/eui', () => {
|
||||
|
@ -93,6 +94,9 @@ describe('metric dimension editor', () => {
|
|||
setState,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
panelRef: React.createRef(),
|
||||
addLayer: jest.fn(),
|
||||
removeLayer: jest.fn(),
|
||||
datasource: {} as DatasourcePublicAPI,
|
||||
};
|
||||
// add a div to the ref
|
||||
props.panelRef.current = document.createElement('div');
|
||||
|
|
|
@ -271,6 +271,7 @@ describe('metric_visualization', () => {
|
|||
label: 'shazm',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`metric visualization dimension groups configuration generates configuration 1`] = `
|
||||
exports[`metric visualization dimension groups configuration primary layer generates configuration 1`] = `
|
||||
Object {
|
||||
"groups": Array [
|
||||
Object {
|
||||
|
@ -20,7 +20,6 @@ Object {
|
|||
},
|
||||
"groupId": "metric",
|
||||
"groupLabel": "Primary metric",
|
||||
"layerId": "first",
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
|
@ -42,11 +41,9 @@ Object {
|
|||
},
|
||||
"groupId": "secondaryMetric",
|
||||
"groupLabel": "Secondary metric",
|
||||
"layerId": "first",
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
"requiredMinDimensionCount": 0,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
Object {
|
||||
|
@ -65,12 +62,10 @@ Object {
|
|||
"groupId": "max",
|
||||
"groupLabel": "Maximum value",
|
||||
"groupTooltip": "If the maximum value is specified, the minimum value is fixed at zero.",
|
||||
"layerId": "first",
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
"prioritizedOperation": "max",
|
||||
"requiredMinDimensionCount": 0,
|
||||
"supportStaticValue": true,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
|
@ -90,15 +85,13 @@ Object {
|
|||
},
|
||||
"groupId": "breakdownBy",
|
||||
"groupLabel": "Break down by",
|
||||
"layerId": "first",
|
||||
"requiredMinDimensionCount": 0,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`metric visualization dimension groups configuration operation filtering breakdownBy supports correct operations 1`] = `
|
||||
exports[`metric visualization dimension groups configuration primary layer operation filtering breakdownBy supports correct operations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"dataType": "number",
|
||||
|
@ -111,7 +104,7 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`metric visualization dimension groups configuration operation filtering max supports correct operations 1`] = `
|
||||
exports[`metric visualization dimension groups configuration primary layer operation filtering max supports correct operations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"dataType": "number",
|
||||
|
@ -120,7 +113,7 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`metric visualization dimension groups configuration operation filtering metric supports correct operations 1`] = `
|
||||
exports[`metric visualization dimension groups configuration primary layer operation filtering metric supports correct operations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"dataType": "number",
|
||||
|
@ -129,7 +122,7 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`metric visualization dimension groups configuration operation filtering secondaryMetric supports correct operations 1`] = `
|
||||
exports[`metric visualization dimension groups configuration primary layer operation filtering secondaryMetric supports correct operations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"dataType": "number",
|
||||
|
@ -137,3 +130,63 @@ Array [
|
|||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`metric visualization dimension groups configuration trendline layer generates configuration 1`] = `
|
||||
Object {
|
||||
"groups": Array [
|
||||
Object {
|
||||
"accessors": Array [
|
||||
Object {
|
||||
"columnId": "trendline-metric-col-id",
|
||||
},
|
||||
],
|
||||
"filterOperations": [Function],
|
||||
"groupId": "trendMetric",
|
||||
"groupLabel": "Primary metric",
|
||||
"hideGrouping": true,
|
||||
"nestingOrder": 3,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
Object {
|
||||
"accessors": Array [
|
||||
Object {
|
||||
"columnId": "trendline-secondary-metric-col-id",
|
||||
},
|
||||
],
|
||||
"filterOperations": [Function],
|
||||
"groupId": "trendSecondaryMetric",
|
||||
"groupLabel": "Secondary metric",
|
||||
"hideGrouping": true,
|
||||
"nestingOrder": 2,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
Object {
|
||||
"accessors": Array [
|
||||
Object {
|
||||
"columnId": "trendline-time-col-id",
|
||||
},
|
||||
],
|
||||
"filterOperations": [Function],
|
||||
"groupId": "trendTime",
|
||||
"groupLabel": "Time field",
|
||||
"hideGrouping": true,
|
||||
"nestingOrder": 1,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
Object {
|
||||
"accessors": Array [
|
||||
Object {
|
||||
"columnId": "trendline-breakdown-col-id",
|
||||
},
|
||||
],
|
||||
"filterOperations": [Function],
|
||||
"groupId": "trendBreakdownBy",
|
||||
"groupLabel": "Break down by",
|
||||
"hideGrouping": true,
|
||||
"nestingOrder": 0,
|
||||
"supportsMoreColumns": false,
|
||||
},
|
||||
],
|
||||
"hidden": true,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -12,4 +12,8 @@ export const GROUP_ID = {
|
|||
SECONDARY_METRIC: 'secondaryMetric',
|
||||
MAX: 'max',
|
||||
BREAKDOWN_BY: 'breakdownBy',
|
||||
TREND_METRIC: 'trendMetric',
|
||||
TREND_SECONDARY_METRIC: 'trendSecondaryMetric',
|
||||
TREND_TIME: 'trendTime',
|
||||
TREND_BREAKDOWN_BY: 'trendBreakdownBy',
|
||||
} as const;
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import React, { FormEvent } from 'react';
|
||||
import { VisualizationDimensionEditorProps } from '../../types';
|
||||
import { OperationDescriptor, VisualizationDimensionEditorProps } from '../../types';
|
||||
import { CustomPaletteParams, PaletteOutput, PaletteRegistry } from '@kbn/coloring';
|
||||
|
||||
import { MetricVisualizationState } from './visualization';
|
||||
import { DimensionEditor } from './dimension_editor';
|
||||
import { DimensionEditor, SupportingVisType } from './dimension_editor';
|
||||
import { HTMLAttributes, mount, ReactWrapper, shallow } from 'enzyme';
|
||||
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||
import { EuiButtonGroup, EuiColorPicker } from '@elastic/eui';
|
||||
import { EuiButtonGroup, EuiColorPicker, PropsOf } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { LayoutDirection } from '@elastic/charts';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
@ -24,6 +24,8 @@ import { createMockFramePublicAPI } from '../../mocks';
|
|||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import { DebouncedInput } from '../../shared_components/debounced_input';
|
||||
import { DatasourcePublicAPI } from '../..';
|
||||
import { CollapseFunction } from '../../../common/expressions';
|
||||
|
||||
jest.mock('lodash', () => {
|
||||
const original = jest.requireActual('lodash');
|
||||
|
@ -37,9 +39,14 @@ jest.mock('lodash', () => {
|
|||
const SELECTORS = {
|
||||
PRIMARY_METRIC_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_primary_metric"]',
|
||||
SECONDARY_METRIC_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_secondary_metric"]',
|
||||
MAX_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_maximum"]',
|
||||
BREAKDOWN_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_breakdown"]',
|
||||
};
|
||||
|
||||
// see https://github.com/facebook/jest/issues/4402#issuecomment-534516219
|
||||
const expectCalledBefore = (mock1: jest.Mock, mock2: jest.Mock) =>
|
||||
expect(mock1.mock.invocationCallOrder[0]).toBeLessThan(mock2.mock.invocationCallOrder[0]);
|
||||
|
||||
describe('dimension editor', () => {
|
||||
const palette: PaletteOutput<CustomPaletteParams> = {
|
||||
type: 'palette',
|
||||
|
@ -63,6 +70,13 @@ describe('dimension editor', () => {
|
|||
maxCols: 5,
|
||||
color: 'static-color',
|
||||
palette,
|
||||
showBar: true,
|
||||
trendlineLayerId: 'second',
|
||||
trendlineLayerType: 'metricTrendline',
|
||||
trendlineMetricAccessor: 'trendline-metric-col-id',
|
||||
trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-accessor',
|
||||
trendlineTimeAccessor: 'trendline-time-col-id',
|
||||
trendlineBreakdownByAccessor: 'trendline-breakdown-col-id',
|
||||
};
|
||||
|
||||
let props: VisualizationDimensionEditorProps<MetricVisualizationState> & {
|
||||
|
@ -75,6 +89,14 @@ describe('dimension editor', () => {
|
|||
groupId: 'some-group',
|
||||
accessor: 'some-accessor',
|
||||
state: fullState,
|
||||
datasource: {
|
||||
hasDefaultTimeField: jest.fn(),
|
||||
getOperationForColumnId: jest.fn(() => ({
|
||||
hasReducedTimeRange: false,
|
||||
})),
|
||||
} as unknown as DatasourcePublicAPI,
|
||||
removeLayer: jest.fn(),
|
||||
addLayer: jest.fn(),
|
||||
frame: createMockFramePublicAPI(),
|
||||
setState: jest.fn(),
|
||||
panelRef: {} as React.MutableRefObject<HTMLDivElement | null>,
|
||||
|
@ -82,8 +104,11 @@ describe('dimension editor', () => {
|
|||
};
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('primary metric dimension', () => {
|
||||
const accessor = 'primary-metric-col-id';
|
||||
const metricAccessorState = { ...fullState, metricAccessor: accessor };
|
||||
|
||||
beforeEach(() => {
|
||||
props.frame.activeData = {
|
||||
|
@ -129,16 +154,53 @@ describe('dimension editor', () => {
|
|||
this.colorPicker.props().onChange!(color, {} as EuiColorPickerOutput);
|
||||
});
|
||||
}
|
||||
|
||||
private get supportingVisButtonGroup() {
|
||||
return this._wrapper.find(
|
||||
'EuiButtonGroup[data-test-subj="lnsMetric_supporting_visualization_buttons"]'
|
||||
) as unknown as ReactWrapper<PropsOf<typeof EuiButtonGroup>>;
|
||||
}
|
||||
|
||||
public get currentSupportingVis() {
|
||||
return this.supportingVisButtonGroup
|
||||
.props()
|
||||
.idSelected?.split('--')[1] as SupportingVisType;
|
||||
}
|
||||
|
||||
public isDisabled(type: SupportingVisType) {
|
||||
return this.supportingVisButtonGroup.props().options.find(({ id }) => id.includes(type))
|
||||
?.isDisabled;
|
||||
}
|
||||
|
||||
public setSupportingVis(type: SupportingVisType) {
|
||||
this.supportingVisButtonGroup.props().onChange(`some-id--${type}`);
|
||||
}
|
||||
|
||||
private get progressDirectionControl() {
|
||||
return this._wrapper.find(
|
||||
'EuiButtonGroup[data-test-subj="lnsMetric_progress_direction_buttons"]'
|
||||
) as unknown as ReactWrapper<PropsOf<typeof EuiButtonGroup>>;
|
||||
}
|
||||
|
||||
public get progressDirectionShowing() {
|
||||
return this.progressDirectionControl.exists();
|
||||
}
|
||||
|
||||
public setProgressDirection(direction: LayoutDirection) {
|
||||
this.progressDirectionControl.props().onChange(direction);
|
||||
this._wrapper.update();
|
||||
}
|
||||
}
|
||||
|
||||
const mockSetState = jest.fn();
|
||||
|
||||
const getHarnessWithState = (state: MetricVisualizationState) =>
|
||||
const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) =>
|
||||
new Harness(
|
||||
mountWithIntl(
|
||||
<DimensionEditor
|
||||
{...props}
|
||||
state={{ ...state, metricAccessor: accessor }}
|
||||
datasource={datasource}
|
||||
state={state}
|
||||
setState={mockSetState}
|
||||
accessor={accessor}
|
||||
/>
|
||||
|
@ -156,21 +218,25 @@ describe('dimension editor', () => {
|
|||
|
||||
expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeTruthy();
|
||||
expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.MAX_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('static color controls', () => {
|
||||
it('is hidden when dynamic coloring is enabled', () => {
|
||||
const harnessWithPalette = getHarnessWithState({ ...fullState, palette });
|
||||
const harnessWithPalette = getHarnessWithState({ ...metricAccessorState, palette });
|
||||
expect(harnessWithPalette.colorPicker.exists()).toBeFalsy();
|
||||
|
||||
const harnessNoPalette = getHarnessWithState({ ...fullState, palette: undefined });
|
||||
const harnessNoPalette = getHarnessWithState({
|
||||
...metricAccessorState,
|
||||
palette: undefined,
|
||||
});
|
||||
expect(harnessNoPalette.colorPicker.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fills with default value', () => {
|
||||
const localHarness = getHarnessWithState({
|
||||
...fullState,
|
||||
...metricAccessorState,
|
||||
palette: undefined,
|
||||
color: undefined,
|
||||
});
|
||||
|
@ -179,7 +245,7 @@ describe('dimension editor', () => {
|
|||
|
||||
it('sets color', () => {
|
||||
const localHarness = getHarnessWithState({
|
||||
...fullState,
|
||||
...metricAccessorState,
|
||||
palette: undefined,
|
||||
color: 'some-color',
|
||||
});
|
||||
|
@ -200,6 +266,144 @@ describe('dimension editor', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supporting visualizations', () => {
|
||||
const stateWOTrend = {
|
||||
...metricAccessorState,
|
||||
trendlineLayerId: undefined,
|
||||
};
|
||||
|
||||
describe('reflecting visualization state', () => {
|
||||
it('should select the correct button', () => {
|
||||
expect(
|
||||
getHarnessWithState({ ...stateWOTrend, showBar: false, maxAccessor: undefined })
|
||||
.currentSupportingVis
|
||||
).toBe<SupportingVisType>('none');
|
||||
expect(
|
||||
getHarnessWithState({ ...stateWOTrend, showBar: true }).currentSupportingVis
|
||||
).toBe<SupportingVisType>('bar');
|
||||
expect(
|
||||
getHarnessWithState(metricAccessorState).currentSupportingVis
|
||||
).toBe<SupportingVisType>('trendline');
|
||||
});
|
||||
|
||||
it('should disable bar when no max dimension', () => {
|
||||
expect(
|
||||
getHarnessWithState({
|
||||
...stateWOTrend,
|
||||
showBar: false,
|
||||
maxAccessor: 'something',
|
||||
}).isDisabled('bar')
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
getHarnessWithState({
|
||||
...stateWOTrend,
|
||||
showBar: false,
|
||||
maxAccessor: undefined,
|
||||
}).isDisabled('bar')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should disable trendline when no default time field', () => {
|
||||
expect(
|
||||
getHarnessWithState(stateWOTrend, {
|
||||
hasDefaultTimeField: () => false,
|
||||
getOperationForColumnId: (id) => ({} as OperationDescriptor),
|
||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
getHarnessWithState(stateWOTrend, {
|
||||
hasDefaultTimeField: () => true,
|
||||
getOperationForColumnId: (id) => ({} as OperationDescriptor),
|
||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable trendline when a metric dimension has a reduced time range', () => {
|
||||
expect(
|
||||
getHarnessWithState(stateWOTrend, {
|
||||
hasDefaultTimeField: () => true,
|
||||
getOperationForColumnId: (id) =>
|
||||
({ hasReducedTimeRange: id === stateWOTrend.metricAccessor } as OperationDescriptor),
|
||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
getHarnessWithState(stateWOTrend, {
|
||||
hasDefaultTimeField: () => true,
|
||||
getOperationForColumnId: (id) =>
|
||||
({
|
||||
hasReducedTimeRange: id === stateWOTrend.secondaryMetricAccessor,
|
||||
} as OperationDescriptor),
|
||||
} as DatasourcePublicAPI).isDisabled('trendline')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('responding to buttons', () => {
|
||||
it('enables trendline', () => {
|
||||
getHarnessWithState(stateWOTrend).setSupportingVis('trendline');
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false });
|
||||
expect(props.addLayer).toHaveBeenCalledWith('metricTrendline');
|
||||
|
||||
expectCalledBefore(mockSetState, props.addLayer as jest.Mock);
|
||||
});
|
||||
|
||||
it('enables bar', () => {
|
||||
getHarnessWithState(metricAccessorState).setSupportingVis('bar');
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: true });
|
||||
expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId);
|
||||
|
||||
expectCalledBefore(mockSetState, props.removeLayer as jest.Mock);
|
||||
});
|
||||
|
||||
it('selects none from bar', () => {
|
||||
getHarnessWithState(stateWOTrend).setSupportingVis('none');
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ ...stateWOTrend, showBar: false });
|
||||
expect(props.removeLayer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selects none from trendline', () => {
|
||||
getHarnessWithState(metricAccessorState).setSupportingVis('none');
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ ...metricAccessorState, showBar: false });
|
||||
expect(props.removeLayer).toHaveBeenCalledWith(metricAccessorState.trendlineLayerId);
|
||||
|
||||
expectCalledBefore(mockSetState, props.removeLayer as jest.Mock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('progress bar direction controls', () => {
|
||||
it('hides direction controls if bar not showing', () => {
|
||||
expect(
|
||||
getHarnessWithState({ ...stateWOTrend, showBar: false }).progressDirectionShowing
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('toggles progress direction', () => {
|
||||
const harness = getHarnessWithState(metricAccessorState);
|
||||
|
||||
expect(harness.progressDirectionShowing).toBeTruthy();
|
||||
expect(harness.currentState.progressDirection).toBe('vertical');
|
||||
|
||||
harness.setProgressDirection('horizontal');
|
||||
harness.setProgressDirection('vertical');
|
||||
harness.setProgressDirection('horizontal');
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledTimes(3);
|
||||
expect(mockSetState.mock.calls.map((args) => args[0].progressDirection))
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
"horizontal",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('secondary metric dimension', () => {
|
||||
|
@ -234,6 +438,7 @@ describe('dimension editor', () => {
|
|||
|
||||
expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeTruthy();
|
||||
expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.MAX_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -317,81 +522,21 @@ describe('dimension editor', () => {
|
|||
});
|
||||
|
||||
describe('maximum dimension', () => {
|
||||
const accessor = 'maximum-col-id';
|
||||
class Harness {
|
||||
public _wrapper;
|
||||
const accessor = 'max-col-id';
|
||||
|
||||
constructor(
|
||||
wrapper: ReactWrapper<HTMLAttributes, unknown, React.Component<{}, {}, unknown>>
|
||||
) {
|
||||
this._wrapper = wrapper;
|
||||
}
|
||||
|
||||
private get rootComponent() {
|
||||
return this._wrapper.find(DimensionEditor);
|
||||
}
|
||||
|
||||
private get progressDirectionControl() {
|
||||
return this._wrapper.find(EuiButtonGroup);
|
||||
}
|
||||
|
||||
public get currentState() {
|
||||
return this.rootComponent.props().state;
|
||||
}
|
||||
|
||||
public setProgressDirection(direction: LayoutDirection) {
|
||||
this.progressDirectionControl.props().onChange(direction);
|
||||
this._wrapper.update();
|
||||
}
|
||||
|
||||
public get progressDirectionDisabled() {
|
||||
return this.progressDirectionControl.find(EuiButtonGroup).props().isDisabled;
|
||||
}
|
||||
|
||||
public setMaxCols(max: number) {
|
||||
act(() => {
|
||||
this._wrapper.find('EuiFieldNumber[data-test-subj="lnsMetric_max_cols"]').props()
|
||||
.onChange!({
|
||||
target: { value: String(max) },
|
||||
} as unknown as FormEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let harness: Harness;
|
||||
const mockSetState = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
harness = new Harness(
|
||||
mountWithIntl(
|
||||
<DimensionEditor
|
||||
{...props}
|
||||
state={{ ...fullState, maxAccessor: accessor }}
|
||||
accessor={accessor}
|
||||
setState={mockSetState}
|
||||
/>
|
||||
)
|
||||
it('renders when the accessor matches', () => {
|
||||
const component = shallow(
|
||||
<DimensionEditor
|
||||
{...props}
|
||||
state={{ ...fullState, maxAccessor: accessor }}
|
||||
accessor={accessor}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => mockSetState.mockClear());
|
||||
|
||||
it('toggles progress direction', () => {
|
||||
expect(harness.currentState.progressDirection).toBe('vertical');
|
||||
|
||||
harness.setProgressDirection('horizontal');
|
||||
harness.setProgressDirection('vertical');
|
||||
harness.setProgressDirection('horizontal');
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledTimes(3);
|
||||
expect(mockSetState.mock.calls.map((args) => args[0].progressDirection))
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
"horizontal",
|
||||
]
|
||||
`);
|
||||
expect(component.exists(SELECTORS.MAX_EDITOR)).toBeTruthy();
|
||||
expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -415,7 +560,7 @@ describe('dimension editor', () => {
|
|||
return this.collapseSetting.props().value;
|
||||
}
|
||||
|
||||
public setCollapseFn(fn: string) {
|
||||
public setCollapseFn(fn: CollapseFunction) {
|
||||
return this.collapseSetting.props().onChange(fn);
|
||||
}
|
||||
|
||||
|
@ -458,6 +603,7 @@ describe('dimension editor', () => {
|
|||
|
||||
expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeTruthy();
|
||||
expect(component.exists(SELECTORS.SECONDARY_METRIC_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.MAX_EDITOR)).toBeFalsy();
|
||||
expect(component.exists(SELECTORS.PRIMARY_METRIC_EDITOR)).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import {
|
|||
DEFAULT_MIN_STOP,
|
||||
} from '@kbn/coloring';
|
||||
import { getDataBoundsForPalette } from '@kbn/expression-metric-vis-plugin/public';
|
||||
import { css } from '@emotion/react';
|
||||
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
|
||||
import {
|
||||
|
@ -39,10 +38,17 @@ import {
|
|||
} from '../../shared_components';
|
||||
import type { VisualizationDimensionEditorProps } from '../../types';
|
||||
import { defaultNumberPaletteParams, defaultPercentagePaletteParams } from './palette_config';
|
||||
import { DEFAULT_MAX_COLUMNS, getDefaultColor, MetricVisualizationState } from './visualization';
|
||||
import {
|
||||
DEFAULT_MAX_COLUMNS,
|
||||
getDefaultColor,
|
||||
MetricVisualizationState,
|
||||
showingBar,
|
||||
} from './visualization';
|
||||
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||
import { DebouncedInput } from '../../shared_components/debounced_input';
|
||||
|
||||
export type SupportingVisType = 'none' | 'bar' | 'trendline';
|
||||
|
||||
type Props = VisualizationDimensionEditorProps<MetricVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
};
|
||||
|
@ -117,7 +123,7 @@ function BreakdownByEditor({ setState, state }: SubProps) {
|
|||
</EuiFormRow>
|
||||
<CollapseSetting
|
||||
value={state.collapseFn || ''}
|
||||
onChange={(collapseFn: string) => {
|
||||
onChange={(collapseFn) => {
|
||||
setState({
|
||||
...state,
|
||||
collapseFn,
|
||||
|
@ -129,49 +135,7 @@ function BreakdownByEditor({ setState, state }: SubProps) {
|
|||
}
|
||||
|
||||
function MaximumEditor({ setState, state, idPrefix }: SubProps) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
||||
defaultMessage: 'Bar direction',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
||||
defaultMessage: 'Bar direction',
|
||||
})}
|
||||
data-test-subj="lnsMetric_progress_direction_buttons"
|
||||
name="alignment"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}vertical`,
|
||||
label: i18n.translate('xpack.lens.metric.progressDirection.vertical', {
|
||||
defaultMessage: 'Vertical',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_progress_bar_vertical',
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}horizontal`,
|
||||
label: i18n.translate('xpack.lens.metric.progressDirection.horizontal', {
|
||||
defaultMessage: 'Horizontal',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_progress_bar_horizontal',
|
||||
},
|
||||
]}
|
||||
idSelected={`${idPrefix}${state.progressDirection ?? 'vertical'}`}
|
||||
onChange={(id) => {
|
||||
const newDirection = id.replace(idPrefix, '') as LayoutDirection;
|
||||
setState({
|
||||
...state,
|
||||
progressDirection: newDirection,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
function SecondaryMetricEditor({ accessor, idPrefix, frame, layerId, setState, state }: SubProps) {
|
||||
|
@ -299,17 +263,176 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
|
||||
const togglePalette = () => setIsPaletteOpen(!isPaletteOpen);
|
||||
|
||||
const supportingVisLabel = i18n.translate('xpack.lens.metric.supportingVis.label', {
|
||||
defaultMessage: 'Supporting visualization',
|
||||
});
|
||||
|
||||
const hasDefaultTimeField = props.datasource?.hasDefaultTimeField();
|
||||
const metricHasReducedTimeRange = Boolean(
|
||||
state.metricAccessor &&
|
||||
props.datasource?.getOperationForColumnId(state.metricAccessor)?.hasReducedTimeRange
|
||||
);
|
||||
const secondaryMetricHasReducedTimeRange = Boolean(
|
||||
state.secondaryMetricAccessor &&
|
||||
props.datasource?.getOperationForColumnId(state.secondaryMetricAccessor)?.hasReducedTimeRange
|
||||
);
|
||||
|
||||
const supportingVisHelpTexts: string[] = [];
|
||||
|
||||
const supportsTrendline =
|
||||
hasDefaultTimeField && !metricHasReducedTimeRange && !secondaryMetricHasReducedTimeRange;
|
||||
|
||||
if (!supportsTrendline) {
|
||||
supportingVisHelpTexts.push(
|
||||
!hasDefaultTimeField
|
||||
? i18n.translate('xpack.lens.metric.supportingVis.needDefaultTimeField', {
|
||||
defaultMessage: 'Use a data view with a default time field to enable trend lines.',
|
||||
})
|
||||
: metricHasReducedTimeRange
|
||||
? i18n.translate('xpack.lens.metric.supportingVis.metricHasReducedTimeRange', {
|
||||
defaultMessage:
|
||||
'Remove the reduced time range on this dimension to enable trend lines.',
|
||||
})
|
||||
: secondaryMetricHasReducedTimeRange
|
||||
? i18n.translate('xpack.lens.metric.supportingVis.secondaryMetricHasReducedTimeRange', {
|
||||
defaultMessage:
|
||||
'Remove the reduced time range on the secondary metric dimension to enable trend lines.',
|
||||
})
|
||||
: ''
|
||||
);
|
||||
}
|
||||
|
||||
if (!state.maxAccessor) {
|
||||
supportingVisHelpTexts.push(
|
||||
i18n.translate('xpack.lens.metric.summportingVis.needMaxDimension', {
|
||||
defaultMessage: 'Add a maximum dimension to enable the progress bar.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const buttonIdPrefix = `${idPrefix}--`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={supportingVisLabel}
|
||||
helpText={supportingVisHelpTexts.map((text) => (
|
||||
<div>{text}</div>
|
||||
))}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={supportingVisLabel}
|
||||
data-test-subj="lnsMetric_supporting_visualization_buttons"
|
||||
options={[
|
||||
{
|
||||
id: `${buttonIdPrefix}none`,
|
||||
label: i18n.translate('xpack.lens.metric.supportingVisualization.none', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_supporting_visualization_none',
|
||||
},
|
||||
{
|
||||
id: `${buttonIdPrefix}trendline`,
|
||||
label: i18n.translate('xpack.lens.metric.supportingVisualization.trendline', {
|
||||
defaultMessage: 'Trend line',
|
||||
}),
|
||||
isDisabled: !supportsTrendline,
|
||||
'data-test-subj': 'lnsMetric_supporting_visualization_trendline',
|
||||
},
|
||||
{
|
||||
id: `${buttonIdPrefix}bar`,
|
||||
label: i18n.translate('xpack.lens.metric.supportingVisualization.bar', {
|
||||
defaultMessage: 'Bar',
|
||||
}),
|
||||
isDisabled: !state.maxAccessor,
|
||||
'data-test-subj': 'lnsMetric_supporting_visualization_bar',
|
||||
},
|
||||
]}
|
||||
idSelected={`${buttonIdPrefix}${
|
||||
state.trendlineLayerId ? 'trendline' : showingBar(state) ? 'bar' : 'none'
|
||||
}`}
|
||||
onChange={(id) => {
|
||||
const supportingVisualizationType = id.split('--')[1] as SupportingVisType;
|
||||
|
||||
switch (supportingVisualizationType) {
|
||||
case 'trendline':
|
||||
setState({
|
||||
...state,
|
||||
showBar: false,
|
||||
});
|
||||
props.addLayer('metricTrendline');
|
||||
break;
|
||||
case 'bar':
|
||||
setState({
|
||||
...state,
|
||||
showBar: true,
|
||||
});
|
||||
if (state.trendlineLayerId) props.removeLayer(state.trendlineLayerId);
|
||||
break;
|
||||
case 'none':
|
||||
setState({
|
||||
...state,
|
||||
showBar: false,
|
||||
});
|
||||
if (state.trendlineLayerId) props.removeLayer(state.trendlineLayerId);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{showingBar(state) && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
||||
defaultMessage: 'Bar direction',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.progressDirectionLabel', {
|
||||
defaultMessage: 'Bar direction',
|
||||
})}
|
||||
data-test-subj="lnsMetric_progress_direction_buttons"
|
||||
name="alignment"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}vertical`,
|
||||
label: i18n.translate('xpack.lens.metric.progressDirection.vertical', {
|
||||
defaultMessage: 'Vertical',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_progress_bar_vertical',
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}horizontal`,
|
||||
label: i18n.translate('xpack.lens.metric.progressDirection.horizontal', {
|
||||
defaultMessage: 'Horizontal',
|
||||
}),
|
||||
'data-test-subj': 'lnsMetric_progress_bar_horizontal',
|
||||
},
|
||||
]}
|
||||
idSelected={`${idPrefix}${state.progressDirection ?? 'vertical'}`}
|
||||
onChange={(id) => {
|
||||
const newDirection = id.replace(idPrefix, '') as LayoutDirection;
|
||||
setState({
|
||||
...state,
|
||||
progressDirection: newDirection,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.metric.dynamicColoring.label', {
|
||||
defaultMessage: 'Color mode',
|
||||
})}
|
||||
css={css`
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
|
@ -362,62 +485,60 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
</EuiFormRow>
|
||||
{!hasDynamicColoring && <StaticColorControls {...props} />}
|
||||
{hasDynamicColoring && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
className="lnsDynamicColoringClickable"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
className="lnsDynamicColoringClickable"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiColorPaletteDisplay
|
||||
data-test-subj="lnsMetric_dynamicColoring_palette"
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
type={FIXED_PROGRESSION}
|
||||
onClick={togglePalette}
|
||||
<EuiFlexItem>
|
||||
<EuiColorPaletteDisplay
|
||||
data-test-subj="lnsMetric_dynamicColoring_palette"
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
type={FIXED_PROGRESSION}
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="lnsMetric_dynamicColoring_trigger"
|
||||
iconType="controlsHorizontal"
|
||||
onClick={togglePalette}
|
||||
size="xs"
|
||||
flush="both"
|
||||
>
|
||||
{i18n.translate('xpack.lens.paletteTableGradient.customize', {
|
||||
defaultMessage: 'Edit',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<PalettePanelContainer
|
||||
siblingRef={props.panelRef}
|
||||
isOpen={isPaletteOpen}
|
||||
handleClose={togglePalette}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
activePalette={activePalette}
|
||||
dataBounds={currentMinMax}
|
||||
showRangeTypeSelector={supportsPercentPalette}
|
||||
setPalette={(newPalette) => {
|
||||
setState({
|
||||
...state,
|
||||
palette: newPalette,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="lnsMetric_dynamicColoring_trigger"
|
||||
iconType="controlsHorizontal"
|
||||
onClick={togglePalette}
|
||||
size="xs"
|
||||
flush="both"
|
||||
>
|
||||
{i18n.translate('xpack.lens.paletteTableGradient.customize', {
|
||||
defaultMessage: 'Edit',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<PalettePanelContainer
|
||||
siblingRef={props.panelRef}
|
||||
isOpen={isPaletteOpen}
|
||||
handleClose={togglePalette}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
activePalette={activePalette}
|
||||
dataBounds={currentMinMax}
|
||||
showRangeTypeSelector={supportsPercentPalette}
|
||||
setPalette={(newPalette) => {
|
||||
setState({
|
||||
...state,
|
||||
palette: newPalette,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PalettePanelContainer>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
</PalettePanelContainer>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -439,7 +560,7 @@ function StaticColorControls({ state, setState }: Pick<Props, 'state' | 'setStat
|
|||
useDebouncedValue<string>(
|
||||
{
|
||||
onChange: setColor,
|
||||
value: state.color || getDefaultColor(!!state.maxAccessor),
|
||||
value: state.color || getDefaultColor(state),
|
||||
},
|
||||
{ allowFalsyValue: true }
|
||||
);
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 { CustomPaletteParams, CUSTOM_PALETTE, PaletteRegistry } from '@kbn/coloring';
|
||||
import {
|
||||
EXPRESSION_METRIC_NAME,
|
||||
EXPRESSION_METRIC_TRENDLINE_NAME,
|
||||
} from '@kbn/expression-metric-vis-plugin/public';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/common';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { CollapseArgs, CollapseFunction } from '../../../common/expressions';
|
||||
import { CollapseExpressionFunction } from '../../../common/expressions/collapse/types';
|
||||
import { DatasourceLayers } from '../../types';
|
||||
import { showingBar } from './metric_visualization';
|
||||
import { DEFAULT_MAX_COLUMNS, getDefaultColor, MetricVisualizationState } from './visualization';
|
||||
|
||||
// TODO - deduplicate with gauges?
|
||||
function computePaletteParams(params: CustomPaletteParams) {
|
||||
return {
|
||||
...params,
|
||||
// rewrite colors and stops as two distinct arguments
|
||||
colors: (params?.stops || []).map(({ color }) => color),
|
||||
stops: params?.name === 'custom' ? (params?.stops || []).map(({ stop }) => stop) : [],
|
||||
reverse: false, // managed at UI level
|
||||
};
|
||||
}
|
||||
|
||||
const getTrendlineExpression = (
|
||||
state: MetricVisualizationState,
|
||||
datasourceExpressionsByLayers: Record<string, Ast>
|
||||
): Ast | undefined => {
|
||||
if (!state.trendlineLayerId || !state.trendlineMetricAccessor || !state.trendlineTimeAccessor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const datasourceExpression = datasourceExpressionsByLayers[state.trendlineLayerId];
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: EXPRESSION_METRIC_TRENDLINE_NAME,
|
||||
arguments: {
|
||||
metric: [state.trendlineMetricAccessor],
|
||||
timeField: [state.trendlineTimeAccessor],
|
||||
breakdownBy:
|
||||
state.trendlineBreakdownByAccessor && !state.collapseFn
|
||||
? [state.trendlineBreakdownByAccessor]
|
||||
: [],
|
||||
inspectorTableId: [state.trendlineLayerId],
|
||||
...(datasourceExpression
|
||||
? {
|
||||
table: [
|
||||
{
|
||||
...datasourceExpression,
|
||||
chain: [
|
||||
...datasourceExpression.chain,
|
||||
...(state.collapseFn
|
||||
? [
|
||||
buildExpressionFunction<CollapseExpressionFunction>('lens_collapse', {
|
||||
by: [state.trendlineTimeAccessor],
|
||||
metric: [state.trendlineMetricAccessor],
|
||||
fn: [state.collapseFn],
|
||||
}).toAst(),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const toExpression = (
|
||||
paletteService: PaletteRegistry,
|
||||
state: MetricVisualizationState,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
datasourceExpressionsByLayers: Record<string, Ast> | undefined = {}
|
||||
): Ast | null => {
|
||||
if (!state.metricAccessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const datasource = datasourceLayers[state.layerId];
|
||||
const datasourceExpression = datasourceExpressionsByLayers[state.layerId];
|
||||
|
||||
const maxPossibleTiles =
|
||||
// if there's a collapse function, no need to calculate since we're dealing with a single tile
|
||||
state.breakdownByAccessor && !state.collapseFn
|
||||
? datasource?.getMaxPossibleNumValues(state.breakdownByAccessor)
|
||||
: null;
|
||||
|
||||
const getCollapseFnArguments = (): CollapseArgs => {
|
||||
const metric = [state.metricAccessor, state.secondaryMetricAccessor, state.maxAccessor].filter(
|
||||
Boolean
|
||||
) as string[];
|
||||
|
||||
const collapseFn = state.collapseFn as CollapseFunction;
|
||||
|
||||
const fn = metric.map((accessor) => {
|
||||
if (accessor !== state.maxAccessor) {
|
||||
return collapseFn;
|
||||
} else {
|
||||
const isMaxStatic = Boolean(
|
||||
datasource?.getOperationForColumnId(state.maxAccessor!)?.isStaticValue
|
||||
);
|
||||
// we do this because the user expects the static value they set to be the same
|
||||
// even if they define a collapse on the breakdown by
|
||||
return isMaxStatic ? 'max' : collapseFn;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
by: [],
|
||||
metric,
|
||||
fn,
|
||||
};
|
||||
};
|
||||
|
||||
const collapseExpressionFunction = state.collapseFn
|
||||
? buildExpressionFunction<CollapseExpressionFunction>(
|
||||
'lens_collapse',
|
||||
getCollapseFnArguments()
|
||||
).toAst()
|
||||
: undefined;
|
||||
|
||||
const trendlineExpression = getTrendlineExpression(state, datasourceExpressionsByLayers);
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
...(datasourceExpression?.chain ?? []),
|
||||
...(collapseExpressionFunction ? [collapseExpressionFunction] : []),
|
||||
{
|
||||
type: 'function',
|
||||
function: EXPRESSION_METRIC_NAME,
|
||||
arguments: {
|
||||
metric: state.metricAccessor ? [state.metricAccessor] : [],
|
||||
secondaryMetric: state.secondaryMetricAccessor ? [state.secondaryMetricAccessor] : [],
|
||||
secondaryPrefix:
|
||||
typeof state.secondaryPrefix !== 'undefined' ? [state.secondaryPrefix] : [],
|
||||
max: showingBar(state) ? [state.maxAccessor] : [],
|
||||
breakdownBy:
|
||||
state.breakdownByAccessor && !state.collapseFn ? [state.breakdownByAccessor] : [],
|
||||
trendline: trendlineExpression ? [trendlineExpression] : [],
|
||||
subtitle: state.subtitle ? [state.subtitle] : [],
|
||||
progressDirection: state.progressDirection ? [state.progressDirection] : [],
|
||||
color: [state.color || getDefaultColor(state)],
|
||||
palette: state.palette?.params
|
||||
? [
|
||||
paletteService
|
||||
.get(CUSTOM_PALETTE)
|
||||
.toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)),
|
||||
]
|
||||
: [],
|
||||
maxCols: [state.maxCols ?? DEFAULT_MAX_COLUMNS],
|
||||
minTiles: maxPossibleTiles ? [maxPossibleTiles] : [],
|
||||
inspectorTableId: [state.layerId],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
|
@ -48,6 +48,13 @@ describe('metric toolbar', () => {
|
|||
maxCols: 5,
|
||||
color: 'static-color',
|
||||
palette,
|
||||
showBar: true,
|
||||
trendlineLayerId: 'second',
|
||||
trendlineLayerType: 'metricTrendline',
|
||||
trendlineMetricAccessor: 'trendline-metric-col-id',
|
||||
trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id',
|
||||
trendlineTimeAccessor: 'trendline-time-col-id',
|
||||
trendlineBreakdownByAccessor: 'trendline-breakdown-col-id',
|
||||
};
|
||||
|
||||
const frame = createMockFramePublicAPI();
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { GROUP_ID } from './constants';
|
||||
import { getMetricVisualization, MetricVisualizationState } from './visualization';
|
||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
|
||||
const paletteService = chartPluginMock.createPaletteRegistry();
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
|
@ -39,7 +40,26 @@ describe('metric visualization', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const fullState: Required<MetricVisualizationState> = {
|
||||
const trendlineProps = {
|
||||
trendlineLayerId: 'second',
|
||||
trendlineLayerType: 'metricTrendline',
|
||||
trendlineMetricAccessor: 'trendline-metric-col-id',
|
||||
trendlineSecondaryMetricAccessor: 'trendline-secondary-metric-col-id',
|
||||
trendlineTimeAccessor: 'trendline-time-col-id',
|
||||
trendlineBreakdownByAccessor: 'trendline-breakdown-col-id',
|
||||
} as const;
|
||||
|
||||
const fullState: Required<
|
||||
Omit<
|
||||
MetricVisualizationState,
|
||||
| 'trendlineLayerId'
|
||||
| 'trendlineLayerType'
|
||||
| 'trendlineMetricAccessor'
|
||||
| 'trendlineSecondaryMetricAccessor'
|
||||
| 'trendlineTimeAccessor'
|
||||
| 'trendlineBreakdownByAccessor'
|
||||
>
|
||||
> = {
|
||||
layerId: 'first',
|
||||
layerType: 'data',
|
||||
metricAccessor: 'metric-col-id',
|
||||
|
@ -53,6 +73,12 @@ describe('metric visualization', () => {
|
|||
maxCols: 5,
|
||||
color: 'static-color',
|
||||
palette,
|
||||
showBar: false,
|
||||
};
|
||||
|
||||
const fullStateWTrend: Required<MetricVisualizationState> = {
|
||||
...fullState,
|
||||
...trendlineProps,
|
||||
};
|
||||
|
||||
const mockFrameApi = createMockFramePublicAPI();
|
||||
|
@ -71,149 +97,163 @@ describe('metric visualization', () => {
|
|||
});
|
||||
|
||||
describe('dimension groups configuration', () => {
|
||||
test('generates configuration', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('color-by-value', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "metric-col-id",
|
||||
"palette": Array [],
|
||||
"triggerIcon": "colorBy",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, palette: undefined, color: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "#0077cc",
|
||||
"columnId": "metric-col-id",
|
||||
"triggerIcon": "color",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('static coloring', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, palette: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "static-color",
|
||||
"columnId": "metric-col-id",
|
||||
"triggerIcon": "color",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, color: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "metric-col-id",
|
||||
"palette": Array [],
|
||||
"triggerIcon": "colorBy",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('collapse function', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[3].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "breakdown-col-id",
|
||||
"triggerIcon": "aggregate",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, collapseFn: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[3].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "breakdown-col-id",
|
||||
"triggerIcon": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('operation filtering', () => {
|
||||
const unsupportedDataType = 'string';
|
||||
|
||||
const operations: OperationMetadata[] = [
|
||||
{
|
||||
isBucketed: true,
|
||||
dataType: 'number',
|
||||
},
|
||||
{
|
||||
isBucketed: true,
|
||||
dataType: unsupportedDataType,
|
||||
},
|
||||
{
|
||||
isBucketed: false,
|
||||
dataType: 'number',
|
||||
},
|
||||
{
|
||||
isBucketed: false,
|
||||
dataType: unsupportedDataType,
|
||||
},
|
||||
];
|
||||
|
||||
const testConfig = visualization
|
||||
.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
.groups.map(({ groupId, filterOperations }) => [groupId, filterOperations]);
|
||||
|
||||
it.each(testConfig)('%s supports correct operations', (_, filterFn) => {
|
||||
describe('primary layer', () => {
|
||||
test('generates configuration', () => {
|
||||
expect(
|
||||
operations.filter(filterFn as (operation: OperationMetadata) => boolean)
|
||||
visualization.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('color-by-value', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "metric-col-id",
|
||||
"palette": Array [],
|
||||
"triggerIcon": "colorBy",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, palette: undefined, color: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "#f5f7fa",
|
||||
"columnId": "metric-col-id",
|
||||
"triggerIcon": "color",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('static coloring', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, palette: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "static-color",
|
||||
"columnId": "metric-col-id",
|
||||
"triggerIcon": "color",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, color: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[0].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "metric-col-id",
|
||||
"palette": Array [],
|
||||
"triggerIcon": "colorBy",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('collapse function', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[3].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "breakdown-col-id",
|
||||
"triggerIcon": "aggregate",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: { ...fullState, collapseFn: undefined },
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
}).groups[3].accessors
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "breakdown-col-id",
|
||||
"triggerIcon": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('operation filtering', () => {
|
||||
const unsupportedDataType = 'string';
|
||||
|
||||
const operations: OperationMetadata[] = [
|
||||
{
|
||||
isBucketed: true,
|
||||
dataType: 'number',
|
||||
},
|
||||
{
|
||||
isBucketed: true,
|
||||
dataType: unsupportedDataType,
|
||||
},
|
||||
{
|
||||
isBucketed: false,
|
||||
dataType: 'number',
|
||||
},
|
||||
{
|
||||
isBucketed: false,
|
||||
dataType: unsupportedDataType,
|
||||
},
|
||||
];
|
||||
|
||||
const testConfig = visualization
|
||||
.getConfiguration({
|
||||
state: fullState,
|
||||
layerId: fullState.layerId,
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
.groups.map(({ groupId, filterOperations }) => [groupId, filterOperations]);
|
||||
|
||||
it.each(testConfig)('%s supports correct operations', (_, filterFn) => {
|
||||
expect(
|
||||
operations.filter(filterFn as (operation: OperationMetadata) => boolean)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trendline layer', () => {
|
||||
test('generates configuration', () => {
|
||||
expect(
|
||||
visualization.getConfiguration({
|
||||
state: fullStateWTrend,
|
||||
layerId: fullStateWTrend.trendlineLayerId,
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -231,6 +271,7 @@ describe('metric visualization', () => {
|
|||
|
||||
datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
second: mockDatasource.publicAPIMock,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -263,9 +304,10 @@ describe('metric visualization', () => {
|
|||
"color": Array [
|
||||
"static-color",
|
||||
],
|
||||
"max": Array [
|
||||
"max-metric-col-id",
|
||||
"inspectorTableId": Array [
|
||||
"first",
|
||||
],
|
||||
"max": Array [],
|
||||
"maxCols": Array [
|
||||
5,
|
||||
],
|
||||
|
@ -301,6 +343,7 @@ describe('metric visualization', () => {
|
|||
"subtitle": Array [
|
||||
"subtitle",
|
||||
],
|
||||
"trendline": Array [],
|
||||
},
|
||||
"function": "metricVis",
|
||||
"type": "function",
|
||||
|
@ -324,9 +367,10 @@ describe('metric visualization', () => {
|
|||
"color": Array [
|
||||
"static-color",
|
||||
],
|
||||
"max": Array [
|
||||
"max-metric-col-id",
|
||||
"inspectorTableId": Array [
|
||||
"first",
|
||||
],
|
||||
"max": Array [],
|
||||
"maxCols": Array [
|
||||
5,
|
||||
],
|
||||
|
@ -364,6 +408,7 @@ describe('metric visualization', () => {
|
|||
"subtitle": Array [
|
||||
"subtitle",
|
||||
],
|
||||
"trendline": Array [],
|
||||
},
|
||||
"function": "metricVis",
|
||||
"type": "function",
|
||||
|
@ -374,6 +419,165 @@ describe('metric visualization', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
describe('trendline expression', () => {
|
||||
const getTrendlineExpression = (state: MetricVisualizationState) => {
|
||||
const expression = visualization.toExpression(
|
||||
state,
|
||||
datasourceLayers,
|
||||
{},
|
||||
{
|
||||
[trendlineProps.trendlineLayerId]: { chain: [] } as unknown as Ast,
|
||||
}
|
||||
) as ExpressionAstExpression;
|
||||
|
||||
return expression.chain!.find((fn) => fn.function === 'metricVis')!.arguments.trendline[0];
|
||||
};
|
||||
|
||||
it('adds trendline if prerequisites are present', () => {
|
||||
expect(getTrendlineExpression({ ...fullStateWTrend, collapseFn: undefined }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"breakdownBy": Array [
|
||||
"trendline-breakdown-col-id",
|
||||
],
|
||||
"inspectorTableId": Array [
|
||||
"second",
|
||||
],
|
||||
"metric": Array [
|
||||
"trendline-metric-col-id",
|
||||
],
|
||||
"table": Array [
|
||||
Object {
|
||||
"chain": Array [],
|
||||
},
|
||||
],
|
||||
"timeField": Array [
|
||||
"trendline-time-col-id",
|
||||
],
|
||||
},
|
||||
"function": "metricTrendline",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
getTrendlineExpression({ ...fullStateWTrend, trendlineBreakdownByAccessor: undefined })
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"breakdownBy": Array [],
|
||||
"inspectorTableId": Array [
|
||||
"second",
|
||||
],
|
||||
"metric": Array [
|
||||
"trendline-metric-col-id",
|
||||
],
|
||||
"table": Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"by": Array [
|
||||
"trendline-time-col-id",
|
||||
],
|
||||
"fn": Array [
|
||||
"sum",
|
||||
],
|
||||
"metric": Array [
|
||||
"trendline-metric-col-id",
|
||||
],
|
||||
},
|
||||
"function": "lens_collapse",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"timeField": Array [
|
||||
"trendline-time-col-id",
|
||||
],
|
||||
},
|
||||
"function": "metricTrendline",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should apply collapse-by fn', () => {
|
||||
expect(getTrendlineExpression(fullStateWTrend)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"breakdownBy": Array [],
|
||||
"inspectorTableId": Array [
|
||||
"second",
|
||||
],
|
||||
"metric": Array [
|
||||
"trendline-metric-col-id",
|
||||
],
|
||||
"table": Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"by": Array [
|
||||
"trendline-time-col-id",
|
||||
],
|
||||
"fn": Array [
|
||||
"sum",
|
||||
],
|
||||
"metric": Array [
|
||||
"trendline-metric-col-id",
|
||||
],
|
||||
},
|
||||
"function": "lens_collapse",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"timeField": Array [
|
||||
"trendline-time-col-id",
|
||||
],
|
||||
},
|
||||
"function": "metricTrendline",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('no trendline if no trendline layer', () => {
|
||||
expect(
|
||||
getTrendlineExpression({ ...fullStateWTrend, trendlineLayerId: undefined })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('no trendline if either metric or timefield are missing', () => {
|
||||
expect(
|
||||
getTrendlineExpression({ ...fullStateWTrend, trendlineMetricAccessor: undefined })
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
getTrendlineExpression({ ...fullStateWTrend, trendlineTimeAccessor: undefined })
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with collapse function', () => {
|
||||
it('builds breakdown by metric with collapse function', () => {
|
||||
const ast = visualization.toExpression(
|
||||
|
@ -491,6 +695,7 @@ describe('metric visualization', () => {
|
|||
visualization.toExpression(
|
||||
{
|
||||
...fullState,
|
||||
showBar: true,
|
||||
color: undefined,
|
||||
},
|
||||
datasourceLayers
|
||||
|
@ -498,12 +703,42 @@ describe('metric visualization', () => {
|
|||
).chain[1].arguments.color[0]
|
||||
).toBe(euiLightVars.euiColorPrimary);
|
||||
|
||||
expect(
|
||||
(
|
||||
visualization.toExpression(
|
||||
{
|
||||
...fullState,
|
||||
showBar: false,
|
||||
color: undefined,
|
||||
},
|
||||
datasourceLayers
|
||||
) as ExpressionAstExpression
|
||||
).chain[1].arguments.color[0]
|
||||
).toBe(euiLightVars.euiColorLightestShade);
|
||||
|
||||
expect(
|
||||
(
|
||||
visualization.toExpression(
|
||||
{
|
||||
...fullState,
|
||||
maxAccessor: undefined,
|
||||
showBar: false,
|
||||
color: undefined,
|
||||
},
|
||||
datasourceLayers
|
||||
) as ExpressionAstExpression
|
||||
).chain[1].arguments.color[0]
|
||||
).toBe(euiLightVars.euiColorLightestShade);
|
||||
|
||||
// this case isn't currently relevant because other parts of the code don't allow showBar to be
|
||||
// set when there isn't a max dimension but this test covers the branch anyhow
|
||||
expect(
|
||||
(
|
||||
visualization.toExpression(
|
||||
{
|
||||
...fullState,
|
||||
maxAccessor: undefined,
|
||||
showBar: true,
|
||||
color: undefined,
|
||||
},
|
||||
datasourceLayers
|
||||
|
@ -523,8 +758,141 @@ describe('metric visualization', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('getLayerIds returns the single layer ID', () => {
|
||||
it('appends a trendline layer', () => {
|
||||
const newLayerId = 'new-layer-id';
|
||||
const chk = visualization.appendLayer!(fullState, newLayerId, 'metricTrendline', '');
|
||||
expect(chk.trendlineLayerId).toBe(newLayerId);
|
||||
expect(chk.trendlineLayerType).toBe('metricTrendline');
|
||||
});
|
||||
|
||||
it('removes trendline layer', () => {
|
||||
const chk = visualization.removeLayer!(fullStateWTrend, fullStateWTrend.trendlineLayerId);
|
||||
expect(chk.trendlineLayerId).toBeUndefined();
|
||||
expect(chk.trendlineLayerType).toBeUndefined();
|
||||
expect(chk.trendlineMetricAccessor).toBeUndefined();
|
||||
expect(chk.trendlineTimeAccessor).toBeUndefined();
|
||||
expect(chk.trendlineBreakdownByAccessor).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getLayerIds', () => {
|
||||
expect(visualization.getLayerIds(fullState)).toEqual([fullState.layerId]);
|
||||
expect(visualization.getLayerIds(fullStateWTrend)).toEqual([
|
||||
fullStateWTrend.layerId,
|
||||
fullStateWTrend.trendlineLayerId,
|
||||
]);
|
||||
});
|
||||
|
||||
test('getLayersToLinkTo', () => {
|
||||
expect(
|
||||
visualization.getLayersToLinkTo!(fullStateWTrend, fullStateWTrend.trendlineLayerId)
|
||||
).toEqual([fullStateWTrend.layerId]);
|
||||
expect(visualization.getLayersToLinkTo!(fullStateWTrend, 'foo-id')).toEqual([]);
|
||||
});
|
||||
|
||||
describe('linked dimensions', () => {
|
||||
it('doesnt report links when no trendline layer', () => {
|
||||
expect(visualization.getLinkedDimensions!(fullState)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('links metrics when present on leader layer', () => {
|
||||
const localState: MetricVisualizationState = {
|
||||
...fullStateWTrend,
|
||||
breakdownByAccessor: undefined,
|
||||
secondaryMetricAccessor: undefined,
|
||||
};
|
||||
|
||||
expect(visualization.getLinkedDimensions!(localState)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"from": Object {
|
||||
"columnId": "metric-col-id",
|
||||
"groupId": "metric",
|
||||
"layerId": "first",
|
||||
},
|
||||
"to": Object {
|
||||
"columnId": "trendline-metric-col-id",
|
||||
"groupId": "trendMetric",
|
||||
"layerId": "second",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
const newColumnId = visualization.getLinkedDimensions!({
|
||||
...localState,
|
||||
trendlineMetricAccessor: undefined,
|
||||
})[0].to.columnId;
|
||||
expect(newColumnId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('links secondary metrics when present on leader layer', () => {
|
||||
const localState: MetricVisualizationState = {
|
||||
...fullStateWTrend,
|
||||
metricAccessor: undefined,
|
||||
breakdownByAccessor: undefined,
|
||||
};
|
||||
|
||||
expect(visualization.getLinkedDimensions!(localState)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"from": Object {
|
||||
"columnId": "secondary-metric-col-id",
|
||||
"groupId": "secondaryMetric",
|
||||
"layerId": "first",
|
||||
},
|
||||
"to": Object {
|
||||
"columnId": "trendline-secondary-metric-col-id",
|
||||
"groupId": "trendSecondaryMetric",
|
||||
"layerId": "second",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
const newColumnId = visualization.getLinkedDimensions!({
|
||||
...localState,
|
||||
trendlineSecondaryMetricAccessor: undefined,
|
||||
})[0].to.columnId;
|
||||
expect(newColumnId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('links breakdowns when present', () => {
|
||||
const localState: MetricVisualizationState = {
|
||||
...fullStateWTrend,
|
||||
metricAccessor: undefined,
|
||||
secondaryMetricAccessor: undefined,
|
||||
};
|
||||
|
||||
expect(visualization.getLinkedDimensions!(localState)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"from": Object {
|
||||
"columnId": "breakdown-col-id",
|
||||
"groupId": "breakdownBy",
|
||||
"layerId": "first",
|
||||
},
|
||||
"to": Object {
|
||||
"columnId": "trendline-breakdown-col-id",
|
||||
"groupId": "trendBreakdownBy",
|
||||
"layerId": "second",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
const newColumnId = visualization.getLinkedDimensions!({
|
||||
...localState,
|
||||
trendlineBreakdownByAccessor: undefined,
|
||||
})[0].to.columnId;
|
||||
expect(newColumnId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('marks trendline layer for removal on index pattern switch', () => {
|
||||
expect(visualization.getLayersToRemoveOnIndexPatternChange!(fullStateWTrend)).toEqual([
|
||||
fullStateWTrend.trendlineLayerId,
|
||||
]);
|
||||
expect(visualization.getLayersToRemoveOnIndexPatternChange!(fullState)).toEqual([]);
|
||||
});
|
||||
|
||||
it('gives a description', () => {
|
||||
|
@ -540,15 +908,32 @@ describe('metric visualization', () => {
|
|||
it('works without state', () => {
|
||||
const supportedLayers = visualization.getSupportedLayers();
|
||||
expect(supportedLayers[0].initialDimensions).toBeUndefined();
|
||||
expect(supportedLayers).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"initialDimensions": undefined,
|
||||
"label": "Visualization",
|
||||
"type": "data",
|
||||
},
|
||||
]
|
||||
expect(supportedLayers[0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"canAddViaMenu": true,
|
||||
"disabled": true,
|
||||
"initialDimensions": undefined,
|
||||
"label": "Visualization",
|
||||
"type": "data",
|
||||
}
|
||||
`);
|
||||
|
||||
expect({ ...supportedLayers[1], initialDimensions: undefined }).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"canAddViaMenu": true,
|
||||
"disabled": false,
|
||||
"initialDimensions": undefined,
|
||||
"label": "Trendline",
|
||||
"type": "metricTrendline",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(supportedLayers[1].initialDimensions).toHaveLength(1);
|
||||
expect(supportedLayers[1].initialDimensions![0]).toMatchObject({
|
||||
groupId: GROUP_ID.TREND_TIME,
|
||||
autoTimeField: true,
|
||||
columnId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('includes max static value dimension when state provided', () => {
|
||||
|
@ -563,52 +948,65 @@ describe('metric visualization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets dimensions', () => {
|
||||
describe('setting dimensions', () => {
|
||||
const state = {} as MetricVisualizationState;
|
||||
const columnId = 'col-id';
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: state,
|
||||
columnId,
|
||||
groupId: GROUP_ID.METRIC,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toEqual({
|
||||
metricAccessor: columnId,
|
||||
|
||||
const cases: Array<{
|
||||
groupId: typeof GROUP_ID[keyof typeof GROUP_ID];
|
||||
accessor: keyof MetricVisualizationState;
|
||||
}> = [
|
||||
{ groupId: GROUP_ID.METRIC, accessor: 'metricAccessor' },
|
||||
{ groupId: GROUP_ID.SECONDARY_METRIC, accessor: 'secondaryMetricAccessor' },
|
||||
{ groupId: GROUP_ID.MAX, accessor: 'maxAccessor' },
|
||||
{ groupId: GROUP_ID.BREAKDOWN_BY, accessor: 'breakdownByAccessor' },
|
||||
{ groupId: GROUP_ID.TREND_METRIC, accessor: 'trendlineMetricAccessor' },
|
||||
{ groupId: GROUP_ID.TREND_SECONDARY_METRIC, accessor: 'trendlineSecondaryMetricAccessor' },
|
||||
{ groupId: GROUP_ID.TREND_TIME, accessor: 'trendlineTimeAccessor' },
|
||||
{ groupId: GROUP_ID.TREND_BREAKDOWN_BY, accessor: 'trendlineBreakdownByAccessor' },
|
||||
];
|
||||
|
||||
it.each(cases)('sets %s', ({ groupId, accessor }) => {
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: state,
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
[accessor]: columnId,
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: state,
|
||||
columnId,
|
||||
groupId: GROUP_ID.SECONDARY_METRIC,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toEqual({
|
||||
secondaryMetricAccessor: columnId,
|
||||
|
||||
it('shows the progress bar when maximum dimension set', () => {
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: state,
|
||||
columnId,
|
||||
groupId: GROUP_ID.MAX,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toEqual({
|
||||
maxAccessor: columnId,
|
||||
showBar: true,
|
||||
});
|
||||
});
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: state,
|
||||
columnId,
|
||||
groupId: GROUP_ID.MAX,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toEqual({
|
||||
maxAccessor: columnId,
|
||||
});
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: state,
|
||||
columnId,
|
||||
groupId: GROUP_ID.BREAKDOWN_BY,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).toEqual({
|
||||
breakdownByAccessor: columnId,
|
||||
|
||||
it('does NOT show the progress bar when maximum dimension set when trendline enabled', () => {
|
||||
expect(
|
||||
visualization.setDimension({
|
||||
prevState: { ...state, ...trendlineProps },
|
||||
columnId,
|
||||
groupId: GROUP_ID.MAX,
|
||||
layerId: 'some-id',
|
||||
frame: mockFrameApi,
|
||||
})
|
||||
).not.toHaveProperty('showBar');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -619,7 +1017,7 @@ describe('metric visualization', () => {
|
|||
layerId: 'some-id',
|
||||
columnId: '',
|
||||
frame: mockFrameApi,
|
||||
prevState: fullState,
|
||||
prevState: fullStateWTrend,
|
||||
};
|
||||
|
||||
it('removes metric dimension', () => {
|
||||
|
@ -660,6 +1058,38 @@ describe('metric visualization', () => {
|
|||
expect(removed).not.toHaveProperty('collapseFn');
|
||||
expect(removed).not.toHaveProperty('maxCols');
|
||||
});
|
||||
it('removes trend time dimension', () => {
|
||||
const removed = visualization.removeDimension({
|
||||
...removeDimensionParam,
|
||||
columnId: fullStateWTrend.trendlineTimeAccessor,
|
||||
});
|
||||
|
||||
expect(removed).not.toHaveProperty('trendlineTimeAccessor');
|
||||
});
|
||||
it('removes trend metric dimension', () => {
|
||||
const removed = visualization.removeDimension({
|
||||
...removeDimensionParam,
|
||||
columnId: fullStateWTrend.trendlineMetricAccessor,
|
||||
});
|
||||
|
||||
expect(removed).not.toHaveProperty('trendlineMetricAccessor');
|
||||
});
|
||||
it('removes trend secondary metric dimension', () => {
|
||||
const removed = visualization.removeDimension({
|
||||
...removeDimensionParam,
|
||||
columnId: fullStateWTrend.trendlineSecondaryMetricAccessor,
|
||||
});
|
||||
|
||||
expect(removed).not.toHaveProperty('trendlineSecondaryMetricAccessor');
|
||||
});
|
||||
it('removes trend breakdown-by dimension', () => {
|
||||
const removed = visualization.removeDimension({
|
||||
...removeDimensionParam,
|
||||
columnId: fullStateWTrend.trendlineBreakdownByAccessor,
|
||||
});
|
||||
|
||||
expect(removed).not.toHaveProperty('trendlineBreakdownByAccessor');
|
||||
});
|
||||
});
|
||||
|
||||
it('implements custom display options', () => {
|
||||
|
|
|
@ -9,23 +9,24 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { render } from 'react-dom';
|
||||
import { Ast, AstFunction } from '@kbn/interpreter';
|
||||
import { PaletteOutput, PaletteRegistry, CUSTOM_PALETTE, CustomPaletteParams } from '@kbn/coloring';
|
||||
import { PaletteOutput, PaletteRegistry, CustomPaletteParams } from '@kbn/coloring';
|
||||
import { ThemeServiceStart } from '@kbn/core/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
|
||||
import { LayoutDirection } from '@elastic/charts';
|
||||
import { euiLightVars, euiThemeVars } from '@kbn/ui-theme';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { IconChartMetric } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { CollapseFunction } from '../../../common/expressions';
|
||||
import type { LayerType } from '../../../common';
|
||||
import { layerTypes } from '../../../common/layer_types';
|
||||
import type { FormBasedPersistedState } from '../../datasources/form_based/types';
|
||||
import { getSuggestions } from './suggestions';
|
||||
import {
|
||||
Visualization,
|
||||
OperationMetadata,
|
||||
DatasourceLayers,
|
||||
AccessorConfig,
|
||||
VisualizationConfigProps,
|
||||
VisualizationDimensionGroupConfig,
|
||||
Suggestion,
|
||||
} from '../../types';
|
||||
import { GROUP_ID, LENS_METRIC_ID } from './constants';
|
||||
|
@ -33,11 +34,17 @@ import { DimensionEditor } from './dimension_editor';
|
|||
import { Toolbar } from './toolbar';
|
||||
import { generateId } from '../../id_generator';
|
||||
import { FormatSelectorOptions } from '../../datasources/form_based/dimension_panel/format_selector';
|
||||
import { toExpression } from './to_expression';
|
||||
|
||||
export const DEFAULT_MAX_COLUMNS = 3;
|
||||
|
||||
export const getDefaultColor = (hasMax: boolean) =>
|
||||
hasMax ? euiLightVars.euiColorPrimary : euiThemeVars.euiColorLightestShade;
|
||||
export const showingBar = (
|
||||
state: MetricVisualizationState
|
||||
): state is MetricVisualizationState & { showBar: true; maxAccessor: string } =>
|
||||
Boolean(state.showBar && state.maxAccessor);
|
||||
|
||||
export const getDefaultColor = (state: MetricVisualizationState) =>
|
||||
showingBar(state) ? euiLightVars.euiColorPrimary : euiThemeVars.euiColorLightestShade;
|
||||
|
||||
export interface MetricVisualizationState {
|
||||
layerId: string;
|
||||
|
@ -48,114 +55,25 @@ export interface MetricVisualizationState {
|
|||
breakdownByAccessor?: string;
|
||||
// the dimensions can optionally be single numbers
|
||||
// computed by collapsing all rows
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
subtitle?: string;
|
||||
secondaryPrefix?: string;
|
||||
progressDirection?: LayoutDirection;
|
||||
showBar?: boolean;
|
||||
color?: string;
|
||||
palette?: PaletteOutput<CustomPaletteParams>;
|
||||
maxCols?: number;
|
||||
|
||||
trendlineLayerId?: string;
|
||||
trendlineLayerType?: LayerType;
|
||||
trendlineTimeAccessor?: string;
|
||||
trendlineMetricAccessor?: string;
|
||||
trendlineSecondaryMetricAccessor?: string;
|
||||
trendlineBreakdownByAccessor?: string;
|
||||
}
|
||||
|
||||
export const supportedDataTypes = new Set(['number']);
|
||||
|
||||
// TODO - deduplicate with gauges?
|
||||
function computePaletteParams(params: CustomPaletteParams) {
|
||||
return {
|
||||
...params,
|
||||
// rewrite colors and stops as two distinct arguments
|
||||
colors: (params?.stops || []).map(({ color }) => color),
|
||||
stops: params?.name === 'custom' ? (params?.stops || []).map(({ stop }) => stop) : [],
|
||||
reverse: false, // managed at UI level
|
||||
};
|
||||
}
|
||||
|
||||
const toExpression = (
|
||||
paletteService: PaletteRegistry,
|
||||
state: MetricVisualizationState,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
datasourceExpressionsByLayers: Record<string, Ast> | undefined = {}
|
||||
): Ast | null => {
|
||||
if (!state.metricAccessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const datasource = datasourceLayers[state.layerId];
|
||||
const datasourceExpression = datasourceExpressionsByLayers[state.layerId];
|
||||
|
||||
const maxPossibleTiles =
|
||||
// if there's a collapse function, no need to calculate since we're dealing with a single tile
|
||||
state.breakdownByAccessor && !state.collapseFn
|
||||
? datasource?.getMaxPossibleNumValues(state.breakdownByAccessor)
|
||||
: null;
|
||||
|
||||
const getCollapseFnArguments = () => {
|
||||
const metric = [state.metricAccessor, state.secondaryMetricAccessor, state.maxAccessor].filter(
|
||||
Boolean
|
||||
);
|
||||
|
||||
const fn = metric.map((accessor) => {
|
||||
if (accessor !== state.maxAccessor) {
|
||||
return state.collapseFn;
|
||||
} else {
|
||||
const isMaxStatic = Boolean(
|
||||
datasource?.getOperationForColumnId(state.maxAccessor!)?.isStaticValue
|
||||
);
|
||||
// we do this because the user expects the static value they set to be the same
|
||||
// even if they define a collapse on the breakdown by
|
||||
return isMaxStatic ? 'max' : state.collapseFn;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
by: [],
|
||||
metric,
|
||||
fn,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
...(datasourceExpression?.chain ?? []),
|
||||
...(state.collapseFn
|
||||
? [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_collapse',
|
||||
arguments: getCollapseFnArguments(),
|
||||
} as AstFunction,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
type: 'function',
|
||||
function: 'metricVis', // TODO import from plugin
|
||||
arguments: {
|
||||
metric: state.metricAccessor ? [state.metricAccessor] : [],
|
||||
secondaryMetric: state.secondaryMetricAccessor ? [state.secondaryMetricAccessor] : [],
|
||||
secondaryPrefix:
|
||||
typeof state.secondaryPrefix !== 'undefined' ? [state.secondaryPrefix] : [],
|
||||
max: state.maxAccessor ? [state.maxAccessor] : [],
|
||||
breakdownBy:
|
||||
state.breakdownByAccessor && !state.collapseFn ? [state.breakdownByAccessor] : [],
|
||||
subtitle: state.subtitle ? [state.subtitle] : [],
|
||||
progressDirection: state.progressDirection ? [state.progressDirection] : [],
|
||||
color: [state.color || getDefaultColor(!!state.maxAccessor)],
|
||||
palette: state.palette?.params
|
||||
? [
|
||||
paletteService
|
||||
.get(CUSTOM_PALETTE)
|
||||
.toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)),
|
||||
]
|
||||
: [],
|
||||
maxCols: [state.maxCols ?? DEFAULT_MAX_COLUMNS],
|
||||
minTiles: maxPossibleTiles ? [maxPossibleTiles] : [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const metricLabel = i18n.translate('xpack.lens.metric.label', {
|
||||
defaultMessage: 'Metric',
|
||||
});
|
||||
|
@ -163,6 +81,225 @@ const metricGroupLabel = i18n.translate('xpack.lens.metric.groupLabel', {
|
|||
defaultMessage: 'Goal and single value',
|
||||
});
|
||||
|
||||
const getMetricLayerConfiguration = (
|
||||
props: VisualizationConfigProps<MetricVisualizationState>
|
||||
): {
|
||||
groups: VisualizationDimensionGroupConfig[];
|
||||
} => {
|
||||
const isSupportedMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType);
|
||||
|
||||
const isSupportedDynamicMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue;
|
||||
|
||||
const getPrimaryAccessorDisplayConfig = (): Partial<AccessorConfig> => {
|
||||
const stops = props.state.palette?.params?.stops || [];
|
||||
const hasStaticColoring = !!props.state.color;
|
||||
const hasDynamicColoring = !!props.state.palette;
|
||||
return hasDynamicColoring
|
||||
? {
|
||||
triggerIcon: 'colorBy',
|
||||
palette: stops.map(({ color }) => color),
|
||||
}
|
||||
: hasStaticColoring
|
||||
? {
|
||||
triggerIcon: 'color',
|
||||
color: props.state.color,
|
||||
}
|
||||
: {
|
||||
triggerIcon: 'color',
|
||||
color: getDefaultColor(props.state),
|
||||
};
|
||||
};
|
||||
|
||||
const isBucketed = (op: OperationMetadata) => op.isBucketed;
|
||||
|
||||
const formatterOptions: FormatSelectorOptions = {
|
||||
disableExtraOptions: true,
|
||||
};
|
||||
|
||||
return {
|
||||
groups: [
|
||||
{
|
||||
groupId: GROUP_ID.METRIC,
|
||||
dataTestSubj: 'lnsMetric_primaryMetricDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.primaryMetric.label', {
|
||||
defaultMessage: 'Primary metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: props.state.metricAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.metricAccessor,
|
||||
...getPrimaryAccessorDisplayConfig(),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.metricAccessor,
|
||||
filterOperations: isSupportedDynamicMetric,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: true,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
requiredMinDimensionCount: 1,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.SECONDARY_METRIC,
|
||||
dataTestSubj: 'lnsMetric_secondaryMetricDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', {
|
||||
defaultMessage: 'Secondary metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: props.state.secondaryMetricAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.secondaryMetricAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.secondaryMetricAccessor,
|
||||
filterOperations: isSupportedDynamicMetric,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: true,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.MAX,
|
||||
dataTestSubj: 'lnsMetric_maxDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.max', { defaultMessage: 'Maximum value' }),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: props.state.maxAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.maxAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.maxAccessor,
|
||||
filterOperations: isSupportedMetric,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: false,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
supportStaticValue: true,
|
||||
prioritizedOperation: 'max',
|
||||
groupTooltip: i18n.translate('xpack.lens.metric.maxTooltip', {
|
||||
defaultMessage: 'If the maximum value is specified, the minimum value is fixed at zero.',
|
||||
}),
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.BREAKDOWN_BY,
|
||||
dataTestSubj: 'lnsMetric_breakdownByDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.breakdownBy', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
accessors: props.state.breakdownByAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.breakdownByAccessor,
|
||||
triggerIcon: props.state.collapseFn ? ('aggregate' as const) : undefined,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.breakdownByAccessor,
|
||||
filterOperations: isBucketed,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: true,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const getTrendlineLayerConfiguration = (
|
||||
props: VisualizationConfigProps<MetricVisualizationState>
|
||||
): {
|
||||
hidden: boolean;
|
||||
groups: VisualizationDimensionGroupConfig[];
|
||||
} => {
|
||||
return {
|
||||
hidden: true,
|
||||
groups: [
|
||||
{
|
||||
groupId: GROUP_ID.TREND_METRIC,
|
||||
groupLabel: i18n.translate('xpack.lens.primaryMetric.label', {
|
||||
defaultMessage: 'Primary metric',
|
||||
}),
|
||||
accessors: props.state.trendlineMetricAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.trendlineMetricAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.trendlineMetricAccessor,
|
||||
filterOperations: () => false,
|
||||
hideGrouping: true,
|
||||
nestingOrder: 3,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.TREND_SECONDARY_METRIC,
|
||||
groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', {
|
||||
defaultMessage: 'Secondary metric',
|
||||
}),
|
||||
accessors: props.state.trendlineSecondaryMetricAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.trendlineSecondaryMetricAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.trendlineSecondaryMetricAccessor,
|
||||
filterOperations: () => false,
|
||||
hideGrouping: true,
|
||||
nestingOrder: 2,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.TREND_TIME,
|
||||
groupLabel: i18n.translate('xpack.lens.metric.timeField', { defaultMessage: 'Time field' }),
|
||||
accessors: props.state.trendlineTimeAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.trendlineTimeAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.trendlineTimeAccessor,
|
||||
filterOperations: () => false,
|
||||
hideGrouping: true,
|
||||
nestingOrder: 1,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.TREND_BREAKDOWN_BY,
|
||||
groupLabel: i18n.translate('xpack.lens.metric.breakdownBy', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
accessors: props.state.trendlineBreakdownByAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.trendlineBreakdownByAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.trendlineBreakdownByAccessor,
|
||||
filterOperations: () => false,
|
||||
hideGrouping: true,
|
||||
nestingOrder: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const removeMetricDimension = (state: MetricVisualizationState) => {
|
||||
delete state.metricAccessor;
|
||||
delete state.palette;
|
||||
|
@ -177,6 +314,7 @@ const removeSecondaryMetricDimension = (state: MetricVisualizationState) => {
|
|||
const removeMaxDimension = (state: MetricVisualizationState) => {
|
||||
delete state.maxAccessor;
|
||||
delete state.progressDirection;
|
||||
delete state.showBar;
|
||||
};
|
||||
|
||||
const removeBreakdownByDimension = (state: MetricVisualizationState) => {
|
||||
|
@ -222,7 +360,7 @@ export const getMetricVisualization = ({
|
|||
},
|
||||
|
||||
getLayerIds(state) {
|
||||
return [state.layerId];
|
||||
return state.trendlineLayerId ? [state.layerId, state.trendlineLayerId] : [state.layerId];
|
||||
},
|
||||
|
||||
getDescription() {
|
||||
|
@ -238,7 +376,7 @@ export const getMetricVisualization = ({
|
|||
return (
|
||||
state ?? {
|
||||
layerId: addNewLayer(),
|
||||
layerType: LayerTypes.DATA,
|
||||
layerType: layerTypes.DATA,
|
||||
palette: mainPalette,
|
||||
}
|
||||
);
|
||||
|
@ -246,153 +384,25 @@ export const getMetricVisualization = ({
|
|||
triggers: [VIS_EVENT_TO_TRIGGER.filter],
|
||||
|
||||
getConfiguration(props) {
|
||||
const isSupportedMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType);
|
||||
return props.layerId === props.state.layerId
|
||||
? getMetricLayerConfiguration(props)
|
||||
: getTrendlineLayerConfiguration(props);
|
||||
},
|
||||
|
||||
const isSupportedDynamicMetric = (op: OperationMetadata) =>
|
||||
!op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue;
|
||||
getLayerType(layerId, state) {
|
||||
if (state?.layerId === layerId) {
|
||||
return state.layerType;
|
||||
}
|
||||
|
||||
const getPrimaryAccessorDisplayConfig = (): Partial<AccessorConfig> => {
|
||||
const stops = props.state.palette?.params?.stops || [];
|
||||
const hasStaticColoring = !!props.state.color;
|
||||
const hasDynamicColoring = !!props.state.palette;
|
||||
return hasDynamicColoring
|
||||
? {
|
||||
triggerIcon: 'colorBy',
|
||||
palette: stops.map(({ color }) => color),
|
||||
}
|
||||
: hasStaticColoring
|
||||
? {
|
||||
triggerIcon: 'color',
|
||||
color: props.state.color,
|
||||
}
|
||||
: {
|
||||
triggerIcon: 'color',
|
||||
color: getDefaultColor(!!props.state.maxAccessor),
|
||||
};
|
||||
};
|
||||
|
||||
const isBucketed = (op: OperationMetadata) => op.isBucketed;
|
||||
|
||||
const formatterOptions: FormatSelectorOptions = {
|
||||
disableExtraOptions: true,
|
||||
};
|
||||
|
||||
return {
|
||||
groups: [
|
||||
{
|
||||
groupId: GROUP_ID.METRIC,
|
||||
dataTestSubj: 'lnsMetric_primaryMetricDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.primaryMetric.label', {
|
||||
defaultMessage: 'Primary metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.metricAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.metricAccessor,
|
||||
...getPrimaryAccessorDisplayConfig(),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.metricAccessor,
|
||||
filterOperations: isSupportedDynamicMetric,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: true,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
requiredMinDimensionCount: 1,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.SECONDARY_METRIC,
|
||||
dataTestSubj: 'lnsMetric_secondaryMetricDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', {
|
||||
defaultMessage: 'Secondary metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.secondaryMetricAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.secondaryMetricAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.secondaryMetricAccessor,
|
||||
filterOperations: isSupportedDynamicMetric,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: true,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
requiredMinDimensionCount: 0,
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.MAX,
|
||||
dataTestSubj: 'lnsMetric_maxDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.max', { defaultMessage: 'Maximum value' }),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.maxAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.maxAccessor,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.maxAccessor,
|
||||
filterOperations: isSupportedMetric,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: false,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
supportStaticValue: true,
|
||||
prioritizedOperation: 'max',
|
||||
requiredMinDimensionCount: 0,
|
||||
groupTooltip: i18n.translate('xpack.lens.metric.maxTooltip', {
|
||||
defaultMessage:
|
||||
'If the maximum value is specified, the minimum value is fixed at zero.',
|
||||
}),
|
||||
},
|
||||
{
|
||||
groupId: GROUP_ID.BREAKDOWN_BY,
|
||||
dataTestSubj: 'lnsMetric_breakdownByDimensionPanel',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.breakdownBy', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.breakdownByAccessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.breakdownByAccessor,
|
||||
triggerIcon: props.state.collapseFn ? ('aggregate' as const) : undefined,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.breakdownByAccessor,
|
||||
filterOperations: isBucketed,
|
||||
enableDimensionEditor: true,
|
||||
enableFormatSelector: true,
|
||||
formatSelectorOptions: formatterOptions,
|
||||
requiredMinDimensionCount: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (state?.trendlineLayerId === layerId) {
|
||||
return state.trendlineLayerType;
|
||||
}
|
||||
},
|
||||
|
||||
getSupportedLayers(state) {
|
||||
return [
|
||||
{
|
||||
type: LayerTypes.DATA,
|
||||
type: layerTypes.DATA,
|
||||
label: i18n.translate('xpack.lens.metric.addLayer', {
|
||||
defaultMessage: 'Visualization',
|
||||
}),
|
||||
|
@ -405,14 +415,112 @@ export const getMetricVisualization = ({
|
|||
},
|
||||
]
|
||||
: undefined,
|
||||
disabled: true,
|
||||
canAddViaMenu: true,
|
||||
},
|
||||
{
|
||||
type: layerTypes.METRIC_TRENDLINE,
|
||||
label: i18n.translate('xpack.lens.metric.layerType.trendLine', {
|
||||
defaultMessage: 'Trendline',
|
||||
}),
|
||||
initialDimensions: [
|
||||
{ groupId: GROUP_ID.TREND_TIME, columnId: generateId(), autoTimeField: true },
|
||||
],
|
||||
disabled: Boolean(state?.trendlineLayerId),
|
||||
canAddViaMenu: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getLayerType(layerId, state) {
|
||||
if (state?.layerId === layerId) {
|
||||
return state.layerType;
|
||||
appendLayer(state, layerId, layerType) {
|
||||
if (layerType !== layerTypes.METRIC_TRENDLINE) {
|
||||
throw new Error(`Metric vis only supports layers of type ${layerTypes.METRIC_TRENDLINE}!`);
|
||||
}
|
||||
|
||||
return { ...state, trendlineLayerId: layerId, trendlineLayerType: layerType };
|
||||
},
|
||||
|
||||
removeLayer(state) {
|
||||
const newState: MetricVisualizationState = {
|
||||
...state,
|
||||
trendlineLayerId: undefined,
|
||||
trendlineLayerType: undefined,
|
||||
trendlineMetricAccessor: undefined,
|
||||
trendlineTimeAccessor: undefined,
|
||||
trendlineBreakdownByAccessor: undefined,
|
||||
};
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
getLayersToLinkTo(state, newLayerId: string): string[] {
|
||||
return newLayerId === state.trendlineLayerId ? [state.layerId] : [];
|
||||
},
|
||||
|
||||
getLinkedDimensions(state) {
|
||||
if (!state.trendlineLayerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const links: Array<{
|
||||
from: { columnId: string; groupId: string; layerId: string };
|
||||
to: {
|
||||
columnId?: string;
|
||||
groupId: string;
|
||||
layerId: string;
|
||||
};
|
||||
}> = [];
|
||||
|
||||
if (state.metricAccessor) {
|
||||
links.push({
|
||||
from: {
|
||||
columnId: state.metricAccessor,
|
||||
groupId: GROUP_ID.METRIC,
|
||||
layerId: state.layerId,
|
||||
},
|
||||
to: {
|
||||
columnId: state.trendlineMetricAccessor,
|
||||
groupId: GROUP_ID.TREND_METRIC,
|
||||
layerId: state.trendlineLayerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (state.secondaryMetricAccessor) {
|
||||
links.push({
|
||||
from: {
|
||||
columnId: state.secondaryMetricAccessor,
|
||||
groupId: GROUP_ID.SECONDARY_METRIC,
|
||||
layerId: state.layerId,
|
||||
},
|
||||
to: {
|
||||
columnId: state.trendlineSecondaryMetricAccessor,
|
||||
groupId: GROUP_ID.TREND_SECONDARY_METRIC,
|
||||
layerId: state.trendlineLayerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (state.breakdownByAccessor) {
|
||||
links.push({
|
||||
from: {
|
||||
columnId: state.breakdownByAccessor,
|
||||
groupId: GROUP_ID.BREAKDOWN_BY,
|
||||
layerId: state.layerId,
|
||||
},
|
||||
to: {
|
||||
columnId: state.trendlineBreakdownByAccessor,
|
||||
groupId: GROUP_ID.TREND_BREAKDOWN_BY,
|
||||
layerId: state.trendlineLayerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
},
|
||||
|
||||
getLayersToRemoveOnIndexPatternChange: (state) => {
|
||||
return state.trendlineLayerId ? [state.trendlineLayerId] : [];
|
||||
},
|
||||
|
||||
toExpression: (state, datasourceLayers, attributes, datasourceExpressionsByLayers) =>
|
||||
|
@ -430,10 +538,25 @@ export const getMetricVisualization = ({
|
|||
break;
|
||||
case GROUP_ID.MAX:
|
||||
updated.maxAccessor = columnId;
|
||||
if (!prevState.trendlineLayerId) {
|
||||
updated.showBar = true;
|
||||
}
|
||||
break;
|
||||
case GROUP_ID.BREAKDOWN_BY:
|
||||
updated.breakdownByAccessor = columnId;
|
||||
break;
|
||||
case GROUP_ID.TREND_TIME:
|
||||
updated.trendlineTimeAccessor = columnId;
|
||||
break;
|
||||
case GROUP_ID.TREND_METRIC:
|
||||
updated.trendlineMetricAccessor = columnId;
|
||||
break;
|
||||
case GROUP_ID.TREND_SECONDARY_METRIC:
|
||||
updated.trendlineSecondaryMetricAccessor = columnId;
|
||||
break;
|
||||
case GROUP_ID.TREND_BREAKDOWN_BY:
|
||||
updated.trendlineBreakdownByAccessor = columnId;
|
||||
break;
|
||||
}
|
||||
|
||||
return updated;
|
||||
|
@ -455,6 +578,19 @@ export const getMetricVisualization = ({
|
|||
removeBreakdownByDimension(updated);
|
||||
}
|
||||
|
||||
if (prevState.trendlineTimeAccessor === columnId) {
|
||||
delete updated.trendlineTimeAccessor;
|
||||
}
|
||||
if (prevState.trendlineMetricAccessor === columnId) {
|
||||
delete updated.trendlineMetricAccessor;
|
||||
}
|
||||
if (prevState.trendlineSecondaryMetricAccessor === columnId) {
|
||||
delete updated.trendlineSecondaryMetricAccessor;
|
||||
}
|
||||
if (prevState.trendlineBreakdownByAccessor === columnId) {
|
||||
delete updated.trendlineBreakdownByAccessor;
|
||||
}
|
||||
|
||||
return updated;
|
||||
},
|
||||
|
||||
|
|
|
@ -331,7 +331,7 @@ export function DimensionEditor(
|
|||
)}
|
||||
<CollapseSetting
|
||||
value={currentLayer?.collapseFns?.[props.accessor] || ''}
|
||||
onChange={(collapseFn: string) => {
|
||||
onChange={(collapseFn) => {
|
||||
props.setState({
|
||||
...props.state,
|
||||
layers: props.state.layers.map((layer) =>
|
||||
|
|
|
@ -20,6 +20,7 @@ import { FramePublicAPI } from '../../types';
|
|||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { PartitionChartsMeta } from './partition_charts_meta';
|
||||
import { CollapseFunction } from '../../../common/expressions';
|
||||
|
||||
jest.mock('../../id_generator');
|
||||
|
||||
|
@ -76,7 +77,7 @@ describe('pie_visualization', () => {
|
|||
|
||||
it("doesn't count collapsed dimensions", () => {
|
||||
state.layers[0].collapseFns = {
|
||||
[colIds[0]]: 'some-fn',
|
||||
[colIds[0]]: 'some-fn' as CollapseFunction,
|
||||
};
|
||||
|
||||
expect(pieVisualization.getErrorMessages(state)).toHaveLength(0);
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from '@kbn/chart-icons';
|
||||
|
||||
import { DistributiveOmit } from '@elastic/eui';
|
||||
import { CollapseFunction } from '../../../common/expressions';
|
||||
import type { VisualizationType } from '../../types';
|
||||
import type { ValueLabelConfig } from '../../../common/types';
|
||||
|
||||
|
@ -99,7 +100,7 @@ export interface XYDataLayerConfig {
|
|||
yConfig?: YConfig[];
|
||||
splitAccessor?: string;
|
||||
palette?: PaletteOutput;
|
||||
collapseFn?: string;
|
||||
collapseFn?: CollapseFunction;
|
||||
xScaleType?: XScaleType;
|
||||
isHistogram?: boolean;
|
||||
columnToLabel?: string;
|
||||
|
|
|
@ -292,6 +292,7 @@ describe('xy_visualization', () => {
|
|||
label: 'date_histogram',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -1685,6 +1686,7 @@ describe('xy_visualization', () => {
|
|||
label: 'date_histogram',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -1715,6 +1717,7 @@ describe('xy_visualization', () => {
|
|||
label: 'date_histogram',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -1759,6 +1762,7 @@ describe('xy_visualization', () => {
|
|||
label: 'histogram',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -1791,6 +1795,7 @@ describe('xy_visualization', () => {
|
|||
label: 'top values',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -1821,6 +1826,7 @@ describe('xy_visualization', () => {
|
|||
label: 'top values',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -1930,6 +1936,7 @@ describe('xy_visualization', () => {
|
|||
label: 'date_histogram',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -156,6 +156,10 @@ export const getXyVisualization = ({
|
|||
},
|
||||
|
||||
appendLayer(state, layerId, layerType, indexPatternId) {
|
||||
if (layerType === 'metricTrendline') {
|
||||
return state;
|
||||
}
|
||||
|
||||
const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType;
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { uniq } from 'lodash';
|
||||
import { IconChartBarHorizontal, IconChartBarStacked, IconChartMixedXy } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { LayerType as XYLayerType } from '@kbn/expression-xy-plugin/common';
|
||||
import { DatasourceLayers, OperationMetadata, VisualizationType } from '../../types';
|
||||
import {
|
||||
State,
|
||||
|
@ -21,7 +21,7 @@ import {
|
|||
SeriesType,
|
||||
} from './types';
|
||||
import { isHorizontalChart } from './state_helpers';
|
||||
import type { LayerType } from '../../../common';
|
||||
import { layerTypes } from '../..';
|
||||
|
||||
export function getAxisName(
|
||||
axis: 'x' | 'y' | 'yLeft' | 'yRight',
|
||||
|
@ -121,7 +121,7 @@ export function checkScaleOperation(
|
|||
}
|
||||
|
||||
export const isDataLayer = (layer: XYLayerConfig): layer is XYDataLayerConfig =>
|
||||
layer.layerType === LayerTypes.DATA || !layer.layerType;
|
||||
layer.layerType === layerTypes.DATA || !layer.layerType;
|
||||
|
||||
export const getDataLayers = (layers: XYLayerConfig[]) =>
|
||||
(layers || []).filter((layer): layer is XYDataLayerConfig => isDataLayer(layer));
|
||||
|
@ -131,31 +131,31 @@ export const getFirstDataLayer = (layers: XYLayerConfig[]) =>
|
|||
|
||||
export const isReferenceLayer = (
|
||||
layer: Pick<XYLayerConfig, 'layerType'>
|
||||
): layer is XYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE;
|
||||
): layer is XYReferenceLineLayerConfig => layer.layerType === layerTypes.REFERENCELINE;
|
||||
|
||||
export const getReferenceLayers = (layers: Array<Pick<XYLayerConfig, 'layerType'>>) =>
|
||||
(layers || []).filter((layer): layer is XYReferenceLineLayerConfig => isReferenceLayer(layer));
|
||||
|
||||
export const isAnnotationsLayer = (
|
||||
layer: Pick<XYLayerConfig, 'layerType'>
|
||||
): layer is XYAnnotationLayerConfig => layer.layerType === LayerTypes.ANNOTATIONS;
|
||||
): layer is XYAnnotationLayerConfig => layer.layerType === layerTypes.ANNOTATIONS;
|
||||
|
||||
export const getAnnotationsLayers = (layers: Array<Pick<XYLayerConfig, 'layerType'>>) =>
|
||||
(layers || []).filter((layer): layer is XYAnnotationLayerConfig => isAnnotationsLayer(layer));
|
||||
|
||||
export interface LayerTypeToLayer {
|
||||
[LayerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig;
|
||||
[LayerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig;
|
||||
[LayerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => XYAnnotationLayerConfig;
|
||||
[layerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig;
|
||||
[layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig;
|
||||
[layerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => XYAnnotationLayerConfig;
|
||||
}
|
||||
|
||||
export const getLayerTypeOptions = (layer: XYLayerConfig, options: LayerTypeToLayer) => {
|
||||
if (isDataLayer(layer)) {
|
||||
return options[LayerTypes.DATA](layer);
|
||||
return options[layerTypes.DATA](layer);
|
||||
} else if (isReferenceLayer(layer)) {
|
||||
return options[LayerTypes.REFERENCELINE](layer);
|
||||
return options[layerTypes.REFERENCELINE](layer);
|
||||
}
|
||||
return options[LayerTypes.ANNOTATIONS](layer);
|
||||
return options[layerTypes.ANNOTATIONS](layer);
|
||||
};
|
||||
|
||||
export function getVisualizationType(state: State): VisualizationType | 'mixed' {
|
||||
|
@ -211,7 +211,7 @@ export const defaultIcon = IconChartBarStacked;
|
|||
export const defaultSeriesType = 'bar_stacked';
|
||||
|
||||
export const supportedDataLayer = {
|
||||
type: LayerTypes.DATA,
|
||||
type: layerTypes.DATA,
|
||||
label: i18n.translate('xpack.lens.xyChart.addDataLayerLabel', {
|
||||
defaultMessage: 'Visualization',
|
||||
}),
|
||||
|
@ -253,7 +253,7 @@ export function getMessageIdsForDimension(
|
|||
}
|
||||
|
||||
const newLayerFn = {
|
||||
[LayerTypes.DATA]: ({
|
||||
[layerTypes.DATA]: ({
|
||||
layerId,
|
||||
seriesType,
|
||||
}: {
|
||||
|
@ -261,16 +261,16 @@ const newLayerFn = {
|
|||
seriesType: SeriesType;
|
||||
}): XYDataLayerConfig => ({
|
||||
layerId,
|
||||
layerType: LayerTypes.DATA,
|
||||
layerType: layerTypes.DATA,
|
||||
accessors: [],
|
||||
seriesType,
|
||||
}),
|
||||
[LayerTypes.REFERENCELINE]: ({ layerId }: { layerId: string }): XYReferenceLineLayerConfig => ({
|
||||
[layerTypes.REFERENCELINE]: ({ layerId }: { layerId: string }): XYReferenceLineLayerConfig => ({
|
||||
layerId,
|
||||
layerType: LayerTypes.REFERENCELINE,
|
||||
layerType: layerTypes.REFERENCELINE,
|
||||
accessors: [],
|
||||
}),
|
||||
[LayerTypes.ANNOTATIONS]: ({
|
||||
[layerTypes.ANNOTATIONS]: ({
|
||||
layerId,
|
||||
indexPatternId,
|
||||
}: {
|
||||
|
@ -278,7 +278,7 @@ const newLayerFn = {
|
|||
indexPatternId: string;
|
||||
}): XYAnnotationLayerConfig => ({
|
||||
layerId,
|
||||
layerType: LayerTypes.ANNOTATIONS,
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [],
|
||||
indexPatternId,
|
||||
ignoreGlobalFilters: true,
|
||||
|
@ -287,12 +287,12 @@ const newLayerFn = {
|
|||
|
||||
export function newLayerState({
|
||||
layerId,
|
||||
layerType = LayerTypes.DATA,
|
||||
layerType = layerTypes.DATA,
|
||||
seriesType,
|
||||
indexPatternId,
|
||||
}: {
|
||||
layerId: string;
|
||||
layerType?: LayerType;
|
||||
layerType?: XYLayerType;
|
||||
seriesType: SeriesType;
|
||||
indexPatternId: string;
|
||||
}) {
|
||||
|
@ -300,7 +300,7 @@ export function newLayerState({
|
|||
}
|
||||
|
||||
export function getLayersByType(state: State, byType?: string) {
|
||||
return state.layers.filter(({ layerType = LayerTypes.DATA }) =>
|
||||
return state.layers.filter(({ layerType = layerTypes.DATA }) =>
|
||||
byType ? layerType === byType : true
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks';
|
|||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { AnnotationsPanel } from '.';
|
||||
import { FramePublicAPI } from '../../../../types';
|
||||
import { DatasourcePublicAPI } from '../../../..';
|
||||
import { createMockFramePublicAPI } from '../../../../mocks';
|
||||
import { State } from '../../types';
|
||||
import { Position } from '@elastic/charts';
|
||||
|
@ -88,6 +89,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -152,6 +156,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -191,6 +198,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -293,6 +303,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -350,6 +363,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -403,6 +419,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -476,6 +495,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -547,6 +569,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -615,6 +640,9 @@ describe('AnnotationsPanel', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -273,6 +273,9 @@ describe('XY Config panels', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -298,6 +301,9 @@ describe('XY Config panels', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -344,6 +350,9 @@ describe('XY Config panels', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -387,6 +396,9 @@ describe('XY Config panels', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -430,6 +442,9 @@ describe('XY Config panels', () => {
|
|||
formatFactory={jest.fn()}
|
||||
paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
panelRef={React.createRef()}
|
||||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const filterBar = getService('filterBar');
|
||||
const retry = getService('retry');
|
||||
const inspector = getService('inspector');
|
||||
|
||||
const clickMetric = async (title: string) => {
|
||||
const tiles = await PageObjects.lens.getMetricTiles();
|
||||
|
@ -62,6 +63,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
extraText: 'Average of bytes 19.76K',
|
||||
value: '19.76K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingTrendline: false,
|
||||
showingBar: false,
|
||||
},
|
||||
{
|
||||
|
@ -70,6 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
extraText: 'Average of bytes 18.99K',
|
||||
value: '18.99K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingTrendline: false,
|
||||
showingBar: false,
|
||||
},
|
||||
{
|
||||
|
@ -78,6 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
extraText: 'Average of bytes 17.25K',
|
||||
value: '17.25K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingTrendline: false,
|
||||
showingBar: false,
|
||||
},
|
||||
{
|
||||
|
@ -86,6 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
extraText: 'Average of bytes 15.69K',
|
||||
value: '15.69K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingTrendline: false,
|
||||
showingBar: false,
|
||||
},
|
||||
{
|
||||
|
@ -94,6 +99,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
extraText: 'Average of bytes 15.61K',
|
||||
value: '15.61K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingTrendline: false,
|
||||
showingBar: false,
|
||||
},
|
||||
{
|
||||
|
@ -102,6 +108,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
extraText: 'Average of bytes 5.72K',
|
||||
value: '5.72K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingTrendline: false,
|
||||
showingBar: false,
|
||||
},
|
||||
]);
|
||||
|
@ -118,6 +125,44 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.removeDimension('lnsMetric_maxDimensionPanel');
|
||||
});
|
||||
|
||||
it('should enable trendlines', async () => {
|
||||
await PageObjects.lens.openDimensionEditor(
|
||||
'lnsMetric_primaryMetricDimensionPanel > lns-dimensionTrigger'
|
||||
);
|
||||
|
||||
await testSubjects.click('lnsMetric_supporting_visualization_trendline');
|
||||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
expect(
|
||||
(await PageObjects.lens.getMetricVisualizationData()).some(
|
||||
(datum) => datum.showingTrendline
|
||||
)
|
||||
).to.be(true);
|
||||
|
||||
await inspector.open('lnsApp_inspectButton');
|
||||
|
||||
expect(await inspector.getNumberOfTables()).to.equal(2);
|
||||
|
||||
await inspector.close();
|
||||
|
||||
await PageObjects.lens.openDimensionEditor(
|
||||
'lnsMetric_primaryMetricDimensionPanel > lns-dimensionTrigger'
|
||||
);
|
||||
|
||||
await testSubjects.click('lnsMetric_supporting_visualization_none');
|
||||
|
||||
await PageObjects.lens.waitForVisualization('mtrVis');
|
||||
|
||||
expect(
|
||||
(await PageObjects.lens.getMetricVisualizationData()).some(
|
||||
(datum) => datum.showingTrendline
|
||||
)
|
||||
).to.be(false);
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
});
|
||||
|
||||
it('should filter by click', async () => {
|
||||
expect((await filterBar.getFiltersLabel()).length).to.be(0);
|
||||
const title = '93.28.27.24';
|
||||
|
|
|
@ -41,6 +41,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '140.05%',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: true,
|
||||
showingTrendline: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '14.01K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -78,6 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '13.1B',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -106,6 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '1.44K',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -159,6 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '13.23B',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
{
|
||||
title: 'win 7',
|
||||
|
@ -167,6 +171,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '13.19B',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
{
|
||||
title: 'win xp',
|
||||
|
@ -175,6 +180,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '13.07B',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
{
|
||||
title: 'win 8',
|
||||
|
@ -183,6 +189,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '13.03B',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
{
|
||||
title: 'ios',
|
||||
|
@ -191,6 +198,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: '13.01B',
|
||||
color: 'rgba(245, 247, 250, 1)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
{
|
||||
title: undefined,
|
||||
|
@ -199,6 +207,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
value: undefined,
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
showingBar: false,
|
||||
showingTrendline: false,
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -1189,6 +1189,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
color: await (
|
||||
await this.getMetricElementIfExists('.echMetric', tile)
|
||||
)?.getComputedStyle('background-color'),
|
||||
showingTrendline: Boolean(
|
||||
await this.getMetricElementIfExists('.echSingleMetricSparkline', tile)
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue