mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] Enable new context constants in formula (#158452)
## Summary Fixes #151827, Fixes #117548 This PR contains constant function features in Lens formula: * direct exposure of the `time_range()` - as suggested in #117548) <img width="297" alt="Screenshot 2023-05-29 at 12 05 43" src="8dbd63a6
-c5ec-4177-a4a2-cc0770a495bc"> * `interval()` (as intended in #151827) <img width="359" alt="Screenshot 2023-05-29 at 12 04 18" src="8c3a4637
-bd23-4b4d-8a5f-36853ccbfee3"> * and `now()` (as suggested in [here](https://github.com/elastic/kibana/issues/112851#issuecomment-1183175053)) functions in the formula input <img width="364" alt="Screenshot 2023-05-25 at 14 30 14" src="2137717b
-44a0-4531-bcc2-1fbda40368ba"> A visual explanation of what each constant represent: <img width="677" alt="Screenshot 2023-05-29 at 11 56 16" src="b7b7e92c
-b061-4cce-a77e-e7baeab323a8"> Documentation: <img width="546" alt="Screenshot 2023-05-31 at 12 59 40" src="1fea7e88
-6e4f-4958-949f-6b71a770cd44"> Some example usage <img width="683" alt="Screenshot 2023-05-25 at 13 33 26" src="e03442ab
-0062-4999-8a38-c00a66b3b13b"> TSVB => Lens with `params._interval` support:  **Notes**: * context values work like static values, with the same limits, therefore it is not possible to build a date histogram with a context value alone (i.e. a formula with only `interval()` or `now()`). It works ok without the `Date Histogram` dimension. * The `interval()` function will report an error if used without a configured `Date Histogram` dimension: <img width="671" alt="Screenshot 2023-05-29 at 12 14 13" src="ca67f102
-35bb-4e4d-ab16-15d1be4cadcc"> * The `interval()` function does not take into account different bucket interval size (i.e. DST changes, leap years, etc...), rather return the same value to all the buckets. This is the same behaviour as in TSVB, but in Lens it can be a problem due to the usage of `calendar_interval`. * I had to duplicate a couple of function from the helpers to avoid issues with tests. I've tried a different organization of the helpers (between pure vs impure fns) but that took longer than expected, so I would defer this task later in another PR. <details> <summary>General approach with `constant(...)` removed</summary> * a more general approach using `constants(value="...")` <img width="301" alt="Screenshot 2023-05-29 at 12 07 23" src="0a800aa4
-8db5-4055-996d-344d37adfc01"> </details> A cloud deployment has been created to test both approaches here. Let me know which one do you prefer cc @elastic/kibana-visualizations ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
e2a060d557
commit
3600f975de
33 changed files with 933 additions and 192 deletions
|
@ -297,6 +297,34 @@ describe('convertMathToFormulaColumn', () => {
|
|||
expect(convertMathToFormulaColumn(...input)).toEqual(expect.objectContaining(expected));
|
||||
}
|
||||
});
|
||||
|
||||
it.each`
|
||||
expression | expected
|
||||
${'params._interval'} | ${'interval()'}
|
||||
${'params._interval + params._interval'} | ${'interval() + interval()'}
|
||||
${'params._all'} | ${null}
|
||||
${'params._all + params.interval'} | ${null}
|
||||
${'params._timestamp'} | ${null}
|
||||
${'params._timestamp + params.interval'} | ${null}
|
||||
${'params._index'} | ${null}
|
||||
${'params._index + params.interval'} | ${null}
|
||||
`(`handle special params cases: $expression`, ({ expression, expected }) => {
|
||||
expect(
|
||||
convertMathToFormulaColumn({
|
||||
series,
|
||||
metrics: [{ ...mathMetric, script: expression }],
|
||||
dataView,
|
||||
})
|
||||
).toEqual(
|
||||
expected
|
||||
? expect.objectContaining({
|
||||
meta: { metricId: 'some-id-1' },
|
||||
operationType: 'formula',
|
||||
params: { formula: expected },
|
||||
})
|
||||
: expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertOtherAggsToFormulaColumn', () => {
|
||||
|
|
|
@ -141,6 +141,11 @@ export const convertMathToFormulaColumn = (
|
|||
return null;
|
||||
}
|
||||
|
||||
// now replace the _interval with the new interval() formula
|
||||
if (script.includes('params._interval')) {
|
||||
script = script.replaceAll('params._interval', 'interval()');
|
||||
}
|
||||
|
||||
const scripthasNoStaticNumber = isNaN(Number(script));
|
||||
if (script.includes('params') || !scripthasNoStaticNumber) {
|
||||
return null;
|
||||
|
|
|
@ -59,6 +59,8 @@ jest.mock('./dimension_panel/reference_editor', () => ({
|
|||
ReferenceEditor: () => null,
|
||||
}));
|
||||
|
||||
const nowInstant = new Date();
|
||||
|
||||
const fieldsOne = [
|
||||
{
|
||||
name: 'timestamp',
|
||||
|
@ -365,7 +367,14 @@ describe('IndexPattern Data Source', () => {
|
|||
it('should generate an empty expression when no columns are selected', async () => {
|
||||
const state = FormBasedDatasource.initialize();
|
||||
expect(
|
||||
FormBasedDatasource.toExpression(state, 'first', indexPatterns, dateRange, 'testing-seed')
|
||||
FormBasedDatasource.toExpression(
|
||||
state,
|
||||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
)
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
|
@ -395,6 +404,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
)
|
||||
).toEqual({
|
||||
|
@ -450,6 +460,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
|
@ -637,6 +648,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
|
||||
|
@ -678,6 +690,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect((ast.chain[1].arguments.aggs[1] as Ast).chain[0].arguments.timeShift).toEqual(['1d']);
|
||||
|
@ -891,6 +904,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const count = (ast.chain[1].arguments.aggs[1] as Ast).chain[0];
|
||||
|
@ -961,6 +975,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.aggs[0]).toMatchInlineSnapshot(`
|
||||
|
@ -1091,6 +1106,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const timeScaleCalls = ast.chain.filter((fn) => fn.function === 'lens_time_scale');
|
||||
|
@ -1162,6 +1178,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const filteredMetricAgg = (ast.chain[1].arguments.aggs[0] as Ast).chain[0].arguments;
|
||||
|
@ -1219,6 +1236,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const formatIndex = ast.chain.findIndex((fn) => fn.function === 'lens_format_column');
|
||||
|
@ -1273,6 +1291,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.metricsAtAllLevels).toEqual([false]);
|
||||
|
@ -1318,6 +1337,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
expect(ast.chain[1].arguments.timeFields).toEqual(['timestamp']);
|
||||
|
@ -1381,6 +1401,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
);
|
||||
|
||||
|
@ -1455,6 +1476,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
|
||||
|
@ -1525,6 +1547,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
|
||||
|
@ -1636,6 +1659,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
// @ts-expect-error we can't isolate just the reference type
|
||||
|
@ -1675,6 +1699,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
|
||||
|
@ -1768,6 +1793,7 @@ describe('IndexPattern Data Source', () => {
|
|||
'first',
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
'testing-seed'
|
||||
) as Ast;
|
||||
const chainLength = ast.chain.length;
|
||||
|
|
|
@ -17,7 +17,7 @@ import { flatten, isEqual } from 'lodash';
|
|||
import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { DataPublicPluginStart, ES_FIELD_TYPES } from '@kbn/data-plugin/public';
|
||||
import { DataPublicPluginStart, ES_FIELD_TYPES, UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
|
@ -186,7 +186,7 @@ export function getFormBasedDatasource({
|
|||
return loadInitialState({
|
||||
persistedState,
|
||||
references,
|
||||
defaultIndexPatternId: core.uiSettings.get('defaultIndex'),
|
||||
defaultIndexPatternId: uiSettings.get('defaultIndex'),
|
||||
storage,
|
||||
initialContext,
|
||||
indexPatternRefs,
|
||||
|
@ -425,8 +425,16 @@ export function getFormBasedDatasource({
|
|||
return fields;
|
||||
},
|
||||
|
||||
toExpression: (state, layerId, indexPatterns, dateRange, searchSessionId) =>
|
||||
toExpression(state, layerId, indexPatterns, uiSettings, dateRange, searchSessionId),
|
||||
toExpression: (state, layerId, indexPatterns, dateRange, nowInstant, searchSessionId) =>
|
||||
toExpression(
|
||||
state,
|
||||
layerId,
|
||||
indexPatterns,
|
||||
uiSettings,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
searchSessionId
|
||||
),
|
||||
|
||||
renderLayerSettings(domElement, props) {
|
||||
render(
|
||||
|
@ -853,7 +861,8 @@ export function getFormBasedDatasource({
|
|||
layer,
|
||||
columnId,
|
||||
frameDatasourceAPI.dataViews.indexPatterns[layer.indexPatternId],
|
||||
frameDatasourceAPI.dateRange
|
||||
frameDatasourceAPI.dateRange,
|
||||
uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* 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 type { FormBasedLayer } from '../../../../..';
|
||||
import { createMockedIndexPattern } from '../../../mocks';
|
||||
import { DateHistogramIndexPatternColumn } from '../date_histogram';
|
||||
import {
|
||||
ConstantsIndexPatternColumn,
|
||||
nowOperation,
|
||||
intervalOperation,
|
||||
timeRangeOperation,
|
||||
} from './context_variables';
|
||||
|
||||
function createLayer<T extends ConstantsIndexPatternColumn>(
|
||||
type: 'interval' | 'now' | 'time_range'
|
||||
): FormBasedLayer {
|
||||
return {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: `Constant: ${type}`,
|
||||
dataType: 'number',
|
||||
operationType: type,
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
references: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createExpression(type: 'interval' | 'now' | 'time_range', value: number) {
|
||||
return [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'mathColumn',
|
||||
arguments: {
|
||||
id: ['col1'],
|
||||
name: [`Constant: ${type}`],
|
||||
expression: [String(value)],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
describe('context variables', () => {
|
||||
describe('interval', () => {
|
||||
describe('getErrorMessages', () => {
|
||||
it('should return error if no date_histogram is configured', () => {
|
||||
expect(
|
||||
intervalOperation.getErrorMessage!(
|
||||
createLayer('interval'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{ fromDate: new Date().toISOString(), toDate: new Date().toISOString() },
|
||||
{},
|
||||
100
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Cannot compute an interval without a date histogram column configured',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error if no dateRange is passed over', () => {
|
||||
expect(
|
||||
intervalOperation.getErrorMessage!(
|
||||
createLayer('interval'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
undefined,
|
||||
{},
|
||||
100
|
||||
)
|
||||
).toEqual(expect.arrayContaining(['The current time range interval is not available']));
|
||||
});
|
||||
|
||||
it('should return error if no targetBar is passed over', () => {
|
||||
expect(
|
||||
intervalOperation.getErrorMessage!(
|
||||
createLayer('interval'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{ fromDate: new Date().toISOString(), toDate: new Date().toISOString() },
|
||||
{}
|
||||
)
|
||||
).toEqual(expect.arrayContaining(['Missing "histogram:barTarget" value']));
|
||||
});
|
||||
|
||||
it('should not return errors if all context is provided', () => {
|
||||
const layer = createLayer('interval');
|
||||
layer.columns = {
|
||||
col2: {
|
||||
label: 'Date histogram',
|
||||
dataType: 'date',
|
||||
operationType: 'date_histogram',
|
||||
sourceField: '@timestamp',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: {
|
||||
interval: 'auto',
|
||||
includeEmptyRows: true,
|
||||
dropPartials: false,
|
||||
},
|
||||
} as DateHistogramIndexPatternColumn,
|
||||
...layer.columns,
|
||||
};
|
||||
layer.columnOrder = ['col2', 'col1'];
|
||||
expect(
|
||||
intervalOperation.getErrorMessage!(
|
||||
layer,
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{ fromDate: new Date().toISOString(), toDate: new Date().toISOString() },
|
||||
{},
|
||||
100
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('toExpression', () => {
|
||||
it('should return 0 if no dateRange is passed', () => {
|
||||
expect(
|
||||
intervalOperation.toExpression(
|
||||
createLayer('interval'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{ now: new Date(), targetBars: 100 }
|
||||
)
|
||||
).toEqual(expect.arrayContaining(createExpression('interval', 0)));
|
||||
});
|
||||
|
||||
it('should return 0 if no targetBars is passed', () => {
|
||||
expect(
|
||||
intervalOperation.toExpression(
|
||||
createLayer('interval'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{
|
||||
dateRange: {
|
||||
fromDate: new Date(2022, 0, 1).toISOString(),
|
||||
toDate: new Date(2023, 0, 1).toISOString(),
|
||||
},
|
||||
now: new Date(),
|
||||
}
|
||||
)
|
||||
).toEqual(expect.arrayContaining(createExpression('interval', 0)));
|
||||
});
|
||||
|
||||
it('should return a valid value > 0 if both dateRange and targetBars is passed', () => {
|
||||
expect(
|
||||
intervalOperation.toExpression(
|
||||
createLayer('interval'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{
|
||||
dateRange: {
|
||||
fromDate: new Date(2022, 0, 1).toISOString(),
|
||||
toDate: new Date(2023, 0, 1).toISOString(),
|
||||
},
|
||||
now: new Date(),
|
||||
targetBars: 100,
|
||||
}
|
||||
)
|
||||
).toEqual(expect.arrayContaining(createExpression('interval', 86400000)));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('time_range', () => {
|
||||
describe('getErrorMessages', () => {
|
||||
it('should return error if no dateRange is passed over', () => {
|
||||
expect(
|
||||
timeRangeOperation.getErrorMessage!(
|
||||
createLayer('time_range'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
undefined,
|
||||
{},
|
||||
100
|
||||
)
|
||||
).toEqual(expect.arrayContaining(['The current time range interval is not available']));
|
||||
});
|
||||
|
||||
it('should return error if dataView is not time-based', () => {
|
||||
const dataView = createMockedIndexPattern();
|
||||
dataView.timeFieldName = undefined;
|
||||
expect(
|
||||
timeRangeOperation.getErrorMessage!(
|
||||
createLayer('time_range'),
|
||||
'col1',
|
||||
dataView,
|
||||
undefined,
|
||||
{},
|
||||
100
|
||||
)
|
||||
).toEqual(expect.arrayContaining(['The current time range interval is not available']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('toExpression', () => {
|
||||
it('should return 0 if no dateRange is passed', () => {
|
||||
expect(
|
||||
timeRangeOperation.toExpression(
|
||||
createLayer('time_range'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{ now: new Date(), targetBars: 100 }
|
||||
)
|
||||
).toEqual(expect.arrayContaining(createExpression('time_range', 0)));
|
||||
});
|
||||
|
||||
it('should return a valid value > 0 if dateRange is passed', () => {
|
||||
expect(
|
||||
timeRangeOperation.toExpression(
|
||||
createLayer('time_range'),
|
||||
'col1',
|
||||
createMockedIndexPattern(),
|
||||
{
|
||||
dateRange: {
|
||||
fromDate: new Date(2022, 0, 1).toISOString(),
|
||||
toDate: new Date(2023, 0, 1).toISOString(),
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual(expect.arrayContaining(createExpression('time_range', 31536000000)));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('now', () => {
|
||||
describe('getErrorMessages', () => {
|
||||
it('should return no error even without context', () => {
|
||||
expect(
|
||||
nowOperation.getErrorMessage!(createLayer('now'), 'col1', createMockedIndexPattern())
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toExpression', () => {
|
||||
it('should return the now value when passed', () => {
|
||||
const now = new Date();
|
||||
expect(
|
||||
nowOperation.toExpression(createLayer('now'), 'col1', createMockedIndexPattern(), {
|
||||
now,
|
||||
})
|
||||
).toEqual(expect.arrayContaining(createExpression('now', +now)));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { calcAutoIntervalNear, UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { partition } from 'lodash';
|
||||
import type {
|
||||
DateHistogramIndexPatternColumn,
|
||||
FormBasedLayer,
|
||||
GenericIndexPatternColumn,
|
||||
} from '../../../../..';
|
||||
import type { DateRange } from '../../../../../../common/types';
|
||||
import type { GenericOperationDefinition, OperationDefinition } from '..';
|
||||
import type { ReferenceBasedIndexPatternColumn } from '../column_types';
|
||||
import { IndexPattern } from '../../../../../types';
|
||||
|
||||
// copied over from layer_helpers
|
||||
// TODO: split layer_helpers util into pure/non-pure functions to avoid issues with tests
|
||||
export function getColumnOrder(layer: FormBasedLayer): string[] {
|
||||
const entries = Object.entries(layer.columns);
|
||||
entries.sort(([idA], [idB]) => {
|
||||
const indexA = layer.columnOrder.indexOf(idA);
|
||||
const indexB = layer.columnOrder.indexOf(idB);
|
||||
if (indexA > -1 && indexB > -1) {
|
||||
return indexA - indexB;
|
||||
} else if (indexA > -1) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
const [aggregations, metrics] = partition(entries, ([, col]) => col.isBucketed);
|
||||
|
||||
return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id));
|
||||
}
|
||||
|
||||
// Copied over from helpers
|
||||
export function isColumnOfType<C extends GenericIndexPatternColumn>(
|
||||
type: C['operationType'],
|
||||
column: GenericIndexPatternColumn
|
||||
): column is C {
|
||||
return column.operationType === type;
|
||||
}
|
||||
|
||||
export interface ContextValues {
|
||||
dateRange?: DateRange;
|
||||
now?: Date;
|
||||
targetBars?: number;
|
||||
}
|
||||
|
||||
export interface TimeRangeIndexPatternColumn extends ReferenceBasedIndexPatternColumn {
|
||||
operationType: 'time_range';
|
||||
}
|
||||
|
||||
function getTimeRangeFromContext({ dateRange }: ContextValues) {
|
||||
return dateRange ? moment(dateRange.toDate).diff(moment(dateRange.fromDate)) : 0;
|
||||
}
|
||||
|
||||
function getTimeRangeErrorMessages(
|
||||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
dateRange?: DateRange | undefined
|
||||
) {
|
||||
const errors = [];
|
||||
if (!indexPattern.timeFieldName) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.lens.indexPattern.dateRange.dataViewNoTimeBased', {
|
||||
defaultMessage: 'The current dataView is not time based',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!dateRange) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.lens.indexPattern.dateRange.noTimeRange', {
|
||||
defaultMessage: 'The current time range interval is not available',
|
||||
})
|
||||
);
|
||||
}
|
||||
return errors.length ? errors : undefined;
|
||||
}
|
||||
|
||||
export const timeRangeOperation = createContextValueBasedOperation<TimeRangeIndexPatternColumn>({
|
||||
type: 'time_range',
|
||||
label: 'Time range',
|
||||
description: i18n.translate('xpack.lens.indexPattern.timeRange.documentation.markdown', {
|
||||
defaultMessage: `
|
||||
The specified time range, in milliseconds (ms).
|
||||
`,
|
||||
}),
|
||||
getContextValue: getTimeRangeFromContext,
|
||||
getErrorMessage: getTimeRangeErrorMessages,
|
||||
});
|
||||
|
||||
export interface NowIndexPatternColumn extends ReferenceBasedIndexPatternColumn {
|
||||
operationType: 'now';
|
||||
}
|
||||
|
||||
function getNowFromContext({ now }: ContextValues) {
|
||||
return now == null ? Date.now() : +now;
|
||||
}
|
||||
function getNowErrorMessage() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const nowOperation = createContextValueBasedOperation<NowIndexPatternColumn>({
|
||||
type: 'now',
|
||||
label: 'Current now',
|
||||
description: i18n.translate('xpack.lens.indexPattern.now.documentation.markdown', {
|
||||
defaultMessage: `
|
||||
The current now moment used in Kibana expressed in milliseconds (ms).
|
||||
`,
|
||||
}),
|
||||
getContextValue: getNowFromContext,
|
||||
getErrorMessage: getNowErrorMessage,
|
||||
});
|
||||
|
||||
export interface IntervalIndexPatternColumn extends ReferenceBasedIndexPatternColumn {
|
||||
operationType: 'interval';
|
||||
}
|
||||
|
||||
function getIntervalFromContext(context: ContextValues) {
|
||||
return context.dateRange && context.targetBars
|
||||
? calcAutoIntervalNear(context.targetBars, getTimeRangeFromContext(context)).asMilliseconds()
|
||||
: 0;
|
||||
}
|
||||
|
||||
function getIntervalErrorMessages(
|
||||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
dateRange?: DateRange | undefined,
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition> | undefined,
|
||||
targetBars?: number
|
||||
) {
|
||||
const errors = [];
|
||||
if (!targetBars) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.lens.indexPattern.interval.noTargetBars', {
|
||||
defaultMessage: `Missing "{uiSettingVar}" value`,
|
||||
values: {
|
||||
uiSettingVar: UI_SETTINGS.HISTOGRAM_BAR_TARGET,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!dateRange) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.lens.indexPattern.interval.noTimeRange', {
|
||||
defaultMessage: 'The current time range interval is not available',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (
|
||||
!Object.values(layer.columns).some((column) =>
|
||||
isColumnOfType<DateHistogramIndexPatternColumn>('date_histogram', column)
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.lens.indexPattern.interval.noDateHistogramColumn', {
|
||||
defaultMessage: 'Cannot compute an interval without a date histogram column configured',
|
||||
})
|
||||
);
|
||||
}
|
||||
return errors.length ? errors : undefined;
|
||||
}
|
||||
|
||||
export const intervalOperation = createContextValueBasedOperation<IntervalIndexPatternColumn>({
|
||||
type: 'interval',
|
||||
label: 'Date histogram interval',
|
||||
description: i18n.translate('xpack.lens.indexPattern.interval.documentation.markdown', {
|
||||
defaultMessage: `
|
||||
The specified minimum interval for the date histogram, in milliseconds (ms).
|
||||
`,
|
||||
}),
|
||||
getContextValue: getIntervalFromContext,
|
||||
getErrorMessage: getIntervalErrorMessages,
|
||||
});
|
||||
|
||||
export type ConstantsIndexPatternColumn =
|
||||
| IntervalIndexPatternColumn
|
||||
| TimeRangeIndexPatternColumn
|
||||
| NowIndexPatternColumn;
|
||||
|
||||
function createContextValueBasedOperation<ColumnType extends ConstantsIndexPatternColumn>({
|
||||
label,
|
||||
type,
|
||||
getContextValue,
|
||||
getErrorMessage,
|
||||
description,
|
||||
}: {
|
||||
label: string;
|
||||
type: ColumnType['operationType'];
|
||||
description: string;
|
||||
getContextValue: (context: ContextValues) => number;
|
||||
getErrorMessage: OperationDefinition<ColumnType, 'managedReference'>['getErrorMessage'];
|
||||
}): OperationDefinition<ColumnType, 'managedReference'> {
|
||||
return {
|
||||
type,
|
||||
displayName: label,
|
||||
input: 'managedReference',
|
||||
selectionStyle: 'hidden',
|
||||
usedInMath: true,
|
||||
getDefaultLabel: () => label,
|
||||
isTransferable: () => true,
|
||||
getDisabledStatus() {
|
||||
return undefined;
|
||||
},
|
||||
getErrorMessage,
|
||||
getPossibleOperation() {
|
||||
return {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
};
|
||||
},
|
||||
buildColumn: () => {
|
||||
return {
|
||||
label,
|
||||
dataType: 'number',
|
||||
operationType: type,
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
references: [],
|
||||
} as unknown as ColumnType;
|
||||
},
|
||||
toExpression: (layer, columnId, _, context = {}) => {
|
||||
const column = layer.columns[columnId] as ColumnType;
|
||||
return [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'mathColumn',
|
||||
arguments: {
|
||||
id: [columnId],
|
||||
name: [column.label],
|
||||
expression: [String(getContextValue(context))],
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
createCopy(layers, source, target) {
|
||||
const currentColumn = layers[source.layerId].columns[source.columnId] as ColumnType;
|
||||
const targetLayer = layers[target.layerId];
|
||||
const columns = {
|
||||
...targetLayer.columns,
|
||||
[target.columnId]: { ...currentColumn },
|
||||
};
|
||||
return {
|
||||
...layers,
|
||||
[target.layerId]: {
|
||||
...targetLayer,
|
||||
columns,
|
||||
columnOrder: getColumnOrder({ ...targetLayer, columns }),
|
||||
},
|
||||
};
|
||||
},
|
||||
documentation: {
|
||||
section: 'constants',
|
||||
signature: '',
|
||||
description,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -31,6 +31,7 @@ import { monaco } from '@kbn/monaco';
|
|||
import classNames from 'classnames';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CodeEditorProps } from '@kbn/kibana-react-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import { useDebounceWithOptions } from '../../../../../../shared_components';
|
||||
import { ParamEditorProps } from '../..';
|
||||
import { getManagedColumnsFrom } from '../../../layer_helpers';
|
||||
|
@ -109,6 +110,7 @@ export function FormulaEditor({
|
|||
dateHistogramInterval,
|
||||
hasData,
|
||||
dateRange,
|
||||
uiSettings,
|
||||
}: Omit<ParamEditorProps<FormulaIndexPatternColumn>, 'activeData'> & {
|
||||
dateHistogramInterval: ReturnType<typeof getDateHistogramInterval>;
|
||||
hasData: boolean;
|
||||
|
@ -356,7 +358,8 @@ export function FormulaEditor({
|
|||
id,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
visibleOperationsMap
|
||||
visibleOperationsMap,
|
||||
uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET)
|
||||
);
|
||||
if (messages) {
|
||||
const startPosition = offsetToRowColumn(text, locations[id].min);
|
||||
|
|
|
@ -22,6 +22,42 @@ import type {
|
|||
} from '../..';
|
||||
import type { FormulaIndexPatternColumn } from '../formula';
|
||||
|
||||
function createNewSection(
|
||||
label: string,
|
||||
description: string,
|
||||
functionsToDocument: string[],
|
||||
operationDefinitionMap: Record<string, GenericOperationDefinition>
|
||||
) {
|
||||
return {
|
||||
label,
|
||||
description,
|
||||
items: functionsToDocument.sort().map((key) => {
|
||||
const fnDescription = getFunctionDescriptionAndExamples(key, operationDefinitionMap);
|
||||
return {
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>{getFunctionSignatureLabel(key, operationDefinitionMap, false)}</h3>
|
||||
|
||||
{fnDescription ? <Markdown markdown={fnDescription} /> : null}
|
||||
</>
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getFunctionDescriptionAndExamples(
|
||||
label: string,
|
||||
operationDefinitionMap: Record<string, GenericOperationDefinition>
|
||||
) {
|
||||
if (tinymathFunctions[label]) {
|
||||
const [description, examples] = tinymathFunctions[label].help.split(`\`\`\``);
|
||||
return `${description.replace(/\n/g, '\n\n')}${examples ? `\`\`\`${examples}\`\`\`` : ''}`;
|
||||
}
|
||||
return operationDefinitionMap[label].documentation?.description;
|
||||
}
|
||||
|
||||
export function getDocumentationSections({
|
||||
indexPattern,
|
||||
operationDefinitionMap,
|
||||
|
@ -159,22 +195,12 @@ max(system.network.in.bytes, reducedTimeRange="30m")
|
|||
],
|
||||
});
|
||||
|
||||
helpGroups.push({
|
||||
label: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSection', {
|
||||
defaultMessage: 'Elasticsearch',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSectionDescription', {
|
||||
defaultMessage:
|
||||
'These functions will be executed on the raw documents for each row of the resulting table, aggregating all documents matching the break down dimensions into a single value.',
|
||||
}),
|
||||
items: [],
|
||||
});
|
||||
|
||||
const {
|
||||
elasticsearch: esFunction,
|
||||
calculation: calculationFunction,
|
||||
elasticsearch: esFunctions,
|
||||
calculation: calculationFunctions,
|
||||
math: mathOperations,
|
||||
comparison: comparisonOperations,
|
||||
constants: constantsOperations,
|
||||
} = groupBy(getPossibleFunctions(indexPattern), (key) => {
|
||||
if (key in operationDefinitionMap) {
|
||||
return operationDefinitionMap[key].documentation?.section;
|
||||
|
@ -185,122 +211,78 @@ max(system.network.in.bytes, reducedTimeRange="30m")
|
|||
});
|
||||
|
||||
// Es aggs
|
||||
helpGroups[2].items.push(
|
||||
...esFunction.sort().map((key) => ({
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>
|
||||
{key}({operationDefinitionMap[key].documentation?.signature})
|
||||
</h3>
|
||||
|
||||
{operationDefinitionMap[key].documentation?.description ? (
|
||||
<Markdown markdown={operationDefinitionMap[key].documentation!.description} />
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
helpGroups.push({
|
||||
label: i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSection', {
|
||||
defaultMessage: 'Column calculations',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.lens.formulaDocumentation.columnCalculationSectionDescription',
|
||||
{
|
||||
helpGroups.push(
|
||||
createNewSection(
|
||||
i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSection', {
|
||||
defaultMessage: 'Elasticsearch',
|
||||
}),
|
||||
i18n.translate('xpack.lens.formulaDocumentation.elasticsearchSectionDescription', {
|
||||
defaultMessage:
|
||||
'These functions are executed for each row, but are provided with the whole column as context. This is also known as a window function.',
|
||||
}
|
||||
),
|
||||
items: [],
|
||||
});
|
||||
'These functions will be executed on the raw documents for each row of the resulting table, aggregating all documents matching the break down dimensions into a single value.',
|
||||
}),
|
||||
|
||||
esFunctions,
|
||||
operationDefinitionMap
|
||||
)
|
||||
);
|
||||
|
||||
// Calculations aggs
|
||||
helpGroups[3].items.push(
|
||||
...calculationFunction.sort().map((key) => ({
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>
|
||||
{key}({operationDefinitionMap[key].documentation?.signature})
|
||||
</h3>
|
||||
helpGroups.push(
|
||||
createNewSection(
|
||||
i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSection', {
|
||||
defaultMessage: 'Column calculations',
|
||||
}),
|
||||
i18n.translate('xpack.lens.formulaDocumentation.columnCalculationSectionDescription', {
|
||||
defaultMessage:
|
||||
'These functions are executed for each row, but are provided with the whole column as context. This is also known as a window function.',
|
||||
}),
|
||||
|
||||
{operationDefinitionMap[key].documentation?.description ? (
|
||||
<Markdown markdown={operationDefinitionMap[key].documentation!.description} />
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
}))
|
||||
calculationFunctions,
|
||||
operationDefinitionMap
|
||||
)
|
||||
);
|
||||
|
||||
helpGroups.push({
|
||||
label: i18n.translate('xpack.lens.formulaDocumentation.mathSection', {
|
||||
defaultMessage: 'Math',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.formulaDocumentation.mathSectionDescription', {
|
||||
defaultMessage:
|
||||
'These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.',
|
||||
}),
|
||||
items: [],
|
||||
});
|
||||
helpGroups.push(
|
||||
createNewSection(
|
||||
i18n.translate('xpack.lens.formulaDocumentation.mathSection', {
|
||||
defaultMessage: 'Math',
|
||||
}),
|
||||
i18n.translate('xpack.lens.formulaDocumentation.mathSectionDescription', {
|
||||
defaultMessage:
|
||||
'These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.',
|
||||
}),
|
||||
|
||||
const mathFns = mathOperations.sort().map((key) => {
|
||||
const [description, examples] = tinymathFunctions[key].help.split(`\`\`\``);
|
||||
return {
|
||||
label: key,
|
||||
description: description.replace(/\n/g, '\n\n'),
|
||||
examples: examples ? `\`\`\`${examples}\`\`\`` : '',
|
||||
};
|
||||
});
|
||||
|
||||
helpGroups[4].items.push(
|
||||
...mathFns.map(({ label, description, examples }) => {
|
||||
return {
|
||||
label,
|
||||
description: (
|
||||
<>
|
||||
<h3>{getFunctionSignatureLabel(label, operationDefinitionMap)}</h3>
|
||||
|
||||
<Markdown markdown={`${description}${examples}`} />
|
||||
</>
|
||||
),
|
||||
};
|
||||
})
|
||||
mathOperations,
|
||||
operationDefinitionMap
|
||||
)
|
||||
);
|
||||
|
||||
helpGroups.push({
|
||||
label: i18n.translate('xpack.lens.formulaDocumentation.comparisonSection', {
|
||||
defaultMessage: 'Comparison',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.formulaDocumentation.comparisonSectionDescription', {
|
||||
defaultMessage: 'These functions are used to perform value comparison.',
|
||||
}),
|
||||
items: [],
|
||||
});
|
||||
helpGroups.push(
|
||||
createNewSection(
|
||||
i18n.translate('xpack.lens.formulaDocumentation.comparisonSection', {
|
||||
defaultMessage: 'Comparison',
|
||||
}),
|
||||
i18n.translate('xpack.lens.formulaDocumentation.comparisonSectionDescription', {
|
||||
defaultMessage: 'These functions are used to perform value comparison.',
|
||||
}),
|
||||
|
||||
const comparisonFns = comparisonOperations.sort().map((key) => {
|
||||
const [description, examples] = tinymathFunctions[key].help.split(`\`\`\``);
|
||||
return {
|
||||
label: key,
|
||||
description: description.replace(/\n/g, '\n\n'),
|
||||
examples: examples ? `\`\`\`${examples}\`\`\`` : '',
|
||||
};
|
||||
});
|
||||
comparisonOperations,
|
||||
operationDefinitionMap
|
||||
)
|
||||
);
|
||||
|
||||
helpGroups[5].items.push(
|
||||
...comparisonFns.map(({ label, description, examples }) => {
|
||||
return {
|
||||
label,
|
||||
description: (
|
||||
<>
|
||||
<h3>{getFunctionSignatureLabel(label, operationDefinitionMap)}</h3>
|
||||
|
||||
<Markdown markdown={`${description}${examples}`} />
|
||||
</>
|
||||
),
|
||||
};
|
||||
})
|
||||
helpGroups.push(
|
||||
createNewSection(
|
||||
i18n.translate('xpack.lens.formulaDocumentation.constantsSection', {
|
||||
defaultMessage: 'Kibana context',
|
||||
}),
|
||||
i18n.translate('xpack.lens.formulaDocumentation.constantsSectionDescription', {
|
||||
defaultMessage:
|
||||
'These functions are used to retrieve Kibana context variables, which are the date histogram `interval`, the current `now` and the selected `time_range` and help you to compute date math operations.',
|
||||
}),
|
||||
constantsOperations,
|
||||
operationDefinitionMap
|
||||
)
|
||||
);
|
||||
|
||||
const sections = {
|
||||
|
@ -353,7 +335,7 @@ Use the symbols +, -, /, and * to perform basic math.
|
|||
export function getFunctionSignatureLabel(
|
||||
name: string,
|
||||
operationDefinitionMap: ParamEditorProps<FormulaIndexPatternColumn>['operationDefinitionMap'],
|
||||
firstParam?: { label: string | [number, number] } | null
|
||||
getFullSignature: boolean = true
|
||||
): string {
|
||||
if (tinymathFunctions[name]) {
|
||||
return `${name}(${tinymathFunctions[name].positionalArguments
|
||||
|
@ -363,26 +345,28 @@ export function getFunctionSignatureLabel(
|
|||
if (operationDefinitionMap[name]) {
|
||||
const def = operationDefinitionMap[name];
|
||||
const extraArgs: string[] = [];
|
||||
if (def.filterable) {
|
||||
extraArgs.push(
|
||||
i18n.translate('xpack.lens.formula.kqlExtraArguments', {
|
||||
defaultMessage: '[kql]?: string, [lucene]?: string',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (def.shiftable) {
|
||||
extraArgs.push(
|
||||
i18n.translate('xpack.lens.formula.shiftExtraArguments', {
|
||||
defaultMessage: '[shift]?: string',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (def.canReduceTimeRange) {
|
||||
extraArgs.push(
|
||||
i18n.translate('xpack.lens.formula.reducedTimeRangeExtraArguments', {
|
||||
defaultMessage: '[reducedTimeRange]?: string',
|
||||
})
|
||||
);
|
||||
if (getFullSignature) {
|
||||
if (def.filterable) {
|
||||
extraArgs.push(
|
||||
i18n.translate('xpack.lens.formula.kqlExtraArguments', {
|
||||
defaultMessage: '[kql]?: string, [lucene]?: string',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (def.shiftable) {
|
||||
extraArgs.push(
|
||||
i18n.translate('xpack.lens.formula.shiftExtraArguments', {
|
||||
defaultMessage: '[shift]?: string',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (def.canReduceTimeRange) {
|
||||
extraArgs.push(
|
||||
i18n.translate('xpack.lens.formula.reducedTimeRangeExtraArguments', {
|
||||
defaultMessage: '[reducedTimeRange]?: string',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const extraComma = extraArgs.length ? ', ' : '';
|
||||
return `${name}(${def.documentation?.signature}${extraComma}${extraArgs.join(', ')})`;
|
||||
|
|
|
@ -219,7 +219,9 @@ export function getPossibleFunctions(
|
|||
available.forEach((a) => {
|
||||
if (a.operationMetaData.dataType === 'number' && !a.operationMetaData.isBucketed) {
|
||||
possibleOperationNames.push(
|
||||
...a.operations.filter((o) => o.type !== 'managedReference').map((o) => o.operationType)
|
||||
...a.operations
|
||||
.filter((o) => o.type !== 'managedReference' || o.usedInMath)
|
||||
.map((o) => o.operationType)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -607,7 +609,7 @@ function getSignaturesForFunction(
|
|||
}
|
||||
: null;
|
||||
|
||||
const functionLabel = getFunctionSignatureLabel(name, operationDefinitionMap, firstParam);
|
||||
const functionLabel = getFunctionSignatureLabel(name, operationDefinitionMap);
|
||||
const documentation = getOperationTypeHelp(name, operationDefinitionMap);
|
||||
if ('operationParams' in def && def.operationParams) {
|
||||
return [
|
||||
|
|
|
@ -73,6 +73,13 @@ const operationDefinitionMap: Record<string, GenericOperationDefinition> = {
|
|||
}),
|
||||
}),
|
||||
cumulative_sum: createOperationDefinitionMock('cumulative_sum', { input: 'fullReference' }),
|
||||
interval: createOperationDefinitionMock('interval', {
|
||||
input: 'managedReference',
|
||||
usedInMath: true,
|
||||
}),
|
||||
opertion_not_available: createOperationDefinitionMock('operation_not_available', {
|
||||
input: 'managedReference',
|
||||
}),
|
||||
};
|
||||
|
||||
describe('formula', () => {
|
||||
|
@ -1871,5 +1878,27 @@ invalid: "
|
|||
)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should work with managed reference operations only when "usedInMath" flag is enabled', () => {
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
getNewLayerWithFormula('interval()', false),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
getNewLayerWithFormula('operation_not_available()', false),
|
||||
'col1',
|
||||
indexPattern,
|
||||
undefined,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(['Operation operation_not_available not found']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,7 +68,7 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
getDisabledStatus(indexPattern: IndexPattern) {
|
||||
return undefined;
|
||||
},
|
||||
getErrorMessage(layer, columnId, indexPattern, dateRange, operationDefinitionMap) {
|
||||
getErrorMessage(layer, columnId, indexPattern, dateRange, operationDefinitionMap, targetBars) {
|
||||
const column = layer.columns[columnId] as FormulaIndexPatternColumn;
|
||||
if (!column.params.formula || !operationDefinitionMap) {
|
||||
return;
|
||||
|
@ -110,7 +110,8 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
id,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
visibleOperationsMap
|
||||
visibleOperationsMap,
|
||||
targetBars
|
||||
);
|
||||
return messages || [];
|
||||
}
|
||||
|
|
|
@ -12,3 +12,10 @@ export { insertOrReplaceFormulaColumn } from './parse';
|
|||
|
||||
export type { MathIndexPatternColumn } from './math';
|
||||
export { mathOperation } from './math';
|
||||
|
||||
export type {
|
||||
TimeRangeIndexPatternColumn,
|
||||
NowIndexPatternColumn,
|
||||
IntervalIndexPatternColumn,
|
||||
} from './context_variables';
|
||||
export { timeRangeOperation, nowOperation, intervalOperation } from './context_variables';
|
||||
|
|
|
@ -73,7 +73,8 @@ function parseAndExtract(
|
|||
i18n.translate('xpack.lens.indexPattern.formulaPartLabel', {
|
||||
defaultMessage: 'Part of {label}',
|
||||
values: { label: label || text },
|
||||
})
|
||||
}),
|
||||
dateRange
|
||||
);
|
||||
return { extracted, isValid: true };
|
||||
}
|
||||
|
@ -84,7 +85,8 @@ function extractColumns(
|
|||
ast: TinymathAST,
|
||||
layer: FormBasedLayer,
|
||||
indexPattern: IndexPattern,
|
||||
label: string
|
||||
label: string,
|
||||
dateRange: DateRange | undefined
|
||||
): Array<{ column: GenericIndexPatternColumn; location?: TinymathLocation }> {
|
||||
const columns: Array<{ column: GenericIndexPatternColumn; location?: TinymathLocation }> = [];
|
||||
const { filter: globalFilter, reducedTimeRange: globalReducedTimeRange } =
|
||||
|
@ -194,6 +196,21 @@ function extractColumns(
|
|||
// replace by new column id
|
||||
return newColId;
|
||||
}
|
||||
|
||||
if (nodeOperation.input === 'managedReference' && nodeOperation.usedInMath) {
|
||||
const newCol = (
|
||||
nodeOperation as OperationDefinition<GenericIndexPatternColumn, 'managedReference'>
|
||||
).buildColumn({
|
||||
layer,
|
||||
indexPattern,
|
||||
});
|
||||
const newColId = getManagedId(idPrefix, columns.length);
|
||||
newCol.customLabel = true;
|
||||
newCol.label = label;
|
||||
columns.push({ column: newCol, location: node.location });
|
||||
// replace by new column id
|
||||
return newColId;
|
||||
}
|
||||
}
|
||||
|
||||
const root = parseNode(ast);
|
||||
|
|
|
@ -41,7 +41,13 @@ import {
|
|||
timeScaleOperation,
|
||||
} from './calculations';
|
||||
import { countOperation } from './count';
|
||||
import { mathOperation, formulaOperation } from './formula';
|
||||
import {
|
||||
mathOperation,
|
||||
formulaOperation,
|
||||
timeRangeOperation,
|
||||
nowOperation,
|
||||
intervalOperation,
|
||||
} from './formula';
|
||||
import { staticValueOperation } from './static_value';
|
||||
import { lastValueOperation } from './last_value';
|
||||
import type {
|
||||
|
@ -100,7 +106,13 @@ export type {
|
|||
export type { CountIndexPatternColumn } from './count';
|
||||
export type { LastValueIndexPatternColumn } from './last_value';
|
||||
export type { RangeIndexPatternColumn } from './ranges';
|
||||
export type { FormulaIndexPatternColumn, MathIndexPatternColumn } from './formula';
|
||||
export type {
|
||||
FormulaIndexPatternColumn,
|
||||
MathIndexPatternColumn,
|
||||
TimeRangeIndexPatternColumn,
|
||||
NowIndexPatternColumn,
|
||||
IntervalIndexPatternColumn,
|
||||
} from './formula';
|
||||
export type { StaticValueIndexPatternColumn } from './static_value';
|
||||
|
||||
// List of all operation definitions registered to this data source.
|
||||
|
@ -134,6 +146,9 @@ const internalOperationDefinitions = [
|
|||
overallAverageOperation,
|
||||
staticValueOperation,
|
||||
timeScaleOperation,
|
||||
timeRangeOperation,
|
||||
nowOperation,
|
||||
intervalOperation,
|
||||
];
|
||||
|
||||
export { termsOperation } from './terms';
|
||||
|
@ -308,7 +323,8 @@ interface BaseOperationDefinitionProps<
|
|||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
dateRange?: DateRange,
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition>
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition>,
|
||||
targetBars?: number
|
||||
) => FieldBasedOperationErrorMessage[] | undefined;
|
||||
|
||||
/*
|
||||
|
@ -338,7 +354,7 @@ interface BaseOperationDefinitionProps<
|
|||
documentation?: {
|
||||
signature: string;
|
||||
description: string;
|
||||
section: 'elasticsearch' | 'calculation';
|
||||
section: 'elasticsearch' | 'calculation' | 'constants';
|
||||
};
|
||||
quickFunctionDocumentation?: string;
|
||||
/**
|
||||
|
@ -665,7 +681,8 @@ interface ManagedReferenceOperationDefinition<C extends BaseIndexPatternColumn>
|
|||
toExpression: (
|
||||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern
|
||||
indexPattern: IndexPattern,
|
||||
context?: { dateRange?: DateRange; now?: Date; targetBars?: number }
|
||||
) => ExpressionAstFunction[];
|
||||
/**
|
||||
* Managed references control the IDs of their inner columns, so we need to be able to copy from the
|
||||
|
@ -677,6 +694,18 @@ interface ManagedReferenceOperationDefinition<C extends BaseIndexPatternColumn>
|
|||
target: DataViewDragDropOperation,
|
||||
operationDefinitionMap: Record<string, GenericOperationDefinition>
|
||||
) => Record<string, FormBasedLayer>;
|
||||
|
||||
/**
|
||||
* Special managed columns can be used in a formula
|
||||
*/
|
||||
usedInMath?: boolean;
|
||||
|
||||
/**
|
||||
* The specification of the arguments used by the operations used for both validation,
|
||||
* and use from external managed operations
|
||||
*/
|
||||
operationParams?: OperationParam[];
|
||||
selectionStyle?: 'hidden';
|
||||
}
|
||||
|
||||
interface OperationDefinitionMap<C extends BaseIndexPatternColumn, P = {}> {
|
||||
|
|
|
@ -39,9 +39,9 @@ import {
|
|||
OperationDefinition,
|
||||
} from './definitions';
|
||||
import { TinymathAST } from '@kbn/tinymath';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { IndexPattern } from '../../../types';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { createCoreStartMock } from '@kbn/core-lifecycle-browser-mocks/src/core_start.mock';
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
dataMock.query.timefilter.timefilter.getAbsoluteTime = jest
|
||||
|
@ -53,6 +53,10 @@ jest.mock('../../../id_generator');
|
|||
jest.mock('../dimension_panel/reference_editor', () => ({
|
||||
ReferenceEditor: () => null,
|
||||
}));
|
||||
const TARGET_BAR_COUNT = 100;
|
||||
|
||||
const CoreStartMock = createCoreStartMock();
|
||||
CoreStartMock.uiSettings.get.mockReturnValue(TARGET_BAR_COUNT);
|
||||
|
||||
const indexPatternFields = [
|
||||
{
|
||||
|
@ -3128,7 +3132,7 @@ describe('state_helpers', () => {
|
|||
indexPattern,
|
||||
{},
|
||||
'1',
|
||||
{},
|
||||
CoreStartMock,
|
||||
dataMock
|
||||
);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
@ -3155,7 +3159,7 @@ describe('state_helpers', () => {
|
|||
indexPattern,
|
||||
{} as FormBasedPrivateState,
|
||||
'1',
|
||||
{} as CoreStart,
|
||||
CoreStartMock,
|
||||
dataMock
|
||||
);
|
||||
expect(mock).toHaveBeenCalled();
|
||||
|
@ -3191,7 +3195,7 @@ describe('state_helpers', () => {
|
|||
indexPattern,
|
||||
{} as FormBasedPrivateState,
|
||||
'1',
|
||||
{} as CoreStart,
|
||||
CoreStartMock,
|
||||
dataMock
|
||||
);
|
||||
expect(notCalledMock).not.toHaveBeenCalled();
|
||||
|
@ -3228,7 +3232,7 @@ describe('state_helpers', () => {
|
|||
indexPattern,
|
||||
{} as FormBasedPrivateState,
|
||||
'1',
|
||||
{} as CoreStart,
|
||||
CoreStartMock,
|
||||
dataMock
|
||||
);
|
||||
expect(savedRef).toHaveBeenCalled();
|
||||
|
@ -3258,7 +3262,7 @@ describe('state_helpers', () => {
|
|||
indexPattern,
|
||||
{} as FormBasedPrivateState,
|
||||
'1',
|
||||
{} as CoreStart,
|
||||
CoreStartMock,
|
||||
dataMock
|
||||
);
|
||||
expect(mock).toHaveBeenCalledWith(
|
||||
|
@ -3281,7 +3285,8 @@ describe('state_helpers', () => {
|
|||
fromDate: '2022-11-01T00:00:00.000Z',
|
||||
toDate: '2022-11-03T00:00:00.000Z',
|
||||
},
|
||||
operationDefinitionMap
|
||||
operationDefinitionMap,
|
||||
TARGET_BAR_COUNT
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import { partition, mapValues, pickBy } from 'lodash';
|
|||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DataPublicPluginStart, UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import type { DateRange } from '../../../../common/types';
|
||||
import type {
|
||||
DatasourceFixAction,
|
||||
|
@ -1580,7 +1580,8 @@ export function getErrorMessages(
|
|||
columnId,
|
||||
indexPattern,
|
||||
{ fromDate: currentTimeRange.from, toDate: currentTimeRange.to },
|
||||
operationDefinitionMap
|
||||
operationDefinitionMap,
|
||||
core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET)
|
||||
);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -442,10 +442,27 @@ describe('getOperationTypesForField', () => {
|
|||
Object {
|
||||
"operationType": "math",
|
||||
"type": "managedReference",
|
||||
"usedInMath": undefined,
|
||||
},
|
||||
Object {
|
||||
"operationType": "formula",
|
||||
"type": "managedReference",
|
||||
"usedInMath": undefined,
|
||||
},
|
||||
Object {
|
||||
"operationType": "time_range",
|
||||
"type": "managedReference",
|
||||
"usedInMath": true,
|
||||
},
|
||||
Object {
|
||||
"operationType": "now",
|
||||
"type": "managedReference",
|
||||
"usedInMath": true,
|
||||
},
|
||||
Object {
|
||||
"operationType": "interval",
|
||||
"type": "managedReference",
|
||||
"usedInMath": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -498,6 +515,7 @@ describe('getOperationTypesForField', () => {
|
|||
Object {
|
||||
"operationType": "static_value",
|
||||
"type": "managedReference",
|
||||
"usedInMath": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -164,6 +164,7 @@ export type OperationFieldTuple =
|
|||
| {
|
||||
type: 'managedReference';
|
||||
operationType: OperationType;
|
||||
usedInMath?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -203,7 +204,11 @@ export function getAvailableOperationsByMetadata(
|
|||
) {
|
||||
const operationByMetadata: Record<
|
||||
string,
|
||||
{ operationMetaData: OperationMetadata; operations: OperationFieldTuple[] }
|
||||
{
|
||||
operationMetaData: OperationMetadata;
|
||||
operations: OperationFieldTuple[];
|
||||
usedInMath?: boolean;
|
||||
}
|
||||
> = {};
|
||||
|
||||
const addToMap = (
|
||||
|
@ -255,7 +260,10 @@ export function getAvailableOperationsByMetadata(
|
|||
const validOperation = operationDefinition.getPossibleOperation(indexPattern);
|
||||
if (validOperation) {
|
||||
addToMap(
|
||||
{ type: 'fullReference', operationType: operationDefinition.type },
|
||||
{
|
||||
type: 'fullReference',
|
||||
operationType: operationDefinition.type,
|
||||
},
|
||||
validOperation
|
||||
);
|
||||
}
|
||||
|
@ -263,7 +271,11 @@ export function getAvailableOperationsByMetadata(
|
|||
const validOperation = operationDefinition.getPossibleOperation();
|
||||
if (validOperation) {
|
||||
addToMap(
|
||||
{ type: 'managedReference', operationType: operationDefinition.type },
|
||||
{
|
||||
type: 'managedReference',
|
||||
operationType: operationDefinition.type,
|
||||
usedInMath: operationDefinition.usedInMath,
|
||||
},
|
||||
validOperation
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import type { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { partition, uniq } from 'lodash';
|
||||
import seedrandom from 'seedrandom';
|
||||
import type {
|
||||
import {
|
||||
AggFunctionsMapping,
|
||||
EsaggsExpressionFunctionDefinition,
|
||||
IndexPatternLoadExpressionFunctionDefinition,
|
||||
UI_SETTINGS,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { queryToAst } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
|
@ -58,6 +59,7 @@ function getExpressionForLayer(
|
|||
indexPattern: IndexPattern,
|
||||
uiSettings: IUiSettingsClient,
|
||||
dateRange: DateRange,
|
||||
nowInstant: Date,
|
||||
searchSessionId?: string
|
||||
): ExpressionAstExpression | null {
|
||||
const { columnOrder } = layer;
|
||||
|
@ -132,19 +134,25 @@ function getExpressionForLayer(
|
|||
if (referenceEntries.length || esAggEntries.length) {
|
||||
let aggs: ExpressionAstExpressionBuilder[] = [];
|
||||
const expressions: ExpressionAstFunction[] = [];
|
||||
const histogramBarsTarget = uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
|
||||
|
||||
sortedReferences(referenceEntries).forEach((colId) => {
|
||||
const col = columns[colId];
|
||||
const def = operationDefinitionMap[col.operationType];
|
||||
if (def.input === 'fullReference' || def.input === 'managedReference') {
|
||||
expressions.push(...def.toExpression(layer, colId, indexPattern));
|
||||
expressions.push(
|
||||
...def.toExpression(layer, colId, indexPattern, {
|
||||
dateRange,
|
||||
now: nowInstant,
|
||||
targetBars: histogramBarsTarget,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const orderedColumnIds = esAggEntries.map(([colId]) => colId);
|
||||
let esAggsIdMap: Record<string, OriginalColumn[]> = {};
|
||||
const aggExpressionToEsAggsIdMap: Map<ExpressionAstExpressionBuilder, string> = new Map();
|
||||
const histogramBarsTarget = uiSettings.get('histogram:barTarget');
|
||||
esAggEntries.forEach(([colId, col], index) => {
|
||||
const def = operationDefinitionMap[col.operationType];
|
||||
if (def.input !== 'fullReference' && def.input !== 'managedReference') {
|
||||
|
@ -469,6 +477,7 @@ export function toExpression(
|
|||
indexPatterns: IndexPatternMap,
|
||||
uiSettings: IUiSettingsClient,
|
||||
dateRange: DateRange,
|
||||
nowInstant: Date,
|
||||
searchSessionId?: string
|
||||
) {
|
||||
if (state.layers[layerId]) {
|
||||
|
@ -477,6 +486,7 @@ export function toExpression(
|
|||
indexPatterns[state.layers[layerId].indexPatternId],
|
||||
uiSettings,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
searchSessionId
|
||||
);
|
||||
}
|
||||
|
|
|
@ -101,7 +101,8 @@ export function isColumnInvalid(
|
|||
layer: FormBasedLayer,
|
||||
columnId: string,
|
||||
indexPattern: IndexPattern,
|
||||
dateRange: DateRange | undefined
|
||||
dateRange: DateRange | undefined,
|
||||
targetBars: number
|
||||
) {
|
||||
const column: GenericIndexPatternColumn | undefined = layer.columns[columnId];
|
||||
if (!column || !indexPattern) return;
|
||||
|
@ -111,7 +112,9 @@ export function isColumnInvalid(
|
|||
const referencesHaveErrors =
|
||||
true &&
|
||||
'references' in column &&
|
||||
Boolean(getReferencesErrors(layer, column, indexPattern, dateRange).filter(Boolean).length);
|
||||
Boolean(
|
||||
getReferencesErrors(layer, column, indexPattern, dateRange, targetBars).filter(Boolean).length
|
||||
);
|
||||
|
||||
const operationErrorMessages =
|
||||
operationDefinition &&
|
||||
|
@ -120,7 +123,8 @@ export function isColumnInvalid(
|
|||
columnId,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
operationDefinitionMap
|
||||
operationDefinitionMap,
|
||||
targetBars
|
||||
);
|
||||
|
||||
// it looks like this is just a back-stop since we prevent
|
||||
|
@ -138,7 +142,8 @@ function getReferencesErrors(
|
|||
layer: FormBasedLayer,
|
||||
column: ReferenceBasedIndexPatternColumn,
|
||||
indexPattern: IndexPattern,
|
||||
dateRange: DateRange | undefined
|
||||
dateRange: DateRange | undefined,
|
||||
targetBars: number
|
||||
) {
|
||||
return column.references?.map((referenceId: string) => {
|
||||
const referencedOperation = layer.columns[referenceId]?.operationType;
|
||||
|
@ -148,7 +153,8 @@ function getReferencesErrors(
|
|||
referenceId,
|
||||
indexPattern,
|
||||
dateRange,
|
||||
operationDefinitionMap
|
||||
operationDefinitionMap,
|
||||
targetBars
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -680,9 +680,9 @@ describe('Textbased Data Source', () => {
|
|||
describe('#toExpression', () => {
|
||||
it('should generate an empty expression when no columns are selected', async () => {
|
||||
const state = TextBasedDatasource.initialize();
|
||||
expect(TextBasedDatasource.toExpression(state, 'first', indexPatterns, dateRange)).toEqual(
|
||||
null
|
||||
);
|
||||
expect(
|
||||
TextBasedDatasource.toExpression(state, 'first', indexPatterns, dateRange, new Date())
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should generate an expression for an SQL query', async () => {
|
||||
|
@ -732,8 +732,9 @@ describe('Textbased Data Source', () => {
|
|||
],
|
||||
} as unknown as TextBasedPrivateState;
|
||||
|
||||
expect(TextBasedDatasource.toExpression(queryBaseState, 'a', indexPatterns, dateRange))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(
|
||||
TextBasedDatasource.toExpression(queryBaseState, 'a', indexPatterns, dateRange, new Date())
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
|
|
|
@ -178,6 +178,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
visualizationMap={visualizationMap}
|
||||
frame={framePublicAPI}
|
||||
getUserMessages={props.getUserMessages}
|
||||
nowProvider={props.plugins.data.nowProvider}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ export function getDatasourceExpressionsByLayers(
|
|||
datasourceStates: DatasourceStates,
|
||||
indexPatterns: IndexPatternMap,
|
||||
dateRange: DateRange,
|
||||
nowInstant: Date,
|
||||
searchSessionId?: string
|
||||
): null | Record<string, Ast> {
|
||||
const datasourceExpressions: Array<[string, Ast | string]> = [];
|
||||
|
@ -32,6 +33,7 @@ export function getDatasourceExpressionsByLayers(
|
|||
layerId,
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
searchSessionId
|
||||
);
|
||||
if (result) {
|
||||
|
@ -63,6 +65,7 @@ export function buildExpression({
|
|||
description,
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
searchSessionId,
|
||||
}: {
|
||||
title?: string;
|
||||
|
@ -75,6 +78,7 @@ export function buildExpression({
|
|||
indexPatterns: IndexPatternMap;
|
||||
searchSessionId?: string;
|
||||
dateRange: DateRange;
|
||||
nowInstant: Date;
|
||||
}): Ast | null {
|
||||
if (visualization === null) {
|
||||
return null;
|
||||
|
@ -85,6 +89,7 @@ export function buildExpression({
|
|||
datasourceStates,
|
||||
indexPatterns,
|
||||
dateRange,
|
||||
nowInstant,
|
||||
searchSessionId
|
||||
);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { difference } from 'lodash';
|
|||
import type { DataViewsContract, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common';
|
||||
import type { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import {
|
||||
EventAnnotationGroupConfig,
|
||||
|
@ -346,6 +346,7 @@ export async function persistedStateToExpression(
|
|||
storage: IStorageWrapper;
|
||||
dataViews: DataViewsContract;
|
||||
timefilter: TimefilterContract;
|
||||
nowProvider: DataPublicPluginStart['nowProvider'];
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
}
|
||||
): Promise<DocumentToExpressionReturnType> {
|
||||
|
@ -432,6 +433,7 @@ export async function persistedStateToExpression(
|
|||
datasourceLayers,
|
||||
indexPatterns,
|
||||
dateRange: { fromDate: currentTimeRange.from, toDate: currentTimeRange.to },
|
||||
nowInstant: services.nowProvider.get(),
|
||||
}),
|
||||
activeVisualizationState,
|
||||
indexPatterns,
|
||||
|
|
|
@ -105,6 +105,7 @@ describe('suggestion_panel', () => {
|
|||
ExpressionRenderer: expressionRendererMock,
|
||||
frame: createMockFramePublicAPI(),
|
||||
getUserMessages: () => [],
|
||||
nowProvider: { get: jest.fn(() => new Date()) },
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
|
|||
import { Ast, fromExpression, toExpression } from '@kbn/interpreter';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { ExecutionContextSearch } from '@kbn/data-plugin/public';
|
||||
import { DataPublicPluginStart, ExecutionContextSearch } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
ReactExpressionRendererProps,
|
||||
ReactExpressionRendererType,
|
||||
|
@ -100,6 +100,7 @@ export interface SuggestionPanelProps {
|
|||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
frame: FramePublicAPI;
|
||||
getUserMessages: UserMessagesGetter;
|
||||
nowProvider: DataPublicPluginStart['nowProvider'];
|
||||
}
|
||||
|
||||
const PreviewRenderer = ({
|
||||
|
@ -219,6 +220,7 @@ export function SuggestionPanel({
|
|||
frame,
|
||||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
getUserMessages,
|
||||
nowProvider,
|
||||
}: SuggestionPanelProps) {
|
||||
const dispatchLens = useLensDispatch();
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
|
@ -289,7 +291,8 @@ export function SuggestionPanel({
|
|||
visualizationMap[suggestion.visualizationId],
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
frame,
|
||||
nowProvider
|
||||
),
|
||||
}));
|
||||
|
||||
|
@ -303,7 +306,8 @@ export function SuggestionPanel({
|
|||
visualizationMap[currentVisualization.activeId],
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
frame,
|
||||
nowProvider
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
@ -508,7 +512,8 @@ function getPreviewExpression(
|
|||
visualization: Visualization,
|
||||
datasources: Record<string, Datasource>,
|
||||
datasourceStates: DatasourceStates,
|
||||
frame: FramePublicAPI
|
||||
frame: FramePublicAPI,
|
||||
nowProvider: DataPublicPluginStart['nowProvider']
|
||||
) {
|
||||
if (!visualization.toPreviewExpression) {
|
||||
return null;
|
||||
|
@ -546,7 +551,8 @@ function getPreviewExpression(
|
|||
datasources,
|
||||
datasourceStates,
|
||||
frame.dataViews.indexPatterns,
|
||||
frame.dateRange
|
||||
frame.dateRange,
|
||||
nowProvider.get()
|
||||
);
|
||||
|
||||
return visualization.toPreviewExpression(
|
||||
|
@ -565,7 +571,8 @@ function preparePreviewExpression(
|
|||
visualization: Visualization,
|
||||
datasourceMap: DatasourceMap,
|
||||
datasourceStates: DatasourceStates,
|
||||
framePublicAPI: FramePublicAPI
|
||||
framePublicAPI: FramePublicAPI,
|
||||
nowProvider: DataPublicPluginStart['nowProvider']
|
||||
) {
|
||||
const suggestionDatasourceId = visualizableState.datasourceId;
|
||||
const suggestionDatasourceState = visualizableState.datasourceState;
|
||||
|
@ -585,7 +592,8 @@ function preparePreviewExpression(
|
|||
visualization,
|
||||
datasourceMap,
|
||||
datasourceStatesWithSuggestions,
|
||||
framePublicAPI
|
||||
framePublicAPI,
|
||||
nowProvider
|
||||
);
|
||||
|
||||
if (!expression) {
|
||||
|
|
|
@ -301,6 +301,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceLayers,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
dateRange: framePublicAPI.dateRange,
|
||||
nowInstant: plugins.data.nowProvider.get(),
|
||||
searchSessionId,
|
||||
});
|
||||
|
||||
|
@ -347,6 +348,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceLayers,
|
||||
dataViews.indexPatterns,
|
||||
framePublicAPI.dateRange,
|
||||
plugins.data.nowProvider,
|
||||
searchSessionId,
|
||||
addUserMessages,
|
||||
]);
|
||||
|
|
|
@ -59,6 +59,7 @@ export interface EditorFramePlugins {
|
|||
uiSettings: IUiSettingsClient;
|
||||
storage: IStorageWrapper;
|
||||
timefilter: TimefilterContract;
|
||||
nowProvider: DataPublicPluginStart['nowProvider'];
|
||||
eventAnnotationService: EventAnnotationServiceType;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ export function createMockDatasource(
|
|||
initialize: jest.fn((_state?) => {}),
|
||||
renderDataPanel: jest.fn(),
|
||||
renderLayerPanel: jest.fn(),
|
||||
toExpression: jest.fn((_frame, _state, _indexPatterns, dateRange) => null),
|
||||
toExpression: jest.fn((_frame, _state, _indexPatterns, dateRange, nowInstant) => null),
|
||||
insertLayer: jest.fn((_state, _newLayerId) => ({})),
|
||||
removeLayer: jest.fn((state, layerId) => ({ newState: state, removedLayerIds: [layerId] })),
|
||||
cloneLayer: jest.fn((_state, _layerId, _newLayerId, getNewId) => {}),
|
||||
|
|
|
@ -332,6 +332,7 @@ export class LensPlugin {
|
|||
storage: new Storage(localStorage),
|
||||
uiSettings: core.uiSettings,
|
||||
timefilter: plugins.data.query.timefilter.timefilter,
|
||||
nowProvider: plugins.data.nowProvider,
|
||||
eventAnnotationService,
|
||||
}),
|
||||
injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
|
|
|
@ -437,6 +437,7 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
layerId: string,
|
||||
indexPatterns: IndexPatternMap,
|
||||
dateRange: DateRange,
|
||||
nowInstant: Date,
|
||||
searchSessionId?: string
|
||||
) => ExpressionAstExpression | string | null;
|
||||
|
||||
|
|
|
@ -61,7 +61,8 @@ describe('#toExpression', () => {
|
|||
frame.datasourceLayers.first,
|
||||
'first',
|
||||
frame.dataViews.indexPatterns,
|
||||
frame.dateRange
|
||||
frame.dateRange,
|
||||
new Date()
|
||||
) ?? {
|
||||
type: 'expression',
|
||||
chain: [],
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/core-notifications-browser-mocks",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/core-lifecycle-browser-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue