diff --git a/public/components/event_analytics/explorer/events_views/data_grid.tsx b/public/components/event_analytics/explorer/events_views/data_grid.tsx index 18679a8a28..65a99a9582 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.tsx +++ b/public/components/event_analytics/explorer/events_views/data_grid.tsx @@ -11,6 +11,7 @@ import { EuiDescriptionListDescription, EuiDescriptionListTitle, EuiPanel, + EuiDataGridProps, } from '@elastic/eui'; import moment from 'moment'; import React, { Fragment, MutableRefObject, useEffect, useRef, useState } from 'react'; @@ -27,11 +28,10 @@ import { useFetchEvents } from '../../hooks'; import { redoQuery } from '../../utils/utils'; import { FlyoutButton } from './docViewRow'; -interface DataGridProps { +export interface DataGridProps { http: HttpSetup; pplService: PPLService; rows: any[]; - rowsAll: any[]; explorerFields: IExplorerFields; timeStampField: string; rawQuery: string; @@ -40,8 +40,12 @@ interface DataGridProps { startTime: string; endTime: string; storedSelectedColumns: IField[]; + formatGridColumn?: (columns: EuiDataGridColumn[]) => EuiDataGridColumn[]; + OuiDataGridProps?: Partial; } +const defaultFormatGrid = (columns: EuiDataGridColumn[]) => columns; + export function DataGrid(props: DataGridProps) { const { http, @@ -54,6 +58,8 @@ export function DataGrid(props: DataGridProps) { requestParams, startTime, endTime, + formatGridColumn = defaultFormatGrid, + OuiDataGridProps, } = props; const { fetchEvents } = useFetchEvents({ pplService, @@ -120,7 +126,7 @@ export function DataGrid(props: DataGridProps) { }); } }); - return columns; + return formatGridColumn(columns); }; // used for which columns are visible and their order @@ -258,6 +264,7 @@ export function DataGrid(props: DataGridProps) { showStyleSelector: false, }} rowHeightsOptions={rowHeightsOptions()} + {...OuiDataGridProps} /> diff --git a/public/components/event_analytics/utils/utils.tsx b/public/components/event_analytics/utils/utils.tsx index b19f3ce022..587ecba1e8 100644 --- a/public/components/event_analytics/utils/utils.tsx +++ b/public/components/event_analytics/utils/utils.tsx @@ -422,10 +422,11 @@ export const redoQuery = ( const start = datemath.parse(startTime)?.utc().format(DATE_PICKER_FORMAT); const end = datemath.parse(endTime, { roundUp: true })?.utc().format(DATE_PICKER_FORMAT); const tokens = rawQuery.replaceAll(PPL_NEWLINE_REGEX, '').match(PPL_INDEX_INSERT_POINT_REGEX); + const timeRange = timeStampField + ? `| where ${timeStampField} >= '${start}' and ${timeStampField} <= '${end}'` + : ''; - finalQuery = `${tokens![1]}=${ - tokens![2] - } | where ${timeStampField} >= '${start}' and ${timeStampField} <= '${end}'`; + finalQuery = `${tokens![1]}=${tokens![2]} ${timeRange}`; finalQuery += tokens![3]; diff --git a/public/dependencies/components/__snapshots__/data_grid_container.test.tsx.snap b/public/dependencies/components/__snapshots__/data_grid_container.test.tsx.snap new file mode 100644 index 0000000000..4619e70739 --- /dev/null +++ b/public/dependencies/components/__snapshots__/data_grid_container.test.tsx.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render empty when rawQuery is not provided 1`] = `
`; + +exports[` should render when all props are provided 1`] = ` +
+
+
+ { + "http": {}, + "pplService": {}, + "rows": [ + { + "timestamp": "now", + "count": 1 + } + ], + "explorerFields": { + "selectedFields": [], + "unselectedFields": [], + "availableFields": [], + "queriedFields": [] + }, + "timeStampField": "timestamp", + "totalHits": 1, + "requestParams": { + "tabId": "OBSERVABILITY_DEFAULT_TAB" + }, + "startTime": "", + "endTime": "now", + "storedSelectedColumns": [], + "OuiDataGridProps": { + "gridStyle": { + "fontSize": "s", + "cellPadding": "s" + }, + "minSizeForControls": 300, + "rowHeightsOptions": { + "defaultHeight": 80 + } + }, + "rawQuery": "source=foo", + "columns": [ + { + "id": "timestamp", + "isSortable": true, + "display": "Time", + "schema": "datetime", + "initialWidth": 50 + }, + { + "id": "_source", + "isSortable": false, + "display": "Source", + "schema": "_source" + } + ] +} +
+
+
+`; + +exports[` should switch props when in full screen mode 1`] = ` +
+
+
+ { + "http": {}, + "pplService": {}, + "rows": [ + { + "timestamp": "now", + "count": 1 + } + ], + "explorerFields": { + "selectedFields": [], + "unselectedFields": [], + "availableFields": [], + "queriedFields": [] + }, + "timeStampField": "timestamp", + "totalHits": 1, + "requestParams": { + "tabId": "OBSERVABILITY_DEFAULT_TAB" + }, + "startTime": "", + "endTime": "now", + "storedSelectedColumns": [], + "OuiDataGridProps": { + "gridStyle": {}, + "minSizeForControls": 300, + "rowHeightsOptions": {} + }, + "rawQuery": "source=foo", + "columns": [ + { + "id": "timestamp", + "isSortable": true, + "display": "Time", + "schema": "datetime", + "initialWidth": 200 + }, + { + "id": "_source", + "isSortable": false, + "display": "Source", + "schema": "_source" + } + ] +} +
+
+
+`; diff --git a/public/dependencies/components/data_grid_container.test.tsx b/public/dependencies/components/data_grid_container.test.tsx new file mode 100644 index 0000000000..96e12af86f --- /dev/null +++ b/public/dependencies/components/data_grid_container.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { DataGridContainer } from './data_grid_container'; +import { RenderProps } from '../../types'; +import { store } from '../../framework/redux/store'; +import { DataGridProps } from '../../components/event_analytics/explorer/events_views/data_grid'; +import { + DEFAULT_SOURCE_COLUMN as mock_DEFAULT_SOURCE_COLUMN, + DEFAULT_TIMESTAMP_COLUMN as mock_DEFAULT_TIMESTAMP_COLUMN, +} from '../../../common/constants/explorer'; + +jest.mock('../../components/event_analytics/utils/utils', () => ({ + redoQuery: (...args: any[]) => { + args[7]([ + { + timestamp: 'now', + count: 1, + }, + ]); + }, +})); + +jest.mock('../../../common/utils', () => ({ + getPPLService: () => ({}), + getOSDHttp: () => ({}), +})); + +jest.mock('../../components/event_analytics/explorer/events_views/data_grid', () => ({ + DataGrid: (props: DataGridProps) => { + const columnsResult = props.formatGridColumn?.([ + mock_DEFAULT_TIMESTAMP_COLUMN, + mock_DEFAULT_SOURCE_COLUMN, + ]); + return ( +
+ {JSON.stringify({ ...props, columns: columnsResult }, null, 2)} +
+ ); + }, +})); + +describe('', () => { + it('should render when all props are provided', async () => { + const { container, findByTestId } = render( + + + + ); + await findByTestId('test'); + expect(container).toMatchSnapshot(); + }); + + it('should render empty when rawQuery is not provided', async () => { + const { container, queryByTestId } = render( + + + + ); + await waitFor(() => { + expect(queryByTestId('test')).toBeNull(); + }); + expect(container).toMatchSnapshot(); + }); + + it('should switch props when in full screen mode', async () => { + const { container, findByTestId } = render( + + + + ); + await findByTestId('test'); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/dependencies/components/data_grid_container.tsx b/public/dependencies/components/data_grid_container.tsx new file mode 100644 index 0000000000..31825579f5 --- /dev/null +++ b/public/dependencies/components/data_grid_container.tsx @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { EuiDataGridColumn } from '@elastic/eui'; +import { + DataGrid, + DataGridProps, +} from '../../components/event_analytics/explorer/events_views/data_grid'; +import { getOSDHttp, getPPLService } from '../../../common/utils'; +import { selectFields } from '../../components/event_analytics/redux/slices/field_slice'; +import { initialTabId } from '../../framework/redux/store/shared_state'; +import { useFetchEvents } from '../../components/event_analytics/hooks'; +import { redoQuery } from '../../components/event_analytics/utils/utils'; +import { DEFAULT_TIMESTAMP_COLUMN } from '../../../common/constants/explorer'; +import { RenderProps } from '../../types'; + +export const DataGridContainer = ( + props: Pick & { renderProps: RenderProps } +) => { + const { renderProps, ...others } = props; + const pplService = getPPLService(); + const http = getOSDHttp(); + const tabId = initialTabId; + const explorerFields = useSelector(selectFields)[tabId]; + const { fetchEvents } = useFetchEvents({ + pplService, + requestParams: { + tabId, + }, + }); + const [data, setData] = useState([]); + const isFullScreen = renderProps.chatContext.flyoutFullScreen; + useEffect(() => { + if (props.rawQuery) { + redoQuery( + '', + '', + props.rawQuery || '', + '', + { current: [] }, + { current: [0, 5] }, + fetchEvents, + setData + ); + } + }, [props.rawQuery]); + const formatGridColumn = useCallback( + (columns: EuiDataGridColumn[]) => + columns.map((item) => { + if (item.id === DEFAULT_TIMESTAMP_COLUMN.id) { + const { initialWidth, ...otherConfig } = item; + return { + ...otherConfig, + initialWidth: renderProps.chatContext.flyoutFullScreen ? initialWidth : 50, + }; + } + + return item; + }), + [renderProps.chatContext.flyoutFullScreen] + ); + if (!data.length || !props.rawQuery) { + return null; + } + return ( +
+ +
+ ); +}; diff --git a/public/dependencies/components/ppl_visualization_model.tsx b/public/dependencies/components/ppl_visualization_model.tsx deleted file mode 100644 index 68a01da448..0000000000 --- a/public/dependencies/components/ppl_visualization_model.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiCodeBlock, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, -} from '@elastic/eui'; -import React from 'react'; -import { SavedVisualization } from '../../../common/types/explorer'; -import { SavedObjectVisualization } from '../../components/visualizations/saved_object_visualization'; -import { PPLSavedVisualizationClient } from '../../services/saved_objects/saved_object_client/ppl'; - -interface PPLVisualizationModelProps { - savedVisualization: SavedVisualization; - onClose: () => void; -} - -export const PPLVisualizationModal: React.FC = (props) => { - return ( - <> - - - {props.savedVisualization.name} - - - - -
- {props.savedVisualization.query} - -
-
- - - { - const response = await savePPLVisualization(props.savedVisualization); - props.onClose(); - window.open(`./observability-logs#/explorer/${response.objectId}`, '_blank'); - }} - fill - > - Save - - Close - - - ); -}; - -const savePPLVisualization = (savedVisualization: SavedVisualization) => { - const createParams = { - query: savedVisualization.query, - name: savedVisualization.name, - dateRange: [ - savedVisualization.selected_date_range.start, - savedVisualization.selected_date_range.end, - ], - fields: [], - timestamp: '', - type: savedVisualization.type, - sub_type: 'visualization', - }; - return PPLSavedVisualizationClient.getInstance().create(createParams); -}; diff --git a/public/dependencies/register_assistant.tsx b/public/dependencies/register_assistant.tsx index ebacf4d6bc..e064fb07b7 100644 --- a/public/dependencies/register_assistant.tsx +++ b/public/dependencies/register_assistant.tsx @@ -3,61 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { merge } from 'lodash'; import React from 'react'; -import { toMountPoint } from '../../../../src/plugins/opensearch_dashboards_react/public'; -import { SavedVisualization } from '../../common/types/explorer'; -import { SavedObjectVisualization } from '../components/visualizations/saved_object_visualization'; -import { coreRefs } from '../framework/core_refs'; -import { AssistantSetup } from '../types'; -import { PPLVisualizationModal } from './components/ppl_visualization_model'; +import { Provider } from 'react-redux'; +import { AssistantSetup, IMessage } from '../types'; +import { store } from '../framework/redux/store'; +import { DataGridContainer } from './components/data_grid_container'; export const registerAsssitantDependencies = (setup?: AssistantSetup) => { if (!setup) return; - setup.registerContentRenderer('ppl_visualization', (content) => { - const params = content as Partial; - const savedVisualization = createSavedVisualization(params); + setup.registerMessageRenderer('ppl_data_grid', (content, renderProps) => { + const params = content as IMessage; return ( - + + + ); }); - - setup.registerActionExecutor('view_ppl_visualization', async (params) => { - const savedVisualization = createSavedVisualization(params as Partial); - const modal = coreRefs.core!.overlays.openModal( - toMountPoint( - modal.close()} - /> - ) - ); - }); -}; - -const createSavedVisualization = (params: Partial) => { - return merge( - { - query: params.query, - selected_date_range: { start: 'now-14d', end: 'now', text: '' }, - selected_timestamp: { name: 'timestamp', type: 'timestamp' }, - selected_fields: { tokens: [], text: '' }, - name: params.name, - description: '', - type: 'line', - sub_type: 'visualization', - }, - { - selected_date_range: params.selected_date_range, - selected_timestamp: params.selected_timestamp, - type: params.type, - } - ) as SavedVisualization; }; diff --git a/public/types.ts b/public/types.ts index cfa38acb25..4d5fbd8322 100644 --- a/public/types.ts +++ b/public/types.ts @@ -42,4 +42,4 @@ export interface ObservabilityStart {} * It will gives an type error when dashboards-assistant is not installed so add a ts-ignore to suppress the error. */ // @ts-ignore -export type { AssistantSetup } from "../../dashboards-assistant/public"; +export type { AssistantSetup, RenderProps, IMessage } from '../../dashboards-assistant/public'; diff --git a/server/parsers/ppl_parser.ts b/server/parsers/ppl_parser.ts index 41c8fa4007..0386e1ef22 100644 --- a/server/parsers/ppl_parser.ts +++ b/server/parsers/ppl_parser.ts @@ -6,49 +6,46 @@ import { MessageParser } from '../types'; const extractPPLQueries = (content: string) => { - return Array.from(content.matchAll(/(^|[\n\r]|:)\s*(source\s*=\s*.+)/gi)).map( - (match) => match[2] - ); + return Array.from(content.matchAll(/(^|[\n\r]|:)\s*(source\s*=\s*.+)/gi)).map( + (match) => match[2] + ); }; export const PPLParsers: MessageParser = { - id: 'ppl_visualization_message', - async parserProvider(interaction) { - const ppls: string[] = (interaction.additional_info?.["PPLTool.output"] as string[] | null)?.flatMap((item: string) => { - let ppl: string = "" - try { - const outputResp = JSON.parse(item); - ppl = outputResp.ppl; - } catch (e) { - ppl = item; - } + id: 'ppl_visualization_message', + async parserProvider(interaction) { + const ppls: string[] = + (interaction.additional_info?.['PPLTool.output'] as string[] | null)?.flatMap( + (item: string) => { + let ppl: string = ''; + try { + const outputResp = JSON.parse(item); + ppl = outputResp.ppl; + } catch (e) { + ppl = item; + } - return extractPPLQueries(ppl); - }) || []; + return extractPPLQueries(ppl); + } + ) || []; - if (!ppls.length) return []; + if (!ppls.length) return []; - const statsPPLs = ppls.filter((ppl) => /\|\s*stats\s+[^|]+\sby\s/i.test(ppl)); - if (!statsPPLs.length) { - return []; - } + const statsPPLs = ppls.filter((ppl) => /\|\s*stats\s+[^|]+\sby\s/i.test(ppl)); + if (!statsPPLs.length) { + return []; + } - return statsPPLs.map((query) => { - const finalQuery = query - .replace(/`/g, '') // workaround for https://github.com/opensearch-project/dashboards-observability/issues/509, https://github.com/opensearch-project/dashboards-observability/issues/557 - .replace(/\bSPAN\(/g, 'span('); // workaround for https://github.com/opensearch-project/dashboards-observability/issues/759 - return ({ - type: 'output', - content: finalQuery, - contentType: 'ppl_visualization', - suggestedActions: [ - { - message: 'View details', - actionType: 'view_ppl_visualization', - metadata: { query: finalQuery, question: interaction.input }, - }, - ], - }); - }); - }, + return statsPPLs.map((query) => { + const finalQuery = query + .replace(/`/g, '') // workaround for https://github.com/opensearch-project/dashboards-observability/issues/509, https://github.com/opensearch-project/dashboards-observability/issues/557 + .replace(/\bSPAN\(/g, 'span('); // workaround for https://github.com/opensearch-project/dashboards-observability/issues/759 + return { + type: 'output', + content: finalQuery, + contentType: 'ppl_data_grid', + fullWidth: true, + }; + }); + }, };