Skip to content

Commit

Permalink
[lens] Datatable visualization plugin (#39390)
Browse files Browse the repository at this point in the history
* [lens] Datatable visualization plugin

* Fix merge issues and add tests

* Update from review

* Fix file locations
  • Loading branch information
Wylie Conlon authored Jun 24, 2019
1 parent 85d4543 commit 02a3698
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 38 deletions.
7 changes: 7 additions & 0 deletions x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import React from 'react';
import { editorFrameSetup, editorFrameStop } from '../editor_frame_plugin';
import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin';
import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin';
import {
datatableVisualizationSetup,
datatableVisualizationStop,
} from '../datatable_visualization_plugin';
import { App } from './app';
import { EditorFrameInstance } from '../types';

Expand All @@ -20,11 +24,13 @@ export class AppPlugin {
// TODO: These plugins should not be called from the top level, but since this is the
// entry point to the app we have no choice until the new platform is ready
const indexPattern = indexPatternDatasourceSetup();
const datatableVisualization = datatableVisualizationSetup();
const xyVisualization = xyVisualizationSetup();
const editorFrame = editorFrameSetup();

editorFrame.registerDatasource('indexpattern', indexPattern);
editorFrame.registerVisualization('xy', xyVisualization);
editorFrame.registerVisualization('datatable', datatableVisualization);

this.instance = editorFrame.createInstance({});

Expand All @@ -39,6 +45,7 @@ export class AppPlugin {
// TODO this will be handled by the plugin platform itself
indexPatternDatasourceStop();
xyVisualizationStop();
datatableVisualizationStop();
editorFrameStop();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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 ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable } from '@elastic/eui';
import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types';
import { KibanaDatatable } from '../types';
import { RenderFunction } from '../interpreter_types';

export interface DatatableColumns {
columnIds: string[];
labels: string[];
}

interface Args {
columns: DatatableColumns;
}

export interface DatatableProps {
data: KibanaDatatable;
args: Args;
}

export interface DatatableRender {
type: 'render';
as: 'lens_datatable_renderer';
value: DatatableProps;
}

export const datatable: ExpressionFunction<
'lens_datatable',
KibanaDatatable,
Args,
DatatableRender
> = ({
name: 'lens_datatable',
type: 'render',
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: '',
},
},
context: {
types: ['kibana_datatable'],
},
fn(data: KibanaDatatable, args: Args) {
return {
type: 'render',
as: 'lens_datatable_renderer',
value: {
data,
args,
},
};
},
// TODO the typings currently don't support custom type args. As soon as they do, this can be removed
} as unknown) as ExpressionFunction<'lens_datatable', KibanaDatatable, Args, DatatableRender>;

type DatatableColumnsResult = DatatableColumns & { type: 'lens_datatable_columns' };

export const datatableColumns: ExpressionFunction<
'lens_datatable_columns',
null,
DatatableColumns,
DatatableColumnsResult
> = {
name: 'lens_datatable_columns',
aliases: [],
type: 'lens_datatable_columns',
help: '',
context: {
types: ['null'],
},
args: {
columnIds: {
types: ['string'],
multi: true,
help: '',
},
labels: {
types: ['string'],
multi: true,
help: '',
},
},
fn: function fn(_context: unknown, args: DatatableColumns) {
return {
type: 'lens_datatable_columns',
...args,
};
},
};

export interface DatatableProps {
data: KibanaDatatable;
args: Args;
}

export const datatableRenderer: RenderFunction<DatatableProps> = {
name: 'lens_datatable_renderer',
displayName: i18n.translate('xpack.lens.datatable.visualizationName', {
defaultMessage: 'Datatable',
}),
help: '',
validate: () => {},
reuseDomNode: true,
render: async (domNode: Element, config: DatatableProps, _handlers: unknown) => {
ReactDOM.render(<DatatableComponent {...config} />, domNode);
},
};

