From 7e41206e0a15032c4d184be3ba5406228c7c9ab2 Mon Sep 17 00:00:00 2001 From: Suchit Sahoo Date: Wed, 19 Jun 2024 22:59:43 +0000 Subject: [PATCH 1/2] Add Drag Across Axis Functionality to Vis Builder Signed-off-by: Suchit Sahoo --- .../components/data_tab/config_panel.tsx | 45 ++- .../components/data_tab/dropbox.scss | 4 + .../components/data_tab/dropbox.tsx | 133 ++++----- .../components/data_tab/field.test.tsx | 9 +- .../application/components/data_tab/field.tsx | 33 ++- .../components/data_tab/field_selector.scss | 2 - .../data_tab/field_selector.test.tsx | 9 +- .../components/data_tab/field_selector.tsx | 105 ++++--- .../application/components/data_tab/index.tsx | 256 +++++++++++++++++- .../components/data_tab/schema_to_dropbox.tsx | 22 +- .../components/data_tab/use/use_dropbox.tsx | 83 +++--- .../data_tab/utils/get_valid_aggregations.tsx | 99 +++++++ .../components/draggable_accordion.scss | 34 +++ .../components/draggable_accordion.tsx | 59 ++++ 14 files changed, 707 insertions(+), 186 deletions(-) create mode 100644 src/plugins/vis_builder/public/application/components/data_tab/utils/get_valid_aggregations.tsx create mode 100644 src/plugins/vis_builder/public/application/components/draggable_accordion.scss create mode 100644 src/plugins/vis_builder/public/application/components/draggable_accordion.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx b/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx index ec3b6b60a09..b5dc91fbbfb 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx @@ -5,22 +5,49 @@ import { EuiForm } from '@elastic/eui'; import React from 'react'; -import { useVisualizationType } from '../../utils/use'; -import { useTypedSelector } from '../../utils/state_management'; + import './config_panel.scss'; import { mapSchemaToAggPanel } from './schema_to_dropbox'; import { SecondaryPanel } from './secondary_panel'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { + AggConfig, + AggConfigs, + CreateAggConfigParams, +} from '../../../../../data/common/search/aggs'; +import { IndexPattern, TimeRange } from '../../../../../data/public'; +import { SchemaDisplayStates } from '.'; -export function ConfigPanel() { - const vizType = useVisualizationType(); - const editingState = useTypedSelector( - (state) => state.visualization.activeVisualization?.draftAgg - ); - const schemas = vizType.ui.containerConfig.data.schemas; +export interface AggProps { + indexPattern: IndexPattern | undefined; + aggConfigs: AggConfigs | undefined; + aggs: AggConfig[]; + timeRange: TimeRange; +} +export interface ConfigPanelProps { + schemas: Schemas; + editingState?: CreateAggConfigParams; + aggProps: AggProps; + schemaDisplayStates: SchemaDisplayStates; + setSchemaDisplayStates: React.Dispatch>; +} + +export function ConfigPanel({ + schemas, + editingState, + aggProps, + schemaDisplayStates, + setSchemaDisplayStates, +}: ConfigPanelProps) { if (!schemas) return null; - const mainPanel = mapSchemaToAggPanel(schemas); + const mainPanel = mapSchemaToAggPanel( + schemas, + aggProps, + schemaDisplayStates, + setSchemaDisplayStates + ); return ( diff --git a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.scss b/src/plugins/vis_builder/public/application/components/data_tab/dropbox.scss index 89c7832ac40..a92639420b1 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.scss +++ b/src/plugins/vis_builder/public/application/components/data_tab/dropbox.scss @@ -15,6 +15,10 @@ border-bottom: none; } + &__droppable { + min-height: 1px; + } + &__container { display: grid; grid-gap: calc($euiSizeXS / 2); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx b/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx index 70b43a2c601..e5b46b589e0 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx @@ -6,13 +6,11 @@ import { i18n } from '@osd/i18n'; import { EuiButtonIcon, - EuiDragDropContext, EuiDraggable, EuiDroppable, EuiFormRow, EuiPanel, EuiText, - DropResult, } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; import { IDropAttributes, IDropState } from '../../utils/drag_drop'; @@ -51,7 +49,6 @@ const DropboxComponent = ({ onAddField, onDeleteField, onEditField, - onReorderField, limit = 1, isValidDropTarget, canDrop, @@ -59,17 +56,6 @@ const DropboxComponent = ({ }: DropboxProps) => { const prefersReducedMotion = usePrefersReducedMotion(); const [closing, setClosing] = useState(false); - const handleDragEnd = useCallback( - ({ source, destination }: DropResult) => { - if (!destination) return; - - onReorderField({ - sourceAggId: fields[source.index].id, - destinationAggId: fields[destination.index].id, - }); - }, - [fields, onReorderField] - ); const animateDelete = useCallback( (id: string) => { @@ -86,71 +72,72 @@ const DropboxComponent = ({ ); return ( - - -
- - {fields.map(({ id, label }, index) => ( - - - onEditField(id)}> - - {label} - - - animateDelete(id)} - data-test-subj="dropBoxRemoveBtn" - /> - - - ))} - - {fields.length < limit && ( - +
+ + {fields.map(({ id, label }, index) => ( + - - {i18n.translate('visBuilder.dropbox.addField.title', { - defaultMessage: 'Click or drop to add', - })} - - onAddField()} - data-test-subj="dropBoxAddBtn" - /> - - )} -
- - + + onEditField(id)}> + + {label} + + + animateDelete(id)} + data-test-subj="dropBoxRemoveBtn" + /> + + + ))} + + {fields.length < limit && ( + + + {i18n.translate('visBuilder.dropbox.addField.title', { + defaultMessage: 'Click or drop to add', + })} + + onAddField()} + data-test-subj="dropBoxAddBtn" + /> + + )} +
+
); }; const Dropbox = React.memo((dropBox: UseDropboxProps) => { const props = useDropbox(dropBox); - return ; }); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.test.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field.test.tsx index 6aed9deb159..d65608c9745 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field.test.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field.test.tsx @@ -9,6 +9,7 @@ import { render, screen } from '@testing-library/react'; import { IndexPatternField } from '../../../../../data/public'; import { DraggableFieldButton } from './field'; +import { DropResult, EuiDragDropContext, EuiDroppable } from '@elastic/eui'; describe('visBuilder field', function () { describe('DraggableFieldButton', () => { @@ -28,7 +29,13 @@ describe('visBuilder field', function () { 'bytes' ), }; - render(); + render( + {}}> + + + + + ); const button = screen.getByTestId('field-bytes-showDetails'); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field.tsx index 287c6aed621..d06f6e7ccd6 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field.tsx @@ -29,7 +29,7 @@ */ import React, { useState } from 'react'; -import { EuiPopover } from '@elastic/eui'; +import { EuiDraggable, EuiPopover } from '@elastic/eui'; import { IndexPatternField } from '../../../../../data/public'; import { @@ -46,10 +46,11 @@ import './field.scss'; export interface FieldProps { field: IndexPatternField; getDetails: (field) => FieldDetails; + id: number; } // TODO: Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) -export const Field = ({ field, getDetails }: FieldProps) => { +export const Field = ({ field, getDetails, id }: FieldProps) => { const [infoIsOpen, setOpen] = useState(false); function togglePopover() { @@ -60,7 +61,14 @@ export const Field = ({ field, getDetails }: FieldProps) => { } + button={ + + } isOpen={infoIsOpen} closePopover={() => setOpen(false)} anchorPosition="rightUp" @@ -77,9 +85,15 @@ export const Field = ({ field, getDetails }: FieldProps) => { export interface DraggableFieldButtonProps extends Partial { dragValue?: IndexPatternField['name'] | null | typeof COUNT_FIELD; field: Partial & Pick; + index: number; } -export const DraggableFieldButton = ({ dragValue, field, ...rest }: DraggableFieldButtonProps) => { +export const DraggableFieldButton = ({ + dragValue, + field, + index, + ...rest +}: DraggableFieldButtonProps) => { const { name, displayName, type, scripted = false } = field; const [dragProps] = useDrag({ namespace: 'field-data', @@ -109,5 +123,14 @@ export const DraggableFieldButton = ({ dragValue, field, ...rest }: DraggableFie onClick: () => {}, }; - return ; + return ( + + + + ); }; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss index 88cca98db86..4bf1c21f006 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss @@ -10,8 +10,6 @@ padding: $euiSizeS; &__fieldGroups { - @include euiYScrollWithShadows; - overflow-y: auto; margin-right: -$euiSizeS; padding-right: $euiSizeS; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx index 980cfb50c66..d86e022d0cc 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { FilterManager, IndexPatternField } from '../../../../../data/public'; import { FieldGroup } from './field_selector'; +import { DropResult, EuiDragDropContext, EuiDroppable } from '@elastic/eui'; const mockUseIndexPatterns = jest.fn(() => ({ selected: 'mockIndexPattern' })); const mockUseOnAddFilter = jest.fn(); @@ -68,7 +69,13 @@ describe('visBuilder sidebar field selector', function () { ...defaultProps, fields: ['bytes', 'machine.ram', 'memory', 'phpmemory'].map(getFields), }; - const { container } = render(); + const { container } = render( + {}}> + + + + + ); expect(container).toHaveTextContent(props.header); expect(container).toHaveTextContent(props.fields.length.toString()); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx index 5c82419d553..cb6763ff93e 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; +import { EuiFlexItem, EuiDroppable } from '@elastic/eui'; import { IndexPattern, IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; @@ -16,6 +16,7 @@ import { Field, DraggableFieldButton } from './field'; import { FieldDetails } from './types'; import { getAvailableFields, getDetails } from './utils'; import './field_selector.scss'; +import { DraggableAccordion } from '../draggable_accordion'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -75,30 +76,59 @@ export const FieldSelector = () => {
{/* Count Field */} - - - - + + + + + + + + + + + +
); @@ -113,27 +143,14 @@ interface FieldGroupProps { export const FieldGroup = ({ fields, header, id, getDetailsByField }: FieldGroupProps) => { return ( - - {header} - - } - extraAction={ - - {fields?.length || 0} - - } - initialIsOpen - > - {fields?.map((field, i) => ( - - + ( + + ))} - + /> ); }; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/index.tsx b/src/plugins/vis_builder/public/application/components/data_tab/index.tsx index 5f71e38141d..68c474f6b66 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/index.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/index.tsx @@ -3,19 +3,265 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { DropResult, EuiDragDropContext } from '@elastic/eui'; import { FieldSelector } from './field_selector'; import './index.scss'; import { ConfigPanel } from './config_panel'; +import { useAggs, useVisualizationType } from '../../utils/use'; +import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; + +import { + reorderAgg, + updateAggConfigParams, +} from '../../utils/state_management/visualization_slice'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../../../types'; +import { createNewAggConfig } from './utils/get_valid_aggregations'; +import { DropboxDisplay } from './dropbox'; export const DATA_TAB_ID = 'data_tab'; +export interface SchemaDisplayStates { + [key: string]: DropboxDisplay[]; +} + export const DataTab = () => { + // Field Selector panel States + const fieldSelectorGroups = [ + 'preDefinedCountMetric', + 'categoricalFields', + 'numericalFields', + 'metaFields', + ]; + + // Config panel States + const vizType = useVisualizationType(); + const editingState = useTypedSelector( + (state) => state.visualization.activeVisualization?.draftAgg + ); + const schemas = vizType.ui.containerConfig.data.schemas; + const { + services: { + data: { + search: { aggs: aggService }, + }, + }, + } = useOpenSearchDashboards(); + + const aggProps = useAggs(); + const [schemaDisplayStates, setSchemaDisplayStates] = useState(() => { + return schemas.all.reduce((acc, schema) => { + acc[schema.name] = []; + return acc; + }, {}); + }); + const dispatch = useTypedDispatch(); + + useEffect(() => { + const newState = schemas.all.reduce((acc, schema) => { + acc[schema.name] = []; + return acc; + }, {}); + const updatedState = { ...newState, ...schemaDisplayStates }; + setSchemaDisplayStates(updatedState); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [schemas, vizType]); + + const handleFieldSelectorToConfigurationPanelTransition = ({ + source, + destination, + combine, + draggableId, + }: DropResult) => { + // destination Schema + const destinationSchemaName = destination?.droppableId || combine?.droppableId; + const destinationSchema = schemas.all.find((schema) => schema.name === destinationSchemaName); + const destinationFieldToCombine = combine?.draggableId; + + const newFieldToAdd = draggableId; + + if (!destinationSchema || !destinationSchemaName) { + // Invalid drop target selected + return; + } + + const destinationFields = schemaDisplayStates[destinationSchemaName]; + + if (!combine && destination && destinationFields.length > destinationSchema?.max) { + // Can't Add additional Fields + return; + } + + // Case 1 we are adding a new field + + createNewAggConfig({ + fieldName: newFieldToAdd, + sourceGroup: source.droppableId, + destinationSchema, + aggProps, + aggService, + sourceAgg: null, + }); + + let updatedAggConfigs = aggProps.aggConfigs?.aggs; + + if (combine) { + // remove the previously selected Aggreagtion + updatedAggConfigs = updatedAggConfigs?.filter((agg) => agg.id !== destinationFieldToCombine); + } + if (updatedAggConfigs) { + dispatch(updateAggConfigParams(updatedAggConfigs.map((agg) => agg.serialize()))); + } + }; + + const handleConfigurationPanelTransition = ({ + source, + destination, + combine, + draggableId, + }: DropResult) => { + const destinationSchemaName = destination?.droppableId || combine?.droppableId; + const destinationAggFields = schemaDisplayStates[destinationSchemaName]; + + const sourceAggId = draggableId; + const destinationAggId = destinationAggFields[destination?.index] || combine?.draggableId; + + const destinationSchema = schemas.all.find( + (schema) => schema.name === (destination?.droppableId || combine?.droppableId) + ); + + if (!destinationSchema) { + // Invalid Transition + return; + } + + const sourceAgg = aggProps.aggConfigs?.aggs.find((agg) => agg.id === sourceAggId); + const sourceFieldName = sourceAgg?.fieldName(); + + if (!combine) { + if (source?.droppableId === destination?.droppableId && source !== null) { + if (source.index === destination.index) { + // Moving the same element + return; + } else { + // Reordering of the selections within a same group + dispatch( + reorderAgg({ + sourceId: sourceAggId, + destinationId: destinationAggId, + }) + ); + } + } else if ( + source?.droppableId !== destination?.droppableId && + source !== null && + destination !== null + ) { + // Moving a element from one Dropable Box to another + + const destinationLimit = destinationSchema?.max; + if (destinationLimit && destinationAggFields.length <= destinationLimit) { + // Case 1: Destination has space + // We Need to update sourceAgg + + createNewAggConfig({ + fieldName: sourceFieldName, + sourceGroup: source.droppableId, + destinationSchema, + aggProps, + aggService, + sourceAgg, + }); + + // Remove the sourceAggConfig from the updated Config + const updatedAggConfig = aggProps.aggConfigs?.aggs.filter( + (agg) => agg.id !== sourceAggId + ); + + if (updatedAggConfig?.length) { + dispatch(updateAggConfigParams(updatedAggConfig.map((agg) => agg.serialize()))); + } + } else { + // Case 2 : Destination has no space + return; + } + } + } else if (combine !== null) { + // Combining Elements + // TODO: Do we need to restrict drag and drop features amongst the Dragables in one Droppables + + // Creating an Aggregation of the Source Field in the destination Schema + createNewAggConfig({ + fieldName: sourceFieldName, + sourceGroup: source.droppableId, + destinationSchema, + aggProps, + aggService, + sourceAgg, + }); + + // Removing the previous destination and source AggId's + const updatedAggConfig = aggProps.aggConfigs?.aggs.filter( + (agg) => agg.id !== destinationAggId && agg.id !== sourceAggId + ); + + if (updatedAggConfig) { + dispatch(updateAggConfigParams(updatedAggConfig.map((agg) => agg.serialize()))); + } + } + }; + + const handleDragEnd = ({ source, destination, combine, draggableId }: DropResult) => { + try { + const destinationSchemaName = destination?.droppableId || combine?.droppableId; + const sourceSchemaName = source.droppableId; + + if (!sourceSchemaName || !destinationSchemaName) { + // Invalid Scenario source should be present + return; + } + + const panelGroups = Array.from(schemas.all.map((schema) => schema.name)); + + // Transition from FieldSelector to Conifg panel + if (fieldSelectorGroups.includes(sourceSchemaName)) { + if (panelGroups.includes(destinationSchemaName)) { + handleFieldSelectorToConfigurationPanelTransition({ + source, + destination, + combine, + draggableId, + } as DropResult); + } + } else if (panelGroups.includes(sourceSchemaName)) { + if (panelGroups.includes(destinationSchemaName)) { + handleConfigurationPanelTransition({ + source, + destination, + combine, + draggableId, + } as DropResult); + } + } + } catch (err) { + return; + } + }; + return ( -
- - -
+ +
+ + +
+
); }; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/schema_to_dropbox.tsx b/src/plugins/vis_builder/public/application/components/data_tab/schema_to_dropbox.tsx index 518a6ae7af2..54072c5261a 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/schema_to_dropbox.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/schema_to_dropbox.tsx @@ -2,15 +2,31 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - import React from 'react'; import { Schemas } from '../../../../../vis_default_editor/public'; import { Dropbox } from './dropbox'; import { Title } from './title'; +import { AggProps } from './config_panel'; +import { SchemaDisplayStates } from '.'; -export const mapSchemaToAggPanel = (schemas: Schemas) => { +export const mapSchemaToAggPanel = ( + schemas: Schemas, + aggProps: AggProps, + schemaDisplayStates: SchemaDisplayStates, + setSchemaDisplayStates: React.Dispatch> +) => { const panelComponents = schemas.all.map((schema) => { - return ; + return ( + + ); }); return ( diff --git a/src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx b/src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx index c41e4bc0866..470b37f4c93 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx @@ -5,7 +5,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { cloneDeep } from 'lodash'; -import { BucketAggType, IndexPatternField, propFilter } from '../../../../../../data/common'; import { Schema } from '../../../../../../vis_default_editor/public'; import { COUNT_FIELD, FieldDragDataType } from '../../../utils/drag_drop/types'; import { useTypedDispatch } from '../../../utils/state_management'; @@ -18,19 +17,30 @@ import { } from '../../../utils/state_management/visualization_slice'; import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; import { VisBuilderServices } from '../../../../types'; -import { useAggs } from '../../../utils/use'; - -const filterByName = propFilter('name'); -const filterByType = propFilter('type'); +import { getValidAggTypes } from '../utils/get_valid_aggregations'; +import { AggProps } from '../config_panel'; +import { SchemaDisplayStates } from '..'; export interface UseDropboxProps extends Pick { schema: Schema; + aggProps: AggProps; + schemaDisplayStates: SchemaDisplayStates; + setSchemaDisplayStates: React.Dispatch>; } export const useDropbox = (props: UseDropboxProps): DropboxProps => { - const { id: dropboxId, label, schema } = props; + const { + id: dropboxId, + label, + schema, + aggProps, + schemaDisplayStates, + setSchemaDisplayStates, + } = props; const [validAggTypes, setValidAggTypes] = useState([]); - const { aggConfigs, indexPattern, aggs, timeRange } = useAggs(); + const { aggConfigs, indexPattern, aggs, timeRange } = aggProps; + const fields = schemaDisplayStates[schema.name]; + const dispatch = useTypedDispatch(); const { services: { @@ -60,6 +70,15 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { [dropboxAggs, timeRange] ); + useEffect(() => { + if (displayFields && JSON.stringify(fields) !== JSON.stringify(displayFields)) { + const newDisplayState = { ...schemaDisplayStates }; + newDisplayState[schema.name] = displayFields; + setSchemaDisplayStates(newDisplayState); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [displayFields]); + // Event handlers for each dropbox action type const onAddField = useCallback(() => { if (!aggConfigs || !indexPattern) { @@ -105,7 +124,6 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { const onDeleteField = useCallback( (aggId: string) => { const newAggs = aggConfigs?.aggs.filter((agg) => agg.id !== aggId); - if (newAggs) { dispatch(updateAggConfigParams(newAggs.map((agg) => agg.serialize()))); } @@ -156,43 +174,25 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { ); useEffect(() => { - const getValidAggTypes = () => { - if (!dragData || schema.group === 'none') return []; - const isCountField = dragData === COUNT_FIELD; - - const indexField = isCountField - ? { type: 'count' } - : getIndexPatternField(dragData, indexPattern?.fields ?? []); - - if (!indexField) return []; - - // Get all aggTypes allowed by the schema and get a list of all the aggTypes that the dragged index field can use - const aggTypes = aggService.types.getAll(); - // `types` can be either a Bucket or Metric aggType, but both types have the name property. - const allowedAggTypes = filterByName( - aggTypes[schema.group] as BucketAggType[], - schema.aggFilter - ); - - return ( - allowedAggTypes - .filter((aggType) => { - const allowedFieldTypes = aggType.paramByName('field')?.filterFieldTypes; - return filterByType([indexField], allowedFieldTypes).length !== 0; - }) - .filter((aggType) => (isCountField ? true : aggType.name !== 'count')) - // `types` can be either a Bucket or Metric aggType, but both types have the name property. - .map((aggType) => (aggType as BucketAggType).name) - ); - }; - - setValidAggTypes(getValidAggTypes()); + const fieldName = typeof dragData === typeof COUNT_FIELD ? '' : (dragData as string); + const sourceGroup = typeof dragData === typeof COUNT_FIELD ? 'preDefinedCountMetric' : ''; + + setValidAggTypes( + getValidAggTypes({ + fieldName, + sourceGroup, + destinationSchema: schema, + aggProps, + aggService, + sourceAgg: null, + }) + ); return () => { setValidAggTypes([]); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [aggService.types, dragData, indexPattern?.fields, schema.aggFilter, schema.group]); - const canDrop = validAggTypes.length > 0 && schema.max > dropboxAggs.length; return { @@ -210,6 +210,3 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { dropProps, }; }; - -const getIndexPatternField = (indexFieldName: string, availableFields: IndexPatternField[]) => - availableFields.find(({ name }) => name === indexFieldName); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_valid_aggregations.tsx b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_valid_aggregations.tsx new file mode 100644 index 00000000000..e88c3099a00 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_valid_aggregations.tsx @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + AggConfig, + AggsStart, + BucketAggType, + IndexPatternField, + propFilter, +} from '../../../../../../data/common'; +import { Schema } from '../../../../../../vis_default_editor/public'; +import { AggProps } from '../config_panel'; + +export interface CreateNewAggConfig { + fieldName: string; + sourceGroup: string; + destinationSchema: Schema; + aggProps: AggProps; + aggService: AggsStart; + sourceAgg: AggConfig | null | undefined; +} + +const filterByName = propFilter('name'); +const filterByType = propFilter('type'); + +const getIndexPatternField = (indexFieldName: string, availableFields: IndexPatternField[]) => + availableFields.find(({ name }) => name === indexFieldName); + +export const getValidAggTypes = ({ + fieldName, + sourceGroup, + destinationSchema, + aggProps, + aggService, + sourceAgg, +}: CreateNewAggConfig) => { + const isCountField = + sourceGroup === 'preDefinedCountMetric' || + (sourceAgg && Object.keys(sourceAgg.params).length === 0); + + const indexField = isCountField + ? { type: 'count' } + : getIndexPatternField(fieldName, aggProps.indexPattern?.fields ?? []); + + if (!indexField) return []; + + // Get all aggTypes allowed by the schema and get a list of all the aggTypes that the dragged index field can use + const aggTypes = aggService.types.getAll(); + // `types` can be either a Bucket or Metric aggType, but both types have the name property. + const allowedAggTypes = filterByName( + aggTypes[destinationSchema.group] as BucketAggType[], + destinationSchema.aggFilter + ); + + return ( + allowedAggTypes + .filter((aggType) => { + const allowedFieldTypes = aggType.paramByName('field')?.filterFieldTypes; + return filterByType([indexField], allowedFieldTypes).length !== 0; + }) + .filter((aggType) => (isCountField ? true : aggType.name !== 'count')) + // `types` can be either a Bucket or Metric aggType, but both types have the name property. + .map((aggType) => (aggType as BucketAggType).name) + ); +}; + +export const createNewAggConfig = ({ + fieldName, + sourceGroup, + destinationSchema, + aggProps, + aggService, + sourceAgg, +}: CreateNewAggConfig) => { + const schemaAggTypes = (destinationSchema.defaults as any).aggTypes; + + const destinationValidAggType = getValidAggTypes({ + fieldName, + sourceGroup, + destinationSchema, + aggProps, + aggService, + sourceAgg, + }); + + const allowedAggTypes = schemaAggTypes + ? schemaAggTypes.filter((type: string) => destinationValidAggType.includes(type)) + : []; + + aggProps.aggConfigs?.createAggConfig({ + type: allowedAggTypes[0] || destinationValidAggType[0], + schema: destinationSchema.name, + params: { + field: fieldName, + }, + }); +}; diff --git a/src/plugins/vis_builder/public/application/components/draggable_accordion.scss b/src/plugins/vis_builder/public/application/components/draggable_accordion.scss new file mode 100644 index 00000000000..3bfcac218d9 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/draggable_accordion.scss @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.draggableAccordion__title { + display: inline-block; +} + +.draggableAccordion__button { + padding: $euiSizeS $euiSizeS $euiSizeS 0; + + &:hover { + text-decoration: none; + + .draggableAccordion__title { + text-decoration: underline; + } + } +} + +.draggableAccordion__badge { + justify-content: center; + align-items: center; +} + +.draggableAccordion { + border-top: $euiBorderThin; + border-bottom: $euiBorderThin; + + & + .draggableAccordion { + border-top: none; + } +} diff --git a/src/plugins/vis_builder/public/application/components/draggable_accordion.tsx b/src/plugins/vis_builder/public/application/components/draggable_accordion.tsx new file mode 100644 index 00000000000..99457800e7f --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/draggable_accordion.tsx @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiNotificationBadge, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import { useState } from 'react'; +import './draggable_accordion.scss'; + +export const DraggableAccordion = ({ children, title, defaultState = true }) => { + const [isOpen, setIsOpen] = useState(defaultState); + + function handleOnClick() { + setIsOpen(!isOpen); + } + + return ( +
+ + + + + + {title} + + + + + + {children?.length || 0} + + + + + {isOpen && children} + + +
+ ); +}; From 34ab28f1161f7811fcf5888baf1da7de7d12e47b Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:07:18 +0000 Subject: [PATCH 2/2] Changeset file for PR #7107 created/updated --- changelogs/fragments/7107.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7107.yml diff --git a/changelogs/fragments/7107.yml b/changelogs/fragments/7107.yml new file mode 100644 index 00000000000..163246e0469 --- /dev/null +++ b/changelogs/fragments/7107.yml @@ -0,0 +1,2 @@ +feat: +- Enhance Drag & Drop functionality in Vis Builder ([#7107](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7107)) \ No newline at end of file