From ed29fb2b092d8d51d680badb27f84144e612e6d8 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Wed, 1 Jul 2020 11:29:33 +0300 Subject: [PATCH 01/67] Use data grid for table vis --- .../vis_type_table/public/_table_vis.scss | 4 + .../vis_type_table/public/components/index.ts | 21 +++ .../public/components/table_vis_cell.tsx | 139 ++++++++++++++++++ .../public/components/table_visualization.tsx | 84 +++++++++++ src/plugins/vis_type_table/public/plugin.ts | 2 +- .../vis_type_table/public/table_vis_type.ts | 8 +- src/plugins/vis_type_table/public/types.ts | 2 + .../vis_type_table/public/vis_controller.ts | 111 -------------- src/plugins/visualizations/public/index.ts | 1 + .../public/vis_types/react_vis_controller.tsx | 9 ++ 10 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/index.ts create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_visualization.tsx delete mode 100644 src/plugins/vis_type_table/public/vis_controller.ts diff --git a/src/plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/_table_vis.scss index 8a36b9818c2a3..62498ec9ff304 100644 --- a/src/plugins/vis_type_table/public/_table_vis.scss +++ b/src/plugins/vis_type_table/public/_table_vis.scss @@ -21,3 +21,7 @@ display: none; } } + +.visEditor--table .visChart > div { + width: 100%; +} diff --git a/src/plugins/vis_type_table/public/components/index.ts b/src/plugins/vis_type_table/public/components/index.ts new file mode 100644 index 0000000000000..3108ff9988c25 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { TableOptions } from './table_vis_options_lazy'; +export { TableVisualization } from './table_visualization'; diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx new file mode 100644 index 0000000000000..1b7075b73892e --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { TableVisParams } from '../types'; +import { Table } from '../table_vis_response_handler'; +import { getFormatService } from '../services'; + +export const createTableVisCell = (table: Table, visParams: TableVisParams) => ({ + rowIndex, + columnId, +}: EuiDataGridCellValueElementProps) => { + const { buckets, metrics, splitColumn, splitRow } = visParams.dimensions; + + const formattedColumns = table.columns + .map(function (col, i) { + const isBucket = buckets.find(({ accessor }) => accessor === i); + const isSplitColumn = splitColumn?.find(({ accessor }) => accessor === i); + const isSplitRow = splitRow?.find(({ accessor }) => accessor === i); + const dimension = isBucket || isSplitColumn || metrics.find(({ accessor }) => accessor === i); + + const formatter = dimension ? getFormatService().deserialize(dimension.format) : undefined; + + const formattedColumn = { + id: col.id, + title: col.name, + formatter, + filterable: !!isBucket, + }; + + return formattedColumn; + + if (isSplitRow) { + $scope.splitRow = formattedColumn; + } + + if (!dimension) return; + + const last = i === table.columns.length - 1; + + if (last || !isBucket) { + formattedColumn.class = 'visualize-table-right'; + } + + const isDate = dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + let { totalFunc } = $scope; + if (typeof totalFunc === 'undefined' && showPercentage) { + totalFunc = 'sum'; + } + + if (allowsNumericalAggregations || isDate || totalFunc === 'count') { + const sum = (tableRows) => { + return _.reduce( + tableRows, + function (prev, curr) { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + curr[col.id]; + }, + 0 + ); + }; + + formattedColumn.sumTotal = sum(table.rows); + switch (totalFunc) { + case 'sum': { + if (!isDate) { + const total = formattedColumn.sumTotal; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case 'avg': { + if (!isDate) { + const total = sum(table.rows) / table.rows.length; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + } + break; + } + case 'min': { + const total = _.chain(table.rows).map(col.id).min().value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'max': { + const total = _.chain(table.rows).map(col.id).max().value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'count': { + const total = table.rows.length; + formattedColumn.formattedTotal = total; + formattedColumn.total = total; + break; + } + default: + break; + } + } + + return formattedColumn; + }) + .filter((column) => column); + + const rowValue = table.rows[rowIndex][columnId]; + const column = formattedColumns.find(({ id }) => id === columnId); + + // An AggConfigResult can "enrich" cell contents by applying a field formatter, + // which we want to do if possible. + const contentsIsDefined = rowValue !== null && rowValue !== undefined; + + const cellContent = contentsIsDefined ? column?.formatter?.convert(rowValue) : ''; + + return cellContent; +}; diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx new file mode 100644 index 0000000000000..93e61b428dd25 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { EuiDataGrid } from '@elastic/eui'; + +import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; +import { createTableVisCell } from './table_vis_cell'; +import { TableVisParams } from '../types'; +import { TableContext } from '../table_vis_response_handler'; + +export const TableVisualization = ({ + renderComplete, + visData: { direction, table, tables }, + visParams, +}: ReactVisComponentProps) => { + useEffect(() => { + renderComplete(); + }, [renderComplete]); + + return table ? ( + ({ + id: col.id, + display: col.name, + }))} + rowCount={table.rows.length} + columnVisibility={{ + visibleColumns: table.columns.map((col) => col.id), + setVisibleColumns: () => {}, + }} + renderCellValue={createTableVisCell(table, visParams)} + pagination={{ + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [50, 100, 200], + onChangePage: () => {}, + onChangeItemsPerPage: () => {}, + }} + /> + ) : null; + // ( + // visData.tables.map((table, key) => ( + // ({ + // id: col.id, + // display: col.name, + // }))} + // rowCount={table.rows.length} + // columnVisibility={{ + // visibleColumns: table.columns.map((col) => col.id), + // setVisibleColumns: () => {}, + // }} + // renderCellValue={createTableVisCell()} + // pagination={{ + // pageIndex: 0, + // pageSize: 10, + // pageSizeOptions: [50, 100, 200], + // onChangePage: () => {}, + // onChangeItemsPerPage: () => {}, + // }} + // /> + // )) + // ); +}; diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 28f823df79d91..140265485645d 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -52,7 +52,7 @@ export class TableVisPlugin implements Plugin, void> { { expressions, visualizations }: TablePluginSetupDependencies ) { expressions.registerFunction(createTableVisFn); - visualizations.createBaseVisualization( + visualizations.createReactVisualization( getTableVisTypeDefinition(core, this.initializerContext) ); } diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index c3bc72497007e..fa30330c6fa26 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -22,10 +22,7 @@ import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { Vis } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; -import { TableOptions } from './components/table_vis_options_lazy'; -import { getTableVisualizationControllerClass } from './vis_controller'; +import { TableOptions, TableVisualization } from './components'; export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { return { @@ -38,8 +35,8 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia description: i18n.translate('visTypeTable.tableVisDescription', { defaultMessage: 'Display values in a table', }), - visualization: getTableVisualizationControllerClass(core, context), visConfig: { + component: TableVisualization, defaults: { perPage: 10, showPartialRows: false, @@ -52,7 +49,6 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia totalFunc: 'sum', percentageCol: '', }, - template: tableVisTemplate, }, editorConfig: { optionsTemplate: TableOptions, diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 39023d1305cb6..e77b37132fe8f 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -30,6 +30,8 @@ export enum AggTypes { export interface Dimensions { buckets: SchemaConfig[]; metrics: SchemaConfig[]; + splitColumn?: SchemaConfig[]; + splitRow?: SchemaConfig[]; } export interface TableVisParams { diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts deleted file mode 100644 index a5086e0c9a2d8..0000000000000 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; -import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; - -import { VisParams, ExprVis } from '../../visualizations/public'; -import { getAngularModule } from './get_inner_angular'; -import { getKibanaLegacy } from './services'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; - -const innerAngularName = 'kibana/table_vis'; - -export function getTableVisualizationControllerClass( - core: CoreSetup, - context: PluginInitializerContext -) { - return class TableVisualizationController { - private tableVisModule: IModule | undefined; - private injector: auto.IInjectorService | undefined; - el: JQuery; - vis: ExprVis; - $rootScope: IRootScopeService | null = null; - $scope: (IScope & { [key: string]: any }) | undefined; - $compile: ICompileService | undefined; - - constructor(domeElement: Element, vis: ExprVis) { - this.el = $(domeElement); - this.vis = vis; - } - - getInjector() { - if (!this.injector) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); - this.injector = angular.bootstrap(mountpoint, [innerAngularName]); - this.el.append(mountpoint); - } - - return this.injector; - } - - async initLocalAngular() { - if (!this.tableVisModule) { - const [coreStart] = await core.getStartServices(); - this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); - initTableVisLegacyModule(this.tableVisModule); - } - } - - async render(esResponse: object, visParams: VisParams) { - getKibanaLegacy().loadFontAwesome(); - await this.initLocalAngular(); - - return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; - } - this.$scope.vis = this.vis; - this.$scope.visState = { params: visParams }; - this.$scope.esResponse = esResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); - updateScope(); - this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); - } - }); - } - - destroy() { - if (this.$rootScope) { - this.$rootScope.$destroy(); - this.$rootScope = null; - } - } - }; -} diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 0bbf862216ed5..149d8423c0586 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -33,6 +33,7 @@ export { TypesService } from './vis_types/types_service'; export { VISUALIZE_EMBEDDABLE_TYPE, VIS_EVENT_TO_TRIGGER } from './embeddable'; /** @public types */ +export { ReactVisComponentProps } from './vis_types/react_vis_controller'; export { VisualizationsSetup, VisualizationsStart }; export { VisTypeAlias, VisType } from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; diff --git a/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx b/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx index 643e6ffcb730b..0c8d75b424f38 100644 --- a/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx +++ b/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx @@ -19,9 +19,18 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { IUiSettingsClient } from 'kibana/public'; import { Vis, VisualizationController } from '../types'; import { getI18n, getUISettings } from '../services'; +export interface ReactVisComponentProps { + config: IUiSettingsClient; + vis: Vis; + visData: TData; + visParams: TParams; + renderComplete: () => void; +} + export class ReactVisController implements VisualizationController { private el: HTMLElement; private vis: Vis; From 9e17f4df00cb58c8220cb6b7c345fe14a44b7116 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 2 Jul 2020 13:29:44 +0300 Subject: [PATCH 02/67] Create basic table template --- .../public/components/table_vis_basic.tsx | 63 +++++++++++++++++++ .../public/components/table_vis_cell.tsx | 54 ++++++++++++++-- .../public/components/table_visualization.tsx | 29 ++------- src/plugins/vis_type_table/public/index.scss | 7 +++ .../public/table_vis_response_handler.ts | 44 ++++++------- .../public/vis_types/react_vis_controller.tsx | 9 +-- 6 files changed, 152 insertions(+), 54 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/table_vis_basic.tsx diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx new file mode 100644 index 0000000000000..7b6e5f8cf0688 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useMemo, memo } from 'react'; +import { EuiDataGrid } from '@elastic/eui'; + +import { ExprVis } from 'src/plugins/visualizations/public'; +import { createTableVisCell } from './table_vis_cell'; +import { Table } from '../table_vis_response_handler'; +import { TableVisParams } from '../types'; + +interface TableVisBasicProps { + table: Table; + vis: ExprVis; + visParams: TableVisParams; +} + +export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps) => { + const renderCellValue = useMemo(() => createTableVisCell(table, vis, visParams), [ + table, + vis, + visParams, + ]); + + return ( + ({ + id: col.id, + display: col.name, + }))} + rowCount={table.rows.length} + columnVisibility={{ + visibleColumns: table.columns.map((col) => col.id), + setVisibleColumns: () => {}, + }} + renderCellValue={renderCellValue} + pagination={{ + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [50, 100, 200], + onChangePage: () => {}, + onChangeItemsPerPage: () => {}, + }} + /> + ); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx index 1b7075b73892e..94cde5463c1d1 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -17,13 +17,22 @@ * under the License. */ -import React from 'react'; -import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { + EuiDataGridCellValueElementProps, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { ExprVis } from 'src/plugins/visualizations/public'; import { TableVisParams } from '../types'; import { Table } from '../table_vis_response_handler'; import { getFormatService } from '../services'; -export const createTableVisCell = (table: Table, visParams: TableVisParams) => ({ +export const createTableVisCell = (table: Table, vis: ExprVis, visParams: TableVisParams) => ({ + // @ts-expect-error + colIndex, rowIndex, columnId, }: EuiDataGridCellValueElementProps) => { @@ -135,5 +144,42 @@ export const createTableVisCell = (table: Table, visParams: TableVisParams) => ( const cellContent = contentsIsDefined ? column?.formatter?.convert(rowValue) : ''; - return cellContent; + const onFilterClick = useCallback( + (negate: boolean) => { + vis.API.events.filter({ + data: [ + { + table, + row: rowIndex, + column: colIndex, + value: rowValue, + }, + ], + negate, + }); + }, + [colIndex, rowIndex, rowValue] + ); + + const cell = ( + + {cellContent} + + onFilterClick(false)} + iconType="magnifyWithPlus" + aria-label="Next" + /> + + + onFilterClick(true)} + iconType="magnifyWithMinus" + aria-label="Next" + /> + + + ); + + return column?.filterable && contentsIsDefined ? cell : cellContent; }; diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index 93e61b428dd25..76385aba51646 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -17,16 +17,16 @@ * under the License. */ -import React, { useEffect } from 'react'; -import { EuiDataGrid } from '@elastic/eui'; +import React, { useEffect, useMemo } from 'react'; import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; -import { createTableVisCell } from './table_vis_cell'; import { TableVisParams } from '../types'; import { TableContext } from '../table_vis_response_handler'; +import { TableVisBasic } from './table_vis_basic'; export const TableVisualization = ({ renderComplete, + vis, visData: { direction, table, tables }, visParams, }: ReactVisComponentProps) => { @@ -34,28 +34,7 @@ export const TableVisualization = ({ renderComplete(); }, [renderComplete]); - return table ? ( - ({ - id: col.id, - display: col.name, - }))} - rowCount={table.rows.length} - columnVisibility={{ - visibleColumns: table.columns.map((col) => col.id), - setVisibleColumns: () => {}, - }} - renderCellValue={createTableVisCell(table, visParams)} - pagination={{ - pageIndex: 0, - pageSize: 10, - pageSizeOptions: [50, 100, 200], - onChangePage: () => {}, - onChangeItemsPerPage: () => {}, - }} - /> - ) : null; + return table ? : null; // ( // visData.tables.map((table, key) => ( // ; + table?: Table; + tables: TableGroup[]; direction?: 'row' | 'column'; } export interface TableGroup { - $parent: TableContext; table: Input; tables: Table[]; title: string; @@ -39,60 +39,62 @@ export interface TableGroup { } export interface Table { - $parent?: TableGroup; columns: Input['columns']; rows: Input['rows']; } -export function tableVisResponseHandler(table: Input, dimensions: any): TableContext { - const converted: TableContext = { - tables: [], - }; +export function tableVisResponseHandler(input: Input, dimensions: any): TableContext { + let table: Table | undefined; + let tables: TableGroup[] = []; + let direction: TableContext['direction']; const split = dimensions.splitColumn || dimensions.splitRow; if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; + tables = []; + direction = dimensions.splitRow ? 'row' : 'column'; const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); - const splitColumn = table.columns[splitColumnIndex]; + const splitColumn = input.columns[splitColumnIndex]; const splitMap = {}; let splitIndex = 0; - table.rows.forEach((row, rowIndex) => { + input.rows.forEach((row, rowIndex) => { const splitValue: any = row[splitColumn.id]; if (!splitMap.hasOwnProperty(splitValue as any)) { (splitMap as any)[splitValue] = splitIndex++; const tableGroup: Required = { - $parent: converted, title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, name: splitColumn.name, key: splitValue, column: splitColumnIndex, row: rowIndex, - table, + table: input, tables: [], }; tableGroup.tables.push({ - $parent: tableGroup, - columns: table.columns, + columns: input.columns, rows: [], }); - converted.tables.push(tableGroup); + tables.push(tableGroup); } const tableIndex = (splitMap as any)[splitValue]; - (converted.tables[tableIndex] as any).tables[0].rows.push(row); + (tables[tableIndex] as any).tables[0].rows.push(row); }); } else { - converted.tables.push({ - columns: table.columns, - rows: table.rows, - }); + table = { + columns: input.columns, + rows: input.rows, + }; } - return converted; + return { + direction, + table, + tables, + }; } diff --git a/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx b/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx index 0c8d75b424f38..a32b105ad0a99 100644 --- a/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx +++ b/src/plugins/visualizations/public/vis_types/react_vis_controller.tsx @@ -20,12 +20,13 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { IUiSettingsClient } from 'kibana/public'; -import { Vis, VisualizationController } from '../types'; +import { VisualizationController } from '../types'; import { getI18n, getUISettings } from '../services'; +import { ExprVis } from '../expressions/vis'; export interface ReactVisComponentProps { config: IUiSettingsClient; - vis: Vis; + vis: ExprVis; visData: TData; visParams: TParams; renderComplete: () => void; @@ -33,9 +34,9 @@ export interface ReactVisComponentProps { export class ReactVisController implements VisualizationController { private el: HTMLElement; - private vis: Vis; + private vis: ExprVis; - constructor(element: HTMLElement, vis: Vis) { + constructor(element: HTMLElement, vis: ExprVis) { this.el = element; this.vis = vis; } From 3a693efa74ce12d44aaf98f44db836938dd326e2 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 2 Jul 2020 14:20:41 +0300 Subject: [PATCH 03/67] Add table_vis_split component --- .../public/components/table_vis_split.tsx | 62 +++++++++++++++++++ .../public/components/table_visualization.tsx | 32 ++-------- .../public/table_vis_response_handler.ts | 2 +- 3 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/table_vis_split.tsx diff --git a/src/plugins/vis_type_table/public/components/table_vis_split.tsx b/src/plugins/vis_type_table/public/components/table_vis_split.tsx new file mode 100644 index 0000000000000..c3c4347514aee --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_split.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo } from 'react'; +import { EuiDataGrid } from '@elastic/eui'; + +import { ExprVis } from 'src/plugins/visualizations/public'; +import { createTableVisCell } from './table_vis_cell'; +import { TableGroup } from '../table_vis_response_handler'; +import { TableVisParams } from '../types'; + +interface TableVisSplitProps { + tables: TableGroup[]; + vis: ExprVis; + visParams: TableVisParams; +} + +export const TableVisSplit = memo(({ tables, vis, visParams }: TableVisSplitProps) => { + return ( + <> + {tables.map(({ table }, key) => ( + ({ + id: col.id, + display: col.name, + }))} + rowCount={table.rows.length} + columnVisibility={{ + visibleColumns: table.columns.map((col) => col.id), + setVisibleColumns: () => {}, + }} + renderCellValue={createTableVisCell(table, vis, visParams)} + pagination={{ + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [50, 100, 200], + onChangePage: () => {}, + onChangeItemsPerPage: () => {}, + }} + /> + ))} + + ); +}); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index 76385aba51646..b067c3f64dc13 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -23,6 +23,7 @@ import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; import { TableVisParams } from '../types'; import { TableContext } from '../table_vis_response_handler'; import { TableVisBasic } from './table_vis_basic'; +import { TableVisSplit } from './table_vis_split'; export const TableVisualization = ({ renderComplete, @@ -34,30 +35,9 @@ export const TableVisualization = ({ renderComplete(); }, [renderComplete]); - return table ? : null; - // ( - // visData.tables.map((table, key) => ( - // ({ - // id: col.id, - // display: col.name, - // }))} - // rowCount={table.rows.length} - // columnVisibility={{ - // visibleColumns: table.columns.map((col) => col.id), - // setVisibleColumns: () => {}, - // }} - // renderCellValue={createTableVisCell()} - // pagination={{ - // pageIndex: 0, - // pageSize: 10, - // pageSizeOptions: [50, 100, 200], - // onChangePage: () => {}, - // onChangeItemsPerPage: () => {}, - // }} - // /> - // )) - // ); + return table ? ( + + ) : ( + + ); }; diff --git a/src/plugins/vis_type_table/public/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.ts index 3a9d47f48d907..0def14e074486 100644 --- a/src/plugins/vis_type_table/public/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/table_vis_response_handler.ts @@ -83,7 +83,7 @@ export function tableVisResponseHandler(input: Input, dimensions: any): TableCon } const tableIndex = (splitMap as any)[splitValue]; - (tables[tableIndex] as any).tables[0].rows.push(row); + tables[tableIndex].tables[0].rows.push(row); }); } else { table = { From 34a13758d13e11cd79d0151a9f4141be1655faa4 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 2 Jul 2020 16:33:20 +0300 Subject: [PATCH 04/67] Apply cell filtering --- .../public/components/table_vis_basic.tsx | 6 +- .../public/components/table_vis_cell.scss | 13 ++ .../public/components/table_vis_cell.tsx | 162 ++++-------------- src/plugins/vis_type_table/public/index.scss | 1 - .../public/paginated_table/_index.scss | 1 - .../paginated_table/_table_cell_filter.scss | 30 ---- .../paginated_table/table_cell_filter.html | 23 --- .../vis_type_table/public/utils/index.ts | 20 +++ .../vis_type_table/public/utils/use/index.ts | 20 +++ .../public/utils/use/use_formatted_columns.ts | 130 ++++++++++++++ .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 12 files changed, 229 insertions(+), 185 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.scss delete mode 100644 src/plugins/vis_type_table/public/paginated_table/_index.scss delete mode 100644 src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss delete mode 100644 src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html create mode 100644 src/plugins/vis_type_table/public/utils/index.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/index.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index 7b6e5f8cf0688..42f833dc3326a 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -24,6 +24,7 @@ import { ExprVis } from 'src/plugins/visualizations/public'; import { createTableVisCell } from './table_vis_cell'; import { Table } from '../table_vis_response_handler'; import { TableVisParams } from '../types'; +import { useFormattedColumns } from '../utils'; interface TableVisBasicProps { table: Table; @@ -32,10 +33,11 @@ interface TableVisBasicProps { } export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps) => { - const renderCellValue = useMemo(() => createTableVisCell(table, vis, visParams), [ + const formattedColumns = useFormattedColumns(table, visParams); + const renderCellValue = useMemo(() => createTableVisCell(table, formattedColumns, vis), [ table, + formattedColumns, vis, - visParams, ]); return ( diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.scss b/src/plugins/vis_type_table/public/components/table_vis_cell.scss new file mode 100644 index 0000000000000..4a455e7578f6f --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.scss @@ -0,0 +1,13 @@ +.tbvChartCell__filterable { + .tbvChartCellFilter { + display: none; + margin: 0 $euiSizeXS; + } + + &:hover { + .tbvChartCellFilter { + display: flex; + } + } +} + diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx index 94cde5463c1d1..962b2c70016cc 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -17,126 +17,28 @@ * under the License. */ +import './table_vis_cell.scss'; import React, { useCallback } from 'react'; import { EuiDataGridCellValueElementProps, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, + EuiToolTip, } from '@elastic/eui'; import { ExprVis } from 'src/plugins/visualizations/public'; -import { TableVisParams } from '../types'; +import { i18n } from '@kbn/i18n'; import { Table } from '../table_vis_response_handler'; -import { getFormatService } from '../services'; -export const createTableVisCell = (table: Table, vis: ExprVis, visParams: TableVisParams) => ({ +export const createTableVisCell = (table: Table, formattedColumns: any[], vis: ExprVis) => ({ // @ts-expect-error colIndex, rowIndex, columnId, }: EuiDataGridCellValueElementProps) => { - const { buckets, metrics, splitColumn, splitRow } = visParams.dimensions; - - const formattedColumns = table.columns - .map(function (col, i) { - const isBucket = buckets.find(({ accessor }) => accessor === i); - const isSplitColumn = splitColumn?.find(({ accessor }) => accessor === i); - const isSplitRow = splitRow?.find(({ accessor }) => accessor === i); - const dimension = isBucket || isSplitColumn || metrics.find(({ accessor }) => accessor === i); - - const formatter = dimension ? getFormatService().deserialize(dimension.format) : undefined; - - const formattedColumn = { - id: col.id, - title: col.name, - formatter, - filterable: !!isBucket, - }; - - return formattedColumn; - - if (isSplitRow) { - $scope.splitRow = formattedColumn; - } - - if (!dimension) return; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = (tableRows) => { - return _.reduce( - tableRows, - function (prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows).map(col.id).min().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows).map(col.id).max().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter((column) => column); - const rowValue = table.rows[rowIndex][columnId]; - const column = formattedColumns.find(({ id }) => id === columnId); + const column = formattedColumns[colIndex]; // An AggConfigResult can "enrich" cell contents by applying a field formatter, // which we want to do if possible. @@ -161,25 +63,37 @@ export const createTableVisCell = (table: Table, vis: ExprVis, visParams: TableV [colIndex, rowIndex, rowValue] ); - const cell = ( - - {cellContent} - - onFilterClick(false)} - iconType="magnifyWithPlus" - aria-label="Next" - /> - - - onFilterClick(true)} - iconType="magnifyWithMinus" - aria-label="Next" - /> - - - ); - - return column?.filterable && contentsIsDefined ? cell : cellContent; + if (column?.filterable && contentsIsDefined) { + return ( + + {cellContent} + + + + onFilterClick(false)} + iconType="magnifyWithPlus" + /> + + + + + + onFilterClick(true)} iconType="magnifyWithMinus" /> + + + + ); + } + + return cellContent; }; diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/index.scss index 2005168c7c4e5..54fc6757d14c0 100644 --- a/src/plugins/vis_type_table/public/index.scss +++ b/src/plugins/vis_type_table/public/index.scss @@ -6,7 +6,6 @@ // tbvChart__legend-isLoading @import './agg_table/index'; -@import './paginated_table/index'; @import './table_vis'; .tbvChart { diff --git a/src/plugins/vis_type_table/public/paginated_table/_index.scss b/src/plugins/vis_type_table/public/paginated_table/_index.scss deleted file mode 100644 index 23d56c09b2818..0000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './_table_cell_filter'; diff --git a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss deleted file mode 100644 index 05d050362ce0b..0000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss +++ /dev/null @@ -1,30 +0,0 @@ -.kbnTableCellFilter__hover { - position: relative; - - /** - * 1. Center vertically regardless of row height. - */ - .kbnTableCellFilter { - position: absolute; - white-space: nowrap; - right: 0; - top: 50%; /* 1 */ - transform: translateY(-50%); /* 1 */ - display: none; - } - - &:hover { - .kbnTableCellFilter { - display: inline; - } - - .kbnTableCellFilter__hover-show { - visibility: visible; - } - } -} - -.kbnTableCellFilter__hover-show { - // so that the cell doesn't change size on hover - visibility: hidden; -} diff --git a/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html deleted file mode 100644 index 57ecb9b221611..0000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html +++ /dev/null @@ -1,23 +0,0 @@ - -
- - - - - -
- diff --git a/src/plugins/vis_type_table/public/utils/index.ts b/src/plugins/vis_type_table/public/utils/index.ts new file mode 100644 index 0000000000000..ec8bf60de6987 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './use'; diff --git a/src/plugins/vis_type_table/public/utils/use/index.ts b/src/plugins/vis_type_table/public/utils/use/index.ts new file mode 100644 index 0000000000000..c91e2890bd6b4 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './use_formatted_columns'; diff --git a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts new file mode 100644 index 0000000000000..0ce0cc352c84a --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useMemo } from 'react'; + +import { Table } from '../../table_vis_response_handler'; +import { TableVisParams } from '../../types'; +import { getFormatService } from '../../services'; + +export const useFormattedColumns = (table: Table, visParams: TableVisParams) => { + const formattedColumns = useMemo(() => { + const { buckets, metrics, splitColumn, splitRow } = visParams.dimensions; + + return table.columns + .map(function (col, i) { + const isBucket = buckets.find(({ accessor }) => accessor === i); + const isSplitColumn = splitColumn?.find(({ accessor }) => accessor === i); + const isSplitRow = splitRow?.find(({ accessor }) => accessor === i); + const dimension = + isBucket || isSplitColumn || metrics.find(({ accessor }) => accessor === i); + + const formatter = dimension ? getFormatService().deserialize(dimension.format) : undefined; + + const formattedColumn = { + id: col.id, + title: col.name, + formatter, + filterable: !!isBucket, + }; + + return formattedColumn; + + if (isSplitRow) { + $scope.splitRow = formattedColumn; + } + + if (!dimension) return; + + const last = i === table.columns.length - 1; + + if (last || !isBucket) { + formattedColumn.class = 'visualize-table-right'; + } + + const isDate = dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + let { totalFunc } = $scope; + if (typeof totalFunc === 'undefined' && showPercentage) { + totalFunc = 'sum'; + } + + if (allowsNumericalAggregations || isDate || totalFunc === 'count') { + const sum = (tableRows) => { + return _.reduce( + tableRows, + function (prev, curr) { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + curr[col.id]; + }, + 0 + ); + }; + + formattedColumn.sumTotal = sum(table.rows); + switch (totalFunc) { + case 'sum': { + if (!isDate) { + const total = formattedColumn.sumTotal; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case 'avg': { + if (!isDate) { + const total = sum(table.rows) / table.rows.length; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + } + break; + } + case 'min': { + const total = _.chain(table.rows).map(col.id).min().value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'max': { + const total = _.chain(table.rows).map(col.id).max().value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'count': { + const total = table.rows.length; + formattedColumn.formattedTotal = total; + formattedColumn.total = total; + break; + } + default: + break; + } + } + + return formattedColumn; + }) + .filter((column) => column); + }, [table, visParams.dimensions]); + + return formattedColumns; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0b466f351d7db..d3a2b2ae8c512 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3064,8 +3064,8 @@ "visTypeTable.aggTable.exportLabel": "エクスポート:", "visTypeTable.aggTable.formattedLabel": "フォーマット済み", "visTypeTable.aggTable.rawLabel": "生", - "visTypeTable.directives.tableCellFilter.filterForValueTooltip": "値でフィルタリング", - "visTypeTable.directives.tableCellFilter.filterOutValueTooltip": "値を除外", + "visTypeTable.tableCellFilter.filterForValueTooltip": "値でフィルタリング", + "visTypeTable.tableCellFilter.filterOutValueTooltip": "値を除外", "visTypeTable.function.help": "表ビジュアライゼーション", "visTypeTable.params.defaultPercentageCol": "非表示", "visTypeTable.params.PercentageColLabel": "パーセンテージ列", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 52c01d292cacc..a14469a4b678c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3067,8 +3067,8 @@ "visTypeTable.aggTable.exportLabel": "导出:", "visTypeTable.aggTable.formattedLabel": "格式化", "visTypeTable.aggTable.rawLabel": "原始", - "visTypeTable.directives.tableCellFilter.filterForValueTooltip": "筛留值", - "visTypeTable.directives.tableCellFilter.filterOutValueTooltip": "筛除值", + "visTypeTable.tableCellFilter.filterForValueTooltip": "筛留值", + "visTypeTable.tableCellFilter.filterOutValueTooltip": "筛除值", "visTypeTable.function.help": "表可视化", "visTypeTable.params.defaultPercentageCol": "不显示", "visTypeTable.params.PercentageColLabel": "百分比列", From de6dd8f1abe6cfc3e2e12f52377adfb89e94d0fb Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 2 Jul 2020 17:01:03 +0300 Subject: [PATCH 05/67] Add aria-label attributes --- .../public/components/table_vis_cell.tsx | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx index 962b2c70016cc..c5a1303e0a497 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -64,17 +64,27 @@ export const createTableVisCell = (table: Table, formattedColumns: any[], vis: E ); if (column?.filterable && contentsIsDefined) { + const filterForToolTipText = i18n.translate( + 'visTypeTable.tableCellFilter.filterForValueTooltip', + { + defaultMessage: 'Filter for value', + } + ); + const filterOutToolTipText = i18n.translate( + 'visTypeTable.tableCellFilter.filterOutValueTooltip', + { + defaultMessage: 'Filter out value', + } + ); + return ( {cellContent} - + onFilterClick(false)} iconType="magnifyWithPlus" @@ -83,12 +93,12 @@ export const createTableVisCell = (table: Table, formattedColumns: any[], vis: E - - onFilterClick(true)} iconType="magnifyWithMinus" /> + + onFilterClick(true)} + iconType="magnifyWithMinus" + /> From d2e8436a054bf6e422baa17631178b5ef5f3aaed Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 6 Jul 2020 13:59:30 +0300 Subject: [PATCH 06/67] Use field formatters for values --- .../public/components/table_vis_cell.tsx | 14 +- .../public/paginated_table/rows.js | 135 ------------------ 2 files changed, 10 insertions(+), 139 deletions(-) delete mode 100644 src/plugins/vis_type_table/public/paginated_table/rows.js diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx index c5a1303e0a497..f3e36c6cdd2b2 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -39,12 +39,18 @@ export const createTableVisCell = (table: Table, formattedColumns: any[], vis: E }: EuiDataGridCellValueElementProps) => { const rowValue = table.rows[rowIndex][columnId]; const column = formattedColumns[colIndex]; - - // An AggConfigResult can "enrich" cell contents by applying a field formatter, - // which we want to do if possible. const contentsIsDefined = rowValue !== null && rowValue !== undefined; - const cellContent = contentsIsDefined ? column?.formatter?.convert(rowValue) : ''; + const cellContent = ( +
+ ); const onFilterClick = useCallback( (negate: boolean) => { diff --git a/src/plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/paginated_table/rows.js deleted file mode 100644 index d2192a5843644..0000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import tableCellFilterHtml from './table_cell_filter.html'; - -export function KbnRows($compile) { - return { - restrict: 'A', - link: function ($scope, $el, attr) { - function addCell($tr, contents, column, row) { - function createCell() { - return $(document.createElement('td')); - } - - function createFilterableCell(value) { - const $template = $(tableCellFilterHtml); - $template.addClass('kbnTableCellFilter__hover'); - - const scope = $scope.$new(); - - scope.onFilterClick = (event, negate) => { - // Don't add filter if a link was clicked. - if ($(event.target).is('a')) { - return; - } - - $scope.filter({ - data: [ - { - table: $scope.table, - row: $scope.rows.findIndex((r) => r === row), - column: $scope.table.columns.findIndex((c) => c.id === column.id), - value, - }, - ], - negate, - }); - }; - - return $compile($template)(scope); - } - - let $cell; - let $cellContent; - - const contentsIsDefined = contents !== null && contents !== undefined; - - if (column.filterable && contentsIsDefined) { - $cell = createFilterableCell(contents); - $cellContent = $cell.find('[data-cell-content]'); - } else { - $cell = $cellContent = createCell(); - } - - // An AggConfigResult can "enrich" cell contents by applying a field formatter, - // which we want to do if possible. - contents = contentsIsDefined ? column.formatter.convert(contents, 'html') : ''; - - if (_.isObject(contents)) { - if (contents.attr) { - $cellContent.attr(contents.attr); - } - - if (contents.class) { - $cellContent.addClass(contents.class); - } - - if (contents.scope) { - $cellContent = $compile($cellContent.prepend(contents.markup))(contents.scope); - } else { - $cellContent.prepend(contents.markup); - } - - if (contents.attr) { - $cellContent.attr(contents.attr); - } - } else { - if (contents === '') { - $cellContent.prepend(' '); - } else { - $cellContent.prepend(contents); - } - } - - $tr.append($cell); - } - - $scope.$watchMulti([attr.kbnRows, attr.kbnRowsMin], function (vals) { - let rows = vals[0]; - const min = vals[1]; - - $el.empty(); - - if (!Array.isArray(rows)) rows = []; - - if (isFinite(min) && rows.length < min) { - // clone the rows so that we can add elements to it without upsetting the original - rows = _.clone(rows); - // crate the empty row which will be pushed into the row list over and over - const emptyRow = {}; - // push as many empty rows into the row array as needed - _.times(min - rows.length, function () { - rows.push(emptyRow); - }); - } - - rows.forEach(function (row) { - const $tr = $(document.createElement('tr')).appendTo($el); - $scope.columns.forEach((column) => { - const value = row[column.id]; - addCell($tr, value, column, row); - }); - }); - }); - }, - }; -} From 66654f8a83f8790f07a26df778bb919a7c8d1fe6 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 6 Jul 2020 16:15:15 +0300 Subject: [PATCH 07/67] Add no results component --- .../public/components/table_vis_basic.tsx | 5 ++- .../components/table_vis_no_results.tsx | 37 +++++++++++++++++++ .../public/components/table_visualization.tsx | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/table_vis_no_results.tsx diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index 42f833dc3326a..30c2bc5554711 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -25,6 +25,7 @@ import { createTableVisCell } from './table_vis_cell'; import { Table } from '../table_vis_response_handler'; import { TableVisParams } from '../types'; import { useFormattedColumns } from '../utils'; +import { TableVisNoResults } from './table_vis_no_results'; interface TableVisBasicProps { table: Table; @@ -40,7 +41,7 @@ export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps vis, ]); - return ( + return table.rows.length > 0 ? ( ({ @@ -61,5 +62,7 @@ export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps onChangeItemsPerPage: () => {}, }} /> + ) : ( + ); }); diff --git a/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx b/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx new file mode 100644 index 0000000000000..4dd560c604b20 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const TableVisNoResults = () => ( +
+ + +
+); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index b067c3f64dc13..33835338de9ab 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; import { TableVisParams } from '../types'; From 743fded8a281c16302359656d918c42f70039800 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 6 Jul 2020 16:42:18 +0300 Subject: [PATCH 08/67] Remove legacy dependencies --- package.json | 5 +- .../vis_type_table/public/_table_vis.scss | 27 -- .../public/components/table_vis_cell.scss | 1 - .../table_visualization.scss} | 3 - .../public/components/table_visualization.tsx | 13 +- .../public/get_inner_angular.ts | 102 ------- src/plugins/vis_type_table/public/index.ts | 1 - .../vis_type_table/public/table_vis.html | 29 -- .../public/table_vis_controller.js | 55 ---- .../public/table_vis_controller.test.ts | 265 ------------------ yarn.lock | 5 - 11 files changed, 11 insertions(+), 495 deletions(-) delete mode 100644 src/plugins/vis_type_table/public/_table_vis.scss rename src/plugins/vis_type_table/public/{index.scss => components/table_visualization.scss} (82%) delete mode 100644 src/plugins/vis_type_table/public/get_inner_angular.ts delete mode 100644 src/plugins/vis_type_table/public/table_vis.html delete mode 100644 src/plugins/vis_type_table/public/table_vis_controller.js delete mode 100644 src/plugins/vis_type_table/public/table_vis_controller.test.ts diff --git a/package.json b/package.json index 8e51f9207eaf1..1043c387dc107 100644 --- a/package.json +++ b/package.json @@ -139,9 +139,9 @@ "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/i18n": "1.0.0", - "@kbn/telemetry-tools": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", + "@kbn/telemetry-tools": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", @@ -151,7 +151,6 @@ "angular": "^1.7.9", "angular-aria": "^1.7.9", "angular-elastic": "^2.5.1", - "angular-recursion": "^1.0.5", "angular-route": "^1.7.9", "angular-sanitize": "^1.7.9", "angular-sortable-view": "^0.0.17", @@ -455,9 +454,9 @@ "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", "jest": "^25.5.4", - "jest-environment-jsdom-thirteen": "^1.0.1", "jest-circus": "^25.5.4", "jest-cli": "^25.5.4", + "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jimp": "^0.9.6", "json5": "^1.0.1", diff --git a/src/plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/_table_vis.scss deleted file mode 100644 index 62498ec9ff304..0000000000000 --- a/src/plugins/vis_type_table/public/_table_vis.scss +++ /dev/null @@ -1,27 +0,0 @@ -// SASSTODO: Update naming to BEM -// This chart is actively being re-written to React and EUI -// Putting off renaming to avoid conflicts -.table-vis { - display: flex; - flex-direction: column; - flex: 1 0 100%; - overflow: auto; -} - -.table-vis-container { - kbn-agg-table-group > .table > tbody > tr > td { - border-top: 0px; - } - - .pagination-other-pages { - justify-content: flex-end; - } - - .pagination-size { - display: none; - } -} - -.visEditor--table .visChart > div { - width: 100%; -} diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.scss b/src/plugins/vis_type_table/public/components/table_vis_cell.scss index 4a455e7578f6f..9ad78e257833b 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.scss +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.scss @@ -10,4 +10,3 @@ } } } - diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/components/table_visualization.scss similarity index 82% rename from src/plugins/vis_type_table/public/index.scss rename to src/plugins/vis_type_table/public/components/table_visualization.scss index 54fc6757d14c0..c467dee6d5aca 100644 --- a/src/plugins/vis_type_table/public/index.scss +++ b/src/plugins/vis_type_table/public/components/table_visualization.scss @@ -5,9 +5,6 @@ // tbvChart__legend--small // tbvChart__legend-isLoading -@import './agg_table/index'; -@import './table_vis'; - .tbvChart { display: flex; flex-direction: column; diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index 33835338de9ab..93139700ddcee 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -17,6 +17,7 @@ * under the License. */ +import './table_visualization.scss'; import React, { useEffect } from 'react'; import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; @@ -35,9 +36,13 @@ export const TableVisualization = ({ renderComplete(); }, [renderComplete]); - return table ? ( - - ) : ( - + return ( +
+ {table ? ( + + ) : ( + + )} +
); }; diff --git a/src/plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts deleted file mode 100644 index 4e4269a1f44f4..0000000000000 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// inner angular imports -// these are necessary to bootstrap the local angular. -// They can stay even after NP cutover -import angular from 'angular'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; -import 'angular-recursion'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; -import { - initAngularBootstrap, - PaginateDirectiveProvider, - PaginateControlsDirectiveProvider, - PrivateProvider, - watchMultiDecorator, - KbnAccessibleClickProvider, -} from '../../kibana_legacy/public'; - -initAngularBootstrap(); - -const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; - -export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { - const uiModule = getInnerAngular(name, core); - return uiModule; -} - -let initialized = false; - -export function getInnerAngular(name = 'kibana/table_vis', core: CoreStart) { - if (!initialized) { - createLocalPrivateModule(); - createLocalI18nModule(); - createLocalConfigModule(core.uiSettings); - createLocalPaginateModule(); - initialized = true; - } - return angular - .module(name, [ - ...thirdPartyAngularDependencies, - 'tableVisPaginate', - 'tableVisConfig', - 'tableVisPrivate', - 'tableVisI18n', - ]) - .config(watchMultiDecorator) - .directive('kbnAccessibleClick', KbnAccessibleClickProvider); -} - -function createLocalPrivateModule() { - angular.module('tableVisPrivate', []).provider('Private', PrivateProvider); -} - -function createLocalConfigModule(uiSettings: IUiSettingsClient) { - angular.module('tableVisConfig', []).provider('config', function () { - return { - $get: () => ({ - get: (value: string) => { - return uiSettings ? uiSettings.get(value) : undefined; - }, - // set method is used in agg_table mocha test - set: (key: string, value: string) => { - return uiSettings ? uiSettings.set(key, value) : undefined; - }, - }), - }; - }); -} - -function createLocalI18nModule() { - angular - .module('tableVisI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createLocalPaginateModule() { - angular - .module('tableVisPaginate', []) - .directive('paginate', PaginateDirectiveProvider) - .directive('paginateControls', PaginateControlsDirectiveProvider); -} diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts index 5621fdb094772..6493c967165db 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import './index.scss'; import { PluginInitializerContext } from 'kibana/public'; import { TableVisPlugin as Plugin } from './plugin'; diff --git a/src/plugins/vis_type_table/public/table_vis.html b/src/plugins/vis_type_table/public/table_vis.html deleted file mode 100644 index f721b670400d6..0000000000000 --- a/src/plugins/vis_type_table/public/table_vis.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
- - -
-
- -

-

-
-
- -
- - -
-
diff --git a/src/plugins/vis_type_table/public/table_vis_controller.js b/src/plugins/vis_type_table/public/table_vis_controller.js deleted file mode 100644 index 8a620df986090..0000000000000 --- a/src/plugins/vis_type_table/public/table_vis_controller.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { assign } from 'lodash'; - -export function TableVisController($scope) { - const uiStateSort = $scope.uiState ? $scope.uiState.get('vis.params.sort') : {}; - assign($scope.visParams.sort, uiStateSort); - - $scope.sort = $scope.visParams.sort; - $scope.$watchCollection('sort', function (newSort) { - $scope.uiState.set('vis.params.sort', newSort); - }); - - /** - * Recreate the entire table when: - * - the underlying data changes (esResponse) - * - one of the view options changes (vis.params) - */ - $scope.$watch('renderComplete', function () { - let tableGroups = ($scope.tableGroups = null); - let hasSomeRows = ($scope.hasSomeRows = null); - - if ($scope.esResponse) { - tableGroups = $scope.esResponse; - - hasSomeRows = tableGroups.tables.some(function haveRows(table) { - if (table.tables) return table.tables.some(haveRows); - return table.rows.length > 0; - }); - } - - $scope.hasSomeRows = hasSomeRows; - if (hasSomeRows) { - $scope.dimensions = $scope.visParams.dimensions; - $scope.tableGroups = tableGroups; - } - $scope.renderComplete(); - }); -} diff --git a/src/plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts deleted file mode 100644 index e7d7f6726b0cd..0000000000000 --- a/src/plugins/vis_type_table/public/table_vis_controller.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import $ from 'jquery'; - -// @ts-ignore -import StubIndexPattern from 'test_utils/stub_index_pattern'; -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { getTableVisTypeDefinition } from './table_vis_type'; -import { Vis } from '../../visualizations/public'; -// eslint-disable-next-line -import { stubFields } from '../../data/public/stubs'; -// eslint-disable-next-line -import { tableVisResponseHandler } from './table_vis_response_handler'; -import { coreMock } from '../../../core/public/mocks'; -import { IAggConfig, search } from '../../data/public'; -// TODO: remove linting disable -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { searchServiceMock } from '../../data/public/search/mocks'; - -const { createAggConfigs } = searchServiceMock.createStartContract().aggs; - -const { tabifyAggResponse } = search; - -jest.mock('../../kibana_legacy/public/angular/angular_config', () => ({ - configureAppAngularModule: () => {}, -})); - -interface TableVisScope extends IScope { - [key: string]: any; -} - -const oneRangeBucket = { - hits: { - total: 6039, - max_score: 0, - hits: [], - }, - aggregations: { - agg_2: { - buckets: { - '0.0-1000.0': { - from: 0, - from_as_string: '0.0', - to: 1000, - to_as_string: '1000.0', - doc_count: 606, - }, - '1000.0-2000.0': { - from: 1000, - from_as_string: '1000.0', - to: 2000, - to_as_string: '2000.0', - doc_count: 298, - }, - }, - }, - }, -}; - -describe('Table Vis - Controller', () => { - let $rootScope: IRootScopeService & { [key: string]: any }; - let $compile: ICompileService; - let $scope: TableVisScope; - let $el: JQuery; - let tableAggResponse: any; - let tabifiedResponse: any; - let stubIndexPattern: any; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule( - 'kibana/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('kibana/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - tableAggResponse = tableVisResponseHandler; - }) - ); - - beforeEach(() => { - stubIndexPattern = new StubIndexPattern( - 'logstash-*', - (cfg: any) => cfg, - 'time', - stubFields, - coreMock.createSetup() - ); - }); - const tableVisTypeDefinition = getTableVisTypeDefinition( - coreMock.createSetup(), - coreMock.createPluginInitializerContext() - ); - - function getRangeVis(params?: object) { - return ({ - type: tableVisTypeDefinition, - params: Object.assign({}, tableVisTypeDefinition.visConfig.defaults, params), - data: { - aggs: createAggConfigs(stubIndexPattern, [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 }, - ], - }, - }, - ]), - }, - } as unknown) as Vis; - } - - const dimensions = { - buckets: [ - { - accessor: 0, - }, - ], - metrics: [ - { - accessor: 1, - format: { id: 'range' }, - }, - ], - }; - - // basically a parameterized beforeEach - function initController(vis: Vis) { - vis.data.aggs!.aggs.forEach((agg: IAggConfig, i: number) => { - agg.id = 'agg_' + (i + 1); - }); - - tabifiedResponse = tabifyAggResponse(vis.data.aggs!, oneRangeBucket); - $rootScope.vis = vis; - $rootScope.visParams = vis.params; - $rootScope.uiState = { - get: jest.fn(), - set: jest.fn(), - }; - $rootScope.renderComplete = () => {}; - $rootScope.newScope = (scope: TableVisScope) => { - $scope = scope; - }; - - $el = $('
') - .attr('ng-controller', 'KbnTableVisController') - .attr('ng-init', 'newScope(this)'); - - $compile($el)($rootScope); - } - - // put a response into the controller - function attachEsResponseToScope(resp: object) { - $rootScope.esResponse = resp; - $rootScope.$apply(); - } - - // remove the response from the controller - function removeEsResponseFromScope() { - delete $rootScope.esResponse; - $rootScope.renderComplete = () => {}; - $rootScope.$apply(); - } - - test('exposes #tableGroups and #hasSomeRows when a response is attached to scope', async () => { - const vis: Vis = getRangeVis(); - initController(vis); - - expect(!$scope.tableGroups).toBeTruthy(); - expect(!$scope.hasSomeRows).toBeTruthy(); - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.hasSomeRows).toBeTruthy(); - expect($scope.tableGroups.tables).toBeDefined(); - expect($scope.tableGroups.tables.length).toBe(1); - expect($scope.tableGroups.tables[0].columns.length).toBe(2); - expect($scope.tableGroups.tables[0].rows.length).toBe(2); - }); - - test('clears #tableGroups and #hasSomeRows when the response is removed', async () => { - const vis = getRangeVis(); - initController(vis); - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - removeEsResponseFromScope(); - - expect(!$scope.hasSomeRows).toBeTruthy(); - expect(!$scope.tableGroups).toBeTruthy(); - }); - - test('sets the sort on the scope when it is passed as a vis param', async () => { - const sortObj = { - columnIndex: 1, - direction: 'asc', - }; - const vis = getRangeVis({ sort: sortObj }); - initController(vis); - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.sort.columnIndex).toEqual(sortObj.columnIndex); - expect($scope.sort.direction).toEqual(sortObj.direction); - }); - - test('sets #hasSomeRows properly if the table group is empty', async () => { - const vis = getRangeVis(); - initController(vis); - - tabifiedResponse.rows = []; - - attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.hasSomeRows).toBeFalsy(); - expect(!$scope.tableGroups).toBeTruthy(); - }); - - test('passes partialRows:true to tabify based on the vis params', () => { - const vis = getRangeVis({ showPartialRows: true }); - initController(vis); - - expect(vis.type.hierarchicalData(vis)).toEqual(true); - }); - - test('passes partialRows:false to tabify based on the vis params', () => { - const vis = getRangeVis({ showPartialRows: false }); - initController(vis); - - expect(vis.type.hierarchicalData(vis)).toEqual(false); - }); -}); diff --git a/yarn.lock b/yarn.lock index 7e44780389531..63fde2edcb728 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6872,11 +6872,6 @@ angular-mocks@^1.7.9: resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.7.9.tgz#0a3b7e28b9a493b4e3010ed2b0f69a68e9b4f79b" integrity sha512-LQRqqiV3sZ7NTHBnNmLT0bXtE5e81t97+hkJ56oU0k3dqKv1s6F+nBWRlOVzqHWPGFOiPS8ZJVdrS8DFzHyNIA== -angular-recursion@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/angular-recursion/-/angular-recursion-1.0.5.tgz#cd405428a0bf55faf52eaa7988c1fe69cd930543" - integrity sha1-zUBUKKC/Vfr1Lqp5iMH+ac2TBUM= - angular-resource@1.7.9: version "1.7.9" resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.7.9.tgz#fa53623fae2c60debe2410d692447dcb0ba02396" From ca802cdd5f3afab4b819d08cbd71e2e8b3d9ca12 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Tue, 7 Jul 2020 16:12:54 +0300 Subject: [PATCH 09/67] Add usePagination --- .../public/components/table_vis_basic.tsx | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index 30c2bc5554711..af01e652189fb 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useMemo, memo } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGrid } from '@elastic/eui'; import { ExprVis } from 'src/plugins/visualizations/public'; @@ -33,6 +33,38 @@ interface TableVisBasicProps { visParams: TableVisParams; } +const usePagination = (visParams: TableVisParams) => { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: visParams.perPage || 0, + pageSizeOptions: [visParams.perPage || 0, 50], + }); + const onChangeItemsPerPage = useCallback( + (pageSize: number) => setPagination((pag) => ({ ...pag, pageSize, pageIndex: 0 })), + [] + ); + const onChangePage = useCallback( + (pageIndex: number) => setPagination((pag) => ({ ...pag, pageIndex })), + [] + ); + + useEffect(() => { + setPagination({ + pageIndex: 0, + pageSize: visParams.perPage || 0, + pageSizeOptions: [visParams.perPage || 0, 50], + }); + }, [visParams.perPage]); + + return pagination.pageSize + ? { + ...pagination, + onChangeItemsPerPage, + onChangePage, + } + : undefined; +}; + export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps) => { const formattedColumns = useFormattedColumns(table, visParams); const renderCellValue = useMemo(() => createTableVisCell(table, formattedColumns, vis), [ @@ -41,6 +73,8 @@ export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps vis, ]); + const pagination = usePagination(visParams); + return table.rows.length > 0 ? ( {}, }} renderCellValue={renderCellValue} - pagination={{ - pageIndex: 0, - pageSize: 10, - pageSizeOptions: [50, 100, 200], - onChangePage: () => {}, - onChangeItemsPerPage: () => {}, - }} + pagination={pagination} /> ) : ( From af74780344e01d5df0786503765b9cc382de3838 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Wed, 8 Jul 2020 15:02:08 +0300 Subject: [PATCH 10/67] Create usePagination util --- .../public/components/table_vis_basic.tsx | 36 +---------- .../vis_type_table/public/utils/use/index.ts | 1 + .../public/utils/use/use_pagination.ts | 59 +++++++++++++++++++ 3 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 src/plugins/vis_type_table/public/utils/use/use_pagination.ts diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index af01e652189fb..d9fb31a9345eb 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -17,14 +17,14 @@ * under the License. */ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useMemo } from 'react'; import { EuiDataGrid } from '@elastic/eui'; import { ExprVis } from 'src/plugins/visualizations/public'; import { createTableVisCell } from './table_vis_cell'; import { Table } from '../table_vis_response_handler'; import { TableVisParams } from '../types'; -import { useFormattedColumns } from '../utils'; +import { useFormattedColumns, usePagination } from '../utils'; import { TableVisNoResults } from './table_vis_no_results'; interface TableVisBasicProps { @@ -33,38 +33,6 @@ interface TableVisBasicProps { visParams: TableVisParams; } -const usePagination = (visParams: TableVisParams) => { - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: visParams.perPage || 0, - pageSizeOptions: [visParams.perPage || 0, 50], - }); - const onChangeItemsPerPage = useCallback( - (pageSize: number) => setPagination((pag) => ({ ...pag, pageSize, pageIndex: 0 })), - [] - ); - const onChangePage = useCallback( - (pageIndex: number) => setPagination((pag) => ({ ...pag, pageIndex })), - [] - ); - - useEffect(() => { - setPagination({ - pageIndex: 0, - pageSize: visParams.perPage || 0, - pageSizeOptions: [visParams.perPage || 0, 50], - }); - }, [visParams.perPage]); - - return pagination.pageSize - ? { - ...pagination, - onChangeItemsPerPage, - onChangePage, - } - : undefined; -}; - export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps) => { const formattedColumns = useFormattedColumns(table, visParams); const renderCellValue = useMemo(() => createTableVisCell(table, formattedColumns, vis), [ diff --git a/src/plugins/vis_type_table/public/utils/use/index.ts b/src/plugins/vis_type_table/public/utils/use/index.ts index c91e2890bd6b4..ffc31e0e7ea0a 100644 --- a/src/plugins/vis_type_table/public/utils/use/index.ts +++ b/src/plugins/vis_type_table/public/utils/use/index.ts @@ -18,3 +18,4 @@ */ export * from './use_formatted_columns'; +export * from './use_pagination'; diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts new file mode 100644 index 0000000000000..b0812f260f69c --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { TableVisParams } from '../../types'; + +export const usePagination = (visParams: TableVisParams) => { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: visParams.perPage || 0, + pageSizeOptions: [visParams.perPage || 0, 50], + }); + const onChangeItemsPerPage = useCallback( + (pageSize: number) => setPagination((pag) => ({ ...pag, pageSize, pageIndex: 0 })), + [] + ); + const onChangePage = useCallback( + (pageIndex: number) => setPagination((pag) => ({ ...pag, pageIndex })), + [] + ); + + useEffect(() => { + setPagination({ + pageIndex: 0, + pageSize: visParams.perPage || 0, + pageSizeOptions: [visParams.perPage || 0, 50], + }); + }, [visParams.perPage]); + + const paginationMemoized = useMemo( + () => + pagination.pageSize + ? { + ...pagination, + onChangeItemsPerPage, + onChangePage, + } + : undefined, + [onChangeItemsPerPage, onChangePage, pagination] + ); + + return paginationMemoized; +}; From f6cdb869778d458b958f260e09eb6ff38ea4b7fe Mon Sep 17 00:00:00 2001 From: sulemanof Date: Wed, 8 Jul 2020 18:17:53 +0300 Subject: [PATCH 11/67] Use percentage column and total row --- .../public/agg_table/agg_table.js | 178 ------------------ .../public/components/table_vis_basic.tsx | 19 +- .../public/components/table_vis_cell.tsx | 13 +- src/plugins/vis_type_table/public/types.ts | 11 ++ .../public/utils/add_percentage_column.ts | 64 +++++++ .../public/utils/use/use_formatted_columns.ts | 101 +++++----- 6 files changed, 149 insertions(+), 237 deletions(-) create mode 100644 src/plugins/vis_type_table/public/utils/add_percentage_column.ts diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js index bd7626a493338..9fe5f2ab6621b 100644 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.js @@ -19,8 +19,6 @@ import _ from 'lodash'; import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; -import { i18n } from '@kbn/i18n'; export function KbnAggTable(config, RecursionHelper) { return { @@ -102,182 +100,6 @@ export function KbnAggTable(config, RecursionHelper) { }) .join(''); }; - - $scope.$watchMulti( - ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], - function () { - const { table, exportTitle, percentageCol } = $scope; - const showPercentage = percentageCol !== ''; - - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - $scope.splitRow = null; - return; - } - - self.csv.filename = (exportTitle || table.title || 'table') + '.csv'; - $scope.rows = table.rows; - $scope.formattedColumns = []; - - if (typeof $scope.dimensions === 'undefined') return; - - const { buckets, metrics, splitColumn, splitRow } = $scope.dimensions; - - $scope.formattedColumns = table.columns - .map(function (col, i) { - const isBucket = buckets.find((bucket) => bucket.accessor === i); - const isSplitColumn = splitColumn - ? splitColumn.find((splitColumn) => splitColumn.accessor === i) - : undefined; - const isSplitRow = splitRow - ? splitRow.find((splitRow) => splitRow.accessor === i) - : undefined; - const dimension = - isBucket || isSplitColumn || metrics.find((metric) => metric.accessor === i); - - const formatter = dimension - ? getFormatService().deserialize(dimension.format) - : undefined; - - const formattedColumn = { - id: col.id, - title: col.name, - formatter: formatter, - filterable: !!isBucket, - }; - - if (isSplitRow) { - $scope.splitRow = formattedColumn; - } - - if (!dimension) return; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = - dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = (tableRows) => { - return _.reduce( - tableRows, - function (prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows).map(col.id).min().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows).map(col.id).max().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter((column) => column); - - if (showPercentage) { - const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); - - // column to show percentage for was removed - if (insertAtIndex < 0) return; - - const { cols, rows } = addPercentageCol( - $scope.formattedColumns, - percentageCol, - table.rows, - insertAtIndex - ); - $scope.rows = rows; - $scope.formattedColumns = cols; - } - } - ); }, }; } - -/** - * @param {Object[]} columns - the formatted columns that will be displayed - * @param {String} title - the title of the column to add to - * @param {Object[]} rows - the row data for the columns - * @param {Number} insertAtIndex - the index to insert the percentage column at - * @returns {Object} - cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol(columns, title, rows, insertAtIndex) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - }); - const newRows = rows.map((row) => ({ - [newId]: row[id] / sumTotal, - ...row, - })); - - return { cols: newCols, rows: newRows }; -} - -function insert(arr, index, ...items) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, ...items); - return newArray; -} diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index d9fb31a9345eb..2167797cadbfe 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -24,7 +24,7 @@ import { ExprVis } from 'src/plugins/visualizations/public'; import { createTableVisCell } from './table_vis_cell'; import { Table } from '../table_vis_response_handler'; import { TableVisParams } from '../types'; -import { useFormattedColumns, usePagination } from '../utils'; +import { useFormattedColumnsAndRows, usePagination } from '../utils'; import { TableVisNoResults } from './table_vis_no_results'; interface TableVisBasicProps { @@ -34,25 +34,26 @@ interface TableVisBasicProps { } export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps) => { - const formattedColumns = useFormattedColumns(table, visParams); - const renderCellValue = useMemo(() => createTableVisCell(table, formattedColumns, vis), [ + const { columns, rows } = useFormattedColumnsAndRows(table, visParams); + const renderCellValue = useMemo(() => createTableVisCell(table, columns, rows, vis), [ table, - formattedColumns, + columns, + rows, vis, ]); const pagination = usePagination(visParams); - return table.rows.length > 0 ? ( + return rows.length > 0 ? ( ({ + columns={columns.map((col) => ({ id: col.id, - display: col.name, + display: col.title, }))} - rowCount={table.rows.length} + rowCount={rows.length} columnVisibility={{ - visibleColumns: table.columns.map((col) => col.id), + visibleColumns: columns.map((col) => col.id), setVisibleColumns: () => {}, }} renderCellValue={renderCellValue} diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx index f3e36c6cdd2b2..85f39c6db50dd 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -30,16 +30,23 @@ import { import { ExprVis } from 'src/plugins/visualizations/public'; import { i18n } from '@kbn/i18n'; import { Table } from '../table_vis_response_handler'; +import { FormattedColumn } from '../types'; -export const createTableVisCell = (table: Table, formattedColumns: any[], vis: ExprVis) => ({ +export const createTableVisCell = ( + table: Table, + formattedColumns: FormattedColumn[], + rows: Table['rows'], + vis: ExprVis +) => ({ // @ts-expect-error colIndex, rowIndex, columnId, }: EuiDataGridCellValueElementProps) => { - const rowValue = table.rows[rowIndex][columnId]; + const rowValue = rows[rowIndex][columnId]; const column = formattedColumns[colIndex]; const contentsIsDefined = rowValue !== null && rowValue !== undefined; + const content = column?.formatter?.convert(rowValue, 'html') || (rowValue as string) || ''; const cellContent = (
); diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index e77b37132fe8f..e36d79b157c3f 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SchemaConfig } from '../../visualizations/public'; export enum AggTypes { @@ -48,3 +49,13 @@ export interface TableVisParams { percentageCol: string; dimensions: Dimensions; } + +export interface FormattedColumn { + id: string; + title: string; + formatter?: ReturnType; + formattedTotal?: string | number; + filterable: boolean; + sumTotal?: number; + total?: number; +} diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_column.ts b/src/plugins/vis_type_table/public/utils/add_percentage_column.ts new file mode 100644 index 0000000000000..9ac782c1527b2 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_column.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { KibanaDatatableRow } from 'src/plugins/expressions'; +import { getFormatService } from '../services'; +import { FormattedColumn } from '../types'; +import { Table } from '../table_vis_response_handler'; + +function insertColumn(arr: FormattedColumn[], index: number, col: FormattedColumn) { + const newArray = [...arr]; + newArray.splice(index + 1, 0, col); + return newArray; +} + +/** + * @param columns - the formatted columns that will be displayed + * @param title - the title of the column to add to + * @param rows - the row data for the columns + * @param insertAtIndex - the index to insert the percentage column at + * @returns cols and rows for the table to render now included percentage column(s) + */ +export function addPercentageColumn( + columns: FormattedColumn[], + title: string, + rows: Table['rows'], + insertAtIndex: number +) { + const { id, sumTotal } = columns[insertAtIndex]; + const newId = `${id}-percents`; + const formatter = getFormatService().deserialize({ id: 'percent' }); + const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { + defaultMessage: '{title} percentages', + values: { title }, + }); + const newCols = insertColumn(columns, insertAtIndex, { + title: i18nTitle, + id: newId, + formatter, + filterable: false, + }); + const newRows = rows.map((row) => ({ + [newId]: (row[id] as number) / (sumTotal as number), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} diff --git a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts index 0ce0cc352c84a..a702a6c25794e 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts @@ -18,17 +18,22 @@ */ import { useMemo } from 'react'; +import { chain, findIndex } from 'lodash'; import { Table } from '../../table_vis_response_handler'; -import { TableVisParams } from '../../types'; +import { FormattedColumn, TableVisParams, AggTypes } from '../../types'; import { getFormatService } from '../../services'; +import { addPercentageColumn } from '../add_percentage_column'; -export const useFormattedColumns = (table: Table, visParams: TableVisParams) => { - const formattedColumns = useMemo(() => { +export const useFormattedColumnsAndRows = (table: Table, visParams: TableVisParams) => { + const { formattedColumns: columns, formattedRows: rows } = useMemo(() => { const { buckets, metrics, splitColumn, splitRow } = visParams.dimensions; + // todo: use for split table by row/column + let splitRowGlobal: FormattedColumn; + let formattedRows = table.rows; - return table.columns - .map(function (col, i) { + let formattedColumns = table.columns + .map((col, i) => { const isBucket = buckets.find(({ accessor }) => accessor === i); const isSplitColumn = splitColumn?.find(({ accessor }) => accessor === i); const isSplitRow = splitRow?.find(({ accessor }) => accessor === i); @@ -37,76 +42,59 @@ export const useFormattedColumns = (table: Table, visParams: TableVisParams) => const formatter = dimension ? getFormatService().deserialize(dimension.format) : undefined; - const formattedColumn = { + const formattedColumn: FormattedColumn = { id: col.id, title: col.name, formatter, filterable: !!isBucket, }; - return formattedColumn; - if (isSplitRow) { - $scope.splitRow = formattedColumn; + splitRowGlobal = formattedColumn; } - if (!dimension) return; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } + if (!dimension) return undefined; const isDate = dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + // @ts-expect-error + const allowsNumericalAggregations: boolean = formatter?.allowsNumericalAggregations; - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } + if (allowsNumericalAggregations || isDate || visParams.totalFunc === AggTypes.COUNT) { + const sumOfColumnValues = table.rows.reduce((prev, curr) => { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + (curr[col.id] as number); + }, 0); - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = (tableRows) => { - return _.reduce( - tableRows, - function (prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { + formattedColumn.sumTotal = sumOfColumnValues; + + switch (visParams.totalFunc) { case 'sum': { if (!isDate) { const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.formattedTotal = formatter?.convert(total); formattedColumn.total = formattedColumn.sumTotal; } break; } case 'avg': { if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); + const total = sumOfColumnValues / table.rows.length; + formattedColumn.formattedTotal = formatter?.convert(total); formattedColumn.total = total; } break; } case 'min': { - const total = _.chain(table.rows).map(col.id).min().value(); - formattedColumn.formattedTotal = formatter.convert(total); + const total = chain(table.rows).map(col.id).min().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); formattedColumn.total = total; break; } case 'max': { - const total = _.chain(table.rows).map(col.id).max().value(); - formattedColumn.formattedTotal = formatter.convert(total); + const total = chain(table.rows).map(col.id).max().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); formattedColumn.total = total; break; } @@ -123,8 +111,27 @@ export const useFormattedColumns = (table: Table, visParams: TableVisParams) => return formattedColumn; }) - .filter((column) => column); - }, [table, visParams.dimensions]); + .filter((column): column is FormattedColumn => !!column); + + if (visParams.percentageCol) { + const insertAtIndex = findIndex(formattedColumns, { title: visParams.percentageCol }); + + // column to show percentage for was removed + if (insertAtIndex < 0) return { formattedColumns, formattedRows }; + + const { cols, rows: rowsWithPercentage } = addPercentageColumn( + formattedColumns, + visParams.percentageCol, + table.rows, + insertAtIndex + ); + + formattedRows = rowsWithPercentage; + formattedColumns = cols; + } + + return { formattedColumns, formattedRows }; + }, [table, visParams.dimensions, visParams.percentageCol, visParams.totalFunc]); - return formattedColumns; + return { columns, rows }; }; From c68b60e551d288836a0b660c0aa71254bee3a749 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 9 Jul 2020 18:17:51 +0300 Subject: [PATCH 12/67] Use csv export button --- .../public/agg_table/_agg_table.scss | 38 ------- .../public/agg_table/_index.scss | 1 - .../public/agg_table/agg_table.js | 105 ------------------ .../vis_type_table/public/components/index.ts | 2 +- .../public/components/table_vis_basic.tsx | 8 ++ .../public/components/table_vis_controls.tsx | 100 +++++++++++++++++ .../public/components/table_visualization.tsx | 20 ++-- .../public/table_vis_legacy_module.ts | 41 ------- .../vis_type_table/public/table_vis_type.ts | 4 +- .../public/utils/export_as_csv.ts | 93 ++++++++++++++++ .../vis_type_table/public/utils/index.ts | 1 + 11 files changed, 217 insertions(+), 196 deletions(-) delete mode 100644 src/plugins/vis_type_table/public/agg_table/_agg_table.scss delete mode 100644 src/plugins/vis_type_table/public/agg_table/_index.scss delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table.js create mode 100644 src/plugins/vis_type_table/public/components/table_vis_controls.tsx delete mode 100644 src/plugins/vis_type_table/public/table_vis_legacy_module.ts create mode 100644 src/plugins/vis_type_table/public/utils/export_as_csv.ts diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss deleted file mode 100644 index 0fffb21eab0fb..0000000000000 --- a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss +++ /dev/null @@ -1,38 +0,0 @@ -kbn-agg-table, -kbn-agg-table-group { - display: block; -} - -.kbnAggTable { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.kbnAggTable__paginated { - flex: 1 1 auto; - overflow: auto; - - th { - text-align: left; - font-weight: $euiFontWeightBold; - } - - tr:hover td, - .kbnTableCellFilter { - background-color: $euiColorLightestShade; - } -} - -.kbnAggTable__controls { - flex: 0 0 auto; - display: flex; - align-items: center; - margin: $euiSizeS $euiSizeXS; - - > paginate-controls { - flex: 1 0 auto; - margin: 0; - padding: 0; - } -} diff --git a/src/plugins/vis_type_table/public/agg_table/_index.scss b/src/plugins/vis_type_table/public/agg_table/_index.scss deleted file mode 100644 index 340e08a76f1bd..0000000000000 --- a/src/plugins/vis_type_table/public/agg_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './agg_table'; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js deleted file mode 100644 index 9fe5f2ab6621b..0000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import _ from 'lodash'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; -import aggTableTemplate from './agg_table.html'; - -export function KbnAggTable(config, RecursionHelper) { - return { - restrict: 'E', - template: aggTableTemplate, - scope: { - table: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - controllerAs: 'aggTable', - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el); - }, - controller: function ($scope) { - const self = this; - - self._saveAs = require('@elastic/filesaver').saveAs; - self.csv = { - separator: config.get(CSV_SEPARATOR_SETTING), - quoteValues: config.get(CSV_QUOTE_VALUES_SETTING), - }; - - self.exportAsCsv = function (formatted) { - const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); - self._saveAs(csv, self.csv.filename); - }; - - self.toCsv = function (formatted) { - const rows = formatted ? $scope.rows : $scope.table.rows; - const columns = formatted ? [...$scope.formattedColumns] : [...$scope.table.columns]; - - if ($scope.splitRow && formatted) { - columns.unshift($scope.splitRow); - } - - const nonAlphaNumRE = /[^a-zA-Z0-9]/; - const allDoubleQuoteRE = /"/g; - - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; - } - - let csvRows = []; - for (const row of rows) { - const rowArray = []; - for (const col of columns) { - const value = row[col.id]; - const formattedValue = - formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value); - rowArray.push(formattedValue); - } - csvRows = [...csvRows, rowArray]; - } - - // add the columns to the rows - csvRows.unshift( - columns.map(function (col) { - return escape(formatted ? col.title : col.name); - }) - ); - - return csvRows - .map(function (row) { - return row.join(self.csv.separator) + '\r\n'; - }) - .join(''); - }; - }, - }; -} diff --git a/src/plugins/vis_type_table/public/components/index.ts b/src/plugins/vis_type_table/public/components/index.ts index 3108ff9988c25..b990e9c5e40b6 100644 --- a/src/plugins/vis_type_table/public/components/index.ts +++ b/src/plugins/vis_type_table/public/components/index.ts @@ -18,4 +18,4 @@ */ export { TableOptions } from './table_vis_options_lazy'; -export { TableVisualization } from './table_visualization'; +export { createTableVisualizationComponent } from './table_visualization'; diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index 2167797cadbfe..54a2aa3c0695e 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -26,6 +26,7 @@ import { Table } from '../table_vis_response_handler'; import { TableVisParams } from '../types'; import { useFormattedColumnsAndRows, usePagination } from '../utils'; import { TableVisNoResults } from './table_vis_no_results'; +import { TableVisControls } from './table_vis_controls'; interface TableVisBasicProps { table: Table; @@ -51,11 +52,18 @@ export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps id: col.id, display: col.title, }))} + gridStyle={{ + border: 'horizontal', + header: 'underline', + }} rowCount={rows.length} columnVisibility={{ visibleColumns: columns.map((col) => col.id), setVisibleColumns: () => {}, }} + toolbarVisibility={{ + additionalControls: , + }} renderCellValue={renderCellValue} pagination={pagination} /> diff --git a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx new file mode 100644 index 0000000000000..0ec42926d830b --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo, useState, useCallback } from 'react'; +import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { KibanaDatatableRow } from 'src/plugins/expressions'; +import { CoreSetup } from 'kibana/public'; +import { useKibana } from '../../../kibana_react/public'; +import { FormattedColumn } from '../types'; +import { Table } from '../table_vis_response_handler'; +import { exportAsCsv } from '../utils'; + +interface TableVisControlsProps { + filename?: string; + cols: FormattedColumn[]; + rows: KibanaDatatableRow[]; + table: Table; +} + +export const TableVisControls = memo((props: TableVisControlsProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen((state) => !state), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + const { + services: { uiSettings }, + } = useKibana(); + + const onClickRawExport = useCallback( + () => + exportAsCsv(false, { + ...props, + uiSettings, + }), + [props, uiSettings] + ); + const onClickFormattedExport = useCallback( + () => + exportAsCsv(true, { + ...props, + uiSettings, + }), + [props, uiSettings] + ); + + const button = ( + + + + ); + + const items = [ + + + , + + + , + ]; + + return ( + + + + ); +}); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index 93139700ddcee..f595a5ad8fe9f 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -20,13 +20,15 @@ import './table_visualization.scss'; import React, { useEffect } from 'react'; +import { CoreSetup } from 'kibana/public'; import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; +import { KibanaContextProvider } from '../../../kibana_react/public'; import { TableVisParams } from '../types'; import { TableContext } from '../table_vis_response_handler'; import { TableVisBasic } from './table_vis_basic'; import { TableVisSplit } from './table_vis_split'; -export const TableVisualization = ({ +export const createTableVisualizationComponent = (core: CoreSetup) => ({ renderComplete, vis, visData: { direction, table, tables }, @@ -37,12 +39,14 @@ export const TableVisualization = ({ }, [renderComplete]); return ( -
- {table ? ( - - ) : ( - - )} -
+ +
+ {table ? ( + + ) : ( + + )} +
+
); }; diff --git a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/table_vis_legacy_module.ts deleted file mode 100644 index 57d8b7c448b4c..0000000000000 --- a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IModule } from 'angular'; - -// @ts-ignore -import { TableVisController } from './table_vis_controller.js'; -// @ts-ignore -import { KbnAggTable } from './agg_table/agg_table'; -// @ts-ignore -import { KbnAggTableGroup } from './agg_table/agg_table_group'; -// @ts-ignore -import { KbnRows } from './paginated_table/rows'; -// @ts-ignore -import { PaginatedTable } from './paginated_table/paginated_table'; - -/** @internal */ -export const initTableVisLegacyModule = (angularIns: IModule): void => { - angularIns - .controller('KbnTableVisController', TableVisController) - .directive('kbnAggTable', KbnAggTable) - .directive('kbnAggTableGroup', KbnAggTableGroup) - .directive('kbnRows', KbnRows) - .directive('paginatedTable', PaginatedTable); -}; diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index cc90ae64232db..64a7bc71b903c 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -22,7 +22,7 @@ import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { Vis } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; -import { TableOptions, TableVisualization } from './components'; +import { TableOptions, createTableVisualizationComponent } from './components'; import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { @@ -40,7 +40,7 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia return [VIS_EVENT_TO_TRIGGER.filter]; }, visConfig: { - component: TableVisualization, + component: createTableVisualizationComponent(core), defaults: { perPage: 10, showPartialRows: false, diff --git a/src/plugins/vis_type_table/public/utils/export_as_csv.ts b/src/plugins/vis_type_table/public/utils/export_as_csv.ts new file mode 100644 index 0000000000000..d018ef0827566 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/export_as_csv.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isObject } from 'lodash'; +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; + +import { CoreSetup } from 'kibana/public'; +import { KibanaDatatableRow, KibanaDatatableColumn } from 'src/plugins/expressions'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; +import { FormattedColumn } from '../types'; +import { Table } from '../table_vis_response_handler'; + +const nonAlphaNumRE = /[^a-zA-Z0-9]/; +const allDoubleQuoteRE = /"/g; + +interface ToCsvData { + filename?: string; + cols: FormattedColumn[]; + rows: KibanaDatatableRow[]; + table: Table; + splitRow?: FormattedColumn; + uiSettings: CoreSetup['uiSettings']; +} + +const toCsv = (formatted: boolean, { cols, rows, table, splitRow, uiSettings }: ToCsvData) => { + const separator = uiSettings.get(CSV_SEPARATOR_SETTING); + const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING); + const rowsData = formatted ? rows : table.rows; + const columns = formatted ? cols : table.columns; + + if (splitRow && formatted) { + (columns as FormattedColumn[]).unshift(splitRow); + } + + function escape(val: unknown) { + if (!formatted && isObject(val)) val = val.valueOf(); + val = String(val); + if (quoteValues && nonAlphaNumRE.test(val as string)) { + val = '"' + (val as string).replace(allDoubleQuoteRE, '""') + '"'; + } + return val as string; + } + + let csvRows: string[][] = []; + + for (const row of rowsData) { + const rowArray: string[] = []; + for (const col of columns) { + const value = row[col.id]; + const formattedValue = + formatted && (col as FormattedColumn).formatter + ? escape((col as FormattedColumn).formatter?.convert(value)) + : escape(value); + rowArray.push(formattedValue); + } + csvRows = [...csvRows, rowArray]; + } + + // add the columns to the rows + csvRows.unshift( + (columns as Array).map((col) => + escape(formatted ? (col as FormattedColumn).title : (col as KibanaDatatableColumn).name) + ) + ); + + return csvRows + .map(function (row) { + return row.join(separator) + '\r\n'; + }) + .join(''); +}; + +export const exportAsCsv = (formatted: boolean, data: ToCsvData) => { + const csv = new Blob([toCsv(formatted, data)], { type: 'text/plain;charset=utf-8' }); + saveAs(csv, `${data.filename || 'table'}.csv`); +}; diff --git a/src/plugins/vis_type_table/public/utils/index.ts b/src/plugins/vis_type_table/public/utils/index.ts index ec8bf60de6987..ab8f4911a5fa2 100644 --- a/src/plugins/vis_type_table/public/utils/index.ts +++ b/src/plugins/vis_type_table/public/utils/index.ts @@ -18,3 +18,4 @@ */ export * from './use'; +export * from './export_as_csv'; From d4850862f5710d98a11d447497e41216cd8f5d86 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 20 Jul 2020 18:22:12 +0300 Subject: [PATCH 13/67] Update labels --- .../vis_type_table/public/components/table_vis_controls.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx index 0ec42926d830b..da6f9d2f8dac0 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx @@ -75,7 +75,7 @@ export const TableVisControls = memo((props: TableVisControlsProps) => { const items = [ - + , { return ( Date: Fri, 24 Jul 2020 14:06:40 +0300 Subject: [PATCH 14/67] Fix merge conflicts --- .../public/agg_table/_agg_table.scss | 42 --- .../public/agg_table/agg_table.html | 34 --- .../public/agg_table/agg_table.js | 283 ------------------ .../public/components/table_vis_basic.tsx | 4 +- .../public/paginated_table/rows.js | 138 --------- src/plugins/vis_type_table/public/types.ts | 1 + .../vis_type_table/public/vis_controller.ts | 121 -------- 7 files changed, 4 insertions(+), 619 deletions(-) delete mode 100644 src/plugins/vis_type_table/public/agg_table/_agg_table.scss delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table.html delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table.js delete mode 100644 src/plugins/vis_type_table/public/paginated_table/rows.js delete mode 100644 src/plugins/vis_type_table/public/vis_controller.ts diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss deleted file mode 100644 index 4bbc4eb034f8d..0000000000000 --- a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss +++ /dev/null @@ -1,42 +0,0 @@ -kbn-agg-table, -kbn-agg-table-group { - display: block; -} - -.kbnAggTable { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.kbnAggTable__paginated { - flex: 1 1 auto; - overflow: auto; - - th { - text-align: left; - font-weight: $euiFontWeightBold; - } - - tr:hover td, - .kbnTableCellFilter { - background-color: $euiColorLightestShade; - } -} - -.kbnAggTable__controls { - flex: 0 0 auto; - display: flex; - align-items: center; - margin: $euiSizeS $euiSizeXS; - - > paginate-controls { - flex: 1 0 auto; - margin: 0; - padding: 0; - } -} - -.small { - font-size: 0.9em !important; -} diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.html b/src/plugins/vis_type_table/public/agg_table/agg_table.html deleted file mode 100644 index 5107bd2048286..0000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.html +++ /dev/null @@ -1,34 +0,0 @@ - - -
-    - - - -     - - - - - -
-
diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js deleted file mode 100644 index 1e98a06c2a6a9..0000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import _ from 'lodash'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; -import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; -import { i18n } from '@kbn/i18n'; - -export function KbnAggTable(config, RecursionHelper) { - return { - restrict: 'E', - template: aggTableTemplate, - scope: { - table: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - controllerAs: 'aggTable', - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el); - }, - controller: function ($scope) { - const self = this; - - self._saveAs = require('@elastic/filesaver').saveAs; - self.csv = { - separator: config.get(CSV_SEPARATOR_SETTING), - quoteValues: config.get(CSV_QUOTE_VALUES_SETTING), - }; - - self.exportAsCsv = function (formatted) { - const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); - self._saveAs(csv, self.csv.filename); - }; - - self.toCsv = function (formatted) { - const rows = formatted ? $scope.rows : $scope.table.rows; - const columns = formatted ? [...$scope.formattedColumns] : [...$scope.table.columns]; - - if ($scope.splitRow && formatted) { - columns.unshift($scope.splitRow); - } - - const nonAlphaNumRE = /[^a-zA-Z0-9]/; - const allDoubleQuoteRE = /"/g; - - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; - } - - let csvRows = []; - for (const row of rows) { - const rowArray = []; - for (const col of columns) { - const value = row[col.id]; - const formattedValue = - formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value); - rowArray.push(formattedValue); - } - csvRows = [...csvRows, rowArray]; - } - - // add the columns to the rows - csvRows.unshift( - columns.map(function (col) { - return escape(formatted ? col.title : col.name); - }) - ); - - return csvRows - .map(function (row) { - return row.join(self.csv.separator) + '\r\n'; - }) - .join(''); - }; - - $scope.$watchMulti( - ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], - function () { - const { table, exportTitle, percentageCol } = $scope; - const showPercentage = percentageCol !== ''; - - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - $scope.splitRow = null; - return; - } - - self.csv.filename = (exportTitle || table.title || 'unsaved') + '.csv'; - $scope.rows = table.rows; - $scope.formattedColumns = []; - - if (typeof $scope.dimensions === 'undefined') return; - - const { buckets, metrics, splitColumn, splitRow } = $scope.dimensions; - - $scope.formattedColumns = table.columns - .map(function (col, i) { - const isBucket = buckets.find((bucket) => bucket.accessor === i); - const isSplitColumn = splitColumn - ? splitColumn.find((splitColumn) => splitColumn.accessor === i) - : undefined; - const isSplitRow = splitRow - ? splitRow.find((splitRow) => splitRow.accessor === i) - : undefined; - const dimension = - isBucket || isSplitColumn || metrics.find((metric) => metric.accessor === i); - - const formatter = dimension - ? getFormatService().deserialize(dimension.format) - : undefined; - - const formattedColumn = { - id: col.id, - title: col.name, - formatter: formatter, - filterable: !!isBucket, - }; - - if (isSplitRow) { - $scope.splitRow = formattedColumn; - } - - if (!dimension) return; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = - dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = (tableRows) => { - return _.reduce( - tableRows, - function (prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows).map(col.id).min().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows).map(col.id).max().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter((column) => column); - - if (showPercentage) { - const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); - - // column to show percentage for was removed - if (insertAtIndex < 0) return; - - const { cols, rows } = addPercentageCol( - $scope.formattedColumns, - percentageCol, - table.rows, - insertAtIndex - ); - $scope.rows = rows; - $scope.formattedColumns = cols; - } - } - ); - }, - }; -} - -/** - * @param {Object[]} columns - the formatted columns that will be displayed - * @param {String} title - the title of the column to add to - * @param {Object[]} rows - the row data for the columns - * @param {Number} insertAtIndex - the index to insert the percentage column at - * @returns {Object} - cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol(columns, title, rows, insertAtIndex) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - }); - const newRows = rows.map((row) => ({ - [newId]: row[id] / sumTotal, - ...row, - })); - - return { cols: newCols, rows: newRows }; -} - -function insert(arr, index, ...items) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, ...items); - return newArray; -} diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx index 54a2aa3c0695e..89a31953f85d4 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_basic.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.tsx @@ -62,7 +62,9 @@ export const TableVisBasic = memo(({ table, vis, visParams }: TableVisBasicProps setVisibleColumns: () => {}, }} toolbarVisibility={{ - additionalControls: , + additionalControls: ( + + ), }} renderCellValue={renderCellValue} pagination={pagination} diff --git a/src/plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/paginated_table/rows.js deleted file mode 100644 index d8f01a10c63fa..0000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import angular from 'angular'; -import tableCellFilterHtml from './table_cell_filter.html'; - -export function KbnRows($compile) { - return { - restrict: 'A', - link: function ($scope, $el, attr) { - function addCell($tr, contents, column, row) { - function createCell() { - return $(document.createElement('td')); - } - - function createFilterableCell(value) { - const $template = $(tableCellFilterHtml); - $template.addClass('kbnTableCellFilter__hover'); - - const scope = $scope.$new(); - - scope.onFilterClick = (event, negate) => { - // Don't add filter if a link was clicked. - if ($(event.target).is('a')) { - return; - } - - $scope.filter({ - data: [ - { - table: $scope.table, - row: $scope.rows.findIndex((r) => r === row), - column: $scope.table.columns.findIndex((c) => c.id === column.id), - value, - }, - ], - negate, - }); - }; - - return $compile($template)(scope); - } - - let $cell; - let $cellContent; - - const contentsIsDefined = contents !== null && contents !== undefined; - - if (column.filterable && contentsIsDefined) { - $cell = createFilterableCell(contents); - // in jest tests 'angular' is using jqLite. In jqLite the method find lookups only by tags. - // Because of this, we should change a way how we get cell content so that tests will pass. - $cellContent = angular.element($cell[0].querySelector('[data-cell-content]')); - } else { - $cell = $cellContent = createCell(); - } - - // An AggConfigResult can "enrich" cell contents by applying a field formatter, - // which we want to do if possible. - contents = contentsIsDefined ? column.formatter.convert(contents, 'html') : ''; - - if (_.isObject(contents)) { - if (contents.attr) { - $cellContent.attr(contents.attr); - } - - if (contents.class) { - $cellContent.addClass(contents.class); - } - - if (contents.scope) { - $cellContent = $compile($cellContent.prepend(contents.markup))(contents.scope); - } else { - $cellContent.prepend(contents.markup); - } - - if (contents.attr) { - $cellContent.attr(contents.attr); - } - } else { - if (contents === '') { - $cellContent.prepend(' '); - } else { - $cellContent.prepend(contents); - } - } - - $tr.append($cell); - } - - $scope.$watchMulti([attr.kbnRows, attr.kbnRowsMin], function (vals) { - let rows = vals[0]; - const min = vals[1]; - - $el.empty(); - - if (!Array.isArray(rows)) rows = []; - - if (isFinite(min) && rows.length < min) { - // clone the rows so that we can add elements to it without upsetting the original - rows = _.clone(rows); - // crate the empty row which will be pushed into the row list over and over - const emptyRow = {}; - // push as many empty rows into the row array as needed - _.times(min - rows.length, function () { - rows.push(emptyRow); - }); - } - - rows.forEach(function (row) { - const $tr = $(document.createElement('tr')).appendTo($el); - $scope.columns.forEach((column) => { - const value = row[column.id]; - addCell($tr, value, column, row); - }); - }); - }); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index e36d79b157c3f..01df297a32670 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -36,6 +36,7 @@ export interface Dimensions { } export interface TableVisParams { + title: string | undefined; type: 'table'; perPage: number | ''; showPartialRows: boolean; diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts deleted file mode 100644 index d87812b9f5d69..0000000000000 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; -import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; - -import { VisParams, ExprVis } from '../../visualizations/public'; -import { getAngularModule } from './get_inner_angular'; -import { getKibanaLegacy } from './services'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; - -const innerAngularName = 'kibana/table_vis'; - -export function getTableVisualizationControllerClass( - core: CoreSetup, - context: PluginInitializerContext -) { - return class TableVisualizationController { - private tableVisModule: IModule | undefined; - private injector: auto.IInjectorService | undefined; - el: JQuery; - vis: ExprVis; - $rootScope: IRootScopeService | null = null; - $scope: (IScope & { [key: string]: any }) | undefined; - $compile: ICompileService | undefined; - - constructor(domeElement: Element, vis: ExprVis) { - this.el = $(domeElement); - this.vis = vis; - } - - getInjector() { - if (!this.injector) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); - this.injector = angular.bootstrap(mountpoint, [innerAngularName]); - this.el.append(mountpoint); - } - - return this.injector; - } - - async initLocalAngular() { - if (!this.tableVisModule) { - const [coreStart] = await core.getStartServices(); - this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); - initTableVisLegacyModule(this.tableVisModule); - } - } - - async render(esResponse: object, visParams: VisParams) { - getKibanaLegacy().loadFontAwesome(); - await this.initLocalAngular(); - - return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; - } - - // How things get into this $scope? - // To inject variables into this $scope there's the following pipeline of stuff to check: - // - visualize_embeddable => that's what the editor creates to wrap this Angular component - // - build_pipeline => it serialize all the params into an Angular template compiled on the fly - // - table_vis_fn => unserialize the params and prepare them for the final React/Angular bridge - // - visualization_renderer => creates the wrapper component for this controller and passes the params - // - // In case some prop is missing check into the top of the chain if they are available and check - // the list above that it is passing through - this.$scope.vis = this.vis; - this.$scope.visState = { params: visParams, title: visParams.title }; - this.$scope.esResponse = esResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); - updateScope(); - this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); - } - }); - } - - destroy() { - if (this.$rootScope) { - this.$rootScope.$destroy(); - this.$rootScope = null; - } - } - }; -} From 14c27a4d8992f555ed4f1b6abee7d326a9b75f2a Mon Sep 17 00:00:00 2001 From: sulemanof Date: Fri, 24 Jul 2020 18:45:28 +0300 Subject: [PATCH 15/67] Use split table --- .../public/components/table_vis_split.tsx | 33 +++++-------------- .../components/table_visualization.scss | 15 +++++++++ .../public/components/table_visualization.tsx | 7 +++- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_vis_split.tsx b/src/plugins/vis_type_table/public/components/table_vis_split.tsx index c3c4347514aee..bcfe79b0beead 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_split.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_split.tsx @@ -18,12 +18,12 @@ */ import React, { memo } from 'react'; -import { EuiDataGrid } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; import { ExprVis } from 'src/plugins/visualizations/public'; -import { createTableVisCell } from './table_vis_cell'; import { TableGroup } from '../table_vis_response_handler'; import { TableVisParams } from '../types'; +import { TableVisBasic } from './table_vis_basic'; interface TableVisSplitProps { tables: TableGroup[]; @@ -34,28 +34,13 @@ interface TableVisSplitProps { export const TableVisSplit = memo(({ tables, vis, visParams }: TableVisSplitProps) => { return ( <> - {tables.map(({ table }, key) => ( - ({ - id: col.id, - display: col.name, - }))} - rowCount={table.rows.length} - columnVisibility={{ - visibleColumns: table.columns.map((col) => col.id), - setVisibleColumns: () => {}, - }} - renderCellValue={createTableVisCell(table, vis, visParams)} - pagination={{ - pageIndex: 0, - pageSize: 10, - pageSizeOptions: [50, 100, 200], - onChangePage: () => {}, - onChangeItemsPerPage: () => {}, - }} - /> + {tables.map(({ tables: dataTable, key, title }) => ( +
+ +

{title}

+
+ +
))} ); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.scss b/src/plugins/vis_type_table/public/components/table_visualization.scss index c467dee6d5aca..ad2a1191c2829 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.scss +++ b/src/plugins/vis_type_table/public/components/table_visualization.scss @@ -10,4 +10,19 @@ flex-direction: column; flex: 1 0 100%; overflow: auto; + + @include euiScrollBar; +} + +.tbvChart__split { + padding: $euiSizeS; + margin-bottom: $euiSizeL; + + > h3 { + text-align: center; + } +} + +.tbvChart__splitColumns { + flex-direction: row; } diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index f595a5ad8fe9f..cbb2cbc5d543a 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -19,6 +19,7 @@ import './table_visualization.scss'; import React, { useEffect } from 'react'; +import classNames from 'classnames'; import { CoreSetup } from 'kibana/public'; import { ReactVisComponentProps } from 'src/plugins/visualizations/public'; @@ -38,9 +39,13 @@ export const createTableVisualizationComponent = (core: CoreSetup) => ({ renderComplete(); }, [renderComplete]); + const className = classNames('tbvChart', { + tbvChart__splitColumns: direction === 'column', + }); + return ( -
+
{table ? ( ) : ( From 5853a10ea8540235e4632c292595114358564044 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 27 Jul 2020 14:32:04 +0300 Subject: [PATCH 16/67] Fix functional tests --- .../public/components/table_vis_cell.tsx | 1 + ...t_start.js => _histogram_request_start.ts} | 25 ++--- .../apps/visualize/_linked_saved_searches.ts | 12 +-- .../page_objects/visualize_chart_page.ts | 20 +--- test/functional/services/data_grid.ts | 92 +++++++++++++++++++ test/functional/services/index.ts | 2 + 6 files changed, 119 insertions(+), 33 deletions(-) rename test/functional/apps/visualize/{_histogram_request_start.js => _histogram_request_start.ts} (81%) create mode 100644 test/functional/services/data_grid.ts diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx index 85f39c6db50dd..460beb47233bd 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -56,6 +56,7 @@ export const createTableVisCell = ( * which we want to do if possible. */ dangerouslySetInnerHTML={{ __html: content }} // eslint-disable-line react/no-danger + data-test-subj="tbvChartCellContent" /> ); diff --git a/test/functional/apps/visualize/_histogram_request_start.js b/test/functional/apps/visualize/_histogram_request_start.ts similarity index 81% rename from test/functional/apps/visualize/_histogram_request_start.js rename to test/functional/apps/visualize/_histogram_request_start.ts index f6440cfbd76ff..b53bad0936181 100644 --- a/test/functional/apps/visualize/_histogram_request_start.js +++ b/test/functional/apps/visualize/_histogram_request_start.ts @@ -18,10 +18,12 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); + const PageObjects = getPageObjects([ 'common', 'visualize', @@ -48,33 +50,32 @@ export default function ({ getService, getPageObjects }) { describe('interval parameter uses autoBounds', function () { it('should use provided value when number of generated buckets is less than histogram:maxBars', async function () { - const providedInterval = 2400000000; + const providedInterval = '2400000000'; log.debug(`Interval = ${providedInterval}`); await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); await PageObjects.visEditor.clickGo(); + await retry.try(async () => { const data = await PageObjects.visChart.getTableVisData(); - const dataArray = data.replace(/,/g, '').split('\n'); - expect(dataArray.length).to.eql(20); - const bucketStart = parseInt(dataArray[0], 10); - const bucketEnd = parseInt(dataArray[2], 10); + expect(data.length).to.eql(10); + const bucketStart = parseInt(data[0][0].replace(/,/g, ''), 10); + const bucketEnd = parseInt(data[1][0].replace(/,/g, ''), 10); const actualInterval = bucketEnd - bucketStart; expect(actualInterval).to.eql(providedInterval); }); }); it('should scale value to round number when number of generated buckets is greater than histogram:maxBars', async function () { - const providedInterval = 100; + const providedInterval = '100'; log.debug(`Interval = ${providedInterval}`); await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); await PageObjects.visEditor.clickGo(); - await PageObjects.common.sleep(1000); //fix this + await PageObjects.common.sleep(1000); // fix this await retry.try(async () => { const data = await PageObjects.visChart.getTableVisData(); - const dataArray = data.replace(/,/g, '').split('\n'); - expect(dataArray.length).to.eql(20); - const bucketStart = parseInt(dataArray[0], 10); - const bucketEnd = parseInt(dataArray[2], 10); + expect(data.length).to.eql(10); + const bucketStart = parseInt(data[0][0].replace(/,/g, ''), 10); + const bucketEnd = parseInt(data[1][0].replace(/,/g, ''), 10); const actualInterval = bucketEnd - bucketStart; expect(actualInterval).to.eql(1200000000); }); diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index b0edcb68f3efc..4f873db15f49b 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); await retry.waitFor('wait for count to equal 9,109', async () => { const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '9,109'; + return data[0][0] === '9,109'; }); }); @@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await retry.waitFor('wait for count to equal 3,950', async () => { const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '3,950'; + return data[0][0] === '3,950'; }); }); @@ -70,7 +70,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 707', async () => { const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '707'; + return data[0][0] === '707'; }); }); @@ -78,7 +78,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickUnlinkSavedSearch(); await retry.waitFor('wait for count to equal 707', async () => { const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '707'; + return data[0][0] === '707'; }); // The filter on the saved search should now be in the editor expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); @@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 1,293', async () => { const unfilteredData = await PageObjects.visChart.getTableVisData(); - return unfilteredData.trim() === '1,293'; + return unfilteredData[0][0] === '1,293'; }); }); @@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 1,293', async () => { const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '1,293'; + return data[0][0] === '1,293'; }); }); }); diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index ade78cbb810d5..37842a89451f3 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -26,6 +26,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr const log = getService('log'); const retry = getService('retry'); const table = getService('table'); + const dataGrid = getService('dataGrid'); const defaultFindTimeout = config.get('timeouts.find'); const { common } = getPageObjects(['common']); @@ -307,25 +308,14 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr } public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) { - const tableVis = await testSubjects.find('tableVis'); - const $ = await tableVis.parseDomContent(); - const headers = $('span[ng-bind="::col.title"]') - .toArray() - .map((header: any) => $(header).text()); + const headers = await dataGrid.getHeaders(); const fieldColumnIndex = headers.indexOf(fieldName); - return await find.byCssSelector( - `[data-test-subj="paginated-table-body"] tr:nth-of-type(${rowIndex}) td:nth-of-type(${ - fieldColumnIndex + 1 - }) a` - ); + const cell = await dataGrid.getCellElement(rowIndex, fieldColumnIndex + 1); + return await cell.findByTagName('a'); } - /** - * If you are writing new tests, you should rather look into getTableVisContent method instead. - * @deprecated Use getTableVisContent instead. - */ public async getTableVisData() { - return await testSubjects.getVisibleText('paginated-table-body'); + return await dataGrid.getDataFromTestSubj(); } /** diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts new file mode 100644 index 0000000000000..72b828034f315 --- /dev/null +++ b/test/functional/services/data_grid.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { WebElementWrapper } from './lib/web_element_wrapper'; + +export function DataGridProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + class DataGrid { + /** + * Finds data grid and returns data in the nested array format + * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] + * @param dataTestSubj data-test-subj selector + */ + + public async getDataFromTestSubj( + dataTestSubj: string = 'dataGridWrapper' + ): Promise { + const table = await testSubjects.find(dataTestSubj); + return await this.getDataFromElement(table); + } + + /** + * Converts the data grid data into nested array + * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] + * @param element table + */ + public async getDataFromElement(element: WebElementWrapper): Promise { + const $ = await element.parseDomContent(); + return $('[data-test-subj="dataGridRow"]') + .toArray() + .map((row) => + $(row) + .find('[data-test-subj="dataGridRowCell"]') + .toArray() + .map((cell) => + $(cell) + .findTestSubject('tbvChartCellContent') + .text() + .replace(/ /g, '') + .trim() + ) + ); + } + + /** + * Returns an array of data grid headers names + */ + public async getHeaders() { + const header = await testSubjects.find('dataGridWrapper > dataGridHeader'); + const $ = await header.parseDomContent(); + return $('.euiDataGridHeaderCell__content') + .toArray() + .map((cell) => $(cell).text()); + } + + /** + * Returns a grid cell element by row & column indexes. + * The row offset equals 1 since the first row of data grid is the header row. + * @param rowIndex data row index starting from 1 (1 means 1st row) + * @param columnIndex column index starting from 1 (1 means 1st column) + */ + public async getCellElement(rowIndex: number, columnIndex: number) { + return await find.byCssSelector( + `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRow"]:nth-of-type(${ + rowIndex + 1 + }) + [data-test-subj="dataGridRowCell"]:nth-of-type(${columnIndex})` + ); + } + } + + return new DataGrid(); +} diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 7891a6b00f729..9ba297530dce3 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -36,6 +36,7 @@ import { DashboardPanelActionsProvider, DashboardVisualizationProvider, } from './dashboard'; +import { DataGridProvider } from './data_grid'; import { DocTableProvider } from './doc_table'; import { EmbeddingProvider } from './embedding'; import { FilterBarProvider } from './filter_bar'; @@ -65,6 +66,7 @@ export const services = { snapshots: SnapshotsProvider, dashboardVisualizations: DashboardVisualizationProvider, dashboardExpect: DashboardExpectProvider, + dataGrid: DataGridProvider, failureDebugging: FailureDebuggingProvider, listingTable: ListingTableProvider, dashboardAddPanel: DashboardAddPanelProvider, From 7a561da7432d47e8be3d68d64b893fe870592270 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Tue, 28 Jul 2020 15:29:33 +0300 Subject: [PATCH 17/67] Fix dashboard tests --- .../components/table_vis_no_results.tsx | 2 +- .../public/components/table_visualization.tsx | 2 +- .../apps/dashboard/embeddable_rendering.js | 2 +- .../page_objects/visualize_chart_page.ts | 6 +++++- .../services/dashboard/expectations.ts | 14 +++++++++----- test/functional/services/data_grid.ts | 19 ++++++++++++------- 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx b/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx index 4dd560c604b20..2382704f8551e 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_no_results.tsx @@ -22,7 +22,7 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export const TableVisNoResults = () => ( -
+