diff --git a/dashboards-observability/.cypress/integration/VisualizationCharts/13_stats_chart.spec.js b/dashboards-observability/.cypress/integration/VisualizationCharts/13_stats_chart.spec.js index 657686dd2..91e0840e9 100644 --- a/dashboards-observability/.cypress/integration/VisualizationCharts/13_stats_chart.spec.js +++ b/dashboards-observability/.cypress/integration/VisualizationCharts/13_stats_chart.spec.js @@ -8,12 +8,12 @@ import { delay, TEST_QUERIES, querySearch, - landOnEventVisualizations, + landOnEventVisualizations } from '../../utils/event_constants'; const numberOfWindow = 4; const metricsPrecisionUpdated = 2; -const metricUnit = 'cm'; +const metricUnit = 'cm' ; const titleSize = '25.5px'; const titleSizeUpdated = '40px'; const valueSize = '60.8px'; @@ -22,9 +22,7 @@ const valueSizeUpdated = '73.0px'; const renderStatsChart = () => { landOnEventVisualizations(); querySearch(TEST_QUERIES[4].query, TEST_QUERIES[4].dateRangeDOM); - cy.get('[data-test-subj="configPane__vizTypeSelector"] [data-test-subj="comboBoxInput"]') - .type('Stats') - .type('{enter}'); + cy.get('[data-test-subj="configPane__vizTypeSelector"] [data-test-subj="comboBoxInput"]').type('Stats').type('{enter}'); }; describe('Render stats chart and verify default behaviour ', () => { @@ -67,7 +65,7 @@ describe('Render stats chart for data configuration panel', () => { cy.get('.euiComboBoxPill.euiComboBoxPill--plainText').eq(3).should('contain', 'host'); }); - it('Render stats chart and verify no result found message if the dimension is removed', () => { + it('Render stats chart and verify no result found message if the dimension is removed' , () => { cy.get('[data-test-subj="comboBoxClearButton"]').eq(0).click(); cy.get('[data-test-subj="visualizeEditorRenderButton"]').click(); cy.get('.euiTextColor.euiTextColor--subdued').contains('No results found').should('exist'); @@ -78,17 +76,17 @@ describe('Render stats chart for data configuration panel', () => { }); it('Render stats chart and verify data config panel no result found if metric is missing', () => { - cy.get('.euiText.euiText--extraSmall').eq(0).click(); - cy.get('.euiText.euiText--extraSmall').eq(1).click(); - cy.get('[data-test-subj="comboBoxClearButton"]').eq(1).click(); - cy.get('[data-test-subj="comboBoxInput"]').eq(0).click(); - cy.get('.euiButton__text').contains('Update chart').click(); - cy.get('.euiTextColor.euiTextColor--subdued').contains('No results found').should('exist'); - cy.get('[data-test-subj="comboBoxInput"]').eq(3).click(); - cy.get('.euiComboBoxOption__content').contains('avg(bytes)').click(); - cy.get('.euiButton__text').contains('Update chart').click(); - cy.get('.main-svg').contains('No results found').should('not.exist'); - }); + cy.get('.euiText.euiText--extraSmall').eq(0).click(); + cy.get('.euiText.euiText--extraSmall').eq(1).click(); + cy.get('[data-test-subj="comboBoxClearButton"]').eq(1).click(); + cy.get('[data-test-subj="comboBoxInput"]').eq(0).click(); + cy.get('.euiButton__text').contains('Update chart').click(); + cy.get('.euiTextColor.euiTextColor--subdued').contains('No results found').should('exist'); + cy.get('[data-test-subj="comboBoxInput"]').eq(3).click(); + cy.get('.euiComboBoxOption__content').contains('avg(bytes)').click(); + cy.get('.euiButton__text').contains('Update chart').click(); + cy.get('.main-svg').contains('No results found').should('not.exist'); + }); }); describe('Render stats chart for panel options', () => { @@ -97,7 +95,7 @@ describe('Render stats chart for panel options', () => { }); it('Render stats chart and verify the title gets updated according to user input ', () => { - cy.get('input[name="title"]').type('stats chart'); + cy.get('input[name="title"]').type("stats chart"); cy.get('textarea[name="description"]').should('exist').click(); cy.get('.gtitle').contains('stats chart').should('exist'); }); @@ -107,13 +105,11 @@ describe('Render stats chart verfiy functionality for Tooltip mode', () => { beforeEach(() => { renderStatsChart(); }); - + it('Render stats chart and verfiy the Show and Hidden Tooltip modes', () => { cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(0).should('have.text', 'Show'); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(1) - .should('have.text', 'Hidden') - .click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(1).should('have.text', 'Hidden') + .click(); }); }); @@ -124,14 +120,10 @@ describe('Render stats chart verfiy functionality for Tooltip text', () => { it('Render stats chart and verfiy the Tootltip text -> All , Dimension , Metric', () => { cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(2).should('have.text', 'All'); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(3) - .should('have.text', 'Dimension') - .click(); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(4) - .should('have.text', 'Metrics') - .click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(3).should('have.text', 'Dimension') + .click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(4).should('have.text', 'Metrics') + .click(); }); }); @@ -142,32 +134,21 @@ describe('Render stats chart for Chart Styles ', () => { it('Render stats chart and verify the various chart type selected', () => { cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(5).should('have.text', 'Auto'); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(6) - .should('have.text', 'Horizontal') - .click(); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(7) - .should('have.text', 'Text mode') - .click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(6).should('have.text', 'Horizontal').click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(7).should('have.text', 'Text mode').click(); }); it('Render stats chart and verify the various chart orientation selected', () => { cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(8).should('have.text', 'Auto'); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(9) - .should('have.text', 'Horizontal') - .click(); - cy.get('.euiButton__text.euiButtonGroupButton__textShift') - .eq(10) - .should('have.text', 'Vertical') - .click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(9).should('have.text', 'Horizontal').click(); + cy.get('.euiButton__text.euiButtonGroupButton__textShift').eq(10).should('have.text', 'Vertical').click(); }); it('Render stats chart and verify Metric unit and Metric Precision on chart ', () => { cy.get('[data-test-subj="valueFieldText"]').click().type(metricUnit); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); cy.get('[data-test-subj="valueFieldNumber"]').eq(0).click().type(metricsPrecisionUpdated); - cy.get('[data-test-subj="visualizeEditorRenderButton"]').click(); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); }); it('Render stats chart and verify behaviour for Title size and Value size on chart ', () => { @@ -175,50 +156,56 @@ describe('Render stats chart for Chart Styles ', () => { cy.get('.annotation-text').eq(2).should('have.css', 'font-size', titleSize); cy.get('.annotation-text').eq(4).should('have.css', 'font-size', titleSize); cy.get('[data-test-subj="valueFieldNumber"]').eq(1).click().type(titleSizeUpdated); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); cy.get('.annotation-text').eq(1).should('have.css', 'font-size', valueSize); cy.get('.annotation-text').eq(3).should('have.css', 'font-size', valueSize); cy.get('.annotation-text').eq(5).should('have.css', 'font-size', valueSize); cy.get('[data-test-subj="valueFieldNumber"]').eq(2).click().type(valueSizeUpdated); - cy.get('[data-test-subj="visualizeEditorRenderButton"]').click(); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); }); }); -describe('Render stats chart and verify the Text Mode options', () => { +describe('Render stats chart and verify the Text Mode options' , () => { beforeEach(() => { - renderStatsChart(); + renderStatsChart(); }); it('Render stats chart and verify text modes ', () => { - cy.get('[data-text="Names"]').should('have.text', 'Names').click(); - cy.get('[data-text="Values"]').should('have.text', 'Values').click(); - cy.get('[data-text="Values + Names"]').should('have.text', 'Values + Names').click(); + cy.get('[data-text="Names"]').should('have.text', 'Names').click(); + cy.get('[data-text="Values"]').should('have.text', 'Values').click(); + cy.get('[data-text="Values + Names"]').should('have.text', 'Values + Names').click(); + cy.wait(delay); }); }); -describe('Render stats chart and verify the +add threshold button option', () => { +describe('Render stats chart and verify the +add threshold button option' , () => { beforeEach(() => { - renderStatsChart(); + renderStatsChart(); }); - it('Render stats chart and verify the +Add Threshold button for color picker', () => { - cy.get('[data-test-subj="euiColorPickerAnchor"]').click(); - cy.get('.euiColorPickerSwatch.euiColorPicker__swatchSelect').eq(5).click(); + it('Render stats chart and verify the +Add Threshold button for color picker' , () => { + cy.get('[data-test-subj="euiColorPickerAnchor"]').click(); + cy.get('.euiColorPickerSwatch.euiColorPicker__swatchSelect').eq(5).click(); + cy.wait(delay); }); }); -describe('Render stats chart and verify the reset button', () => { +describe('Render stats chart and verify the reset button' , () => { beforeEach(() => { - renderStatsChart(); - }); - - it.only('Render stats chart and test the Reset button functionality', () => { - cy.get('[data-test-subj="valueFieldText"]').click().type(metricUnit); - cy.get('[data-test-subj="valueFieldNumber"]').eq(0).click().type(metricsPrecisionUpdated); - cy.get('[data-test-subj="valueFieldNumber"]').eq(1).click().type(titleSizeUpdated); - cy.get('[data-test-subj="valueFieldNumber"]').eq(2).click().type(valueSizeUpdated); - cy.get('[data-test-subj="visualizeEditorRenderButton"]').click(); - cy.get('[data-test-subj="euiColorPickerAnchor"]').click(); - cy.get('.euiColorPickerSwatch.euiColorPicker__swatchSelect').eq(5).click(); - cy.get('[data-test-subj="visualizeEditorResetButton"]').click(); - }); -}); + renderStatsChart(); + }); + + it('Render stats chart and test the Reset button functionality' , () => { + cy.get('[data-test-subj="valueFieldText"]').click().type(metricUnit); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); + cy.get('[data-test-subj="valueFieldNumber"]').eq(0).click().type(metricsPrecisionUpdated); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); + cy.get('[data-test-subj="valueFieldNumber"]').eq(1).click().type(titleSizeUpdated); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); + cy.get('[data-test-subj="valueFieldNumber"]').eq(2).click().type(valueSizeUpdated); + cy.get('.euiSpacer.euiSpacer--s').eq(12).click(); + cy.get('[data-test-subj="euiColorPickerAnchor"]').click(); + cy.get('.euiColorPickerSwatch.euiColorPicker__swatchSelect').eq(5).click(); + cy.get('[data-test-subj="visualizeEditorResetButton"]').click(); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/common/constants/colors.ts b/dashboards-observability/common/constants/colors.ts index 76cf9c9f1..3845e41a0 100644 --- a/dashboards-observability/common/constants/colors.ts +++ b/dashboards-observability/common/constants/colors.ts @@ -166,7 +166,7 @@ export const COLOR_PALETTES = [ type: 'gradient', }, ]; -export const HEX_CONTRAST_COLOR = 0xFFFFFF; +export const HEX_CONTRAST_COLOR = 0xffffff; export const PIE_PALETTES = [ { value: DEFAULT_PALETTE, @@ -177,10 +177,12 @@ export const PIE_PALETTES = [ value: SINGLE_COLOR_PALETTE, title: 'Single Color', type: 'text', - } + }, ]; export const HEATMAP_PALETTE_COLOR = { name: REDS_PALETTE.label, color: REDS_PALETTE.label }; export const HEATMAP_SINGLE_COLOR = { name: 'singleColor', color: '#000000' }; export const OPACITY = 'opacity'; export const SPECTRUM = 'spectrum'; +export const COLOR_BLACK = 'rgb(0,0,0)'; +export const COLOR_WHITE = 'rgb(255,255,255)'; diff --git a/dashboards-observability/common/constants/explorer.ts b/dashboards-observability/common/constants/explorer.ts index 76d0ef80f..6f6d758b0 100644 --- a/dashboards-observability/common/constants/explorer.ts +++ b/dashboards-observability/common/constants/explorer.ts @@ -3,7 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { htmlIdGenerator } from '@elastic/eui'; import { VIS_CHART_TYPES } from './shared'; +import { ThresholdUnitType } from '../../public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds'; + export const EVENT_ANALYTICS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugin/event-analytics/'; export const OPEN_TELEMETRY_LOG_CORRELATION_LINK = @@ -79,6 +82,7 @@ export const PLOTLY_GAUGE_COLUMN_NUMBER = 4; export const APP_ANALYTICS_TAB_ID_REGEX = /application-analytics-tab.+/; export const DEFAULT_AVAILABILITY_QUERY = 'stats count() by span( timestamp, 1h )'; export const ADD_BUTTON_TEXT = '+ Add color theme'; +export const NUMBER_INPUT_MIN_LIMIT = 1; export const VIZ_CONTAIN_XY_AXIS = [ VIS_CHART_TYPES.Bar, @@ -175,3 +179,65 @@ export const DEFAULT_PIE_CHART_PARAMETERS: DefaultPieChartParameterProps = { }; export const GROUPBY = 'dimensions'; export const AGGREGATIONS = 'series'; + +// stats constants +export const STATS_GRID_SPACE_BETWEEN_X_AXIS = 0.01; +export const STATS_GRID_SPACE_BETWEEN_Y_AXIS = 100; +export const STATS_REDUCE_VALUE_SIZE_PERCENTAGE = 0.08; +export const STATS_REDUCE_TITLE_SIZE_PERCENTAGE = 0.05; +export const STATS_REDUCE_SERIES_UNIT_SIZE_PERCENTAGE = 0.2; +export const STATS_SERIES_UNIT_SUBSTRING_LENGTH = 3; +export const STATS_AXIS_MARGIN = { + l: 0, + r: 0, + b: 0, + t: 80, +}; + +export const STATS_ANNOTATION = { + xref: 'paper', + yref: 'paper', + showarrow: false, +}; + +export interface DefaultStatsChartParametersProps { + DefaultTextMode: string; + DefaultOrientation: string; + DefaultTitleSize: number; + DefaultChartType: string; + TextAlignment: string; + DefaultPrecision: number; + DefaultValueSize: number; + BaseThreshold: ThresholdUnitType; +} + +export const DEFAULT_STATS_CHART_PARAMETERS: DefaultStatsChartParametersProps = { + DefaultTextMode: 'auto', + DefaultOrientation: 'auto', + DefaultTitleSize: 30, + DefaultValueSize: 80, + DefaultChartType: 'auto', + TextAlignment: 'auto', + DefaultPrecision: 1, + BaseThreshold: { + thid: htmlIdGenerator('thr')(), + name: 'Base', + color: '#3CA1C7', + value: 0, + isReadOnly: true, + }, +}; + +export enum ConfigChartOptionsEnum { + palettePicker = 'palettePicker', + singleColorPicker = 'singleColorPicker', + colorpicker = 'colorpicker', + treemapColorPicker = 'treemapColorPicker', + input = 'input', + textInput = 'textInput', + slider = 'slider', + switchButton = 'switchButton', + buttons = 'buttons', +} + +export const CUSTOM_LABEL = 'customLabel'; diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index 71f46f706..eeed52377 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import CSS from 'csstype'; +import { IField } from '../../common/types/explorer'; // Client route export const PPL_BASE = '/api/ppl'; @@ -82,6 +83,7 @@ export enum VIS_CHART_TYPES { TreeMap = 'tree_map', Scatter = 'scatter', LogsView = 'logs_view', + Stats = 'stats', } export const NUMERICAL_FIELDS = ['short', 'integer', 'long', 'float', 'double']; @@ -98,9 +100,10 @@ export const ENABLED_VIS_TYPES = [ VIS_CHART_TYPES.Histogram, VIS_CHART_TYPES.Scatter, VIS_CHART_TYPES.LogsView, + VIS_CHART_TYPES.Stats, ]; -//Live tail constants +// Live tail constants export const LIVE_OPTIONS = [ { label: '5s', diff --git a/dashboards-observability/common/query_manager/ast/builder/stats_builder.ts b/dashboards-observability/common/query_manager/ast/builder/stats_builder.ts index 201431b27..b4d173f1b 100644 --- a/dashboards-observability/common/query_manager/ast/builder/stats_builder.ts +++ b/dashboards-observability/common/query_manager/ast/builder/stats_builder.ts @@ -25,7 +25,7 @@ import { statsChunk, SpanExpressionChunk, } from '../types'; - +import { CUSTOM_LABEL } from '../../../../common/constants/explorer'; export class StatsBuilder implements QueryBuilder { constructor(private statsChunk: statsChunk) {} @@ -33,16 +33,16 @@ export class StatsBuilder implements QueryBuilder { // return a new stats subtree return new Aggregations( 'stats_command', - [] as Array, + [] as PPLNode[], !isEmpty(this.statsChunk.partitions) ? this.buildParttions(this.statsChunk.partitions) : '', !isEmpty(this.statsChunk.all_num) ? this.buildAllNum(this.statsChunk.all_num) : '', !isEmpty(this.statsChunk.delim) ? this.buildDelim(this.statsChunk.delim) : '', !isEmpty(this.statsChunk.aggregations) ? this.buildAggList(this.statsChunk.aggregations) - : ([] as Array), + : ([] as PPLNode[]), !isEmpty(this.statsChunk.groupby) ? this.buildGroupList(this.statsChunk.groupby) - : new GroupBy('stats_by_clause', [] as Array, [], null), + : new GroupBy('stats_by_clause', [] as PPLNode[], [], null), !isEmpty(this.statsChunk.dedup_split_value) ? this.buildDedupSplitValue(this.statsChunk.dedup_split_value) : '' @@ -71,7 +71,7 @@ export class StatsBuilder implements QueryBuilder { /** * Aggregations */ - buildAggList(aggregations: Array) { + buildAggList(aggregations: StatsAggregationChunk[]) { return aggregations.map((aggregation) => { return this.buildAggTerm(aggregation); }); @@ -80,7 +80,7 @@ export class StatsBuilder implements QueryBuilder { buildAggTerm(aggTerm: StatsAggregationChunk) { return new AggregateTerm( 'stats_agg_term', - [] as Array, + [] as PPLNode[], this.buildAggregateFunction(aggTerm.function), aggTerm.function_alias ); @@ -89,7 +89,7 @@ export class StatsBuilder implements QueryBuilder { buildAggregateFunction(aggFunction: StatsAggregationFunctionChunk) { return new AggregateFunction( 'stats_function', - [] as Array, + [] as PPLNode[], aggFunction.name, aggFunction.value_expression, aggFunction.percentile_agg_function @@ -102,31 +102,31 @@ export class StatsBuilder implements QueryBuilder { buildGroupList(groupby: GroupByChunk) { return new GroupBy( 'stats_by_clause', - [] as Array, + [] as PPLNode[], this.buildFieldList(groupby.group_fields), groupby.span ? this.buildSpan(groupby.span) : null ); } - buildFieldList(group_fields: Array) { + buildFieldList(group_fields: GroupField[]) { return group_fields.map((gf: GroupField) => { - return new Field('field_expression', [] as Array, gf.name); + return new Field('field_expression', [] as PPLNode[], gf.name); }); } buildSpan(span: SpanChunk) { return new Span( 'span_clause', - [] as Array, + [] as PPLNode[], this.buildeSpanExpression(span.span_expression), - span.alias + span[CUSTOM_LABEL] ); } buildeSpanExpression(spanExpression: SpanExpressionChunk) { return new SpanExpression( 'span_expression', - [] as Array, + [] as PPLNode[], spanExpression.field, spanExpression.literal_value, spanExpression.time_unit diff --git a/dashboards-observability/common/query_manager/ast/expression/AggregateTerm.ts b/dashboards-observability/common/query_manager/ast/expression/AggregateTerm.ts index a19b488d5..ceea30e5f 100644 --- a/dashboards-observability/common/query_manager/ast/expression/AggregateTerm.ts +++ b/dashboards-observability/common/query_manager/ast/expression/AggregateTerm.ts @@ -4,13 +4,13 @@ */ import { PPLNode } from '../node'; - +import { CUSTOM_LABEL } from '../../../../common/constants/explorer'; export class AggregateTerm extends PPLNode { constructor( name: string, - children: Array, + children: PPLNode[], private statsFunction: PPLNode, - private alias: string + private customLabel: string ) { super(name, children); } @@ -18,13 +18,15 @@ export class AggregateTerm extends PPLNode { getTokens() { return { function: this.statsFunction.getTokens(), - alias: this.alias, + [CUSTOM_LABEL]: this[CUSTOM_LABEL], }; } toString(): string { - if (this.alias) { - return `${this.statsFunction.toString()}${this.alias ? ` as ${this.alias}` : ''}`; + if (this[CUSTOM_LABEL]) { + return `${this.statsFunction.toString()}${ + this[CUSTOM_LABEL] ? ` as ${this[CUSTOM_LABEL]}` : '' + }`; } return `${this.statsFunction.toString()}`; } diff --git a/dashboards-observability/common/query_manager/ast/expression/span.ts b/dashboards-observability/common/query_manager/ast/expression/span.ts index d2a570915..278ca18eb 100644 --- a/dashboards-observability/common/query_manager/ast/expression/span.ts +++ b/dashboards-observability/common/query_manager/ast/expression/span.ts @@ -8,9 +8,9 @@ import { PPLNode } from '../node'; export class Span extends PPLNode { constructor( name: string, - children: Array, + children: PPLNode[], private spanExpression: PPLNode, - private alias: string + private customLabel: string ) { super(name, children); } @@ -18,11 +18,11 @@ export class Span extends PPLNode { getTokens() { return { span_expression: this.spanExpression.getTokens(), - alias: this.alias, + customLabel: this.customLabel, }; } toString(): string { - return `${this.spanExpression.toString()}${this.alias ? ` as ${this.alias}` : ''}`; + return `${this.spanExpression.toString()}${this.customLabel ? ` as ${this.customLabel}` : ''}`; } } diff --git a/dashboards-observability/common/query_manager/ast/types/stats.ts b/dashboards-observability/common/query_manager/ast/types/stats.ts index 3127c5c6f..13a77eb11 100644 --- a/dashboards-observability/common/query_manager/ast/types/stats.ts +++ b/dashboards-observability/common/query_manager/ast/types/stats.ts @@ -22,7 +22,7 @@ export interface GroupField { } export interface SpanChunk { - alias: string; + customLabel: string; span_expression: SpanExpressionChunk; } @@ -34,12 +34,12 @@ export interface SpanExpressionChunk { } export interface GroupByChunk { - group_fields: Array; + group_fields: GroupField[]; span: SpanChunk | null; } export interface statsChunk { - aggregations: Array; + aggregations: StatsAggregationChunk[]; groupby: GroupByChunk; partitions: ExpressionChunk; all_num: ExpressionChunk; @@ -54,15 +54,15 @@ export interface ExpressionChunk { } export interface DataConfigSeries { - alias: string; + customLabel: string; label: string; name: string; aggregation: string; } export interface AggregationConfigurations { - series: Array; - dimensions: Array; + series: DataConfigSeries[]; + dimensions: GroupField[]; span: SpanChunk; } diff --git a/dashboards-observability/common/query_manager/utils/index.ts b/dashboards-observability/common/query_manager/utils/index.ts index 5d84ed72e..b5f22013c 100644 --- a/dashboards-observability/common/query_manager/utils/index.ts +++ b/dashboards-observability/common/query_manager/utils/index.ts @@ -4,6 +4,7 @@ */ import { AggregationConfigurations, PreviouslyParsedStaleStats } from '../ast/types'; +import { CUSTOM_LABEL } from '../../../common/constants/explorer'; export const composeAggregations = ( aggConfig: AggregationConfigurations, @@ -11,7 +12,7 @@ export const composeAggregations = ( ) => { return { aggregations: aggConfig.series.map((metric) => ({ - function_alias: metric.alias, + function_alias: metric[CUSTOM_LABEL], function: { name: metric.aggregation, value_expression: metric.name, @@ -31,12 +32,12 @@ export const composeAggregations = ( const composeSpan = (spanConfig) => { return { - alias: spanConfig.alias ?? '', + [CUSTOM_LABEL]: spanConfig[CUSTOM_LABEL] ?? '', span_expression: { type: spanConfig.time_field[0]?.type ?? 'timestamp', field: spanConfig.time_field[0]?.name ?? 'timestamp', time_unit: spanConfig.unit[0]?.value ?? 'd', - literal_value: spanConfig.interval ?? 1 - } + literal_value: spanConfig.interval ?? 1, + }, }; }; diff --git a/dashboards-observability/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts index d3c6ad1f2..16fe99db0 100644 --- a/dashboards-observability/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -282,6 +282,7 @@ export interface ConfigListEntry { name: string; side: string; type: string; + alias?: string; } export interface HistogramConfigList { diff --git a/dashboards-observability/public/components/common/helpers/ppl_docs/language_structure/identifiers.ts b/dashboards-observability/public/components/common/helpers/ppl_docs/language_structure/identifiers.ts index e52037913..1a931f642 100644 --- a/dashboards-observability/public/components/common/helpers/ppl_docs/language_structure/identifiers.ts +++ b/dashboards-observability/public/components/common/helpers/ppl_docs/language_structure/identifiers.ts @@ -9,7 +9,7 @@ export const pplIdentifiers = `## Indentifiers ### **Introduction** Identifiers are used for naming your database objects, such as index -name, field name, alias etc. Basically there are two types of +name, field name, customLabel etc. Basically there are two types of identifiers: regular identifiers and delimited identifiers. ### **Regular Identifiers** @@ -104,4 +104,4 @@ same as what is stored in OpenSearch. For example, if you run \`source=Accounts\`, it will end up with an index not found exception from our plugin because the actual index name is under lower case. -` \ No newline at end of file +`; diff --git a/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx b/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx index 2c01a585c..6bd3cb7c4 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx @@ -53,6 +53,7 @@ import { DATE_PICKER_FORMAT, GROUPBY, AGGREGATIONS, + CUSTOM_LABEL, } from '../../../../common/constants/explorer'; import { PPL_STATS_REGEX, @@ -81,7 +82,11 @@ import { getVizContainerProps } from '../../visualizations/charts/helpers'; import { parseGetSuggestions, onItemSelect } from '../../common/search/autocomplete_logic'; import { formatError } from '../utils'; import { sleep } from '../../common/live_tail/live_tail_button'; -import { statsChunk, GroupByChunk } from '../../../../common/query_manager/ast/types'; +import { + statsChunk, + GroupByChunk, + StatsAggregationChunk, +} from '../../../../common/query_manager/ast/types'; const TYPE_TAB_MAPPING = { [SAVED_QUERY]: TAB_EVENT_ID, @@ -497,7 +502,6 @@ export const Explorer = ({ handleQuerySearch(availability); }; - /** * Toggle fields between selected and unselected sets * @param field field to be toggled @@ -899,6 +903,7 @@ export const Explorer = ({ label: agg.function?.value_expression, name: agg.function?.value_expression, aggregation: agg.function?.name, + [CUSTOM_LABEL]: agg[CUSTOM_LABEL], })), [GROUPBY]: groupByToken?.group_fields?.map((agg) => ({ label: agg.name ?? '', @@ -927,7 +932,6 @@ export const Explorer = ({ if (selectedContentTabId === TAB_CHART_ID) { // parse stats section on every search const statsTokens = queryManager.queryParser().parse(tempQuery).getStats(); - const updatedDataConfig = getUpdatedDataConfig(statsTokens); await dispatch( changeVizConfig({ diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap index 2ab71e6a1..988944911 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -3311,6 +3311,296 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, }, }, + Object { + "category": "Visualizations", + "categoryaxis": "xaxis", + "charttype": "auto", + "component": [Function], + "editorconfig": Object { + "panelTabs": Array [ + Object { + "editor": [Function], + "id": "data-panel", + "mapTo": "dataConfig", + "name": "Style", + "sections": Array [ + Object { + "editor": [Function], + "id": "tooltip_options", + "mapTo": "tooltipOptions", + "name": "Tooltip options", + "schemas": Array [ + Object { + "component": null, + "mapTo": "tooltipMode", + "name": "Tooltip mode", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "show", + "name": "Show", + }, + ], + "options": Array [ + Object { + "id": "show", + "name": "Show", + }, + Object { + "id": "hidden", + "name": "Hidden", + }, + ], + }, + }, + Object { + "component": null, + "mapTo": "tooltipText", + "name": "Tooltip text", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "all", + "name": "All", + }, + ], + "options": Array [ + Object { + "id": "all", + "name": "All", + }, + Object { + "id": "x", + "name": "Dimension", + }, + Object { + "id": "y", + "name": "Series", + }, + ], + }, + }, + ], + }, + Object { + "editor": [Function], + "id": "chart_styles", + "mapTo": "chartStyles", + "name": "Chart styles", + "schemas": Array [ + Object { + "component": [Function], + "eleType": "buttons", + "mapTo": "chartType", + "name": "Chart type", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "auto", + "name": "Auto", + }, + ], + "options": Array [ + Object { + "id": "auto", + "name": "Auto", + }, + Object { + "id": "horizontal", + "name": "Horizontal", + }, + Object { + "id": "text", + "name": "Text mode", + }, + ], + }, + }, + Object { + "component": [Function], + "eleType": "buttons", + "mapTo": "orientation", + "name": "Orientation", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "auto", + "name": "Auto", + }, + ], + "options": Array [ + Object { + "id": "auto", + "name": "Auto", + }, + Object { + "id": "h", + "name": "Horizontal", + }, + Object { + "id": "v", + "name": "Vertical", + }, + ], + }, + }, + Object { + "component": [Function], + "eleType": "textInput", + "mapTo": "seriesUnits", + "name": "Series units", + "title": "Series units", + }, + Object { + "component": [Function], + "eleType": "input", + "mapTo": "precisionValue", + "name": "Series precision", + "props": Object { + "minLimit": 0, + }, + "title": "Series precision", + }, + Object { + "component": [Function], + "eleType": "input", + "mapTo": "titleSize", + "name": "Title size", + "title": "Title size", + }, + Object { + "component": [Function], + "eleType": "input", + "mapTo": "valueSize", + "name": "Value size", + "title": "Value size", + }, + Object { + "component": [Function], + "eleType": "buttons", + "mapTo": "textMode", + "name": "Text mode", + "props": Object { + "defaultSelections": Array [ + Object { + "id": "auto", + "name": "Values + Names", + }, + ], + "options": Array [ + Object { + "id": "auto", + "name": "Auto", + }, + Object { + "id": "names", + "name": "Names", + }, + Object { + "id": "values", + "name": "Values", + }, + Object { + "id": "values+names", + "name": "Values + Names", + }, + ], + }, + }, + ], + }, + Object { + "defaultState": Array [ + Object { + "color": "#3CA1C7", + "isReadOnly": true, + "name": "Base", + "thid": "random_html_id", + "value": 0, + }, + ], + "editor": [Function], + "id": "thresholds", + "mapTo": "thresholds", + "name": "Thresholds", + "schemas": Array [], + }, + ], + }, + Object { + "content": Array [], + "editor": [Function], + "id": "style-panel", + "mapTo": "layoutConfig", + "name": "Layout", + }, + Object { + "editor": [Function], + "id": "availability-panel", + "mapTo": "availabilityConfig", + "name": "Availability", + }, + ], + }, + "fulllabel": "Stats", + "icon": [Function], + "icontype": "stats", + "id": "stats", + "label": "Stats", + "name": "stats", + "orientation": "auto", + "precisionvalue": 1, + "seriesaxis": "yaxis", + "textmode": "auto", + "titlesize": 30, + "type": "stats", + "valuesize": 80, + "visconfig": Object { + "config": Object { + "displaylogo": false, + "responsive": true, + }, + "layout": Object { + "colorway": Array [ + "#3CA1C7", + "#8C55A3", + "#DB748A", + "#F2BE4B", + "#68CCC2", + "#2A7866", + "#843769", + "#374FB8", + "#BD6F26", + "#4C636F", + ], + "height": 1180, + "legend": Object { + "orientation": "v", + "traceorder": "normal", + }, + "margin": Object { + "b": 30, + "l": 60, + "pad": 0, + "r": 30, + "t": 50, + }, + "paper_bgcolor": "rgba(0, 0, 0, 0)", + "plot_bgcolor": "rgba(0, 0, 0, 0)", + "showlegend": true, + "xaxis": Object { + "fixedrange": true, + "showgrid": false, + "visible": true, + }, + "yaxis": Object { + "fixedrange": true, + "showgrid": false, + "visible": true, + }, + }, + }, + }, ] } placeholder="Select a chart" @@ -13436,7 +13726,6 @@ exports[`Config panel component Renders config panel with visualization data 1`] data-test-subj="valueFieldNumber" fullWidth={true} id="random_html_id" - min={1} onBlur={[Function]} onChange={[Function]} placeholder="auto" @@ -13459,7 +13748,6 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiFieldNumber euiFieldNumber--fullWidth" data-test-subj="valueFieldNumber" id="random_html_id" - min={1} onBlur={[Function]} onChange={[Function]} placeholder="auto" diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx index 365a13785..58f9d715e 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx @@ -104,7 +104,7 @@ export const ConfigPanel = ({ // To check, If user empty any of the value options const isValidValueOptionConfigSelected = useMemo(() => { const valueOptions = vizConfigs.dataConfig?.valueOptions; - const { TreeMap, Gauge, HeatMap } = VIS_CHART_TYPES; + const { TreeMap, Gauge, HeatMap, Stats } = VIS_CHART_TYPES; const isValidValueOptionsXYAxes = VIZ_CONTAIN_XY_AXIS.includes(curVisId) && valueOptions?.xaxis?.length !== 0 && @@ -115,7 +115,7 @@ export const ConfigPanel = ({ curVisId === TreeMap && valueOptions?.childField?.length !== 0 && valueOptions?.valueField?.length !== 0, - gauge: true, + gauge: curVisId === Gauge && valueOptions?.yaxis?.length !== 0, heatmap: Boolean( curVisId === HeatMap && valueOptions?.metrics && valueOptions.metrics?.length !== 0 ), @@ -125,6 +125,7 @@ export const ConfigPanel = ({ pie: isValidValueOptionsXYAxes, scatter: isValidValueOptionsXYAxes, logs_view: true, + stats: curVisId === Stats && valueOptions?.yaxis?.length !== 0, }; return isValid_valueOptions[curVisId]; }, [vizConfigs.dataConfig]); @@ -249,7 +250,7 @@ export const ConfigPanel = ({ const selectedOption = find(memorizedVisualizationTypes, (v) => { return v.id === visId; }); - selectedOption['iconType'] = selectedOption.icontype; + selectedOption.iconType = selectedOption.icontype; return selectedOption; }, [memorizedVisualizationTypes] diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx index e5f61773b..ea5cdfe7e 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_chart_options.tsx @@ -7,6 +7,10 @@ import React, { useMemo, useCallback, Fragment } from 'react'; import { EuiAccordion, EuiSpacer, EuiForm } from '@elastic/eui'; import { PanelItem } from './config_panel_item'; import { SPECTRUM, OPACITY } from '../../../../../../../../common/constants/colors'; +import { + ConfigChartOptionsEnum, + NUMBER_INPUT_MIN_LIMIT, +} from '../../../../../../../../common/constants/explorer'; export const ConfigChartOptions = ({ visualizations, @@ -15,8 +19,7 @@ export const ConfigChartOptions = ({ handleConfigChange, }: any) => { const { data } = visualizations; - const { data: vizData = {}, metadata: { fields = [] } = {} } = data?.rawVizData; - const { dataConfig = {}, layoutConfig = {} } = visualizations?.data?.userConfigs; + const { data: vizData = {}, metadata: { fields = [] } = {}, tree_map } = data?.rawVizData; const handleConfigurationChange = useCallback( (stateFiledName) => { @@ -50,87 +53,116 @@ export const ConfigChartOptions = ({ ...schema.props, }; const DimensionComponent = schema.component || PanelItem; - if (schema.eleType === 'palettePicker') { - params = { - ...params, - colorPalettes: schema.options || [], - selectedColor: vizState[schema.mapTo] || schema.defaultState, - onSelectChange: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'singleColorPicker') { - params = { - ...params, - selectedColor: vizState[schema.mapTo] || schema.defaultState, - onSelectChange: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'colorpicker') { - params = { - ...params, - selectedColor: vizState[schema.mapTo] || schema?.defaultState, - colorPalettes: schema.options || [], - onSelectChange: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'treemapColorPicker') { - params = { - ...params, - selectedColor: vizState[schema.mapTo] || schema?.defaultState, - colorPalettes: schema.options || [], - numberOfParents: - (dataConfig?.valueOptions?.dimensions !== undefined && - dataConfig.valueOptions.dimensions[0].parentFields.length) | 0, - onSelectChange: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'input') { - params = { - ...params, - currentValue: vizState[schema.mapTo] || '', - numValue: vizState[schema.mapTo] || '', - handleInputChange: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'slider') { - params = { - ...params, - maxRange: schema.props.max, - currentRange: vizState[schema.mapTo] || schema?.defaultState, - handleSliderChange: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'switchButton') { - params = { - ...params, - title: schema.name, - currentValue: vizState[schema.mapTo], - onToggle: handleConfigurationChange(schema.mapTo), - }; - } else if (schema.eleType === 'buttons') { - params = { - ...params, - title: schema.name, - legend: schema.name, - groupOptions: schema?.props?.options.map((btn: { name: string }) => ({ - ...btn, - label: btn.name, - })), - idSelected: vizState[schema.mapTo] || schema?.props?.defaultSelections[0]?.id, - handleButtonChange: handleConfigurationChange(schema.mapTo), - }; - } else { - params = { - ...params, - paddingTitle: schema.name, - advancedTitle: 'advancedTitle', - dropdownList: - schema?.options?.map((option) => ({ ...option })) || - fields.map((item) => ({ ...item })), - onSelectChange: handleConfigurationChange(schema.mapTo), - isSingleSelection: schema.isSingleSelection, - selectedAxis: vizState[schema.mapTo] || schema.defaultState, - }; + + switch (schema.eleType) { + case ConfigChartOptionsEnum.palettePicker: + params = { + ...params, + colorPalettes: schema.options || [], + selectedColor: vizState[schema.mapTo] || schema.defaultState, + onSelectChange: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.singleColorPicker: + params = { + ...params, + selectedColor: vizState[schema.mapTo] || schema.defaultState, + onSelectChange: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.colorpicker: + params = { + ...params, + selectedColor: vizState[schema.mapTo] || schema?.defaultState, + colorPalettes: schema.options || [], + onSelectChange: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.treemapColorPicker: + params = { + ...params, + selectedColor: vizState[schema.mapTo] || schema?.defaultState, + colorPalettes: schema.options || [], + numberOfParents: + (tree_map?.dataConfig?.dimensions !== undefined && + tree_map?.dataConfig.dimensions[0].parentFields.length) | 0, + onSelectChange: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.input: + params = { + ...params, + currentValue: vizState[schema.mapTo] || '', + numValue: vizState[schema.mapTo] || '', + handleInputChange: handleConfigurationChange(schema.mapTo), + minLimit: schema.props?.hasOwnProperty('minLimit') + ? schema.props.minLimit + : NUMBER_INPUT_MIN_LIMIT, + }; + break; + + case ConfigChartOptionsEnum.textInput: + params = { + ...params, + currentValue: vizState[schema.mapTo] || '', + name: schema.mapTo, + handleInputChange: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.slider: + params = { + ...params, + maxRange: schema.props.max, + currentRange: vizState[schema.mapTo] || schema?.defaultState, + handleSliderChange: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.switchButton: + params = { + ...params, + title: schema.name, + currentValue: vizState[schema.mapTo], + onToggle: handleConfigurationChange(schema.mapTo), + }; + break; + + case ConfigChartOptionsEnum.buttons: + params = { + ...params, + title: schema.name, + legend: schema.name, + groupOptions: schema?.props?.options.map((btn: { name: string }) => ({ + ...btn, + label: btn.name, + })), + idSelected: vizState[schema.mapTo] || schema?.props?.defaultSelections[0]?.id, + handleButtonChange: handleConfigurationChange(schema.mapTo), + }; + break; + + default: + params = { + ...params, + paddingTitle: schema.name, + advancedTitle: 'advancedTitle', + dropdownList: schema?.options || fields, + onSelectChange: handleConfigurationChange(schema.mapTo), + isSingleSelection: schema.isSingleSelection, + selectedAxis: vizState[schema.mapTo] || schema.defaultState, + }; + break; } return ( - + diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_number_input.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_number_input.tsx index f9d283493..af67533bd 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_number_input.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_number_input.tsx @@ -10,17 +10,18 @@ interface InputFieldProps { title: string; numValue: number; handleInputChange: (value?: any) => void; + minLimit?: number; } export const InputFieldItem: React.FC = ({ title, numValue, handleInputChange, + minLimit, }) => { - const [fieldValue, setFieldValue] = useState(numValue); + const [fieldValue, setFieldValue] = useState(''); useEffect(() => { - setFieldValue(''); if (numValue !== undefined || numValue !== '') { setFieldValue(numValue); } @@ -37,7 +38,7 @@ export const InputFieldItem: React.FC = ({ fullWidth placeholder="auto" value={fieldValue} - min={1} + min={minLimit} onChange={(e) => setFieldValue(e.target.value)} onBlur={() => handleInputChange(fieldValue)} data-test-subj="valueFieldNumber" diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_text_input.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_text_input.tsx new file mode 100644 index 000000000..0aa634b2a --- /dev/null +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_text_input.tsx @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { EuiFieldText, EuiTitle, EuiSpacer, htmlIdGenerator } from '@elastic/eui'; + +interface InputFieldProps { + name: string; + title: string; + currentValue: string; + handleInputChange: (value: string) => void; +} + +export const TextInputFieldItem: React.FC = ({ + name, + title, + currentValue, + handleInputChange, +}) => { + const [fieldValue, setFieldValue] = useState(''); + + useEffect(() => { + if (currentValue !== undefined || currentValue !== '') { + setFieldValue(currentValue); + } + }, [currentValue]); + + return ( + <> + +

{title}

+
+ + setFieldValue(e.target.value)} + onBlur={() => handleInputChange(fieldValue)} + data-test-subj="valueFieldText" + /> + + ); +}; diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds.tsx index 3aa60c02b..013b25b0c 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds.tsx @@ -25,6 +25,7 @@ export interface ThresholdUnitType { name: string; color: string; value: number; + isReadOnly?: boolean; } export const ConfigThresholds = ({ @@ -35,6 +36,7 @@ export const ConfigThresholds = ({ sectionName = 'Thresholds', props, }: any) => { + const { type } = visualizations?.vis; const addButtonText = '+ Add threshold'; const AddButtonTextWrapper = () => props?.maxLimit && !isEmpty(vizState) && vizState.length === props.maxLimit ? ( @@ -51,6 +53,7 @@ export const ConfigThresholds = ({ name: '', color: '#FC0505', value: 0, + isReadOnly: false, }; }; @@ -85,7 +88,6 @@ export const ConfigThresholds = ({ }, [vizState, handleConfigChange] ); - return ( @@ -139,14 +142,17 @@ export const ConfigThresholds = ({ onChange={handleThresholdChange(thr.thid, 'value')} aria-label="Input threshold value" data-test-subj="valueFieldNumber" + readOnly={thr.isReadOnly} /> - - - - - + {!thr.isReadOnly && ( + + + + + + )} diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/data_configurations_panel.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/data_configurations_panel.tsx index ecadf78b0..1914ba7bb 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/data_configurations_panel.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/data_configurations_panel.tsx @@ -31,6 +31,7 @@ import { GROUPBY, RAW_QUERY, TIME_INTERVAL_OPTIONS, + CUSTOM_LABEL, } from '../../../../../../../../common/constants/explorer'; import { ButtonGroupItem } from './config_button_group'; import { VIS_CHART_TYPES } from '../../../../../../../../common/constants/shared'; @@ -44,7 +45,7 @@ const initialDimensionEntry = { }; const initialSeriesEntry = { - alias: '', + [CUSTOM_LABEL]: '', label: '', name: '', aggregation: 'count', @@ -115,7 +116,7 @@ export const DataConfigPanelItem = ({ let listItem = { ...list[name][index] }; listItem = { ...listItem, - [field === 'custom_label' ? 'alias' : field]: value, + [field]: value, }; if (field === 'label') { listItem.name = value; @@ -314,9 +315,9 @@ export const DataConfigPanelItem = ({ - updateList(e.target.value, index, sectionName, 'custom_label') + updateList(e.target.value, index, sectionName, CUSTOM_LABEL) } aria-label="Use aria labels when no actual label is in use" /> diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts index eb3507d85..9576f9a0b 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/index.ts @@ -23,3 +23,4 @@ export { SliderConfig } from './config_style_slider'; export { ConfigColorTheme } from './config_color_theme'; export { SwitchButton } from './config_switch_button'; export { ButtonGroupItem } from './config_button_group'; +export { TextInputFieldItem } from './config_text_input'; \ No newline at end of file diff --git a/dashboards-observability/public/components/event_analytics/utils/utils.tsx b/dashboards-observability/public/components/event_analytics/utils/utils.tsx index 41fe02467..98554e60c 100644 --- a/dashboards-observability/public/components/event_analytics/utils/utils.tsx +++ b/dashboards-observability/public/components/event_analytics/utils/utils.tsx @@ -12,11 +12,12 @@ import { IExplorerFields, IField, GetTooltipHoverInfoType, + ConfigListEntry, } from '../../../../common/types/explorer'; import { DocViewRow, IDocType } from '../explorer/events_views'; import { HttpStart } from '../../../../../../src/core/public'; import PPLService from '../../../services/requests/ppl'; -import { TIME_INTERVAL_OPTIONS } from '../../../../common/constants/explorer'; +import { CUSTOM_LABEL, TIME_INTERVAL_OPTIONS } from '../../../../common/constants/explorer'; import { PPL_DATE_FORMAT, PPL_INDEX_REGEX } from '../../../../common/constants/shared'; import { ConfigTooltip } from '../explorer/visualizations/config_panel/config_panes/config_controls'; @@ -368,3 +369,20 @@ export const getTooltipHoverInfo = ({ tooltipMode, tooltipText }: GetTooltipHove } return tooltipText; }; + +export const filterDataConfigParameter = (parameter: ConfigListEntry[]) => + parameter.filter((configItem: ConfigListEntry) => configItem.label); + +export const getRoundOf = (value: number, places: number) => value.toFixed(places); + +export const getPropName = (queriedVizObj: { + customLabel?: string; + aggregation: string; + name: string; + label: string; +}) => { + if (queriedVizObj[CUSTOM_LABEL] === '' || queriedVizObj[CUSTOM_LABEL] === undefined) { + return `${queriedVizObj.aggregation}(${queriedVizObj.name})`; + } + return queriedVizObj[CUSTOM_LABEL]; +}; diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/stats.test.tsx.snap b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/stats.test.tsx.snap new file mode 100644 index 000000000..6cb9a040b --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/stats.test.tsx.snap @@ -0,0 +1,595 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Stats component Renders stats component 1`] = ` + + + +
+ +
+ +
+ + + + + + +
+ +

+ + + No results found + + +

+
+ +
+ +
+ + + +`; diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/stats.test.tsx b/dashboards-observability/public/components/visualizations/charts/__tests__/stats.test.tsx new file mode 100644 index 000000000..db8d95d96 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/stats.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Stats } from '../stats/stats'; +import { + LAYOUT_CONFIG, + STATS_TEST_VISUALIZATIONS_DATA, +} from '../../../../../test/event_analytics_constants'; + +describe('Stats component', () => { + configure({ adapter: new Adapter() }); + + it('Renders stats component', async () => { + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts b/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts index adc7aeabd..20050dc25 100644 --- a/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts +++ b/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts @@ -17,6 +17,7 @@ import { AGGREGATIONS, GROUPBY, TIME_INTERVAL_OPTIONS, + CUSTOM_LABEL, } from '../../../../../common/constants/explorer'; interface IVizContainerProps { vizId: string; @@ -38,7 +39,7 @@ const initialDimensionEntry = { }; const initialSeriesEntry = { - alias: '', + [CUSTOM_LABEL]: '', label: '', name: '', aggregation: 'count', @@ -62,7 +63,7 @@ const getDefaultXYAxisLabels = (vizFields: IField[], visName: string) => { : [vizFieldsWithLabel[vizFieldsWithLabel.length - 1]]; }; - const mapYaxis = (): { [key: string]: string }[] => + const mapYaxis = (): Array<{ [key: string]: string }> => visName === VIS_CHART_TYPES.Line ? vizFieldsWithLabel.filter((field) => field.type !== 'timestamp') : take( @@ -134,7 +135,7 @@ const defaultUserConfigs = (queryString, visualizationName: string) => { tempUserConfigs = { ...tempUserConfigs, [AGGREGATIONS]: statsTokens.aggregations.map((agg) => ({ - alias: agg.alias, + [CUSTOM_LABEL]: agg[CUSTOM_LABEL], label: agg.function?.value_expression, name: agg.function?.value_expression, aggregation: agg.function?.name, diff --git a/dashboards-observability/public/components/visualizations/charts/lines/line.tsx b/dashboards-observability/public/components/visualizations/charts/lines/line.tsx index 1edc9397e..48ca892e5 100644 --- a/dashboards-observability/public/components/visualizations/charts/lines/line.tsx +++ b/dashboards-observability/public/components/visualizations/charts/lines/line.tsx @@ -14,7 +14,7 @@ import { PLOTLY_COLOR, VIS_CHART_TYPES, } from '../../../../../common/constants/shared'; -import { hexToRgb } from '../../../../components/event_analytics/utils/utils'; +import { hexToRgb, getPropName } from '../../../../components/event_analytics/utils/utils'; import { EmptyPlaceholder } from '../../../event_analytics/explorer/visualizations/shared_components/empty_placeholder'; import { IVisualizationContainerProps } from '../../../../../common/types/explorer'; import { AGGREGATIONS, GROUPBY } from '../../../../../common/constants/explorer'; @@ -145,9 +145,9 @@ export const Line = ({ visualizations, layout, config }: any) => { return { x: queriedVizData[!isEmpty(xaxis) ? xaxis[0]?.label : fields[lastIndex].name], - y: queriedVizData[`${field.aggregation}(${field.name})`], + y: queriedVizData[getPropName(field)], type: isBarMode ? 'bar' : 'scatter', - name: field.label, + name: getPropName(field), mode, ...(!['bar', 'markers'].includes(mode) && fillProperty), line: { diff --git a/dashboards-observability/public/components/visualizations/charts/lines/line_type.ts b/dashboards-observability/public/components/visualizations/charts/lines/line_type.ts index cdcb0fd07..598913d12 100644 --- a/dashboards-observability/public/components/visualizations/charts/lines/line_type.ts +++ b/dashboards-observability/public/components/visualizations/charts/lines/line_type.ts @@ -239,32 +239,28 @@ export const createLineTypeDefinition = (params: any = {}) => ({ visconfig: { layout: { ...sharedConfigs.layout, - ...{ - colorway: PLOTLY_COLOR, - plot_bgcolor: 'rgba(0, 0, 0, 0)', - paper_bgcolor: 'rgba(0, 0, 0, 0)', - xaxis: { - fixedrange: true, - showgrid: false, - visible: true, - }, - yaxis: { - fixedrange: true, - showgrid: false, - visible: true, - }, + colorway: PLOTLY_COLOR, + plot_bgcolor: 'rgba(0, 0, 0, 0)', + paper_bgcolor: 'rgba(0, 0, 0, 0)', + xaxis: { + fixedrange: true, + showgrid: false, + visible: true, + }, + yaxis: { + fixedrange: true, + showgrid: false, + visible: true, }, }, config: { ...sharedConfigs.config, - ...{ - barmode: params.type, - xaxis: { - automargin: true, - }, - yaxis: { - automargin: true, - }, + barmode: params.type, + xaxis: { + automargin: true, + }, + yaxis: { + automargin: true, }, }, }, diff --git a/dashboards-observability/public/components/visualizations/charts/maps/heatmap.tsx b/dashboards-observability/public/components/visualizations/charts/maps/heatmap.tsx index 7b06b2bdc..a290c1276 100644 --- a/dashboards-observability/public/components/visualizations/charts/maps/heatmap.tsx +++ b/dashboards-observability/public/components/visualizations/charts/maps/heatmap.tsx @@ -15,7 +15,11 @@ import { OPACITY, HEATMAP_SINGLE_COLOR, } from '../../../../../common/constants/colors'; -import { hexToRgb, lightenColor } from '../../../../components/event_analytics/utils/utils'; +import { + hexToRgb, + lightenColor, + getPropName, +} from '../../../../components/event_analytics/utils/utils'; import { IVisualizationContainerProps } from '../../../../../common/types/explorer'; import { AGGREGATIONS, GROUPBY } from '../../../../../common/constants/explorer'; @@ -47,7 +51,7 @@ export const HeatMap = ({ visualizations, layout, config }: any) => { isEmpty(zMetrics) || isEmpty(queriedVizData[xaxisField.label]) || isEmpty(queriedVizData[yaxisField.label]) || - isEmpty(queriedVizData[`${zMetrics.aggregation}(${zMetrics.name})`]) || + isEmpty(queriedVizData[getPropName(zMetrics)]) || dataConfig[GROUPBY].length > 2 || dataConfig[AGGREGATIONS].length > 1 ) @@ -91,7 +95,7 @@ export const HeatMap = ({ visualizations, layout, config }: any) => { // maps bukcets to metrics for (let i = 0; i < queriedVizData[xaxisField.label].length; i++) { buckets[`${queriedVizData[xaxisField.label][i]},${queriedVizData[yaxisField.label][i]}`] = - queriedVizData[`${zMetrics.aggregation}(${zMetrics.name})`][i]; + queriedVizData[getPropName(zMetrics)][i]; } // initialize empty 2 dimensional array, inner loop for each xaxis field, outer loop for yaxis diff --git a/dashboards-observability/public/components/visualizations/charts/maps/heatmap_type.ts b/dashboards-observability/public/components/visualizations/charts/maps/heatmap_type.ts index 1bb826a53..0aeeb1061 100644 --- a/dashboards-observability/public/components/visualizations/charts/maps/heatmap_type.ts +++ b/dashboards-observability/public/components/visualizations/charts/maps/heatmap_type.ts @@ -127,11 +127,9 @@ export const createMapsVisDefinition = () => ({ visconfig: { layout: { ...sharedConfigs.layout, - ...{ - plot_bgcolor: 'rgba(0, 0, 0, 0)', - paper_bgcolor: 'rgba(0, 0, 0, 0)', - margin: { left: 60 }, - }, + plot_bgcolor: 'rgba(0, 0, 0, 0)', + paper_bgcolor: 'rgba(0, 0, 0, 0)', + margin: { left: 60 }, }, config: { ...sharedConfigs.config, diff --git a/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx b/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx index 13a4e1611..ed24022b3 100644 --- a/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx +++ b/dashboards-observability/public/components/visualizations/charts/pie/pie.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { isEmpty, find } from 'lodash'; import { Plt } from '../../plotly/plot'; import { EmptyPlaceholder } from '../../../event_analytics/explorer/visualizations/shared_components/empty_placeholder'; -import { getTooltipHoverInfo } from '../../../event_analytics/utils/utils'; +import { getTooltipHoverInfo, getPropName } from '../../../event_analytics/utils/utils'; import { ConfigListEntry, IVisualizationContainerProps, @@ -87,7 +87,7 @@ export const Pie = ({ visualizations, layout, config }: any) => { const pies = useMemo( () => series.map((field: any, index: number) => { - const fieldName = field.alias ? field.alias : `${field.aggregation}(${field.name})`; + const fieldName = getPropName(field); const marker = colorTheme.name !== DEFAULT_PALETTE ? { diff --git a/dashboards-observability/public/components/visualizations/charts/stats/stats.tsx b/dashboards-observability/public/components/visualizations/charts/stats/stats.tsx new file mode 100644 index 000000000..5ee1ff86b --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/stats/stats.tsx @@ -0,0 +1,463 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import Plotly from 'plotly.js-dist'; +import { uniqBy, find, isEmpty } from 'lodash'; +import { Plt } from '../../plotly/plot'; +import { ThresholdUnitType } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds'; +import { EmptyPlaceholder } from '../../../event_analytics/explorer/visualizations/shared_components/empty_placeholder'; +import { + ConfigListEntry, + IVisualizationContainerProps, +} from '../../../../../common/types/explorer'; +import { + hexToRgb, + getRoundOf, + getTooltipHoverInfo, + getPropName, +} from '../../../event_analytics/utils/utils'; +import { uiSettingsService } from '../../../../../common/utils'; +import { + STATS_GRID_SPACE_BETWEEN_X_AXIS, + STATS_GRID_SPACE_BETWEEN_Y_AXIS, + DEFAULT_STATS_CHART_PARAMETERS, + STATS_AXIS_MARGIN, + STATS_ANNOTATION, + STATS_REDUCE_VALUE_SIZE_PERCENTAGE, + STATS_REDUCE_TITLE_SIZE_PERCENTAGE, + STATS_REDUCE_SERIES_UNIT_SIZE_PERCENTAGE, + STATS_SERIES_UNIT_SUBSTRING_LENGTH, + GROUPBY, + AGGREGATIONS, +} from '../../../../../common/constants/explorer'; +import { + DEFAULT_CHART_STYLES, + FILLOPACITY_DIV_FACTOR, +} from '../../../../../common/constants/shared'; +import { COLOR_BLACK, COLOR_WHITE } from '../../../../../common/constants/colors'; + +const { + DefaultOrientation, + DefaultTextMode, + DefaultChartType, + BaseThreshold, +} = DEFAULT_STATS_CHART_PARAMETERS; + +interface CreateAnnotationType { + index: number; + label: string; + value: number | string; + valueColor: string; +} + +export const Stats = ({ visualizations, layout, config }: any) => { + const { + data: { + rawVizData: { + data: queriedVizData, + metadata: { fields }, + }, + userConfigs, + }, + vis: visMetaData, + }: IVisualizationContainerProps = visualizations; + + // data config parametrs + const { + dataConfig: { + span = {}, + [GROUPBY]: xaxis = [], + [AGGREGATIONS]: series = [], + chartStyles = {}, + panelOptions = {}, + tooltipOptions = {}, + thresholds = [], + }, + layoutConfig = {}, + } = userConfigs; + const timestampField = find(fields, (field) => field.type === 'timestamp'); + + /** + * determine x axis + */ + let xaxes: ConfigListEntry[]; + if (span && span.time_field && timestampField) { + xaxes = [timestampField, ...xaxis]; + } else { + xaxes = xaxis; + } + + const seriesLength = series.length; + const chartType = chartStyles.chartType || visMetaData.charttype; + + if ( + isEmpty(queriedVizData) || + (chartType === DefaultChartType && xaxes.length === 0) || + seriesLength === 0 || + chartType !== DefaultChartType + ) + return ; + + // thresholds + const appliedThresholds = thresholds.length ? thresholds : [BaseThreshold]; + const sortedThresholds = uniqBy( + [...appliedThresholds].sort((a: ThresholdUnitType, b: ThresholdUnitType) => a.value - b.value), + 'value' + ); + + // style panel parameters + const titleSize = + chartStyles.titleSize || + visMetaData.titlesize - + visMetaData.titlesize * seriesLength * STATS_REDUCE_TITLE_SIZE_PERCENTAGE; + const valueSize = + chartStyles.valueSize || + visMetaData.valuesize - + visMetaData.valuesize * seriesLength * STATS_REDUCE_VALUE_SIZE_PERCENTAGE; + const selectedOrientation = chartStyles.orientation || visMetaData.orientation; + const orientation = + selectedOrientation === DefaultOrientation || selectedOrientation === 'v' + ? DefaultOrientation + : 'h'; + const selectedTextMode = chartStyles.textMode || visMetaData.textmode; + const textMode = + selectedTextMode === DefaultTextMode || selectedTextMode === 'values+names' + ? DefaultTextMode + : selectedTextMode; + const precisionValue = chartStyles.precisionValue || visMetaData.precisionvalue; + const seriesUnits = + chartStyles.seriesUnits?.substring(0, STATS_SERIES_UNIT_SUBSTRING_LENGTH) || ''; + const seriesUnitsSize = valueSize - valueSize * STATS_REDUCE_SERIES_UNIT_SIZE_PERCENTAGE; + const isDarkMode = uiSettingsService.get('theme:darkMode'); + + // margin from left of grid cell for label/value + const ANNOTATION_MARGIN_LEFT = seriesLength > 1 ? 0.01 : 0; + let autoChartLayout: object = { + annotations: [], + }; + + const xaxesData = xaxes.reduce((prev, cur) => { + if (queriedVizData[cur.name]) { + if (prev.length === 0) return queriedVizData[cur.name].flat(); + return prev.map( + (item: string | number, index: number) => `${item},
${queriedVizData[cur.name][index]}` + ); + } + }, []); + + const createValueText = (value: string | number) => + `${value}${ + seriesUnits ? ` ${seriesUnits}` : '' + }`; + + const calculateTextCooridinate = (seriesCount: number, index: number) => { + // calculating center of each subplot based on orienation vertical(single column) or horizontal(single row) + // splitting whole plot area with series length and find center of each individual subplot w.r.t index of series + if (seriesCount === 1) { + return 0.5; + } else if (index === 0) { + return 1 / seriesCount / 2; + } + return (index + 1) / seriesCount - 1 / seriesCount / 2; + }; + + const createAnnotationsAutoModeHorizontal = ({ + label, + value, + index, + valueColor, + }: CreateAnnotationType) => { + const yCordinate = index > 0 ? (index + 1) / seriesLength : 1 / seriesLength; + return textMode === DefaultTextMode + ? [ + { + ...STATS_ANNOTATION, + x: ANNOTATION_MARGIN_LEFT, + y: yCordinate, + xanchor: 'left', + yanchor: 'top', + text: label, + font: { + size: titleSize, + color: isDarkMode ? COLOR_WHITE : COLOR_BLACK, + family: 'Roboto', + }, + type: 'name', + seriesValue: value, + }, + { + ...STATS_ANNOTATION, + x: 1, + y: yCordinate, + xanchor: 'right', + yanchor: 'top', + text: createValueText(value), + font: { + size: valueSize, + color: valueColor, + family: 'Roboto', + }, + type: 'value', + seriesValue: value, + }, + ] + : [ + { + ...STATS_ANNOTATION, + x: 0.5, + y: calculateTextCooridinate(seriesLength, index), + xanchor: 'center', + yanchor: 'bottom', + text: textMode === 'values' ? createValueText(value) : label, + font: { + size: textMode === 'values' ? valueSize : titleSize, + color: textMode === 'names' ? (isDarkMode ? COLOR_WHITE : COLOR_BLACK) : valueColor, + family: 'Roboto', + }, + type: textMode === 'names' ? 'name' : 'value', + seriesValue: value, + }, + ]; + }; + + const createAnnotationAutoModeVertical = ({ + label, + value, + index, + valueColor, + }: CreateAnnotationType) => { + const xCoordinate = index / seriesLength + ANNOTATION_MARGIN_LEFT; + return textMode === DefaultTextMode + ? [ + { + ...STATS_ANNOTATION, + xanchor: 'left', + yanchor: 'bottom', + text: label, + font: { + size: titleSize, + color: isDarkMode ? COLOR_WHITE : COLOR_BLACK, + family: 'Roboto', + }, + x: xCoordinate, + y: 1, + seriesValue: value, + type: 'name', + }, + { + ...STATS_ANNOTATION, + xanchor: 'left', + yanchor: 'top', + text: createValueText(value), + font: { + size: valueSize, + color: valueColor, + family: 'Roboto', + }, + x: xCoordinate, + y: 1, + type: 'value', + seriesValue: value, + }, + ] + : [ + { + ...STATS_ANNOTATION, + x: calculateTextCooridinate(seriesLength, index), + xanchor: 'center', + y: 0.95, + yanchor: 'bottom', + text: textMode === 'values' ? createValueText(value) : label, + font: { + size: textMode === 'values' ? valueSize : titleSize, + color: textMode === 'names' ? (isDarkMode ? COLOR_WHITE : COLOR_BLACK) : valueColor, + family: 'Roboto', + }, + type: textMode === 'names' ? 'name' : 'value', + seriesValue: value, + }, + ]; + }; + + // extend y axis range to increase height of subplot w.r.t series data + const extendYaxisRange = (seriesLabel: string) => { + const sortedData = queriedVizData[seriesLabel] + .slice() + .sort((curr: number, next: number) => next - curr); + return isNaN(sortedData[0]) ? 100 : sortedData[0] + sortedData[0] / 2; + }; + + const getSeriesValue = (label: string) => + typeof queriedVizData[label][queriedVizData[label].length - 1] === 'number' + ? getRoundOf( + queriedVizData[label][queriedVizData[label].length - 1], + Math.abs(precisionValue) + ) + : 0; + + const generateLineTraces = () => { + return series.map((seriesItem: ConfigListEntry, seriesIndex: number) => { + const seriesLabel = getPropName(seriesItem); + const isLabelExisted = queriedVizData[seriesLabel] ? true : false; + const annotationOption = { + label: seriesLabel, + value: isLabelExisted ? getSeriesValue(seriesLabel) : 0, + index: seriesIndex, + valueColor: '', + }; + const layoutAxisIndex = seriesIndex > 0 ? seriesIndex + 1 : ''; + autoChartLayout = { + ...autoChartLayout, + annotations: autoChartLayout.annotations.concat( + orientation === DefaultOrientation || seriesLength === 1 + ? createAnnotationAutoModeVertical(annotationOption) + : createAnnotationsAutoModeHorizontal(annotationOption) + ), + [`xaxis${layoutAxisIndex}`]: { + visible: false, + showgrid: false, + anchor: `y${layoutAxisIndex}`, + layoutFor: seriesLabel, + }, + [`yaxis${layoutAxisIndex}`]: { + visible: false, + showgrid: false, + anchor: `x${layoutAxisIndex}`, + range: isLabelExisted ? [0, extendYaxisRange(seriesLabel)] : [0, 100], + layoutFor: seriesLabel, + }, + }; + + return { + x: xaxesData, + y: queriedVizData[seriesLabel], + seriesValue: isLabelExisted ? getSeriesValue(seriesLabel) : 0, + fill: 'tozeroy', + mode: 'lines', + type: 'scatter', + fillcolor: '', + line: { + color: '', + }, + name: seriesLabel, + ...(seriesIndex > 0 && { + xaxis: `x${seriesIndex + 1}`, + yaxis: `y${seriesIndex + 1}`, + }), + hoverinfo: getTooltipHoverInfo({ + tooltipMode: tooltipOptions.tooltipMode, + tooltipText: tooltipOptions.tooltipText, + }), + }; + }); + }; + + const [statsData, statsLayout]: Plotly.Data[] = useMemo(() => { + let calculatedStatsData: Plotly.Data[] = []; + calculatedStatsData = generateLineTraces(); + + if (sortedThresholds.length) { + const sortedStatsData = calculatedStatsData + .map((stat, statIndex) => ({ ...stat, oldIndex: statIndex })) + .sort((statCurrent, statNext) => statCurrent.seriesValue - statNext.seriesValue); + // threshold ranges with min, max values + let thresholdRanges: number[][] = []; + thresholdRanges = sortedThresholds.map((thresh, index) => [ + thresh.value, + index === sortedThresholds.length - 1 + ? sortedStatsData[sortedStatsData.length - 1].seriesValue + : sortedThresholds[index + 1].value, + ]); + + if (thresholdRanges.length) { + // change color for line traces + for (let statIndex = 0; statIndex < sortedStatsData.length; statIndex++) { + for (let threshIndex = 0; threshIndex < thresholdRanges.length; threshIndex++) { + if ( + Number(sortedStatsData[statIndex].seriesValue) >= + Number(thresholdRanges[threshIndex][0]) && + Number(sortedStatsData[statIndex].seriesValue) <= + Number(thresholdRanges[threshIndex][1]) + ) { + calculatedStatsData[sortedStatsData[statIndex].oldIndex].fillcolor = hexToRgb( + sortedThresholds[threshIndex].color, + DEFAULT_CHART_STYLES.FillOpacity / FILLOPACITY_DIV_FACTOR + ); + calculatedStatsData[sortedStatsData[statIndex].oldIndex].line.color = + sortedThresholds[threshIndex].color; + } + } + } + + // change color of text annotations + for ( + let annotationIndex = 0; + annotationIndex < autoChartLayout.annotations.length; + annotationIndex++ + ) { + const isSeriesValueText = autoChartLayout.annotations[annotationIndex].type === 'value'; + const seriesValue = Number(autoChartLayout.annotations[annotationIndex].seriesValue); + for (let threshIndex = 0; threshIndex < thresholdRanges.length; threshIndex++) { + if ( + isSeriesValueText && + seriesValue >= Number(thresholdRanges[threshIndex][0]) && + seriesValue <= Number(thresholdRanges[threshIndex][1]) + ) { + autoChartLayout.annotations[annotationIndex].font.color = + sortedThresholds[threshIndex].color; + } + } + } + } + } + return [calculatedStatsData, autoChartLayout]; + }, [ + xaxes, + series, + fields, + appliedThresholds, + orientation, + titleSize, + valueSize, + textMode, + seriesUnits, + queriedVizData, + precisionValue, + ]); + + const mergedLayout = useMemo(() => { + return { + ...layout, + ...(layoutConfig.layout && layoutConfig.layout), + showlegend: false, + margin: STATS_AXIS_MARGIN, + ...statsLayout, + grid: { + ...(orientation === DefaultOrientation + ? { + rows: 1, + columns: seriesLength, + xgap: STATS_GRID_SPACE_BETWEEN_X_AXIS, + } + : { + rows: seriesLength, + columns: 1, + ygap: STATS_GRID_SPACE_BETWEEN_Y_AXIS, + }), + pattern: 'independent', + roworder: 'bottom to top', + }, + title: panelOptions?.title || layoutConfig.layout?.title || '', + }; + }, [layout, layoutConfig.layout, panelOptions?.title, orientation, seriesLength, statsLayout]); + + const mergedConfigs = { + ...config, + ...(layoutConfig.config && layoutConfig.config), + }; + + return ; +}; diff --git a/dashboards-observability/public/components/visualizations/charts/stats/stats_type.ts b/dashboards-observability/public/components/visualizations/charts/stats/stats_type.ts new file mode 100644 index 000000000..d4083193b --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/stats/stats_type.ts @@ -0,0 +1,197 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Stats } from './stats'; +import { getPlotlySharedConfigs, getPlotlyCategory } from '../shared/shared_configs'; +import { LensIconChartLine } from '../../assets/chart_line'; +import { PLOTLY_COLOR } from '../../../../../common/constants/shared'; +import { VizDataPanel } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/default_vis_editor'; +import { ConfigEditor } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/json_editor'; +import { + ConfigThresholds, + InputFieldItem, + ButtonGroupItem, + ConfigChartOptions, + TextInputFieldItem, +} from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/config_controls'; +import { fetchConfigObject } from '../../../../components/event_analytics/utils/utils'; +import { ConfigAvailability } from '../../../event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_availability'; +import { DEFAULT_STATS_CHART_PARAMETERS } from '../../../../../common/constants/explorer'; + +const sharedConfigs = getPlotlySharedConfigs(); +const VIS_CATEGORY = getPlotlyCategory(); +const { + DefaultTextMode, + DefaultOrientation, + DefaultChartType, + DefaultPrecision, + DefaultTitleSize, + DefaultValueSize, + BaseThreshold, +} = DEFAULT_STATS_CHART_PARAMETERS; + +export const createStatsTypeDefinition = (params: any = {}) => ({ + name: 'stats', + type: 'stats', + id: 'stats', + label: 'Stats', + fulllabel: 'Stats', + icontype: 'stats', + category: VIS_CATEGORY.BASICS, + icon: LensIconChartLine, + categoryaxis: 'xaxis', + seriesaxis: 'yaxis', + charttype: DefaultChartType, + precisionvalue: DefaultPrecision, + titlesize: DefaultTitleSize, + valuesize: DefaultValueSize, + textmode: DefaultTextMode, + orientation: DefaultOrientation, + editorconfig: { + panelTabs: [ + { + id: 'data-panel', + name: 'Style', + mapTo: 'dataConfig', + editor: VizDataPanel, + sections: [ + fetchConfigObject('Tooltip', { + options: [ + { name: 'All', id: 'all' }, + { name: 'Dimension', id: 'x' }, + { name: 'Series', id: 'y' }, + ], + defaultSelections: [{ name: 'All', id: 'all' }], + }), + { + id: 'chart_styles', + name: 'Chart styles', + editor: ConfigChartOptions, + mapTo: 'chartStyles', + schemas: [ + { + name: 'Chart type', + mapTo: 'chartType', + component: ButtonGroupItem, + eleType: 'buttons', + props: { + options: [ + { name: 'Auto', id: DefaultChartType }, + { name: 'Horizontal', id: 'horizontal' }, + { name: 'Text mode', id: 'text' }, + ], + defaultSelections: [{ name: 'Auto', id: DefaultChartType }], + }, + }, + { + name: 'Orientation', + component: ButtonGroupItem, + mapTo: 'orientation', + eleType: 'buttons', + props: { + options: [ + { name: 'Auto', id: DefaultOrientation }, + { name: 'Horizontal', id: 'h' }, + { name: 'Vertical', id: 'v' }, + ], + defaultSelections: [{ name: 'Auto', id: DefaultOrientation }], + }, + }, + { + title: 'Series units', + name: 'Series units', + component: TextInputFieldItem, + mapTo: 'seriesUnits', + eleType: 'textInput', + }, + { + title: 'Series precision', + name: 'Series precision', + component: InputFieldItem, + mapTo: 'precisionValue', + eleType: 'input', + props: { + minLimit: 0, + }, + }, + { + title: 'Title size', + name: 'Title size', + component: InputFieldItem, + mapTo: 'titleSize', + eleType: 'input', + }, + { + title: 'Value size', + name: 'Value size', + component: InputFieldItem, + mapTo: 'valueSize', + eleType: 'input', + }, + { + name: 'Text mode', + component: ButtonGroupItem, + mapTo: 'textMode', + eleType: 'buttons', + props: { + options: [ + { name: 'Auto', id: DefaultTextMode }, + { name: 'Names', id: 'names' }, + { name: 'Values', id: 'values' }, + { name: 'Values + Names', id: 'values+names' }, + ], + defaultSelections: [{ name: 'Values + Names', id: DefaultTextMode }], + }, + }, + ], + }, + { + id: 'thresholds', + name: 'Thresholds', + editor: ConfigThresholds, + mapTo: 'thresholds', + defaultState: [BaseThreshold], + schemas: [], + }, + ], + }, + { + id: 'style-panel', + name: 'Layout', + mapTo: 'layoutConfig', + editor: ConfigEditor, + content: [], + }, + { + id: 'availability-panel', + name: 'Availability', + mapTo: 'availabilityConfig', + editor: ConfigAvailability, + }, + ], + }, + visconfig: { + layout: { + ...sharedConfigs.layout, + colorway: PLOTLY_COLOR, + plot_bgcolor: 'rgba(0, 0, 0, 0)', + paper_bgcolor: 'rgba(0, 0, 0, 0)', + xaxis: { + fixedrange: true, + showgrid: false, + visible: true, + }, + yaxis: { + fixedrange: true, + showgrid: false, + visible: true, + }, + }, + config: { + ...sharedConfigs.config, + }, + }, + component: Stats, +}); diff --git a/dashboards-observability/public/components/visualizations/charts/vis_types.ts b/dashboards-observability/public/components/visualizations/charts/vis_types.ts index 9f37f2fbb..7fdf6ea13 100644 --- a/dashboards-observability/public/components/visualizations/charts/vis_types.ts +++ b/dashboards-observability/public/components/visualizations/charts/vis_types.ts @@ -15,6 +15,7 @@ import { createGaugeTypeDefinition } from './financial/gauge/gauge_type'; import { createTreeMapDefinition } from './maps/treemap_type'; import { createTextTypeDefinition } from './text/text_type'; import { createLogsViewTypeDefinition } from './logs_view/logs_view_type'; +import { createStatsTypeDefinition } from "./stats/stats_type" export const VIS_TYPES = { bar: createBarTypeDefinition, @@ -30,6 +31,7 @@ export const VIS_TYPES = { text: createTextTypeDefinition, scatter: createLineTypeDefinition, logs_view: createLogsViewTypeDefinition, + stats: createStatsTypeDefinition }; export const getVisType = (visType: string, params: any = {}) => { diff --git a/dashboards-observability/test/event_analytics_constants.ts b/dashboards-observability/test/event_analytics_constants.ts index 48054dd5e..c5f6df25d 100644 --- a/dashboards-observability/test/event_analytics_constants.ts +++ b/dashboards-observability/test/event_analytics_constants.ts @@ -6,6 +6,7 @@ import { LONG_CHART_COLOR } from '../common/constants/shared'; import { createBarTypeDefinition } from '../public/components/visualizations/charts/bar/bar_type'; import { createGaugeTypeDefinition } from '../public/components/visualizations/charts/financial/gauge/gauge_type'; +import { createStatsTypeDefinition } from '../public/components/visualizations/charts/stats/stats_type'; import { SELECTED_FIELDS, AVAILABLE_FIELDS as AVAILABLE_FIELDS_NAME, @@ -552,3 +553,8 @@ export const GAUGE_TEST_VISUALIZATIONS_DATA = { ...TEST_VISUALIZATIONS_DATA, vis: createGaugeTypeDefinition(), }; + +export const STATS_TEST_VISUALIZATIONS_DATA = { + ...TEST_VISUALIZATIONS_DATA, + vis: createStatsTypeDefinition({}), +};