[codemod][prereq] Convert Functions from arrow to function (#35749)

This commit is contained in:
Clint Andrew Hall 2019-04-29 14:07:53 -04:00 committed by GitHub
parent 6d45682c5c
commit 7aa698f522
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2639 additions and 2502 deletions

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const browser = () => ({
name: 'browser',
help: 'Force the interpreter to return to the browser',
args: {},
fn: context => context,
});
export function browser() {
return {
name: 'browser',
help: 'Force the interpreter to return to the browser',
args: {},
fn: context => context,
};
}

View file

@ -6,27 +6,29 @@
const noop = () => {};
export const location = () => ({
name: 'location',
type: 'datatable',
context: {
types: ['null'],
},
help:
"Use the browser's location functionality to get your current location. Usually quite slow, but fairly accurate",
fn: () => {
return new Promise(resolve => {
function createLocation(geoposition) {
const { latitude, longitude } = geoposition.coords;
return resolve({
type: 'datatable',
columns: [{ name: 'latitude', type: 'number' }, { name: 'longitude', type: 'number' }],
rows: [{ latitude, longitude }],
export function location() {
return {
name: 'location',
type: 'datatable',
context: {
types: ['null'],
},
help:
"Use the browser's location functionality to get your current location. Usually quite slow, but fairly accurate",
fn: () => {
return new Promise(resolve => {
function createLocation(geoposition) {
const { latitude, longitude } = geoposition.coords;
return resolve({
type: 'datatable',
columns: [{ name: 'latitude', type: 'number' }, { name: 'longitude', type: 'number' }],
rows: [{ latitude, longitude }],
});
}
return navigator.geolocation.getCurrentPosition(createLocation, noop, {
maximumAge: 5000,
});
}
return navigator.geolocation.getCurrentPosition(createLocation, noop, {
maximumAge: 5000,
});
});
},
});
},
};
}

View file

@ -6,47 +6,49 @@
import { Handlebars } from '../../../common/lib/handlebars';
export const markdown = () => ({
name: 'markdown',
aliases: [],
type: 'render',
help:
'An element for rendering markdown text. Great for single numbers, metrics or paragraphs of text.',
context: {
types: ['datatable', 'null'],
},
args: {
expression: {
aliases: ['_'],
types: ['string'],
help: 'A markdown expression. You can pass this multiple times to achieve concatenation',
default: '""',
multi: true,
export function markdown() {
return {
name: 'markdown',
aliases: [],
type: 'render',
help:
'An element for rendering markdown text. Great for single numbers, metrics or paragraphs of text.',
context: {
types: ['datatable', 'null'],
},
font: {
types: ['style'],
help: 'Font settings. Technically, you can add other styles in here as well',
default: '{font}',
},
},
fn: (context, args) => {
const compileFunctions = args.expression.map(str =>
Handlebars.compile(String(str), { knownHelpersOnly: true })
);
const ctx = {
columns: [],
rows: [],
type: null,
...context,
};
return {
type: 'render',
as: 'markdown',
value: {
content: compileFunctions.map(fn => fn(ctx)).join(''),
font: args.font,
args: {
expression: {
aliases: ['_'],
types: ['string'],
help: 'A markdown expression. You can pass this multiple times to achieve concatenation',
default: '""',
multi: true,
},
};
},
});
font: {
types: ['style'],
help: 'Font settings. Technically, you can add other styles in here as well',
default: '{font}',
},
},
fn: (context, args) => {
const compileFunctions = args.expression.map(str =>
Handlebars.compile(String(str), { knownHelpersOnly: true })
);
const ctx = {
columns: [],
rows: [],
type: null,
...context,
};
return {
type: 'render',
as: 'markdown',
value: {
content: compileFunctions.map(fn => fn(ctx)).join(''),
font: args.font,
},
};
},
};
}

View file

@ -6,30 +6,32 @@
import { parse } from 'url';
export const urlparam = () => ({
name: 'urlparam',
aliases: [],
type: 'string',
help:
'Access URL parameters and use them in expressions. Eg https://localhost:5601/app/canvas?myVar=20. This will always return a string',
context: {
types: ['null'],
},
args: {
param: {
types: ['string'],
aliases: ['_', 'var', 'variable'],
help: 'The URL hash parameter to access',
multi: false,
export function urlparam() {
return {
name: 'urlparam',
aliases: [],
type: 'string',
help:
'Access URL parameters and use them in expressions. Eg https://localhost:5601/app/canvas?myVar=20. This will always return a string',
context: {
types: ['null'],
},
default: {
types: ['string'],
default: '""',
help: 'Return this string if the url parameter is not defined',
args: {
param: {
types: ['string'],
aliases: ['_', 'var', 'variable'],
help: 'The URL hash parameter to access',
multi: false,
},
default: {
types: ['string'],
default: '""',
help: 'Return this string if the url parameter is not defined',
},
},
},
fn: (context, args) => {
const query = parse(window.location.href, true).query;
return query[args.param] || args.default;
},
});
fn: (context, args) => {
const query = parse(window.location.href, true).query;
return query[args.param] || args.default;
},
};
}

View file

@ -4,21 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const all = () => ({
name: 'all',
type: 'boolean',
help: 'Return true if all of the conditions are true',
args: {
condition: {
aliases: ['_'],
types: ['boolean', 'null'],
required: true,
multi: true,
help: 'One or more conditions to check',
export function all() {
return {
name: 'all',
type: 'boolean',
help: 'Return true if all of the conditions are true',
args: {
condition: {
aliases: ['_'],
types: ['boolean', 'null'],
required: true,
multi: true,
help: 'One or more conditions to check',
},
},
},
fn: (context, args) => {
const conditions = args.condition || [];
return conditions.every(Boolean);
},
});
fn: (context, args) => {
const conditions = args.condition || [];
return conditions.every(Boolean);
},
};
}

View file

@ -6,88 +6,90 @@
import { omit } from 'lodash';
export const alterColumn = () => ({
name: 'alterColumn',
type: 'datatable',
help: 'Converts between core types, eg string, number, null, boolean, date and rename columns',
context: {
types: ['datatable'],
},
args: {
column: {
aliases: ['_'],
types: ['string'],
help: 'The name of the column to alter',
export function alterColumn() {
return {
name: 'alterColumn',
type: 'datatable',
help: 'Converts between core types, eg string, number, null, boolean, date and rename columns',
context: {
types: ['datatable'],
},
type: {
types: ['string'],
help: 'The type to convert the column to. Leave blank to not change type',
default: null,
options: ['null', 'boolean', 'number', 'string'],
args: {
column: {
aliases: ['_'],
types: ['string'],
help: 'The name of the column to alter',
},
type: {
types: ['string'],
help: 'The type to convert the column to. Leave blank to not change type',
default: null,
options: ['null', 'boolean', 'number', 'string'],
},
name: {
types: ['string'],
help: 'The resultant column name. Leave blank to not rename',
default: null,
},
},
name: {
types: ['string'],
help: 'The resultant column name. Leave blank to not rename',
default: null,
},
},
fn: (context, args) => {
if (!args.column || (!args.type && !args.name)) {
return context;
}
const column = context.columns.find(col => col.name === args.column);
if (!column) {
throw new Error(`Column not found: '${args.column}'`);
}
const name = args.name || column.name;
const type = args.type || column.type;
const columns = context.columns.reduce((all, col) => {
if (col.name !== args.name) {
if (col.name !== column.name) {
all.push(col);
} else {
all.push({ name, type });
}
fn: (context, args) => {
if (!args.column || (!args.type && !args.name)) {
return context;
}
return all;
}, []);
let handler = val => val;
const column = context.columns.find(col => col.name === args.column);
if (!column) {
throw new Error(`Column not found: '${args.column}'`);
}
if (args.type) {
handler = (function getHandler() {
switch (type) {
case 'string':
if (column.type === 'date') {
return v => new Date(v).toISOString();
}
return String;
case 'number':
return Number;
case 'date':
return v => new Date(v).valueOf();
case 'boolean':
return Boolean;
case 'null':
return () => null;
default:
throw new Error(`Cannot convert to '${type}'`);
const name = args.name || column.name;
const type = args.type || column.type;
const columns = context.columns.reduce((all, col) => {
if (col.name !== args.name) {
if (col.name !== column.name) {
all.push(col);
} else {
all.push({ name, type });
}
}
})();
}
return all;
}, []);
const rows = context.rows.map(row => ({
...omit(row, column.name),
[name]: handler(row[column.name]),
}));
let handler = val => val;
return {
type: 'datatable',
columns,
rows,
};
},
});
if (args.type) {
handler = (function getHandler() {
switch (type) {
case 'string':
if (column.type === 'date') {
return v => new Date(v).toISOString();
}
return String;
case 'number':
return Number;
case 'date':
return v => new Date(v).valueOf();
case 'boolean':
return Boolean;
case 'null':
return () => null;
default:
throw new Error(`Cannot convert to '${type}'`);
}
})();
}
const rows = context.rows.map(row => ({
...omit(row, column.name),
[name]: handler(row[column.name]),
}));
return {
type: 'datatable',
columns,
rows,
};
},
};
}

View file

@ -4,21 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const any = () => ({
name: 'any',
type: 'boolean',
help: 'Return true if any of the conditions are true',
args: {
condition: {
aliases: ['_'],
types: ['boolean', 'null'],
required: true,
multi: true,
help: 'One or more conditions to check',
export function any() {
return {
name: 'any',
type: 'boolean',
help: 'Return true if any of the conditions are true',
args: {
condition: {
aliases: ['_'],
types: ['boolean', 'null'],
required: true,
multi: true,
help: 'One or more conditions to check',
},
},
},
fn: (context, args) => {
const conditions = args.condition || [];
return conditions.some(Boolean);
},
});
fn: (context, args) => {
const conditions = args.condition || [];
return conditions.some(Boolean);
},
};
}

View file

@ -6,35 +6,37 @@
import { getType } from '@kbn/interpreter/common';
export const asFn = () => ({
name: 'as',
type: 'datatable',
context: {
types: ['string', 'boolean', 'number', 'null'],
},
help: 'Creates a datatable with a single value',
args: {
name: {
types: ['string'],
aliases: ['_'],
help: 'A name to give the column',
default: 'value',
export function asFn() {
return {
name: 'as',
type: 'datatable',
context: {
types: ['string', 'boolean', 'number', 'null'],
},
},
fn: (context, args) => {
return {
type: 'datatable',
columns: [
{
name: args.name,
type: getType(context),
},
],
rows: [
{
[args.name]: context,
},
],
};
},
});
help: 'Creates a datatable with a single value',
args: {
name: {
types: ['string'],
aliases: ['_'],
help: 'A name to give the column',
default: 'value',
},
},
fn: (context, args) => {
return {
type: 'datatable',
columns: [
{
name: args.name,
type: getType(context),
},
],
rows: [
{
[args.name]: context,
},
],
};
},
};
}

View file

@ -5,70 +5,72 @@
*/
import moment from 'moment';
export const axisConfig = () => ({
name: 'axisConfig',
aliases: [],
type: 'axisConfig',
context: {
types: ['datatable'],
},
help: 'Configure axis of a visualization',
args: {
show: {
types: ['boolean'],
help: 'Show the axis labels?',
default: true,
export function axisConfig() {
return {
name: 'axisConfig',
aliases: [],
type: 'axisConfig',
context: {
types: ['datatable'],
},
position: {
types: ['string'],
help: 'Position of the axis labels - top, bottom, left, and right',
options: ['top', 'bottom', 'left', 'right'],
default: 'left',
help: 'Configure axis of a visualization',
args: {
show: {
types: ['boolean'],
help: 'Show the axis labels?',
default: true,
},
position: {
types: ['string'],
help: 'Position of the axis labels - top, bottom, left, and right',
options: ['top', 'bottom', 'left', 'right'],
default: 'left',
},
min: {
types: ['number', 'date', 'string', 'null'],
help:
'Minimum value displayed in the axis. Must be a number or a date in ms or ISO8601 string',
},
max: {
types: ['number', 'date', 'string', 'null'],
help:
'Maximum value displayed in the axis. Must be a number or a date in ms or ISO8601 string',
},
tickSize: {
types: ['number', 'null'],
help: 'Increment size between each tick. Use for number axes only',
},
},
min: {
types: ['number', 'date', 'string', 'null'],
help:
'Minimum value displayed in the axis. Must be a number or a date in ms or ISO8601 string',
},
max: {
types: ['number', 'date', 'string', 'null'],
help:
'Maximum value displayed in the axis. Must be a number or a date in ms or ISO8601 string',
},
tickSize: {
types: ['number', 'null'],
help: 'Increment size between each tick. Use for number axes only',
},
},
fn: (context, args) => {
const positions = ['top', 'bottom', 'left', 'right', ''];
if (!positions.includes(args.position)) {
throw new Error(`Invalid position: '${args.position}'`);
}
fn: (context, args) => {
const positions = ['top', 'bottom', 'left', 'right', ''];
if (!positions.includes(args.position)) {
throw new Error(`Invalid position: '${args.position}'`);
}
const min = typeof args.min === 'string' ? moment.utc(args.min).valueOf() : args.min;
const max = typeof args.max === 'string' ? moment.utc(args.max).valueOf() : args.max;
const min = typeof args.min === 'string' ? moment.utc(args.min).valueOf() : args.min;
const max = typeof args.max === 'string' ? moment.utc(args.max).valueOf() : args.max;
if (min != null && isNaN(min)) {
throw new Error(
`Invalid date string: '${
args.min
}'. 'min' must be a number, date in ms, or ISO8601 date string`
);
}
if (max != null && isNaN(max)) {
throw new Error(
`Invalid date string: '${
args.max
}'. 'max' must be a number, date in ms, or ISO8601 date string`
);
}
if (min != null && isNaN(min)) {
throw new Error(
`Invalid date string: '${
args.min
}'. 'min' must be a number, date in ms, or ISO8601 date string`
);
}
if (max != null && isNaN(max)) {
throw new Error(
`Invalid date string: '${
args.max
}'. 'max' must be a number, date in ms, or ISO8601 date string`
);
}
return {
type: 'axisConfig',
...args,
min,
max,
};
},
});
return {
type: 'axisConfig',
...args,
min,
max,
};
},
};
}

View file

@ -4,33 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const caseFn = () => ({
name: 'case',
type: 'case',
help: 'Build a case (including a condition/result) to pass to the switch function',
args: {
when: {
aliases: ['_'],
resolve: false,
help:
'This value is compared to the context to see if the condition is met. It is overridden by the "if" argument if both are provided.',
export function caseFn() {
return {
name: 'case',
type: 'case',
help: 'Build a case (including a condition/result) to pass to the switch function',
args: {
when: {
aliases: ['_'],
resolve: false,
help:
'This value is compared to the context to see if the condition is met. It is overridden by the "if" argument if both are provided.',
},
if: {
types: ['boolean'],
help:
'This value is used as whether or not the condition is met. It overrides the unnamed argument if both are provided.',
},
then: {
resolve: false,
help: 'The value to return if the condition is met',
},
},
if: {
types: ['boolean'],
help:
'This value is used as whether or not the condition is met. It overrides the unnamed argument if both are provided.',
fn: async (context, args) => {
const matches = await doesMatch(context, args);
const result = matches ? await getResult(context, args) : null;
return { type: 'case', matches, result };
},
then: {
resolve: false,
help: 'The value to return if the condition is met',
},
},
fn: async (context, args) => {
const matches = await doesMatch(context, args);
const result = matches ? await getResult(context, args) : null;
return { type: 'case', matches, result };
},
});
};
}
async function doesMatch(context, args) {
if (typeof args.if !== 'undefined') {

View file

@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const clear = () => ({
name: 'clear',
type: 'null',
help: 'Clears context and returns null',
args: {},
fn: () => null,
});
export function clear() {
return {
name: 'clear',
type: 'null',
help: 'Clears context and returns null',
args: {},
fn: () => null,
};
}

View file

@ -6,54 +6,56 @@
import { omit, pick, find } from 'lodash';
export const columns = () => ({
name: 'columns',
type: 'datatable',
help:
'Include or exclude columns from a data table. If you specify both, this will exclude first',
context: {
types: ['datatable'],
},
args: {
include: {
types: ['string'],
help: 'A comma separated list of column names to keep in the table',
default: null,
export function columns() {
return {
name: 'columns',
type: 'datatable',
help:
'Include or exclude columns from a data table. If you specify both, this will exclude first',
context: {
types: ['datatable'],
},
exclude: {
types: ['string'],
help: 'A comma separated list of column names to remove from the table',
default: null,
args: {
include: {
types: ['string'],
help: 'A comma separated list of column names to keep in the table',
default: null,
},
exclude: {
types: ['string'],
help: 'A comma separated list of column names to remove from the table',
default: null,
},
},
},
fn: (context, args) => {
const { include, exclude } = args;
fn: (context, args) => {
const { include, exclude } = args;
let result = { ...context };
let result = { ...context };
if (exclude) {
const fields = exclude.split(',').map(field => field.trim());
const columns = result.columns.filter(col => !fields.includes(col.name));
const rows = columns.length > 0 ? result.rows.map(row => omit(row, fields)) : [];
if (exclude) {
const fields = exclude.split(',').map(field => field.trim());
const columns = result.columns.filter(col => !fields.includes(col.name));
const rows = columns.length > 0 ? result.rows.map(row => omit(row, fields)) : [];
result = { ...result, rows, columns };
}
result = { ...result, rows, columns };
}
if (include) {
const fields = include.split(',').map(field => field.trim());
//const columns = result.columns.filter(col => fields.includes(col.name));
// Include columns in the order the user specified
const columns = [];
fields.forEach(field => {
const column = find(result.columns, { name: field });
if (column) {
columns.push(column);
}
});
const rows = columns.length > 0 ? result.rows.map(row => pick(row, fields)) : [];
result = { ...result, rows, columns };
}
if (include) {
const fields = include.split(',').map(field => field.trim());
//const columns = result.columns.filter(col => fields.includes(col.name));
// Include columns in the order the user specified
const columns = [];
fields.forEach(field => {
const column = find(result.columns, { name: field });
if (column) {
columns.push(column);
}
});
const rows = columns.length > 0 ? result.rows.map(row => pick(row, fields)) : [];
result = { ...result, rows, columns };
}
return result;
},
});
return result;
},
};
}

View file

@ -4,66 +4,68 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const compare = () => ({
name: 'compare',
help:
'Compare the input to something else to determine true or false. Usually used in combination with `{if}`. This only works with primitive types, such as number, string, and boolean.',
aliases: ['condition'],
example: 'math "random()" | compare gt this=0.5',
type: 'boolean',
context: {
types: ['null', 'string', 'number', 'boolean'],
},
args: {
op: {
aliases: ['_'],
types: ['string'],
default: 'eq',
help:
'The operator to use in the comparison: ' +
' eq (equal), ne (not equal), lt (less than), gt (greater than), lte (less than equal), gte (greater than eq)',
options: ['eq', 'ne', 'lt', 'gt', 'lte', 'gte'],
export function compare() {
return {
name: 'compare',
help:
'Compare the input to something else to determine true or false. Usually used in combination with `{if}`. This only works with primitive types, such as number, string, and boolean.',
aliases: ['condition'],
example: 'math "random()" | compare gt this=0.5',
type: 'boolean',
context: {
types: ['null', 'string', 'number', 'boolean'],
},
to: {
aliases: ['this', 'b'],
help: 'The value to compare the context to, usually returned by a subexpression',
args: {
op: {
aliases: ['_'],
types: ['string'],
default: 'eq',
help:
'The operator to use in the comparison: ' +
' eq (equal), ne (not equal), lt (less than), gt (greater than), lte (less than equal), gte (greater than eq)',
options: ['eq', 'ne', 'lt', 'gt', 'lte', 'gte'],
},
to: {
aliases: ['this', 'b'],
help: 'The value to compare the context to, usually returned by a subexpression',
},
},
},
fn: (context, args) => {
const a = context;
const b = args.to;
const op = args.op;
const typesMatch = typeof a === typeof b;
fn: (context, args) => {
const a = context;
const b = args.to;
const op = args.op;
const typesMatch = typeof a === typeof b;
switch (op) {
case 'eq':
return a === b;
case 'ne':
return a !== b;
case 'lt':
if (typesMatch) {
return a < b;
}
return false;
case 'lte':
if (typesMatch) {
return a <= b;
}
return false;
case 'gt':
if (typesMatch) {
return a > b;
}
return false;
case 'gte':
if (typesMatch) {
return a >= b;
}
return false;
default:
throw new Error(`Invalid compare operator: '${op}'. Use eq, ne, lt, gt, lte, or gte.`);
}
switch (op) {
case 'eq':
return a === b;
case 'ne':
return a !== b;
case 'lt':
if (typesMatch) {
return a < b;
}
return false;
case 'lte':
if (typesMatch) {
return a <= b;
}
return false;
case 'gt':
if (typesMatch) {
return a > b;
}
return false;
case 'gte':
if (typesMatch) {
return a >= b;
}
return false;
default:
throw new Error(`Invalid compare operator: '${op}'. Use eq, ne, lt, gt, lte, or gte.`);
}
return false;
},
});
return false;
},
};
}

View file

@ -6,76 +6,78 @@
import { isValidUrl } from '../../../common/lib/url';
export const containerStyle = () => ({
name: 'containerStyle',
aliases: [],
context: {
types: ['null'],
},
type: 'containerStyle',
help:
'Creates an object used for describing the properties of a series on a chart.' +
' You would usually use this inside of a charting function',
args: {
border: {
types: ['string', 'null'],
help: 'Valid CSS border string',
export function containerStyle() {
return {
name: 'containerStyle',
aliases: [],
context: {
types: ['null'],
},
borderRadius: {
types: ['string', 'null'],
help: 'Number of pixels to use when rounding the border',
type: 'containerStyle',
help:
'Creates an object used for describing the properties of a series on a chart.' +
' You would usually use this inside of a charting function',
args: {
border: {
types: ['string', 'null'],
help: 'Valid CSS border string',
},
borderRadius: {
types: ['string', 'null'],
help: 'Number of pixels to use when rounding the border',
},
padding: {
types: ['string', 'null'],
help: 'Content distance in pixels from border',
},
backgroundColor: {
types: ['string', 'null'],
help: 'Valid CSS background color string',
},
backgroundImage: {
types: ['string', 'null'],
help: 'Valid CSS background image string',
},
backgroundSize: {
types: ['string'],
help: 'Valid CSS background size string',
default: 'contain',
options: ['contain', 'cover', 'auto'],
},
backgroundRepeat: {
types: ['string'],
help: 'Valid CSS background repeat string',
default: 'no-repeat',
options: ['repeat-x', 'repeat', 'space', 'round', 'no-repeat', 'space'],
},
opacity: {
types: ['number', 'null'],
help: 'A number between 0 and 1 representing the degree of transparency of the element',
},
overflow: {
types: ['string'],
help: 'Sets overflow of the container',
options: ['visible', 'hidden', 'scroll', 'auto'],
},
},
padding: {
types: ['string', 'null'],
help: 'Content distance in pixels from border',
},
backgroundColor: {
types: ['string', 'null'],
help: 'Valid CSS background color string',
},
backgroundImage: {
types: ['string', 'null'],
help: 'Valid CSS background image string',
},
backgroundSize: {
types: ['string'],
help: 'Valid CSS background size string',
default: 'contain',
options: ['contain', 'cover', 'auto'],
},
backgroundRepeat: {
types: ['string'],
help: 'Valid CSS background repeat string',
default: 'no-repeat',
options: ['repeat-x', 'repeat', 'space', 'round', 'no-repeat', 'space'],
},
opacity: {
types: ['number', 'null'],
help: 'A number between 0 and 1 representing the degree of transparency of the element',
},
overflow: {
types: ['string'],
help: 'Sets overflow of the container',
options: ['visible', 'hidden', 'scroll', 'auto'],
},
},
fn: (context, args) => {
const { backgroundImage, backgroundSize, backgroundRepeat, ...remainingArgs } = args;
const style = {
type: 'containerStyle',
...remainingArgs,
};
fn: (context, args) => {
const { backgroundImage, backgroundSize, backgroundRepeat, ...remainingArgs } = args;
const style = {
type: 'containerStyle',
...remainingArgs,
};
if (backgroundImage) {
if (!isValidUrl(backgroundImage)) {
throw new Error('Invalid backgroundImage. Please provide an asset or a URL.');
if (backgroundImage) {
if (!isValidUrl(backgroundImage)) {
throw new Error('Invalid backgroundImage. Please provide an asset or a URL.');
}
style.backgroundImage = `url(${backgroundImage})`;
style.backgroundSize = backgroundSize;
style.backgroundRepeat = backgroundRepeat;
}
style.backgroundImage = `url(${backgroundImage})`;
style.backgroundSize = backgroundSize;
style.backgroundRepeat = backgroundRepeat;
}
// removes keys with undefined value
return JSON.parse(JSON.stringify(style));
},
});
// removes keys with undefined value
return JSON.parse(JSON.stringify(style));
},
};
}

View file

@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const context = () => ({
name: 'context',
help:
'Returns whatever you pass into it. This can be useful when you need to use context as argument to a function as a sub-expression',
args: {},
fn: context => {
return context;
},
});
export function context() {
return {
name: 'context',
help:
'Returns whatever you pass into it. This can be useful when you need to use context as argument to a function as a sub-expression',
args: {},
fn: context => {
return context;
},
};
}

View file

@ -6,75 +6,77 @@
import Papa from 'papaparse';
export const csv = () => ({
name: 'csv',
type: 'datatable',
context: {
types: ['null'],
},
args: {
data: {
aliases: ['_'],
types: ['string'],
help: 'CSV data to use',
export function csv() {
return {
name: 'csv',
type: 'datatable',
context: {
types: ['null'],
},
delimiter: {
types: ['string'],
help: 'Data separation character',
},
newline: {
types: ['string'],
help: 'Row separation character',
},
},
help: 'Create datatable from csv input',
fn(context, args) {
const { data: csvString, delimiter, newline } = args;
const config = {
transform: val => {
if (val.indexOf('"') >= 0) {
const trimmed = val.trim();
return trimmed.replace(/(^\"|\"$)/g, '');
}
return val;
args: {
data: {
aliases: ['_'],
types: ['string'],
help: 'CSV data to use',
},
};
if (delimiter != null) {
config.delimiter = delimiter;
}
if (newline != null) {
config.newline = newline;
}
// TODO: handle errors, check output.errors
const output = Papa.parse(csvString, config);
// output.data is an array of arrays, rows and values in each row
return output.data.reduce(
(acc, row, i) => {
if (i === 0) {
// first row, assume header values
row.forEach(colName => acc.columns.push({ name: colName.trim(), type: 'string' }));
} else {
// any other row is a data row
const rowObj = row.reduce((rowAcc, colValue, j) => {
const colName = acc.columns[j].name;
rowAcc[colName] = colValue;
return rowAcc;
}, {});
acc.rows.push(rowObj);
}
return acc;
delimiter: {
types: ['string'],
help: 'Data separation character',
},
{
type: 'datatable',
columns: [],
rows: [],
newline: {
types: ['string'],
help: 'Row separation character',
},
},
help: 'Create datatable from csv input',
fn(context, args) {
const { data: csvString, delimiter, newline } = args;
const config = {
transform: val => {
if (val.indexOf('"') >= 0) {
const trimmed = val.trim();
return trimmed.replace(/(^\"|\"$)/g, '');
}
return val;
},
};
if (delimiter != null) {
config.delimiter = delimiter;
}
);
},
});
if (newline != null) {
config.newline = newline;
}
// TODO: handle errors, check output.errors
const output = Papa.parse(csvString, config);
// output.data is an array of arrays, rows and values in each row
return output.data.reduce(
(acc, row, i) => {
if (i === 0) {
// first row, assume header values
row.forEach(colName => acc.columns.push({ name: colName.trim(), type: 'string' }));
} else {
// any other row is a data row
const rowObj = row.reduce((rowAcc, colValue, j) => {
const colName = acc.columns[j].name;
rowAcc[colName] = colValue;
return rowAcc;
}, {});
acc.rows.push(rowObj);
}
return acc;
},
{
type: 'datatable',
columns: [],
rows: [],
}
);
},
};
}

View file

@ -16,36 +16,40 @@ const getInputDate = input => {
return input;
};
export const date = () => ({
name: 'date',
type: 'number',
context: {
types: ['null'],
},
help: 'Returns the current time, or a time parsed from a string, as milliseconds since epoch',
args: {
value: {
aliases: ['_'],
types: ['string', 'null'],
help:
'An optional date string to parse into milliseconds since epoch ' +
'Can be either a valid Javascript Date input or a string to parse using the format argument. Must be an ISO 8601 string or you must provide the format',
export function date() {
return {
name: 'date',
type: 'number',
context: {
types: ['null'],
},
format: {
types: ['string'],
help:
'The momentJS format for parsing the optional date string (See https://momentjs.com/docs/#/displaying/)',
help: 'Returns the current time, or a time parsed from a string, as milliseconds since epoch',
args: {
value: {
aliases: ['_'],
types: ['string', 'null'],
help:
'An optional date string to parse into milliseconds since epoch ' +
'Can be either a valid Javascript Date input or a string to parse using the format argument. Must be an ISO 8601 string or you must provide the format',
},
format: {
types: ['string'],
help:
'The momentJS format for parsing the optional date string (See https://momentjs.com/docs/#/displaying/)',
},
},
},
fn: (context, args) => {
const { value: date, format } = args;
const useMoment = date && format;
const outputDate = useMoment ? moment.utc(date, format).toDate() : new Date(getInputDate(date));
fn: (context, args) => {
const { value: date, format } = args;
const useMoment = date && format;
const outputDate = useMoment
? moment.utc(date, format).toDate()
: new Date(getInputDate(date));
if (isNaN(outputDate.getTime())) {
throw new Error(`Invalid date input: ${date}`);
}
if (isNaN(outputDate.getTime())) {
throw new Error(`Invalid date input: ${date}`);
}
return outputDate.valueOf();
},
});
return outputDate.valueOf();
},
};
}

View file

@ -4,17 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const doFn = () => ({
name: 'do',
help:
'Runs multiple sub-expressions. Returns the passed in context. Nice for running actions producing functions.',
args: {
fn: {
aliases: ['_'],
multi: true,
help:
'One or more sub-expressions. The value of these is not available in the root pipeline as this function simply returns the passed in context',
export function doFn() {
return {
name: 'do',
help:
'Runs multiple sub-expressions. Returns the passed in context. Nice for running actions producing functions.',
args: {
fn: {
aliases: ['_'],
multi: true,
help:
'One or more sub-expressions. The value of these is not available in the root pipeline as this function simply returns the passed in context',
},
},
},
fn: context => context,
});
fn: context => context,
};
}

View file

@ -6,39 +6,41 @@
import { uniq } from 'lodash';
export const dropdownControl = () => ({
name: 'dropdownControl',
aliases: [],
type: 'render',
context: {
types: ['datatable'],
},
help: 'Configure a drop down filter control element',
args: {
filterColumn: {
type: ['string'],
help: 'The column or field to attach the filter to',
export function dropdownControl() {
return {
name: 'dropdownControl',
aliases: [],
type: 'render',
context: {
types: ['datatable'],
},
valueColumn: {
type: ['string'],
help: 'The datatable column from which to extract the unique values for the drop down',
},
},
fn: (context, { valueColumn, filterColumn }) => {
let choices = [];
if (context.rows[0][valueColumn]) {
choices = uniq(context.rows.map(row => row[valueColumn])).sort();
}
const column = filterColumn || valueColumn;
return {
type: 'render',
as: 'dropdown_filter',
value: {
column,
choices,
help: 'Configure a drop down filter control element',
args: {
filterColumn: {
type: ['string'],
help: 'The column or field to attach the filter to',
},
};
},
});
valueColumn: {
type: ['string'],
help: 'The datatable column from which to extract the unique values for the drop down',
},
},
fn: (context, { valueColumn, filterColumn }) => {
let choices = [];
if (context.rows[0][valueColumn]) {
choices = uniq(context.rows.map(row => row[valueColumn])).sort();
}
const column = filterColumn || valueColumn;
return {
type: 'render',
as: 'dropdown_filter',
value: {
column,
choices,
},
};
},
};
}

View file

@ -4,19 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const eq = () => ({
name: 'eq',
type: 'boolean',
help: 'Return if the context is equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
export function eq() {
return {
name: 'eq',
type: 'boolean',
help: 'Return if the context is equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
},
},
},
fn: (context, args) => {
return context === args.value;
},
});
fn: (context, args) => {
return context === args.value;
},
};
}

View file

@ -4,35 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const exactly = () => ({
name: 'exactly',
aliases: [],
type: 'filter',
context: {
types: ['filter'],
},
help: 'Create a filter that matches a given column for a perfectly exact value',
args: {
column: {
types: ['string'],
aliases: ['field', 'c'],
help: 'The column or field to attach the filter to',
export function exactly() {
return {
name: 'exactly',
aliases: [],
type: 'filter',
context: {
types: ['filter'],
},
value: {
types: ['string'],
aliases: ['v', 'val'],
help: 'The value to match exactly, including white space and capitalization',
help: 'Create a filter that matches a given column for a perfectly exact value',
args: {
column: {
types: ['string'],
aliases: ['field', 'c'],
help: 'The column or field to attach the filter to',
},
value: {
types: ['string'],
aliases: ['v', 'val'],
help: 'The value to match exactly, including white space and capitalization',
},
},
},
fn: (context, args) => {
const { value, column } = args;
fn: (context, args) => {
const { value, column } = args;
const filter = {
type: 'exactly',
value,
column,
};
const filter = {
type: 'exactly',
value,
column,
};
return { ...context, and: [...context.and, filter] };
},
});
return { ...context, and: [...context.and, filter] };
},
};
}

View file

@ -4,37 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const filterrows = () => ({
name: 'filterrows',
aliases: [],
type: 'datatable',
context: {
types: ['datatable'],
},
help: 'Filter rows in a datatable based on the return value of a subexpression.',
args: {
fn: {
resolve: false,
aliases: ['_'],
types: ['boolean'],
help:
'An expression to pass each rows in the datatable into. The expression should return a boolean. ' +
'A true value will preserve the row, and a false value will remove it.',
export function filterrows() {
return {
name: 'filterrows',
aliases: [],
type: 'datatable',
context: {
types: ['datatable'],
},
},
fn(context, { fn }) {
const checks = context.rows.map(row =>
fn({
...context,
rows: [row],
})
);
help: 'Filter rows in a datatable based on the return value of a subexpression.',
args: {
fn: {
resolve: false,
aliases: ['_'],
types: ['boolean'],
help:
'An expression to pass each rows in the datatable into. The expression should return a boolean. ' +
'A true value will preserve the row, and a false value will remove it.',
},
},
fn(context, { fn }) {
const checks = context.rows.map(row =>
fn({
...context,
rows: [row],
})
);
return Promise.all(checks)
.then(results => context.rows.filter((row, i) => results[i]))
.then(rows => ({
...context,
rows,
}));
},
});
return Promise.all(checks)
.then(results => context.rows.filter((row, i) => results[i]))
.then(rows => ({
...context,
rows,
}));
},
};
}

View file

@ -24,90 +24,92 @@ const weights = [
];
const alignments = ['center', 'left', 'right', 'justify'];
export const font = () => ({
name: 'font',
aliases: [],
type: 'style',
help: 'Create a font style',
context: {
types: ['null'],
},
args: {
size: {
types: ['number'],
help: 'Font size (px)',
default: 14,
export function font() {
return {
name: 'font',
aliases: [],
type: 'style',
help: 'Create a font style',
context: {
types: ['null'],
},
lHeight: {
types: ['number'],
aliases: ['lineHeight'],
help: 'Line height (px)',
args: {
size: {
types: ['number'],
help: 'Font size (px)',
default: 14,
},
lHeight: {
types: ['number'],
aliases: ['lineHeight'],
help: 'Line height (px)',
},
family: {
types: ['string'],
default: `"${openSans.value}"`,
help: 'An acceptable CSS web font string',
},
color: {
types: ['string', 'null'],
help: 'Text color',
},
weight: {
types: ['string'],
help:
'Set the font weight, e.g. normal, bold, bolder, lighter, 100, 200, 300, 400, 500, 600, 700, 800, 900',
default: 'normal',
options: weights,
},
underline: {
types: ['boolean'],
default: false,
help: 'Underline the text, true or false',
options: [true, false],
},
italic: {
types: ['boolean'],
default: false,
help: 'Italicize, true or false',
options: [true, false],
},
align: {
types: ['string'],
help: 'Horizontal text alignment',
default: 'left',
options: alignments,
},
},
family: {
types: ['string'],
default: `"${openSans.value}"`,
help: 'An acceptable CSS web font string',
},
color: {
types: ['string', 'null'],
help: 'Text color',
},
weight: {
types: ['string'],
help:
'Set the font weight, e.g. normal, bold, bolder, lighter, 100, 200, 300, 400, 500, 600, 700, 800, 900',
default: 'normal',
options: weights,
},
underline: {
types: ['boolean'],
default: false,
help: 'Underline the text, true or false',
options: [true, false],
},
italic: {
types: ['boolean'],
default: false,
help: 'Italicize, true or false',
options: [true, false],
},
align: {
types: ['string'],
help: 'Horizontal text alignment',
default: 'left',
options: alignments,
},
},
fn: (context, args) => {
if (!weights.includes(args.weight)) {
throw new Error(`Invalid font weight: '${args.weight}'`);
}
if (!alignments.includes(args.align)) {
throw new Error(`Invalid text alignment: '${args.align}'`);
}
fn: (context, args) => {
if (!weights.includes(args.weight)) {
throw new Error(`Invalid font weight: '${args.weight}'`);
}
if (!alignments.includes(args.align)) {
throw new Error(`Invalid text alignment: '${args.align}'`);
}
// the line height shouldn't ever be lower than the size
const lineHeight = args.lHeight ? `${args.lHeight}px` : 1;
// the line height shouldn't ever be lower than the size
const lineHeight = args.lHeight ? `${args.lHeight}px` : 1;
const spec = {
fontFamily: args.family,
fontWeight: args.weight,
fontStyle: args.italic ? 'italic' : 'normal',
textDecoration: args.underline ? 'underline' : 'none',
textAlign: args.align,
fontSize: `${args.size}px`, // apply font size as a pixel setting
lineHeight: lineHeight, // apply line height as a pixel setting
};
const spec = {
fontFamily: args.family,
fontWeight: args.weight,
fontStyle: args.italic ? 'italic' : 'normal',
textDecoration: args.underline ? 'underline' : 'none',
textAlign: args.align,
fontSize: `${args.size}px`, // apply font size as a pixel setting
lineHeight: lineHeight, // apply line height as a pixel setting
};
// conditionally apply styles based on input
if (args.color) {
spec.color = args.color;
}
// conditionally apply styles based on input
if (args.color) {
spec.color = args.color;
}
return {
type: 'style',
spec,
css: inlineStyle(spec),
};
},
});
return {
type: 'style',
spec,
css: inlineStyle(spec),
};
},
};
}

View file

@ -5,24 +5,26 @@
*/
import moment from 'moment';
export const formatdate = () => ({
name: 'formatdate',
type: 'string',
help: 'Output a ms since epoch number as a formatted string',
context: {
types: ['number'],
},
args: {
format: {
aliases: ['_'],
types: ['string'],
help: 'MomentJS Format with which to bucket (See https://momentjs.com/docs/#/displaying/)',
export function formatdate() {
return {
name: 'formatdate',
type: 'string',
help: 'Output a ms since epoch number as a formatted string',
context: {
types: ['number'],
},
},
fn: (context, args) => {
if (!args.format) {
return moment.utc(new Date(context)).toISOString();
}
return moment.utc(new Date(context)).format(args.format);
},
});
args: {
format: {
aliases: ['_'],
types: ['string'],
help: 'MomentJS Format with which to bucket (See https://momentjs.com/docs/#/displaying/)',
},
},
fn: (context, args) => {
if (!args.format) {
return moment.utc(new Date(context)).toISOString();
}
return moment.utc(new Date(context)).format(args.format);
},
};
}

View file

@ -6,24 +6,26 @@
import numeral from '@elastic/numeral';
export const formatnumber = () => ({
name: 'formatnumber',
type: 'string',
help: 'Turn a number into a string using a NumberJS format',
context: {
types: ['number'],
},
args: {
format: {
aliases: ['_'],
types: ['string'],
help: 'NumeralJS format string http://numeraljs.com/#format',
export function formatnumber() {
return {
name: 'formatnumber',
type: 'string',
help: 'Turn a number into a string using a NumberJS format',
context: {
types: ['number'],
},
},
fn: (context, args) => {
if (!args.format) {
return String(context);
}
return numeral(context).format(args.format);
},
});
args: {
format: {
aliases: ['_'],
types: ['string'],
help: 'NumeralJS format string http://numeraljs.com/#format',
},
},
fn: (context, args) => {
if (!args.format) {
return String(context);
}
return numeral(context).format(args.format);
},
};
}

View file

@ -4,38 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const getCell = () => ({
name: 'getCell',
help: 'Fetch a single cell in a table',
context: {
types: ['datatable'],
},
args: {
column: {
types: ['string'],
aliases: ['_', 'c'],
help: 'The name of the column value to fetch',
export function getCell() {
return {
name: 'getCell',
help: 'Fetch a single cell in a table',
context: {
types: ['datatable'],
},
row: {
types: ['number'],
aliases: ['r'],
help: 'The row number, starting at 0',
default: 0,
args: {
column: {
types: ['string'],
aliases: ['_', 'c'],
help: 'The name of the column value to fetch',
},
row: {
types: ['number'],
aliases: ['r'],
help: 'The row number, starting at 0',
default: 0,
},
},
},
fn: (context, args) => {
const row = context.rows[args.row];
if (!row) {
throw new Error(`Row not found: '${args.row}'`);
}
fn: (context, args) => {
const row = context.rows[args.row];
if (!row) {
throw new Error(`Row not found: '${args.row}'`);
}
const { column = context.columns[0].name } = args;
const value = row[column];
const { column = context.columns[0].name } = args;
const value = row[column];
if (typeof value === 'undefined') {
throw new Error(`Column not found: '${column}'`);
}
if (typeof value === 'undefined') {
throw new Error(`Column not found: '${column}'`);
}
return value;
},
});
return value;
},
};
}

View file

@ -4,22 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const gt = () => ({
name: 'gt',
type: 'boolean',
help: 'Return if the context is greater than the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
export function gt() {
return {
name: 'gt',
type: 'boolean',
help: 'Return if the context is greater than the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
},
},
},
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context > args.value;
},
});
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context > args.value;
},
};
}

View file

@ -4,22 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const gte = () => ({
name: 'gte',
type: 'boolean',
help: 'Return if the context is greater than or equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
export function gte() {
return {
name: 'gte',
type: 'boolean',
help: 'Return if the context is greater than or equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
},
},
},
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context >= args.value;
},
});
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context >= args.value;
},
};
}

View file

@ -6,24 +6,26 @@
import { take } from 'lodash';
export const head = () => ({
name: 'head',
aliases: [],
type: 'datatable',
help: 'Get the first N rows from the datatable. Also see `tail`',
context: {
types: ['datatable'],
},
args: {
count: {
aliases: ['_'],
types: ['number'],
help: 'Return this many rows from the beginning of the datatable',
default: 1,
export function head() {
return {
name: 'head',
aliases: [],
type: 'datatable',
help: 'Get the first N rows from the datatable. Also see `tail`',
context: {
types: ['datatable'],
},
},
fn: (context, args) => ({
...context,
rows: take(context.rows, args.count),
}),
});
args: {
count: {
aliases: ['_'],
types: ['number'],
help: 'Return this many rows from the beginning of the datatable',
default: 1,
},
},
fn: (context, args) => ({
...context,
rows: take(context.rows, args.count),
}),
};
}

View file

@ -4,38 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const ifFn = () => ({
name: 'if',
help: 'Perform conditional logic',
args: {
condition: {
types: ['boolean', 'null'],
aliases: ['_'],
help:
'A boolean true or false, usually returned by a subexpression. If this is not supplied then the input context will be used',
export function ifFn() {
return {
name: 'if',
help: 'Perform conditional logic',
args: {
condition: {
types: ['boolean', 'null'],
aliases: ['_'],
help:
'A boolean true or false, usually returned by a subexpression. If this is not supplied then the input context will be used',
},
then: {
resolve: false,
help: 'The return value if true',
},
else: {
resolve: false,
help:
'The return value if false. If else is not specified, and the condition is false' +
'then the input context to the function will be returned',
},
},
then: {
resolve: false,
help: 'The return value if true',
},
else: {
resolve: false,
help:
'The return value if false. If else is not specified, and the condition is false' +
'then the input context to the function will be returned',
},
},
fn: async (context, args) => {
if (args.condition) {
if (typeof args.then === 'undefined') {
return context;
fn: async (context, args) => {
if (args.condition) {
if (typeof args.then === 'undefined') {
return context;
}
return await args.then();
} else {
if (typeof args.else === 'undefined') {
return context;
}
return await args.else();
}
return await args.then();
} else {
if (typeof args.else === 'undefined') {
return context;
}
return await args.else();
}
},
});
},
};
}

View file

@ -9,43 +9,45 @@ import { elasticLogo } from '../../lib/elastic_logo';
const modes = ['contain', 'cover', 'stretch'];
export const image = () => ({
name: 'image',
aliases: [],
type: 'image',
help: 'Display an image',
context: {
types: ['null'],
},
args: {
dataurl: {
// This was accepting dataurl, but there was no facility in fn for checking type and handling a dataurl type.
types: ['string', 'null'],
help: 'The HTTP(S) URL or base64 data of an image.',
aliases: ['_', 'url'],
default: elasticLogo,
export function image() {
return {
name: 'image',
aliases: [],
type: 'image',
help: 'Display an image',
context: {
types: ['null'],
},
mode: {
types: ['string', 'null'],
help:
'"contain" will show the entire image, scaled to fit.' +
'"cover" will fill the container with the image, cropping from the sides or bottom as needed.' +
'"stretch" will resize the height and width of the image to 100% of the container',
default: 'contain',
options: modes,
args: {
dataurl: {
// This was accepting dataurl, but there was no facility in fn for checking type and handling a dataurl type.
types: ['string', 'null'],
help: 'The HTTP(S) URL or base64 data of an image.',
aliases: ['_', 'url'],
default: elasticLogo,
},
mode: {
types: ['string', 'null'],
help:
'"contain" will show the entire image, scaled to fit.' +
'"cover" will fill the container with the image, cropping from the sides or bottom as needed.' +
'"stretch" will resize the height and width of the image to 100% of the container',
default: 'contain',
options: modes,
},
},
},
fn: (context, { dataurl, mode }) => {
if (!modes.includes(mode)) {
throw '"mode" must be "contain", "cover", or "stretch"';
}
fn: (context, { dataurl, mode }) => {
if (!modes.includes(mode)) {
throw '"mode" must be "contain", "cover", or "stretch"';
}
const modeStyle = mode === 'stretch' ? '100% 100%' : mode;
const modeStyle = mode === 'stretch' ? '100% 100%' : mode;
return {
type: 'image',
mode: modeStyle,
dataurl: resolveWithMissingImage(dataurl, elasticLogo),
};
},
});
return {
type: 'image',
mode: modeStyle,
dataurl: resolveWithMissingImage(dataurl, elasticLogo),
};
},
};
}

View file

@ -4,22 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const lt = () => ({
name: 'lt',
type: 'boolean',
help: 'Return if the context is less than the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
export function lt() {
return {
name: 'lt',
type: 'boolean',
help: 'Return if the context is less than the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
},
},
},
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context < args.value;
},
});
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context < args.value;
},
};
}

View file

@ -4,22 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const lte = () => ({
name: 'lte',
type: 'boolean',
help: 'Return if the context is less than or equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
export function lte() {
return {
name: 'lte',
type: 'boolean',
help: 'Return if the context is less than or equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
},
},
},
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context <= args.value;
},
});
fn: (context, args) => {
if (typeof context !== typeof args.value) {
return false;
}
return context <= args.value;
},
};
}

View file

@ -6,60 +6,62 @@
import { getType } from '@kbn/interpreter/common';
export const mapColumn = () => ({
name: 'mapColumn',
aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file.
type: 'datatable',
help: 'Add a column calculated as the result of other columns, or not',
context: {
types: ['datatable'],
},
args: {
name: {
types: ['string'],
aliases: ['_', 'column'],
help: 'The name of the resulting column',
required: true,
export function mapColumn() {
return {
name: 'mapColumn',
aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file.
type: 'datatable',
help: 'Add a column calculated as the result of other columns, or not',
context: {
types: ['datatable'],
},
expression: {
types: ['boolean', 'number', 'string', 'null'],
resolve: false,
aliases: ['exp', 'fn'],
help: 'A canvas expression which will be passed each row as a single row datatable',
args: {
name: {
types: ['string'],
aliases: ['_', 'column'],
help: 'The name of the resulting column',
required: true,
},
expression: {
types: ['boolean', 'number', 'string', 'null'],
resolve: false,
aliases: ['exp', 'fn'],
help: 'A canvas expression which will be passed each row as a single row datatable',
},
},
},
fn: (context, args) => {
args.expression = args.expression || (() => Promise.resolve(null));
fn: (context, args) => {
args.expression = args.expression || (() => Promise.resolve(null));
const columns = [...context.columns];
const rowPromises = context.rows.map(row => {
return args
.expression({
const columns = [...context.columns];
const rowPromises = context.rows.map(row => {
return args
.expression({
type: 'datatable',
columns,
rows: [row],
})
.then(val => ({
...row,
[args.name]: val,
}));
});
return Promise.all(rowPromises).then(rows => {
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
const type = rows.length ? getType(rows[0][args.name]) : 'null';
const newColumn = { name: args.name, type };
if (existingColumnIndex === -1) {
columns.push(newColumn);
} else {
columns[existingColumnIndex] = newColumn;
}
return {
type: 'datatable',
columns,
rows: [row],
})
.then(val => ({
...row,
[args.name]: val,
}));
});
return Promise.all(rowPromises).then(rows => {
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
const type = rows.length ? getType(rows[0][args.name]) : 'null';
const newColumn = { name: args.name, type };
if (existingColumnIndex === -1) {
columns.push(newColumn);
} else {
columns[existingColumnIndex] = newColumn;
}
return {
type: 'datatable',
columns,
rows,
};
});
},
});
rows,
};
});
},
};
}

View file

@ -7,52 +7,54 @@
import { evaluate } from 'tinymath';
import { pivotObjectArray } from '../../../common/lib/pivot_object_array';
export const math = () => ({
name: 'math',
type: 'number',
help:
'Interpret a math expression, with a number or datatable as context. Datatable columns are available by their column name. ' +
'If you pass in a number it is available as "value" (without the quotes)',
context: {
types: ['number', 'datatable'],
},
args: {
expression: {
aliases: ['_'],
types: ['string'],
help:
'An evaluated TinyMath expression. (See [TinyMath Functions](https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html))',
export function math() {
return {
name: 'math',
type: 'number',
help:
'Interpret a math expression, with a number or datatable as context. Datatable columns are available by their column name. ' +
'If you pass in a number it is available as "value" (without the quotes)',
context: {
types: ['number', 'datatable'],
},
},
fn: (context, args) => {
if (!args.expression || args.expression.trim() === '') {
throw new Error('Empty expression');
}
args: {
expression: {
aliases: ['_'],
types: ['string'],
help:
'An evaluated TinyMath expression. (See [TinyMath Functions](https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html))',
},
},
fn: (context, args) => {
if (!args.expression || args.expression.trim() === '') {
throw new Error('Empty expression');
}
const isDatatable = context && context.type === 'datatable';
const mathContext = isDatatable
? pivotObjectArray(context.rows, context.columns.map(col => col.name))
: { value: context };
try {
const result = evaluate(args.expression, mathContext);
if (Array.isArray(result)) {
if (result.length === 1) {
return result[0];
const isDatatable = context && context.type === 'datatable';
const mathContext = isDatatable
? pivotObjectArray(context.rows, context.columns.map(col => col.name))
: { value: context };
try {
const result = evaluate(args.expression, mathContext);
if (Array.isArray(result)) {
if (result.length === 1) {
return result[0];
}
throw new Error(
'Expressions must return a single number. Try wrapping your expression in mean() or sum()'
);
}
if (isNaN(result)) {
throw new Error('Failed to execute math expression. Check your column names');
}
return result;
} catch (e) {
if (context.rows.length === 0) {
throw new Error('Empty datatable');
} else {
throw e;
}
throw new Error(
'Expressions must return a single number. Try wrapping your expression in mean() or sum()'
);
}
if (isNaN(result)) {
throw new Error('Failed to execute math expression. Check your column names');
}
return result;
} catch (e) {
if (context.rows.length === 0) {
throw new Error('Empty datatable');
} else {
throw e;
}
}
},
});
},
};
}

View file

@ -5,42 +5,46 @@
*/
import { openSans } from '../../../common/lib/fonts';
export const metric = () => ({
name: 'metric',
aliases: [],
type: 'render',
help: 'A number with a label',
context: {
types: ['string', 'null'],
},
args: {
label: {
types: ['string'],
aliases: ['_', 'text', 'description'],
help: 'Text describing the metric',
default: '""',
export function metric() {
return {
name: 'metric',
aliases: [],
type: 'render',
help: 'A number with a label',
context: {
types: ['string', 'null'],
},
metricFont: {
types: ['style'],
help: 'Font settings for the metric. Technically you can stick other styles in here too!',
default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`,
},
labelFont: {
types: ['style'],
help: 'Font settings for the label. Technically you can stick other styles in here too!',
default: `{font size=14 family="${openSans.value}" color="#000000" align=center}`,
},
},
fn: (context, { label, metricFont, labelFont }) => {
return {
type: 'render',
as: 'metric',
value: {
metric: context === null ? '?' : context,
label,
metricFont,
labelFont,
args: {
label: {
types: ['string'],
aliases: ['_', 'text', 'description'],
help: 'Text describing the metric',
default: '""',
},
};
},
});
metricFont: {
types: ['style'],
help: 'Font settings for the metric. Technically you can stick other styles in here too!',
default: `{font size=48 family="${
openSans.value
}" color="#000000" align=center lHeight=48}`,
},
labelFont: {
types: ['style'],
help: 'Font settings for the label. Technically you can stick other styles in here too!',
default: `{font size=14 family="${openSans.value}" color="#000000" align=center}`,
},
},
fn: (context, { label, metricFont, labelFont }) => {
return {
type: 'render',
as: 'metric',
value: {
metric: context === null ? '?' : context,
label,
metricFont,
labelFont,
},
};
},
};
}

View file

@ -4,19 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const neq = () => ({
name: 'neq',
type: 'boolean',
help: 'Return if the context is not equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
export function neq() {
return {
name: 'neq',
type: 'boolean',
help: 'Return if the context is not equal to the argument',
args: {
value: {
aliases: ['_'],
types: ['boolean', 'number', 'string', 'null'],
required: true,
help: 'The value to compare the context to',
},
},
},
fn: (context, args) => {
return context !== args.value;
},
});
fn: (context, args) => {
return context !== args.value;
},
};
}

View file

@ -5,40 +5,42 @@
*/
import { palettes } from '../../../common/lib/palettes';
export const palette = () => ({
name: 'palette',
aliases: [],
type: 'palette',
help: 'Create a color palette',
context: {
types: ['null'],
},
args: {
color: {
aliases: ['_'],
multi: true,
types: ['string'],
help: 'Palette colors, rgba, hex, or HTML color string. Pass this multiple times.',
export function palette() {
return {
name: 'palette',
aliases: [],
type: 'palette',
help: 'Create a color palette',
context: {
types: ['null'],
},
gradient: {
types: ['boolean'],
default: false,
help: 'Prefer to make a gradient where supported and useful?',
options: [true, false],
args: {
color: {
aliases: ['_'],
multi: true,
types: ['string'],
help: 'Palette colors, rgba, hex, or HTML color string. Pass this multiple times.',
},
gradient: {
types: ['boolean'],
default: false,
help: 'Prefer to make a gradient where supported and useful?',
options: [true, false],
},
reverse: {
type: ['boolean'],
default: false,
help: 'Reverse the palette',
options: [true, false],
},
},
reverse: {
type: ['boolean'],
default: false,
help: 'Reverse the palette',
options: [true, false],
fn: (context, args) => {
const colors = [].concat(args.color || palettes.paul_tor_14.colors);
return {
type: 'palette',
colors: args.reverse ? colors.reverse() : colors,
gradient: args.gradient,
};
},
},
fn: (context, args) => {
const colors = [].concat(args.color || palettes.paul_tor_14.colors);
return {
type: 'palette',
colors: args.reverse ? colors.reverse() : colors,
gradient: args.gradient,
};
},
});
};
}

View file

@ -9,116 +9,118 @@ import { get, map, groupBy } from 'lodash';
import { getColorsFromPalette } from '../../../common/lib/get_colors_from_palette';
import { getLegendConfig } from '../../../common/lib/get_legend_config';
export const pie = () => ({
name: 'pie',
aliases: [],
type: 'render',
help: 'Configure a pie chart element',
context: {
types: ['pointseries'],
},
args: {
palette: {
types: ['palette', 'null'],
help: 'A palette object for describing the colors to use on this pie',
default: '{palette}',
export function pie() {
return {
name: 'pie',
aliases: [],
type: 'render',
help: 'Configure a pie chart element',
context: {
types: ['pointseries'],
},
seriesStyle: {
multi: true,
types: ['seriesStyle', 'null'],
help: 'A style of a specific series',
args: {
palette: {
types: ['palette', 'null'],
help: 'A palette object for describing the colors to use on this pie',
default: '{palette}',
},
seriesStyle: {
multi: true,
types: ['seriesStyle', 'null'],
help: 'A style of a specific series',
},
radius: {
type: ['string', 'number'],
help: `Radius of the pie as a percentage (between 0 and 1) of the available space. Set to 'auto' to automatically set radius`,
default: 'auto',
},
hole: {
types: ['number'],
default: 0,
help: 'Draw a hole in the pie, 0-100, as a percentage of the pie radius',
},
labels: {
types: ['boolean'],
default: true,
help: 'Show pie labels',
options: [true, false],
},
labelRadius: {
types: ['number'],
default: 100,
help: 'Percentage of area of container to use as radius for the label circle',
},
font: {
types: ['style'],
help: 'Label font',
default: '{font}',
},
legend: {
types: ['string', 'boolean'],
help: 'Legend position, nw, sw, ne, se or false',
default: false,
options: ['nw', 'sw', 'ne', 'se', false],
},
tilt: {
types: ['number'],
default: 1,
help: 'Percentage of tilt where 1 is fully vertical and 0 is completely flat',
},
},
radius: {
type: ['string', 'number'],
help: `Radius of the pie as a percentage (between 0 and 1) of the available space. Set to 'auto' to automatically set radius`,
default: 'auto',
},
hole: {
types: ['number'],
default: 0,
help: 'Draw a hole in the pie, 0-100, as a percentage of the pie radius',
},
labels: {
types: ['boolean'],
default: true,
help: 'Show pie labels',
options: [true, false],
},
labelRadius: {
types: ['number'],
default: 100,
help: 'Percentage of area of container to use as radius for the label circle',
},
font: {
types: ['style'],
help: 'Label font',
default: '{font}',
},
legend: {
types: ['string', 'boolean'],
help: 'Legend position, nw, sw, ne, se or false',
default: false,
options: ['nw', 'sw', 'ne', 'se', false],
},
tilt: {
types: ['number'],
default: 1,
help: 'Percentage of tilt where 1 is fully vertical and 0 is completely flat',
},
},
fn: (context, args) => {
const seriesStyles = keyBy(args.seriesStyle || [], 'label') || {};
fn: (context, args) => {
const seriesStyles = keyBy(args.seriesStyle || [], 'label') || {};
const data = map(groupBy(context.rows, 'color'), (series, label) => {
const item = {
label: label,
data: series.map(point => point.size || 1),
};
const data = map(groupBy(context.rows, 'color'), (series, label) => {
const item = {
label: label,
data: series.map(point => point.size || 1),
};
const seriesStyle = seriesStyles[label];
const seriesStyle = seriesStyles[label];
// append series style, if there is a match
if (seriesStyle) {
item.color = get(seriesStyle, 'color');
}
// append series style, if there is a match
if (seriesStyle) {
item.color = get(seriesStyle, 'color');
}
return item;
});
return item;
});
return {
type: 'render',
as: 'pie',
value: {
font: args.font,
data,
options: {
canvas: false,
colors: getColorsFromPalette(args.palette, data.length),
legend: getLegendConfig(args.legend, data.length),
grid: {
show: false,
},
series: {
pie: {
show: true,
innerRadius: Math.max(args.hole, 0) / 100,
stroke: {
width: 0,
},
label: {
show: args.labels,
radius: (args.labelRadius >= 0 ? args.labelRadius : 100) / 100,
},
tilt: args.tilt,
radius: args.radius,
},
bubbles: {
return {
type: 'render',
as: 'pie',
value: {
font: args.font,
data,
options: {
canvas: false,
colors: getColorsFromPalette(args.palette, data.length),
legend: getLegendConfig(args.legend, data.length),
grid: {
show: false,
},
shadowSize: 0,
series: {
pie: {
show: true,
innerRadius: Math.max(args.hole, 0) / 100,
stroke: {
width: 0,
},
label: {
show: args.labels,
radius: (args.labelRadius >= 0 ? args.labelRadius : 100) / 100,
},
tilt: args.tilt,
radius: args.radius,
},
bubbles: {
show: false,
},
shadowSize: 0,
},
},
},
},
};
},
});
};
},
};
}

View file

@ -13,136 +13,138 @@ import { getFontSpec } from './get_font_spec';
import { seriesStyleToFlot } from './series_style_to_flot';
import { getTickHash } from './get_tick_hash';
export const plot = () => ({
name: 'plot',
aliases: [],
type: 'render',
help: 'Configure a plot element',
context: {
types: ['pointseries'],
},
args: {
seriesStyle: {
multi: true,
types: ['seriesStyle', 'null'],
help: 'A style of a specific series',
export function plot() {
return {
name: 'plot',
aliases: [],
type: 'render',
help: 'Configure a plot element',
context: {
types: ['pointseries'],
},
defaultStyle: {
multi: false,
types: ['seriesStyle'],
help: 'The default style to use for every series',
default: '{seriesStyle points=5}',
},
palette: {
types: ['palette'],
help: 'A palette object for describing the colors to use on this plot',
default: '{palette}',
},
font: {
types: ['style'],
help: 'Legend and tick mark fonts',
default: '{font}',
},
legend: {
types: ['string', 'boolean'],
help: 'Legend position, nw, sw, ne, se or false',
default: 'ne',
options: ['nw', 'sw', 'ne', 'se', false],
},
yaxis: {
types: ['boolean', 'axisConfig'],
help: 'Axis configuration, or false to disable',
default: true,
},
xaxis: {
types: ['boolean', 'axisConfig'],
help: 'Axis configuration, or false to disable',
default: true,
},
},
fn: (context, args) => {
const seriesStyles = keyBy(args.seriesStyle || [], 'label') || {};
const sortedRows = sortBy(context.rows, ['x', 'y', 'color', 'size', 'text']);
const ticks = getTickHash(context.columns, sortedRows);
const font = args.font ? getFontSpec(args.font) : {};
const data = map(groupBy(sortedRows, 'color'), (series, label) => {
const seriesStyle = {
...args.defaultStyle,
...seriesStyles[label],
};
const flotStyle = seriesStyle ? seriesStyleToFlot(seriesStyle) : {};
return {
...flotStyle,
label: label,
data: series.map(point => {
const attrs = {};
const x = get(context.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x;
const y = get(context.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y;
if (point.size != null) {
attrs.size = point.size;
} else if (get(seriesStyle, 'points')) {
attrs.size = seriesStyle.points;
set(flotStyle, 'bubbles.size.min', seriesStyle.points);
}
if (point.text != null) {
attrs.text = point.text;
}
return [x, y, attrs];
}),
};
});
const gridConfig = {
borderWidth: 0,
borderColor: null,
color: 'rgba(0,0,0,0)',
labelMargin: 30,
margin: {
right: 30,
top: 20,
bottom: 0,
left: 0,
args: {
seriesStyle: {
multi: true,
types: ['seriesStyle', 'null'],
help: 'A style of a specific series',
},
};
defaultStyle: {
multi: false,
types: ['seriesStyle'],
help: 'The default style to use for every series',
default: '{seriesStyle points=5}',
},
palette: {
types: ['palette'],
help: 'A palette object for describing the colors to use on this plot',
default: '{palette}',
},
font: {
types: ['style'],
help: 'Legend and tick mark fonts',
default: '{font}',
},
legend: {
types: ['string', 'boolean'],
help: 'Legend position, nw, sw, ne, se or false',
default: 'ne',
options: ['nw', 'sw', 'ne', 'se', false],
},
yaxis: {
types: ['boolean', 'axisConfig'],
help: 'Axis configuration, or false to disable',
default: true,
},
xaxis: {
types: ['boolean', 'axisConfig'],
help: 'Axis configuration, or false to disable',
default: true,
},
},
fn: (context, args) => {
const seriesStyles = keyBy(args.seriesStyle || [], 'label') || {};
const sortedRows = sortBy(context.rows, ['x', 'y', 'color', 'size', 'text']);
const ticks = getTickHash(context.columns, sortedRows);
const font = args.font ? getFontSpec(args.font) : {};
const result = {
type: 'render',
as: 'plot',
value: {
font: args.font,
data: sortBy(data, 'label'),
options: {
canvas: false,
colors: getColorsFromPalette(args.palette, data.length),
legend: getLegendConfig(args.legend, data.length),
grid: gridConfig,
xaxis: getFlotAxisConfig('x', args.xaxis, {
columns: context.columns,
ticks,
font,
const data = map(groupBy(sortedRows, 'color'), (series, label) => {
const seriesStyle = {
...args.defaultStyle,
...seriesStyles[label],
};
const flotStyle = seriesStyle ? seriesStyleToFlot(seriesStyle) : {};
return {
...flotStyle,
label: label,
data: series.map(point => {
const attrs = {};
const x = get(context.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x;
const y = get(context.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y;
if (point.size != null) {
attrs.size = point.size;
} else if (get(seriesStyle, 'points')) {
attrs.size = seriesStyle.points;
set(flotStyle, 'bubbles.size.min', seriesStyle.points);
}
if (point.text != null) {
attrs.text = point.text;
}
return [x, y, attrs];
}),
yaxis: getFlotAxisConfig('y', args.yaxis, {
columns: context.columns,
ticks,
font,
}),
series: {
shadowSize: 0,
...seriesStyleToFlot(args.defaultStyle),
};
});
const gridConfig = {
borderWidth: 0,
borderColor: null,
color: 'rgba(0,0,0,0)',
labelMargin: 30,
margin: {
right: 30,
top: 20,
bottom: 0,
left: 0,
},
};
const result = {
type: 'render',
as: 'plot',
value: {
font: args.font,
data: sortBy(data, 'label'),
options: {
canvas: false,
colors: getColorsFromPalette(args.palette, data.length),
legend: getLegendConfig(args.legend, data.length),
grid: gridConfig,
xaxis: getFlotAxisConfig('x', args.xaxis, {
columns: context.columns,
ticks,
font,
}),
yaxis: getFlotAxisConfig('y', args.yaxis, {
columns: context.columns,
ticks,
font,
}),
series: {
shadowSize: 0,
...seriesStyleToFlot(args.defaultStyle),
},
},
},
},
};
};
// fix the issue of plot sometimes re-rendering with an empty chart
// TODO: holy hell, why does this work?! the working theory is that some values become undefined
// and serializing the result here causes them to be dropped off, and this makes flot react differently.
// It's also possible that something else ends up mutating this object, but that seems less likely.
return JSON.parse(JSON.stringify(result));
},
});
// fix the issue of plot sometimes re-rendering with an empty chart
// TODO: holy hell, why does this work?! the working theory is that some values become undefined
// and serializing the result here causes them to be dropped off, and this makes flot react differently.
// It's also possible that something else ends up mutating this object, but that seems less likely.
return JSON.parse(JSON.stringify(result));
},
};
}

View file

@ -55,89 +55,93 @@ function combineAcross(datatableArray) {
};
}
export const ply = () => ({
name: 'ply',
type: 'datatable',
help:
'Subdivide a datatable and pass the resulting tables into an expression, then merge the output',
context: {
types: ['datatable'],
},
args: {
by: {
types: ['string'],
help: 'The column to subdivide on',
multi: true,
},
expression: {
export function ply() {
return {
name: 'ply',
type: 'datatable',
help:
'Subdivide a datatable and pass the resulting tables into an expression, then merge the output',
context: {
types: ['datatable'],
resolve: false,
multi: true,
aliases: ['fn', 'function'],
help:
'An expression to pass each resulting data table into. Tips: \n' +
' Expressions must return a datatable. Use `as` to turn literals into datatables.\n' +
' Multiple expressions must return the same number of rows.' +
' If you need to return a differing row count, pipe into another instance of ply.\n' +
' If multiple expressions return the same columns, the last one wins.',
},
// In the future it may make sense to add things like shape, or tooltip values, but I think what we have is good for now
// The way the function below is written you can add as many arbitrary named args as you want.
},
fn: (context, args) => {
if (!args) {
return context;
}
let byColumns;
let originalDatatables;
args: {
by: {
types: ['string'],
help: 'The column to subdivide on',
multi: true,
},
expression: {
types: ['datatable'],
resolve: false,
multi: true,
aliases: ['fn', 'function'],
help:
'An expression to pass each resulting data table into. Tips: \n' +
' Expressions must return a datatable. Use `as` to turn literals into datatables.\n' +
' Multiple expressions must return the same number of rows.' +
' If you need to return a differing row count, pipe into another instance of ply.\n' +
' If multiple expressions return the same columns, the last one wins.',
},
// In the future it may make sense to add things like shape, or tooltip values, but I think what we have is good for now
// The way the function below is written you can add as many arbitrary named args as you want.
},
fn: (context, args) => {
if (!args) {
return context;
}
let byColumns;
let originalDatatables;
if (args.by) {
byColumns = args.by.map(by => {
const column = context.columns.find(column => column.name === by);
if (!column) {
throw new Error(`Column not found: '${by}'`);
}
return column;
});
const keyedDatatables = groupBy(context.rows, row => JSON.stringify(pick(row, args.by)));
originalDatatables = Object.values(keyedDatatables).map(rows => ({
...context,
rows,
}));
} else {
originalDatatables = [context];
}
const datatablePromises = originalDatatables.map(originalDatatable => {
let expressionResultPromises = [];
if (args.expression) {
expressionResultPromises = args.expression.map(expression => expression(originalDatatable));
if (args.by) {
byColumns = args.by.map(by => {
const column = context.columns.find(column => column.name === by);
if (!column) {
throw new Error(`Column not found: '${by}'`);
}
return column;
});
const keyedDatatables = groupBy(context.rows, row => JSON.stringify(pick(row, args.by)));
originalDatatables = Object.values(keyedDatatables).map(rows => ({
...context,
rows,
}));
} else {
expressionResultPromises.push(Promise.resolve(originalDatatable));
originalDatatables = [context];
}
return Promise.all(expressionResultPromises).then(combineAcross);
});
const datatablePromises = originalDatatables.map(originalDatatable => {
let expressionResultPromises = [];
return Promise.all(datatablePromises).then(newDatatables => {
// Here we're just merging each for the by splits, so it doesn't actually matter if the rows are the same length
const columns = combineColumns([byColumns].concat(map(newDatatables, 'columns')));
const rows = flatten(
newDatatables.map((dt, i) => {
const byColumnValues = pick(originalDatatables[i].rows[0], args.by);
return dt.rows.map(row => ({
...byColumnValues,
...row,
}));
})
);
if (args.expression) {
expressionResultPromises = args.expression.map(expression =>
expression(originalDatatable)
);
} else {
expressionResultPromises.push(Promise.resolve(originalDatatable));
}
return {
type: 'datatable',
rows,
columns,
};
});
},
});
return Promise.all(expressionResultPromises).then(combineAcross);
});
return Promise.all(datatablePromises).then(newDatatables => {
// Here we're just merging each for the by splits, so it doesn't actually matter if the rows are the same length
const columns = combineColumns([byColumns].concat(map(newDatatables, 'columns')));
const rows = flatten(
newDatatables.map((dt, i) => {
const byColumnValues = pick(originalDatatables[i].rows[0], args.by);
return dt.rows.map(row => ({
...byColumnValues,
...row,
}));
})
);
return {
type: 'datatable',
rows,
columns,
};
});
},
};
}

View file

@ -18,87 +18,89 @@ const shapes = [
'wheel',
];
export const progress = () => ({
name: 'progress',
aliases: [],
type: 'render',
help: 'Configure a progress element',
context: {
types: ['number'],
},
args: {
shape: {
type: ['string'],
alias: ['_'],
help: `Select ${shapes.slice(0, -1).join(', ')}, or ${shapes.slice(-1)[0]}`,
options: shapes,
default: 'gauge',
export function progress() {
return {
name: 'progress',
aliases: [],
type: 'render',
help: 'Configure a progress element',
context: {
types: ['number'],
},
max: {
type: ['number'],
help: 'Maximum value of the progress element',
default: 1,
},
valueColor: {
type: ['string'],
help: 'Color of the progress bar',
default: `#1785b0`,
},
barColor: {
type: ['string'],
help: 'Color of the background bar',
default: `#f0f0f0`,
},
valueWeight: {
type: ['number'],
help: 'Thickness of the progress bar',
default: 20,
},
barWeight: {
type: ['number'],
help: 'Thickness of the background bar',
default: 20,
},
label: {
type: ['boolean', 'string'],
help: `Set true/false to show/hide label or provide a string to display as the label`,
default: true,
},
font: {
types: ['style'],
help: 'Font settings for the label. Technically you can stick other styles in here too!',
default: `{font size=24 family="${openSans.value}" color="#000000" align=center}`,
},
},
fn: (value, args) => {
if (args.max <= 0) {
throw new Error(`Invalid max value: '${args.max}'. 'max' must be greater than 0`);
}
if (value > args.max || value < 0) {
throw new Error(`Invalid value: '${value}'. Value must be between 0 and ${args.max}`);
}
let label = '';
if (args.label) {
label = typeof args.label === 'string' ? args.label : `${value}`;
}
let font = {};
if (get(args, 'font.spec')) {
font = { ...args.font };
font.spec.fill = args.font.spec.color; // SVG <text> uses fill for font color
}
return {
type: 'render',
as: 'progress',
value: {
value,
...args,
label,
font,
args: {
shape: {
type: ['string'],
alias: ['_'],
help: `Select ${shapes.slice(0, -1).join(', ')}, or ${shapes.slice(-1)[0]}`,
options: shapes,
default: 'gauge',
},
};
},
});
max: {
type: ['number'],
help: 'Maximum value of the progress element',
default: 1,
},
valueColor: {
type: ['string'],
help: 'Color of the progress bar',
default: `#1785b0`,
},
barColor: {
type: ['string'],
help: 'Color of the background bar',
default: `#f0f0f0`,
},
valueWeight: {
type: ['number'],
help: 'Thickness of the progress bar',
default: 20,
},
barWeight: {
type: ['number'],
help: 'Thickness of the background bar',
default: 20,
},
label: {
type: ['boolean', 'string'],
help: `Set true/false to show/hide label or provide a string to display as the label`,
default: true,
},
font: {
types: ['style'],
help: 'Font settings for the label. Technically you can stick other styles in here too!',
default: `{font size=24 family="${openSans.value}" color="#000000" align=center}`,
},
},
fn: (value, args) => {
if (args.max <= 0) {
throw new Error(`Invalid max value: '${args.max}'. 'max' must be greater than 0`);
}
if (value > args.max || value < 0) {
throw new Error(`Invalid value: '${value}'. Value must be between 0 and ${args.max}`);
}
let label = '';
if (args.label) {
label = typeof args.label === 'string' ? args.label : `${value}`;
}
let font = {};
if (get(args, 'font.spec')) {
font = { ...args.font };
font.spec.fill = args.font.spec.color; // SVG <text> uses fill for font color
}
return {
type: 'render',
as: 'progress',
value: {
value,
...args,
label,
font,
},
};
},
};
}

View file

@ -4,37 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const render = () => ({
name: 'render',
aliases: [],
type: 'render',
help: 'Render an input as a specific element and set element level options such as styling',
context: {
types: ['render'],
},
args: {
as: {
types: ['string', 'null'],
help:
'The element type to use in rendering. You probably want a specialized function instead, such as plot or grid',
options: ['debug', 'error', 'image', 'pie', 'plot', 'shape', 'table', 'text'],
export function render() {
return {
name: 'render',
aliases: [],
type: 'render',
help: 'Render an input as a specific element and set element level options such as styling',
context: {
types: ['render'],
},
css: {
types: ['string', 'null'],
default: '"* > * {}"',
help: 'Any block of custom CSS to be scoped to this element.',
args: {
as: {
types: ['string', 'null'],
help:
'The element type to use in rendering. You probably want a specialized function instead, such as plot or grid',
options: ['debug', 'error', 'image', 'pie', 'plot', 'shape', 'table', 'text'],
},
css: {
types: ['string', 'null'],
default: '"* > * {}"',
help: 'Any block of custom CSS to be scoped to this element.',
},
containerStyle: {
types: ['containerStyle', 'null'],
help: 'Style for the container, including background, border, and opacity',
},
},
containerStyle: {
types: ['containerStyle', 'null'],
help: 'Style for the container, including background, border, and opacity',
fn: (context, args) => {
return {
...context,
as: args.as || context.as,
css: args.css,
containerStyle: args.containerStyle,
};
},
},
fn: (context, args) => {
return {
...context,
as: args.as || context.as,
css: args.css,
containerStyle: args.containerStyle,
};
},
});
};
}

View file

@ -7,47 +7,49 @@
import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl';
import { elasticOutline } from '../../lib/elastic_outline';
export const repeatImage = () => ({
name: 'repeatImage',
aliases: [],
type: 'render',
help: 'Configure a repeating image element',
context: {
types: ['number'],
},
args: {
image: {
types: ['string', 'null'],
help: 'The image to repeat. Usually a dataURL or an asset',
default: elasticOutline,
},
size: {
export function repeatImage() {
return {
name: 'repeatImage',
aliases: [],
type: 'render',
help: 'Configure a repeating image element',
context: {
types: ['number'],
default: 100,
help:
'The maximum height or width of the image, in pixels. Eg, if you images is taller than it is wide, this will limit its height',
},
max: {
types: ['number', 'null'],
help: 'Maximum number of times the image may repeat',
default: 1000,
},
emptyImage: {
types: ['string', 'null'],
help: 'Fill the difference between the input and the `max=` parameter with this image',
default: null,
},
},
fn: (count, args) => {
return {
type: 'render',
as: 'repeatImage',
value: {
count: Math.floor(count),
...args,
image: resolveWithMissingImage(args.image, elasticOutline),
emptyImage: resolveWithMissingImage(args.emptyImage),
args: {
image: {
types: ['string', 'null'],
help: 'The image to repeat. Usually a dataURL or an asset',
default: elasticOutline,
},
};
},
});
size: {
types: ['number'],
default: 100,
help:
'The maximum height or width of the image, in pixels. Eg, if you images is taller than it is wide, this will limit its height',
},
max: {
types: ['number', 'null'],
help: 'Maximum number of times the image may repeat',
default: 1000,
},
emptyImage: {
types: ['string', 'null'],
help: 'Fill the difference between the input and the `max=` parameter with this image',
default: null,
},
},
fn: (count, args) => {
return {
type: 'render',
as: 'repeatImage',
value: {
count: Math.floor(count),
...args,
image: resolveWithMissingImage(args.image, elasticOutline),
emptyImage: resolveWithMissingImage(args.emptyImage),
},
};
},
};
}

View file

@ -4,33 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const replace = () => ({
name: 'replace',
type: 'string',
help: 'Use a regular expression to replace parts of a string',
context: {
types: ['string'],
},
args: {
pattern: {
aliases: ['_', 'regex'],
export function replace() {
return {
name: 'replace',
type: 'string',
help: 'Use a regular expression to replace parts of a string',
context: {
types: ['string'],
help:
'The text or pattern of a JavaScript regular expression, eg "[aeiou]". You can use capture groups here.',
},
flags: {
aliases: ['modifiers'],
types: ['string'],
help:
'Specify flags. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp for reference.',
default: 'g',
args: {
pattern: {
aliases: ['_', 'regex'],
types: ['string'],
help:
'The text or pattern of a JavaScript regular expression, eg "[aeiou]". You can use capture groups here.',
},
flags: {
aliases: ['modifiers'],
types: ['string'],
help:
'Specify flags. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp for reference.',
default: 'g',
},
replacement: {
types: ['string'],
help:
'The replacement for the matching parts of string. Capture groups can be accessed by their index, eg $1',
default: '""',
},
},
replacement: {
types: ['string'],
help:
'The replacement for the matching parts of string. Capture groups can be accessed by their index, eg $1',
default: '""',
},
},
fn: (context, args) => context.replace(new RegExp(args.pattern, args.flags), args.replacement),
});
fn: (context, args) => context.replace(new RegExp(args.pattern, args.flags), args.replacement),
};
}

View file

@ -7,46 +7,48 @@
import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl';
import { elasticOutline } from '../../lib/elastic_outline';
export const revealImage = () => ({
name: 'revealImage',
aliases: [],
type: 'render',
help: 'Configure a image reveal element',
context: {
types: ['number'],
},
args: {
image: {
types: ['string', 'null'],
help: 'The image to reveal',
default: elasticOutline,
export function revealImage() {
return {
name: 'revealImage',
aliases: [],
type: 'render',
help: 'Configure a image reveal element',
context: {
types: ['number'],
},
emptyImage: {
types: ['string', 'null'],
help: 'An optional background image to reveal over',
default: null,
},
origin: {
types: ['string'],
help: 'Where to start from. Eg, top, left, bottom or right',
default: 'bottom',
options: ['top', 'left', 'bottom', 'right'],
},
},
fn: (percent, args) => {
if (percent > 1 || percent < 0) {
throw new Error(`Invalid value: '${percent}'. Percentage must be between 0 and 1`);
}
return {
type: 'render',
as: 'revealImage',
value: {
percent,
...args,
image: resolveWithMissingImage(args.image, elasticOutline),
emptyImage: resolveWithMissingImage(args.emptyImage),
args: {
image: {
types: ['string', 'null'],
help: 'The image to reveal',
default: elasticOutline,
},
};
},
});
emptyImage: {
types: ['string', 'null'],
help: 'An optional background image to reveal over',
default: null,
},
origin: {
types: ['string'],
help: 'Where to start from. Eg, top, left, bottom or right',
default: 'bottom',
options: ['top', 'left', 'bottom', 'right'],
},
},
fn: (percent, args) => {
if (percent > 1 || percent < 0) {
throw new Error(`Invalid value: '${percent}'. Percentage must be between 0 and 1`);
}
return {
type: 'render',
as: 'revealImage',
value: {
percent,
...args,
image: resolveWithMissingImage(args.image, elasticOutline),
emptyImage: resolveWithMissingImage(args.emptyImage),
},
};
},
};
}

View file

@ -6,25 +6,27 @@
import moment from 'moment';
export const rounddate = () => ({
name: 'rounddate',
type: 'number',
help: 'Round ms since epoch using a moment formatting string. Returns ms since epoch',
context: {
types: ['number'],
},
args: {
format: {
aliases: ['_'],
types: ['string'],
help:
'MomentJS Format with which to bucket (See https://momentjs.com/docs/#/displaying/). For example "YYYY-MM" would round to the month',
export function rounddate() {
return {
name: 'rounddate',
type: 'number',
help: 'Round ms since epoch using a moment formatting string. Returns ms since epoch',
context: {
types: ['number'],
},
},
fn: (context, args) => {
if (!args.format) {
return context;
}
return moment.utc(moment.utc(context).format(args.format), args.format).valueOf();
},
});
args: {
format: {
aliases: ['_'],
types: ['string'],
help:
'MomentJS Format with which to bucket (See https://momentjs.com/docs/#/displaying/). For example "YYYY-MM" would round to the month',
},
},
fn: (context, args) => {
if (!args.format) {
return context;
}
return moment.utc(moment.utc(context).format(args.format), args.format).valueOf();
},
};
}

View file

@ -4,15 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const rowCount = () => ({
name: 'rowCount',
aliases: [],
type: 'number',
context: {
types: ['datatable'],
},
help:
'Return the number of rows. Pair with ply to get the count of unique column values, or combinations of unique column values.',
args: {},
fn: context => context.rows.length,
});
export function rowCount() {
return {
name: 'rowCount',
aliases: [],
type: 'number',
context: {
types: ['datatable'],
},
help:
'Return the number of rows. Pair with ply to get the count of unique column values, or combinations of unique column values.',
args: {},
fn: context => context.rows.length,
};
}

View file

@ -6,60 +6,62 @@
const name = 'seriesStyle';
export const seriesStyle = () => ({
name,
help:
'Creates an object used for describing the properties of a series on a chart.' +
' You would usually use this inside of a charting function',
context: {
types: ['null'],
},
args: {
label: {
types: ['string'],
displayName: 'Series label',
help:
'The label of the line this style applies to, not the name you would like to give the line',
export function seriesStyle() {
return {
name,
help:
'Creates an object used for describing the properties of a series on a chart.' +
' You would usually use this inside of a charting function',
context: {
types: ['null'],
},
color: {
types: ['string', 'null'],
displayName: 'Color',
help: 'Color to assign the line',
args: {
label: {
types: ['string'],
displayName: 'Series label',
help:
'The label of the line this style applies to, not the name you would like to give the line',
},
color: {
types: ['string', 'null'],
displayName: 'Color',
help: 'Color to assign the line',
},
lines: {
types: ['number'],
displayName: 'Line width',
help: 'Width of the line',
},
bars: {
types: ['number'],
displayName: 'Bar width',
help: 'Width of bars',
},
points: {
types: ['number'],
displayName: 'Show points',
help: 'Size of points on line',
},
fill: {
types: ['number', 'boolean'],
displayName: 'Fill points',
help: 'Should we fill points?',
default: false,
options: [true, false],
},
stack: {
types: ['number', 'null'],
displayName: 'Stack series',
help:
'Should we stack the series? This is the stack "id". Series with the same stack id will be stacked together',
},
horizontalBars: {
types: ['boolean'],
displayName: 'Horizontal bars orientation',
help: 'Sets the orientation of bars in the chart to horizontal',
options: [true, false],
},
},
lines: {
types: ['number'],
displayName: 'Line width',
help: 'Width of the line',
},
bars: {
types: ['number'],
displayName: 'Bar width',
help: 'Width of bars',
},
points: {
types: ['number'],
displayName: 'Show points',
help: 'Size of points on line',
},
fill: {
types: ['number', 'boolean'],
displayName: 'Fill points',
help: 'Should we fill points?',
default: false,
options: [true, false],
},
stack: {
types: ['number', 'null'],
displayName: 'Stack series',
help:
'Should we stack the series? This is the stack "id". Series with the same stack id will be stacked together',
},
horizontalBars: {
types: ['boolean'],
displayName: 'Horizontal bars orientation',
help: 'Sets the orientation of bars in the chart to horizontal',
options: [true, false],
},
},
fn: (context, args) => ({ type: name, ...args }),
});
fn: (context, args) => ({ type: name, ...args }),
};
}

View file

@ -4,68 +4,70 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const shape = () => ({
name: 'shape',
aliases: [],
type: 'shape',
help: 'Create a shape',
context: {
types: ['null'],
},
args: {
shape: {
types: ['string', 'null'],
help: 'Pick a shape',
aliases: ['_'],
default: 'square',
options: [
'arrow',
'arrowMulti',
'bookmark',
'cross',
'circle',
'hexagon',
'kite',
'pentagon',
'rhombus',
'semicircle',
'speechBubble',
'square',
'star',
'tag',
'triangle',
'triangleRight',
],
},
fill: {
types: ['string', 'null'],
help: 'Valid CSS color string',
default: 'black',
},
border: {
types: ['string', 'null'],
aliases: ['stroke'],
help: 'Valid CSS color string',
},
borderWidth: {
types: ['number', 'null'],
aliases: ['strokeWidth'],
help: 'Thickness of the border',
default: '0',
},
maintainAspect: {
types: ['boolean'],
help: 'Select true to maintain aspect ratio',
default: false,
options: [true, false],
},
},
fn: (context, { shape, fill, border, borderWidth, maintainAspect }) => ({
export function shape() {
return {
name: 'shape',
aliases: [],
type: 'shape',
shape,
fill,
border,
borderWidth,
maintainAspect,
}),
});
help: 'Create a shape',
context: {
types: ['null'],
},
args: {
shape: {
types: ['string', 'null'],
help: 'Pick a shape',
aliases: ['_'],
default: 'square',
options: [
'arrow',
'arrowMulti',
'bookmark',
'cross',
'circle',
'hexagon',
'kite',
'pentagon',
'rhombus',
'semicircle',
'speechBubble',
'square',
'star',
'tag',
'triangle',
'triangleRight',
],
},
fill: {
types: ['string', 'null'],
help: 'Valid CSS color string',
default: 'black',
},
border: {
types: ['string', 'null'],
aliases: ['stroke'],
help: 'Valid CSS color string',
},
borderWidth: {
types: ['number', 'null'],
aliases: ['strokeWidth'],
help: 'Thickness of the border',
default: '0',
},
maintainAspect: {
types: ['boolean'],
help: 'Select true to maintain aspect ratio',
default: false,
options: [true, false],
},
},
fn: (context, { shape, fill, border, borderWidth, maintainAspect }) => ({
type: 'shape',
shape,
fill,
border,
borderWidth,
maintainAspect,
}),
};
}

View file

@ -6,34 +6,36 @@
import { sortBy } from 'lodash';
export const sort = () => ({
name: 'sort',
type: 'datatable',
help: 'Sorts a datatable on a column',
context: {
types: ['datatable'],
},
args: {
by: {
types: ['string'],
aliases: ['_', 'column'],
multi: false, // TODO: No reason you couldn't.
help:
'The column to sort on. If column is not specified, the datatable will be sorted on the first column.',
export function sort() {
return {
name: 'sort',
type: 'datatable',
help: 'Sorts a datatable on a column',
context: {
types: ['datatable'],
},
reverse: {
types: ['boolean'],
help:
'Reverse the sort order. If reverse is not specified, the datatable will be sorted in ascending order.',
options: [true, false],
args: {
by: {
types: ['string'],
aliases: ['_', 'column'],
multi: false, // TODO: No reason you couldn't.
help:
'The column to sort on. If column is not specified, the datatable will be sorted on the first column.',
},
reverse: {
types: ['boolean'],
help:
'Reverse the sort order. If reverse is not specified, the datatable will be sorted in ascending order.',
options: [true, false],
},
},
},
fn: (context, args) => {
const column = args.by || context.columns[0].name;
fn: (context, args) => {
const column = args.by || context.columns[0].name;
return {
...context,
rows: args.reverse ? sortBy(context.rows, column).reverse() : sortBy(context.rows, column),
};
},
});
return {
...context,
rows: args.reverse ? sortBy(context.rows, column).reverse() : sortBy(context.rows, column),
};
},
};
}

View file

@ -6,44 +6,46 @@
import { getType } from '@kbn/interpreter/common';
export const staticColumn = () => ({
name: 'staticColumn',
type: 'datatable',
help: 'Add a column with a static value',
context: {
types: ['datatable'],
},
args: {
name: {
types: ['string'],
aliases: ['_', 'column'],
help: 'The name of the new column column',
required: true,
export function staticColumn() {
return {
name: 'staticColumn',
type: 'datatable',
help: 'Add a column with a static value',
context: {
types: ['datatable'],
},
value: {
types: ['string', 'number', 'boolean', 'null'],
help:
'The value to insert in each column. Tip: use a sub-expression to rollup other columns into a static value',
default: null,
args: {
name: {
types: ['string'],
aliases: ['_', 'column'],
help: 'The name of the new column column',
required: true,
},
value: {
types: ['string', 'number', 'boolean', 'null'],
help:
'The value to insert in each column. Tip: use a sub-expression to rollup other columns into a static value',
default: null,
},
},
},
fn: (context, args) => {
const rows = context.rows.map(row => ({ ...row, [args.name]: args.value }));
const type = getType(args.value);
const columns = [...context.columns];
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
const newColumn = { name: args.name, type };
fn: (context, args) => {
const rows = context.rows.map(row => ({ ...row, [args.name]: args.value }));
const type = getType(args.value);
const columns = [...context.columns];
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
const newColumn = { name: args.name, type };
if (existingColumnIndex > -1) {
columns[existingColumnIndex] = newColumn;
} else {
columns.push(newColumn);
}
if (existingColumnIndex > -1) {
columns[existingColumnIndex] = newColumn;
} else {
columns.push(newColumn);
}
return {
type: 'datatable',
columns,
rows,
};
},
});
return {
type: 'datatable',
columns,
rows,
};
},
};
}

View file

@ -4,20 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const string = () => ({
name: 'string',
aliases: [],
type: 'string',
help:
'Output a string made of other strings. Mostly useful when combined with sub-expressions that output a string, ' +
' or something castable to a string',
args: {
value: {
aliases: ['_'],
types: ['string'],
multi: true,
help: "One or more strings to join together. Don't forget spaces where needed!",
export function string() {
return {
name: 'string',
aliases: [],
type: 'string',
help:
'Output a string made of other strings. Mostly useful when combined with sub-expressions that output a string, ' +
' or something castable to a string',
args: {
value: {
aliases: ['_'],
types: ['string'],
multi: true,
help: "One or more strings to join together. Don't forget spaces where needed!",
},
},
},
fn: (context, args) => args.value.join(''),
});
fn: (context, args) => args.value.join(''),
};
}

View file

@ -4,34 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const switchFn = () => ({
name: 'switch',
help: 'Perform conditional logic with multiple conditions',
args: {
case: {
types: ['case'],
aliases: ['_'],
resolve: false,
multi: true,
help: 'The list of conditions to check',
export function switchFn() {
return {
name: 'switch',
help: 'Perform conditional logic with multiple conditions',
args: {
case: {
types: ['case'],
aliases: ['_'],
resolve: false,
multi: true,
help: 'The list of conditions to check',
},
default: {
aliases: ['finally'],
resolve: false,
help: 'The default case if no cases match',
},
},
default: {
aliases: ['finally'],
resolve: false,
help: 'The default case if no cases match',
},
},
fn: async (context, args) => {
const cases = args.case || [];
for (let i = 0; i < cases.length; i++) {
const { matches, result } = await cases[i]();
if (matches) {
return result;
fn: async (context, args) => {
const cases = args.case || [];
for (let i = 0; i < cases.length; i++) {
const { matches, result } = await cases[i]();
if (matches) {
return result;
}
}
}
if (typeof args.default !== 'undefined') {
return await args.default();
}
return context;
},
});
if (typeof args.default !== 'undefined') {
return await args.default();
}
return context;
},
};
}

View file

@ -4,51 +4,54 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const table = () => ({
name: 'table',
aliases: [],
type: 'render',
help: 'Configure a Data Table element',
context: {
types: ['datatable'],
},
args: {
font: {
types: ['style'],
default: '{font}',
help: 'Font style',
export function table() {
return {
name: 'table',
aliases: [],
type: 'render',
help: 'Configure a Data Table element',
context: {
types: ['datatable'],
},
paginate: {
types: ['boolean'],
default: true,
help: 'Show pagination controls. If set to false only the first page will be displayed',
options: [true, false],
},
perPage: {
types: ['number'],
default: 10,
help: 'Show this many rows per page. You probably want to raise this is disabling pagination',
},
showHeader: {
types: ['boolean'],
default: true,
help: 'Show or hide the header row with titles for each column',
options: [true, false],
},
},
fn: (context, args) => {
const { font, paginate, perPage, showHeader } = args;
return {
type: 'render',
as: 'table',
value: {
datatable: context,
font,
paginate,
perPage,
showHeader,
args: {
font: {
types: ['style'],
default: '{font}',
help: 'Font style',
},
};
},
});
paginate: {
types: ['boolean'],
default: true,
help: 'Show pagination controls. If set to false only the first page will be displayed',
options: [true, false],
},
perPage: {
types: ['number'],
default: 10,
help:
'Show this many rows per page. You probably want to raise this is disabling pagination',
},
showHeader: {
types: ['boolean'],
default: true,
help: 'Show or hide the header row with titles for each column',
options: [true, false],
},
},
fn: (context, args) => {
const { font, paginate, perPage, showHeader } = args;
return {
type: 'render',
as: 'table',
value: {
datatable: context,
font,
paginate,
perPage,
showHeader,
},
};
},
};
}

View file

@ -6,23 +6,25 @@
import { takeRight } from 'lodash';
export const tail = () => ({
name: 'tail',
aliases: [],
type: 'datatable',
help: 'Get the last N rows from the end of a datatable. Also see `head`',
context: {
types: ['datatable'],
},
args: {
count: {
aliases: ['_'],
types: ['number'],
help: 'Return this many rows from the end of the datatable',
export function tail() {
return {
name: 'tail',
aliases: [],
type: 'datatable',
help: 'Get the last N rows from the end of a datatable. Also see `head`',
context: {
types: ['datatable'],
},
},
fn: (context, args) => ({
...context,
rows: takeRight(context.rows, args.count),
}),
});
args: {
count: {
aliases: ['_'],
types: ['number'],
help: 'Return this many rows from the end of the datatable',
},
},
fn: (context, args) => ({
...context,
rows: takeRight(context.rows, args.count),
}),
};
}

View file

@ -6,63 +6,65 @@
import dateMath from '@elastic/datemath';
export const timefilter = () => ({
name: 'timefilter',
aliases: [],
type: 'filter',
context: {
types: ['filter'],
},
help: 'Create a timefilter for querying a source',
args: {
column: {
type: ['string'],
aliases: ['field', 'c'],
default: '@timestamp',
help: 'The column or field to attach the filter to',
export function timefilter() {
return {
name: 'timefilter',
aliases: [],
type: 'filter',
context: {
types: ['filter'],
},
from: {
types: ['string', 'null'],
aliases: ['f', 'start'],
help: 'Beginning of the range, in ISO8601 or Elasticsearch datemath format',
help: 'Create a timefilter for querying a source',
args: {
column: {
type: ['string'],
aliases: ['field', 'c'],
default: '@timestamp',
help: 'The column or field to attach the filter to',
},
from: {
types: ['string', 'null'],
aliases: ['f', 'start'],
help: 'Beginning of the range, in ISO8601 or Elasticsearch datemath format',
},
to: {
types: ['string', 'null'],
aliases: ['t', 'end'],
help: 'End of the range, in ISO8601 or Elasticsearch datemath format',
},
},
to: {
types: ['string', 'null'],
aliases: ['t', 'end'],
help: 'End of the range, in ISO8601 or Elasticsearch datemath format',
},
},
fn: (context, args) => {
if (!args.from && !args.to) {
return context;
}
const { from, to, column } = args;
const filter = {
type: 'time',
column,
};
function parseAndValidate(str) {
if (!str) {
return;
fn: (context, args) => {
if (!args.from && !args.to) {
return context;
}
const moment = dateMath.parse(str);
if (!moment || !moment.isValid()) {
throw new Error(`Invalid date/time string: '${str}'`);
const { from, to, column } = args;
const filter = {
type: 'time',
column,
};
function parseAndValidate(str) {
if (!str) {
return;
}
const moment = dateMath.parse(str);
if (!moment || !moment.isValid()) {
throw new Error(`Invalid date/time string: '${str}'`);
}
return moment.toISOString();
}
return moment.toISOString();
}
if (to != null) {
filter.to = parseAndValidate(to);
}
if (to != null) {
filter.to = parseAndValidate(to);
}
if (from != null) {
filter.from = parseAndValidate(from);
}
if (from != null) {
filter.from = parseAndValidate(from);
}
return { ...context, and: [...context.and, filter] };
},
});
return { ...context, and: [...context.and, filter] };
},
};
}

View file

@ -4,32 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const timefilterControl = () => ({
name: 'timefilterControl',
aliases: [],
type: 'render',
context: {
types: ['null'],
},
help: 'Configure a time filter control element',
args: {
column: {
type: ['string'],
aliases: ['field', 'c'],
help: 'The column or field to attach the filter to',
export function timefilterControl() {
return {
name: 'timefilterControl',
aliases: [],
type: 'render',
context: {
types: ['null'],
},
compact: {
type: ['boolean'],
help: 'Show the time filter as a button that triggers a popover',
default: true,
options: [true, false],
help: 'Configure a time filter control element',
args: {
column: {
type: ['string'],
aliases: ['field', 'c'],
help: 'The column or field to attach the filter to',
},
compact: {
type: ['boolean'],
help: 'Show the time filter as a button that triggers a popover',
default: true,
options: [true, false],
},
},
},
fn: (context, args) => {
return {
type: 'render',
as: 'time_filter',
value: args,
};
},
});
fn: (context, args) => {
return {
type: 'render',
as: 'time_filter',
value: args,
};
},
};
}

View file

@ -8,63 +8,65 @@ import { sortBy } from 'lodash';
import { queryDatatable } from '../../../../common/lib/datatable/query';
import { getDemoRows } from './get_demo_rows';
export const demodata = () => ({
name: 'demodata',
aliases: [],
type: 'datatable',
help: 'A mock data set that includes project CI times with usernames, countries and run phases',
context: {
types: ['filter'],
},
args: {
type: {
types: ['string', 'null'],
aliases: ['_'],
help: 'The name of the demo data set to use',
default: 'ci',
options: ['ci', 'shirts'],
export function demodata() {
return {
name: 'demodata',
aliases: [],
type: 'datatable',
help: 'A mock data set that includes project CI times with usernames, countries and run phases',
context: {
types: ['filter'],
},
},
fn: (context, args) => {
const demoRows = getDemoRows(args.type);
let set = {};
if (args.type === 'ci') {
set = {
columns: [
{ name: '@timestamp', type: 'date' },
{ name: 'time', type: 'date' },
{ name: 'cost', type: 'number' },
{ name: 'username', type: 'string' },
{ name: 'price', type: 'number' },
{ name: 'age', type: 'number' },
{ name: 'country', type: 'string' },
{ name: 'state', type: 'string' },
{ name: 'project', type: 'string' },
{ name: 'percent_uptime', type: 'number' },
],
rows: sortBy(demoRows, 'time'),
};
} else if (args.type === 'shirts') {
set = {
columns: [
{ name: 'size', type: 'string' },
{ name: 'color', type: 'string' },
{ name: 'price', type: 'number' },
{ name: 'cut', type: 'string' },
],
rows: demoRows,
};
}
const { columns, rows } = set;
return queryDatatable(
{
type: 'datatable',
columns,
rows,
args: {
type: {
types: ['string', 'null'],
aliases: ['_'],
help: 'The name of the demo data set to use',
default: 'ci',
options: ['ci', 'shirts'],
},
context
);
},
});
},
fn: (context, args) => {
const demoRows = getDemoRows(args.type);
let set = {};
if (args.type === 'ci') {
set = {
columns: [
{ name: '@timestamp', type: 'date' },
{ name: 'time', type: 'date' },
{ name: 'cost', type: 'number' },
{ name: 'username', type: 'string' },
{ name: 'price', type: 'number' },
{ name: 'age', type: 'number' },
{ name: 'country', type: 'string' },
{ name: 'state', type: 'string' },
{ name: 'project', type: 'string' },
{ name: 'percent_uptime', type: 'number' },
],
rows: sortBy(demoRows, 'time'),
};
} else if (args.type === 'shirts') {
set = {
columns: [
{ name: 'size', type: 'string' },
{ name: 'color', type: 'string' },
{ name: 'price', type: 'number' },
{ name: 'cut', type: 'string' },
],
rows: demoRows,
};
}
const { columns, rows } = set;
return queryDatatable(
{
type: 'datatable',
columns,
rows,
},
context
);
},
};
}

View file

@ -6,48 +6,50 @@
import { buildESRequest } from '../../../server/lib/build_es_request';
export const escount = () => ({
name: 'escount',
type: 'number',
help: 'Query elasticsearch for a count of the number of hits matching a query',
context: {
types: ['filter'],
},
args: {
index: {
types: ['string', 'null'],
default: '_all',
help: 'Specify an index pattern. Eg "logstash-*"',
export function escount() {
return {
name: 'escount',
type: 'number',
help: 'Query elasticsearch for a count of the number of hits matching a query',
context: {
types: ['filter'],
},
query: {
types: ['string'],
aliases: ['_', 'q'],
help: 'A Lucene query string',
default: '"-_index:.kibana"',
},
},
fn: (context, args, handlers) => {
context.and = context.and.concat([
{
type: 'luceneQueryString',
query: args.query,
args: {
index: {
types: ['string', 'null'],
default: '_all',
help: 'Specify an index pattern. Eg "logstash-*"',
},
]);
query: {
types: ['string'],
aliases: ['_', 'q'],
help: 'A Lucene query string',
default: '"-_index:.kibana"',
},
},
fn: (context, args, handlers) => {
context.and = context.and.concat([
{
type: 'luceneQueryString',
query: args.query,
},
]);
const esRequest = buildESRequest(
{
index: args.index,
body: {
query: {
bool: {
must: [{ match_all: {} }],
const esRequest = buildESRequest(
{
index: args.index,
body: {
query: {
bool: {
must: [{ match_all: {} }],
},
},
},
},
},
context
);
context
);
return handlers.elasticsearchClient('count', esRequest).then(resp => resp.count);
},
});
return handlers.elasticsearchClient('count', esRequest).then(resp => resp.count);
},
};
}

View file

@ -7,78 +7,80 @@
import squel from 'squel';
import { queryEsSQL } from '../../../server/lib/query_es_sql';
export const esdocs = () => ({
name: 'esdocs',
type: 'datatable',
help:
'Query elasticsearch and get back raw documents. We recommend you specify the fields you want, ' +
'especially if you are going to ask for a lot of rows',
context: {
types: ['filter'],
},
args: {
index: {
types: ['string', 'null'],
default: '_all',
help: 'Specify an index pattern. Eg "logstash-*"',
export function esdocs() {
return {
name: 'esdocs',
type: 'datatable',
help:
'Query elasticsearch and get back raw documents. We recommend you specify the fields you want, ' +
'especially if you are going to ask for a lot of rows',
context: {
types: ['filter'],
},
query: {
types: ['string'],
aliases: ['_', 'q'],
help: 'A Lucene query string',
default: '-_index:.kibana',
},
sort: {
types: ['string', 'null'],
help: 'Sort directions as "field, direction". Eg "@timestamp, desc" or "bytes, asc"',
},
fields: {
help: 'Comma separated list of fields. Fewer fields will perform better',
types: ['string', 'null'],
},
metaFields: {
help: 'Comma separated list of meta fields, eg "_index,_type"',
types: ['string', 'null'],
},
count: {
types: ['number'],
default: 100,
help: 'The number of docs to pull back. Smaller numbers perform better',
},
},
fn: (context, args, handlers) => {
context.and = context.and.concat([
{
type: 'luceneQueryString',
query: args.query,
args: {
index: {
types: ['string', 'null'],
default: '_all',
help: 'Specify an index pattern. Eg "logstash-*"',
},
]);
query: {
types: ['string'],
aliases: ['_', 'q'],
help: 'A Lucene query string',
default: '-_index:.kibana',
},
sort: {
types: ['string', 'null'],
help: 'Sort directions as "field, direction". Eg "@timestamp, desc" or "bytes, asc"',
},
fields: {
help: 'Comma separated list of fields. Fewer fields will perform better',
types: ['string', 'null'],
},
metaFields: {
help: 'Comma separated list of meta fields, eg "_index,_type"',
types: ['string', 'null'],
},
count: {
types: ['number'],
default: 100,
help: 'The number of docs to pull back. Smaller numbers perform better',
},
},
fn: (context, args, handlers) => {
context.and = context.and.concat([
{
type: 'luceneQueryString',
query: args.query,
},
]);
let query = squel
.select({
autoQuoteTableNames: true,
autoQuoteFieldNames: true,
autoQuoteAliasNames: true,
nameQuoteCharacter: '"',
})
.from(args.index.toLowerCase());
let query = squel
.select({
autoQuoteTableNames: true,
autoQuoteFieldNames: true,
autoQuoteAliasNames: true,
nameQuoteCharacter: '"',
})
.from(args.index.toLowerCase());
if (args.fields) {
const fields = args.fields.split(',').map(field => field.trim());
fields.forEach(field => (query = query.field(field)));
}
if (args.sort) {
const [sortField, sortOrder] = args.sort.split(',').map(str => str.trim());
if (sortField) {
query.order(`"${sortField}"`, sortOrder.toLowerCase() === 'asc');
if (args.fields) {
const fields = args.fields.split(',').map(field => field.trim());
fields.forEach(field => (query = query.field(field)));
}
}
return queryEsSQL(handlers.elasticsearchClient, {
count: args.count,
query: query.toString(),
filter: context.and,
});
},
});
if (args.sort) {
const [sortField, sortOrder] = args.sort.split(',').map(str => str.trim());
if (sortField) {
query.order(`"${sortField}"`, sortOrder.toLowerCase() === 'asc');
}
}
return queryEsSQL(handlers.elasticsearchClient, {
count: args.count,
query: query.toString(),
filter: context.and,
});
},
};
}

View file

@ -6,30 +6,32 @@
import { queryEsSQL } from '../../../server/lib/query_es_sql';
export const essql = () => ({
name: 'essql',
type: 'datatable',
context: {
types: ['filter'],
},
help: 'Elasticsearch SQL',
args: {
query: {
aliases: ['_', 'q'],
types: ['string'],
help: 'SQL query',
export function essql() {
return {
name: 'essql',
type: 'datatable',
context: {
types: ['filter'],
},
count: {
types: ['number'],
default: 1000,
help: 'Elasticsearch SQL',
args: {
query: {
aliases: ['_', 'q'],
types: ['string'],
help: 'SQL query',
},
count: {
types: ['number'],
default: 1000,
},
timezone: {
aliases: ['tz'],
types: ['string'],
default: 'UTC',
help: 'Timezone to use for date operations, valid ISO formats and UTC offsets both work',
},
},
timezone: {
aliases: ['tz'],
types: ['string'],
default: 'UTC',
help: 'Timezone to use for date operations, valid ISO formats and UTC offsets both work',
},
},
fn: (context, args, handlers) =>
queryEsSQL(handlers.elasticsearchClient, { ...args, filter: context.and }),
});
fn: (context, args, handlers) =>
queryEsSQL(handlers.elasticsearchClient, { ...args, filter: context.and }),
};
}

View file

@ -17,166 +17,170 @@ import { getExpressionType } from './lib/get_expression_type';
const columnExists = (cols, colName) => cols.includes(unquoteString(colName));
export const pointseries = () => ({
name: 'pointseries',
type: 'pointseries',
help:
'Turn a datatable into a point series model. Currently we differentiate measure from dimensions by looking for a [TinyMath function](https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html). ' +
'If you enter a TinyMath expression in your argument, we treat that argument as a measure, otherwise it is a dimension. Dimensions are combined to create unique ' +
'keys. Measures are then deduplicated by those keys using the specified TinyMath function',
context: {
types: ['datatable'],
},
args: {
x: {
types: ['string', 'null'],
help: 'The values along the X-axis',
export function pointseries() {
return {
name: 'pointseries',
type: 'pointseries',
help:
'Turn a datatable into a point series model. Currently we differentiate measure from dimensions by looking for a [TinyMath function](https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html). ' +
'If you enter a TinyMath expression in your argument, we treat that argument as a measure, otherwise it is a dimension. Dimensions are combined to create unique ' +
'keys. Measures are then deduplicated by those keys using the specified TinyMath function',
context: {
types: ['datatable'],
},
y: {
types: ['string', 'null'],
help: 'The values along the y-axis',
args: {
x: {
types: ['string', 'null'],
help: 'The values along the X-axis',
},
y: {
types: ['string', 'null'],
help: 'The values along the y-axis',
},
color: {
types: ['string', 'null'],
help: "An expression to use in determining the mark's color", // If you need categorization, transform the field.
},
size: {
types: ['string', 'null'],
help: 'For elements that support it, the size of the marks',
},
text: {
types: ['string', 'null'],
help: 'For use in charts that support it, the text to show in the mark',
},
// In the future it may make sense to add things like shape, or tooltip values, but I think what we have is good for now
// The way the function below is written you can add as many arbitrary named args as you want.
},
color: {
types: ['string', 'null'],
help: "An expression to use in determining the mark's color", // If you need categorization, transform the field.
},
size: {
types: ['string', 'null'],
help: 'For elements that support it, the size of the marks',
},
text: {
types: ['string', 'null'],
help: 'For use in charts that support it, the text to show in the mark',
},
// In the future it may make sense to add things like shape, or tooltip values, but I think what we have is good for now
// The way the function below is written you can add as many arbitrary named args as you want.
},
fn: (context, args) => {
// Note: can't replace pivotObjectArray with datatableToMathContext, lose name of non-numeric columns
const columnNames = context.columns.map(col => col.name);
const mathScope = pivotObjectArray(context.rows, columnNames);
const autoQuoteColumn = col => {
if (!columnNames.includes(col)) {
return col;
}
return col.match(/\s/) ? `'${col}'` : col;
};
const measureNames = [];
const dimensions = [];
const columns = {};
// Separates args into dimensions and measures arrays
// by checking if arg is a column reference (dimension)
Object.keys(args).forEach(arg => {
const mathExp = autoQuoteColumn(args[arg]);
if (mathExp != null && mathExp.trim() !== '') {
const col = {
type: '',
role: '',
expression: mathExp,
};
if (isColumnReference(mathExp)) {
// TODO: Do something better if the column does not exist
if (!columnExists(columnNames, mathExp)) {
return;
}
dimensions.push({
name: arg,
value: mathExp,
});
col.type = getExpressionType(context.columns, mathExp);
col.role = 'dimension';
} else {
measureNames.push(arg);
col.type = 'number';
col.role = 'measure';
fn: (context, args) => {
// Note: can't replace pivotObjectArray with datatableToMathContext, lose name of non-numeric columns
const columnNames = context.columns.map(col => col.name);
const mathScope = pivotObjectArray(context.rows, columnNames);
const autoQuoteColumn = col => {
if (!columnNames.includes(col)) {
return col;
}
return col.match(/\s/) ? `'${col}'` : col;
};
columns[arg] = col;
}
});
const measureNames = [];
const dimensions = [];
const columns = {};
const PRIMARY_KEY = '%%CANVAS_POINTSERIES_PRIMARY_KEY%%';
const rows = context.rows.map((row, i) => ({ ...row, [PRIMARY_KEY]: i }));
// Separates args into dimensions and measures arrays
// by checking if arg is a column reference (dimension)
Object.keys(args).forEach(arg => {
const mathExp = autoQuoteColumn(args[arg]);
function normalizeValue(expression, value) {
switch (getExpressionType(context.columns, expression)) {
case 'string':
return String(value);
case 'number':
return Number(value);
case 'date':
return moment(value).valueOf();
default:
return value;
}
}
if (mathExp != null && mathExp.trim() !== '') {
const col = {
type: '',
role: '',
expression: mathExp,
};
// Dimensions
// Group rows by their dimension values, using the argument values and preserving the PRIMARY_KEY
// There's probably a better way to do this
const results = rows.reduce((acc, row, i) => {
const newRow = dimensions.reduce(
(acc, { name, value }) => {
try {
acc[name] = args[name] ? normalizeValue(value, evaluate(value, mathScope)[i]) : '_all';
} catch (e) {
// TODO: handle invalid column names...
// Do nothing if column does not exist
// acc[dimension] = '_all';
if (isColumnReference(mathExp)) {
// TODO: Do something better if the column does not exist
if (!columnExists(columnNames, mathExp)) {
return;
}
dimensions.push({
name: arg,
value: mathExp,
});
col.type = getExpressionType(context.columns, mathExp);
col.role = 'dimension';
} else {
measureNames.push(arg);
col.type = 'number';
col.role = 'measure';
}
return acc;
},
{ [PRIMARY_KEY]: row[PRIMARY_KEY] }
columns[arg] = col;
}
});
const PRIMARY_KEY = '%%CANVAS_POINTSERIES_PRIMARY_KEY%%';
const rows = context.rows.map((row, i) => ({ ...row, [PRIMARY_KEY]: i }));
function normalizeValue(expression, value) {
switch (getExpressionType(context.columns, expression)) {
case 'string':
return String(value);
case 'number':
return Number(value);
case 'date':
return moment(value).valueOf();
default:
return value;
}
}
// Dimensions
// Group rows by their dimension values, using the argument values and preserving the PRIMARY_KEY
// There's probably a better way to do this
const results = rows.reduce((acc, row, i) => {
const newRow = dimensions.reduce(
(acc, { name, value }) => {
try {
acc[name] = args[name]
? normalizeValue(value, evaluate(value, mathScope)[i])
: '_all';
} catch (e) {
// TODO: handle invalid column names...
// Do nothing if column does not exist
// acc[dimension] = '_all';
}
return acc;
},
{ [PRIMARY_KEY]: row[PRIMARY_KEY] }
);
return Object.assign(acc, { [row[PRIMARY_KEY]]: newRow });
}, {});
// Measures
// First group up all of the distinct dimensioned bits. Each of these will be reduced to just 1 value
// for each measure
const measureKeys = groupBy(rows, row =>
dimensions.map(({ name }) => (args[name] ? row[args[name]] : '_all')).join('::%BURLAP%::')
);
return Object.assign(acc, { [row[PRIMARY_KEY]]: newRow });
}, {});
// Then compute that 1 value for each measure
values(measureKeys).forEach(rows => {
const subtable = { type: 'datatable', columns: context.columns, rows: rows };
const subScope = pivotObjectArray(subtable.rows, subtable.columns.map(col => col.name));
const measureValues = measureNames.map(measure => {
try {
const ev = evaluate(args[measure], subScope);
if (Array.isArray(ev)) {
throw new Error('Expressions must be wrapped in a function such as sum()');
}
// Measures
// First group up all of the distinct dimensioned bits. Each of these will be reduced to just 1 value
// for each measure
const measureKeys = groupBy(rows, row =>
dimensions.map(({ name }) => (args[name] ? row[args[name]] : '_all')).join('::%BURLAP%::')
);
// Then compute that 1 value for each measure
values(measureKeys).forEach(rows => {
const subtable = { type: 'datatable', columns: context.columns, rows: rows };
const subScope = pivotObjectArray(subtable.rows, subtable.columns.map(col => col.name));
const measureValues = measureNames.map(measure => {
try {
const ev = evaluate(args[measure], subScope);
if (Array.isArray(ev)) {
throw new Error('Expressions must be wrapped in a function such as sum()');
return ev;
} catch (e) {
// TODO: don't catch if eval to Array
return null;
}
});
return ev;
} catch (e) {
// TODO: don't catch if eval to Array
return null;
}
rows.forEach(row => {
Object.assign(results[row[PRIMARY_KEY]], zipObject(measureNames, measureValues));
});
});
rows.forEach(row => {
Object.assign(results[row[PRIMARY_KEY]], zipObject(measureNames, measureValues));
});
});
// It only makes sense to uniq the rows in a point series as 2 values can not exist in the exact same place at the same time.
const resultingRows = uniqBy(
values(results).map(row => omit(row, PRIMARY_KEY)),
JSON.stringify
);
// It only makes sense to uniq the rows in a point series as 2 values can not exist in the exact same place at the same time.
const resultingRows = uniqBy(
values(results).map(row => omit(row, PRIMARY_KEY)),
JSON.stringify
);
return {
type: 'pointseries',
columns: columns,
rows: resultingRows,
};
},
});
return {
type: 'pointseries',
columns: columns,
rows: resultingRows,
};
},
};
}

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const server = () => ({
name: 'server',
help: 'Force the interpreter to return to the server',
args: {},
fn: context => context,
});
export function server() {
return {
name: 'server',
help: 'Force the interpreter to return to the server',
args: {},
fn: context => context,
};
}