/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ import React, { ChangeEvent, Component, Fragment } from "react"; import { EuiSpacer, EuiBasicTable, EuiButton, EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, EuiModalFooter, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiForm, // @ts-ignore Pagination, // @ts-ignore Criteria, EuiTableSelectionType, EuiTableFieldDataColumnType, EuiFormRow, EuiSelect, EuiText, EuiLink, EuiIcon, EuiPanel, EuiTitle, EuiFormHelpText, EuiHorizontalRule, EuiCallOut, EuiFieldNumber, EuiTableSortingType, } from "@elastic/eui"; import { AddFieldsColumns } from "../../utils/constants"; import { DEFAULT_PAGE_SIZE_OPTIONS } from "../../../Rollups/utils/constants"; import { isNumericMapping } from "../../utils/helpers"; import { DimensionItem, FieldItem } from "../../../../../models/interfaces"; interface AdvancedAggregationProps { fieldsOption: FieldItem[]; onDimensionSelectionChange: (selectedFields: DimensionItem[]) => void; selectedDimensionField: DimensionItem[]; } interface AdvancedAggregationState { isModalVisible: boolean; selectedFields: FieldItem[]; allSelectedFields: FieldItem[]; fieldsShown: FieldItem[]; dimensionsShown: DimensionItem[]; dimensionFrom: number; dimensionSize: number; dimensionSortField: string; dimensionSortDirection: string; } export default class AdvancedAggregation extends Component { constructor(props: AdvancedAggregationProps) { super(props); const { selectedDimensionField, fieldsOption } = this.props; this.state = { isModalVisible: false, allSelectedFields: [], fieldsShown: fieldsOption.slice(0, 10), dimensionsShown: selectedDimensionField.slice(0, 10), selectedFields: [], dimensionFrom: 0, dimensionSize: 10, dimensionSortField: "sequence", dimensionSortDirection: "desc", }; } closeModal = () => this.setState({ isModalVisible: false }); showModal = () => this.setState({ isModalVisible: true }); onSelectionChange = (selectedFields: FieldItem[]): void => { this.setState({ selectedFields }); }; onClickAdd() { const { onDimensionSelectionChange, selectedDimensionField } = this.props; const { selectedFields, allSelectedFields, dimensionFrom, dimensionSize } = this.state; //Clone selectedDimensionField let updatedDimensions = Array.from(selectedDimensionField); const toAddFields = Array.from(selectedFields); selectedDimensionField.map((dimension) => { if (allSelectedFields.includes(dimension.field)) { const index = toAddFields.indexOf(dimension.field); toAddFields.splice(index, 1); } }); //Update sequence number this.updateSequence(updatedDimensions); //Parse selectedFields to an array of DimensionItem if any of the field does not exist let i = updatedDimensions.length + 1; const toAdd: DimensionItem[] = toAddFields.map((field) => { return isNumericMapping(field.type) ? { sequence: i++, field: field, aggregationMethod: "histogram", interval: 5, } : { sequence: i++, field: field, aggregationMethod: "terms", }; }); const result = updatedDimensions.length ? updatedDimensions.concat(toAdd) : toAdd; onDimensionSelectionChange(result); this.setState({ allSelectedFields: allSelectedFields.concat(toAddFields) }); this.setState({ dimensionsShown: result.slice(dimensionFrom, dimensionFrom + dimensionSize) }); this.forceUpdate(); } //Check the dimension num updateSequence(items: DimensionItem[]) { if (items.length == 0) { this.setState({ dimensionsShown: [] }); return; } const { onDimensionSelectionChange } = this.props; const { dimensionSize: dimensionSize, dimensionFrom } = this.state; let dimensionNum; for (dimensionNum = 0; dimensionNum < items.length; dimensionNum++) { items[dimensionNum].sequence = dimensionNum + 1; } onDimensionSelectionChange(items); this.setState({ dimensionsShown: items.slice(dimensionFrom, dimensionFrom + dimensionSize) }); this.forceUpdate(); } moveUp(item: DimensionItem) { const { selectedDimensionField } = this.props; const toMoveindex = selectedDimensionField.indexOf(item); if (toMoveindex == 0) return; let toSwap = selectedDimensionField[toMoveindex - 1]; selectedDimensionField[toMoveindex] = toSwap; selectedDimensionField[toMoveindex - 1] = item; this.updateSequence(selectedDimensionField); } moveDown(item: DimensionItem) { const { selectedDimensionField } = this.props; const toMoveindex = selectedDimensionField.indexOf(item); if (toMoveindex == selectedDimensionField.length - 1) return; let toSwap = selectedDimensionField[toMoveindex + 1]; selectedDimensionField[toMoveindex] = toSwap; selectedDimensionField[toMoveindex + 1] = item; this.updateSequence(selectedDimensionField); } deleteField = (item: DimensionItem) => { const { selectedDimensionField } = this.props; const { selectedFields, allSelectedFields } = this.state; //Remove the dimension item and then update sequence. selectedDimensionField.splice(selectedDimensionField.indexOf(item), 1); selectedFields.splice(selectedFields.indexOf(item.field), 1); allSelectedFields.splice(allSelectedFields.indexOf(item.field), 1); this.setState({ selectedFields, allSelectedFields }); this.updateSequence(selectedDimensionField); }; onChangeInterval = (e: ChangeEvent, item: DimensionItem): void => { const { selectedDimensionField, onDimensionSelectionChange } = this.props; const index = selectedDimensionField.indexOf(item); const newItem: DimensionItem = { sequence: item.sequence, field: item.field, aggregationMethod: "histogram", interval: e.target.valueAsNumber, }; selectedDimensionField[index] = newItem; this.updateSequence(selectedDimensionField); onDimensionSelectionChange(selectedDimensionField); }; onChangeAggregationMethod = (e: ChangeEvent, item: DimensionItem): void => { const { selectedDimensionField, onDimensionSelectionChange } = this.props; const index = selectedDimensionField.indexOf(item); const newItem: DimensionItem = { sequence: item.sequence, field: item.field, aggregationMethod: e.target.value, }; if (e.target.value == "histogram") { newItem.interval = 5; } selectedDimensionField[index] = newItem; this.updateSequence(selectedDimensionField); onDimensionSelectionChange(selectedDimensionField); }; onDimensionTableChange = ({ page: tablePage, sort }: Criteria): void => { const { index: page, size } = tablePage; const { field: sortField, direction: sortDirection } = sort; const { selectedDimensionField } = this.props; this.setState({ dimensionFrom: page * size, dimensionSize: size, dimensionSortField: sortField, dimensionSortDirection: sortDirection, dimensionsShown: selectedDimensionField.slice(page * size, page * size + size), }); }; render() { const { fieldsOption, selectedDimensionField } = this.props; const { allSelectedFields, isModalVisible, dimensionFrom, dimensionSize, dimensionSortDirection, dimensionSortField, dimensionsShown, } = this.state; const dimensionPage = Math.floor(dimensionFrom / dimensionSize); const dimensionPagination: Pagination = { pageIndex: dimensionPage, pageSize: dimensionSize, pageSizeOptions: DEFAULT_PAGE_SIZE_OPTIONS, totalItemCount: selectedDimensionField.length, }; const dimensionSorting: EuiTableSortingType = { sort: { direction: dimensionSortDirection, field: dimensionSortField, }, }; const selection: EuiTableSelectionType = { onSelectionChange: this.onSelectionChange, initialSelected: allSelectedFields, }; const aggregationColumns: EuiTableFieldDataColumnType[] = [ { field: "sequence", name: "Sequence", sortable: true, dataType: "number", align: "left", }, { field: "field.label", name: "Field name", align: "left", }, { field: "field.type", name: "Field type", align: "left", render: (type) => (type == null ? "-" : type), }, { field: "aggregationMethod", name: "Aggregation method", align: "left", render: (aggregationMethod, item) => ( this.onChangeAggregationMethod(e, item)} data-test-subj={`aggregationMethodSelect-${item.field.label}`} /> ), }, { field: "interval", name: "Interval", dataType: "number", align: "left", render: (interval: number, item) => interval == null ? ( "-" ) : ( this.onChangeInterval(e, item)} data-test-subj={`interval-${item.field.label}`} /> ), }, { field: "sequence", name: "", align: "center", render: (sequence, item: DimensionItem) => { return ( {item.sequence != 1 && ( this.moveUp(item)} data-test-subj={`moveUp-${item.field.label}`}> Move up )} {item.sequence != selectedDimensionField.length && ( this.moveDown(item)} data-test-subj={`moveDown-${item.field.label}`}> Move down )} ); }, }, { field: "sequence", name: "Actions", align: "center", render: (sequence, item: DimensionItem) => { return ( this.deleteField(item)} data-test-subj={`delete-${item.field.label}`} /> ); }, }, ]; return (

Additional aggregation

{` (${selectedDimensionField.length})`}

– optional
The sequence of fields may influence rollup performance.{" "} Learn more {selectedDimensionField.length != 0 && (

The order of fields impacts rollup performance. Aggregating by smaller buckets and then by larger buckets is faster than the opposite. For example, if you are rolling up flight data for five airlines with 100 destinations, aggregating by airline and then by destination is faster than aggregating by destination first.

)}
Add fields
No fields added for aggregations Add fields } /> {isModalVisible && ( Add fields Cancel { this.closeModal(); this.onClickAdd(); }} data-test-subj="addFieldsAggregationAdd" > Add )}
); } }