From a7115b208482bdedd67c8d37fe92e2082d65d951 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Wed, 7 Sep 2022 18:32:37 -0700 Subject: [PATCH] [D&D] Adds Bar line and Area charts to Wizard (#2266) * Working histogram Signed-off-by: Ashwin Pc * Adds histogram options Signed-off-by: Ashwin Pc * Adds line chart Signed-off-by: Ashwin Pc * Adds area chart Signed-off-by: Ashwin Pc * updates nav sizes Signed-off-by: Ashwin Pc * adds resizeable container to right nav Signed-off-by: Ashwin Pc * Misc fixes Signed-off-by: Ashwin Pc * Adds chart switcher test Signed-off-by: Ashwin Pc * Github comment feedback Co-authored-by: Josh Romero Signed-off-by: Ashwin Pc * Updates copy Signed-off-by: Ashwin Pc Signed-off-by: Ashwin Pc Co-authored-by: Josh Romero Signed-off-by: Sergey V. Osipov --- src/plugins/vis_type_vislib/public/index.ts | 2 + src/plugins/vis_type_vislib/public/types.ts | 2 + src/plugins/visualizations/public/index.ts | 2 +- src/plugins/wizard/opensearch_dashboards.json | 3 +- .../wizard/public/application/_variables.scss | 2 +- .../wizard/public/application/app.scss | 13 +++- src/plugins/wizard/public/application/app.tsx | 35 ++++++++- .../components/data_tab/field_selector.scss | 2 + .../components/experimental_info.tsx | 32 ++++---- .../application/components/right_nav.tsx | 55 ++++++++++++-- .../components/searchable_dropdown.scss | 2 +- .../application/components/side_nav.scss | 3 +- .../application/components/workspace.scss | 1 + .../application/components/workspace.tsx | 9 ++- .../common/expression_helpers.ts | 43 +++++++++++ .../wizard/public/visualizations/index.ts | 8 +- .../visualizations/metric/to_expression.ts | 32 +------- .../vislib/area/area_vis_type.ts | 64 ++++++++++++++++ .../area/components/area_vis_options.tsx | 41 ++++++++++ .../visualizations/vislib/area/index.ts | 6 ++ .../vislib/area/to_expression.ts | 46 ++++++++++++ .../vislib/common/basic_vis_options.tsx | 49 ++++++++++++ .../vislib/common/get_value_axes.ts | 31 ++++++++ .../visualizations/vislib/common/types.ts | 18 +++++ .../components/histogram_vis_options.tsx | 41 ++++++++++ .../vislib/histogram/histogram_vis_type.ts | 64 ++++++++++++++++ .../visualizations/vislib/histogram/index.ts | 6 ++ .../vislib/histogram/to_expression.ts | 46 ++++++++++++ .../public/visualizations/vislib/index.ts | 8 ++ .../line/components/line_vis_options.tsx | 41 ++++++++++ .../visualizations/vislib/line/index.ts | 6 ++ .../vislib/line/line_vis_type.ts | 75 +++++++++++++++++++ .../vislib/line/to_expression.ts | 46 ++++++++++++ test/functional/apps/wizard/_base.ts | 34 ++++++--- test/functional/page_objects/wizard_page.ts | 23 +++++- 35 files changed, 811 insertions(+), 80 deletions(-) create mode 100644 src/plugins/wizard/public/visualizations/common/expression_helpers.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx create mode 100644 src/plugins/wizard/public/visualizations/vislib/area/index.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx create mode 100644 src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/common/types.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx create mode 100644 src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/histogram/index.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/index.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx create mode 100644 src/plugins/wizard/public/visualizations/vislib/line/index.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts create mode 100644 src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts diff --git a/src/plugins/vis_type_vislib/public/index.ts b/src/plugins/vis_type_vislib/public/index.ts index 525be8eaf829..1b13f06ca6d2 100644 --- a/src/plugins/vis_type_vislib/public/index.ts +++ b/src/plugins/vis_type_vislib/public/index.ts @@ -35,4 +35,6 @@ export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } +export { getConfigCollections } from './utils/collections'; + export * from './types'; diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts index 3416a1ee3c4a..3267f04db87b 100644 --- a/src/plugins/vis_type_vislib/public/types.ts +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -106,3 +106,5 @@ export interface BasicVislibParams extends CommonVislibParams { times: TimeMarker[]; type: string; } + +export { Positions }; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 4eda49da6bec..46d3b3dd7d03 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -43,7 +43,7 @@ export { Vis } from './vis'; export { TypesService } from './vis_types/types_service'; export { VISUALIZE_EMBEDDABLE_TYPE, VIS_EVENT_TO_TRIGGER } from './embeddable'; export { VisualizationContainer, VisualizationNoResults } from './components'; -export { getSchemas as getVisSchemas } from './legacy/build_pipeline'; +export { getSchemas as getVisSchemas, buildVislibDimensions } from './legacy/build_pipeline'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json index 020ca5f2ab9b..ae088bfe4c8f 100644 --- a/src/plugins/wizard/opensearch_dashboards.json +++ b/src/plugins/wizard/opensearch_dashboards.json @@ -16,7 +16,8 @@ "dashboard", "visualizations", "opensearchUiShared", - "visDefaultEditor" + "visDefaultEditor", + "visTypeVislib" ], "optionalPlugins": [] } diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss index 2baa9db275f2..da530e2e46a4 100644 --- a/src/plugins/wizard/public/application/_variables.scss +++ b/src/plugins/wizard/public/application/_variables.scss @@ -6,4 +6,4 @@ @import "@elastic/eui/src/global_styling/variables/form"; $osdHeaderOffset: $euiHeaderHeightCompensation; -$wizSideNavWidth: 470px; +$wizLeftNavWidth: 462px; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss index f38959445100..f4a73eb3ecd4 100644 --- a/src/plugins/wizard/public/application/app.scss +++ b/src/plugins/wizard/public/application/app.scss @@ -8,9 +8,18 @@ padding: 0; display: grid; grid-template: - "topNav topNav topNav" min-content - "leftNav workspace rightNav" 1fr / #{$wizSideNavWidth} 1fr #{$wizSideNavWidth}; + "topNav topNav" min-content + "leftNav workspaceNav" 1fr / #{$wizLeftNavWidth} 1fr; height: calc(100vh - #{$osdHeaderOffset}); + + &__resizeContainer { + min-height: 0; + background-color: $euiColorEmptyShade; + } + + &__resizeButton { + transform: translateX(-$euiSizeM / 2); + } } .headerIsExpanded .wizLayout { diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx index 9c93dcb1b552..52a0f537646e 100644 --- a/src/plugins/wizard/public/application/app.tsx +++ b/src/plugins/wizard/public/application/app.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { I18nProvider } from '@osd/i18n/react'; -import { EuiPage } from '@elastic/eui'; +import { EuiPage, EuiResizableContainer } from '@elastic/eui'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; import { LeftNav } from './components/left_nav'; import { TopNav } from './components/top_nav'; @@ -21,8 +21,37 @@ export const WizardApp = () => { - - + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + )} + diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss index b2fb337e1dc2..b9b75a8a260d 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss @@ -15,6 +15,8 @@ overflow-y: auto; margin-right: -$euiSizeS; padding-right: $euiSizeS; + margin-left: -$euiSizeS; + padding-left: $euiSizeS; margin-top: $euiSizeS; } diff --git a/src/plugins/wizard/public/application/components/experimental_info.tsx b/src/plugins/wizard/public/application/components/experimental_info.tsx index cc4d0cab1340..35ea235c1ba3 100644 --- a/src/plugins/wizard/public/application/components/experimental_info.tsx +++ b/src/plugins/wizard/public/application/components/experimental_info.tsx @@ -6,37 +6,35 @@ import React, { memo } from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; export const InfoComponent = () => { - const title = ( - <> + return ( + - GitHub + the GitHub issue ), }} /> - - ); - - return ( - + ); }; diff --git a/src/plugins/wizard/public/application/components/right_nav.tsx b/src/plugins/wizard/public/application/components/right_nav.tsx index bbd1ce844c2c..ea7c3e17eab6 100644 --- a/src/plugins/wizard/public/application/components/right_nav.tsx +++ b/src/plugins/wizard/public/application/components/right_nav.tsx @@ -3,8 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { EuiSuperSelect, EuiSuperSelectOption, EuiIcon, IconType } from '@elastic/eui'; +import React, { useState } from 'react'; +import { + EuiSuperSelect, + EuiSuperSelectOption, + EuiIcon, + IconType, + EuiConfirmModal, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; import { useVisualizationType } from '../utils/use'; import './side_nav.scss'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; @@ -12,6 +20,7 @@ import { WizardServices } from '../../types'; import { setActiveVisualization, useTypedDispatch } from '../utils/state_management'; export const RightNav = () => { + const [newVisType, setNewVisType] = useState(); const { services: { types }, } = useOpenSearchDashboards(); @@ -23,6 +32,7 @@ export const RightNav = () => { value: name, inputDisplay: , dropdownDisplay: , + 'data-test-subj': `visType-${name}`, })); return ( @@ -32,19 +42,48 @@ export const RightNav = () => { options={options} valueOfSelected={activeVisName} onChange={(name) => { - dispatch( - setActiveVisualization({ - name, - style: types.get(name)?.ui.containerConfig.style.defaults, - }) - ); + setNewVisType(name); }} fullWidth + data-test-subj="chartPicker" />
+ {newVisType && ( + setNewVisType(undefined)} + onConfirm={() => { + dispatch( + setActiveVisualization({ + name: newVisType, + style: types.get(newVisType)?.ui.containerConfig.style.defaults, + }) + ); + + setNewVisType(undefined); + }} + maxWidth="300px" + data-test-subj="confirmVisChangeModal" + > +

+ +

+
+ )} ); }; diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss index de03454dffbe..be8d6144df3a 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.scss +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -22,7 +22,7 @@ } &--fixedWidthChild { - width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2); + width: calc(#{$wizLeftNavWidth} - #{$euiSizeXL} * 2); } &--selectableWrapper .euiSelectableList { diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss index 5fade4b11b4d..025e42d08170 100644 --- a/src/plugins/wizard/public/application/components/side_nav.scss +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -16,6 +16,7 @@ &.right { border-left: $euiBorderThin; grid-area: rightNav; + height: 100%; } &__header { @@ -46,5 +47,5 @@ } .wizDatasourceSelect { - max-width: calc(#{$wizSideNavWidth} - 1px); + max-width: calc(#{$wizLeftNavWidth} - 1px); } diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss index 0b7b851cfddb..1a57a891262e 100644 --- a/src/plugins/wizard/public/application/components/workspace.scss +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -10,6 +10,7 @@ grid-gap: $euiSizeM; padding: $euiSizeM; background-color: $euiColorEmptyShade; + height: 100%; &__empty { height: 100%; diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 7e51a29be1a6..1aa58a272dff 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -11,6 +11,7 @@ import { WizardServices } from '../../types'; import { validateSchemaState } from '../utils/validate_schema_state'; import { useTypedSelector } from '../utils/state_management'; import { useVisualizationType } from '../utils/use'; +import { PersistedState } from '../../../../visualizations/public'; import hand_field from '../../assets/hand_field.svg'; import fields_bg from '../../assets/fields_bg.svg'; @@ -34,6 +35,8 @@ export const Workspace: FC = ({ children }) => { timeRange: data.query.timefilter.timefilter.getTime(), }); const rootState = useTypedSelector((state) => state); + // Visualizations require the uiState to persist even when the expression changes + const uiState = useMemo(() => new PersistedState(), []); useEffect(() => { async function loadExpression() { @@ -77,7 +80,11 @@ export const Workspace: FC = ({ children }) => { {expression ? ( - + ) : ( { + const { activeVisualization, indexPattern: indexId = '' } = visualization; + const { aggConfigParams } = activeVisualization || {}; + + const indexPatternsService = getIndexPatterns(); + const indexPattern = await indexPatternsService.get(indexId); + // aggConfigParams is the serealizeable aggConfigs that need to be reconstructed here using the agg servce + const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + + const opensearchDashboards = buildExpressionFunction( + 'opensearchDashboards', + {} + ); + + // soon this becomes: const opensearchaggs = vis.data.aggs!.toExpressionAst(); + const opensearchaggs = buildExpressionFunction( + 'opensearchaggs', + { + index: indexId, + metricsAtAllLevels: false, + partialRows: false, + aggConfigs: JSON.stringify(aggConfigs.aggs), + includeFormatHints: false, + } + ); + + return { + aggConfigs, + expressionFns: [opensearchDashboards, opensearchaggs], + }; +}; diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts index 52d3a7234f2a..6787c28a6ff8 100644 --- a/src/plugins/wizard/public/visualizations/index.ts +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -5,9 +5,15 @@ import type { TypeServiceSetup } from '../services/type_service'; import { createMetricConfig } from './metric'; +import { createHistogramConfig, createLineConfig, createAreaConfig } from './vislib'; export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { - const visualizationTypes = [createMetricConfig]; + const visualizationTypes = [ + createHistogramConfig, + createLineConfig, + createAreaConfig, + createMetricConfig, + ]; visualizationTypes.forEach((createTypeConfig) => { typeServiceSetup.createVisualizationType(createTypeConfig()); diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts index ce930d9b8e40..181e504ec0e8 100644 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.ts +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -3,18 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { cloneDeep } from 'lodash'; import { SchemaConfig } from '../../../../visualizations/public'; import { MetricVisExpressionFunctionDefinition } from '../../../../vis_type_metric/public'; -import { - AggConfigs, - IAggConfig, - OpenSearchaggsExpressionFunctionDefinition, -} from '../../../../data/common'; +import { AggConfigs, IAggConfig } from '../../../../data/common'; import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; import { RootState } from '../../application/utils/state_management'; import { MetricOptionsDefaults } from './metric_viz_type'; -import { getAggService, getIndexPatterns } from '../../plugin_services'; +import { getAggExpressionFunctions } from '../common/expression_helpers'; const prepareDimension = (params: SchemaConfig) => { const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); @@ -92,24 +87,7 @@ export interface MetricRootState extends RootState { } export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { - const { activeVisualization, indexPattern: indexId = '' } = visualization; - const { aggConfigParams } = activeVisualization || {}; - - const indexPatternsService = getIndexPatterns(); - const indexPattern = await indexPatternsService.get(indexId); - const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); - - // soon this becomes: const opensearchaggs = vis.data.aggs!.toExpressionAst(); - const opensearchaggs = buildExpressionFunction( - 'opensearchaggs', - { - index: indexId, - metricsAtAllLevels: false, - partialRows: false, - aggConfigs: JSON.stringify(aggConfigs.aggs), - includeFormatHints: false, - } - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); // TODO: Update to use the getVisSchemas function from the Visualizations plugin // const schemas = getVisSchemas(vis, params); @@ -169,7 +147,5 @@ export const toExpression = async ({ style: styleState, visualization }: MetricR metricVis.addArgument('metric', prepareDimension(metric)); }); - const ast = buildExpression([opensearchaggs, metricVis]); - - return `opensearchDashboards | opensearch_dashboards_context | ${ast.toString()}`; + return buildExpression([...expressionFns, metricVis]).toString(); }; diff --git a/src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts b/src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts new file mode 100644 index 000000000000..14b524bf008f --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { AreaVisOptions } from './components/area_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; +import { BasicOptionsDefaults } from '../common/types'; + +export interface AreaOptionsDefaults extends BasicOptionsDefaults { + type: 'area'; +} + +export const createAreaConfig = (): VisualizationTypeOptions => ({ + name: 'area', + title: 'Area', + icon: 'visArea', + description: 'Display area chart', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.area.metricTitle', { + defaultMessage: 'Y-axis', + }), + min: 1, + max: 3, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: { aggTypes: ['median'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('visTypeVislib.area.segmentTitle', { + defaultMessage: 'X-axis', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!filters'], + defaults: { aggTypes: ['terms'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + type: 'area', + }, + render: AreaVisOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx new file mode 100644 index 000000000000..4b3116c83992 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { AreaOptionsDefaults } from '../area_vis_type'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; +import { BasicVisOptions } from '../../common/basic_vis_options'; + +function AreaVisOptions() { + const styleState = useTypedSelector((state) => state.style) as AreaOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft) => void) => { + const newState = produce(styleState, callback); + dispatch(setState(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + + + ); +} + +export { AreaVisOptions }; diff --git a/src/plugins/wizard/public/visualizations/vislib/area/index.ts b/src/plugins/wizard/public/visualizations/vislib/area/index.ts new file mode 100644 index 000000000000..7ec1f37a601d --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createAreaConfig } from './area_vis_type'; diff --git a/src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts b/src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts new file mode 100644 index 000000000000..012c6589f57b --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis, buildVislibDimensions } from '../../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../../../expressions/public'; +import { AreaOptionsDefaults } from './area_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState } from '../common/types'; +import { getValueAxes } from '../common/get_value_axes'; + +export const toExpression = async ({ + style: styleState, + visualization, +}: VislibRootState) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + const { addLegend, addTooltip, legendPosition, type } = styleState; + const pipelineConfigs = { + // todo: this will blow up for time x dimensions + timefilter: null, // todo: get the time filter from elsewhere + }; + + const vis = new Vis(type); + vis.data.aggs = aggConfigs; + + const dimensions = await buildVislibDimensions(vis, pipelineConfigs as any); + const valueAxes = getValueAxes(dimensions.y); + + // TODO: what do we want to put in this "vis config"? + const visConfig = { + addLegend, + legendPosition, + addTimeMarker: false, + addTooltip, + dimensions, + valueAxes, + }; + + const vislib = buildExpressionFunction('vislib', { + type, + visConfig: JSON.stringify(visConfig), + }); + + return buildExpression([...expressionFns, vislib]).toString(); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx new file mode 100644 index 000000000000..6b088a68547f --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { Draft } from 'immer'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; +import { getConfigCollections } from '../../../../../vis_type_vislib/public'; +import { BasicOptionsDefaults } from './types'; + +interface Props { + styleState: BasicOptionsDefaults; + setOption: (callback: (draft: Draft) => void) => void; +} + +export const BasicVisOptions = ({ styleState, setOption }: Props) => { + const { legendPositions } = getConfigCollections(); + return ( + <> + + setOption((draft) => { + draft.legendPosition = value; + }) + } + /> + + setOption((draft) => { + draft.addTooltip = value; + }) + } + /> + + ); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts b/src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts new file mode 100644 index 000000000000..86c135110f50 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SchemaConfig } from '../../../../../visualizations/public'; +import { ValueAxis } from '../../../../../vis_type_vislib/public'; + +interface ValueAxisConfig extends ValueAxis { + style: any; +} + +export const getValueAxes = (yAxes: SchemaConfig[]): ValueAxisConfig[] => + yAxes.map((y, index) => ({ + id: `ValueAxis-${index + 1}`, + labels: { + show: true, + }, + name: `ValueAxis-${index + 1}`, + position: 'left', + scale: { + type: 'linear', + mode: 'normal', + }, + show: true, + style: {}, + title: { + text: y.label, + }, + type: 'value', + })); diff --git a/src/plugins/wizard/public/visualizations/vislib/common/types.ts b/src/plugins/wizard/public/visualizations/vislib/common/types.ts new file mode 100644 index 000000000000..4861c1af2e55 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Positions } from '../../../../../vis_type_vislib/public'; +import { RootState } from '../../../application/utils/state_management'; + +export interface BasicOptionsDefaults { + addTooltip: boolean; + addLegend: boolean; + legendPosition: Positions; + type: string; +} + +export interface VislibRootState extends RootState { + style: T; +} diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx new file mode 100644 index 000000000000..873b26ca4301 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { HistogramOptionsDefaults } from '../histogram_vis_type'; +import { BasicVisOptions } from '../../common/basic_vis_options'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; + +function HistogramVisOptions() { + const styleState = useTypedSelector((state) => state.style) as HistogramOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft) => void) => { + const newState = produce(styleState, callback); + dispatch(setState(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + + + ); +} + +export { HistogramVisOptions }; diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts b/src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts new file mode 100644 index 000000000000..e99f062cb615 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { BasicOptionsDefaults } from '../common/types'; +import { HistogramVisOptions } from './components/histogram_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; + +export interface HistogramOptionsDefaults extends BasicOptionsDefaults { + type: 'histogram'; +} + +export const createHistogramConfig = (): VisualizationTypeOptions => ({ + name: 'histogram', + title: 'Histogram', + icon: 'visBarVertical', + description: 'Display histogram visualizations', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.histogram.metricTitle', { + defaultMessage: 'Y-axis', + }), + min: 1, + max: 3, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: { aggTypes: ['median'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('visTypeVislib.histogram.segmentTitle', { + defaultMessage: 'X-axis', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!filters'], + defaults: { aggTypes: ['terms'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + type: 'histogram', + }, + render: HistogramVisOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/index.ts b/src/plugins/wizard/public/visualizations/vislib/histogram/index.ts new file mode 100644 index 000000000000..bba280de2d77 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createHistogramConfig } from './histogram_vis_type'; diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts b/src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts new file mode 100644 index 000000000000..6b6f0fe30baf --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis, buildVislibDimensions } from '../../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../../../expressions/public'; +import { HistogramOptionsDefaults } from './histogram_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState } from '../common/types'; +import { getValueAxes } from '../common/get_value_axes'; + +export const toExpression = async ({ + style: styleState, + visualization, +}: VislibRootState) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + const { addLegend, addTooltip, legendPosition, type } = styleState; + const pipelineConfigs = { + // todo: this will blow up for time x dimensions + timefilter: null, // todo: get the time filter from elsewhere + }; + + const vis = new Vis(type); + vis.data.aggs = aggConfigs; + + const dimensions = await buildVislibDimensions(vis, pipelineConfigs as any); + const valueAxes = getValueAxes(dimensions.y); + + // TODO: what do we want to put in this "vis config"? + const visConfig = { + addLegend, + legendPosition, + addTimeMarker: false, + addTooltip, + dimensions, + valueAxes, + }; + + const vislib = buildExpressionFunction('vislib', { + type, + visConfig: JSON.stringify(visConfig), + }); + + return buildExpression([...expressionFns, vislib]).toString(); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/index.ts b/src/plugins/wizard/public/visualizations/vislib/index.ts new file mode 100644 index 000000000000..84dc3e346ef5 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createHistogramConfig } from './histogram'; +export { createLineConfig } from './line'; +export { createAreaConfig } from './area'; diff --git a/src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx new file mode 100644 index 000000000000..a5bb1994c92a --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { LineOptionsDefaults } from '../line_vis_type'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; +import { BasicVisOptions } from '../../common/basic_vis_options'; + +function LineVisOptions() { + const styleState = useTypedSelector((state) => state.style) as LineOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft) => void) => { + const newState = produce(styleState, callback); + dispatch(setState(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + + + ); +} + +export { LineVisOptions }; diff --git a/src/plugins/wizard/public/visualizations/vislib/line/index.ts b/src/plugins/wizard/public/visualizations/vislib/line/index.ts new file mode 100644 index 000000000000..721ec7858a7a --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createLineConfig } from './line_vis_type'; diff --git a/src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts b/src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts new file mode 100644 index 000000000000..b92019cddd9d --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { LineVisOptions } from './components/line_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; +import { BasicOptionsDefaults } from '../common/types'; + +export interface LineOptionsDefaults extends BasicOptionsDefaults { + type: 'line'; +} + +export const createLineConfig = (): VisualizationTypeOptions => ({ + name: 'line', + title: 'Line', + icon: 'visLine', + description: 'Display line chart', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.line.metricTitle', { + defaultMessage: 'Y-axis', + }), + min: 1, + max: 3, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: { aggTypes: ['median'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('visTypeVislib.line.segmentTitle', { + defaultMessage: 'X-axis', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!filters'], + defaults: { aggTypes: ['terms'] }, + }, + { + group: AggGroupNames.Metrics, + name: 'radius', + title: i18n.translate('visTypeVislib.line.radiusTitle', { + defaultMessage: 'Dot size', + }), + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], + defaults: { aggTypes: ['count'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + type: 'line', + }, + render: LineVisOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts b/src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts new file mode 100644 index 000000000000..d7c7a13d2be7 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis, buildVislibDimensions } from '../../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../../../expressions/public'; +import { LineOptionsDefaults } from './line_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState } from '../common/types'; +import { getValueAxes } from '../common/get_value_axes'; + +export const toExpression = async ({ + style: styleState, + visualization, +}: VislibRootState) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + const { addLegend, addTooltip, legendPosition, type } = styleState; + const pipelineConfigs = { + // todo: this will blow up for time x dimensions + timefilter: null, // todo: get the time filter from elsewhere + }; + + const vis = new Vis(type); + vis.data.aggs = aggConfigs; + + const dimensions = await buildVislibDimensions(vis, pipelineConfigs as any); + const valueAxes = getValueAxes(dimensions.y); + + // TODO: what do we want to put in this "vis config"? + const visConfig = { + addLegend, + legendPosition, + addTimeMarker: false, + addTooltip, + dimensions, + valueAxes, + }; + + const vislib = buildExpressionFunction('vislib', { + type, + visConfig: JSON.stringify(visConfig), + }); + + return buildExpression([...expressionFns, vislib]).toString(); +}; diff --git a/test/functional/apps/wizard/_base.ts b/test/functional/apps/wizard/_base.ts index 99cce7fce489..c8940704da5c 100644 --- a/test/functional/apps/wizard/_base.ts +++ b/test/functional/apps/wizard/_base.ts @@ -7,7 +7,8 @@ import expect from '@osd/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'wizard']); + const PageObjects = getPageObjects(['visualize', 'wizard', 'visChart']); + const testSubjects = getService('testSubjects'); const log = getService('log'); const retry = getService('retry'); @@ -27,22 +28,33 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show visualization when a field is added', async () => { - await PageObjects.wizard.addField('metric', 'Average', 'machine.ram'); - const avgMachineRam = ['13,104,036,080.615', 'Average machine.ram']; + const expectedData = [2858, 2904, 2814, 1322, 2784]; + await PageObjects.wizard.addField('metric', 'Count'); + await PageObjects.wizard.addField('segment', 'Terms', 'machine.os.raw'); - await retry.try(async function tryingForTime() { - const metricValue = await PageObjects.wizard.getMetric(); - expect(avgMachineRam).to.eql(metricValue); - }); + const data = await PageObjects.visChart.getBarChartData(); + expect(data).to.eql(expectedData); }); it('should clear visualization when field is deleted', async () => { await PageObjects.wizard.removeField('metric', 0); - await retry.try(async function tryingForTime() { - const isEmptyWorkspace = await PageObjects.wizard.isEmptyWorkspace(); - expect(isEmptyWorkspace).to.be(true); - }); + const isEmptyWorkspace = await PageObjects.wizard.isEmptyWorkspace(); + expect(isEmptyWorkspace).to.be(true); + }); + + it('should show warning before changing visualization type', async () => { + await PageObjects.wizard.selectVisType('metric', false); + const confirmModalExists = await testSubjects.exists('confirmVisChangeModal'); + expect(confirmModalExists).to.be(true); + + await testSubjects.click('confirmModalCancelButton'); + }); + + it('should change visualization type', async () => { + const pickerValue = await PageObjects.wizard.selectVisType('metric'); + + expect(pickerValue).to.eql('Metric'); }); }); } diff --git a/test/functional/page_objects/wizard_page.ts b/test/functional/page_objects/wizard_page.ts index 08ed41df57d5..bd206f71fa2f 100644 --- a/test/functional/page_objects/wizard_page.ts +++ b/test/functional/page_objects/wizard_page.ts @@ -50,10 +50,22 @@ export function WizardPageProvider({ getService, getPageObjects }: FtrProviderCo return await dataSourceDropdown.getVisibleText(); } + public async selectVisType(type: string, confirm = true) { + const chartPicker = await testSubjects.find('chartPicker'); + await chartPicker.click(); + await testSubjects.click(`visType-${type}`); + + if (confirm) { + await testSubjects.click('confirmModalConfirmButton'); + } + + return chartPicker.getVisibleText(); + } + public async addField( dropBoxId: string, aggValue: string, - fieldValue: string, + fieldValue?: string, returnToMainPanel = true ) { await testSubjects.click(`dropBoxAddField-${dropBoxId} > dropBoxAddBtn`); @@ -61,9 +73,12 @@ export function WizardPageProvider({ getService, getPageObjects }: FtrProviderCo const aggComboBoxElement = await testSubjects.find('defaultEditorAggSelect'); await comboBox.setElement(aggComboBoxElement, aggValue); await common.sleep(500); - const fieldComboBoxElement = await testSubjects.find('visDefaultEditorField'); - await comboBox.setElement(fieldComboBoxElement, fieldValue); - await common.sleep(500); + + if (fieldValue) { + const fieldComboBoxElement = await testSubjects.find('visDefaultEditorField'); + await comboBox.setElement(fieldComboBoxElement, fieldValue); + await common.sleep(500); + } if (returnToMainPanel) { await testSubjects.click('panelCloseBtn');