function DatatableComponent(props: DatatableProps) {
return (
<EuiBasicTable
columns={props.args.columns.columnIds
.map((id, index) => {
return {
field: props.args.columns.columnIds[index],
name: props.args.columns.labels[index],
};
})
.filter(({ field }) => !!field)}
items={props.data.rows}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/

export * from './plugin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 { Registry } from '@kbn/interpreter/target/common';
import { CoreSetup } from 'src/core/public';
import { datatableVisualization } from './visualization';

import {
renderersRegistry,
functionsRegistry,
// @ts-ignore untyped dependency
} from '../../../../../../src/legacy/core_plugins/interpreter/public/registries';
import { InterpreterSetup, RenderFunction } from '../interpreter_types';
import { datatable, datatableColumns, datatableRenderer } from './expression';

export interface DatatableVisualizationPluginSetupPlugins {
interpreter: InterpreterSetup;
}

class DatatableVisualizationPlugin {
constructor() {}

setup(_core: CoreSetup | null, { interpreter }: DatatableVisualizationPluginSetupPlugins) {
interpreter.functionsRegistry.register(() => datatableColumns);
interpreter.functionsRegistry.register(() => datatable);
interpreter.renderersRegistry.register(() => datatableRenderer as RenderFunction<unknown>);

return datatableVisualization;
}

stop() {}
}

const plugin = new DatatableVisualizationPlugin();

export const datatableVisualizationSetup = () =>
plugin.setup(null, {
interpreter: {
renderersRegistry,
functionsRegistry,
},
});
export const datatableVisualizationStop = () => plugin.stop();
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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 { createMockDatasource } from '../editor_frame_plugin/mocks';
import {
DatatableVisualizationState,
DatatableConfigPanel,
datatableVisualization,
} from './visualization';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Operation, DataType } from '../types';

describe('Datatable Visualization', () => {
describe('#initialize', () => {
it('should initialize from the empty state', () => {
const datasource = createMockDatasource();
datasource.publicAPIMock.generateColumnId.mockReturnValueOnce('id');
expect(datatableVisualization.initialize(datasource.publicAPIMock)).toEqual({
columns: [{ id: 'id', label: '' }],
});
});

it('should initialize from a persisted state', () => {
const datasource = createMockDatasource();
const expectedState: DatatableVisualizationState = {
columns: [{ id: 'saved', label: 'label' }],
};
expect(datasource.publicAPIMock.generateColumnId).not.toHaveBeenCalled();
expect(datatableVisualization.initialize(datasource.publicAPIMock, expectedState)).toEqual(
expectedState
);
});
});

describe('#getPersistableState', () => {
it('should persist the internal state', () => {
const expectedState: DatatableVisualizationState = {
columns: [{ id: 'saved', label: 'label' }],
};
expect(datatableVisualization.getPersistableState(expectedState)).toEqual(expectedState);
});
});

describe('DatatableConfigPanel', () => {
it('should update the column label', () => {
const setState = jest.fn();
const wrapper = mount(
<DatatableConfigPanel
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
datasource={createMockDatasource().publicAPIMock}
setState={setState}
state={{ columns: [{ id: 'saved', label: 'label' }] }}
/>
);

const labelEditor = wrapper.find('[data-test-subj="lnsDatatable-columnLabel"]').at(1);

act(() => {
labelEditor.simulate('change', { target: { value: 'New Label' } });
});

expect(setState).toHaveBeenCalledWith({
columns: [{ id: 'saved', label: 'New Label' }],
});
});

it('should allow all operations to be shown', () => {
const setState = jest.fn();
const datasource = createMockDatasource();

mount(
<DatatableConfigPanel
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
datasource={datasource.publicAPIMock}
setState={setState}
state={{ columns: [{ id: 'saved', label: 'label' }] }}
/>
);

expect(datasource.publicAPIMock.renderDimensionPanel).toHaveBeenCalled();

const filterOperations =
datasource.publicAPIMock.renderDimensionPanel.mock.calls[0][1].filterOperations;

const baseOperation: Operation = {
dataType: 'string',
isBucketed: true,
label: '',
id: '',
};
expect(filterOperations({ ...baseOperation })).toEqual(true);
expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(true);
expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(true);
expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true);
expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true);
expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual(
true
);
});

it('should remove a column', () => {
const setState = jest.fn();
const wrapper = mount(
<DatatableConfigPanel
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
datasource={createMockDatasource().publicAPIMock}
setState={setState}
state={{ columns: [{ id: 'saved', label: '' }, { id: 'second', label: '' }] }}
/>
);

act(() => {
wrapper
.find('[data-test-subj="lnsDatatable_dimensionPanelRemove_saved"]')
.first()
.simulate('click');
});

expect(setState).toHaveBeenCalledWith({
columns: [{ id: 'second', label: '' }],
});
});

it('should be able to add more columns', () => {
const setState = jest.fn();
const datasource = createMockDatasource();
const wrapper = mount(
<DatatableConfigPanel
dragDropContext={{ dragging: undefined, setDragging: () => {} }}
datasource={datasource.publicAPIMock}
setState={setState}
state={{ columns: [{ id: 'saved', label: 'label' }] }}
/>
);

datasource.publicAPIMock.generateColumnId.mockReturnValueOnce('newId');

act(() => {
wrapper
.find('[data-test-subj="lnsDatatable_dimensionPanel_add"]')
.first()
.simulate('click');
});

expect(setState).toHaveBeenCalledWith({
columns: [{ id: 'saved', label: 'label' }, { id: 'newId', label: '' }],
});
});
});
});
Loading

0 comments on commit 02a3698

Please sign in to comment.