diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
new file mode 100644
index 0000000000000..76063d230bdb6
--- /dev/null
+++ b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`datatable_expression DatatableComponent it renders the title and value 1`] = `
+
+
+
+`;
diff --git a/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss
index e36326d710f72..7d95d73143870 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss
+++ b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss
@@ -1,3 +1,13 @@
.lnsDataTable {
align-self: flex-start;
}
+
+.lnsDataTable__filter {
+ opacity: 0;
+ transition: opacity $euiAnimSpeedNormal ease-in-out;
+}
+
+.lnsDataTable__cell:hover .lnsDataTable__filter,
+.lnsDataTable__filter:focus-within {
+ opacity: 1;
+}
diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
new file mode 100644
index 0000000000000..6d5b1153ad1bc
--- /dev/null
+++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { datatable, DatatableComponent } from './expression';
+import { LensMultiTable } from '../types';
+import { DatatableProps } from './expression';
+import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
+import { IFieldFormat } from '../../../../../src/plugins/data/public';
+import { IAggType } from 'src/plugins/data/public';
+const executeTriggerActions = jest.fn();
+
+function sampleArgs() {
+ const data: LensMultiTable = {
+ type: 'lens_multitable',
+ tables: {
+ l1: {
+ type: 'kibana_datatable',
+ columns: [
+ { id: 'a', name: 'a', meta: { type: 'count' } },
+ { id: 'b', name: 'b', meta: { type: 'date_histogram', aggConfigParams: { field: 'b' } } },
+ { id: 'c', name: 'c', meta: { type: 'cardinality' } },
+ ],
+ rows: [{ a: 10110, b: 1588024800000, c: 3 }],
+ },
+ },
+ };
+
+ const args: DatatableProps['args'] = {
+ title: 'My fanci metric chart',
+ columns: {
+ columnIds: ['a', 'b', 'c'],
+ type: 'lens_datatable_columns',
+ },
+ };
+
+ return { data, args };
+}
+
+describe('datatable_expression', () => {
+ describe('datatable renders', () => {
+ test('it renders with the specified data and args', () => {
+ const { data, args } = sampleArgs();
+ const result = datatable.fn(data, args, createMockExecutionContext());
+
+ expect(result).toEqual({
+ type: 'render',
+ as: 'lens_datatable_renderer',
+ value: { data, args },
+ });
+ });
+ });
+
+ describe('DatatableComponent', () => {
+ test('it renders the title and value', () => {
+ const { data, args } = sampleArgs();
+
+ expect(
+ shallow(
+ x as IFieldFormat}
+ executeTriggerActions={executeTriggerActions}
+ getType={jest.fn()}
+ />
+ )
+ ).toMatchSnapshot();
+ });
+
+ test('it invokes executeTriggerActions with correct context on click on top value', () => {
+ const { args, data } = sampleArgs();
+
+ const wrapper = mountWithIntl(
+ x as IFieldFormat}
+ executeTriggerActions={executeTriggerActions}
+ getType={jest.fn(() => ({ type: 'buckets' } as IAggType))}
+ />
+ );
+
+ wrapper
+ .find('[data-test-subj="lensDatatableFilterOut"]')
+ .first()
+ .simulate('click');
+
+ expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', {
+ data: {
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table: data.tables.l1,
+ value: 10110,
+ },
+ ],
+ negate: true,
+ },
+ timeFieldName: undefined,
+ });
+ });
+
+ test('it invokes executeTriggerActions with correct context on click on timefield', () => {
+ const { args, data } = sampleArgs();
+
+ const wrapper = mountWithIntl(
+ x as IFieldFormat}
+ executeTriggerActions={executeTriggerActions}
+ getType={jest.fn(() => ({ type: 'buckets' } as IAggType))}
+ />
+ );
+
+ wrapper
+ .find('[data-test-subj="lensDatatableFilterFor"]')
+ .at(3)
+ .simulate('click');
+
+ expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', {
+ data: {
+ data: [
+ {
+ column: 1,
+ row: 0,
+ table: data.tables.l1,
+ value: 1588024800000,
+ },
+ ],
+ negate: false,
+ },
+ timeFieldName: 'b',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
index 772ee13168d02..71d29be1744bb 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
@@ -7,7 +7,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable } from '@elastic/eui';
+import { I18nProvider } from '@kbn/i18n/react';
+import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import { IAggType } from 'src/plugins/data/public';
import { FormatFactory, LensMultiTable } from '../types';
import {
ExpressionFunctionDefinition,
@@ -15,7 +17,10 @@ import {
IInterpreterRenderHandlers,
} from '../../../../../src/plugins/expressions/public';
import { VisualizationContainer } from '../visualization_container';
-
+import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public';
+import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
+import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
+import { getExecuteTriggerActions } from '../services';
export interface DatatableColumns {
columnIds: string[];
}
@@ -30,6 +35,12 @@ export interface DatatableProps {
args: Args;
}
+type DatatableRenderProps = DatatableProps & {
+ formatFactory: FormatFactory;
+ executeTriggerActions: UiActionsStart['executeTriggerActions'];
+ getType: (name: string) => IAggType;
+};
+
export interface DatatableRender {
type: 'render';
as: 'lens_datatable_renderer';
@@ -100,9 +111,10 @@ export const datatableColumns: ExpressionFunctionDefinition<
},
};
-export const getDatatableRenderer = (
- formatFactory: Promise
-): ExpressionRenderDefinition => ({
+export const getDatatableRenderer = (dependencies: {
+ formatFactory: Promise;
+ getType: Promise<(name: string) => IAggType>;
+}): ExpressionRenderDefinition => ({
name: 'lens_datatable_renderer',
displayName: i18n.translate('xpack.lens.datatable.visualizationName', {
defaultMessage: 'Datatable',
@@ -115,9 +127,18 @@ export const getDatatableRenderer = (
config: DatatableProps,
handlers: IInterpreterRenderHandlers
) => {
- const resolvedFormatFactory = await formatFactory;
+ const resolvedFormatFactory = await dependencies.formatFactory;
+ const executeTriggerActions = getExecuteTriggerActions();
+ const resolvedGetType = await dependencies.getType;
ReactDOM.render(
- ,
+
+
+ ,
domNode,
() => {
handlers.done();
@@ -127,7 +148,7 @@ export const getDatatableRenderer = (
},
});
-function DatatableComponent(props: DatatableProps & { formatFactory: FormatFactory }) {
+export function DatatableComponent(props: DatatableRenderProps) {
const [firstTable] = Object.values(props.data.tables);
const formatters: Record> = {};
@@ -135,6 +156,29 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto
formatters[column.id] = props.formatFactory(column.formatHint);
});
+ const handleFilterClick = (field: string, value: unknown, colIndex: number, negate = false) => {
+ const col = firstTable.columns[colIndex];
+ const isDateHistogram = col.meta?.type === 'date_histogram';
+ const timeFieldName = negate && isDateHistogram ? undefined : col?.meta?.aggConfigParams?.field;
+ const rowIndex = firstTable.rows.findIndex(row => row[field] === value);
+
+ const context: ValueClickTriggerContext = {
+ data: {
+ negate,
+ data: [
+ {
+ row: rowIndex,
+ column: colIndex,
+ value,
+ table: firstTable,
+ },
+ ],
+ },
+ timeFieldName,
+ };
+ props.executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context);
+ };
+
return (
{
const col = firstTable.columns.find(c => c.id === field);
+ const colIndex = firstTable.columns.findIndex(c => c.id === field);
+
+ const filterable = col?.meta?.type && props.getType(col.meta.type)?.type === 'buckets';
return {
field,
name: (col && col.name) || '',
+ render: (value: unknown) => {
+ const formattedValue = formatters[field]?.convert(value);
+ const fieldName = col?.meta?.aggConfigParams?.field;
+
+ if (filterable) {
+ return (
+
+ {formattedValue}
+
+
+
+ handleFilterClick(field, value, colIndex)}
+ />
+
+
+
+ handleFilterClick(field, value, colIndex, true)}
+ />
+
+
+
+
+
+ );
+ }
+ return {formattedValue};
+ },
};
})
.filter(({ field }) => !!field)}
- items={
- firstTable
- ? firstTable.rows.map(row => {
- const formattedRow: Record = {};
- Object.entries(formatters).forEach(([columnId, formatter]) => {
- formattedRow[columnId] = formatter.convert(row[columnId]);
- });
- return formattedRow;
- })
- : []
- }
+ items={firstTable ? firstTable.rows : []}
/>
);
diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts
index ff036aadfd4cf..44894d31da51d 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/index.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts
@@ -4,12 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { CoreSetup } from 'kibana/public';
+import { CoreSetup, CoreStart } from 'kibana/public';
import { datatableVisualization } from './visualization';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
import { datatable, datatableColumns, getDatatableRenderer } from './expression';
import { EditorFrameSetup, FormatFactory } from '../types';
+import { setExecuteTriggerActions } from '../services';
+import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+interface DatatableVisualizationPluginStartPlugins {
+ uiActions: UiActionsStart;
+ data: DataPublicPluginStart;
+}
export interface DatatableVisualizationPluginSetupPlugins {
expressions: ExpressionsSetup;
formatFactory: Promise;
@@ -20,12 +27,22 @@ export class DatatableVisualization {
constructor() {}
setup(
- _core: CoreSetup | null,
+ core: CoreSetup,
{ expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins
) {
expressions.registerFunction(() => datatableColumns);
expressions.registerFunction(() => datatable);
- expressions.registerRenderer(() => getDatatableRenderer(formatFactory));
+ expressions.registerRenderer(() =>
+ getDatatableRenderer({
+ formatFactory,
+ getType: core
+ .getStartServices()
+ .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get),
+ })
+ );
editorFrame.registerVisualization(datatableVisualization);
}
+ start(core: CoreStart, { uiActions }: DatatableVisualizationPluginStartPlugins) {
+ setExecuteTriggerActions(uiActions.executeTriggerActions);
+ }
}
diff --git a/x-pack/plugins/lens/public/plugin.tsx b/x-pack/plugins/lens/public/plugin.tsx
index 8d760eb0df501..fe0e81177e259 100644
--- a/x-pack/plugins/lens/public/plugin.tsx
+++ b/x-pack/plugins/lens/public/plugin.tsx
@@ -200,6 +200,7 @@ export class LensPlugin {
start(core: CoreStart, startDependencies: LensPluginStartDependencies) {
this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance;
this.xyVisualization.start(core, startDependencies);
+ this.datatableVisualization.start(core, startDependencies);
}
stop() {
diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts
index be7a2faae6711..082008bccddd1 100644
--- a/x-pack/test/functional/apps/lens/smokescreen.ts
+++ b/x-pack/test/functional/apps/lens/smokescreen.ts
@@ -26,12 +26,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const filterBar = getService('filterBar');
- async function assertExpectedMetric() {
+ async function assertExpectedMetric(metricCount: string = '19,986') {
await PageObjects.lens.assertExactText(
'[data-test-subj="lns_metric_title"]',
'Maximum of bytes'
);
- await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', '19,986');
+ await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', metricCount);
}
async function assertExpectedTable() {
@@ -40,8 +40,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
'Maximum of bytes'
);
await PageObjects.lens.assertExactText(
- '[data-test-subj="lnsDataTable"] tbody .euiTableCellContent__text',
- '19,986'
+ '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValue"]',
+ '19,985'
+ );
+ await PageObjects.lens.assertExactText(
+ '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValueFilterable"]',
+ 'IN'
);
}
@@ -86,7 +90,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
await assertExpectedMetric();
});
- it('click on the bar in XYChart adds proper filters/timerange', async () => {
+ it('click on the bar in XYChart adds proper filters/timerange in dashboard', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
await dashboardAddPanel.clickOpenAddPanel();
@@ -102,15 +106,22 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
expect(hasIpFilter).to.be(true);
});
- it('should allow seamless transition to and from table view', async () => {
+ it('should allow seamless transition to and from table view and add a filter', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens');
await PageObjects.lens.goToTimeRange();
await assertExpectedMetric();
await PageObjects.lens.switchToVisualization('lnsChartSwitchPopover_lnsDatatable');
+ await PageObjects.lens.configureDimension({
+ dimension: '[data-test-subj="lnsDatatable_column"] [data-test-subj="lns-empty-dimension"]',
+ operation: 'terms',
+ field: 'geo.dest',
+ });
+ await PageObjects.lens.save('Artistpreviouslyknownaslens');
+ await find.clickByCssSelector('[data-test-subj="lensDatatableFilterOut"]');
await assertExpectedTable();
await PageObjects.lens.switchToVisualization('lnsChartSwitchPopover_lnsMetric');
- await assertExpectedMetric();
+ await assertExpectedMetric('19,985');
});
it('should allow creation of lens visualizations', async () => {