2019-09-17 20:57:53 +02:00
|
|
|
/*
|
|
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
|
|
*/
|
|
|
|
|
2020-09-29 19:00:29 +02:00
|
|
|
import './expression.scss';
|
|
|
|
|
2020-05-19 11:14:26 +02:00
|
|
|
import React, { useMemo } from 'react';
|
2019-09-17 20:57:53 +02:00
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
import { i18n } from '@kbn/i18n';
|
2020-04-30 23:44:16 +02:00
|
|
|
import { I18nProvider } from '@kbn/i18n/react';
|
|
|
|
import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
|
|
|
import { IAggType } from 'src/plugins/data/public';
|
2020-05-15 12:01:27 +02:00
|
|
|
import {
|
|
|
|
FormatFactory,
|
|
|
|
ILensInterpreterRenderHandlers,
|
|
|
|
LensFilterEvent,
|
|
|
|
LensMultiTable,
|
|
|
|
} from '../types';
|
2019-10-25 22:32:59 +02:00
|
|
|
import {
|
2020-02-11 19:47:36 +01:00
|
|
|
ExpressionFunctionDefinition,
|
|
|
|
ExpressionRenderDefinition,
|
2020-04-15 12:22:37 +02:00
|
|
|
} from '../../../../../src/plugins/expressions/public';
|
2019-09-25 17:11:13 +02:00
|
|
|
import { VisualizationContainer } from '../visualization_container';
|
2020-05-19 11:14:26 +02:00
|
|
|
import { EmptyPlaceholder } from '../shared_components';
|
2020-05-29 09:38:07 +02:00
|
|
|
import { desanitizeFilterContext } from '../utils';
|
2020-09-15 11:24:02 +02:00
|
|
|
import { LensIconChartDatatable } from '../assets/chart_datatable';
|
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
export interface DatatableColumns {
|
|
|
|
columnIds: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Args {
|
2020-02-11 19:47:36 +01:00
|
|
|
title: string;
|
|
|
|
columns: DatatableColumns & { type: 'lens_datatable_columns' };
|
2019-09-17 20:57:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface DatatableProps {
|
|
|
|
data: LensMultiTable;
|
|
|
|
args: Args;
|
|
|
|
}
|
|
|
|
|
2020-04-30 23:44:16 +02:00
|
|
|
type DatatableRenderProps = DatatableProps & {
|
|
|
|
formatFactory: FormatFactory;
|
2020-05-15 12:01:27 +02:00
|
|
|
onClickValue: (data: LensFilterEvent['data']) => void;
|
2020-04-30 23:44:16 +02:00
|
|
|
getType: (name: string) => IAggType;
|
|
|
|
};
|
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
export interface DatatableRender {
|
|
|
|
type: 'render';
|
|
|
|
as: 'lens_datatable_renderer';
|
|
|
|
value: DatatableProps;
|
|
|
|
}
|
|
|
|
|
2020-02-11 19:47:36 +01:00
|
|
|
export const datatable: ExpressionFunctionDefinition<
|
2019-09-17 20:57:53 +02:00
|
|
|
'lens_datatable',
|
2020-02-11 19:47:36 +01:00
|
|
|
LensMultiTable,
|
2019-09-17 20:57:53 +02:00
|
|
|
Args,
|
|
|
|
DatatableRender
|
2020-02-11 19:47:36 +01:00
|
|
|
> = {
|
2019-09-17 20:57:53 +02:00
|
|
|
name: 'lens_datatable',
|
|
|
|
type: 'render',
|
2020-02-11 19:47:36 +01:00
|
|
|
inputTypes: ['lens_multitable'],
|
2019-09-17 20:57:53 +02:00
|
|
|
help: i18n.translate('xpack.lens.datatable.expressionHelpLabel', {
|
|
|
|
defaultMessage: 'Datatable renderer',
|
|
|
|
}),
|
|
|
|
args: {
|
|
|
|
title: {
|
|
|
|
types: ['string'],
|
|
|
|
help: i18n.translate('xpack.lens.datatable.titleLabel', {
|
|
|
|
defaultMessage: 'Title',
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
columns: {
|
|
|
|
types: ['lens_datatable_columns'],
|
|
|
|
help: '',
|
|
|
|
},
|
|
|
|
},
|
2020-02-11 19:47:36 +01:00
|
|
|
fn(data, args) {
|
2019-09-17 20:57:53 +02:00
|
|
|
return {
|
|
|
|
type: 'render',
|
|
|
|
as: 'lens_datatable_renderer',
|
|
|
|
value: {
|
|
|
|
data,
|
|
|
|
args,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
},
|
2020-02-11 19:47:36 +01:00
|
|
|
};
|
2019-09-17 20:57:53 +02:00
|
|
|
|
|
|
|
type DatatableColumnsResult = DatatableColumns & { type: 'lens_datatable_columns' };
|
|
|
|
|
2020-02-11 19:47:36 +01:00
|
|
|
export const datatableColumns: ExpressionFunctionDefinition<
|
2019-09-17 20:57:53 +02:00
|
|
|
'lens_datatable_columns',
|
|
|
|
null,
|
|
|
|
DatatableColumns,
|
|
|
|
DatatableColumnsResult
|
|
|
|
> = {
|
|
|
|
name: 'lens_datatable_columns',
|
|
|
|
aliases: [],
|
|
|
|
type: 'lens_datatable_columns',
|
|
|
|
help: '',
|
2020-02-11 19:47:36 +01:00
|
|
|
inputTypes: ['null'],
|
2019-09-17 20:57:53 +02:00
|
|
|
args: {
|
|
|
|
columnIds: {
|
|
|
|
types: ['string'],
|
|
|
|
multi: true,
|
|
|
|
help: '',
|
|
|
|
},
|
|
|
|
},
|
2020-02-11 19:47:36 +01:00
|
|
|
fn: function fn(input: unknown, args: DatatableColumns) {
|
2019-09-17 20:57:53 +02:00
|
|
|
return {
|
|
|
|
type: 'lens_datatable_columns',
|
|
|
|
...args,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2020-04-30 23:44:16 +02:00
|
|
|
export const getDatatableRenderer = (dependencies: {
|
|
|
|
formatFactory: Promise<FormatFactory>;
|
|
|
|
getType: Promise<(name: string) => IAggType>;
|
|
|
|
}): ExpressionRenderDefinition<DatatableProps> => ({
|
2019-09-17 20:57:53 +02:00
|
|
|
name: 'lens_datatable_renderer',
|
|
|
|
displayName: i18n.translate('xpack.lens.datatable.visualizationName', {
|
|
|
|
defaultMessage: 'Datatable',
|
|
|
|
}),
|
|
|
|
help: '',
|
2020-02-11 19:47:36 +01:00
|
|
|
validate: () => undefined,
|
2019-09-17 20:57:53 +02:00
|
|
|
reuseDomNode: true,
|
2019-10-25 22:32:59 +02:00
|
|
|
render: async (
|
|
|
|
domNode: Element,
|
|
|
|
config: DatatableProps,
|
2020-05-15 12:01:27 +02:00
|
|
|
handlers: ILensInterpreterRenderHandlers
|
2019-10-25 22:32:59 +02:00
|
|
|
) => {
|
2020-04-30 23:44:16 +02:00
|
|
|
const resolvedFormatFactory = await dependencies.formatFactory;
|
|
|
|
const resolvedGetType = await dependencies.getType;
|
2020-05-15 12:01:27 +02:00
|
|
|
const onClickValue = (data: LensFilterEvent['data']) => {
|
|
|
|
handlers.event({ name: 'filter', data });
|
|
|
|
};
|
2019-10-25 22:32:59 +02:00
|
|
|
ReactDOM.render(
|
2020-04-30 23:44:16 +02:00
|
|
|
<I18nProvider>
|
|
|
|
<DatatableComponent
|
|
|
|
{...config}
|
|
|
|
formatFactory={resolvedFormatFactory}
|
2020-05-15 12:01:27 +02:00
|
|
|
onClickValue={onClickValue}
|
2020-04-30 23:44:16 +02:00
|
|
|
getType={resolvedGetType}
|
|
|
|
/>
|
|
|
|
</I18nProvider>,
|
2019-10-25 22:32:59 +02:00
|
|
|
domNode,
|
|
|
|
() => {
|
|
|
|
handlers.done();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
|
2019-09-17 20:57:53 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2020-04-30 23:44:16 +02:00
|
|
|
export function DatatableComponent(props: DatatableRenderProps) {
|
2019-09-17 20:57:53 +02:00
|
|
|
const [firstTable] = Object.values(props.data.tables);
|
|
|
|
const formatters: Record<string, ReturnType<FormatFactory>> = {};
|
|
|
|
|
2020-05-22 09:08:58 +02:00
|
|
|
firstTable.columns.forEach((column) => {
|
2019-09-17 20:57:53 +02:00
|
|
|
formatters[column.id] = props.formatFactory(column.formatHint);
|
|
|
|
});
|
|
|
|
|
2020-08-06 10:10:09 +02:00
|
|
|
const { onClickValue } = props;
|
2020-05-19 11:14:26 +02:00
|
|
|
const handleFilterClick = useMemo(
|
|
|
|
() => (field: string, value: unknown, colIndex: number, negate: boolean = false) => {
|
|
|
|
const col = firstTable.columns[colIndex];
|
2020-08-27 00:27:40 +02:00
|
|
|
const isDate = col.meta?.type === 'date_histogram' || col.meta?.type === 'date_range';
|
|
|
|
const timeFieldName = negate && isDate ? undefined : col?.meta?.aggConfigParams?.field;
|
2020-05-22 09:08:58 +02:00
|
|
|
const rowIndex = firstTable.rows.findIndex((row) => row[field] === value);
|
2020-05-19 11:14:26 +02:00
|
|
|
|
|
|
|
const data: LensFilterEvent['data'] = {
|
|
|
|
negate,
|
|
|
|
data: [
|
|
|
|
{
|
|
|
|
row: rowIndex,
|
|
|
|
column: colIndex,
|
|
|
|
value,
|
|
|
|
table: firstTable,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
timeFieldName,
|
|
|
|
};
|
2020-08-06 10:10:09 +02:00
|
|
|
onClickValue(desanitizeFilterContext(data));
|
2020-05-19 11:14:26 +02:00
|
|
|
},
|
2020-08-06 10:10:09 +02:00
|
|
|
[firstTable, onClickValue]
|
2020-05-19 11:14:26 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
const bucketColumns = firstTable.columns
|
2020-05-22 09:08:58 +02:00
|
|
|
.filter((col) => {
|
2020-05-19 11:14:26 +02:00
|
|
|
return col?.meta?.type && props.getType(col.meta.type)?.type === 'buckets';
|
|
|
|
})
|
2020-05-22 09:08:58 +02:00
|
|
|
.map((col) => col.id);
|
2020-05-19 11:14:26 +02:00
|
|
|
|
|
|
|
const isEmpty =
|
|
|
|
firstTable.rows.length === 0 ||
|
|
|
|
(bucketColumns.length &&
|
2020-05-22 09:08:58 +02:00
|
|
|
firstTable.rows.every((row) =>
|
|
|
|
bucketColumns.every((col) => typeof row[col] === 'undefined')
|
|
|
|
));
|
2020-05-19 11:14:26 +02:00
|
|
|
|
|
|
|
if (isEmpty) {
|
2020-09-15 11:24:02 +02:00
|
|
|
return <EmptyPlaceholder icon={LensIconChartDatatable} />;
|
2020-05-19 11:14:26 +02:00
|
|
|
}
|
2020-04-30 23:44:16 +02:00
|
|
|
|
2019-09-17 20:57:53 +02:00
|
|
|
return (
|
2019-09-25 17:11:13 +02:00
|
|
|
<VisualizationContainer>
|
|
|
|
<EuiBasicTable
|
|
|
|
className="lnsDataTable"
|
|
|
|
data-test-subj="lnsDataTable"
|
2020-04-17 23:03:33 +02:00
|
|
|
tableLayout="auto"
|
2019-09-25 17:11:13 +02:00
|
|
|
columns={props.args.columns.columnIds
|
2020-05-22 09:08:58 +02:00
|
|
|
.map((field) => {
|
|
|
|
const col = firstTable.columns.find((c) => c.id === field);
|
2020-05-19 11:14:26 +02:00
|
|
|
const filterable = bucketColumns.includes(field);
|
2020-05-22 09:08:58 +02:00
|
|
|
const colIndex = firstTable.columns.findIndex((c) => c.id === field);
|
2019-09-25 17:11:13 +02:00
|
|
|
return {
|
|
|
|
field,
|
2019-10-16 05:02:18 +02:00
|
|
|
name: (col && col.name) || '',
|
2020-04-30 23:44:16 +02:00
|
|
|
render: (value: unknown) => {
|
|
|
|
const formattedValue = formatters[field]?.convert(value);
|
|
|
|
const fieldName = col?.meta?.aggConfigParams?.field;
|
|
|
|
|
|
|
|
if (filterable) {
|
|
|
|
return (
|
|
|
|
<EuiFlexGroup
|
|
|
|
className="lnsDataTable__cell"
|
|
|
|
data-test-subj="lnsDataTableCellValueFilterable"
|
|
|
|
gutterSize="xs"
|
|
|
|
>
|
|
|
|
<EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem>
|
|
|
|
<EuiFlexItem grow={false}>
|
|
|
|
<EuiFlexGroup
|
|
|
|
responsive={false}
|
|
|
|
gutterSize="none"
|
|
|
|
alignItems="center"
|
|
|
|
className="lnsDataTable__filter"
|
|
|
|
>
|
|
|
|
<EuiToolTip
|
|
|
|
position="bottom"
|
|
|
|
content={i18n.translate('xpack.lens.includeValueButtonTooltip', {
|
|
|
|
defaultMessage: 'Include value',
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<EuiButtonIcon
|
|
|
|
iconType="plusInCircle"
|
|
|
|
color="text"
|
|
|
|
aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', {
|
|
|
|
defaultMessage: `Include {value}`,
|
|
|
|
values: {
|
|
|
|
value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`,
|
|
|
|
},
|
|
|
|
})}
|
|
|
|
data-test-subj="lensDatatableFilterFor"
|
|
|
|
onClick={() => handleFilterClick(field, value, colIndex)}
|
|
|
|
/>
|
|
|
|
</EuiToolTip>
|
|
|
|
<EuiFlexItem grow={false}>
|
|
|
|
<EuiToolTip
|
|
|
|
position="bottom"
|
|
|
|
content={i18n.translate('xpack.lens.excludeValueButtonTooltip', {
|
|
|
|
defaultMessage: 'Exclude value',
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<EuiButtonIcon
|
|
|
|
iconType="minusInCircle"
|
|
|
|
color="text"
|
|
|
|
aria-label={i18n.translate(
|
|
|
|
'xpack.lens.excludeValueButtonAriaLabel',
|
|
|
|
{
|
|
|
|
defaultMessage: `Exclude {value}`,
|
|
|
|
values: {
|
|
|
|
value: `${
|
|
|
|
fieldName ? `${fieldName}: ` : ''
|
|
|
|
}${formattedValue}`,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)}
|
|
|
|
data-test-subj="lensDatatableFilterOut"
|
|
|
|
onClick={() => handleFilterClick(field, value, colIndex, true)}
|
|
|
|
/>
|
|
|
|
</EuiToolTip>
|
|
|
|
</EuiFlexItem>
|
|
|
|
</EuiFlexGroup>
|
|
|
|
</EuiFlexItem>
|
|
|
|
</EuiFlexGroup>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>;
|
|
|
|
},
|
2019-09-25 17:11:13 +02:00
|
|
|
};
|
|
|
|
})
|
|
|
|
.filter(({ field }) => !!field)}
|
2020-04-30 23:44:16 +02:00
|
|
|
items={firstTable ? firstTable.rows : []}
|
2019-09-25 17:11:13 +02:00
|
|
|
/>
|
|
|
|
</VisualizationContainer>
|
2019-09-17 20:57:53 +02:00
|
|
|
);
|
|
|
|
}
|