From f1030e4bdd95d681cebb00f48c1b988f78e6338d Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Tue, 20 Sep 2022 09:21:43 -0700 Subject: [PATCH 1/4] [Vis Builder] Add an experimental table visualization in vis builder In this PR, we hook up an experimental table vis in vis builder. This table vis is a refactor of previous table. It is written in React and DataGrid component. In this PR, we did two main things: * add an experimental table visualization * enable it in vis builder Issue Resolved (hook up table in vis builder): https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2704 The experimental table vis has all the features from current table, including * restore table vis in react using a Datagrid component * datagrid component does not support splitted grids. For future transfer to OUI Datagrid, we create a tableGroup in visData for splitted grids. * restore basic pagenation, sort and format. * implement datagrid columns * display column title correctly * deangular and re-use formatted column * convert formatted column to data grid column * restore filter in and filter out value functions * format table cell to show Date and percent * restore showTotal feature: it allows table vis to show total, avg, min, max and count statics on count * restore export csv feature to table vis * split table in rows and columns Beside of restoring original features, there are some changes: * [IMPROVE] remove repeated column from split tables Currently, when we split table by columns, the split column is shown both in the table title and as a separate column. This is not needed. In this PR, we remove the repeated column in split tables in col. * [NEW FEATURE] adjustable table column width In the new table visualization, customer can adjust the column width as needed. Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2212 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2213 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2305 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2379 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2579 Since this is a hookup PR, we remove un-used table vis types and options because they could be defined in vis builder. We also create follow up issues for some un-resolved PR comments. Signed-off-by: Anan Zhuang --- CHANGELOG.md | 3 +- .../data/common/field_formats/field_format.ts | 6 + src/plugins/vis_builder/README.md | 2 +- .../utils/use/use_saved_vis_builder_vis.ts | 3 +- .../common/expression_helpers.ts | 10 +- .../public/visualizations/index.ts | 2 + .../table/components/table_viz_options.tsx | 219 ++++++++++++++++++ .../public/visualizations/table/index.ts | 6 + .../visualizations/table/table_viz_type.ts | 102 ++++++++ .../visualizations/table/to_expression.ts | 130 +++++++++++ .../public/visualizations/table/types.ts | 12 + src/plugins/vis_type_table_new/README.md | 1 + .../opensearch_dashboards.json | 16 ++ .../public/components/table_vis_app.scss | 19 ++ .../public/components/table_vis_app.tsx | 71 ++++++ .../public/components/table_vis_component.tsx | 150 ++++++++++++ .../components/table_vis_component_group.tsx | 38 +++ .../public/components/table_vis_control.tsx | 55 +++++ .../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_renderer.tsx | 36 +++ .../public/table_vis_response_handler.ts | 112 +++++++++ .../vis_type_table_new/public/types.ts | 84 +++++++ .../public/utils/convert_to_csv_data.ts | 85 +++++++ .../public/utils/convert_to_formatted_data.ts | 179 ++++++++++++++ .../vis_type_table_new/public/utils/index.ts | 8 + .../public/utils/use_pagination.ts | 39 ++++ 30 files changed, 1654 insertions(+), 7 deletions(-) create mode 100644 src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx create mode 100644 src/plugins/vis_builder/public/visualizations/table/index.ts create mode 100644 src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts create mode 100644 src/plugins/vis_builder/public/visualizations/table/to_expression.ts create mode 100644 src/plugins/vis_builder/public/visualizations/table/types.ts create mode 100644 src/plugins/vis_type_table_new/README.md create mode 100644 src/plugins/vis_type_table_new/opensearch_dashboards.json create mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_app.scss create mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_app.tsx create mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_component.tsx create mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_component_group.tsx create mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_control.tsx create mode 100644 src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx create mode 100644 src/plugins/vis_type_table_new/public/index.ts create mode 100644 src/plugins/vis_type_table_new/public/plugin.ts create mode 100644 src/plugins/vis_type_table_new/public/services.ts create mode 100644 src/plugins/vis_type_table_new/public/table_vis_fn.ts create mode 100644 src/plugins/vis_type_table_new/public/table_vis_renderer.tsx create mode 100644 src/plugins/vis_type_table_new/public/table_vis_response_handler.ts create mode 100644 src/plugins/vis_type_table_new/public/types.ts create mode 100644 src/plugins/vis_type_table_new/public/utils/convert_to_csv_data.ts create mode 100644 src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts create mode 100644 src/plugins/vis_type_table_new/public/utils/index.ts create mode 100644 src/plugins/vis_type_table_new/public/utils/use_pagination.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a7438ef7f5..daabf1e48f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multi DataSource] Update MD data source documentation link ([#2693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2693)) - [Save Object Aggregation View] Add extension point in saved object management to register namespaces and show filter ([#2656](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2656)) - [Save Object Aggregation View] Fix for export all after scroll count response changed in PR#2656 ([#2696](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2696)) +- [Vis Builder] Add an experimental table visualization in vis builder ([#2705](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2705)) ### 🐛 Bug Fixes @@ -53,7 +54,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multi DataSource] Address UX comments on index pattern management stack ([#2611](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2611)) - [Multi DataSource] Apply get indices error handling in step index pattern ([#2652](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2652)) - [Vis Builder] Last Updated Timestamp for visbuilder savedobject is getting Generated ([#2628](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2628)) -- Removed Leftover X Pack references ([#2638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2638)) +- Removed Leftover X Pack references ([#2638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2638)) - Removes Add Integration button ([#2723](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2723)) ### 🚞 Infrastructure diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 8bff51d1f16c..c5c945f1b899 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -95,6 +95,12 @@ export abstract class FieldFormat { */ public type: any = this.constructor; + /** + * @property {boolean} - allow numeric aggregation + * @private + */ + allowsNumericalAggregations?: boolean; + protected readonly _params: any; protected getConfig: FieldFormatsGetConfigFn | undefined; diff --git a/src/plugins/vis_builder/README.md b/src/plugins/vis_builder/README.md index 88b5afbda1f4..4bbf82d9dc87 100755 --- a/src/plugins/vis_builder/README.md +++ b/src/plugins/vis_builder/README.md @@ -31,6 +31,6 @@ Outline: **Notes:** -- Currently only the metric viz is defined, so schema properties that other vis types might need may be missing and require further setup. +- Currently only the metric and table viz are defined, so schema properties that other vis types might need may be missing and require further setup. - `to_expression` has not yet been abstracted into a common utility for different visualizations. Adding more visualization types should make it easier to identify which parts of expression creation are common, and which are visualization-specific. diff --git a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts index d7840b92f8ad..6e5d861c5318 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts @@ -13,7 +13,6 @@ import { } from '../../../../../opensearch_dashboards_utils/public'; import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; import { VisBuilderServices } from '../../../types'; -import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz_type'; import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; import { getSavedVisBuilderVis } from '../get_saved_vis_builder_vis'; import { @@ -81,7 +80,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined } } - dispatch(setStyleState(styleState)); + dispatch(setStyleState(styleState)); dispatch(setVisualizationState(visualizationState)); } diff --git a/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts b/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts index 069666677d60..f50ab9172cdb 100644 --- a/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts +++ b/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts @@ -9,8 +9,12 @@ import { ExpressionFunctionOpenSearchDashboards } from '../../../../expressions' import { buildExpressionFunction } from '../../../../expressions/public'; import { VisualizationState } from '../../application/utils/state_management'; import { getSearchService, getIndexPatterns } from '../../plugin_services'; +import { StyleState } from '../../application/utils/state_management'; -export const getAggExpressionFunctions = async (visualization: VisualizationState) => { +export const getAggExpressionFunctions = async ( + visualization: VisualizationState, + style?: StyleState +) => { const { activeVisualization, indexPattern: indexId = '' } = visualization; const { aggConfigParams } = activeVisualization || {}; @@ -32,8 +36,8 @@ export const getAggExpressionFunctions = async (visualization: VisualizationStat 'opensearchaggs', { index: indexId, - metricsAtAllLevels: false, - partialRows: false, + metricsAtAllLevels: style?.showMetricsAtAllLevels || false, + partialRows: style?.showPartialRows || false, aggConfigs: JSON.stringify(aggConfigs.aggs), includeFormatHints: false, } diff --git a/src/plugins/vis_builder/public/visualizations/index.ts b/src/plugins/vis_builder/public/visualizations/index.ts index 6787c28a6ff8..c867e570143e 100644 --- a/src/plugins/vis_builder/public/visualizations/index.ts +++ b/src/plugins/vis_builder/public/visualizations/index.ts @@ -5,6 +5,7 @@ import type { TypeServiceSetup } from '../services/type_service'; import { createMetricConfig } from './metric'; +import { createTableConfig } from './table'; import { createHistogramConfig, createLineConfig, createAreaConfig } from './vislib'; export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { @@ -13,6 +14,7 @@ export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { createLineConfig, createAreaConfig, createMetricConfig, + createTableConfig, ]; visualizationTypes.forEach((createTypeConfig) => { 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 new file mode 100644 index 000000000000..1cac62854792 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx @@ -0,0 +1,219 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get } from 'lodash'; +import React, { useCallback, useEffect, useMemo } 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, SelectOption } from '../../../../../charts/public'; +import { + useTypedDispatch, + useTypedSelector, + setStyleState, +} from '../../../application/utils/state_management'; +import { useAggs } from '../../../../public/application/utils/use'; +import { TableOptionsDefaults } from '../table_viz_type'; +import { Option } from '../../../application/app'; +import { AggTypes } from '../types'; + +const { tabifyGetColumns } = search; + +const totalAggregations = [ + { + value: AggTypes.SUM, + text: i18n.translate('visTypeTableNew.totalAggregations.sumText', { + defaultMessage: 'Sum', + }), + }, + { + value: AggTypes.AVG, + text: i18n.translate('visTypeTableNew.totalAggregations.averageText', { + defaultMessage: 'Average', + }), + }, + { + value: AggTypes.MIN, + text: i18n.translate('visTypeTableNewNew.totalAggregations.minText', { + defaultMessage: 'Min', + }), + }, + { + value: AggTypes.MAX, + text: i18n.translate('visTypeTableNewNew.totalAggregations.maxText', { + defaultMessage: 'Max', + }), + }, + { + value: AggTypes.COUNT, + text: i18n.translate('visTypeTableNewNew.totalAggregations.countText', { + defaultMessage: 'Count', + }), + }, +]; + +function TableVizOptions() { + const styleState = useTypedSelector((state) => state.style) as TableOptionsDefaults; + const { aggConfigs } = useAggs(); + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft) => void) => { + const newState = produce(styleState, callback); + dispatch(setStyleState(newState)); + }, + [dispatch, styleState] + ); + + const percentageColumns = useMemo(() => { + const defaultPercentageColText = { + value: '', + text: i18n.translate('visTypeTableNew.params.defaultPercentageCol', { + defaultMessage: 'Don’t show', + }), + }; + return aggConfigs + ? [ + defaultPercentageColText, + ...tabifyGetColumns(aggConfigs.getResponseAggs(), true) + .filter((col) => get(col.aggConfig.toSerializedFieldFormat(), 'id') === 'number') + .map(({ name }) => ({ value: name, text: name })), + ] + : [defaultPercentageColText]; + }, [aggConfigs]); + + useEffect(() => { + if ( + !percentageColumns.find(({ value }) => value === styleState.percentageCol) && + percentageColumns[0] && + percentageColumns[0].value !== styleState.percentageCol + ) { + setOption((draft) => { + draft.percentageCol = percentageColumns[0].value; + }); + } + }, [percentageColumns, styleState.percentageCol, setOption]); + + const isPerPageValid = styleState.perPage === '' || styleState.perPage > 0; + + return ( + <> + + + ); +} + +export { TableVizOptions }; diff --git a/src/plugins/vis_builder/public/visualizations/table/index.ts b/src/plugins/vis_builder/public/visualizations/table/index.ts new file mode 100644 index 000000000000..51fd19d291e7 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createTableConfig } from './table_viz_type'; diff --git a/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts new file mode 100644 index 000000000000..62426c301dc3 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../vis_default_editor/public'; +import { AggGroupNames } from '../../../../data/public'; +import { TableVizOptions } from './components/table_viz_options'; +import { VisualizationTypeOptions } from '../../services/type_service'; +import { toExpression } from './to_expression'; +import { AggTypes } from './types'; + +export interface TableOptionsDefaults { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; +} + +export const createTableConfig = (): VisualizationTypeOptions => ({ + name: 'table', + title: 'Table', + icon: 'visTable', + description: 'Display table visualizations', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + min: 1, + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + defaults: { + aggTypes: ['avg', 'cardinality'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'split_row', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table in rows', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'split_column', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table in columns', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + ]), + }, + style: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + }, + render: TableVizOptions, + }, + }, + }, +}); diff --git a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts new file mode 100644 index 000000000000..212c93248d40 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts @@ -0,0 +1,130 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SchemaConfig } from '../../../../visualizations/public'; +import { TableVisExpressionFunctionDefinition } from '../../../../vis_type_table_new/public'; +import { AggConfigs, IAggConfig } from '../../../../data/common'; +import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; +import { RenderState } from '../../application/utils/state_management'; +import { TableOptionsDefaults } from './table_viz_type'; +import { getAggExpressionFunctions } from '../common/expression_helpers'; + +// TODO: Update to the common getShemas from src/plugins/visualizations/public/legacy/build_pipeline.ts +// And move to a common location accessible by all the visualizations +const getVisSchemas = (aggConfigs: AggConfigs, showMetricsAtAllLevels: boolean): any => { + const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { + const hasSubAgg = [ + 'derivative', + 'moving_avg', + 'serial_diff', + 'cumulative_sum', + 'sum_bucket', + 'avg_bucket', + 'min_bucket', + 'max_bucket', + ].includes(agg.type.name); + + const formatAgg = hasSubAgg + ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) + : agg; + + const params = {}; + + const label = agg.makeLabel && agg.makeLabel(); + + return { + accessor, + format: formatAgg.toSerializedFieldFormat(), + params, + label, + aggType: agg.type.name, + }; + }; + + let cnt = 0; + const schemas: any = { + metric: [], + }; + + if (!aggConfigs) { + return schemas; + } + + const responseAggs = aggConfigs.getResponseAggs().filter((agg: IAggConfig) => agg.enabled); + const metrics = responseAggs.filter((agg: IAggConfig) => agg.type.type === 'metrics'); + + responseAggs.forEach((agg) => { + let skipMetrics = false; + const schemaName = agg.schema; + + if (!schemaName) { + cnt++; + return; + } + + if (schemaName === 'split_row' || schemaName === 'split_column') { + skipMetrics = responseAggs.length - metrics.length > 1; + } + + if (!schemas[schemaName]) { + schemas[schemaName] = []; + } + + if (!showMetricsAtAllLevels || agg.type.type !== 'metrics') { + schemas[schemaName]!.push(createSchemaConfig(cnt++, agg)); + } + + if ( + showMetricsAtAllLevels && + (agg.type.type !== 'metrics' || metrics.length === responseAggs.length) + ) { + metrics.forEach((metric: any) => { + const schemaConfig = createSchemaConfig(cnt++, metric); + if (!skipMetrics) { + schemas.metric.push(schemaConfig); + } + }); + } + }); + + return schemas; +}; + +export interface TableRootState extends RenderState { + style: TableOptionsDefaults; +} + +export const toExpression = async ({ style: styleState, visualization }: TableRootState) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, styleState); + const { showPartialRows, showMetricsAtAllLevels } = styleState; + + const schemas = getVisSchemas(aggConfigs, showMetricsAtAllLevels); + + const metrics = + schemas.bucket && showPartialRows && !showMetricsAtAllLevels + ? schemas.metric.slice(-1 * (schemas.metric.length / schemas.bucket.length)) + : schemas.metric; + + const tableData = { + metrics, + buckets: schemas.bucket || [], + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const visConfig = { + ...styleState, + ...tableData, + }; + + const tableVis = buildExpressionFunction( + 'opensearch_dashboards_table_new', + { + visConfig: JSON.stringify(visConfig), + } + ); + + return buildExpression([...expressionFns, tableVis]).toString(); +}; diff --git a/src/plugins/vis_builder/public/visualizations/table/types.ts b/src/plugins/vis_builder/public/visualizations/table/types.ts new file mode 100644 index 000000000000..b79f6f1a1811 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} diff --git a/src/plugins/vis_type_table_new/README.md b/src/plugins/vis_type_table_new/README.md new file mode 100644 index 000000000000..06299ed963a2 --- /dev/null +++ b/src/plugins/vis_type_table_new/README.md @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000000..598ca7581b83 --- /dev/null +++ b/src/plugins/vis_type_table_new/opensearch_dashboards.json @@ -0,0 +1,16 @@ +{ + "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_app.scss b/src/plugins/vis_type_table_new/public/components/table_vis_app.scss new file mode 100644 index 000000000000..666df3614c17 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_app.scss @@ -0,0 +1,19 @@ +.visTable { + flex-direction: column; + flex-grow: 1 0 0; +} + +.visTable__group { + padding: $euiSizeS; + margin-bottom: $euiSizeL; + + > h3 { + text-align: center; + } +} + +.visTable__groupInColumns { + display: flex; + flex-direction: row; + align-items: flex-start; +} diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_app.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_app.tsx new file mode 100644 index 000000000000..7958b2187620 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_app.tsx @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './table_vis_app.scss'; +import React, { useEffect, useState } from 'react'; +import classNames from 'classnames'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { I18nProvider } from '@osd/i18n/react'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { TableContext } from '../table_vis_response_handler'; +import { TableVisConfig, SortColumn, ColumnWidth, TableUiState } from '../types'; +import { TableVisComponent } from './table_vis_component'; +import { TableVisComponentGroup } from './table_vis_component_group'; + +interface TableVisAppProps { + services: CoreStart; + visData: TableContext; + visConfig: TableVisConfig; + handlers: IInterpreterRenderHandlers; +} + +export const TableVisApp = ({ + services, + visData: { table, tableGroups, direction }, + visConfig, + handlers, +}: TableVisAppProps) => { + // Rendering is asynchronous, completed by handlers.done() + useEffect(() => { + handlers.done(); + }, [handlers]); + + const className = classNames('visTable', { + // eslint-disable-next-line @typescript-eslint/naming-convention + visTable__groupInColumns: direction === 'column', + }); + + // 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 [width, setWidth] = useState([]); + + const tableUiState: TableUiState = { sort, setSort, width, setWidth }; + + return ( + + +
+ {table ? ( + + ) : ( + + )} +
+
+
+ ); +}; diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx new file mode 100644 index 000000000000..634b54aca7cc --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx @@ -0,0 +1,150 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo } from 'react'; +import { orderBy } from 'lodash'; +import { EuiDataGridProps, EuiDataGrid, EuiDataGridSorting, EuiTitle } from '@elastic/eui'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { TableVisConfig, ColumnWidth, SortColumn, TableUiState } from '../types'; +import { getDataGridColumns } from './table_vis_grid_columns'; +import { usePagination } from '../utils'; +import { convertToFormattedData } from '../utils/convert_to_formatted_data'; +import { TableVisControl } from './table_vis_control'; + +interface TableVisComponentProps { + title?: string; + table: Table; + visConfig: TableVisConfig; + event: IInterpreterRenderHandlers['event']; + uiState: TableUiState; +} + +export const TableVisComponent = ({ + title, + table, + visConfig, + event, + uiState, +}: TableVisComponentProps) => { + const { formattedRows: rows, formattedColumns: columns } = convertToFormattedData( + table, + visConfig + ); + + const pagination = usePagination(visConfig, rows.length); + + const sortedRows = useMemo(() => { + return uiState.sort?.colIndex && uiState.sort.direction + ? orderBy(rows, columns[uiState.sort.colIndex]?.id, uiState.sort.direction) + : rows; + }, [columns, rows, uiState]); + + const renderCellValue = useMemo(() => { + return (({ rowIndex, columnId }) => { + const rawContent = sortedRows[rowIndex][columnId]; + const colIndex = columns.findIndex((col) => col.id === columnId); + const column = columns[colIndex]; + // use formatter to format raw content + // this can format date and percentage data + const formattedContent = column.formatter.convert(rawContent, 'text'); + return sortedRows.hasOwnProperty(rowIndex) ? formattedContent || null : null; + }) as EuiDataGridProps['renderCellValue']; + }, [sortedRows, columns]); + + const dataGridColumns = getDataGridColumns(sortedRows, columns, table, event, uiState.width); + + const sortedColumns = useMemo(() => { + return uiState.sort?.colIndex && uiState.sort.direction + ? [{ id: dataGridColumns[uiState.sort.colIndex]?.id, direction: uiState.sort.direction }] + : []; + }, [dataGridColumns, uiState]); + + const onSort = useCallback( + (sortingCols: EuiDataGridSorting['columns'] | []) => { + const nextSortValue = sortingCols[sortingCols.length - 1]; + const nextSort: SortColumn = + sortingCols.length > 0 + ? { + colIndex: dataGridColumns.findIndex((col) => col.id === nextSortValue?.id), + direction: nextSortValue.direction, + } + : { + colIndex: null, + direction: null, + }; + uiState.setSort(nextSort); + return nextSort; + }, + [dataGridColumns, uiState] + ); + + const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( + ({ columnId, width }) => { + const curWidth: ColumnWidth[] = uiState.width; + const nextWidth = [...curWidth]; + const nextColIndex = columns.findIndex((col) => col.id === columnId); + const curColIndex = curWidth.findIndex((col) => col.colIndex === nextColIndex); + const nextColWidth = { colIndex: nextColIndex, width }; + + // if updated column index is not found, then add it to nextWidth + // else reset it in nextWidth + if (curColIndex < 0) nextWidth.push(nextColWidth); + else nextWidth[curColIndex] = nextColWidth; + + // update uiState.width + uiState.setWidth(nextWidth); + }, + [columns, uiState] + ); + + const ariaLabel = title || visConfig.title || 'tableVis'; + + const footerCellValue = visConfig.showTotal + ? ({ columnId }: { columnId: any }) => { + const colIndex = columns.findIndex((col) => col.id === columnId); + return columns[colIndex]?.formattedTotal || null; + } + : undefined; + + return ( + <> + {title && ( + +

{title}

+
+ )} + id), + setVisibleColumns: () => {}, + }} + rowCount={rows.length} + renderCellValue={renderCellValue} + sorting={{ columns: sortedColumns, onSort }} + onColumnResize={onColumnResize} + pagination={pagination} + gridStyle={{ + border: 'horizontal', + header: 'underline', + }} + minSizeForControls={1} + renderFooterCellValue={footerCellValue} + toolbarVisibility={{ + showColumnSelector: false, + showSortSelector: false, + showFullScreenSelector: false, + showStyleSelector: false, + additionalControls: ( + + ), + }} + /> + + ); +}; diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_component_group.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_component_group.tsx new file mode 100644 index 000000000000..633b9d2230bd --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_component_group.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo } from 'react'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { TableGroup } from '../table_vis_response_handler'; +import { TableVisConfig, TableUiState } from '../types'; +import { TableVisComponent } from './table_vis_component'; + +interface TableVisGroupComponentProps { + tableGroups: TableGroup[]; + visConfig: TableVisConfig; + event: IInterpreterRenderHandlers['event']; + uiState: TableUiState; +} + +export const TableVisComponentGroup = memo( + ({ tableGroups, visConfig, event, uiState }: TableVisGroupComponentProps) => { + return ( + <> + {tableGroups.map(({ tables, title }) => ( +
+ +
+ ))} + + ); + } +); diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_control.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_control.tsx new file mode 100644 index 000000000000..26b51c9cc85b --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_control.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { exportAsCsv } from '../utils/convert_to_csv_data'; +import { FormattedColumn } from '../types'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; + +interface TableVisControlProps { + filename?: string; + rows: OpenSearchDashboardsDatatableRow[]; + columns: FormattedColumn[]; +} + +export const TableVisControl = (props: TableVisControlProps) => { + const { + services: { uiSettings }, + } = useOpenSearchDashboards(); + const [isPopoverOpen, setPopover] = useState(false); + + return ( + setPopover((open) => !open)} /> + } + isOpen={isPopoverOpen} + closePopover={() => setPopover(false)} + panelPaddingSize="none" + > + exportAsCsv(false, { ...props, uiSettings })} + > + Raw + , + exportAsCsv(true, { ...props, uiSettings })} + > + Formatted + , + ]} + /> + + ); +}; diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx new file mode 100644 index 000000000000..036cac284f68 --- /dev/null +++ b/src/plugins/vis_type_table_new/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'], + 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 new file mode 100644 index 000000000000..4ed30b71eeaa --- /dev/null +++ b/src/plugins/vis_type_table_new/public/index.ts @@ -0,0 +1,13 @@ +/* + * 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 new file mode 100644 index 000000000000..9cc96c3e9895 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/plugin.ts @@ -0,0 +1,36 @@ +/* + * 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 new file mode 100644 index 000000000000..f8ca4b574307 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/services.ts @@ -0,0 +1,11 @@ +/* + * 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 new file mode 100644 index 000000000000..ec9eafc344af --- /dev/null +++ b/src/plugins/vis_type_table_new/public/table_vis_fn.ts @@ -0,0 +1,65 @@ +/* + * 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_renderer.tsx b/src/plugins/vis_type_table_new/public/table_vis_renderer.tsx new file mode 100644 index 000000000000..8e467112528d --- /dev/null +++ b/src/plugins/vis_type_table_new/public/table_vis_renderer.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { CoreStart } from 'opensearch-dashboards/public'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { TableVisRenderValue } from './table_vis_fn'; +import { TableVisApp } from './components/table_vis_app'; + +export const getTableVisRenderer: ( + core: CoreStart +) => ExpressionRenderDefinition = (core) => ({ + name: 'table_vis', + displayName: 'table visualization', + reuseDomNode: true, + render: async (domNode, { visData, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const showNoResult = visData.table + ? visData.table.rows.length === 0 + : visData.tableGroups?.length === 0; + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_table_new/public/table_vis_response_handler.ts b/src/plugins/vis_type_table_new/public/table_vis_response_handler.ts new file mode 100644 index 000000000000..b1d41edfff8b --- /dev/null +++ b/src/plugins/vis_type_table_new/public/table_vis_response_handler.ts @@ -0,0 +1,112 @@ +/* + * 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 new file mode 100644 index 000000000000..f77abe78a1fa --- /dev/null +++ b/src/plugins/vis_type_table_new/public/types.ts @@ -0,0 +1,84 @@ +/* + * 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 enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} + +export interface TableVisConfig extends TableVisParams { + title: string; + metrics: SchemaConfig[]; + buckets: SchemaConfig[]; + splitRow?: SchemaConfig[]; + splitColumn?: SchemaConfig[]; +} + +export interface TableVisParams { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; +} + +export interface FormattedColumn { + id: string; + title: string; + formatter: IFieldFormat; + filterable: boolean; + formattedTotal?: string | number; + sumTotal?: number; + total?: number; +} + +export interface ColumnWidth { + colIndex: number; + width: number; +} + +export interface SortColumn { + colIndex: number | 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_csv_data.ts b/src/plugins/vis_type_table_new/public/utils/convert_to_csv_data.ts new file mode 100644 index 000000000000..2c37df1aa3d5 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/utils/convert_to_csv_data.ts @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isObject } from 'lodash'; +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; +import { OpenSearchDashboardsDatatable } from '../../../expressions/public'; +import { FormattedColumn } from '../types'; + +const nonAlphaNumRE = /[^a-zA-Z0-9]/; +const allDoubleQuoteRE = /"/g; + +interface CSVDataProps { + filename?: string; + rows: OpenSearchDashboardsDatatable['rows']; + columns: FormattedColumn[]; + uiSettings: CoreStart['uiSettings']; +} + +const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { + const separator = uiSettings.get(CSV_SEPARATOR_SETTING); + const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING); + + function escape(val: any) { + if (!formatted && isObject(val)) val = val.valueOf(); + val = String(val); + if (quoteValues && nonAlphaNumRE.test(val)) { + val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; + } + return val; + } + + let csvRows: string[][] = []; + for (const row of rows) { + const rowArray = []; + for (const col of columns) { + const value = row[col.id]; + const formattedValue = + formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value); + rowArray.push(formattedValue); + } + csvRows = [...csvRows, rowArray]; + } + + // add the columns to the rows + csvRows.unshift(columns.map((col) => escape(col.title))); + + return csvRows.map((row) => row.join(separator) + '\r\n').join(''); +}; + +export const exportAsCsv = function (formatted: boolean, csvData: CSVDataProps) { + const csv = new Blob([toCsv(formatted, csvData)], { type: 'text/csv;charset=utf-8' }); + const type = formatted ? 'formatted' : 'raw'; + if (csvData.filename) saveAs(csv, `${csvData.filename}-${type}.csv`); + else saveAs(csv, `unsaved-${type}.csv`); +}; diff --git a/src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts b/src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts new file mode 100644 index 000000000000..3dbf7c291355 --- /dev/null +++ b/src/plugins/vis_type_table_new/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('visTypeTableNew.params.percentageTableColumnName', { + defaultMessage: '{title} percentages', + values: { title }, + }); + const newCols = insert(columns, insertAtIndex, { + title: i18nTitle, + id: newId, + formatter, + filterable: false, + }); + const newRows = rows.map((row) => ({ + [newId]: (row[id] as number) / (sumTotal as number), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} + +export interface FormattedDataProps { + formattedRows: OpenSearchDashboardsDatatableRow[]; + formattedColumns: FormattedColumn[]; +} + +export const convertToFormattedData = ( + table: Table, + visConfig: TableVisConfig +): FormattedDataProps => { + const { buckets, metrics } = visConfig; + let formattedRows: OpenSearchDashboardsDatatableRow[] = table.rows; + let formattedColumns: FormattedColumn[] = table.columns + .map(function (col, i) { + const isBucket = buckets.find((bucket) => bucket.accessor === i); + const dimension = isBucket || metrics.find((metric) => metric.accessor === i); + + if (!dimension) return undefined; + + const formatter = getFormatService().deserialize(dimension.format); + + const formattedColumn: FormattedColumn = { + id: col.id, + title: col.name, + formatter, + filterable: !!isBucket, + }; + + const isDate = dimension?.format?.id === 'date' || dimension?.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + if (allowsNumericalAggregations || isDate || visConfig.totalFunc === AggTypes.COUNT) { + const sum = table.rows.reduce((prev, curr) => { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + (curr[col.id] as number); + }, 0); + + formattedColumn.sumTotal = sum; + switch (visConfig.totalFunc) { + case AggTypes.SUM: { + if (!isDate) { + formattedColumn.formattedTotal = formatter?.convert(sum); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case AggTypes.AVG: { + if (!isDate) { + const total = sum / table.rows.length; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + } + break; + } + case AggTypes.MIN: { + const total = chain(table.rows).map(col.id).min().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + break; + } + case AggTypes.MAX: { + const total = chain(table.rows).map(col.id).max().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + break; + } + case 'count': { + const total = table.rows.length; + formattedColumn.formattedTotal = total; + formattedColumn.total = total; + break; + } + default: + break; + } + } + + return formattedColumn; + }) + .filter((column): column is FormattedColumn => !!column); + + if (visConfig.percentageCol) { + const insertAtIndex = formattedColumns.findIndex( + (col) => col.title === visConfig.percentageCol + ); + + // column to show percentage for was removed + if (insertAtIndex < 0) return { formattedRows, formattedColumns }; + + const { cols, rows } = addPercentageCol( + formattedColumns, + visConfig.percentageCol, + table.rows, + insertAtIndex + ); + formattedRows = rows; + formattedColumns = cols; + } + return { formattedRows, formattedColumns }; +}; diff --git a/src/plugins/vis_type_table_new/public/utils/index.ts b/src/plugins/vis_type_table_new/public/utils/index.ts new file mode 100644 index 000000000000..1fd0e3f1e0fd --- /dev/null +++ b/src/plugins/vis_type_table_new/public/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './convert_to_csv_data'; +export * from './convert_to_formatted_data'; +export * from './use_pagination'; diff --git a/src/plugins/vis_type_table_new/public/utils/use_pagination.ts b/src/plugins/vis_type_table_new/public/utils/use_pagination.ts new file mode 100644 index 000000000000..45dbed2c0da8 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/utils/use_pagination.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { TableVisConfig } from '../types'; + +export const usePagination = (visConfig: TableVisConfig, nRow: number) => { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: visConfig.perPage || 10, + }); + const onChangeItemsPerPage = useCallback( + (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), + [setPagination] + ); + const onChangePage = useCallback((pageIndex) => setPagination((p) => ({ ...p, pageIndex })), [ + setPagination, + ]); + + useEffect(() => { + const perPage = visConfig.perPage || 10; + const maxiPageIndex = Math.ceil(nRow / perPage) - 1; + setPagination((p) => ({ + pageIndex: p.pageIndex > maxiPageIndex ? maxiPageIndex : p.pageIndex, + pageSize: perPage, + })); + }, [nRow, visConfig.perPage]); + + return useMemo( + () => ({ + ...pagination, + onChangeItemsPerPage, + onChangePage, + }), + [pagination, onChangeItemsPerPage, onChangePage] + ); +}; From a52185d9e7570e1e917ae23a79afb3e1c12b0099 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 3 Nov 2022 17:14:16 +0000 Subject: [PATCH 2/4] remove unused scss tyle Signed-off-by: Anan Zhuang --- .../vis_type_table_new/public/components/table_vis_app.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_app.scss b/src/plugins/vis_type_table_new/public/components/table_vis_app.scss index 666df3614c17..af6558774da3 100644 --- a/src/plugins/vis_type_table_new/public/components/table_vis_app.scss +++ b/src/plugins/vis_type_table_new/public/components/table_vis_app.scss @@ -1,8 +1,3 @@ -.visTable { - flex-direction: column; - flex-grow: 1 0 0; -} - .visTable__group { padding: $euiSizeS; margin-bottom: $euiSizeL; From c0c463fcc4144e0f8adc5d9e293cb7ddf16f620f Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 3 Nov 2022 18:17:35 +0000 Subject: [PATCH 3/4] remove total func and percentage col total func and percentage col are two features that we might need to remove or re-invent for future table vis. For hookup purpose, it doesn't make sense to include some features that we would like to remove. this PR removes total func and percentage col in both table vis and vis builder Signed-off-by: Anan Zhuang --- .../data/common/field_formats/field_format.ts | 6 - .../table/components/table_viz_options.tsx | 112 +------------- .../visualizations/table/table_viz_type.ts | 7 - .../public/visualizations/table/types.ts | 12 -- .../public/components/table_vis_component.tsx | 8 - .../components/table_vis_grid_columns.tsx | 146 +++++++++--------- .../vis_type_table_new/public/types.ts | 14 -- .../public/utils/convert_to_formatted_data.ts | 117 +------------- 8 files changed, 77 insertions(+), 345 deletions(-) delete mode 100644 src/plugins/vis_builder/public/visualizations/table/types.ts diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index c5c945f1b899..8bff51d1f16c 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -95,12 +95,6 @@ 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 1cac62854792..8c934fff8dac 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 @@ -11,55 +11,17 @@ import produce from 'immer'; import { Draft } from 'immer'; import { EuiIconTip } from '@elastic/eui'; import { search } from '../../../../../data/public'; -import { NumberInputOption, SwitchOption, SelectOption } from '../../../../../charts/public'; +import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; import { useTypedDispatch, useTypedSelector, setStyleState, } from '../../../application/utils/state_management'; -import { useAggs } from '../../../../public/application/utils/use'; import { TableOptionsDefaults } from '../table_viz_type'; import { Option } from '../../../application/app'; -import { AggTypes } from '../types'; - -const { tabifyGetColumns } = search; - -const totalAggregations = [ - { - value: AggTypes.SUM, - text: i18n.translate('visTypeTableNew.totalAggregations.sumText', { - defaultMessage: 'Sum', - }), - }, - { - value: AggTypes.AVG, - text: i18n.translate('visTypeTableNew.totalAggregations.averageText', { - defaultMessage: 'Average', - }), - }, - { - value: AggTypes.MIN, - text: i18n.translate('visTypeTableNewNew.totalAggregations.minText', { - defaultMessage: 'Min', - }), - }, - { - value: AggTypes.MAX, - text: i18n.translate('visTypeTableNewNew.totalAggregations.maxText', { - defaultMessage: 'Max', - }), - }, - { - value: AggTypes.COUNT, - text: i18n.translate('visTypeTableNewNew.totalAggregations.countText', { - defaultMessage: 'Count', - }), - }, -]; function TableVizOptions() { const styleState = useTypedSelector((state) => state.style) as TableOptionsDefaults; - const { aggConfigs } = useAggs(); const dispatch = useTypedDispatch(); const setOption = useCallback( @@ -70,35 +32,6 @@ function TableVizOptions() { [dispatch, styleState] ); - const percentageColumns = useMemo(() => { - const defaultPercentageColText = { - value: '', - text: i18n.translate('visTypeTableNew.params.defaultPercentageCol', { - defaultMessage: 'Don’t show', - }), - }; - return aggConfigs - ? [ - defaultPercentageColText, - ...tabifyGetColumns(aggConfigs.getResponseAggs(), true) - .filter((col) => get(col.aggConfig.toSerializedFieldFormat(), 'id') === 'number') - .map(({ name }) => ({ value: name, text: name })), - ] - : [defaultPercentageColText]; - }, [aggConfigs]); - - useEffect(() => { - if ( - !percentageColumns.find(({ value }) => value === styleState.percentageCol) && - percentageColumns[0] && - percentageColumns[0].value !== styleState.percentageCol - ) { - setOption((draft) => { - draft.percentageCol = percentageColumns[0].value; - }); - } - }, [percentageColumns, styleState.percentageCol, setOption]); - const isPerPageValid = styleState.perPage === '' || styleState.perPage > 0; return ( @@ -168,49 +101,6 @@ function TableVizOptions() { } data-test-subj="showPartialRows" /> - - - setOption((draft) => { - draft.showTotal = value; - }) - } - /> - - - setOption((draft) => { - draft.totalFunc = value; - }) - } - /> - - - setOption((draft) => { - draft.percentageCol = value; - }) - } - id="datatableVisualizationPercentageCol" - /> ); diff --git a/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts index 62426c301dc3..733ad986f289 100644 --- a/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts +++ b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts @@ -9,15 +9,11 @@ import { AggGroupNames } from '../../../../data/public'; import { TableVizOptions } from './components/table_viz_options'; import { VisualizationTypeOptions } from '../../services/type_service'; import { toExpression } from './to_expression'; -import { AggTypes } from './types'; export interface TableOptionsDefaults { perPage: number | ''; showPartialRows: boolean; showMetricsAtAllLevels: boolean; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; } export const createTableConfig = (): VisualizationTypeOptions => ({ @@ -91,9 +87,6 @@ export const createTableConfig = (): VisualizationTypeOptions { - const colIndex = columns.findIndex((col) => col.id === columnId); - return columns[colIndex]?.formattedTotal || null; - } - : undefined; - return ( <> {title && ( @@ -134,7 +127,6 @@ 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_grid_columns.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_grid_columns.tsx index 036cac284f68..c5ca2f37feea 100644 --- 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 @@ -40,82 +40,82 @@ export const getDataGridColumns = ( }; 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 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, - }, - } - ); + // 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); + // 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, - }, - } - ); + // 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; + // 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); @@ -138,7 +138,7 @@ export const getDataGridColumns = ( }), }, }, - cellActions, + //cellActions, }; if (initialWidth) { dataGridColumn.initialWidth = initialWidth.width; diff --git a/src/plugins/vis_type_table_new/public/types.ts b/src/plugins/vis_type_table_new/public/types.ts index f77abe78a1fa..0c5a9f9955e0 100644 --- a/src/plugins/vis_type_table_new/public/types.ts +++ b/src/plugins/vis_type_table_new/public/types.ts @@ -31,14 +31,6 @@ import { SchemaConfig } from 'src/plugins/visualizations/public'; import { IFieldFormat } from 'src/plugins/data/public'; -export enum AggTypes { - SUM = 'sum', - AVG = 'avg', - MIN = 'min', - MAX = 'max', - COUNT = 'count', -} - export interface TableVisConfig extends TableVisParams { title: string; metrics: SchemaConfig[]; @@ -51,9 +43,6 @@ export interface TableVisParams { perPage: number | ''; showPartialRows: boolean; showMetricsAtAllLevels: boolean; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; } export interface FormattedColumn { @@ -61,9 +50,6 @@ export interface FormattedColumn { title: string; formatter: IFieldFormat; filterable: boolean; - formattedTotal?: string | number; - sumTotal?: number; - total?: number; } export interface ColumnWidth { 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 index 3dbf7c291355..cd997dfe5d5e 100644 --- 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 @@ -28,54 +28,11 @@ * 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 { 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('visTypeTableNew.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[]; @@ -86,8 +43,8 @@ export const convertToFormattedData = ( visConfig: TableVisConfig ): FormattedDataProps => { const { buckets, metrics } = visConfig; - let formattedRows: OpenSearchDashboardsDatatableRow[] = table.rows; - let formattedColumns: FormattedColumn[] = table.columns + 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); @@ -103,77 +60,9 @@ export const convertToFormattedData = ( filterable: !!isBucket, }; - const isDate = dimension?.format?.id === 'date' || dimension?.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - if (allowsNumericalAggregations || isDate || visConfig.totalFunc === AggTypes.COUNT) { - const sum = table.rows.reduce((prev, curr) => { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + (curr[col.id] as number); - }, 0); - - formattedColumn.sumTotal = sum; - switch (visConfig.totalFunc) { - case AggTypes.SUM: { - if (!isDate) { - formattedColumn.formattedTotal = formatter?.convert(sum); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case AggTypes.AVG: { - if (!isDate) { - const total = sum / table.rows.length; - formattedColumn.formattedTotal = formatter?.convert(total); - formattedColumn.total = total; - } - break; - } - case AggTypes.MIN: { - const total = chain(table.rows).map(col.id).min().value() as number; - formattedColumn.formattedTotal = formatter?.convert(total); - formattedColumn.total = total; - break; - } - case AggTypes.MAX: { - const total = chain(table.rows).map(col.id).max().value() as number; - formattedColumn.formattedTotal = formatter?.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - return formattedColumn; }) .filter((column): column is FormattedColumn => !!column); - if (visConfig.percentageCol) { - const insertAtIndex = formattedColumns.findIndex( - (col) => col.title === visConfig.percentageCol - ); - - // column to show percentage for was removed - if (insertAtIndex < 0) return { formattedRows, formattedColumns }; - - const { cols, rows } = addPercentageCol( - formattedColumns, - visConfig.percentageCol, - table.rows, - insertAtIndex - ); - formattedRows = rows; - formattedColumns = cols; - } return { formattedRows, formattedColumns }; }; From 4136f0a58a0a4eee60b350b632a4b3ffc13c3752 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 3 Nov 2022 19:29:18 +0000 Subject: [PATCH 4/4] comment out cellActions currently filter in/out cell doesn't function in vis builder. we will coumment out cell actions for now. Signed-off-by: Anan Zhuang --- .../public/components/table_vis_grid_columns.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index c5ca2f37feea..ba204ea6ae33 100644 --- 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 @@ -40,7 +40,7 @@ export const getDataGridColumns = ( }; return cols.map((col, colIndex) => { - //const cellActions = col.filterable + // const cellActions = col.filterable // ? [ // ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { // const filterValue = rows[rowIndex][columnId]; @@ -138,7 +138,7 @@ export const getDataGridColumns = ( }), }, }, - //cellActions, + // cellActions, }; if (initialWidth) { dataGridColumn.initialWidth = initialWidth.width;