From 865408644362a603cc263c2d799f246227472d4c Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 3 Nov 2022 17:42:45 -0700 Subject: [PATCH] [Vis Builder] Add an experimental table visualization in vis builder (#2705) * [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 * remove unused scss tyle Signed-off-by: Anan Zhuang * 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 * 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 Signed-off-by: Anan Zhuang Signed-off-by: Ajay Gupta --- CHANGELOG.md | 1 + 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 | 109 +++++++++++++ .../public/visualizations/table/index.ts | 6 + .../visualizations/table/table_viz_type.ts | 95 +++++++++++ .../visualizations/table/to_expression.ts | 130 +++++++++++++++ src/plugins/vis_type_table_new/README.md | 1 + .../opensearch_dashboards.json | 16 ++ .../public/components/table_vis_app.scss | 14 ++ .../public/components/table_vis_app.tsx | 71 +++++++++ .../public/components/table_vis_component.tsx | 142 +++++++++++++++++ .../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 | 70 +++++++++ .../public/utils/convert_to_csv_data.ts | 85 ++++++++++ .../public/utils/convert_to_formatted_data.ts | 68 ++++++++ .../vis_type_table_new/public/utils/index.ts | 8 + .../public/utils/use_pagination.ts | 39 +++++ 28 files changed, 1380 insertions(+), 6 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_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 158ef8adb3fc..b2ad560973bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,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 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..8c934fff8dac --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx @@ -0,0 +1,109 @@ +/* + * 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 } from '../../../../../charts/public'; +import { + useTypedDispatch, + useTypedSelector, + setStyleState, +} from '../../../application/utils/state_management'; +import { TableOptionsDefaults } from '../table_viz_type'; +import { Option } from '../../../application/app'; + +function TableVizOptions() { + const styleState = useTypedSelector((state) => state.style) as TableOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft) => void) => { + const newState = produce(styleState, callback); + dispatch(setStyleState(newState)); + }, + [dispatch, styleState] + ); + + 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..733ad986f289 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts @@ -0,0 +1,95 @@ +/* + * 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'; + +export interface TableOptionsDefaults { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; +} + +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, + }, + 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_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..af6558774da3 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_app.scss @@ -0,0 +1,14 @@ +.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..e24784d9eb1a --- /dev/null +++ b/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx @@ -0,0 +1,142 @@ +/* + * 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'; + + 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} + 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..ba204ea6ae33 --- /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..0c5a9f9955e0 --- /dev/null +++ b/src/plugins/vis_type_table_new/public/types.ts @@ -0,0 +1,70 @@ +/* + * 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_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..cd997dfe5d5e --- /dev/null +++ b/src/plugins/vis_type_table_new/public/utils/convert_to_formatted_data.ts @@ -0,0 +1,68 @@ +/* + * 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_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] + ); +};