From e8dd40952432d922ef29ebca453f348ed784d409 Mon Sep 17 00:00:00 2001 From: ananzh Date: Thu, 20 Jul 2023 06:01:53 +0000 Subject: [PATCH] Add initial DiscoverChart DiscoverChart contains HitsCounter, TimeChartHeader and Histogram in the previous discover_legacy. Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4571 Signed-off-by: ananzh --- .../application/components/chart/chart.tsx | 101 +++++++++++++++ .../{histogram => chart}/histogram.tsx | 20 ++- .../chart/utils/create_histogram_configs.ts | 29 +++++ .../components/chart/utils/get_dimensions.ts | 73 +++++++++++ .../components/chart/utils/index.ts | 8 ++ .../components/chart/utils/point_series.ts | 122 ++++++++++++++++++ .../view_components/canvas/canvas.tsx | 4 +- .../{discover_table.tsx => data_display.tsx} | 12 +- .../canvas/data_display_app.scss | 34 +++++ .../canvas/data_display_app.tsx | 103 +++++++++++++++ ...e_service.tsx => data_display_service.tsx} | 19 ++- .../canvas/discover_table_app.tsx | 62 --------- ...service.ts => use_data_display_service.ts} | 6 +- .../utils/update_data_source.ts | 10 +- .../view_components/utils/use_saved_search.ts | 43 +++++- 15 files changed, 549 insertions(+), 97 deletions(-) create mode 100644 src/plugins/discover/public/application/components/chart/chart.tsx rename src/plugins/discover/public/application/components/{histogram => chart}/histogram.tsx (95%) create mode 100644 src/plugins/discover/public/application/components/chart/utils/create_histogram_configs.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/get_dimensions.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/index.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/point_series.ts rename src/plugins/discover/public/application/view_components/canvas/{discover_table.tsx => data_display.tsx} (86%) create mode 100644 src/plugins/discover/public/application/view_components/canvas/data_display_app.scss create mode 100644 src/plugins/discover/public/application/view_components/canvas/data_display_app.tsx rename src/plugins/discover/public/application/view_components/canvas/{discover_table_service.tsx => data_display_service.tsx} (65%) delete mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx rename src/plugins/discover/public/application/view_components/canvas/utils/{use_discover_canvas_service.ts => use_data_display_service.ts} (90%) diff --git a/src/plugins/discover/public/application/components/chart/chart.tsx b/src/plugins/discover/public/application/components/chart/chart.tsx new file mode 100644 index 000000000000..0c83bba8d747 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/chart.tsx @@ -0,0 +1,101 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { IUiSettingsClient } from 'opensearch-dashboards/public'; +import { DataPublicPluginStart, search } from '../../../../../data/public'; +import { SavedSearch } from '../../../saved_searches'; +import { HitsCounter } from '../hits_counter'; +import { TimechartHeader } from '../timechart_header'; +import { DiscoverHistogram } from './histogram'; +import { DiscoverServices } from '../../../build_services'; +interface DiscoverChartProps { + bucketInterval: any; + chartData: any; + config: IUiSettingsClient; + data: DataPublicPluginStart; + hits: number; + resetQuery: () => void; + savedSearch: SavedSearch; + timeField?: string; + services: DiscoverServices; +} + +export const DiscoverChart = ({ + bucketInterval, + chartData, + config, + data, + hits, + resetQuery, + savedSearch, + timeField, + services, +}: DiscoverChartProps) => { + const { from, to } = data.query.timefilter.timefilter.getTime(); + const timeRange = { + from: dateMath.parse(from)?.format('YYYY-MM-DDTHH:mm:ss.SSSZ') || '', + to: dateMath.parse(to, { roundUp: true })?.format('YYYY-MM-DDTHH:mm:ss.SSSZ') || '', + }; + + const onChangeInterval = () => {}; + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + return ( + + + 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} + /> + + {timeField && ( + + + + )} + {timeField && chartData && ( + +
+
+ +
+
+
+ )} +
+ ); +}; diff --git a/src/plugins/discover/public/application/components/histogram/histogram.tsx b/src/plugins/discover/public/application/components/chart/histogram.tsx similarity index 95% rename from src/plugins/discover/public/application/components/histogram/histogram.tsx rename to src/plugins/discover/public/application/components/chart/histogram.tsx index 5cf9ba88e878..caec3bce26c2 100644 --- a/src/plugins/discover/public/application/components/histogram/histogram.tsx +++ b/src/plugins/discover/public/application/components/chart/histogram.tsx @@ -58,12 +58,13 @@ import { i18n } from '@osd/i18n'; import { IUiSettingsClient } from 'opensearch-dashboards/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription, combineLatest } from 'rxjs'; -import { getServices } from '../../../opensearch_dashboards_services'; -import { Chart as IChart } from '../helpers/point_series'; +import { Chart as IChart } from './utils/point_series'; +import { DiscoverServices } from '../../../build_services'; export interface DiscoverHistogramProps { chartData: IChart; timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + services: DiscoverServices; } interface DiscoverHistogramState { @@ -140,15 +141,11 @@ export class DiscoverHistogram extends Component this.setState({ chartsTheme, chartsBaseTheme }) ); @@ -220,10 +217,11 @@ export class DiscoverHistogram extends Component; +} + +export interface Dimensions { + x: Dimension & { params: HistogramParams }; + y: Dimension; +} + +interface Ordered { + date: true; + interval: Duration; + intervalOpenSearchUnit: string; + intervalOpenSearchValue: number; + min: Moment; + max: Moment; +} +export interface Chart { + values: Array<{ + x: number; + y: number; + }>; + xAxisOrderedValues: number[]; + xAxisFormat: Dimension['format']; + xAxisLabel: Column['name']; + yAxisLabel?: Column['name']; + ordered: Ordered; +} + +export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { + const { x, y } = dimensions; + const xAccessor = table.columns[x.accessor].id; + const yAccessor = table.columns[y.accessor].id; + const chart = {} as Chart; + + chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); + chart.xAxisFormat = x.format; + chart.xAxisLabel = table.columns[x.accessor].name; + + const { intervalOpenSearchUnit, intervalOpenSearchValue, interval, bounds } = x.params; + chart.ordered = { + date: true, + interval, + intervalOpenSearchUnit, + intervalOpenSearchValue, + min: bounds.min, + max: bounds.max, + }; + + chart.yAxisLabel = table.columns[y.accessor].name; + + chart.values = table.rows + .filter((row) => row && row[yAccessor] !== 'NaN') + .map((row) => ({ + x: row[xAccessor] as number, + y: row[yAccessor] as number, + })); + + return chart; +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx index fe52673832b2..03d1287c5ea5 100644 --- a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx @@ -8,7 +8,7 @@ import { AppMountParameters } from '../../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; import { TopNav } from './top_nav'; -import { DiscoverTable } from './discover_table'; +import { DataDisplay } from './data_display'; interface CanvasProps { opts: { @@ -23,7 +23,7 @@ export const Canvas = ({ opts }: CanvasProps) => { return (
- +
); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/data_display.tsx similarity index 86% rename from src/plugins/discover/public/application/view_components/canvas/discover_table.tsx rename to src/plugins/discover/public/application/view_components/canvas/data_display.tsx index 84af208dbff8..76bdad8e5686 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/data_display.tsx @@ -8,15 +8,15 @@ import { History } from 'history'; import { IndexPattern } from '../../../opensearch_dashboards_services'; import { DiscoverServices } from '../../../build_services'; import { SavedSearch } from '../../../saved_searches'; -import { DiscoverTableService } from './discover_table_service'; +import { DataDisplayService } from './data_display_service'; import { fetchIndexPattern, fetchSavedSearch } from '../utils/index_pattern_helper'; -export interface DiscoverTableProps { +export interface DataDisplayProps { services: DiscoverServices; history: History; } -export const DiscoverTable = ({ history, services }: DiscoverTableProps) => { +export const DataDisplay = ({ history, services }: DataDisplayProps) => { const { core, chrome, data, uiSettings: config, toastNotifications } = services; const [savedSearch, setSavedSearch] = useState(); const [indexPattern, setIndexPattern] = useState(undefined); @@ -57,10 +57,6 @@ export const DiscoverTable = ({ history, services }: DiscoverTableProps) => { return null; } return ( - + ); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/data_display_app.scss b/src/plugins/discover/public/application/view_components/canvas/data_display_app.scss new file mode 100644 index 000000000000..e70f5d7f5d3d --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/data_display_app.scss @@ -0,0 +1,34 @@ +.dscWrapper__canvasContext { + padding: 0 $euiSize $euiSize 0; + overflow: hidden; + + @include euiBreakpoint("xs", "s") { + padding: 0 $euiSize $euiSize; + } +} + +.dscResultCount { + padding-top: $euiSizeXS; +} + +.dscTimechart { + display: block; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } +} + +.dscHistogram { + display: flex; + height: 200px; + padding: $euiSizeS; +} + +.dscHistogram__header--partial { + font-weight: $euiFontWeightRegular; + min-width: $euiSize * 12; +} diff --git a/src/plugins/discover/public/application/view_components/canvas/data_display_app.tsx b/src/plugins/discover/public/application/view_components/canvas/data_display_app.tsx new file mode 100644 index 000000000000..9c9f8fd2473a --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/data_display_app.tsx @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './data_display_app.scss'; +import React, { useState, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; +import { DataGridTable } from '../../components/data_grid/data_grid_table'; +import { DiscoverChart } from '../../components/chart/chart'; +import { DiscoverServices } from '../../../build_services'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { SavedSearch } from '../../../saved_searches'; + +export interface DataDisplayAppProps { + data$: any; + indexPattern: IndexPattern; + savedSearch: SavedSearch; + services: DiscoverServices; + resetQuery: () => void; +} + +export const DataDisplayApplication = ({ + data$, + indexPattern, + savedSearch, + services, + resetQuery, +}: DataDisplayAppProps) => { + const { data, uiSettings } = services; + const [fetchState, setFetchState] = useState({ + status: data$.getValue().status, + hits: 0, + fieldCounts: {}, + rows: [], + bucketInterval: '', + histogramData: {}, + }); + + const { rows, histogramData, bucketInterval, hits } = fetchState; + + useEffect(() => { + const subscription = data$.subscribe((next) => { + if ( + (next.status && next.status !== fetchState.status) || + (next.rows && next.rows !== fetchState.rows) || + (next.hits && next.hits !== fetchState.hits) || + (next.bucketInterval && next.bucketInterval !== fetchState.bucketInterval) || + (next.histogramData && next.histogramData !== fetchState.histogramData) + ) { + setFetchState({ ...fetchState, ...next }); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [data$, fetchState]); + + // TODO: implement columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns using config, indexPattern, appState + + const timeField = indexPattern.timeFieldName ? indexPattern.timeFieldName : undefined; + + if (rows.length === 0) { + return
{'loading...'}
; + } else { + return ( + + + + + + +
+ {}} + onFilter={() => {}} + onRemoveColumn={() => {}} + onSetColumns={() => {}} + onSort={() => {}} + sort={[]} + rows={rows} + displayTimeColumn={true} + services={services} + /> +
+
+
+
+ ); + } +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx b/src/plugins/discover/public/application/view_components/canvas/data_display_service.tsx similarity index 65% rename from src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx rename to src/plugins/discover/public/application/view_components/canvas/data_display_service.tsx index d0b0b0d1543e..1d1567b8d460 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/data_display_service.tsx @@ -7,21 +7,21 @@ import React, { useEffect } from 'react'; import { IndexPattern } from '../../../opensearch_dashboards_services'; import { DiscoverServices } from '../../../build_services'; import { SavedSearch } from '../../../saved_searches'; -import { useDiscoverTableService } from './utils/use_discover_canvas_service'; -import { DiscoverTableApplication } from './discover_table_app'; +import { useDataDisplayService } from './utils/use_data_display_service'; +import { DataDisplayApplication } from './data_display_app'; -export interface DiscoverTableAppProps { +export interface DataDisplayAppProps { services: DiscoverServices; savedSearch: SavedSearch; indexPattern: IndexPattern; } -export const DiscoverTableService = ({ +export const DataDisplayService = ({ services, savedSearch, indexPattern, -}: DiscoverTableAppProps) => { - const { data$, refetch$ } = useDiscoverTableService({ +}: DataDisplayAppProps) => { + const { data$, refetch$ } = useDataDisplayService({ services, savedSearch, indexPattern, @@ -38,12 +38,17 @@ export const DiscoverTableService = ({ refetch$.next(); }, [refetch$]); + // TODO: implement resetQuery which should reset savedSearch. This function should be returned + // in useDiscoverTableService as it is part of the service + const resetQuery = () => {}; + return ( - ); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx deleted file mode 100644 index b40492da8db3..000000000000 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import React, { useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; -import { DataGridTable } from '../../components/data_grid/data_grid_table'; - -export const DiscoverTableApplication = ({ data$, indexPattern, savedSearch, services }) => { - const [fetchState, setFetchState] = useState({ - status: data$.getValue().status, - fetchCounter: 0, - fieldCounts: {}, - rows: [], - }); - - const { rows } = fetchState; - - useEffect(() => { - const subscription = data$.subscribe((next) => { - if ( - (next.status && next.status !== fetchState.status) || - (next.rows && next.rows !== fetchState.rows) - ) { - setFetchState({ ...fetchState, ...next }); - } - }); - return () => { - subscription.unsubscribe(); - }; - }, [data$, fetchState]); - - // ToDo: implement columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns using config, indexPattern, appState - - if (rows.length === 0) { - return
{'loading...'}
; - } else { - return ( - - - -
- {}} - onFilter={() => {}} - onRemoveColumn={() => {}} - onSetColumns={() => {}} - onSort={() => {}} - sort={[]} - rows={rows} - displayTimeColumn={true} - services={services} - /> -
-
-
-
- ); - } -}; diff --git a/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts b/src/plugins/discover/public/application/view_components/canvas/utils/use_data_display_service.ts similarity index 90% rename from src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts rename to src/plugins/discover/public/application/view_components/canvas/utils/use_data_display_service.ts index 38d2b18b7b9b..ca58748c1d34 100644 --- a/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts +++ b/src/plugins/discover/public/application/view_components/canvas/utils/use_data_display_service.ts @@ -9,17 +9,17 @@ import { SavedSearch } from '../../../../saved_searches'; import { useSavedSearch } from '../../utils/use_saved_search'; import { IndexPattern } from '../../../../opensearch_dashboards_services'; -export interface DiscoverTableServiceProps { +export interface DataDisplayServiceProps { services: DiscoverServices; savedSearch: SavedSearch; indexPattern: IndexPattern; } -export const useDiscoverTableService = ({ +export const useDataDisplayService = ({ services, savedSearch, indexPattern, -}: DiscoverTableServiceProps) => { +}: DataDisplayServiceProps) => { const searchSource = useMemo(() => { savedSearch.searchSource.setField('index', indexPattern); return savedSearch.searchSource; diff --git a/src/plugins/discover/public/application/view_components/utils/update_data_source.ts b/src/plugins/discover/public/application/view_components/utils/update_data_source.ts index 00ab963e8863..5876fbb91a67 100644 --- a/src/plugins/discover/public/application/view_components/utils/update_data_source.ts +++ b/src/plugins/discover/public/application/view_components/utils/update_data_source.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ISearchSource, IndexPattern } from 'src/plugins/data/public'; +import { ISearchSource, IndexPattern, AggConfigs } from 'src/plugins/data/public'; import { DiscoverServices } from '../../../build_services'; import { SortOrder } from '../../../saved_searches/types'; import { getSortForSearchSource } from './get_sort_for_search_source'; @@ -14,6 +14,7 @@ export interface UpdateDataSourceProps { indexPattern: IndexPattern; services: DiscoverServices; sort: SortOrder[] | undefined; + histogramConfigs?: AggConfigs; } export const updateDataSource = ({ @@ -21,6 +22,7 @@ export const updateDataSource = ({ indexPattern, services, sort, + histogramConfigs, }: UpdateDataSourceProps) => { const { uiSettings, data } = services; const sortForSearchSource = getSortForSearchSource( @@ -38,5 +40,11 @@ export const updateDataSource = ({ .setField('highlightAll', true) .setField('version', true); + if (histogramConfigs) { + const dslAggs = histogramConfigs.toDsl(); + const updatedSearchSourceWithHstgAgg = updatedSearchSource.setField('aggs', dslAggs); + return updatedSearchSourceWithHstgAgg; + } + return updatedSearchSource; }; diff --git a/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts b/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts index 61d802d45140..21d158af260d 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts @@ -4,14 +4,20 @@ */ import { useCallback, useMemo, useRef } from 'react'; -import { ISearchSource, IndexPattern } from 'src/plugins/data/public'; import { BehaviorSubject, Subject, merge } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { useEffect } from 'react'; +import { ISearchSource, IndexPattern, search } from '../../../../../data/public'; import { DiscoverServices } from '../../../build_services'; import { validateTimeRange } from '../../../application/helpers/validate_time_range'; import { updateDataSource } from './update_data_source'; +import { tabifyAggResponse } from '../../../opensearch_dashboards_services'; +import { + getDimensions, + buildPointSeriesData, + createHistogramConfigs, +} from '../../components/chart/utils'; export enum FetchStatus { UNINITIALIZED = 'uninitialized', @@ -27,6 +33,8 @@ export interface SavedSearchData { fetchError?: Error; hits?: number; rows?: any[]; // TODO: type + bucketInterval?: string; + histogramData?: any; } export type SavedSearchRefetch = 'refetch' | undefined; @@ -68,8 +76,19 @@ export const useSavedSearch = ({ if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort(); fetchStateRef.current.abortController = new AbortController(); + const sort = undefined; - const updatedSearchSource = updateDataSource({ searchSource, indexPattern, services, sort }); + const histogramConfigs = indexPattern.timeFieldName + ? createHistogramConfigs(indexPattern, 'auto', data) + : undefined; + + const updatedSearchSource = updateDataSource({ + searchSource, + indexPattern, + services, + sort, + histogramConfigs, + }); try { const fetchResp = await updatedSearchSource.fetch({ @@ -77,6 +96,9 @@ export const useSavedSearch = ({ }); const hits = fetchResp.hits.total as number; const rows = fetchResp.hits.hits; + let bucketInterval; + let histogramData; + for (const row of rows) { const fields = Object.keys(indexPattern.flattenHit(row)); for (const fieldName of fields) { @@ -84,6 +106,19 @@ export const useSavedSearch = ({ (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1; } } + + if (histogramConfigs) { + const bucketAggConfig = histogramConfigs.aggs[1]; + const tabifiedData = tabifyAggResponse(histogramConfigs, fetchResp); + const dimensions = getDimensions(histogramConfigs, data); + if (dimensions) { + if (bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)) { + bucketInterval = bucketAggConfig.buckets?.getInterval(); + } + histogramData = buildPointSeriesData(tabifiedData, dimensions); + } + } + fetchStateRef.current.fieldCounts = fetchStateRef.current.fieldCounts!; fetchStateRef.current.fetchStatus = FetchStatus.COMPLETE; data$.next({ @@ -91,11 +126,13 @@ export const useSavedSearch = ({ fieldCounts: fetchStateRef.current.fieldCounts, hits, rows, + bucketInterval, + histogramData, }); } catch (err) { // TODO: handle the error } - }, [data$, timefilter, services, searchSource, indexPattern, fetchStateRef]); + }, [data, data$, timefilter, services, searchSource, indexPattern, fetchStateRef]); useEffect(() => { const fetch$ = merge(