From bde624f1f091d823056213b1a7b92ed89f6b2af7 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Tue, 20 Sep 2022 09:21:43 -0700 Subject: [PATCH] [Table Visualization] remove legacy table and add table visualization (#2279) To convert the table visualization into React & OUI DataGrid component, in this PR, we did two main things: * clean out legacy angular code * restore table vis in react * Datagrid component does not support splitted grids. For future transfer to OUI Datagrid, we create a tableGroup in visData for splitted grids. issue resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2212 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2213a Signed-off-by: Anan Zhuang rename visTable back to opensearch_dashboards_table and add stronger type Signed-off-by: Anan Zhuang Signed-off-by: Anan Zhuang [Table Visualization] add a plain datagrid component (#2390) implement a plain OuiDataGrid component use the basic pagenation, sort and format. Partially resolve: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2305 Signed-off-by: Anan Zhuang Signed-off-by: Anan Zhuang [Table Visualization] restore datagrid columns (#2411) * restore datagrid columns * display column title correctly * deangular and re-use formatted column * convert formatted column to data grid column * restore filter in and filter out value functions Partially resolve: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2305 Signed-off-by: Anan Zhuang [Table Visualization] restore pagination to table vis (#2461) * add pagination patically resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2305 Signed-off-by: Anan Zhuang [Table Visualization] make table vis column resizable (#2464) * add resizable state to column Partially resolve: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2305 Signed-off-by: Anan Zhuang [Table Visualization] make table vis column sortable (#2502) * add sort state (asc | desc) to column * fix pagination issue Partially resolve: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2305 Signed-off-by: Anan Zhuang [Table Visualization] format table cell and restore showTotal feature (#2562) * format table cell to show Date and percent * restore showTotal feature: it allows table vis to show total, avg, min, max and count statics on count * fix some type errors Partically resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2379 Signed-off-by: Anan Zhuang [Table Visualization]restore export csv feature to table vis (#2568) * add addtional action in toolbar to allow export data to csv. there are two types of csv, raw and formatted. raw is the original data and formatted is to show formatted Date and percentage data when needed. * when table is not saved, export csv file will be named as unsaved-raw.csv if choose raw. when table is saved with a filename, it will be saved as [filename]-[raw/formatted].csv Partically resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2379 Signed-off-by: Anan Zhuang [Table Visualization] split table in rows and columns This PR implement a group component TableVisGroupComponent utilizing TableVisComponent as sub component. It also adds a title to TableVisComponent. Partically resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2379 Signed-off-by: Anan Zhuang [Table Visualization] remove local state * remove local col width state to allow split tables to fetch updated col width state * fix type errors in usePagination Signed-off-by: Anan Zhuang [Table Visualization] remove repeated column from split tables Signed-off-by: Anan Zhuang [Table Visualization][BUG] partical rows option generate repeated metrics Signed-off-by: Anan Zhuang --- .../data/common/field_formats/field_format.ts | 6 + src/plugins/vis_type_table/README.md | 2 +- .../vis_type_table/opensearch_dashboards.json | 8 +- .../__snapshots__/table_vis_fn.test.ts.snap | 47 - .../vis_type_table/public/_table_vis.scss | 23 - .../public/agg_table/_agg_table.scss | 42 - .../public/agg_table/_index.scss | 1 - .../public/agg_table/agg_table.html | 34 - .../public/agg_table/agg_table.js | 295 ------- .../public/agg_table/agg_table.test.js | 512 ----------- .../public/agg_table/agg_table_group.html | 77 -- .../public/agg_table/agg_table_group.js | 67 -- .../public/agg_table/agg_table_group.test.js | 152 ---- .../public/agg_table/tabified_data.js | 806 ------------------ .../public/components/table_vis_app.scss | 19 + .../public/components/table_vis_app.tsx | 57 ++ .../public/components/table_vis_component.tsx | 156 ++++ .../components/table_vis_component_group.tsx | 36 + .../public/components/table_vis_control.tsx | 57 ++ .../components/table_vis_grid_columns.tsx | 148 ++++ .../public/get_inner_angular.ts | 117 --- src/plugins/vis_type_table/public/index.scss | 10 - src/plugins/vis_type_table/public/index.ts | 1 - .../public/paginated_table/_index.scss | 1 - .../paginated_table/_table_cell_filter.scss | 30 - .../paginated_table/paginated_table.html | 55 -- .../public/paginated_table/paginated_table.js | 120 --- .../paginated_table/paginated_table.test.ts | 485 ----------- .../public/paginated_table/rows.js | 149 ---- .../paginated_table/table_cell_filter.html | 23 - src/plugins/vis_type_table/public/plugin.ts | 70 +- src/plugins/vis_type_table/public/services.ts | 5 - .../vis_type_table/public/table_vis.html | 29 - .../public/table_vis_controller.js | 67 -- .../public/table_vis_controller.test.ts | 272 ------ .../public/table_vis_fn.test.ts | 89 -- .../vis_type_table/public/table_vis_fn.ts | 50 +- .../public/table_vis_legacy_module.ts | 52 -- .../public/table_vis_renderer.tsx | 37 + .../public/table_vis_response_handler.ts | 69 +- .../vis_type_table/public/table_vis_type.ts | 143 ++-- src/plugins/vis_type_table/public/to_ast.ts | 62 ++ src/plugins/vis_type_table/public/types.ts | 36 +- .../public/utils/convert_to_csv_data.ts | 85 ++ .../public/utils/convert_to_formatted_data.ts | 179 ++++ .../vis_type_table/public/utils/index.ts | 7 + .../public/utils/use_pagination.ts | 39 + .../vis_type_table/public/vis_controller.ts | 135 --- src/plugins/vis_type_table/server/index.ts | 3 - .../__snapshots__/build_pipeline.test.ts.snap | 10 +- .../public/legacy/build_pipeline.test.ts | 78 -- .../public/legacy/build_pipeline.ts | 27 - 52 files changed, 1062 insertions(+), 4018 deletions(-) delete mode 100644 src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap delete mode 100644 src/plugins/vis_type_table/public/_table_vis.scss 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.html delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table.js delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table.test.js delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table_group.html delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table_group.js delete mode 100644 src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js delete mode 100644 src/plugins/vis_type_table/public/agg_table/tabified_data.js create mode 100644 src/plugins/vis_type_table/public/components/table_vis_app.scss create mode 100644 src/plugins/vis_type_table/public/components/table_vis_app.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_component.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_component_group.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_control.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx delete mode 100644 src/plugins/vis_type_table/public/get_inner_angular.ts delete mode 100644 src/plugins/vis_type_table/public/index.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/paginated_table.html delete mode 100644 src/plugins/vis_type_table/public/paginated_table/paginated_table.js delete mode 100644 src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts delete mode 100644 src/plugins/vis_type_table/public/paginated_table/rows.js delete mode 100644 src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html 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 delete mode 100644 src/plugins/vis_type_table/public/table_vis_fn.test.ts delete mode 100644 src/plugins/vis_type_table/public/table_vis_legacy_module.ts create mode 100644 src/plugins/vis_type_table/public/table_vis_renderer.tsx create mode 100644 src/plugins/vis_type_table/public/to_ast.ts create mode 100644 src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts create mode 100644 src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts create mode 100644 src/plugins/vis_type_table/public/utils/index.ts create mode 100644 src/plugins/vis_type_table/public/utils/use_pagination.ts delete mode 100644 src/plugins/vis_type_table/public/vis_controller.ts diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 8bff51d1f16c..c5c945f1b899 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -95,6 +95,12 @@ export abstract class FieldFormat { */ public type: any = this.constructor; + /** + * @property {boolean} - allow numeric aggregation + * @private + */ + allowsNumericalAggregations?: boolean; + protected readonly _params: any; protected getConfig: FieldFormatsGetConfigFn | undefined; diff --git a/src/plugins/vis_type_table/README.md b/src/plugins/vis_type_table/README.md index cf37e133ed1c..166f9a36b584 100644 --- a/src/plugins/vis_type_table/README.md +++ b/src/plugins/vis_type_table/README.md @@ -1 +1 @@ -Contains the data table visualization, that allows presenting data in a simple table format. \ No newline at end of file +Contains the data table visualization, that allows presenting data using OUI datagrid. \ No newline at end of file diff --git a/src/plugins/vis_type_table/opensearch_dashboards.json b/src/plugins/vis_type_table/opensearch_dashboards.json index e2f050534c1e..668331680549 100644 --- a/src/plugins/vis_type_table/opensearch_dashboards.json +++ b/src/plugins/vis_type_table/opensearch_dashboards.json @@ -6,13 +6,13 @@ "requiredPlugins": [ "expressions", "visualizations", - "data", - "opensearchDashboardsLegacy" + "data" ], "requiredBundles": [ "opensearchDashboardsUtils", - "share", + "opensearchDashboardsReact", "charts", + "share", "visDefaultEditor" ] -} +} \ No newline at end of file diff --git a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap deleted file mode 100644 index dc6571de969f..000000000000 --- a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`interpreter/functions#table returns an object with the correct structure 1`] = ` -Object { - "as": "visualization", - "type": "render", - "value": Object { - "params": Object { - "listenOnChange": true, - }, - "visConfig": Object { - "dimensions": Object { - "buckets": Array [], - "metrics": Array [ - Object { - "accessor": 0, - "aggType": "count", - "format": Object { - "id": "number", - }, - "params": Object {}, - }, - ], - }, - "perPage": 10, - "showMetricsAtAllLevels": false, - "showPartialRows": false, - "showTotal": false, - "sort": Object { - "columnIndex": null, - "direction": null, - }, - "title": "My Chart title", - "totalFunc": "sum", - }, - "visData": Object { - "tables": Array [ - Object { - "columns": Array [], - "rows": Array [], - }, - ], - }, - "visType": "table", - }, -} -`; 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 ea4b4d0d1c98..000000000000 --- a/src/plugins/vis_type_table/public/_table_vis.scss +++ /dev/null @@ -1,23 +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 { - osd-agg-table-group > .table > tbody > tr > td { - border-top: 0; - } - - .pagination-other-pages { - justify-content: flex-end; - } - - .pagination-size { - display: none; - } -} 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 156db063c8db..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss +++ /dev/null @@ -1,42 +0,0 @@ -osd-agg-table, -osd-agg-table-group { - display: block; -} - -.osdAggTable { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.osdAggTable__paginated { - flex: 1 1 auto; - overflow: auto; - - th { - text-align: left; - font-weight: $euiFontWeightBold; - } - - tr:hover td, - .osdTableCellFilter { - background-color: $euiColorLightestShade; - } -} - -.osdAggTable__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/_index.scss b/src/plugins/vis_type_table/public/agg_table/_index.scss deleted file mode 100644 index ed94e8449120..000000000000 --- 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.html b/src/plugins/vis_type_table/public/agg_table/agg_table.html deleted file mode 100644 index 8e8aafa83fd8..000000000000 --- 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 a00aea27869f..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ /dev/null @@ -1,295 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 '@osd/i18n'; - -export function OsdAggTable(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/csv;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/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/agg_table/agg_table.test.js deleted file mode 100644 index 14d0c7fe7952..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js +++ /dev/null @@ -1,512 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 moment from 'moment'; -import angular from 'angular'; -import 'angular-mocks'; -import sinon from 'sinon'; -import { round } from 'lodash'; - -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../opensearch_dashboards_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { UI_SETTINGS } from '../../../data/public/'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; - -import { setFormatService } from '../services'; -import { getInnerAngular } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -const uiSettings = new Map(); - -describe('Table Vis - AggTable Directive', function () { - const core = coreMock.createStart(); - - core.uiSettings.set = jest.fn((key, value) => { - uiSettings.set(key, value); - }); - - core.uiSettings.get = jest.fn((key) => { - const defaultValues = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - 'dateFormat:tz': 'UTC', - [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, - [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', - [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', - [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, - [CSV_SEPARATOR_SETTING]: ',', - [CSV_QUOTE_VALUES_SETTING]: true, - }; - - return defaultValues[key] || uiSettings.get(key); - }); - - let $rootScope; - let $compile; - let settings; - - const initLocalAngular = () => { - const tableVisModule = getInnerAngular('opensearch-dashboards/table_vis', core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(() => { - setUiSettings(core.uiSettings); - setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); - initLocalAngular(); - angular.mock.module('opensearch-dashboards/table_vis'); - angular.mock.inject(($injector, config) => { - settings = config; - - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }); - }); - - let $scope; - beforeEach(function () { - $scope = $rootScope.$new(); - }); - afterEach(function () { - $scope.$destroy(); - }); - - test('renders a simple response properly', function () { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.table = tabifiedData.metricOnly.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(1); - expect($el.find('td').length).toBe(1); - expect($el.find('td').text()).toEqual('1,000'); - }); - - test('renders nothing if the table is empty', function () { - $scope.dimensions = {}; - $scope.table = null; - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(0); - }); - - test('renders a complex response properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - const $el = $(''); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(1); - - const $rows = $el.find('tbody tr'); - expect($rows.length).toBeGreaterThan(0); - - function validBytes(str) { - const num = str.replace(/,/g, ''); - if (num !== '-') { - expect(num).toMatch(/^\d+$/); - } - } - - $rows.each(function () { - // 6 cells in every row - const $cells = $(this).find('td'); - expect($cells.length).toBe(6); - - const txts = $cells.map(function () { - return $(this).text().trim(); - }); - - // two character country code - expect(txts[0]).toMatch(/^(png|jpg|gif|html|css)$/); - validBytes(txts[1]); - - // country - expect(txts[2]).toMatch(/^\w\w$/); - validBytes(txts[3]); - - // os - expect(txts[4]).toMatch(/^(win|mac|linux)$/); - validBytes(txts[5]); - }); - }); - - describe('renders totals row', function () { - async function totalsRowTest(totalFunc, expected) { - function setDefaultTimezone() { - moment.tz.setDefault(settings.get('dateFormat:tz')); - } - - const oldTimezoneSetting = settings.get('dateFormat:tz'); - settings.set('dateFormat:tz', 'UTC'); - setDefaultTimezone(); - - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.showTotal = true; - $scope.totalFunc = totalFunc; - const $el = $(``); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tfoot').length).toBe(1); - - const $rows = $el.find('tfoot tr'); - expect($rows.length).toBe(1); - - const $cells = $($rows[0]).find('th'); - expect($cells.length).toBe(6); - - for (let i = 0; i < 6; i++) { - expect($($cells[i]).text().trim()).toBe(expected[i]); - } - settings.set('dateFormat:tz', oldTimezoneSetting); - setDefaultTimezone(); - } - test('as count', async function () { - await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); - }); - test('as min', async function () { - await totalsRowTest('min', [ - '', - '2014-09-28', - '9,283', - 'Sep 28, 2014 @ 00:00:00.000', - '1', - '11', - ]); - }); - test('as max', async function () { - await totalsRowTest('max', [ - '', - '2014-10-03', - '220,943', - 'Oct 3, 2014 @ 00:00:00.000', - '239', - '837', - ]); - }); - test('as avg', async function () { - await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); - }); - test('as sum', async function () { - await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); - }); - }); - - describe('aggTable.toCsv()', function () { - test('escapes rows and columns properly', function () { - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - expect(aggTable.toCsv()).toBe( - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' - ); - }); - - test('exports rows and columns properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - const raw = aggTable.toCsv(false); - expect(raw).toBe( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - 'png,412032,IT,9299,win,0' + - '\r\n' + - 'png,412032,IT,9299,mac,9299' + - '\r\n' + - 'png,412032,US,8293,linux,3992' + - '\r\n' + - 'png,412032,US,8293,mac,3029' + - '\r\n' + - 'css,412032,MX,9299,win,4992' + - '\r\n' + - 'css,412032,MX,9299,mac,5892' + - '\r\n' + - 'css,412032,US,8293,linux,3992' + - '\r\n' + - 'css,412032,US,8293,mac,3029' + - '\r\n' + - 'html,412032,CN,9299,win,4992' + - '\r\n' + - 'html,412032,CN,9299,mac,5892' + - '\r\n' + - 'html,412032,FR,8293,win,3992' + - '\r\n' + - 'html,412032,FR,8293,mac,3029' + - '\r\n' - ); - }); - - test('exports formatted rows and columns properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - // Create our own converter since the ones we use for tests don't actually transform the provided value - $tableScope.formattedColumns[0].formatter.convert = (v) => `${v}_formatted`; - - const formatted = aggTable.toCsv(true); - expect(formatted).toBe( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - '"png_formatted",412032,IT,9299,win,0' + - '\r\n' + - '"png_formatted",412032,IT,9299,mac,9299' + - '\r\n' + - '"png_formatted",412032,US,8293,linux,3992' + - '\r\n' + - '"png_formatted",412032,US,8293,mac,3029' + - '\r\n' + - '"css_formatted",412032,MX,9299,win,4992' + - '\r\n' + - '"css_formatted",412032,MX,9299,mac,5892' + - '\r\n' + - '"css_formatted",412032,US,8293,linux,3992' + - '\r\n' + - '"css_formatted",412032,US,8293,mac,3029' + - '\r\n' + - '"html_formatted",412032,CN,9299,win,4992' + - '\r\n' + - '"html_formatted",412032,CN,9299,mac,5892' + - '\r\n' + - '"html_formatted",412032,FR,8293,win,3992' + - '\r\n' + - '"html_formatted",412032,FR,8293,mac,3029' + - '\r\n' - ); - }); - }); - - test('renders percentage columns', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.percentageCol = 'Average bytes'; - - const $el = $(``); - - $compile($el)($scope); - $scope.$digest(); - - const $headings = $el.find('th'); - expect($headings.length).toBe(7); - expect($headings.eq(3).text().trim()).toBe('Average bytes percentages'); - - const countColId = $scope.table.columns.find((col) => col.name === $scope.percentageCol).id; - const counts = $scope.table.rows.map((row) => row[countColId]); - const total = counts.reduce((sum, curr) => sum + curr, 0); - const $percentageColValues = $el.find('tbody tr').map((i, el) => $(el).find('td').eq(3).text()); - - $percentageColValues.each((i, value) => { - const percentage = `${round((counts[i] / total) * 100, 3)}%`; - expect(value).toBe(percentage); - }); - }); - - describe('aggTable.exportAsCsv()', function () { - let origBlob; - function FakeBlob(slices, opts) { - this.slices = slices; - this.opts = opts; - } - - beforeEach(function () { - origBlob = window.Blob; - window.Blob = FakeBlob; - }); - - afterEach(function () { - window.Blob = origBlob; - }); - - test('calls _saveAs properly', function () { - const $el = $compile('')($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - - const saveAs = sinon.stub(aggTable, '_saveAs'); - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - aggTable.csv.filename = 'somefilename.csv'; - aggTable.exportAsCsv(); - - expect(saveAs.callCount).toBe(1); - const call = saveAs.getCall(0); - expect(call.args[0]).toBeInstanceOf(FakeBlob); - expect(call.args[0].slices).toEqual([ - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', - ]); - expect(call.args[0].opts).toEqual({ - type: 'text/csv;charset=utf-8', - }); - expect(call.args[1]).toBe('somefilename.csv'); - }); - - test('should use the export-title attribute', function () { - const expected = 'export file name'; - const $el = $compile( - `` - )($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [], - rows: [], - }; - $tableScope.exportTitle = expected; - $scope.$digest(); - - expect(aggTable.csv.filename).toEqual(`${expected}.csv`); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/agg_table/agg_table_group.html deleted file mode 100644 index 2dcf7f125f61..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - -
- {{ table.title }} -
- - - -
- - - - - - - - - - - - -
- {{ table.title }} -
- - - -
diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.js deleted file mode 100644 index 133b20800a18..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 aggTableGroupTemplate from './agg_table_group.html'; - -export function OsdAggTableGroup(RecursionHelper) { - return { - restrict: 'E', - template: aggTableGroupTemplate, - scope: { - group: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el, { - post: function ($scope) { - $scope.$watch('group', function (group) { - // clear the previous "state" - $scope.rows = $scope.columns = false; - - if (!group || !group.tables.length) return; - - const childLayout = group.direction === 'row' ? 'rows' : 'columns'; - - $scope[childLayout] = group.tables; - }); - }, - }); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js deleted file mode 100644 index 18a48e922116..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js +++ /dev/null @@ -1,152 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 angular from 'angular'; -import 'angular-mocks'; -import expect from '@osd/expect'; - -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../opensearch_dashboards_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { setFormatService } from '../services'; -import { getInnerAngular } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -const uiSettings = new Map(); - -describe('Table Vis - AggTableGroup Directive', function () { - const core = coreMock.createStart(); - let $rootScope; - let $compile; - - core.uiSettings.set = jest.fn((key, value) => { - uiSettings.set(key, value); - }); - - core.uiSettings.get = jest.fn((key) => { - return uiSettings.get(key); - }); - - const initLocalAngular = () => { - const tableVisModule = getInnerAngular('opensearch-dashboards/table_vis', core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(() => { - setUiSettings(core.uiSettings); - setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); - initLocalAngular(); - angular.mock.module('opensearch-dashboards/table_vis'); - angular.mock.inject(($injector) => { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }); - }); - - let $scope; - beforeEach(function () { - $scope = $rootScope.$new(); - }); - afterEach(function () { - $scope.$destroy(); - }); - - it('renders a simple split response properly', function () { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.group = tabifiedData.metricOnly; - $scope.sort = { - columnIndex: null, - direction: null, - }; - const $el = $( - '' - ); - - $compile($el)($scope); - $scope.$digest(); - - // should create one sub-tbale - expect($el.find('osd-agg-table').length).to.be(1); - }); - - it('renders nothing if the table list is empty', function () { - const $el = $( - '' - ); - - $scope.group = { - tables: [], - }; - - $compile($el)($scope); - $scope.$digest(); - - const $subTables = $el.find('osd-agg-table'); - expect($subTables.length).to.be(0); - }); - - it('renders a complex response properly', function () { - $scope.dimensions = { - splitRow: [{ accessor: 0, params: {} }], - buckets: [ - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - const group = ($scope.group = tabifiedData.threeTermBucketsWithSplit); - const $el = $( - '' - ); - $compile($el)($scope); - $scope.$digest(); - - const $subTables = $el.find('osd-agg-table'); - expect($subTables.length).to.be(3); - - const $subTableHeaders = $el.find('.osdAggTable__groupHeader'); - expect($subTableHeaders.length).to.be(3); - - $subTableHeaders.each(function (i) { - expect($(this).text()).to.be(group.tables[i].title); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/agg_table/tabified_data.js b/src/plugins/vis_type_table/public/agg_table/tabified_data.js deleted file mode 100644 index ce344d5c48b6..000000000000 --- a/src/plugins/vis_type_table/public/agg_table/tabified_data.js +++ /dev/null @@ -1,806 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 const tabifiedData = { - metricOnly: { - tables: [ - { - columns: [ - { - id: 'col-0-1', - name: 'Count', - }, - ], - rows: [ - { - 'col-0-1': 1000, - }, - ], - }, - ], - }, - threeTermBuckets: { - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_1', - name: 'Average bytes', - }, - { - id: 'col-2-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - { - id: 'col-4-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-5-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'IT', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'IT', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'linux', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'MX', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'MX', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'linux', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'CN', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'CN', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'FR', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'FR', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - ], - }, - ], - }, - threeTermBucketsWithSplit: { - tables: [ - { - title: 'png: extension: Descending', - name: 'extension: Descending', - key: 'png', - column: 0, - row: 0, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - { - title: 'css: extension: Descending', - name: 'extension: Descending', - key: 'css', - column: 0, - row: 4, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - { - title: 'html: extension: Descending', - name: 'extension: Descending', - key: 'html', - column: 0, - row: 8, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - ], - direction: 'row', - }, - oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative: { - tables: [ - { - columns: [ - { - id: 'col-0-agg_3', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_4', - name: '@timestamp per day', - }, - { - id: 'col-2-agg_1', - name: 'Average bytes', - }, - { - id: 'col-3-agg_2', - name: 'Min @timestamp', - }, - { - id: 'col-4-agg_5', - name: 'Derivative of Count', - }, - { - id: 'col-5-agg_6', - name: 'Last bytes', - }, - ], - rows: [ - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 23, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 203, - 'col-5-agg_6': 39, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 200, - 'col-5-agg_6': 329, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 103, - 'col-5-agg_6': 22, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 153, - 'col-5-agg_6': 93, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 239, - 'col-5-agg_6': 72, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 75, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 10, - 'col-5-agg_6': 11, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 24, - 'col-5-agg_6': 238, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 49, - 'col-5-agg_6': 343, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 100, - 'col-5-agg_6': 837, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 23, - 'col-5-agg_6': 302, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 30, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 1, - 'col-5-agg_6': 43, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 5, - 'col-5-agg_6': 88, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 10, - 'col-5-agg_6': 91, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 43, - 'col-5-agg_6': 534, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 1, - 'col-5-agg_6': 553, - }, - ], - }, - ], - }, -}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.scss b/src/plugins/vis_type_table/public/components/table_vis_app.scss new file mode 100644 index 000000000000..666df3614c17 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_app.scss @@ -0,0 +1,19 @@ +.visTable { + flex-direction: column; + flex-grow: 1 0 0; +} + +.visTable__group { + padding: $euiSizeS; + margin-bottom: $euiSizeL; + + > h3 { + text-align: center; + } +} + +.visTable__groupInColumns { + display: flex; + flex-direction: row; + align-items: flex-start; +} diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.tsx b/src/plugins/vis_type_table/public/components/table_vis_app.tsx new file mode 100644 index 000000000000..2f17a5b1132c --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_app.tsx @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './table_vis_app.scss'; +import React from 'react'; +import classNames from 'classnames'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { I18nProvider } from '@osd/i18n/react'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; + +import { TableContext } from '../table_vis_response_handler'; +import { TableVisConfig } from '../types'; +import { TableVisComponent } from './table_vis_component'; +import { TableVisComponentGroup } from './table_vis_component_group'; + +interface TableVisAppProps { + visData: TableContext; + visConfig: TableVisConfig; + handlers: IInterpreterRenderHandlers; +} + +export const TableVisApp = ({ + services, + visData: { table, tableGroups, direction }, + visConfig, + handlers, +}: TableVisAppProps & { services: CoreStart }) => { + const className = classNames('visTable', { + // eslint-disable-next-line @typescript-eslint/naming-convention + visTable__groupInColumns: direction === 'column', + }); + + return ( + + +
+ {table ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TableVisApp as default }; diff --git a/src/plugins/vis_type_table/public/components/table_vis_component.tsx b/src/plugins/vis_type_table/public/components/table_vis_component.tsx new file mode 100644 index 000000000000..d93044254835 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component.tsx @@ -0,0 +1,156 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo, useRef } from 'react'; +import { orderBy } from 'lodash'; +import { EuiDataGridProps, EuiDataGrid, EuiDataGridSorting, EuiTitle } from '@elastic/eui'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { TableVisConfig, ColumnWidth, SortColumn } from '../types'; +import { getDataGridColumns } from './table_vis_grid_columns'; +import { usePagination } from '../utils'; +import { convertToFormattedData } from '../utils/convert_to_formatted_data'; +import { TableVisControl } from './table_vis_control'; + +interface TableVisComponentProps { + title?: string; + table: Table; + visConfig: TableVisConfig; + handlers: IInterpreterRenderHandlers; +} + +export const TableVisComponent = ({ + title, + table, + visConfig, + handlers, +}: TableVisComponentProps) => { + const { formattedRows: rows, formattedColumns: columns } = convertToFormattedData( + table, + visConfig + ); + + const pagination = usePagination(visConfig, rows.length); + + const sortedRows = useMemo(() => { + const sort = handlers.uiState.get('vis.sortColumn'); + return sort && sort.colIndex !== null && sort.direction + ? orderBy(rows, columns[sort.colIndex]?.id, sort.direction) + : rows; + }, [columns, rows, handlers.uiState]); + + const renderCellValue = useMemo(() => { + return (({ rowIndex, columnId }) => { + const rawContent = sortedRows[rowIndex][columnId]; + const colIndex = columns.findIndex((col) => col.id === columnId); + const column = columns[colIndex]; + // use formatter to format raw content + // this can format date and percentage data + const formattedContent = column.formatter.convert(rawContent, 'text'); + return sortedRows.hasOwnProperty(rowIndex) ? formattedContent || null : null; + }) as EuiDataGridProps['renderCellValue']; + }, [sortedRows, columns]); + + const dataGridColumns = getDataGridColumns( + sortedRows, + columns, + table, + handlers, + handlers.uiState.get('vis.columnsWidth') || [] + ); + + const sortedColumns = useMemo(() => { + const sort: SortColumn = handlers.uiState.get('vis.sortColumn') || {}; + return sort && sort.colIndex !== null && sort.direction + ? [{ id: dataGridColumns[sort.colIndex]?.id, direction: sort.direction }] + : []; + }, [handlers.uiState, dataGridColumns]); + + const onSort = useCallback( + (sortingCols: EuiDataGridSorting['columns'] | []) => { + const nextSortValue = sortingCols[sortingCols.length - 1]; + const nextSort = + sortingCols.length > 0 + ? { + colIndex: dataGridColumns.findIndex((col) => col.id === nextSortValue?.id), + direction: nextSortValue.direction, + } + : []; + handlers.uiState.set('vis.sortColumn', nextSort); + handlers.uiState?.emit('reload'); + return nextSort; + }, + [dataGridColumns, handlers.uiState] + ); + + const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( + ({ columnId, width }) => { + const curState: ColumnWidth[] = handlers.uiState.get('vis.columnsWidth') || []; + const nextState = [...curState]; + const nextColIndex = columns.findIndex((col) => col.id === columnId); + const curColIndex = curState.findIndex((col) => col.colIndex === nextColIndex); + const nextColWidth = { colIndex: nextColIndex, width }; + + // if updated column index is not found, then add it to nextState + // else reset it in nextState + if (curColIndex < 0) nextState.push(nextColWidth); + else nextState[curColIndex] = nextColWidth; + + // update uiState + handlers.uiState.set('vis.columnsWidth', nextState); + handlers.uiState?.emit('reload'); + }, + [columns, handlers.uiState] + ); + + const ariaLabel = title || visConfig.title || 'tableVis'; + + const footerCellValue = visConfig.showTotal + ? // @ts-expect-error + ({ columnId }) => { + const colIndex = columns.findIndex((col) => col.id === columnId); + return columns[colIndex]?.formattedTotal || null; + } + : undefined; + + return ( + <> + {title && ( + +

{title}

+
+ )} + id), + setVisibleColumns: () => {}, + }} + rowCount={rows.length} + renderCellValue={renderCellValue} + sorting={{ columns: sortedColumns, onSort }} + onColumnResize={onColumnResize} + pagination={pagination} + gridStyle={{ + border: 'horizontal', + header: 'underline', + }} + minSizeForControls={1} + renderFooterCellValue={footerCellValue} + toolbarVisibility={{ + showColumnSelector: false, + showSortSelector: false, + showFullScreenSelector: false, + showStyleSelector: false, + additionalControls: ( + + ), + }} + /> + + ); +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx new file mode 100644 index 000000000000..c01f578863d4 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo } from 'react'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { TableGroup } from '../table_vis_response_handler'; +import { TableVisConfig } from '../types'; +import { TableVisComponent } from './table_vis_component'; + +interface TableVisGroupComponentProps { + tableGroups: TableGroup[]; + visConfig: TableVisConfig; + handlers: IInterpreterRenderHandlers; +} + +export const TableVisComponentGroup = memo( + ({ tableGroups, visConfig, handlers }: TableVisGroupComponentProps) => { + return ( + <> + {tableGroups.map(({ tables, title }) => ( +
+ +
+ ))} + + ); + } +); diff --git a/src/plugins/vis_type_table/public/components/table_vis_control.tsx b/src/plugins/vis_type_table/public/components/table_vis_control.tsx new file mode 100644 index 000000000000..a190177df2a7 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_control.tsx @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { exportAsCsv } from '../utils/convert_to_csv_data'; +import { FormattedColumn } from '../types'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; + +interface TableVisControlProps { + filename?: string; + rows: OpenSearchDashboardsDatatableRow[]; + columns: FormattedColumn[]; +} + +export const TableVisControl = (props: TableVisControlProps) => { + const { + services: { uiSettings }, + } = useOpenSearchDashboards(); + const [isPopoverOpen, setPopover] = useState(false); + + return ( + setPopover((open) => !open)}> + Export + + } + isOpen={isPopoverOpen} + closePopover={() => setPopover(false)} + panelPaddingSize="none" + > + exportAsCsv(false, { ...props, uiSettings })} + > + Raw + , + exportAsCsv(true, { ...props, uiSettings })} + > + Formatted + , + ]} + /> + + ); +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx b/src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx new file mode 100644 index 000000000000..3f8d7bcd112e --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { ColumnWidth, FormattedColumn } from '../types'; + +export const getDataGridColumns = ( + rows: OpenSearchDashboardsDatatableRow[], + cols: FormattedColumn[], + table: Table, + handlers: IInterpreterRenderHandlers, + columnsWidth: ColumnWidth[] +) => { + const filterBucket = (rowIndex: number, columnIndex: number, negate: boolean) => { + const foramttedColumnId = cols[columnIndex].id; + const rawColumnIndex = table.columns.findIndex((col) => col.id === foramttedColumnId); + handlers.event({ + name: 'filterBucket', + data: { + data: [ + { + table: { + columns: table.columns, + rows, + }, + row: rowIndex, + column: rawColumnIndex, + }, + ], + negate, + }, + }); + }; + + return cols.map((col, colIndex) => { + const cellActions = col.filterable + ? [ + ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { + const filterValue = rows[rowIndex][columnId]; + const filterContent = col.formatter?.convert(filterValue); + + const filterForValueText = i18n.translate( + 'visTypeTable.tableVisFilter.filterForValue', + { + defaultMessage: 'Filter for value', + } + ); + const filterForValueLabel = i18n.translate( + 'visTypeTable.tableVisFilter.filterForValueLabel', + { + defaultMessage: 'Filter for value: {filterContent}', + values: { + filterContent, + }, + } + ); + + return ( + filterValue != null && ( + { + filterBucket(rowIndex, colIndex, false); + closePopover(); + }} + iconType="plusInCircle" + aria-label={filterForValueLabel} + data-test-subj="filterForValue" + > + {filterForValueText} + + ) + ); + }, + ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { + const filterValue = rows[rowIndex][columnId]; + const filterContent = col.formatter?.convert(filterValue); + + const filterOutValueText = i18n.translate( + 'visTypeTable.tableVisFilter.filterOutValue', + { + defaultMessage: 'Filter out value', + } + ); + const filterOutValueLabel = i18n.translate( + 'visTypeTable.tableVisFilter.filterOutValueLabel', + { + defaultMessage: 'Filter out value: {filterContent}', + values: { + filterContent, + }, + } + ); + + return ( + filterValue != null && ( + { + filterBucket(rowIndex, colIndex, true); + closePopover(); + }} + iconType="minusInCircle" + aria-label={filterOutValueLabel} + data-test-subj="filterOutValue" + > + {filterOutValueText} + + ) + ); + }, + ] + : undefined; + + const initialWidth = columnsWidth.find((c) => c.colIndex === colIndex); + + const dataGridColumn: EuiDataGridColumn = { + id: col.id, + display: col.title, + displayAsText: col.title, + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: { + label: i18n.translate('visTypeTable.tableVisSort.ascSortLabel', { + defaultMessage: 'Sort asc', + }), + }, + showSortDesc: { + label: i18n.translate('visTypeTable.tableVisSort.descSortLabel', { + defaultMessage: 'Sort desc', + }), + }, + }, + cellActions, + }; + if (initialWidth) { + dataGridColumn.initialWidth = initialWidth.width; + } + return dataGridColumn; + }); +}; 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 7f42984d7c02..000000000000 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 '@osd/i18n/angular'; -import { - CoreStart, - IUiSettingsClient, - PluginInitializerContext, -} from 'opensearch-dashboards/public'; -import { - initAngularBootstrap, - PaginateDirectiveProvider, - PaginateControlsDirectiveProvider, - PrivateProvider, - watchMultiDecorator, - OsdAccessibleClickProvider, -} from '../../opensearch_dashboards_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 = 'opensearch-dashboards/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('osdAccessibleClick', OsdAccessibleClickProvider); -} - -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.scss b/src/plugins/vis_type_table/public/index.scss deleted file mode 100644 index d21bf5262602..000000000000 --- a/src/plugins/vis_type_table/public/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Prefix all styles with "tbv" to avoid conflicts. -// Examples -// tbvChart -// tbvChart__legend -// tbvChart__legend--small -// tbvChart__legend-isLoading - -@import "./agg_table/index"; -@import "./paginated_table/index"; -@import "./table_vis"; diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts index dc7e7a16b6ce..af33ed90d650 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -28,7 +28,6 @@ * under the License. */ -import './index.scss'; import { PluginInitializerContext } from 'opensearch-dashboards/public'; import { TableVisPlugin as Plugin } from './plugin'; 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 66275b5c7da8..000000000000 --- 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 3deece36b2c6..000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss +++ /dev/null @@ -1,30 +0,0 @@ -.osdTableCellFilter__hover { - position: relative; - - /** - * 1. Center vertically regardless of row height. - */ - .osdTableCellFilter { - position: absolute; - white-space: nowrap; - right: 0; - top: 50%; /* 1 */ - transform: translateY(-50%); /* 1 */ - display: none; - } - - &:hover { - .osdTableCellFilter { - display: inline; - } - - .osdTableCellFilter__hover-show { - visibility: visible; - } - } -} - -.osdTableCellFilter__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/paginated_table.html b/src/plugins/vis_type_table/public/paginated_table/paginated_table.html deleted file mode 100644 index 83de29a12737..000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.html +++ /dev/null @@ -1,55 +0,0 @@ - -
- - - - - - - - - - - - - -
- - - - - - -
- {{ col.formattedTotal }} -
-
- - - -
-
diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/paginated_table/paginated_table.js deleted file mode 100644 index c97fa6c26587..000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 paginatedTableTemplate from './paginated_table.html'; - -export function PaginatedTable($filter) { - const orderBy = $filter('orderBy'); - - return { - restrict: 'E', - template: paginatedTableTemplate, - transclude: true, - scope: { - table: '=', - rows: '=', - columns: '=', - linkToTop: '=', - perPage: '=?', - sortHandler: '=?', - sort: '=?', - showSelector: '=?', - showTotal: '=', - totalFunc: '=', - filter: '=', - percentageCol: '=', - }, - controllerAs: 'paginatedTable', - controller: function ($scope) { - const self = this; - self.sort = { - columnIndex: null, - direction: null, - }; - - self.sortColumn = function (colIndex, sortDirection = 'asc') { - const col = $scope.columns[colIndex]; - - if (!col) return; - if (col.sortable === false) return; - - if (self.sort.columnIndex === colIndex) { - const directions = { - null: 'asc', - asc: 'desc', - desc: null, - }; - sortDirection = directions[self.sort.direction]; - } - - self.sort.columnIndex = colIndex; - self.sort.direction = sortDirection; - if ($scope.sort) { - _.assign($scope.sort, self.sort); - } - }; - - function valueGetter(row) { - const col = $scope.columns[self.sort.columnIndex]; - let value = row[col.id]; - if (typeof value === 'boolean') value = value ? 0 : 1; - return value; - } - - // Set the sort state if it is set - if ($scope.sort && $scope.sort.columnIndex !== null) { - self.sortColumn($scope.sort.columnIndex, $scope.sort.direction); - } - - function resortRows() { - const newSort = $scope.sort; - if (newSort && !_.isEqual(newSort, self.sort)) { - self.sortColumn(newSort.columnIndex, newSort.direction); - } - - if (!$scope.rows || !$scope.columns) { - $scope.sortedRows = false; - return; - } - - const sort = self.sort; - if (sort.direction == null) { - $scope.sortedRows = $scope.rows.slice(0); - } else { - $scope.sortedRows = orderBy($scope.rows, valueGetter, sort.direction === 'desc'); - } - } - - // update the sortedRows result - $scope.$watchMulti(['rows', 'columns', '[]sort', '[]paginatedTable.sort'], resortRows); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts deleted file mode 100644 index cc86bda46573..000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ /dev/null @@ -1,485 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 { isNumber, times, identity, random } from 'lodash'; -import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; -import 'angular-sanitize'; -import 'angular-mocks'; - -import { getAngularModule } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../core/public/mocks'; - -jest.mock('../../../opensearch_dashboards_legacy/public/angular/angular_config', () => ({ - configureAppAngularModule: () => {}, -})); - -interface Sort { - columnIndex: number; - direction: string; -} - -interface Row { - [key: string]: number | string; -} - -interface Column { - id?: string; - title: string; - formatter?: { - convert?: (val: string) => string; - }; - sortable?: boolean; -} - -interface Table { - columns: Column[]; - rows: Row[]; -} - -interface PaginatedTableScope extends IScope { - table?: Table; - cols?: Column[]; - rows?: Row[]; - perPage?: number; - sort?: Sort; - linkToTop?: boolean; -} - -describe('Table Vis - Paginated table', () => { - let $el: JQuery; - let $rootScope: IRootScopeService; - let $compile: ICompileService; - let $scope: PaginatedTableScope; - const defaultPerPage = 10; - let paginatedTable: any; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule( - 'opensearch-dashboards/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('opensearch-dashboards/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - $scope = $rootScope.$new(); - }) - ); - - afterEach(() => { - $scope.$destroy(); - }); - - const makeData = (colCount: number | Column[], rowCount: number | string[][]) => { - let columns: Column[] = []; - let rows: Row[] = []; - - if (isNumber(colCount)) { - times(colCount, (i) => { - columns.push({ id: `${i}`, title: `column${i}`, formatter: { convert: identity } }); - }); - } else { - columns = colCount.map( - (col, i) => - ({ - id: `${i}`, - title: col.title, - formatter: col.formatter || { convert: identity }, - } as Column) - ); - } - - if (isNumber(rowCount)) { - times(rowCount, (row) => { - const rowItems: Row = {}; - - times(columns.length, (col) => { - rowItems[`${col}`] = `item-${col}-${row}`; - }); - - rows.push(rowItems); - }); - } else { - rows = rowCount.map((row: string[]) => { - const newRow: Row = {}; - row.forEach((v, i) => (newRow[i] = v)); - return newRow; - }); - } - - return { - columns, - rows, - }; - }; - - const renderTable = ( - table: { columns: Column[]; rows: Row[] } | null, - cols: Column[], - rows: Row[], - perPage?: number, - sort?: Sort, - linkToTop?: boolean - ) => { - $scope.table = table || { columns: [], rows: [] }; - $scope.cols = cols || []; - $scope.rows = rows || []; - $scope.perPage = perPage || defaultPerPage; - $scope.sort = sort; - $scope.linkToTop = linkToTop; - - const template = ` - `; - const element = $compile(template)($scope); - $el = $(element); - - $scope.$digest(); - paginatedTable = element.controller('paginatedTable'); - }; - - describe('rendering', () => { - test('should not display without rows', () => { - const cols: Column[] = [ - { - id: 'col-1-1', - title: 'test1', - }, - ]; - const rows: Row[] = []; - - renderTable(null, cols, rows); - expect($el.children().length).toBe(0); - }); - - test('should render columns and rows', () => { - const data = makeData(2, 2); - const cols = data.columns; - const rows = data.rows; - - renderTable(data, cols, rows); - expect($el.children().length).toBe(1); - const tableRows = $el.find('tbody tr'); - - // should contain the row data - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(rows[0][0]); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe(rows[0][1]); - expect(tableRows.eq(1).find('td').eq(0).text()).toBe(rows[1][0]); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe(rows[1][1]); - }); - - test('should paginate rows', () => { - // note: paginate truncates pages, so don't make too many - const rowCount = random(16, 24); - const perPageCount = random(5, 8); - const data = makeData(3, rowCount); - const pageCount = Math.ceil(rowCount / perPageCount); - - renderTable(data, data.columns, data.rows, perPageCount); - const tableRows = $el.find('tbody tr'); - expect(tableRows.length).toBe(perPageCount); - // add 2 for the first and last page links - expect($el.find('paginate-controls button').length).toBe(pageCount + 2); - }); - - test('should not show blank rows on last page', () => { - const rowCount = 7; - const perPageCount = 10; - const data = makeData(3, rowCount); - - renderTable(data, data.columns, data.rows, perPageCount); - const tableRows = $el.find('tbody tr'); - expect(tableRows.length).toBe(rowCount); - }); - - test('should not show link to top when not set', () => { - const data = makeData(5, 5); - renderTable(data, data.columns, data.rows, 10); - - const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]'); - expect(linkToTop.length).toBe(0); - }); - - test('should show link to top when set', () => { - const data = makeData(5, 5); - renderTable(data, data.columns, data.rows, 10, undefined, true); - - const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]'); - expect(linkToTop.length).toBe(1); - }); - }); - - describe('sorting', () => { - let data: Table; - let lastRowIndex: number; - - beforeEach(() => { - data = makeData(3, [ - ['bbbb', 'aaaa', 'zzzz'], - ['cccc', 'cccc', 'aaaa'], - ['zzzz', 'bbbb', 'bbbb'], - ['aaaa', 'zzzz', 'cccc'], - ]); - - lastRowIndex = data.rows.length - 1; - renderTable(data, data.columns, data.rows); - }); - - test('should not sort by default', () => { - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(data.rows[0][0]); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe(data.rows[lastRowIndex][0]); - }); - - test('should do nothing when sorting by invalid column id', () => { - // sortColumn - paginatedTable.sortColumn(999); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('zzzz'); - }); - - test('should do nothing when sorting by non sortable column', () => { - data.columns[0].sortable = false; - - // sortColumn - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('zzzz'); - }); - - test("should set the sort direction to asc when it's not explicitly set", () => { - paginatedTable.sortColumn(1); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(2).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - }); - - test('should allow you to explicitly set the sort direction', () => { - paginatedTable.sortColumn(1, 'desc'); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('zzzz'); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(2).find('td').eq(1).text()).toBe('bbbb'); - }); - - test('should sort ascending on first invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('aaaa'); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('zzzz'); - }); - - test('should sort descending on second invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('zzzz'); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('aaaa'); - }); - - test('should clear sorting on third invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(data.rows[0][0]); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('aaaa'); - }); - - test('should sort new column ascending', () => { - // sort by first column - paginatedTable.sortColumn(0); - $scope.$digest(); - - // sort by second column - paginatedTable.sortColumn(1); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(lastRowIndex).find('td').eq(1).text()).toBe('zzzz'); - }); - }); - - describe('sorting duplicate columns', () => { - let data; - const colText = 'test row'; - - beforeEach(() => { - const cols: Column[] = [{ title: colText }, { title: colText }, { title: colText }]; - const rows = [ - ['bbbb', 'aaaa', 'zzzz'], - ['cccc', 'cccc', 'aaaa'], - ['zzzz', 'bbbb', 'bbbb'], - ['aaaa', 'zzzz', 'cccc'], - ]; - data = makeData(cols, rows); - - renderTable(data, data.columns, data.rows); - }); - - test('should have duplicate column titles', () => { - const columns = $el.find('thead th span'); - columns.each((i, col) => { - expect($(col).text()).toBe(colText); - }); - }); - - test('should handle sorting on columns with the same name', () => { - // sort by the last column - paginatedTable.sortColumn(2); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('aaaa'); - expect(tableRows.eq(1).find('td').eq(2).text()).toBe('bbbb'); - expect(tableRows.eq(2).find('td').eq(2).text()).toBe('cccc'); - expect(tableRows.eq(3).find('td').eq(2).text()).toBe('zzzz'); - }); - - test('should sort correctly between columns', () => { - // sort by the last column - paginatedTable.sortColumn(2); - $scope.$digest(); - - let tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('aaaa'); - - // sort by the first column - paginatedTable.sortColumn(0); - $scope.$digest(); - - tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('zzzz'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('cccc'); - - expect(tableRows.eq(1).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(2).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(3).find('td').eq(0).text()).toBe('zzzz'); - }); - - test('should not sort duplicate columns', () => { - paginatedTable.sortColumn(1); - $scope.$digest(); - - const sorters = $el.find('thead th i'); - expect(sorters.eq(0).hasClass('fa-sort')).toBe(true); - expect(sorters.eq(1).hasClass('fa-sort')).toBe(false); - expect(sorters.eq(2).hasClass('fa-sort')).toBe(true); - }); - }); - - describe('object rows', () => { - let cols: Column[]; - let rows: any; - - beforeEach(() => { - cols = [ - { - title: 'object test', - id: '0', - formatter: { - convert: (val) => { - return val === 'zzz' ? '

hello

' : val; - }, - }, - }, - ]; - rows = [['aaaa'], ['zzz'], ['bbbb']]; - renderTable({ columns: cols, rows }, cols, rows); - }); - - test('should append object markup', () => { - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('h1').length).toBe(0); - expect(tableRows.eq(1).find('h1').length).toBe(1); - expect(tableRows.eq(2).find('h1').length).toBe(0); - }); - - test('should sort using object value', () => { - paginatedTable.sortColumn(0); - $scope.$digest(); - let tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('h1').length).toBe(0); - expect(tableRows.eq(1).find('h1').length).toBe(0); - // html row should be the last row - expect(tableRows.eq(2).find('h1').length).toBe(1); - - paginatedTable.sortColumn(0); - $scope.$digest(); - tableRows = $el.find('tbody tr'); - // html row should be the first row - expect(tableRows.eq(0).find('h1').length).toBe(1); - expect(tableRows.eq(1).find('h1').length).toBe(0); - expect(tableRows.eq(2).find('h1').length).toBe(0); - }); - }); -}); 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 5ed2de5de176..000000000000 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 OsdRows($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('osdTableCellFilter__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.osdRows, attr.osdRowsMin], 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/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html deleted file mode 100644 index a9185884dae5..000000000000 --- 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/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 8c01fee8841e..c5648f75d4fb 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -1,31 +1,6 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 { @@ -40,45 +15,44 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { createTableVisFn } from './table_vis_fn'; import { getTableVisTypeDefinition } from './table_vis_type'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setOpenSearchDashboardsLegacy } from './services'; -import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public'; +import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; +import { getTableVisRenderer } from './table_vis_renderer'; /** @internal */ -export interface TablePluginSetupDependencies { +export interface TableVisPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; } /** @internal */ -export interface TablePluginStartDependencies { +export interface TableVisPluginStartDependencies { data: DataPublicPluginStart; - opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart; } +const setupTableVis = async ( + core: CoreSetup, + { expressions, visualizations }: TableVisPluginSetupDependencies +) => { + const [coreStart] = await core.getStartServices(); + expressions.registerFunction(createTableVisFn); + expressions.registerRenderer(getTableVisRenderer(coreStart)); + visualizations.createBaseVisualization(getTableVisTypeDefinition()); +}; + /** @internal */ -export class TableVisPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - createBaseVisualization: any; +export class TableVisPlugin implements Plugin { + initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( - core: CoreSetup, - { expressions, visualizations }: TablePluginSetupDependencies - ) { - expressions.registerFunction(createTableVisFn); - visualizations.createBaseVisualization( - getTableVisTypeDefinition(core, this.initializerContext) - ); + public async setup(core: CoreSetup, dependencies: TableVisPluginSetupDependencies) { + setupTableVis(core, dependencies); } - public start( - core: CoreStart, - { data, opensearchDashboardsLegacy }: TablePluginStartDependencies - ) { + public start(core: CoreStart, { data }: TableVisPluginStartDependencies) { setFormatService(data.fieldFormats); - setOpenSearchDashboardsLegacy(opensearchDashboardsLegacy); } } diff --git a/src/plugins/vis_type_table/public/services.ts b/src/plugins/vis_type_table/public/services.ts index 4fb56f6bfbdb..65c9b4ff5fc5 100644 --- a/src/plugins/vis_type_table/public/services.ts +++ b/src/plugins/vis_type_table/public/services.ts @@ -30,12 +30,7 @@ import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public'; export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('table data.fieldFormats'); - -export const [getOpenSearchDashboardsLegacy, setOpenSearchDashboardsLegacy] = createGetterSetter< - OpenSearchDashboardsLegacyStart ->('table opensearchDashboardsLegacy'); 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 169b53390fe3..000000000000 --- 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 9fa71534903d..000000000000 --- a/src/plugins/vis_type_table/public/table_vis_controller.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 (opensearchResponse) - * - one of the view options changes (vis.params) - */ - $scope.$watch('renderComplete', function () { - let tableGroups = ($scope.tableGroups = null); - let hasSomeRows = ($scope.hasSomeRows = null); - - if ($scope.opensearchResponse) { - tableGroups = $scope.opensearchResponse; - - 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 db12e2b51426..000000000000 --- a/src/plugins/vis_type_table/public/table_vis_controller.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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'; - -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { getTableVisTypeDefinition } from './table_vis_type'; -import { Vis } from '../../visualizations/public'; -import { stubFields } from '../../data/public/stubs'; -import { tableVisResponseHandler } from './table_vis_response_handler'; -import { coreMock } from '../../../core/public/mocks'; -import { IAggConfig, search } from '../../data/public'; -import { getStubIndexPattern } from '../../data/public/test_utils'; -// TODO: remove linting disable -import { searchServiceMock } from '../../data/public/search/mocks'; - -const { createAggConfigs } = searchServiceMock.createStartContract().aggs; - -const { tabifyAggResponse } = search; - -jest.mock('../../opensearch_dashboards_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( - 'opensearch-dashboards/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('opensearch-dashboards/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - tableAggResponse = tableVisResponseHandler; - }) - ); - - beforeEach(() => { - stubIndexPattern = getStubIndexPattern( - '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', 'OsdTableVisController') - .attr('ng-init', 'newScope(this)'); - - $compile($el)($rootScope); - } - - // put a response into the controller - function attachOpenSearchResponseToScope(resp: object) { - $rootScope.opensearchResponse = resp; - $rootScope.$apply(); - } - - // remove the response from the controller - function removeOpenSearchResponseFromScope() { - delete $rootScope.opensearchResponse; - $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(); - - attachOpenSearchResponseToScope(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); - - attachOpenSearchResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - removeOpenSearchResponseFromScope(); - - 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); - - attachOpenSearchResponseToScope(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 = []; - - attachOpenSearchResponseToScope(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 as Function)(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 as Function)(vis)).toEqual(false); - }); -}); diff --git a/src/plugins/vis_type_table/public/table_vis_fn.test.ts b/src/plugins/vis_type_table/public/table_vis_fn.test.ts deleted file mode 100644 index f7723456b757..000000000000 --- a/src/plugins/vis_type_table/public/table_vis_fn.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 { createTableVisFn } from './table_vis_fn'; -import { tableVisResponseHandler } from './table_vis_response_handler'; - -import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; - -jest.mock('./table_vis_response_handler', () => ({ - tableVisResponseHandler: jest.fn().mockReturnValue({ - tables: [{ columns: [], rows: [] }], - }), -})); - -describe('interpreter/functions#table', () => { - const fn = functionWrapper(createTableVisFn()); - const context = { - type: 'opensearch_dashboards_datatable', - rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - }; - const visConfig = { - title: 'My Chart title', - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - dimensions: { - metrics: [ - { - accessor: 0, - format: { - id: 'number', - }, - params: {}, - aggType: 'count', - }, - ], - buckets: [], - }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('returns an object with the correct structure', async () => { - const actual = await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined); - expect(actual).toMatchSnapshot(); - }); - - it('calls response handler with correct values', async () => { - await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined); - expect(tableVisResponseHandler).toHaveBeenCalledTimes(1); - expect(tableVisResponseHandler).toHaveBeenCalledWith(context, visConfig.dimensions); - }); -}); diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index daf76580b59f..3e3bd38dba47 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -1,31 +1,6 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 '@osd/i18n'; @@ -35,7 +10,7 @@ import { OpenSearchDashboardsDatatable, Render, } from '../../expressions/public'; -import { VisRenderValue } from '../../visualizations/public'; +import { TableVisConfig } from './types'; export type Input = OpenSearchDashboardsDatatable; @@ -43,17 +18,20 @@ interface Arguments { visConfig: string | null; } -interface RenderValue extends VisRenderValue { +export interface TableVisRenderValue { visData: TableContext; visType: 'table'; + visConfig: TableVisConfig; } -export const createTableVisFn = (): ExpressionFunctionDefinition< +export type TableVisExpressionFunctionDefinition = ExpressionFunctionDefinition< 'opensearch_dashboards_table', Input, Arguments, - Render -> => ({ + Render +>; + +export const createTableVisFn = (): TableVisExpressionFunctionDefinition => ({ name: 'opensearch_dashboards_table', type: 'render', inputTypes: ['opensearch_dashboards_datatable'], @@ -69,18 +47,18 @@ export const createTableVisFn = (): ExpressionFunctionDefinition< }, fn(input, args) { const visConfig = args.visConfig && JSON.parse(args.visConfig); - const convertedData = tableVisResponseHandler(input, visConfig.dimensions); + const convertedData = tableVisResponseHandler(input, visConfig); return { type: 'render', - as: 'visualization', + as: 'table_vis', value: { visData: convertedData, visType: 'table', visConfig, - params: { - listenOnChange: true, - }, + }, + params: { + listenOnChange: true, }, }; }, 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 49eed3494f92..000000000000 --- a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 { OsdAggTable } from './agg_table/agg_table'; -// @ts-ignore -import { OsdAggTableGroup } from './agg_table/agg_table_group'; -// @ts-ignore -import { OsdRows } from './paginated_table/rows'; -// @ts-ignore -import { PaginatedTable } from './paginated_table/paginated_table'; - -/** @internal */ -export const initTableVisLegacyModule = (angularIns: IModule): void => { - angularIns - .controller('OsdTableVisController', TableVisController) - .directive('osdAggTable', OsdAggTable) - .directive('osdAggTableGroup', OsdAggTableGroup) - .directive('osdRows', OsdRows) - .directive('paginatedTable', PaginatedTable); -}; diff --git a/src/plugins/vis_type_table/public/table_vis_renderer.tsx b/src/plugins/vis_type_table/public/table_vis_renderer.tsx new file mode 100644 index 000000000000..a717d38e04b2 --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_renderer.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { CoreStart } from 'opensearch-dashboards/public'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { TableVisRenderValue } from './table_vis_fn'; +import { TableVisApp } from './components/table_vis_app'; + +export const getTableVisRenderer: ( + core: CoreStart +) => ExpressionRenderDefinition = (core) => ({ + name: 'table_vis', + displayName: 'table visualization', + reuseDomNode: true, + render: async (domNode, { visData, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const showNoResult = visData.table + ? visData.table.rows.length === 0 + : visData.tableGroups?.length === 0; + + render( + + + , + domNode + ); + }, +}); 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 78b2306e744b..b1d41edfff8b 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 @@ -28,19 +28,17 @@ * under the License. */ -import { Required } from '@osd/utility-types'; - import { getFormatService } from './services'; -import { Input } from './table_vis_fn'; +import { OpenSearchDashboardsDatatable } from '../../expressions/public'; +import { TableVisConfig } from './types'; -export interface TableContext { - tables: Array; - direction?: 'row' | 'column'; +export interface Table { + columns: OpenSearchDashboardsDatatable['columns']; + rows: OpenSearchDashboardsDatatable['rows']; } export interface TableGroup { - $parent: TableContext; - table: Input; + table: OpenSearchDashboardsDatatable; tables: Table[]; title: string; name: string; @@ -49,61 +47,66 @@ export interface TableGroup { row: number; } -export interface Table { - $parent?: TableGroup; - columns: Input['columns']; - rows: Input['rows']; +export interface TableContext { + table?: Table; + tableGroups: TableGroup[]; + direction?: 'row' | 'column'; } -export function tableVisResponseHandler(table: Input, dimensions: any): TableContext { - const converted: TableContext = { - tables: [], - }; +export function tableVisResponseHandler( + input: OpenSearchDashboardsDatatable, + config: TableVisConfig +): TableContext { + let table: Table | undefined; + const tableGroups: TableGroup[] = []; + let direction: TableContext['direction']; - const split = dimensions.splitColumn || dimensions.splitRow; + const split = config.splitColumn || config.splitRow; if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; + direction = config.splitRow ? 'row' : 'column'; const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); - const splitColumn = table.columns[splitColumnIndex]; - const splitMap = {}; + const splitColumn = input.columns[splitColumnIndex]; + const splitMap: { [key: string]: number } = {}; 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, + const tableGroup: TableGroup = { 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); + tableGroups.push(tableGroup); } const tableIndex = (splitMap as any)[splitValue]; - (converted.tables[tableIndex] as any).tables[0].rows.push(row); + (tableGroups[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 { + table, + tableGroups, + direction, + }; } 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 df1495a3d06b..0c27e7a8af0b 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -28,91 +28,78 @@ * under the License. */ -import { CoreSetup, PluginInitializerContext } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; +import { toExpressionAst } from './to_ast'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; import { TableOptions } from './components/table_vis_options_lazy'; -import { getTableVisualizationControllerClass } from './vis_controller'; -import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; -export function getTableVisTypeDefinition( - core: CoreSetup, - context: PluginInitializerContext -): BaseVisTypeOptions { - return { - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: getTableVisualizationControllerClass(core, context), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; +export const getTableVisTypeDefinition = (): BaseVisTypeOptions => ({ + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + toExpressionAst, + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', }, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', - }, - template: tableVisTemplate, - }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, - }; -} + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + responseHandler: tableVisResponseHandler, + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, + hierarchicalData: (vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, +}); diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts new file mode 100644 index 000000000000..61752c971d00 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getVisSchemas, Vis } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { TableVisExpressionFunctionDefinition } from './table_vis_fn'; +import { OpenSearchaggsExpressionFunctionDefinition } from '../../data/common/search/expressions'; + +export const toExpressionAst = (vis: Vis, params: any) => { + const opensearchaggs = buildExpressionFunction( + 'opensearchaggs', + { + index: vis.data.indexPattern!.id!, + metricsAtAllLevels: vis.isHierarchical(), + partialRows: vis.params.showPartialRows || false, + aggConfigs: JSON.stringify(vis.data.aggs!.aggs), + includeFormatHints: false, + } + ); + + const schemas = getVisSchemas(vis, params); + // manul slice to remove duplicate metrics + const metrics = + schemas.bucket && vis.params.showPartialRows && !vis.params.showMetricsAtAllLevels + ? schemas.metric.slice(-1 * (schemas.metric.length / schemas.bucket.length)) + : schemas.metric; + + const tableData = { + title: vis.title, + metrics, + buckets: schemas.bucket || [], + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const tableConfig = { + perPage: vis.params.perPage, + percentageCol: vis.params.percentageCol, + showPartialRows: vis.params.showPartialRows, + showMetricsAtAllLevels: vis.params.showMetricsAtAllLevels, + showTotal: vis.params.showTotal, + totalFunc: vis.params.totalFunc, + }; + + const visConfig = { + ...tableConfig, + ...tableData, + }; + + const tableVis = buildExpressionFunction( + 'opensearch_dashboards_table', + { + visConfig: JSON.stringify(visConfig), + } + ); + + const ast = buildExpression([opensearchaggs, tableVis]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index c780ef3b5db9..6355044556ab 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -28,7 +28,8 @@ * under the License. */ -import { SchemaConfig } from '../../visualizations/public'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { IFieldFormat } from 'src/plugins/data/public'; export enum AggTypes { SUM = 'sum', @@ -38,22 +39,39 @@ export enum AggTypes { COUNT = 'count', } -export interface Dimensions { - buckets: SchemaConfig[]; +export interface TableVisConfig extends TableVisParams { + title: string; metrics: SchemaConfig[]; + buckets: SchemaConfig[]; + splitRow?: SchemaConfig[]; + splitColumn?: SchemaConfig[]; } export interface TableVisParams { - type: 'table'; perPage: number | ''; showPartialRows: boolean; showMetricsAtAllLevels: boolean; - sort: { - columnIndex: number | null; - direction: string | null; - }; showTotal: boolean; totalFunc: AggTypes; percentageCol: string; - dimensions: Dimensions; +} + +export interface FormattedColumn { + id: string; + title: string; + formatter: IFieldFormat; + filterable: boolean; + formattedTotal?: string | number; + sumTotal?: number; + total?: number; +} + +export interface ColumnWidth { + colIndex: number; + width: number; +} + +export interface SortColumn { + colIndex: number; + direction: 'asc' | 'desc'; } diff --git a/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts new file mode 100644 index 000000000000..2c37df1aa3d5 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * 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 { CoreStart } from 'opensearch-dashboards/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; +import { OpenSearchDashboardsDatatable } from '../../../expressions/public'; +import { FormattedColumn } from '../types'; + +const nonAlphaNumRE = /[^a-zA-Z0-9]/; +const allDoubleQuoteRE = /"/g; + +interface CSVDataProps { + filename?: string; + rows: OpenSearchDashboardsDatatable['rows']; + columns: FormattedColumn[]; + uiSettings: CoreStart['uiSettings']; +} + +const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { + const separator = uiSettings.get(CSV_SEPARATOR_SETTING); + const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING); + + function escape(val: any) { + if (!formatted && isObject(val)) val = val.valueOf(); + val = String(val); + if (quoteValues && nonAlphaNumRE.test(val)) { + val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; + } + return val; + } + + let csvRows: string[][] = []; + 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((col) => escape(col.title))); + + return csvRows.map((row) => row.join(separator) + '\r\n').join(''); +}; + +export const exportAsCsv = function (formatted: boolean, csvData: CSVDataProps) { + const csv = new Blob([toCsv(formatted, csvData)], { type: 'text/csv;charset=utf-8' }); + const type = formatted ? 'formatted' : 'raw'; + if (csvData.filename) saveAs(csv, `${csvData.filename}-${type}.csv`); + else saveAs(csv, `unsaved-${type}.csv`); +}; diff --git a/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts new file mode 100644 index 000000000000..0b0c31eaec8e --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts @@ -0,0 +1,179 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * 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 '@osd/i18n'; +import { chain } from 'lodash'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { AggTypes, TableVisConfig } from '../types'; +import { getFormatService } from '../services'; +import { FormattedColumn } from '../types'; + +function insert(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) + */ +function addPercentageCol( + 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 = insert(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 }; +} + +export interface FormattedDataProps { + formattedRows: OpenSearchDashboardsDatatableRow[]; + formattedColumns: FormattedColumn[]; +} + +export const convertToFormattedData = ( + table: Table, + visConfig: TableVisConfig +): FormattedDataProps => { + const { buckets, metrics } = visConfig; + let formattedRows: OpenSearchDashboardsDatatableRow[] = table.rows; + let formattedColumns: FormattedColumn[] = table.columns + .map(function (col, i) { + const isBucket = buckets.find((bucket) => bucket.accessor === i); + const dimension = isBucket || metrics.find((metric) => metric.accessor === i); + + if (!dimension) return undefined; + + const formatter = getFormatService().deserialize(dimension.format); + + const formattedColumn: FormattedColumn = { + id: col.id, + title: col.name, + formatter, + filterable: !!isBucket, + }; + + const isDate = dimension?.format?.id === 'date' || dimension?.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + if (allowsNumericalAggregations || isDate || visConfig.totalFunc === AggTypes.COUNT) { + const sum = 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); + + formattedColumn.sumTotal = sum; + switch (visConfig.totalFunc) { + case AggTypes.SUM: { + if (!isDate) { + formattedColumn.formattedTotal = formatter?.convert(sum); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case AggTypes.AVG: { + if (!isDate) { + const total = sum / table.rows.length; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + } + break; + } + case AggTypes.MIN: { + const total = chain(table.rows).map(col.id).min().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + break; + } + case AggTypes.MAX: { + const total = chain(table.rows).map(col.id).max().value() as number; + 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 is FormattedColumn => !!column); + + if (visConfig.percentageCol) { + const insertAtIndex = formattedColumns.findIndex( + (col) => col.title === visConfig.percentageCol + ); + + // column to show percentage for was removed + if (insertAtIndex < 0) return { formattedRows, formattedColumns }; + + const { cols, rows } = addPercentageCol( + formattedColumns, + visConfig.percentageCol, + table.rows, + insertAtIndex + ); + formattedRows = rows; + formattedColumns = cols; + } + return { formattedRows, formattedColumns }; +}; 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 000000000000..eb9be554ae05 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './convert_to_formatted_data'; +export * from './use_pagination'; diff --git a/src/plugins/vis_type_table/public/utils/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use_pagination.ts new file mode 100644 index 000000000000..41bb31d34086 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use_pagination.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { TableVisConfig } from '../types'; + +export const usePagination = (visConfig: TableVisConfig, nRow: number) => { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: visConfig.perPage || 0, + }); + const onChangeItemsPerPage = useCallback( + (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), + [setPagination] + ); + const onChangePage = useCallback((pageIndex) => setPagination((p) => ({ ...p, pageIndex })), [ + setPagination, + ]); + + useEffect(() => { + const perPage = visConfig.perPage || 0; + const maxiPageIndex = Math.ceil(nRow / perPage) - 1; + setPagination((p) => ({ + pageIndex: p.pageIndex > maxiPageIndex ? maxiPageIndex : p.pageIndex, + pageSize: perPage, + })); + }, [nRow, visConfig.perPage]); + + return useMemo( + () => ({ + ...pagination, + onChangeItemsPerPage, + onChangePage, + }), + [pagination, onChangeItemsPerPage, onChangePage] + ); +}; 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 aa7ffb05110a..000000000000 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * 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 'opensearch-dashboards/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 { getOpenSearchDashboardsLegacy } from './services'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; - -const innerAngularName = 'opensearch-dashboards/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(opensearchResponse: object, visParams: VisParams): Promise { - getOpenSearchDashboardsLegacy().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.opensearchResponse = opensearchResponse; - - 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/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts index 5ec9b8b91c03..e0c27a37755c 100644 --- a/src/plugins/vis_type_table/server/index.ts +++ b/src/plugins/vis_type_table/server/index.ts @@ -34,9 +34,6 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { schema: configSchema, - deprecations: ({ renameFromRoot }) => [ - renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'), - ], }; export const plugin = () => ({ diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index b3b4dc5e09b1..6a16b5caa15c 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -12,15 +12,15 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"opensearch_dashboards_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"visTable visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"opensearch_dashboards_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":2,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"visTable visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":2,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"opensearch_dashboards_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"visTable visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"opensearch_dashboards_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"visTable visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"opensearch_dashboards_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"visTable visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[]}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 90721f66c4af..5f240f82602c 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -128,84 +128,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - describe('handles table function', () => { - it('without splits or buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - split_row: [1, 2], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits and buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - split_row: [2, 4], - bucket: [3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=true', () => { - const params = { - showMetricsAtAllLevels: true, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=false', () => { - const params = { - showMetricsAtAllLevels: false, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 7a28ae7ac394..1cbb3bc38879 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -278,13 +278,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param)); return `tsvb ${paramsArray.join(' ')}`; }, - table: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.table(schemas, params), - }; - return `opensearch_dashboards_table ${prepareJson('visConfig', visConfig)}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, @@ -309,26 +302,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }; const buildVisConfig: BuildVisConfigFunction = { - table: (schemas, visParams = {}) => { - const visConfig = {} as any; - const metrics = schemas.metric; - const buckets = schemas.bucket || []; - visConfig.dimensions = { - metrics, - buckets, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - - if (visParams.showMetricsAtAllLevels === false && visParams.showPartialRows === true) { - // Handle case where user wants to see partial rows but not metrics at all levels. - // This requires calculating how many metrics will come back in the tabified response, - // and removing all metrics from the dimensions except the last set. - const metricsPerBucket = metrics.length / buckets.length; - visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); - } - return visConfig; - }, region_map: (schemas) => { const visConfig = {} as any; visConfig.metric = schemas.metric[0];