From 1feef2a5c9e3e32362427337c6c59c5d33625686 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Wed, 28 Dec 2022 12:26:53 -0800 Subject: [PATCH] [Table Visualization] Replace table visualization with React and DataGrid (#2863) * [Table Visualization] Replace table visualization with React and DataGrid In this PR, we add back functions to make new table usage to be consistent with the replaced one. * total function * percentage column * filter in/out Meanwhile, we also add back server. Functional tests are removed. We will add new functional test in opensearch-dashboards-functional-test repo. We also clean out some legacy codes. Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2855 Signed-off-by: Anan Zhuang * add some data-test-subj and fix PR comments Signed-off-by: Anan Zhuang * Fix PR comments and add unit tests Signed-off-by: Anan Zhuang * remove listenOnChange Signed-off-by: Anan Zhuang Signed-off-by: Anan Zhuang Signed-off-by: Arpit Bandejiya --- CHANGELOG.md | 1 + .../data/common/field_formats/field_format.ts | 6 + .../table/components/table_viz_options.tsx | 4 +- .../visualizations/table/to_expression.ts | 4 +- src/plugins/vis_type_table/README.md | 36 +- .../vis_type_table/opensearch_dashboards.json | 4 +- .../__snapshots__/table_vis_fn.test.ts.snap | 5 +- .../public/__snapshots__/to_ast.test.ts.snap | 115 +++ .../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 | 0 .../public/components/table_vis_app.tsx | 4 +- .../public/components/table_vis_component.tsx | 14 +- .../components/table_vis_component_group.tsx | 0 .../public/components/table_vis_control.tsx | 2 +- .../components/table_vis_grid_columns.tsx | 148 ++++ .../public/components/table_vis_options.tsx | 4 +- .../public/get_inner_angular.ts | 117 --- src/plugins/vis_type_table/public/index.scss | 10 - src/plugins/vis_type_table/public/index.ts | 28 +- .../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 | 76 +- src/plugins/vis_type_table/public/services.ts | 32 +- .../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 | 2 +- .../vis_type_table/public/table_vis_fn.ts | 47 +- .../public/table_vis_legacy_module.ts | 52 -- .../public/table_vis_renderer.tsx | 0 .../public/table_vis_response_handler.ts | 69 +- .../vis_type_table/public/table_vis_type.ts | 143 ++-- .../vis_type_table/public/to_ast.test.ts | 58 ++ src/plugins/vis_type_table/public/to_ast.ts | 65 ++ src/plugins/vis_type_table/public/types.ts | 43 +- .../public/utils/convert_to_csv_data.ts | 0 .../public/utils/convert_to_formatted_data.ts | 179 ++++ .../public/utils/index.ts | 0 .../public/utils/use_pagination.ts | 0 .../vis_type_table/public/vis_controller.ts | 135 --- src/plugins/vis_type_table_new/README.md | 1 - .../opensearch_dashboards.json | 16 - .../components/table_vis_grid_columns.tsx | 148 ---- .../vis_type_table_new/public/index.ts | 13 - .../vis_type_table_new/public/plugin.ts | 36 - .../vis_type_table_new/public/services.ts | 11 - .../vis_type_table_new/public/table_vis_fn.ts | 65 -- .../public/table_vis_response_handler.ts | 112 --- .../vis_type_table_new/public/types.ts | 70 -- .../public/utils/convert_to_formatted_data.ts | 68 -- .../components/inspector_data_grid.tsx | 12 +- .../__snapshots__/build_pipeline.test.ts.snap | 10 - .../public/legacy/build_pipeline.test.ts | 78 -- .../public/legacy/build_pipeline.ts | 27 - .../apps/dashboard/dashboard_filtering.js | 12 - .../apps/dashboard/dashboard_grid.js | 2 +- .../apps/dashboard/embeddable_rendering.js | 6 +- .../apps/dashboard/url_field_formatter.ts | 8 - test/functional/apps/visualize/_data_table.js | 485 ----------- .../visualize/_data_table_nontimeindex.js | 170 ---- .../_data_table_notimeindex_filters.ts | 101 --- .../apps/visualize/_embedding_chart.js | 187 ---- .../visualize/_histogram_request_start.js | 95 --- .../apps/visualize/_linked_saved_searches.ts | 138 --- test/functional/apps/visualize/index.ts | 6 - .../opensearch_dashboards/data.json.gz | Bin 21378 -> 20707 bytes .../functional/page_objects/dashboard_page.ts | 1 - .../page_objects/visualize_chart_page.ts | 72 -- .../functional/page_objects/visualize_page.ts | 4 - .../services/dashboard/expectations.ts | 11 - test/functional/services/index.ts | 2 - test/functional/services/table.ts | 73 -- 86 files changed, 809 insertions(+), 5874 deletions(-) create mode 100644 src/plugins/vis_type_table/public/__snapshots__/to_ast.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 rename src/plugins/{vis_type_table_new => vis_type_table}/public/components/table_vis_app.scss (100%) rename src/plugins/{vis_type_table_new => vis_type_table}/public/components/table_vis_app.tsx (94%) rename src/plugins/{vis_type_table_new => vis_type_table}/public/components/table_vis_component.tsx (92%) rename src/plugins/{vis_type_table_new => vis_type_table}/public/components/table_vis_component_group.tsx (100%) rename src/plugins/{vis_type_table_new => vis_type_table}/public/components/table_vis_control.tsx (93%) 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_legacy_module.ts rename src/plugins/{vis_type_table_new => vis_type_table}/public/table_vis_renderer.tsx (100%) create mode 100644 src/plugins/vis_type_table/public/to_ast.test.ts create mode 100644 src/plugins/vis_type_table/public/to_ast.ts rename src/plugins/{vis_type_table_new => vis_type_table}/public/utils/convert_to_csv_data.ts (100%) create mode 100644 src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts rename src/plugins/{vis_type_table_new => vis_type_table}/public/utils/index.ts (100%) rename src/plugins/{vis_type_table_new => vis_type_table}/public/utils/use_pagination.ts (100%) delete mode 100644 src/plugins/vis_type_table/public/vis_controller.ts delete mode 100644 src/plugins/vis_type_table_new/README.md delete mode 100644 src/plugins/vis_type_table_new/opensearch_dashboards.json delete mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx delete mode 100644 src/plugins/vis_type_table_new/public/index.ts delete mode 100644 src/plugins/vis_type_table_new/public/plugin.ts delete mode 100644 src/plugins/vis_type_table_new/public/services.ts delete mode 100644 src/plugins/vis_type_table_new/public/table_vis_fn.ts delete mode 100644 src/plugins/vis_type_table_new/public/table_vis_response_handler.ts delete mode 100644 src/plugins/vis_type_table_new/public/types.ts delete mode 100644 src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts delete mode 100644 test/functional/apps/visualize/_data_table.js delete mode 100644 test/functional/apps/visualize/_data_table_nontimeindex.js delete mode 100644 test/functional/apps/visualize/_data_table_notimeindex_filters.ts delete mode 100644 test/functional/apps/visualize/_embedding_chart.js delete mode 100644 test/functional/apps/visualize/_histogram_request_start.js delete mode 100644 test/functional/apps/visualize/_linked_saved_searches.ts delete mode 100644 test/functional/services/table.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bf8a778c351..13cb0174d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add yarn opensearch arg to setup plugin dependencies ([#2544](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2544)) - [Multi DataSource] Test the connection to an external data source when creating or updating ([#2973](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2973)) - [Doc] Add current plugin persistence implementation readme ([#3081](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3081)) +- [Table Visualization] Refactor table visualization using React and DataGrid component ([#2863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2863)) ### 🐛 Bug Fixes diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 8bff51d1f16..c5c945f1b89 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_builder/public/visualizations/table/components/table_viz_options.tsx b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx index 8c934fff8da..a77a0811e60 100644 --- a/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx +++ b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx @@ -3,14 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { get } from 'lodash'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import produce from 'immer'; import { Draft } from 'immer'; import { EuiIconTip } from '@elastic/eui'; -import { search } from '../../../../../data/public'; import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; import { useTypedDispatch, diff --git a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts index 212c93248d4..bbec4c1cc7e 100644 --- a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts @@ -4,7 +4,7 @@ */ import { SchemaConfig } from '../../../../visualizations/public'; -import { TableVisExpressionFunctionDefinition } from '../../../../vis_type_table_new/public'; +import { TableVisExpressionFunctionDefinition } from '../../../../vis_type_table/public'; import { AggConfigs, IAggConfig } from '../../../../data/common'; import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; import { RenderState } from '../../application/utils/state_management'; @@ -120,7 +120,7 @@ export const toExpression = async ({ style: styleState, visualization }: TableRo }; const tableVis = buildExpressionFunction( - 'opensearch_dashboards_table_new', + 'opensearch_dashboards_table', { visConfig: JSON.stringify(visConfig), } diff --git a/src/plugins/vis_type_table/README.md b/src/plugins/vis_type_table/README.md index cf37e133ed1..42304c39e93 100644 --- a/src/plugins/vis_type_table/README.md +++ b/src/plugins/vis_type_table/README.md @@ -1 +1,35 @@ -Contains the data table visualization, that allows presenting data in a simple table format. \ No newline at end of file +# Data Table + +This is an OpenSearch Dashboards plugin that is used to visualize data and aggregations in tabular format. + +## Create Data Table +To create a data table in OpenSearch Dashboards, first select `Visualize` from the navigation menu. Then click `Create Visualization` and choose `Data Table` as the visualization type. + +## Select Metrics + +### Metrics Aggregation +At the `Metrics`, select the metric aggregation type from the menu and configure it accordingly. You could also add multiple metrics and each metrics is a separate column in table visualization. + +### Buckets Aggregation +At the `Buckets`, configure the columns to be displayed in the table visualization. +- `Split Rows` is used when you want to divide one row into more based on some category. It splits one row into multiple and add columns based on the categories you choose to split. For example, if you split the data based on gender then you want to know more on each gender's clothing preference. You could click `Split Rows` and input `clothing.category` in terms. Each gender's data is now split across to multiple rows based on a new added column `clothing.category`. +- `Split Table` splits the table into separate tables for the aggregation you choose. It is similar to `Split Rows`, but this time each row becomes a single table with aggregation columns arranged horizontally or vertically. + +## Select Options +In the `Options` tab, you can configure more options. +- `Max rows per page` is the maximum number of rows displayed per page. +- `Show metrics for every bucket/level` adds metrics aggregation to every column. +- `Show partial rows` will include data with missing columns. +- `Show total` calculates the selected metrics per column and displays the result at the bottom. Warning - depending on your columns and the total aggregation function selected, this may generate statistically invalid results. For example, avoid summing or averaging averages. +- `Percentage column` adds one percentage column based on the chosen metrics aggregation. + +## Example + +Below is an example of creating a table visualization using sample ecommerce data. + +- Create a new data table visualization and set a relative time 15 weeks ago. +- Compute the count of ecommerce: Choose `Count` in Metrics Aggregation. +- Split the rows on the top 5 of `manufacturer.keyword` ordered by `Metric:Count` in descending and add a label "manufacturer". +- Split the table in rows on the top 5 of `geoip.city_name` ordered by `Metric:Count` in ascending order. +- Click the `Save` button on the top left and save the visualization as "Top manufacturers by count per city". +- Choose a table and click the download icon to download the table. diff --git a/src/plugins/vis_type_table/opensearch_dashboards.json b/src/plugins/vis_type_table/opensearch_dashboards.json index e2f050534c1..ba0ebb1bc4c 100644 --- a/src/plugins/vis_type_table/opensearch_dashboards.json +++ b/src/plugins/vis_type_table/opensearch_dashboards.json @@ -6,11 +6,11 @@ "requiredPlugins": [ "expressions", "visualizations", - "data", - "opensearchDashboardsLegacy" + "data" ], "requiredBundles": [ "opensearchDashboardsUtils", + "opensearchDashboardsReact", "share", "charts", "visDefaultEditor" 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 index dc6571de969..a32609c2e3d 100644 --- 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 @@ -2,12 +2,9 @@ exports[`interpreter/functions#table returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "table_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "dimensions": Object { "buckets": Array [], diff --git a/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 00000000000..296f33f90c7 --- /dev/null +++ b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis toExpressionAst function with customized params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "opensearchaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"perPage\\":5,\\"percentageCol\\":\\"Count\\",\\"showPartialRows\\":false,\\"showMetricsAtAllLevels\\":false,\\"showTotal\\":true,\\"totalFunc\\":\\"min\\",\\"metrics\\":[],\\"buckets\\":[]}", + ], + }, + "function": "opensearch_dashboards_table", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`table vis toExpressionAst function with default params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "opensearchaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"perPage\\":10,\\"percentageCol\\":\\"\\",\\"showPartialRows\\":false,\\"showMetricsAtAllLevels\\":false,\\"showTotal\\":false,\\"totalFunc\\":\\"sum\\",\\"metrics\\":[],\\"buckets\\":[]}", + ], + }, + "function": "opensearch_dashboards_table", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`table vis toExpressionAst function without params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "opensearchaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"metrics\\":[],\\"buckets\\":[]}", + ], + }, + "function": "opensearch_dashboards_table", + "type": "function", + }, + ], + "type": "expression", +} +`; 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 ea4b4d0d1c9..00000000000 --- 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 156db063c8d..00000000000 --- 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 ed94e844912..00000000000 --- 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 8e8aafa83fd..00000000000 --- 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 a00aea27869..00000000000 --- 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 14d0c7fe795..00000000000 --- 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 2dcf7f125f6..00000000000 --- 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 133b20800a1..00000000000 --- 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 18a48e92211..00000000000 --- 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 ce344d5c48b..00000000000 --- 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_new/public/components/table_vis_app.scss b/src/plugins/vis_type_table/public/components/table_vis_app.scss similarity index 100% rename from src/plugins/vis_type_table_new/public/components/table_vis_app.scss rename to src/plugins/vis_type_table/public/components/table_vis_app.scss diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_app.tsx b/src/plugins/vis_type_table/public/components/table_vis_app.tsx similarity index 94% rename from src/plugins/vis_type_table_new/public/components/table_vis_app.tsx rename to src/plugins/vis_type_table/public/components/table_vis_app.tsx index 7958b218762..af10500a1a9 100644 --- a/src/plugins/vis_type_table_new/public/components/table_vis_app.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_app.tsx @@ -11,7 +11,7 @@ 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, SortColumn, ColumnWidth, TableUiState } from '../types'; +import { TableVisConfig, ColumnSort, ColumnWidth, TableUiState } from '../types'; import { TableVisComponent } from './table_vis_component'; import { TableVisComponentGroup } from './table_vis_component_group'; @@ -40,7 +40,7 @@ export const TableVisApp = ({ // TODO: remove duplicate sort and width state // Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2704#issuecomment-1299380818 - const [sort, setSort] = useState({ colIndex: null, direction: null }); + const [sort, setSort] = useState({ colIndex: null, direction: null }); const [width, setWidth] = useState([]); const tableUiState: TableUiState = { sort, setSort, width, setWidth }; diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx b/src/plugins/vis_type_table/public/components/table_vis_component.tsx similarity index 92% rename from src/plugins/vis_type_table_new/public/components/table_vis_component.tsx rename to src/plugins/vis_type_table/public/components/table_vis_component.tsx index cbd0331b419..4576e3420e2 100644 --- a/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_component.tsx @@ -10,7 +10,7 @@ import { EuiDataGridProps, EuiDataGrid, EuiDataGridSorting, EuiTitle } from '@el import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { Table } from '../table_vis_response_handler'; -import { TableVisConfig, ColumnWidth, SortColumn, TableUiState } from '../types'; +import { TableVisConfig, ColumnWidth, ColumnSort, TableUiState } from '../types'; import { getDataGridColumns } from './table_vis_grid_columns'; import { usePagination } from '../utils'; import { convertToFormattedData } from '../utils/convert_to_formatted_data'; @@ -50,8 +50,7 @@ export const TableVisComponent = ({ return (({ rowIndex, columnId }) => { const rawContent = sortedRows[rowIndex][columnId]; const colIndex = columns.findIndex((col) => col.id === columnId); - const column = columns[colIndex]; - const htmlContent = column.formatter.convert(rawContent, 'html'); + const htmlContent = columns[colIndex].formatter.convert(rawContent, 'html'); const formattedContent = ( /* * Justification for dangerouslySetInnerHTML: @@ -80,7 +79,7 @@ export const TableVisComponent = ({ const onSort = useCallback( (sortingCols: EuiDataGridSorting['columns'] | []) => { const nextSortValue = sortingCols[sortingCols.length - 1]; - const nextSort: SortColumn = + const nextSort: ColumnSort = sortingCols.length > 0 ? { colIndex: dataGridColumns.findIndex((col) => col.id === nextSortValue?.id), @@ -117,6 +116,12 @@ export const TableVisComponent = ({ const ariaLabel = title || visConfig.title || 'tableVis'; + const footerCellValue = visConfig.showTotal + ? ({ columnId }: { columnId: any }) => { + return columns.find((col) => col.id === columnId)?.formattedTotal || null; + } + : undefined; + return ( <> {title && ( @@ -141,6 +146,7 @@ export const TableVisComponent = ({ header: 'underline', }} minSizeForControls={1} + renderFooterCellValue={footerCellValue} toolbarVisibility={{ showColumnSelector: false, showSortSelector: false, diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_component_group.tsx b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx similarity index 100% rename from src/plugins/vis_type_table_new/public/components/table_vis_component_group.tsx rename to src/plugins/vis_type_table/public/components/table_vis_component_group.tsx diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_control.tsx b/src/plugins/vis_type_table/public/components/table_vis_control.tsx similarity index 93% rename from src/plugins/vis_type_table_new/public/components/table_vis_control.tsx rename to src/plugins/vis_type_table/public/components/table_vis_control.tsx index 26b51c9cc85..1e11610b0c2 100644 --- a/src/plugins/vis_type_table_new/public/components/table_vis_control.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_control.tsx @@ -9,7 +9,7 @@ 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'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; interface TableVisControlProps { filename?: string; 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 00000000000..77d496a1eb4 --- /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, + event: IInterpreterRenderHandlers['event'], + columnWidths: ColumnWidth[] +) => { + const filterBucket = (rowIndex: number, columnIndex: number, negate: boolean) => { + const foramttedColumnId = cols[columnIndex].id; + const rawColumnIndex = table.columns.findIndex((col) => col.id === foramttedColumnId); + 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 = columnWidths.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/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index e4cebe69fb4..ff99972c83a 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -132,6 +132,7 @@ function TableOptions({ paramName="showTotal" value={stateParams.showTotal} setValue={setValue} + data-test-subj="showTotal" /> ); 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 7f42984d7c0..00000000000 --- 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 d21bf526260..00000000000 --- 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 dc7e7a16b6c..b5ab796210f 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -1,34 +1,8 @@ /* + * 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 './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 66275b5c7da..00000000000 --- 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 3deece36b2c..00000000000 --- 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 83de29a1273..00000000000 --- 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 c97fa6c2658..00000000000 --- 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 cc86bda4657..00000000000 --- 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 5ed2de5de17..00000000000 --- 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 a9185884dae..00000000000 --- 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 8c01fee8841..0582ebbedeb 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,38 @@ 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'; - -/** @internal */ -export interface TablePluginSetupDependencies { +import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; +import { getTableVisRenderer } from './table_vis_renderer'; +export interface TableVisPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; } - -/** @internal */ -export interface TablePluginStartDependencies { +export interface TableVisPluginStartDependencies { data: DataPublicPluginStart; - opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart; } -/** @internal */ -export class TableVisPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - createBaseVisualization: any; - - constructor(initializerContext: PluginInitializerContext) { +const setupTableVis = async ( + core: CoreSetup, + { expressions, visualizations }: TableVisPluginSetupDependencies +) => { + const [coreStart] = await core.getStartServices(); + expressions.registerFunction(createTableVisFn); + expressions.registerRenderer(getTableVisRenderer(coreStart)); + visualizations.createBaseVisualization(getTableVisTypeDefinition()); +}; +export class TableVisPlugin implements Plugin { + 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 4fb56f6bfbd..f8ca4b57430 100644 --- a/src/plugins/vis_type_table/public/services.ts +++ b/src/plugins/vis_type_table/public/services.ts @@ -1,41 +1,11 @@ /* + * 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 { 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 169b53390fe..00000000000 --- 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 9fa71534903..00000000000 --- 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 db12e2b5142..00000000000 --- 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 index f7723456b75..b4a4ca4776b 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.test.ts @@ -84,6 +84,6 @@ describe('interpreter/functions#table', () => { 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); + expect(tableVisResponseHandler).toHaveBeenCalledWith(context, visConfig); }); }); 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 daf76580b59..4f0fb2c0ba1 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,15 @@ 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, - }, }, }; }, 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 49eed3494f9..00000000000 --- 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_new/public/table_vis_renderer.tsx b/src/plugins/vis_type_table/public/table_vis_renderer.tsx similarity index 100% rename from src/plugins/vis_type_table_new/public/table_vis_renderer.tsx rename to src/plugins/vis_type_table/public/table_vis_renderer.tsx 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 78b2306e744..b1d41edfff8 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 df1495a3d06..0c27e7a8af0 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.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts new file mode 100644 index 00000000000..be2741e8de4 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { toExpressionAst } from './to_ast'; +import { Vis } from '../../visualizations/public'; + +describe('table vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + isHierarchical: () => false, + type: {}, + params: {}, + data: { + indexPattern: { id: '123' } as any, + aggs: { + getResponseAggs: () => [], + aggs: [], + } as any, + }, + } as any; + }); + + it('without params', () => { + vis.params = { table: {} }; + const actual = toExpressionAst(vis, {}); + expect(actual).toMatchSnapshot(); + }); + + it('with default params', () => { + vis.params = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }; + const actual = toExpressionAst(vis, {}); + expect(actual).toMatchSnapshot(); + }); + + it('with customized params', () => { + vis.params = { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: true, + totalFunc: 'min', + percentageCol: 'Count', + }; + const actual = toExpressionAst(vis, {}); + expect(actual).toMatchSnapshot(); + }); +}); 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 00000000000..3753c35cfc2 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -0,0 +1,65 @@ +/* + * 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); + // if customer selects showPartialRows without showMetricsAtAllLevels, + // we need to remove the duplicated metrics. + // First, we need to calculate the required number of metrics + // Then, we return one copy of the required metrics from metric array + 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 c780ef3b5db..814a86f5ac6 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,46 @@ 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 ColumnSort { + colIndex?: number; + direction?: 'asc' | 'desc'; +} + +export interface TableUiState { + sort: ColumnSort; + setSort: (sort: ColumnSort) => void; + width: ColumnWidth[]; + setWidth: (columnWidths: ColumnWidth[]) => void; } diff --git a/src/plugins/vis_type_table_new/public/utils/convert_to_csv_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts similarity index 100% rename from src/plugins/vis_type_table_new/public/utils/convert_to_csv_data.ts rename to src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts 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 00000000000..2ab67e3b0a6 --- /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 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_new/public/utils/index.ts b/src/plugins/vis_type_table/public/utils/index.ts similarity index 100% rename from src/plugins/vis_type_table_new/public/utils/index.ts rename to src/plugins/vis_type_table/public/utils/index.ts diff --git a/src/plugins/vis_type_table_new/public/utils/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use_pagination.ts similarity index 100% rename from src/plugins/vis_type_table_new/public/utils/use_pagination.ts rename to src/plugins/vis_type_table/public/utils/use_pagination.ts 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 aa7ffb05110..00000000000 --- 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_new/README.md b/src/plugins/vis_type_table_new/README.md deleted file mode 100644 index 06299ed963a..00000000000 --- a/src/plugins/vis_type_table_new/README.md +++ /dev/null @@ -1 +0,0 @@ -Contains the data table visualization, that allows presenting data using a Datagrid component. diff --git a/src/plugins/vis_type_table_new/opensearch_dashboards.json b/src/plugins/vis_type_table_new/opensearch_dashboards.json deleted file mode 100644 index 598ca7581b8..00000000000 --- a/src/plugins/vis_type_table_new/opensearch_dashboards.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "visTypeTableNew", - "version": "opensearchDashboards", - "server": false, - "ui": true, - "requiredPlugins": [ - "expressions", - "visualizations", - "data" - ], - "requiredBundles": [ - "opensearchDashboardsUtils", - "opensearchDashboardsReact", - "share" - ] -} diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx deleted file mode 100644 index ba204ea6ae3..00000000000 --- a/src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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, - event: IInterpreterRenderHandlers['event'], - columnsWidth: ColumnWidth[] -) => { - const filterBucket = (rowIndex: number, columnIndex: number, negate: boolean) => { - const foramttedColumnId = cols[columnIndex].id; - const rawColumnIndex = table.columns.findIndex((col) => col.id === foramttedColumnId); - 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( - // 'visTypeTableNew.tableVisFilter.filterForValue', - // { - // defaultMessage: 'Filter for value', - // } - // ); - // const filterForValueLabel = i18n.translate( - // 'visTypeTableNew.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( - // 'visTypeTableNew.tableVisFilter.filterOutValue', - // { - // defaultMessage: 'Filter out value', - // } - // ); - // const filterOutValueLabel = i18n.translate( - // 'visTypeTableNew.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('visTypeTableNew.tableVisSort.ascSortLabel', { - defaultMessage: 'Sort asc', - }), - }, - showSortDesc: { - label: i18n.translate('visTypeTableNew.tableVisSort.descSortLabel', { - defaultMessage: 'Sort desc', - }), - }, - }, - // cellActions, - }; - if (initialWidth) { - dataGridColumn.initialWidth = initialWidth.width; - } - return dataGridColumn; - }); -}; diff --git a/src/plugins/vis_type_table_new/public/index.ts b/src/plugins/vis_type_table_new/public/index.ts deleted file mode 100644 index 4ed30b71eea..00000000000 --- a/src/plugins/vis_type_table_new/public/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// import { PluginInitializerContext } from 'opensearch-dashboards/public'; -import { TableVisPlugin as Plugin } from './plugin'; - -export function plugin() { - return new Plugin(); -} -/* Public Types */ -export { TableVisExpressionFunctionDefinition } from './table_vis_fn'; diff --git a/src/plugins/vis_type_table_new/public/plugin.ts b/src/plugins/vis_type_table_new/public/plugin.ts deleted file mode 100644 index 9cc96c3e989..00000000000 --- a/src/plugins/vis_type_table_new/public/plugin.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; - -import { createTableVisFn } from './table_vis_fn'; -import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService } from './services'; -import { getTableVisRenderer } from './table_vis_renderer'; - -export interface TableVisPluginSetupDependencies { - expressions: ReturnType; -} - -export interface TableVisPluginStartDependencies { - data: DataPublicPluginStart; -} - -const setupTableVis = async (core: CoreSetup, { expressions }: TableVisPluginSetupDependencies) => { - const [coreStart] = await core.getStartServices(); - expressions.registerFunction(createTableVisFn); - expressions.registerRenderer(getTableVisRenderer(coreStart)); -}; - -export class TableVisPlugin implements Plugin { - public async setup(core: CoreSetup, dependencies: TableVisPluginSetupDependencies) { - setupTableVis(core, dependencies); - } - - public start(core: CoreStart, { data }: TableVisPluginStartDependencies) { - setFormatService(data.fieldFormats); - } -} diff --git a/src/plugins/vis_type_table_new/public/services.ts b/src/plugins/vis_type_table_new/public/services.ts deleted file mode 100644 index f8ca4b57430..00000000000 --- a/src/plugins/vis_type_table_new/public/services.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; -import { DataPublicPluginStart } from '../../data/public'; - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('table data.fieldFormats'); diff --git a/src/plugins/vis_type_table_new/public/table_vis_fn.ts b/src/plugins/vis_type_table_new/public/table_vis_fn.ts deleted file mode 100644 index ec9eafc344a..00000000000 --- a/src/plugins/vis_type_table_new/public/table_vis_fn.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { i18n } from '@osd/i18n'; -import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; -import { - ExpressionFunctionDefinition, - OpenSearchDashboardsDatatable, - Render, -} from '../../expressions/public'; -import { TableVisConfig } from './types'; - -export type Input = OpenSearchDashboardsDatatable; - -interface Arguments { - visConfig: string | null; -} - -export interface TableVisRenderValue { - visData: TableContext; - visType: 'table'; - visConfig: TableVisConfig; -} - -export type TableVisExpressionFunctionDefinition = ExpressionFunctionDefinition< - 'opensearch_dashboards_table_new', - Input, - Arguments, - Render ->; - -export const createTableVisFn = (): TableVisExpressionFunctionDefinition => ({ - name: 'opensearch_dashboards_table_new', - type: 'render', - inputTypes: ['opensearch_dashboards_datatable'], - help: i18n.translate('visTypeTableNew.function.help', { - defaultMessage: 'Table visualization', - }), - args: { - visConfig: { - types: ['string', 'null'], - default: '"{}"', - help: '', - }, - }, - fn(input, args) { - const visConfig = args.visConfig && JSON.parse(args.visConfig); - const convertedData = tableVisResponseHandler(input, visConfig); - - return { - type: 'render', - as: 'table_vis', - value: { - visData: convertedData, - visType: 'table', - visConfig, - }, - params: { - listenOnChange: true, - }, - }; - }, -}); diff --git a/src/plugins/vis_type_table_new/public/table_vis_response_handler.ts b/src/plugins/vis_type_table_new/public/table_vis_response_handler.ts deleted file mode 100644 index b1d41edfff8..00000000000 --- a/src/plugins/vis_type_table_new/public/table_vis_response_handler.ts +++ /dev/null @@ -1,112 +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 { getFormatService } from './services'; -import { OpenSearchDashboardsDatatable } from '../../expressions/public'; -import { TableVisConfig } from './types'; - -export interface Table { - columns: OpenSearchDashboardsDatatable['columns']; - rows: OpenSearchDashboardsDatatable['rows']; -} - -export interface TableGroup { - table: OpenSearchDashboardsDatatable; - tables: Table[]; - title: string; - name: string; - key: any; - column: number; - row: number; -} - -export interface TableContext { - table?: Table; - tableGroups: TableGroup[]; - direction?: 'row' | 'column'; -} - -export function tableVisResponseHandler( - input: OpenSearchDashboardsDatatable, - config: TableVisConfig -): TableContext { - let table: Table | undefined; - const tableGroups: TableGroup[] = []; - let direction: TableContext['direction']; - - const split = config.splitColumn || config.splitRow; - - if (split) { - direction = config.splitRow ? 'row' : 'column'; - const splitColumnIndex = split[0].accessor; - const splitColumnFormatter = getFormatService().deserialize(split[0].format); - const splitColumn = input.columns[splitColumnIndex]; - const splitMap: { [key: string]: number } = {}; - let splitIndex = 0; - - input.rows.forEach((row, rowIndex) => { - const splitValue: any = row[splitColumn.id]; - - if (!splitMap.hasOwnProperty(splitValue as any)) { - (splitMap as any)[splitValue] = splitIndex++; - const tableGroup: TableGroup = { - title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, - name: splitColumn.name, - key: splitValue, - column: splitColumnIndex, - row: rowIndex, - table: input, - tables: [], - }; - - tableGroup.tables.push({ - columns: input.columns, - rows: [], - }); - - tableGroups.push(tableGroup); - } - - const tableIndex = (splitMap as any)[splitValue]; - (tableGroups[tableIndex] as any).tables[0].rows.push(row); - }); - } else { - table = { - columns: input.columns, - rows: input.rows, - }; - } - - return { - table, - tableGroups, - direction, - }; -} diff --git a/src/plugins/vis_type_table_new/public/types.ts b/src/plugins/vis_type_table_new/public/types.ts deleted file mode 100644 index 0c5a9f9955e..00000000000 --- a/src/plugins/vis_type_table_new/public/types.ts +++ /dev/null @@ -1,70 +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 { SchemaConfig } from 'src/plugins/visualizations/public'; -import { IFieldFormat } from 'src/plugins/data/public'; - -export interface TableVisConfig extends TableVisParams { - title: string; - metrics: SchemaConfig[]; - buckets: SchemaConfig[]; - splitRow?: SchemaConfig[]; - splitColumn?: SchemaConfig[]; -} - -export interface TableVisParams { - perPage: number | ''; - showPartialRows: boolean; - showMetricsAtAllLevels: boolean; -} - -export interface FormattedColumn { - id: string; - title: string; - formatter: IFieldFormat; - filterable: boolean; -} - -export interface ColumnWidth { - colIndex: number; - width: number; -} - -export interface SortColumn { - colIndex: number | null; - direction: 'asc' | 'desc' | null; -} - -export interface TableUiState { - sort: SortColumn; - setSort: (sort: SortColumn) => void; - width: ColumnWidth[]; - setWidth: (columnsWidth: ColumnWidth[]) => void; -} diff --git a/src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts b/src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts deleted file mode 100644 index cd997dfe5d5..00000000000 --- a/src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts +++ /dev/null @@ -1,68 +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 { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; -import { Table } from '../table_vis_response_handler'; -import { TableVisConfig } from '../types'; -import { getFormatService } from '../services'; -import { FormattedColumn } from '../types'; -export interface FormattedDataProps { - formattedRows: OpenSearchDashboardsDatatableRow[]; - formattedColumns: FormattedColumn[]; -} - -export const convertToFormattedData = ( - table: Table, - visConfig: TableVisConfig -): FormattedDataProps => { - const { buckets, metrics } = visConfig; - const formattedRows: OpenSearchDashboardsDatatableRow[] = table.rows; - const 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, - }; - - return formattedColumn; - }) - .filter((column): column is FormattedColumn => !!column); - - return { formattedRows, formattedColumns }; -}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx index bf0eb584fc5..e5cd924e6e4 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx @@ -110,26 +110,26 @@ export const InspectorDataGrid = ({ columns, data, dataGridAriaLabel }: Inspecto }, [gridData, pagination]); // Resize - const [columnsWidth, setColumnsWidth] = useState>({}); + const [columnWidths, setColumnWidths] = useState>({}); const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( ({ columnId, width }) => { - setColumnsWidth({ - ...columnsWidth, + setColumnWidths({ + ...columnWidths, [columnId]: width, }); }, - [columnsWidth] + [columnWidths] ); return ( { - if (columnsWidth[column.id]) { + if (columnWidths[column.id]) { return { ...column, - initialWidth: columnsWidth[column.id], + initialWidth: columnWidths[column.id], }; } return column; 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 b3b4dc5e09b..5712f358f7d 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,16 +12,6 @@ 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=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 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 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 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 tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 90721f66c4a..5f240f82602 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 7a28ae7ac39..1cbb3bc3887 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]; diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index fd05f5b134e..deb2ae39924 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -98,10 +98,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.seriesElementCount(0); }); - it('data tables are filtered', async () => { - await dashboardExpect.dataTableRowCount(0); - }); - it('goal and guages are filtered', async () => { await dashboardExpect.goalAndGuageLabelsExist(['0', '0%']); }); @@ -159,10 +155,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.seriesElementCount(0); }); - it('data tables are filtered', async () => { - await dashboardExpect.dataTableRowCount(0); - }); - it('goal and guages are filtered', async () => { await dashboardExpect.goalAndGuageLabelsExist(['0', '0%']); }); @@ -212,10 +204,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.seriesElementCount(3); }); - it('data tables', async () => { - await dashboardExpect.dataTableRowCount(10); - }); - it('goal and guages', async () => { await dashboardExpect.goalAndGuageLabelsExist(['39.958%', '7,544']); }); diff --git a/test/functional/apps/dashboard/dashboard_grid.js b/test/functional/apps/dashboard/dashboard_grid.js index 3e68f33f204..0da0c58573a 100644 --- a/test/functional/apps/dashboard/dashboard_grid.js +++ b/test/functional/apps/dashboard/dashboard_grid.js @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }) { describe('move panel', () => { // Specific test after https://github.com/elastic/kibana/issues/14764 fix it('Can move panel from bottom to top row', async () => { - const lastVisTitle = 'Rendering Test: datatable'; + const lastVisTitle = 'Rendering Test: pie'; const panelTitleBeforeMove = await dashboardPanelActions.getPanelHeading(lastVisTitle); const position1 = await panelTitleBeforeMove.getPosition(); await browser.dragAndDrop( diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index b11955a1e24..5cacc85cb0e 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -67,7 +67,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.markdownWithValuesExists(["I'm a markdown!"]); await dashboardExpect.vegaTextsExist(['5,000']); await dashboardExpect.goalAndGuageLabelsExist(['62.925%', '55.625%', '11.915 GB']); - await dashboardExpect.dataTableRowCount(5); await dashboardExpect.tagCloudWithValuesFound(['CN', 'IN', 'US', 'BR', 'ID']); // TODO add test for 'region map viz' // TODO add test for 'tsvb gauge' viz @@ -90,7 +89,6 @@ export default function ({ getService, getPageObjects }) { const expectNoDataRenders = async () => { await pieChart.expectPieSliceCount(0); await dashboardExpect.seriesElementCount(0); - await dashboardExpect.dataTableRowCount(0); await dashboardExpect.savedSearchRowCount(0); await dashboardExpect.inputControlItemCount(5); await dashboardExpect.metricValuesExist(['0']); @@ -146,7 +144,7 @@ export default function ({ getService, getPageObjects }) { visNames.push(await dashboardAddPanel.addVisualization('Filter Bytes Test: vega')); await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardExpect.visualizationsArePresent(visNames); - expect(visNames.length).to.be.equal(27); + expect(visNames.length).to.be.equal(26); await PageObjects.dashboard.waitForRenderComplete(); }); @@ -157,7 +155,7 @@ export default function ({ getService, getPageObjects }) { await dashboardAddPanel.closeAddPanel(); await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardExpect.visualizationsArePresent(visAndSearchNames); - expect(visAndSearchNames.length).to.be.equal(28); + expect(visAndSearchNames.length).to.be.equal(27); await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.dashboard.saveDashboard('embeddable rendering test', { diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts index 0f7d8172063..798adeb99bf 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -74,14 +74,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await settings.controlChangeSave(); }); - it('applied on dashboard', async () => { - await common.navigateToApp('dashboard'); - await dashboard.loadSavedDashboard('dashboard with everything'); - await dashboard.waitForRenderComplete(); - const fieldLink = await visChart.getFieldLinkInVisTable(`${fieldName}: Descending`, 1); - await clickFieldAndCheckUrl(fieldLink); - }); - it('applied on discover', async () => { await common.navigateToApp('discover'); await timePicker.setAbsoluteRange( diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js deleted file mode 100644 index fde30412bdc..00000000000 --- a/test/functional/apps/visualize/_data_table.js +++ /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 expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const log = getService('log'); - const inspector = getService('inspector'); - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['visualize', 'timePicker', 'visEditor', 'visChart']); - - describe('data table', function indexPatternCreation() { - const vizName1 = 'Visualization DataTable'; - - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = Split rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should allow applying changed params', async () => { - await PageObjects.visEditor.setInterval('1', { type: 'numeric', append: true }); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('20001'); - const isApplyButtonEnabled = await PageObjects.visEditor.isApplyEnabled(); - expect(isApplyButtonEnabled).to.be(true); - }); - - it('should allow reseting changed params', async () => { - await PageObjects.visEditor.clickReset(); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('2000'); - }); - - it('should be able to save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); - }); - - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); - }); - - it('should show correct data', function () { - const expectedChartData = [ - ['0B', '2,088'], - ['1.953KB', '2,748'], - ['3.906KB', '2,707'], - ['5.859KB', '2,876'], - ['7.813KB', '2,863'], - ['9.766KB', '147'], - ['11.719KB', '148'], - ['13.672KB', '129'], - ['15.625KB', '161'], - ['17.578KB', '137'], - ]; - - return retry.try(async function () { - await inspector.open(); - await inspector.expectTableData(expectedChartData); - await inspector.close(); - }); - }); - - it('should show percentage columns', async () => { - async function expectValidTableData() { - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '≥ 0B and < 1,000B', - '1,351 64.703%', - '≥ 1,000B and < 1.953KB', - '737 35.297%', - ]); - } - - // load a plain table - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Range'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.clickGo(); - await PageObjects.visEditor.clickOptionsTab(); - await PageObjects.visEditor.setSelectByOptionText( - 'datatableVisualizationPercentageCol', - 'Count' - ); - await PageObjects.visEditor.clickGo(); - - await expectValidTableData(); - - // check that it works after a save and reload - const SAVE_NAME = 'viz w/ percents'; - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(SAVE_NAME); - - await PageObjects.visualize.loadSavedVisualization(SAVE_NAME); - await PageObjects.visChart.waitForVisualization(); - - await expectValidTableData(); - - // check that it works after selecting a column that's deleted - await PageObjects.visEditor.clickDataTab(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average', 'metrics'); - await PageObjects.visEditor.selectField('bytes', 'metrics'); - await PageObjects.visEditor.removeDimension(1); - await PageObjects.visEditor.clickGo(); - await PageObjects.visEditor.clickOptionsTab(); - - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '≥ 0B and < 1,000B', - '344.094B', - '≥ 1,000B and < 1.953KB', - '1.697KB', - ]); - }); - - it('should show correct data when using average pipeline aggregation', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets'); - await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); - }); - - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); - - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); - - it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - - it('should correctly filter for pinned filters', async () => { - await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - - it('should show correct data for a data table with top hits', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickMetricEditor(); - await PageObjects.visEditor.selectAggregation('Top Hit', 'metrics'); - await PageObjects.visEditor.selectField('agent.raw', 'metrics'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data); - expect(data.length).to.be.greaterThan(0); - }); - - it('should show correct data for a data table with range agg', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Range'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '≥ 0B and < 1,000B', - '1,351', - '≥ 1,000B and < 1.953KB', - '737', - ]); - }); - - describe('otherBucket', () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.clickGo(); - - await PageObjects.visEditor.toggleOtherBucket(); - await PageObjects.visEditor.toggleMissingBucket(); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data', async () => { - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109'], - ['css', '2,159'], - ['Other', '2,736'], - ]); - }); - - it('should apply correct filter', async () => { - await PageObjects.visChart.filterOnTableCell(1, 3); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['png', '1,373'], - ['gif', '918'], - ['Other', '445'], - ]); - }); - }); - - describe('metricsOnAllLevels', () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.dest'); - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data without showMetricsAtAllLevels', async () => { - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', 'CN', '1,718'], - ['jpg', 'IN', '1,511'], - ['jpg', 'US', '770'], - ['jpg', 'ID', '314'], - ['jpg', 'PK', '244'], - ['css', 'CN', '422'], - ['css', 'IN', '346'], - ['css', 'US', '189'], - ['css', 'ID', '68'], - ['css', 'BR', '58'], - ]); - }); - - it('should show correct data without showMetricsAtAllLevels even if showPartialRows is selected', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showPartialRows', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', 'CN', '1,718'], - ['jpg', 'IN', '1,511'], - ['jpg', 'US', '770'], - ['jpg', 'ID', '314'], - ['jpg', 'PK', '244'], - ['css', 'CN', '422'], - ['css', 'IN', '346'], - ['css', 'US', '189'], - ['css', 'ID', '68'], - ['css', 'BR', '58'], - ]); - }); - - it('should show metrics on each level', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109', 'CN', '1,718'], - ['jpg', '9,109', 'IN', '1,511'], - ['jpg', '9,109', 'US', '770'], - ['jpg', '9,109', 'ID', '314'], - ['jpg', '9,109', 'PK', '244'], - ['css', '2,159', 'CN', '422'], - ['css', '2,159', 'IN', '346'], - ['css', '2,159', 'US', '189'], - ['css', '2,159', 'ID', '68'], - ['css', '2,159', 'BR', '58'], - ]); - }); - - it('should show metrics other than count on each level', async () => { - await PageObjects.visEditor.clickDataTab(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average', 'metrics'); - await PageObjects.visEditor.selectField('bytes', 'metrics'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109', '5.469KB', 'CN', '1,718', '5.477KB'], - ['jpg', '9,109', '5.469KB', 'IN', '1,511', '5.456KB'], - ['jpg', '9,109', '5.469KB', 'US', '770', '5.371KB'], - ['jpg', '9,109', '5.469KB', 'ID', '314', '5.424KB'], - ['jpg', '9,109', '5.469KB', 'PK', '244', '5.41KB'], - ['css', '2,159', '5.566KB', 'CN', '422', '5.712KB'], - ['css', '2,159', '5.566KB', 'IN', '346', '5.754KB'], - ['css', '2,159', '5.566KB', 'US', '189', '5.333KB'], - ['css', '2,159', '5.566KB', 'ID', '68', '4.82KB'], - ['css', '2,159', '5.566KB', 'BR', '58', '5.915KB'], - ]); - }); - }); - - describe('split tables', () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split table'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.dest'); - await PageObjects.visEditor.setSize(3, 3); - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.src'); - await PageObjects.visEditor.setSize(3, 4); - await PageObjects.visEditor.toggleOpenEditor(4, 'false'); - await PageObjects.visEditor.clickGo(); - }); - - it('should have a splitted table', async () => { - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - [ - ['CN', 'CN', '330'], - ['CN', 'IN', '274'], - ['CN', 'US', '140'], - ['IN', 'CN', '286'], - ['IN', 'IN', '281'], - ['IN', 'US', '133'], - ['US', 'CN', '135'], - ['US', 'IN', '134'], - ['US', 'US', '52'], - ], - [ - ['CN', 'CN', '90'], - ['CN', 'IN', '84'], - ['CN', 'US', '27'], - ['IN', 'CN', '69'], - ['IN', 'IN', '58'], - ['IN', 'US', '34'], - ['US', 'IN', '36'], - ['US', 'CN', '29'], - ['US', 'US', '13'], - ], - ]); - }); - - it('should show metrics for split bucket when using showMetricsAtAllLevels', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - [ - ['CN', '1,718', 'CN', '330'], - ['CN', '1,718', 'IN', '274'], - ['CN', '1,718', 'US', '140'], - ['IN', '1,511', 'CN', '286'], - ['IN', '1,511', 'IN', '281'], - ['IN', '1,511', 'US', '133'], - ['US', '770', 'CN', '135'], - ['US', '770', 'IN', '134'], - ['US', '770', 'US', '52'], - ], - [ - ['CN', '422', 'CN', '90'], - ['CN', '422', 'IN', '84'], - ['CN', '422', 'US', '27'], - ['IN', '346', 'CN', '69'], - ['IN', '346', 'IN', '58'], - ['IN', '346', 'US', '34'], - ['US', '189', 'IN', '36'], - ['US', '189', 'CN', '29'], - ['US', '189', 'US', '13'], - ], - ]); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js deleted file mode 100644 index 37ad4c2a9eb..00000000000 --- a/test/functional/apps/visualize/_data_table_nontimeindex.js +++ /dev/null @@ -1,170 +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 expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const log = getService('log'); - const inspector = getService('inspector'); - const retry = getService('retry'); - const filterBar = getService('filterBar'); - const renderable = getService('renderable'); - const PageObjects = getPageObjects(['visualize', 'visEditor', 'header', 'visChart']); - - describe('data table with index without time filter', function indexPatternCreation() { - const vizName1 = 'Visualization DataTable without time filter'; - - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - log.debug('Bucket = Split Rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should allow applying changed params', async () => { - await PageObjects.visEditor.setInterval('1', { type: 'numeric', append: true }); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('20001'); - const isApplyButtonEnabled = await PageObjects.visEditor.isApplyEnabled(); - expect(isApplyButtonEnabled).to.be(true); - }); - - it('should allow reseting changed params', async () => { - await PageObjects.visEditor.clickReset(); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('2000'); - }); - - it('should be able to save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); - }); - - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); - }); - - it('should show correct data', function () { - const expectedChartData = [ - ['0B', '2,088'], - ['1.953KB', '2,748'], - ['3.906KB', '2,707'], - ['5.859KB', '2,876'], - ['7.813KB', '2,863'], - ['9.766KB', '147'], - ['11.719KB', '148'], - ['13.672KB', '129'], - ['15.625KB', '161'], - ['17.578KB', '137'], - ]; - - return retry.try(async function () { - await inspector.open(); - await inspector.expectTableData(expectedChartData); - await inspector.close(); - }); - }); - - it('should show correct data when using average pipeline aggregation', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets'); - await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); - }); - - describe('data table with date histogram', async () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data', async () => { - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); - - it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - - it('should correctly filter for pinned filters', async () => { - await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts deleted file mode 100644 index 3661a4847bd..00000000000 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ /dev/null @@ -1,101 +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 expect from '@osd/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const filterBar = getService('filterBar'); - const renderable = getService('renderable'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'header', - 'dashboard', - 'timePicker', - 'visEditor', - 'visChart', - ]); - - describe('data table with index without time filter filters', function indexPatternCreation() { - const vizName1 = 'Visualization DataTable w/o time filter'; - - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - log.debug('Bucket = Split Rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should be able to save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); - }); - - it('timefilter should be disabled', async () => { - const isOff = await PageObjects.timePicker.isOff(); - expect(isOff).to.be(true); - }); - - // test to cover bug #54548 - add this visualization to a dashboard and filter - it('should add to dashboard and allow filtering', async function () { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.addVisualization(vizName1); - - // hover and click on cell to filter - await PageObjects.visChart.filterOnTableCell('1', '2'); - - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.be(1); - - await filterBar.removeAllFilters(); - }); - }); -} diff --git a/test/functional/apps/visualize/_embedding_chart.js b/test/functional/apps/visualize/_embedding_chart.js deleted file mode 100644 index c47319303dc..00000000000 --- a/test/functional/apps/visualize/_embedding_chart.js +++ /dev/null @@ -1,187 +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 expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const filterBar = getService('filterBar'); - const log = getService('log'); - const renderable = getService('renderable'); - const embedding = getService('embedding'); - const PageObjects = getPageObjects([ - 'visualize', - 'visEditor', - 'visChart', - 'header', - 'timePicker', - ]); - - describe('embedding', () => { - describe('a data table', () => { - before(async function () { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Histogram'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric', aggNth: 3 }); - await PageObjects.visEditor.clickGo(); - }); - - it('should allow opening table vis in embedded mode', async () => { - await embedding.openInEmbeddedMode(); - await renderable.waitForRender(); - - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20 00:00', - '0B', - '5', - '2015-09-20 00:00', - '1.953KB', - '5', - '2015-09-20 00:00', - '3.906KB', - '9', - '2015-09-20 00:00', - '5.859KB', - '4', - '2015-09-20 00:00', - '7.813KB', - '14', - '2015-09-20 03:00', - '0B', - '32', - '2015-09-20 03:00', - '1.953KB', - '33', - '2015-09-20 03:00', - '3.906KB', - '45', - '2015-09-20 03:00', - '5.859KB', - '31', - '2015-09-20 03:00', - '7.813KB', - '48', - ]); - }); - - it('should allow to filter in embedded mode', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-21', '2015-09-23'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-21 00:00', - '0B', - '7', - '2015-09-21 00:00', - '1.953KB', - '9', - '2015-09-21 00:00', - '3.906KB', - '9', - '2015-09-21 00:00', - '5.859KB', - '6', - '2015-09-21 00:00', - '7.813KB', - '10', - '2015-09-21 00:00', - '11.719KB', - '1', - '2015-09-21 03:00', - '0B', - '28', - '2015-09-21 03:00', - '1.953KB', - '39', - '2015-09-21 03:00', - '3.906KB', - '36', - '2015-09-21 03:00', - '5.859KB', - '43', - ]); - }); - - it('should allow to change timerange from the visualization in embedded mode', async () => { - await PageObjects.visChart.filterOnTableCell(1, 7); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '03:00', - '0B', - '1', - '03:00', - '1.953KB', - '1', - '03:00', - '3.906KB', - '1', - '03:00', - '5.859KB', - '2', - '03:10', - '0B', - '1', - '03:10', - '5.859KB', - '1', - '03:10', - '7.813KB', - '1', - '03:15', - '0B', - '1', - '03:15', - '1.953KB', - '1', - '03:20', - '1.953KB', - '1', - ]); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_histogram_request_start.js b/test/functional/apps/visualize/_histogram_request_start.js deleted file mode 100644 index f797b83af73..00000000000 --- a/test/functional/apps/visualize/_histogram_request_start.js +++ /dev/null @@ -1,95 +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 expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const log = getService('log'); - const retry = getService('retry'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'visEditor', - 'visChart', - 'timePicker', - ]); - - describe('histogram agg onSearchRequestStart', function () { - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = Split Rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = machine.ram'); - await PageObjects.visEditor.selectField('machine.ram'); - }); - - describe('interval parameter uses autoBounds', function () { - it('should use provided value when number of generated buckets is less than histogram:maxBars', async function () { - const providedInterval = 2400000000; - log.debug(`Interval = ${providedInterval}`); - await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - await retry.try(async () => { - const data = await PageObjects.visChart.getTableVisData(); - const dataArray = data.replace(/,/g, '').split('\n'); - expect(dataArray.length).to.eql(20); - const bucketStart = parseInt(dataArray[0], 10); - const bucketEnd = parseInt(dataArray[2], 10); - const actualInterval = bucketEnd - bucketStart; - expect(actualInterval).to.eql(providedInterval); - }); - }); - - it('should scale value to round number when number of generated buckets is greater than histogram:maxBars', async function () { - const providedInterval = 100; - log.debug(`Interval = ${providedInterval}`); - await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - await PageObjects.common.sleep(1000); //fix this - await retry.try(async () => { - const data = await PageObjects.visChart.getTableVisData(); - const dataArray = data.replace(/,/g, '').split('\n'); - expect(dataArray.length).to.eql(20); - const bucketStart = parseInt(dataArray[0], 10); - const bucketEnd = parseInt(dataArray[2], 10); - const actualInterval = bucketEnd - bucketStart; - expect(actualInterval).to.eql(1200000000); - }); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts deleted file mode 100644 index 72c2a4322a2..00000000000 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ /dev/null @@ -1,138 +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 expect from '@osd/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const browser = getService('browser'); - const filterBar = getService('filterBar'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects([ - 'common', - 'discover', - 'visualize', - 'header', - 'timePicker', - 'visChart', - ]); - - describe('saved search visualizations from visualize app', function describeIndexTests() { - describe('linked saved searched', () => { - const savedSearchName = 'vis_saved_search'; - let discoverSavedSearchUrlPath: string; - - before(async () => { - await PageObjects.common.navigateToApp('discover'); - await filterBar.addFilter('extension.raw', 'is', 'jpg'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.discover.saveSearch(savedSearchName); - discoverSavedSearchUrlPath = (await browser.getCurrentUrl()).split('?')[0]; - }); - - it('should create a visualization from a saved search', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickSavedSearch(savedSearchName); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await retry.waitFor('wait for count to equal 9,109', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '9,109'; - }); - }); - - it('should have a valid link to the saved search from the visualization', async () => { - await testSubjects.click('showUnlinkSavedSearchPopover'); - await testSubjects.click('viewSavedSearch'); - await PageObjects.header.waitUntilLoadingHasFinished(); - - await retry.waitFor('wait discover load its breadcrumbs', async () => { - const discoverBreadcrumb = await PageObjects.discover.getCurrentQueryName(); - return discoverBreadcrumb === savedSearchName; - }); - - const discoverURLPath = (await browser.getCurrentUrl()).split('?')[0]; - expect(discoverURLPath).to.equal(discoverSavedSearchUrlPath); - - // go back to visualize - await browser.goBack(); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - it('should respect the time filter when linked to a saved search', async () => { - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 19, 2015 @ 06:31:44.000', - 'Sep 21, 2015 @ 10:00:00.000' - ); - await retry.waitFor('wait for count to equal 3,950', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '3,950'; - }); - }); - - it('should allow adding filters while having a linked saved search', async () => { - await filterBar.addFilter('bytes', 'is between', '100', '3000'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('wait for count to equal 707', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '707'; - }); - }); - - it('should allow unlinking from a linked search', async () => { - await PageObjects.visualize.clickUnlinkSavedSearch(); - await retry.waitFor('wait for count to equal 707', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '707'; - }); - // The filter on the saved search should now be in the editor - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); - - // Disabling this filter should now result in different values, since - // the visualization should not be linked anymore with the saved search. - await filterBar.toggleFilterEnabled('extension.raw'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('wait for count to equal 1,293', async () => { - const unfilteredData = await PageObjects.visChart.getTableVisData(); - return unfilteredData.trim() === '1,293'; - }); - }); - - it('should not break when saving after unlinking', async () => { - await PageObjects.visualize.saveVisualizationExpectSuccess('Unlinked before saved'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('wait for count to equal 1,293', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '1,293'; - }); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index b6563a1c28c..fb7e721db7d 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -57,12 +57,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags('ciGroup9'); loadTestFile(require.resolve('./_custom_branding')); - loadTestFile(require.resolve('./_embedding_chart')); loadTestFile(require.resolve('./_chart_types')); loadTestFile(require.resolve('./_area_chart')); - loadTestFile(require.resolve('./_data_table')); - loadTestFile(require.resolve('./_data_table_nontimeindex')); - loadTestFile(require.resolve('./_data_table_notimeindex_filters')); }); describe('', function () { @@ -73,7 +69,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_gauge_chart')); loadTestFile(require.resolve('./_heatmap_chart')); loadTestFile(require.resolve('./input_control_vis')); - loadTestFile(require.resolve('./_histogram_request_start')); loadTestFile(require.resolve('./_metric_chart')); }); @@ -86,7 +81,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_markdown_vis')); loadTestFile(require.resolve('./_shared_item')); loadTestFile(require.resolve('./_lab_mode')); - loadTestFile(require.resolve('./_linked_saved_searches')); loadTestFile(require.resolve('./_visualize_listing')); if (isOss) { loadTestFile(require.resolve('./_tile_map')); diff --git a/test/functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards/data.json.gz b/test/functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards/data.json.gz index 9944453bff1a01420b3f46c35af304c4fc5122ee..1ca01589588ce98027d39d1650ce7ae1cfc11e43 100644 GIT binary patch literal 20707 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 M3qUV`;*%f)00w`{3;+NC diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 40e17779069..58f75555b15 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -425,7 +425,6 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide { name: PIE_CHART_VIS_NAME, description: 'PieChart' }, { name: 'Visualization☺ VerticalBarChart', description: 'VerticalBarChart' }, { name: AREA_CHART_VIS_NAME, description: 'AreaChart' }, - { name: 'Visualization☺漢字 DataTable', description: 'DataTable' }, { name: LINE_CHART_VIS_NAME, description: 'LineChart' }, { name: 'Visualization TileMap', description: 'TileMap' }, { name: 'Visualization MetricChart', description: 'MetricChart' }, diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 02d9e60ca06..e13d8eed608 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -36,7 +36,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr const find = getService('find'); const log = getService('log'); const retry = getService('retry'); - const table = getService('table'); const defaultFindTimeout = config.get('timeouts.find'); const { common } = getPageObjects(['common']); @@ -294,18 +293,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr }); } - public async filterOnTableCell(column: string, row: string) { - await retry.try(async () => { - const tableVis = await testSubjects.find('tableVis'); - const cell = await tableVis.findByCssSelector( - `tbody tr:nth-child(${row}) td:nth-child(${column})` - ); - await cell.moveMouseTo(); - const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell); - await filterBtn.click(); - }); - } - public async getMarkdownText() { const markdownContainer = await testSubjects.find('markdownBody'); return markdownContainer.getVisibleText(); @@ -317,65 +304,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr return element.getVisibleText(); } - public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) { - const tableVis = await testSubjects.find('tableVis'); - const $ = await tableVis.parseDomContent(); - const headers = $('span[ng-bind="::col.title"]') - .toArray() - .map((header: any) => $(header).text()); - const fieldColumnIndex = headers.indexOf(fieldName); - return await find.byCssSelector( - `[data-test-subj="paginated-table-body"] tr:nth-of-type(${rowIndex}) td:nth-of-type(${ - fieldColumnIndex + 1 - }) a` - ); - } - - /** - * If you are writing new tests, you should rather look into getTableVisContent method instead. - * @deprecated Use getTableVisContent instead. - */ - public async getTableVisData() { - return await testSubjects.getVisibleText('paginated-table-body'); - } - - /** - * This function is the newer function to retrieve data from within a table visualization. - * It uses a better return format, than the old getTableVisData, by properly splitting - * cell values into arrays. Please use this function for newer tests. - */ - public async getTableVisContent({ stripEmptyRows = true } = {}) { - return await retry.try(async () => { - const container = await testSubjects.find('tableVis'); - const allTables = await testSubjects.findAllDescendant('paginated-table-body', container); - - if (allTables.length === 0) { - return []; - } - - const allData = await Promise.all( - allTables.map(async (t) => { - let data = await table.getDataFromElement(t); - if (stripEmptyRows) { - data = data.filter( - (row) => row.length > 0 && row.some((cell) => cell.trim().length > 0) - ); - } - return data; - }) - ); - - if (allTables.length === 1) { - // If there was only one table we return only the data for that table - // This prevents an unnecessary array around that single table, which - // is the case we have in most tests. - return allData[0]; - } - - return allData; - }); - } - public async getMetric() { const elements = await find.allByCssSelector( '[data-test-subj="visualizationLoader"] .mtrVis__container' diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 4f019c1a4a0..b60d50b449c 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -99,10 +99,6 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await this.clickVisType('area'); } - public async clickDataTable() { - await this.clickVisType('table'); - } - public async clickLineChart() { await this.clickVisType('line'); } diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index a676730f4c1..ab6eaed8a9b 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -241,17 +241,6 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi }); } - async dataTableRowCount(expectedCount: number) { - log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`); - await retry.try(async () => { - const dataTableRows = await find.allByCssSelector( - '[data-test-subj="paginated-table-body"] [data-cell-content]', - findTimeout - ); - expect(dataTableRows.length).to.be(expectedCount); - }); - } - async seriesElementCount(expectedCount: number) { log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`); await retry.try(async () => { diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 391967de7bc..c09d6399516 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -57,7 +57,6 @@ import { ManagementMenuProvider } from './management'; import { QueryBarProvider } from './query_bar'; import { RemoteProvider } from './remote'; import { RenderableProvider } from './renderable'; -import { TableProvider } from './table'; import { ToastsProvider } from './toasts'; import { DataGridProvider } from './data_grid'; import { @@ -93,7 +92,6 @@ export const services = { dataGrid: DataGridProvider, embedding: EmbeddingProvider, renderable: RenderableProvider, - table: TableProvider, browser: BrowserProvider, pieChart: PieChartProvider, inspector: InspectorProvider, diff --git a/test/functional/services/table.ts b/test/functional/services/table.ts deleted file mode 100644 index 34578df40e8..00000000000 --- a/test/functional/services/table.ts +++ /dev/null @@ -1,73 +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 { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from './lib/web_element_wrapper'; - -export function TableProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - - class Table { - /** - * Finds table and returns data in the nested array format - * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] - * @param dataTestSubj data-test-subj selector - */ - - public async getDataFromTestSubj(dataTestSubj: string): Promise { - const table = await testSubjects.find(dataTestSubj); - return await this.getDataFromElement(table); - } - - /** - * Converts the table data into nested array - * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] - * @param element table - */ - public async getDataFromElement(element: WebElementWrapper): Promise { - const $ = await element.parseDomContent(); - return $('tr') - .toArray() - .map((row) => - $(row) - .find('td') - .toArray() - .map((cell) => - $(cell) - .text() - .replace(/ /g, '') - .trim() - ) - ); - } - } - - return new Table(); -}