From f21939d0848d88c8b87be960142887ac49ba3787 Mon Sep 17 00:00:00 2001 From: Simeon Widdis Date: Thu, 14 Mar 2024 14:45:43 -0700 Subject: [PATCH 1/3] Update snapshots (#1533) Signed-off-by: Simeon Widdis --- .../__tests__/__snapshots__/data_grid.test.tsx.snap | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap b/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap index 3818564438..d949e5d101 100644 --- a/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap +++ b/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap @@ -137,7 +137,9 @@ exports[`Datagrid component Renders data grid component 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -165,7 +167,9 @@ exports[`Datagrid component Renders data grid component 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -623,7 +627,9 @@ exports[`Datagrid component renders data grid with different timestamp 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -651,7 +657,9 @@ exports[`Datagrid component renders data grid with different timestamp 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", From 71dccc3c14c9a75106bf736e98b1a2b64fd8b5c7 Mon Sep 17 00:00:00 2001 From: Jialiang Liang Date: Thu, 14 Mar 2024 15:50:27 -0700 Subject: [PATCH 2/3] [Feature] Acceleration components' data implementation (#1521) * 1st commit of acc details connection Signed-off-by: Ryan Liang * Update the snapshot Signed-off-by: Ryan Liang * Fix the interface naming Signed-off-by: Ryan Liang * Fix the status Signed-off-by: Ryan Liang * Add the index health Signed-off-by: Ryan Liang * Add change the field name into action Signed-off-by: Ryan Liang * Wired up schema tab Signed-off-by: Ryan Liang * Cache is working 0 with max depth exceeding issue Signed-off-by: Ryan Liang * update snapshots Signed-off-by: Ryan Liang * Fix the infinite loop and apply the status check correctly Signed-off-by: Ryan Liang * Implement the refresh button Signed-off-by: Ryan Liang * Rebase after apply new interface 1 Signed-off-by: Ryan Liang * Rebase after apply new interface 2 + finalize the design of refreshing button Signed-off-by: Ryan Liang * refactor some comments Signed-off-by: Ryan Liang * Fix table type column Signed-off-by: Ryan Liang * Fix empty item with replacement of unredered - Signed-off-by: Ryan Liang * Fix the destination index column Signed-off-by: Ryan Liang * Fix status Signed-off-by: Ryan Liang * Fix the skip index name Signed-off-by: Ryan Liang * Fix the destination index column behavior when it is skip index Signed-off-by: Ryan Liang * Correct the render behavior for skip index flyout Signed-off-by: Ryan Liang * Fix the table loading infinite loop Signed-off-by: Ryan Liang * Modify the behavior of getting this refreh interval and type for skipping Signed-off-by: Ryan Liang * Fix the data source at the flyout details tab Signed-off-by: Ryan Liang * Swtich the data connection tabs back to default order and update snapshots Signed-off-by: Ryan Liang * Add refresh time for refreshing Signed-off-by: Ryan Liang * Add loading panel 0 Signed-off-by: Ryan Liang * Fix loading state for table Signed-off-by: Ryan Liang * Add the refresh type column to acceleration table Signed-off-by: Ryan Liang * Add acceleration table test Signed-off-by: Ryan Liang * Add acceleration table test 2 Signed-off-by: Ryan Liang * Add refresh field to flyout Signed-off-by: Ryan Liang * Fix some comments Signed-off-by: Ryan Liang * Fix some comments 2 Signed-off-by: Ryan Liang * Add null/undefined check for flintIndexName Signed-off-by: Ryan Liang * remove console logs Signed-off-by: Ryan Liang * Add eslint-dsiable for export in dsl Signed-off-by: Ryan Liang --------- Signed-off-by: Ryan Liang --- common/constants/shared.ts | 1 + .../custom_panel_view.test.tsx.snap | 4 + .../acceleration_details_flyout.test.tsx | 95 +++++++++ .../__tests__/acceleration_table.test.tsx | 163 ++++++++++++++ .../acceleration_details_flyout.tsx | 103 ++++++++- .../accelerations/acceleration_table.tsx | 198 +++++++++++++----- .../acceleration_details_tab.tsx | 130 +++++++----- .../accelerations_schema_tab.tsx | 34 ++- .../manage/accelerations/helpers/utils.tsx | 38 ---- .../utils/acceleration_utils.tsx | 80 +++++++ .../associated_objects_details_flyout.tsx | 2 +- .../components/manage/data_connection.tsx | 48 +++-- public/framework/core_refs.ts | 2 + public/plugin.tsx | 2 + public/services/requests/dsl.ts | 32 ++- server/routes/dsl.ts | 35 +++- 16 files changed, 784 insertions(+), 183 deletions(-) create mode 100644 public/components/datasources/components/__tests__/acceleration_details_flyout.test.tsx create mode 100644 public/components/datasources/components/__tests__/acceleration_table.test.tsx delete mode 100644 public/components/datasources/components/manage/accelerations/helpers/utils.tsx create mode 100644 public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx diff --git a/common/constants/shared.ts b/common/constants/shared.ts index d452b14c4a..5af6933995 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -10,6 +10,7 @@ export const DSL_BASE = '/api/dsl'; export const DSL_SEARCH = '/search'; export const DSL_CAT = '/cat.indices'; export const DSL_MAPPING = '/indices.getFieldMapping'; +export const DSL_SETTINGS = '/indices.getFieldSettings'; export const OBSERVABILITY_BASE = '/api/observability'; export const INTEGRATIONS_BASE = '/api/integrations'; export const JOBS_BASE = '/query/jobs'; diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap index a1fa01383d..ecd0abef40 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap @@ -1287,6 +1287,7 @@ exports[`Panels View Component renders panel view container with visualizations "fetch": [Function], "fetchFields": [Function], "fetchIndices": [Function], + "fetchSettings": [Function], "http": [MockFunction], } } @@ -1754,6 +1755,7 @@ exports[`Panels View Component renders panel view container with visualizations "fetch": [Function], "fetchFields": [Function], "fetchIndices": [Function], + "fetchSettings": [Function], "http": [MockFunction], } } @@ -3520,6 +3522,7 @@ exports[`Panels View Component renders panel view container without visualizatio "fetch": [Function], "fetchFields": [Function], "fetchIndices": [Function], + "fetchSettings": [Function], "http": [MockFunction], } } @@ -3985,6 +3988,7 @@ exports[`Panels View Component renders panel view container without visualizatio "fetch": [Function], "fetchFields": [Function], "fetchIndices": [Function], + "fetchSettings": [Function], "http": [MockFunction], } } diff --git a/public/components/datasources/components/__tests__/acceleration_details_flyout.test.tsx b/public/components/datasources/components/__tests__/acceleration_details_flyout.test.tsx new file mode 100644 index 0000000000..b3c4225142 --- /dev/null +++ b/public/components/datasources/components/__tests__/acceleration_details_flyout.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { mount, configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { AccelerationDetailsFlyout } from '../manage/accelerations/acceleration_details_flyout'; +import * as coreRefsModule from '../../../../framework/core_refs'; + +jest.mock('../../../../framework/core_refs', () => { + const actualModule = jest.requireActual('../../../../framework/core_refs'); + return { + coreRefs: { + ...actualModule.coreRefs, + dslService: { + fetchFields: jest.fn().mockResolvedValue({ data: 'mockFieldData' }), + fetchSettings: jest.fn().mockResolvedValue({ data: 'mockSettingsData' }), + fetchIndices: jest.fn().mockResolvedValue({ data: 'mockIndexData' }), + }, + }, + }; +}); + +jest.mock('../../../../framework/core_refs', () => { + return { + coreRefs: { + dslService: { + fetchFields: jest.fn().mockResolvedValue({ data: 'mockFieldData' }), + fetchSettings: jest.fn().mockResolvedValue({ data: 'mockSettingsData' }), + fetchIndices: jest.fn().mockResolvedValue({ + status: 'fulfilled', + action: 'getIndexInfo', + data: [ + { + health: 'yellow', + status: 'open', + index: 'flint_mys3_default_http_count_view', + uuid: 'VImREbK4SMqJ-i6hSB84eQ', + pri: '1', + rep: '1', + 'docs.count': '0', + 'docs.deleted': '0', + 'store.size': '208b', + 'pri.store.size': '208b', + }, + ], + }), + }, + }, + }; +}); + +const mockAcceleration = { + index: 'mockIndex', + dataSourceName: 'mockDataSource', + acceleration: { + flintIndexName: 'testIndex', + }, +}; + +configure({ adapter: new Adapter() }); + +describe('AccelerationDetailsFlyout Component Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches acceleration details on mount', async () => { + mount(); + + expect(coreRefsModule.coreRefs.dslService!.fetchFields).toHaveBeenCalledWith('testIndex'); + expect(coreRefsModule.coreRefs.dslService!.fetchSettings).toHaveBeenCalledWith('testIndex'); + expect(coreRefsModule.coreRefs.dslService!.fetchIndices).toHaveBeenCalledWith('testIndex'); + }); + + it('switches tabs correctly', async () => { + const wrapper = mount(); + await new Promise(setImmediate); + wrapper.update(); + + const schemaTabExists = wrapper.find('EuiTab').someWhere((node) => node.text() === 'Schema'); + expect(schemaTabExists).toBeTruthy(); + + const schemaTab = wrapper.find('EuiTab').filterWhere((node) => node.text() === 'Schema'); + schemaTab.simulate('click'); + await new Promise(setImmediate); + wrapper.update(); + + expect(wrapper.find('AccelerationSchemaTab').exists()).toBe(true); + + // TODO: SQL DEFINATION TAB CHECK + }); +}); diff --git a/public/components/datasources/components/__tests__/acceleration_table.test.tsx b/public/components/datasources/components/__tests__/acceleration_table.test.tsx new file mode 100644 index 0000000000..5531d030ba --- /dev/null +++ b/public/components/datasources/components/__tests__/acceleration_table.test.tsx @@ -0,0 +1,163 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { mount, configure } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { AccelerationTable } from '../manage/accelerations/acceleration_table'; +import { act } from 'react-dom/test-utils'; +import Adapter from 'enzyme-adapter-react-16'; +import { ACC_LOADING_MSG } from '../manage/accelerations/utils/acceleration_utils'; +import { ReactWrapper } from 'enzyme'; + +const accelerationCache = { + accelerations: [ + { + flintIndexName: 'flint_mys3_default_http_count_view', + type: 'materialized', + database: 'default', + table: null, + indexName: 'http_count_view', + autoRefresh: true, + status: 'refreshing', + }, + { + flintIndexName: 'flint_mys3_default_http_count_view_alt', + type: 'materialized', + database: 'default', + table: null, + indexName: 'http_count_view_alt', + autoRefresh: true, + status: 'refreshing', + }, + { + flintIndexName: 'flint_mys3_default_http_logs', + type: 'materialized', + database: 'default', + table: null, + indexName: 'http_logs', + autoRefresh: true, + status: 'deleted', + }, + { + flintIndexName: 'flint_mys3_default_http_logs_skipping_index', + type: 'skipping', + database: 'default', + table: 'http_logs', + indexName: null, + autoRefresh: false, + status: 'active', + }, + { + flintIndexName: 'flint_mys3_other_http_count_view', + type: 'materialized', + database: 'other', + table: null, + indexName: 'http_count_view', + autoRefresh: true, + status: 'refreshing', + }, + ], + lastUpdated: 'Thu, 14 Mar 2024 04:05:53 GMT', + status: 'Updated', +}; + +jest.mock('../../../../framework/catalog_cache/cache_manager', () => ({ + CatalogCacheManager: { + getOrCreateAccelerationsByDataSource: jest.fn().mockReturnValue(accelerationCache), + }, +})); + +jest.mock('../../../../framework/catalog_cache/cache_loader', () => ({ + useLoadAccelerationsToCache: jest.fn(() => ({ + loadStatus: 'success', + startLoading: jest.fn(), + })), +})); + +jest.mock('../../../../plugin', () => ({ + getRenderAccelerationDetailsFlyout: jest.fn(() => jest.fn()), +})); + +describe('AccelerationTable Component', () => { + configure({ adapter: new Adapter() }); + + it('renders without crashing', () => { + const wrapper = mount(); + expect(wrapper).toBeDefined(); + }); + + it('shows loading spinner when refreshing accelerations', async () => { + jest.mock('../../../../framework/catalog_cache/cache_loader', () => ({ + useLoadAccelerationsToCache: jest.fn(() => ({ + loadStatus: 'loading', + startLoading: jest.fn(), + })), + })); + + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount(); + }); + + wrapper!.update(); + + await act(async () => { + wrapper!.find('[data-test-subj="refreshButton"]').simulate('click'); + }); + wrapper!.update(); + + expect(wrapper!.find(EuiLoadingSpinner).exists()).toBe(true); + expect(wrapper!.text()).toContain(ACC_LOADING_MSG); + + jest.restoreAllMocks(); + }); + + it('correctly displays accelerations in the table', async () => { + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount(); + }); + wrapper!.update(); + + const tableRows = wrapper!.find('EuiTableRow'); + expect(tableRows.length).toBe(accelerationCache.accelerations.length); + }); + + it('filters rows based on active status correctly', async () => { + jest.mock('../../../../framework/catalog_cache/cache_loader', () => ({ + useLoadAccelerationsToCache: jest.fn(() => ({ + loadStatus: 'loading', + startLoading: jest.fn(), + })), + })); + + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount(); + await new Promise((resolve) => setTimeout(resolve, 0)); + wrapper!.update(); + }); + + const activeStatusRows = wrapper!.find('tr.euiTableRow').filterWhere((node) => { + return node.find('.euiFlexItem').someWhere((subNode) => subNode.text() === 'active'); + }); + + expect(activeStatusRows.length).toBe( + accelerationCache.accelerations.filter((acc) => acc.status === 'active').length + ); + jest.restoreAllMocks(); + }); + + it('displays updated time correctly', async () => { + let wrapper: ReactWrapper; + await act(async () => { + wrapper = mount(); + }); + wrapper!.update(); + + expect(wrapper!.text()).toContain(accelerationCache.lastUpdated); + }); +}); diff --git a/public/components/datasources/components/manage/accelerations/acceleration_details_flyout.tsx b/public/components/datasources/components/manage/accelerations/acceleration_details_flyout.tsx index 89ea007002..9ec84e1ea3 100644 --- a/public/components/datasources/components/manage/accelerations/acceleration_details_flyout.tsx +++ b/public/components/datasources/components/manage/accelerations/acceleration_details_flyout.tsx @@ -15,7 +15,7 @@ import { EuiTabs, EuiText, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AccelerationDetailsTab } from './flyout_modules/acceleration_details_tab'; import { AccelerationSchemaTab } from './flyout_modules/accelerations_schema_tab'; import { AccelerationSqlTab } from './flyout_modules/acceleration_sql_tab'; @@ -24,20 +24,85 @@ import { onRefreshButtonClick, onDiscoverButtonClick, onDeleteButtonClick, -} from '../accelerations/helpers/utils'; +} from './utils/acceleration_utils'; +import { coreRefs } from '../../../../../framework/core_refs'; +import { OpenSearchDashboardsResponse } from '../../../../../../../../src/core/server/http/router'; export interface AccelerationDetailsFlyoutProps { acceleration: any; } -export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) => { - const { acceleration } = props; +const getMappings = (index: string): Promise | undefined => { + return coreRefs.dslService?.fetchFields(index); +}; + +const getSettings = (index: string): Promise | undefined => { + return coreRefs.dslService?.fetchSettings(index); +}; + +const getIndexInfo = (index: string): Promise | undefined => { + return coreRefs.dslService?.fetchIndices(index); +}; + +const handleDetailsFetchingPromise = ( + promise: Promise | undefined, + action: string +) => { + return promise! + .then((data) => ({ status: 'fulfilled', action, data })) + .catch((error) => ({ status: 'rejected', action, error })); +}; + +export const AccelerationDetailsFlyout = ({ + acceleration: selectedAcc, +}: AccelerationDetailsFlyoutProps) => { + const { index, dataSourceName, acceleration } = selectedAcc; + const { flintIndexName } = acceleration; const [selectedTab, setSelectedTab] = useState('details'); const tabsMap: { [key: string]: any } = { details: AccelerationDetailsTab, schema: AccelerationSchemaTab, sql_definition: AccelerationSqlTab, }; + const [settings, setSettings] = useState(); + const [mappings, setMappings] = useState(); + const [indexInfo, setIndexInfo] = useState(); + + const updateMapping = (result) => { + setMappings(result); + }; + + const updateSetting = (result, slectedIndex: string) => { + setSettings(result.data[slectedIndex]); + }; + + const updateIndexInfo = (result) => { + setIndexInfo(result); + }; + + const getAccDetail = (selectedIndex: string) => { + Promise.all([ + handleDetailsFetchingPromise(getMappings(selectedIndex), 'getMappings'), + handleDetailsFetchingPromise(getSettings(selectedIndex), 'getSettings'), + handleDetailsFetchingPromise(getIndexInfo(selectedIndex), 'getIndexInfo'), + ]) + .then((results) => { + updateMapping(results[0]); + updateSetting(results[1], selectedIndex); + updateIndexInfo(results[2]); + }) + .catch((errors: Error[]) => { + errors.forEach((error, errorIndex) => { + console.error(`Error in async call ${errorIndex + 1}:`, error); + }); + }); + }; + + useEffect(() => { + if (flintIndexName !== undefined && flintIndexName.trim().length > 0) { + getAccDetail(flintIndexName); + } + }, [flintIndexName]); const DiscoverButton = () => { // TODO: display button if can be sent to discover @@ -83,13 +148,13 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) ]; const renderTabs = () => { - return accelerationDetailsTabs.map((tab, index) => { + return accelerationDetailsTabs.map((tab, tabIndex) => { return ( setSelectedTab(tab.id)} isSelected={tab.id === selectedTab} disabled={tab.disabled} - key={index} + key={tabIndex} > {tab.name} @@ -97,9 +162,27 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) }); }; - const renderTabContent = (tab: string, tabAcceleration: any) => { + const renderTabContent = (tab: string) => { + let propsForTab; + + switch (tab) { + case 'details': + propsForTab = { acceleration, settings, mappings, indexInfo, dataSourceName }; + break; + case 'schema': + propsForTab = { mappings, indexInfo }; + break; + case 'sql_definition': + propsForTab = { mappings }; + break; + default: + console.log('Unknown Tab: ', tab); + return null; + } + const TabToDisplay = tabsMap[tab]; - return ; + + return ; }; return ( @@ -108,7 +191,7 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) -

{acceleration.name}

+

{index}

@@ -124,7 +207,7 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) {renderTabs()} - {renderTabContent(selectedTab, acceleration)} + {renderTabContent(selectedTab)} ); }; diff --git a/public/components/datasources/components/manage/accelerations/acceleration_table.tsx b/public/components/datasources/components/manage/accelerations/acceleration_table.tsx index aeda0d7ea7..65b5b507db 100644 --- a/public/components/datasources/components/manage/accelerations/acceleration_table.tsx +++ b/public/components/datasources/components/manage/accelerations/acceleration_table.tsx @@ -14,31 +14,84 @@ import { EuiLink, EuiInMemoryTable, EuiBasicTableColumn, + EuiLoadingSpinner, + EuiEmptyPrompt, } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getRefreshButtonIcon, onRefreshButtonClick, onDiscoverButtonClick, onDeleteButtonClick, AccelerationStatus, -} from './helpers/utils'; + ACC_LOADING_MSG, + ACC_PANEL_TITLE, + ACC_PANEL_DESC, +} from './utils/acceleration_utils'; import { getRenderAccelerationDetailsFlyout } from '../../../../../plugin'; +import { CatalogCacheManager } from '../../../../../framework/catalog_cache/cache_manager'; +import { + CachedAccelerations, + CachedDataSourceStatus, +} from '../../../../../../common/types/data_connections'; +import { useLoadAccelerationsToCache } from '../../../../../framework/catalog_cache/cache_loader'; +import { DirectQueryLoadingStatus } from '../../../../../../common/types/explorer'; -interface AccelerationTableTabProps { - // TODO: Add acceleration type to plugin types - accelerations: any[]; +interface AccelerationTableProps { + dataSourceName: string; } -export const AccelerationTable = (props: AccelerationTableTabProps) => { - const { accelerations } = props; +export const AccelerationTable = ({ dataSourceName }: AccelerationTableProps) => { + const [accelerations, setAccelerations] = useState([]); + const [updatedTime, setUpdatedTime] = useState(); + const { loadStatus, startLoading } = useLoadAccelerationsToCache(); + const [isRefreshing, setIsRefreshing] = useState(false); + + useEffect(() => { + const cachedDataSource = CatalogCacheManager.getOrCreateAccelerationsByDataSource( + dataSourceName + ); + if (cachedDataSource.status === CachedDataSourceStatus.Empty) { + console.log( + `Cache for dataSource ${dataSourceName} is empty or outdated. Loading accelerations...` + ); + setIsRefreshing(true); + startLoading(dataSourceName); + } else { + console.log(`Using cached accelerations for dataSource: ${dataSourceName}`); + + setAccelerations(cachedDataSource.accelerations); + setUpdatedTime(cachedDataSource.lastUpdated); + } + }, []); + + useEffect(() => { + if (loadStatus === DirectQueryLoadingStatus.SUCCESS) { + const cachedDataSource = CatalogCacheManager.getOrCreateAccelerationsByDataSource( + dataSourceName + ); + setAccelerations(cachedDataSource.accelerations); + setUpdatedTime(cachedDataSource.lastUpdated); + setIsRefreshing(false); + console.log('Refresh process is success.'); + } + if (loadStatus === DirectQueryLoadingStatus.FAILED) { + setIsRefreshing(false); + console.log('Refresh process is failed.'); + } + }, [loadStatus]); + + const handleRefresh = () => { + console.log('Initiating refresh...'); + setIsRefreshing(true); + startLoading(dataSourceName); + }; const RefreshButton = () => { - // TODO: Implement logic for refreshing acceleration return ( - <> - console.log('clicked on refresh button')}>Refresh - + + Refresh + ); }; @@ -54,29 +107,50 @@ export const AccelerationTable = (props: AccelerationTableTabProps) => { ); }; + console.log('HERE IS THE UPDATED TIME', updatedTime); const AccelerationTableHeader = () => { return ( <> - + -

Accelerations

-

- Accelerations optimize query performance by indexing external data into OpenSearch. -

+

{ACC_PANEL_TITLE}

+

{ACC_PANEL_DESC}

- - - - + + + + + + + + + + {'Last updated'} + + + {updatedTime} + + +
); }; + const AccelerationLoading = () => { + const BodyText = () => ( + <> +

{ACC_LOADING_MSG}

+ + ); + + return } body={} />; + }; + const tableActions = [ { name: 'Discover', @@ -100,24 +174,29 @@ export const AccelerationTable = (props: AccelerationTableTabProps) => { }, ]; - const accelerationTableColumns: Array> = [ - // TODO: fields should be determined by what the acceleration is - // Show N/A if not applicable + const accelerationTableColumns = [ { - field: 'name', + field: 'indexName', name: 'Name', sortable: true, - render: (name: string) => ( - - renderAccelerationDetailsFlyout( - accelerations.find((acceleration) => acceleration.name === name) - ) - } - > - {name} - - ), + render: (indexName: string, acceleration: any) => { + const displayName = + indexName || + `${dataSourceName}_${acceleration.database}_${acceleration.table}`.replace(/\s+/g, '_'); + return ( + { + renderAccelerationDetailsFlyout({ + index: displayName, + acceleration, + dataSourceName, + }); + }} + > + {displayName} + + ); + }, }, { field: 'status', @@ -132,17 +211,17 @@ export const AccelerationTable = (props: AccelerationTableTabProps) => { render: (type: string) => { let label; switch (type) { - case 'skip': + case 'skipping': label = 'Skipping Index'; break; - case 'mv': + case 'materialized': label = 'Materialized View'; break; - case 'ci': + case 'covering': label = 'Covering Index'; break; default: - label = 'default'; + label = 'INVALID TYPE'; } return {label}; }, @@ -157,21 +236,32 @@ export const AccelerationTable = (props: AccelerationTableTabProps) => { field: 'table', name: 'Table', sortable: true, - render: (table: string) => {table}, + render: (table: string) => {table || '-'}, + }, + { + field: 'refreshType', + name: 'Refresh Type', + sortable: true, + render: (autoRefresh: boolean, acceleration: CachedAccelerations) => { + return {acceleration.autoRefresh ? 'Auto refresh' : 'Manual'}; + }, }, { - field: 'destination', + field: 'flintIndexName', name: 'Destination Index', sortable: true, - render: (destination: string) => ( - console.log('clicked on', destination)}>{destination} - ), + render: (flintIndexName: string, acceleration: CachedAccelerations) => { + if (acceleration.type === 'skipping') { + return '-'; + } + return flintIndexName || '-'; + }, }, { name: 'Actions', actions: tableActions, }, - ]; + ] as Array>; const pagination = { initialPageSize: 10, @@ -180,12 +270,10 @@ export const AccelerationTable = (props: AccelerationTableTabProps) => { const sorting = { sort: { - field: 'name', direction: 'asc', }, }; - // Render flyout using OSD overlay service const renderAccelerationDetailsFlyout = getRenderAccelerationDetailsFlyout(); return ( @@ -195,12 +283,16 @@ export const AccelerationTable = (props: AccelerationTableTabProps) => { - + {isRefreshing ? ( + + ) : ( + + )} ); diff --git a/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_details_tab.tsx b/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_details_tab.tsx index 078d7c74ac..a6818f9882 100644 --- a/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_details_tab.tsx +++ b/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_details_tab.tsx @@ -2,7 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - +import React from 'react'; import { EuiDescriptionList, EuiDescriptionListDescription, @@ -14,39 +14,69 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import React from 'react'; -import { AccelerationStatus } from '../helpers/utils'; +import { AccelerationHealth, AccelerationStatus } from '../utils/acceleration_utils'; interface AccelerationDetailsTabProps { - acceleration: any; + acceleration: { + flintIndexName: string; + kind: string; + database: string; + table: string; + indexName: string; + autoRefresh: boolean; + status: string; + }; + settings: object; + mappings: object; + indexInfo: any; + dataSourceName: string; } -export const AccelerationDetailsTab = (props: AccelerationDetailsTabProps) => { - const { acceleration } = props; +export const AccelerationDetailsTab = ({ + acceleration, + settings, + mappings, + indexInfo, + dataSourceName, +}: AccelerationDetailsTabProps) => { + const isSkippingIndex = + mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.kind === 'skipping'; + const refreshIntervalDescription = acceleration.autoRefresh ? 'Auto refresh' : 'Manual'; + const showRefreshTime = + acceleration.autoRefresh || + mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.options.incremental_refresh; + const refreshTime = showRefreshTime + ? mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.options.refresh_interval + : '-'; + const creationDate = new Date( + parseInt(settings?.settings?.index?.creation_date, 10) + ).toLocaleString(); + const checkpointName = + mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.options?.checkpoint_location; - const DetailComponent = (detailProps: { title: string; description: any }) => { - const { title, description } = detailProps; - return ( - - - {title} - {description} - - - ); - }; + const DetailComponent = ({ + title, + description, + }: { + title: string; + description: React.ReactNode; + }) => ( + + + {title} + {description} + + + ); - const TitleComponent = (titleProps: { title: string }) => { - const { title } = titleProps; - return ( - <> - -

{title}

-
- - - ); - }; + const TitleComponent = ({ title }: { title: string }) => ( + <> + +

{title}

+
+ + + ); return ( <> @@ -56,37 +86,43 @@ export const AccelerationDetailsTab = (props: AccelerationDetailsTabProps) => { title="Status" description={} /> - - -
- + console.log()}>{acceleration.index}} + description={ console.log()}>{dataSourceName}} /> - + + {!isSkippingIndex && ( + <> + + + + + } + /> + + + )} - - - - } - /> - + + + + + {checkpointName && ( + + )} ); diff --git a/public/components/datasources/components/manage/accelerations/flyout_modules/accelerations_schema_tab.tsx b/public/components/datasources/components/manage/accelerations/flyout_modules/accelerations_schema_tab.tsx index 7498ad2e0f..9c11fd7267 100644 --- a/public/components/datasources/components/manage/accelerations/flyout_modules/accelerations_schema_tab.tsx +++ b/public/components/datasources/components/manage/accelerations/flyout_modules/accelerations_schema_tab.tsx @@ -3,36 +3,48 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBasicTable } from '@elastic/eui'; +import { EuiInMemoryTable, EuiTableFieldDataColumnType } from '@elastic/eui'; import React from 'react'; interface AccelerationSchemaTabProps { - acceleration: any; + mappings: object; + indexInfo: object; } -export const AccelerationSchemaTab = (props: AccelerationSchemaTabProps) => { - const { acceleration } = props; - // TODO: Use schema returned from backend - console.log(acceleration); +export const AccelerationSchemaTab = ({ mappings, indexInfo }: AccelerationSchemaTabProps) => { + const indexName = indexInfo.data[0]?.index; + const indexData = mappings.data[indexName]?.mappings._meta?.indexedColumns; + const indexType = mappings.data[indexName]?.mappings._meta?.kind; + const isSkippingIndex = indexType === 'skipping'; + + const items = + indexData?.map((column: { columnName: string; columnType: string; kind: string }) => ({ + columns_name: column.columnName, + data_type: column.columnType, + acceleration_type: column.kind, + })) || []; const columns = [ { - field: 'columns', + field: 'columns_name', name: 'Column name', }, { field: 'data_type', name: 'Data type', }, - { + ] as Array>; + + if (isSkippingIndex) { + columns.push({ field: 'acceleration_type', name: 'Acceleration index type', - }, - ]; + }); + } return ( <> - + ); }; diff --git a/public/components/datasources/components/manage/accelerations/helpers/utils.tsx b/public/components/datasources/components/manage/accelerations/helpers/utils.tsx deleted file mode 100644 index b3f992a945..0000000000 --- a/public/components/datasources/components/manage/accelerations/helpers/utils.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { EuiHealth } from '@elastic/eui'; - -export const AccelerationStatus = (props: { status: string }) => { - const { status } = props; - // TODO: actually get status from acceleration - console.log('status is', status); - const label = status === 'ACTIVE' ? 'Active' : 'Paused'; - const color = status === 'ACTIVE' ? 'success' : 'inactive'; - return {label}; -}; - -export const getRefreshButtonIcon = () => { - // TODO: If acceleration can only be manually refreshed, return inputOutput - // If acceleration is automatically refreshed and paused, return play - // If acceleration is automatically refreshed and is refreshing, return pause - return 'inputOutput'; -}; - -export const onRefreshButtonClick = (acceleration: any) => { - // TODO: send request to refresh - console.log('refreshing', acceleration.name); -}; - -export const onDiscoverButtonClick = (acceleration: any) => { - // TODO: send user to Discover - console.log('sending user to discover for', acceleration.name); -}; - -export const onDeleteButtonClick = (acceleration: any) => { - // TODO: delete acceleration - console.log('deleting', acceleration.name); -}; diff --git a/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx b/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx new file mode 100644 index 0000000000..901279f76d --- /dev/null +++ b/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiHealth } from '@elastic/eui'; + +export const ACC_PANEL_TITLE = 'Accelerations'; +export const ACC_PANEL_DESC = + 'Accelerations optimize query performance by indexing external data into OpenSearch.'; +export const ACC_LOADING_MSG = 'Loading/Refreshing accelerations...'; + +export const AccelerationStatus = ({ status }: { status: string }) => { + const label = status; + let color; + + switch (status) { + case 'active': + color = 'success'; + break; + case 'refreshing': + color = 'warning'; + break; + case 'deleted': + color = 'danger'; + break; + default: + color = 'subdued'; + } + + return {label}; +}; + +export const AccelerationHealth = ({ health }: { health: string }) => { + let label = health; + let color; + + switch (health) { + case 'green': + label = 'Green'; + color = 'success'; + break; + case 'red': + label = 'Red'; + color = 'danger'; + break; + case 'yellow': + label = 'Yellow'; + color = 'warning'; + break; + default: + label = 'Invalid'; + color = 'danger'; + } + + return {label}; +}; + +export const getRefreshButtonIcon = () => { + // TODO: If acceleration can only be manually refreshed, return inputOutput + // If acceleration is automatically refreshed and paused, return play + // If acceleration is automatically refreshed and is refreshing, return pause + return 'inputOutput'; +}; + +export const onRefreshButtonClick = (acceleration: any) => { + // TODO: send request to refresh + console.log('refreshing', acceleration.name); +}; + +export const onDiscoverButtonClick = (acceleration: any) => { + // TODO: send user to Discover + console.log('sending user to discover for', acceleration.name); +}; + +export const onDeleteButtonClick = (acceleration: any) => { + // TODO: delete acceleration + console.log('deleting', acceleration.name); +}; diff --git a/public/components/datasources/components/manage/associated_objects/associated_objects_details_flyout.tsx b/public/components/datasources/components/manage/associated_objects/associated_objects_details_flyout.tsx index 86fb12d82b..ea2d4b776d 100644 --- a/public/components/datasources/components/manage/associated_objects/associated_objects_details_flyout.tsx +++ b/public/components/datasources/components/manage/associated_objects/associated_objects_details_flyout.tsx @@ -32,7 +32,7 @@ import { onDiscoverButtonClick, } from './utils/associated_objects_tab_utils'; import { getRenderAccelerationDetailsFlyout } from '../../../../../plugin'; -import { AccelerationStatus } from '../accelerations/helpers/utils'; +import { AccelerationStatus } from '../accelerations/utils/acceleration_utils'; import { ACCE_NO_DATA_TITLE, ACCE_NO_DATA_DESCRIPTION, diff --git a/public/components/datasources/components/manage/data_connection.tsx b/public/components/datasources/components/manage/data_connection.tsx index 2b8dc5895b..4fa2cd9f0a 100644 --- a/public/components/datasources/components/manage/data_connection.tsx +++ b/public/components/datasources/components/manage/data_connection.tsx @@ -67,19 +67,43 @@ export const DataConnection = (props: any) => { // Dummy accelerations variables for mock purposes // Actual accelerations should be retrieved from the backend - const sampleSql = 'select * from `httplogs`.`default`.`table2` limit 10'; - const dummyAccelerations = [ + // const sampleSql = 'select * from `httplogs`.`default`.`table2` limit 10'; + const _dummyAccelerations = [ { - name: 'dummy_acceleration_1', - status: 'ACTIVE', - type: 'skip', + flintIndexName: 'flint_mys3_default_http_logs_skipping_index', + kind: 'skipping', database: 'default', - table: 'table1', - destination: 'N/A', - dateCreated: 1709339290, - dateUpdated: 1709339290, - index: 'security_logs_2022', - sql: sampleSql, + table: 'test', + indexName: 'skipping_index', + autoRefresh: true, + status: 'Active', + }, + { + flintIndexName: 'flint_mys3_default_test_mycv_index', + kind: 'covering', + database: 'default', + table: 'test', + indexName: 'mycv', + autoRefresh: false, + status: 'Active', + }, + { + flintIndexName: 'flint_mys3_default_mymv', + kind: ' ', + database: 'default', + table: '', + indexName: 'mymv', + autoRefresh: true, + status: 'Active', + }, + { + flintIndexName: 'flint_mys3_default_sample_mv', + kind: 'mv', + database: 'default', + table: 'sample_table', + indexName: 'sample_mv', + autoRefresh: true, + status: 'Active', }, ]; @@ -179,7 +203,7 @@ export const DataConnection = (props: any) => { id: 'acceleration_table', name: 'Accelerations', disabled: false, - content: , + content: , }, // TODO: Installed integrations page { diff --git a/public/framework/core_refs.ts b/public/framework/core_refs.ts index 4849d85f1f..275158258a 100644 --- a/public/framework/core_refs.ts +++ b/public/framework/core_refs.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import DSLService from 'public/services/requests/dsl'; import { ApplicationStart, ChromeStart, @@ -21,6 +22,7 @@ class CoreRefs { public http?: HttpStart; public savedObjectsClient?: SavedObjectsClientContract; public pplService?: PPLService; + public dslService?: DSLService; public toasts?: IToasts; public chrome?: ChromeStart; public application?: ApplicationStart; diff --git a/public/plugin.tsx b/public/plugin.tsx index 8d4bfdea0e..36690d0674 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -356,11 +356,13 @@ export class ObservabilityPlugin public start(core: CoreStart, startDeps: AppPluginStartDependencies): ObservabilityStart { const pplService: PPLService = new PPLService(core.http); + const dslService = new DSLService(core.http); coreRefs.core = core; coreRefs.http = core.http; coreRefs.savedObjectsClient = core.savedObjects.client; coreRefs.pplService = pplService; + coreRefs.dslService = dslService; coreRefs.toasts = core.notifications.toasts; coreRefs.chrome = core.chrome; coreRefs.dataSources = startDeps.data.dataSources; diff --git a/public/services/requests/dsl.ts b/public/services/requests/dsl.ts index 5ce0ce0570..db0ba00c69 100644 --- a/public/services/requests/dsl.ts +++ b/public/services/requests/dsl.ts @@ -4,8 +4,15 @@ */ import { CoreStart } from '../../../../../src/core/public'; -import { DSL_BASE, DSL_SEARCH, DSL_CAT, DSL_MAPPING } from '../../../common/constants/shared'; +import { + DSL_BASE, + DSL_SEARCH, + DSL_CAT, + DSL_MAPPING, + DSL_SETTINGS, +} from '../../../common/constants/shared'; +/* eslint-disable import/no-default-export */ export default class DSLService { private http; constructor(http: CoreStart['http']) { @@ -19,23 +26,30 @@ export default class DSLService { .catch((error) => console.error(error)); }; - fetchIndices = async () => { + fetchIndices = async (index: string = '') => { return this.http .get(`${DSL_BASE}${DSL_CAT}`, { query: { format: 'json', + index, }, }) .catch((error) => console.error(error)); }; fetchFields = async (index: string) => { - return this.http - .get(`${DSL_BASE}${DSL_MAPPING}`, { - query: { - index, - }, - }) - .catch((error) => console.error(error)); + return this.http.get(`${DSL_BASE}${DSL_MAPPING}`, { + query: { + index, + }, + }); + }; + + fetchSettings = async (index: string) => { + return this.http.get(`${DSL_BASE}${DSL_SETTINGS}`, { + query: { + index, + }, + }); }; } diff --git a/server/routes/dsl.ts b/server/routes/dsl.ts index 04453c6275..580a55152f 100644 --- a/server/routes/dsl.ts +++ b/server/routes/dsl.ts @@ -7,9 +7,15 @@ import { schema } from '@osd/config-schema'; import { RequestParams } from '@elastic/elasticsearch'; import { IRouter } from '../../../../src/core/server'; import { DSLFacet } from '../services/facets/dsl_facet'; -import { DSL_BASE, DSL_SEARCH, DSL_CAT, DSL_MAPPING } from '../../common/constants/shared'; +import { + DSL_BASE, + DSL_SEARCH, + DSL_CAT, + DSL_MAPPING, + DSL_SETTINGS, +} from '../../common/constants/shared'; -export function registerDslRoute({ router, facet }: { router: IRouter; facet: DSLFacet }) { +export function registerDslRoute({ router }: { router: IRouter; facet: DSLFacet }) { router.post( { path: `${DSL_BASE}${DSL_SEARCH}`, @@ -46,6 +52,7 @@ export function registerDslRoute({ router, facet }: { router: IRouter; facet: DS validate: { query: schema.object({ format: schema.string(), + index: schema.string(), }), }, }, @@ -91,4 +98,28 @@ export function registerDslRoute({ router, facet }: { router: IRouter; facet: DS } } ); + + router.get( + { + path: `${DSL_BASE}${DSL_SETTINGS}`, + validate: { query: schema.any() }, + }, + async (context, request, response) => { + try { + const resp = await context.core.opensearch.legacy.client.callAsCurrentUser( + 'indices.getSettings', + { index: request.query.index } + ); + return response.ok({ + body: resp, + }); + } catch (error) { + if (error.statusCode !== 404) console.error(error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); } From 9a4e532d3f2fd9addf3f71cca1f01dafe8925d3e Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 14 Mar 2024 20:39:07 -0700 Subject: [PATCH 3/3] Update UI styles for query assist (#1523) * update ui for query assist Signed-off-by: Joshua Li * mock prohibited query when generating ppl Signed-off-by: Joshua Li * update tests Signed-off-by: Joshua Li * move guardrails check to server side Signed-off-by: Joshua Li * focus input when selecting from suggestion Signed-off-by: Joshua Li * update styles of run buttons Signed-off-by: Joshua Li * hide patterns table by default Signed-off-by: Joshua Li * show loading state for ppl generation Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li --- common/constants/query_assist.ts | 2 + .../components/common/search/query_area.scss | 8 + .../components/common/search/query_area.tsx | 101 +++--- public/components/common/search/search.tsx | 47 +-- .../explorer/log_patterns/log_patterns.tsx | 3 +- .../event_analytics/explorer/no_results.tsx | 21 +- .../query_assist/__tests__/input.test.tsx | 34 +- .../explorer/query_assist/input.tsx | 315 ++++++++++-------- .../query_assistant_summarization_slice.ts | 9 + server/routes/query_assist/routes.ts | 27 +- 10 files changed, 338 insertions(+), 229 deletions(-) create mode 100644 public/components/common/search/query_area.scss diff --git a/common/constants/query_assist.ts b/common/constants/query_assist.ts index e0b3bb29e6..8de10583d6 100644 --- a/common/constants/query_assist.ts +++ b/common/constants/query_assist.ts @@ -11,3 +11,5 @@ export const QUERY_ASSIST_API = { }; export const ML_COMMONS_API_PREFIX = '/_plugins/_ml'; + +export const ERROR_DETAILS = { GUARDRAILS_TRIGGERED: 'guardrail triggered' }; diff --git a/public/components/common/search/query_area.scss b/public/components/common/search/query_area.scss new file mode 100644 index 0000000000..3d5d31463e --- /dev/null +++ b/public/components/common/search/query_area.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.ppl-query-accordion { + padding: $euiSizeS; +} diff --git a/public/components/common/search/query_area.tsx b/public/components/common/search/query_area.tsx index a298e546ae..16d44e0cbd 100644 --- a/public/components/common/search/query_area.tsx +++ b/public/components/common/search/query_area.tsx @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiCodeEditor, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import React, { useEffect, useMemo } from 'react'; +import { EuiAccordion, EuiCodeEditor, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; import { coreRefs } from '../../../framework/core_refs'; import { QueryAssistInput } from '../../event_analytics/explorer/query_assist/input'; import { useFetchEvents } from '../../event_analytics/hooks/use_fetch_events'; +import './query_area.scss'; export function QueryArea({ tabId, @@ -17,7 +18,7 @@ export function QueryArea({ runQuery, tempQuery, setNeedsUpdate, - setFillRun, + runChanges, selectedIndex, nlqInput, setNlqInput, @@ -37,47 +38,63 @@ export function QueryArea({ memoizedHandleQueryChange(indexQuery); memoizedGetAvailableFields(indexQuery); }, [selectedIndex, memoizedGetAvailableFields, memoizedHandleQueryChange]); + const [lastFocusedInput, setLastFocusedInput] = useState<'query_area' | 'nlq_input'>('nlq_input'); + + const queryEditor = ( + { + handleQueryChange(query); + // query is considered updated when the last run query is not the same as whats in the editor + // setUpdatedQuery(runQuery !== query); + setNeedsUpdate(runQuery !== query); + }} + onFocus={() => setLastFocusedInput('query_area')} + value={tempQuery} + wrapEnabled={true} + /> + ); + + if (!coreRefs.queryAssistEnabled) { + return {queryEditor}; + } return ( - - - - { - handleQueryChange(query); - // query is considered updated when the last run query is not the same as whats in the editor - // setUpdatedQuery(runQuery !== query); - setNeedsUpdate(runQuery !== query); - }} - onFocus={() => setFillRun(true)} - onBlur={() => setFillRun(false)} - value={tempQuery} - wrapEnabled={true} - /> - - {coreRefs.queryAssistEnabled && ( - - - - )} - + + + <> + + + {queryEditor} + + + ); } diff --git a/public/components/common/search/search.tsx b/public/components/common/search/search.tsx index ec78d9f342..f434dae917 100644 --- a/public/components/common/search/search.tsx +++ b/public/components/common/search/search.tsx @@ -10,20 +10,20 @@ import { EuiButtonEmpty, EuiComboBox, EuiComboBoxOptionOption, + EuiContextMenuItem, EuiContextMenuPanel, EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, EuiPopover, EuiPopoverFooter, EuiText, EuiToolTip, - EuiContextMenuItem, - EuiModal, - EuiModalHeader, - EuiModalBody, - EuiModalHeaderTitle, - EuiModalFooter, } from '@elastic/eui'; import { isEqual } from 'lodash'; import React, { useEffect, useState } from 'react'; @@ -59,11 +59,11 @@ import { import { update as updateSearchMetaData } from '../../event_analytics/redux/slices/search_meta_data_slice'; import { PPLReferenceFlyout } from '../helpers'; import { LiveTailButton, StopLiveButton } from '../live_tail/live_tail_button'; +import { Autocomplete } from './autocomplete'; import { DatePicker } from './date_picker'; import { QueryArea } from './query_area'; -import './search.scss'; import { QueryAssistSummarization } from './query_assist_summarization'; -import { Autocomplete } from './autocomplete'; +import './search.scss'; export interface IQueryBarProps { query: string; @@ -136,7 +136,6 @@ export const Search = (props: any) => { const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [queryLang, setQueryLang] = useState(QUERY_LANGUAGE.PPL); const [needsUpdate, setNeedsUpdate] = useState(false); - const [fillRun, setFillRun] = useState(false); const sqlService = new SQLService(coreRefs.http); const { application } = coreRefs; const [nlqInput, setNlqInput] = useState(''); @@ -475,19 +474,21 @@ export const Search = (props: any) => { /> )}
- - - - {needsUpdate ? 'Update' : 'Run'} - - - + {!showQueryArea && ( + + + + {needsUpdate ? 'Update' : 'Run'} + + + + )} {!showQueryArea && showSaveButton && !showSavePanelOptionsList && ( { runQuery={query} tempQuery={tempQuery} setNeedsUpdate={setNeedsUpdate} - setFillRun={setFillRun} selectedIndex={selectedIndex} nlqInput={nlqInput} setNlqInput={setNlqInput} pplService={pplService} + runChanges={runChanges} /> {(queryAssistantSummarization?.summary?.length > 0 || diff --git a/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx b/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx index 2ed2d408fe..6a81597d68 100644 --- a/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx +++ b/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx @@ -10,7 +10,6 @@ import { FILTERED_PATTERN, PATTERN_REGEX, PPL_DEFAULT_PATTERN_REGEX_FILETER, - RAW_QUERY, } from '../../../../../common/constants/explorer'; import { PatternTableData, Query as IQuery } from '../../../../../common/types/explorer'; import { TabContext, useFetchPatterns } from '../../hooks'; @@ -39,7 +38,7 @@ const EventPatterns = ({ const dispatch = useDispatch(); const { tabId, pplService, notifications } = useContext(TabContext); const patternsData = patterns[tabId]; - const [viewLogPatterns, setViewLogPatterns] = useState(true); + const [viewLogPatterns, setViewLogPatterns] = useState(false); const [isPatternConfigPopoverOpen, setIsPatternConfigPopoverOpen] = useState(false); const [patternRegexInput, setPatternRegexInput] = useState(PPL_DEFAULT_PATTERN_REGEX_FILETER); const { isEventsLoading: isPatternLoading, getPatterns } = useFetchPatterns({ diff --git a/public/components/event_analytics/explorer/no_results.tsx b/public/components/event_analytics/explorer/no_results.tsx index c4ce3a9b09..272417f7f2 100644 --- a/public/components/event_analytics/explorer/no_results.tsx +++ b/public/components/event_analytics/explorer/no_results.tsx @@ -3,31 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { FormattedMessage } from '@osd/i18n/react'; import { EuiCallOut, + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, EuiPage, EuiSpacer, EuiText, - EuiEmptyPrompt, } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { coreRefs } from '../../../framework/core_refs'; +import { selectQueryAssistantSummarization } from '../redux/slices/query_assistant_summarization_slice'; import { selectQueries } from '../redux/slices/query_slice'; export const NoResults = ({ tabId }: any) => { // get the queries isLoaded, if it exists AND is true = show no res const queryInfo = useSelector(selectQueries)[tabId]; + const summaryData = useSelector(selectQueryAssistantSummarization)[tabId]; + const queryAssistLoading = summaryData.loading; return ( {coreRefs.queryAssistEnabled ? ( <> {/* check to see if the rawQuery is empty or not */} - {queryInfo?.rawQuery ? ( + {queryAssistLoading ? ( + } + body={

Loading results...

} + /> + ) : queryInfo?.rawQuery ? ( { title={

Get started

} body={

- Run a query to view results, or use the Query Assistant to automatically generate - complex queries using simple conversational prompts. + Run a query to view results, or use the Natural Language Query Generator to + automatically generate complex queries using simple conversational prompts.

} /> diff --git a/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx b/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx index 1a95a5e379..31eb1c872c 100644 --- a/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx +++ b/public/components/event_analytics/explorer/query_assist/__tests__/input.test.tsx @@ -8,7 +8,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import React, { ComponentProps } from 'react'; import { Provider } from 'react-redux'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { QUERY_ASSIST_API } from '../../../../../../common/constants/query_assist'; +import { ERROR_DETAILS, QUERY_ASSIST_API } from '../../../../../../common/constants/query_assist'; import * as coreServices from '../../../../../../common/utils/core_services'; import { coreRefs } from '../../../../../framework/core_refs'; import { rootReducer } from '../../../../../framework/redux/reducers'; @@ -29,6 +29,7 @@ const renderQueryAssistInput = ( selectedIndex: [{ label: 'selected-test-index' }], nlqInput: 'test-input', setNlqInput: jest.fn(), + handleTimePickerChange: jest.fn(), }, overrideProps ); @@ -59,7 +60,8 @@ describe(' spec', () => { const { component, props } = renderQueryAssistInput(); await waitFor(() => { - fireEvent.click(component.getByTestId('query-assist-generate-and-run-button')); + // splitbutton data-test-subj doesn't work in Oui 1.5, this should be query-assist-generate-and-run-button + fireEvent.click(component.getByText('Generate and run')); }); expect(httpMock.post).toBeCalledWith(QUERY_ASSIST_API.GENERATE_PPL, { @@ -73,6 +75,8 @@ describe(' spec', () => { const { component } = renderQueryAssistInput(); await waitFor(() => { + // splitbutton dropdown buttons don't support custom test id in Oui 1.5 + fireEvent.click(component.getByTestId('splitButton--dropdown')); fireEvent.click(component.getByTestId('query-assist-generate-button')); }); @@ -91,7 +95,8 @@ describe(' spec', () => { const { component } = renderQueryAssistInput(); await waitFor(() => { - fireEvent.click(component.getByTestId('query-assist-generate-and-run-button')); + // splitbutton data-test-subj doesn't work in Oui 1.5, this should be query-assist-generate-and-run-button + fireEvent.click(component.getByText('Generate and run')); }); expect(httpMock.post).toBeCalledWith(QUERY_ASSIST_API.GENERATE_PPL, { @@ -116,7 +121,8 @@ describe(' spec', () => { const { component } = renderQueryAssistInput(); await waitFor(() => { - fireEvent.click(component.getByTestId('query-assist-generate-and-run-button')); + // splitbutton data-test-subj doesn't work in Oui 1.5, this should be query-assist-generate-and-run-button + fireEvent.click(component.getByText('Generate and run')); }); expect(httpMock.post).toBeCalledWith(QUERY_ASSIST_API.GENERATE_PPL, { @@ -124,7 +130,25 @@ describe(' spec', () => { }); expect(httpMock.post).toBeCalledWith(QUERY_ASSIST_API.SUMMARIZE, { body: - '{"question":"test-input","index":"selected-test-index","isError":true,"query":"","response":"{\\"statusCode\\":429}"}', + '{"question":"test-input","index":"selected-test-index","isError":true,"query":"","response":"{\\"statusCode\\":429,\\"message\\":\\"Request is throttled. Try again later or contact your administrator\\"}"}', }); }); + + it('should display callout when response returned 400 with guardrails denied', async () => { + coreRefs.summarizeEnabled = true; + httpMock.post.mockRejectedValueOnce({ + body: { statusCode: 400, message: ERROR_DETAILS.GUARDRAILS_TRIGGERED }, + }); + + const { component } = renderQueryAssistInput(); + await waitFor(() => { + // splitbutton data-test-subj doesn't work in Oui 1.5, this should be query-assist-generate-and-run-button + fireEvent.click(component.getByText('Generate and run')); + }); + + expect(httpMock.post).toBeCalledWith(QUERY_ASSIST_API.GENERATE_PPL, { + body: '{"question":"test-input","index":"selected-test-index"}', + }); + expect(component.getByTestId('query-assist-guard-callout')).toBeInTheDocument(); + }); }); diff --git a/public/components/event_analytics/explorer/query_assist/input.tsx b/public/components/event_analytics/explorer/query_assist/input.tsx index fba8aa72e3..e6b18df585 100644 --- a/public/components/event_analytics/explorer/query_assist/input.tsx +++ b/public/components/event_analytics/explorer/query_assist/input.tsx @@ -4,26 +4,26 @@ */ import { - EuiBadge, EuiButton, + EuiButtonIcon, + EuiCallOut, EuiComboBoxOptionOption, EuiFieldText, EuiFlexGroup, EuiFlexItem, - EuiForm, EuiIcon, EuiInputPopover, - EuiLink, EuiListGroup, EuiListGroupItem, - EuiPanel, + EuiSpacer, + EuiSplitButton, EuiText, } from '@elastic/eui'; import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RAW_QUERY } from '../../../../../common/constants/explorer'; -import { QUERY_ASSIST_API } from '../../../../../common/constants/query_assist'; +import { ERROR_DETAILS, QUERY_ASSIST_API } from '../../../../../common/constants/query_assist'; import { QUERY_ASSIST_START_TIME } from '../../../../../common/constants/shared'; import { getOSDHttp } from '../../../../../common/utils'; import { coreRefs } from '../../../../framework/core_refs'; @@ -32,11 +32,18 @@ import { changeSummary, resetSummary, selectQueryAssistantSummarization, + setLoading, setResponseForSummaryStatus, } from '../../redux/slices/query_assistant_summarization_slice'; import { reset, selectQueryResult } from '../../redux/slices/query_result_slice'; import { changeQuery, selectQueries } from '../../redux/slices/query_slice'; +class ProhibitedQueryError extends Error { + constructor(message?: string) { + super(message); + } +} + interface SummarizationContext { question: string; query?: string; @@ -54,6 +61,9 @@ interface Props { selectedIndex: Array>; nlqInput: string; setNlqInput: React.Dispatch>; + lastFocusedInput: 'query_area' | 'nlq_input'; + setLastFocusedInput: React.Dispatch>; + runChanges: () => void; } const HARDCODED_SUGGESTIONS: Record = { @@ -79,13 +89,35 @@ const HARDCODED_SUGGESTIONS: Record = { ], }; -export const QueryAssistInput: React.FC = (props) => { +const prohibitedQueryCallOut = ( + +); + +const emptyQueryCallOut = ( + +); + +export const QueryAssistInput: React.FC> = (props) => { // @ts-ignore const queryRedux = useSelector(selectQueries)[props.tabId]; // @ts-ignore const explorerData = useSelector(selectQueryResult)[props.tabId]; // @ts-ignore const summaryData = useSelector(selectQueryAssistantSummarization)[props.tabId]; + const loading = summaryData.loading; + const inputRef = useRef(null); useEffect(() => { if ( @@ -108,15 +140,12 @@ export const QueryAssistInput: React.FC = (props) => { })(); }, [summaryData.responseForSummaryStatus]); - const [barSelected, setBarSelected] = useState(false); - const dispatch = useDispatch(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [generating, setGenerating] = useState(false); - const [generatingOrRunning, setGeneratingOrRunning] = useState(false); // below is only used for url redirection const [autoRun, setAutoRun] = useState(false); + const [callOut, setCallOut] = useState(null); useEffect(() => { if (autoRun) { @@ -149,13 +178,18 @@ export const QueryAssistInput: React.FC = (props) => { ); return generatedPPL; }; - const formatError = (error: ResponseError): Error => { - if (error.body) { + const formatError = (error: ResponseError | Error): Error => { + if ('body' in error) { if (error.body.statusCode === 429) return { ...error.body, message: 'Request is throttled. Try again later or contact your administrator', } as Error; + if ( + error.body.statusCode === 400 && + error.body.message === ERROR_DETAILS.GUARDRAILS_TRIGGERED + ) + return new ProhibitedQueryError(error.body.message); return error.body as Error; } return error; @@ -165,15 +199,23 @@ export const QueryAssistInput: React.FC = (props) => { dispatch(reset({ tabId: props.tabId })); dispatch(resetSummary({ tabId: props.tabId })); if (!props.selectedIndex.length) return; + if (props.nlqInput.trim().length === 0) { + setCallOut(emptyQueryCallOut); + return; + } try { - setGenerating(true); + dispatch(setLoading({ tabId: props.tabId, loading: true })); + setCallOut(null); await request(); - } catch (error) { - coreRefs.toasts?.addError(formatError(error as ResponseError), { - title: 'Failed to generate results', - }); + } catch (err) { + const error = formatError(err); + if (error instanceof ProhibitedQueryError) { + setCallOut(prohibitedQueryCallOut); + return; + } + coreRefs.toasts?.addError(error, { title: 'Failed to generate results' }); } finally { - setGenerating(false); + dispatch(setLoading({ tabId: props.tabId, loading: false })); } }; const generateSummary = async (context?: Partial) => { @@ -219,10 +261,13 @@ export const QueryAssistInput: React.FC = (props) => { }, }) ); - } catch (error) { - coreRefs.toasts?.addError(formatError(error as ResponseError), { - title: 'Failed to summarize results', - }); + } catch (err) { + const error = formatError(err); + if (error instanceof ProhibitedQueryError) { + setCallOut(prohibitedQueryCallOut); + return; + } + coreRefs.toasts?.addError(error, { title: 'Failed to summarize results' }); } finally { await dispatch( changeSummary({ @@ -245,137 +290,127 @@ export const QueryAssistInput: React.FC = (props) => { dispatch(reset({ tabId: props.tabId })); dispatch(resetSummary({ tabId: props.tabId })); if (!props.selectedIndex.length) return; + if (props.nlqInput.trim().length === 0) { + setCallOut(emptyQueryCallOut); + return; + } try { - setGeneratingOrRunning(true); + dispatch(setLoading({ tabId: props.tabId, loading: true })); + setCallOut(null); await request(); await props.handleTimePickerChange([QUERY_ASSIST_START_TIME, 'now']); await props.handleTimeRangePickerRefresh(undefined, true); - } catch (error) { + } catch (err) { + const error = formatError(err); + if (error instanceof ProhibitedQueryError) { + setCallOut(prohibitedQueryCallOut); + return; + } if (coreRefs.summarizeEnabled) { - generateSummary({ isError: true, response: JSON.stringify((error as ResponseError).body) }); + generateSummary({ isError: true, response: JSON.stringify(error) }); } else { - coreRefs.toasts?.addError(formatError(error as ResponseError), { - title: 'Failed to generate results', - }); + coreRefs.toasts?.addError(error, { title: 'Failed to generate results' }); } } finally { - setGeneratingOrRunning(false); + dispatch(setLoading({ tabId: props.tabId, loading: false })); } }; return ( <> - - { - e.preventDefault(); - request(); - }} + + + props.setNlqInput(e.target.value)} + onKeyDown={(e) => { + // listen to enter key manually. the cursor jumps to CodeEditor with EuiForm's onSubmit + if (e.key === 'Enter') runAndSummarize(); + }} + prepend={} + fullWidth + onFocus={() => { + props.setNeedsUpdate(false); + props.setLastFocusedInput('nlq_input'); + if (props.nlqInput.length === 0) setIsPopoverOpen(true); + }} + /> + } + disableFocusTrap + fullWidth={true} + isOpen={isPopoverOpen} + closePopover={() => { + setIsPopoverOpen(false); + }} + > + + {HARDCODED_SUGGESTIONS[props.selectedIndex[0]?.label]?.map((question) => ( + { + props.setNlqInput(question); + inputRef.current?.focus(); + setIsPopoverOpen(false); + }} + label={question} + /> + ))} + + + + + + + + {callOut} + {props.children && } + {props.children} + + {props.lastFocusedInput === 'query_area' ? ( + + Run + + ) : ( + Generate query + ), + onClick: generatePPL, + }, + ]} + onClick={runAndSummarize} > - - - - - - - - Query Assistant - - - New! - - - props.setNlqInput(e.target.value)} - fullWidth - onFocus={() => { - setBarSelected(true); - props.setNeedsUpdate(false); - if (props.nlqInput.length === 0) setIsPopoverOpen(true); - }} - onBlur={() => setBarSelected(false)} - /> - } - disableFocusTrap - fullWidth={true} - isOpen={isPopoverOpen} - closePopover={() => { - setIsPopoverOpen(false); - }} - > - - {HARDCODED_SUGGESTIONS[props.selectedIndex[0]?.label]?.map((question) => ( - { - props.setNlqInput(question); - setIsPopoverOpen(false); - }} - label={question} - /> - ))} - - - - - - - - - - - Share feedback via{' '} - - Forum - {' '} - or{' '} - - Slack - - - - - - - Generate query - - - - - Generate and run - - - - - - - + {loading ? 'Running...' : 'Generate and run'} + + )} ); }; diff --git a/public/components/event_analytics/redux/slices/query_assistant_summarization_slice.ts b/public/components/event_analytics/redux/slices/query_assistant_summarization_slice.ts index e25ebab022..286f87fdd0 100644 --- a/public/components/event_analytics/redux/slices/query_assistant_summarization_slice.ts +++ b/public/components/event_analytics/redux/slices/query_assistant_summarization_slice.ts @@ -8,6 +8,7 @@ import { initialTabId } from '../../../../framework/redux/store/shared_state'; const initialState = { [initialTabId]: { + loading: false, responseForSummaryStatus: 'false' as 'false' | 'success' | 'failure', }, }; @@ -30,9 +31,16 @@ export const summarizationSlice = createSlice({ }, resetSummary: (state, { payload }) => { state[payload.tabId] = { + loading: false, responseForSummaryStatus: initialState[initialTabId].responseForSummaryStatus, }; }, + setLoading: (state, { payload }) => { + state[payload.tabId] = { + ...state[payload.tabId], + loading: payload.loading, + }; + }, }, }); @@ -40,6 +48,7 @@ export const { setResponseForSummaryStatus, changeSummary, resetSummary, + setLoading, } = summarizationSlice.actions; export const selectQueryAssistantSummarization = createSelector( diff --git a/server/routes/query_assist/routes.ts b/server/routes/query_assist/routes.ts index ff3e2b5cdb..ae96cc6abf 100644 --- a/server/routes/query_assist/routes.ts +++ b/server/routes/query_assist/routes.ts @@ -10,9 +10,9 @@ import { ResponseError, } from '../../../../../src/core/server'; import { isResponseError } from '../../../../../src/core/server/opensearch/client/errors'; -import { QUERY_ASSIST_API } from '../../../common/constants/query_assist'; +import { ERROR_DETAILS, QUERY_ASSIST_API } from '../../../common/constants/query_assist'; import { generateFieldContext } from '../../common/helpers/query_assist/generate_field_context'; -import { requestWithRetryAgentSearch, getAgentIdByConfig } from './utils/agents'; +import { getAgentIdByConfig, requestWithRetryAgentSearch } from './utils/agents'; import { AGENT_CONFIGS } from './utils/constants'; export function registerQueryAssistRoutes(router: IRouter) { @@ -81,11 +81,12 @@ export function registerQueryAssistRoutes(router: IRouter) { .replace(/\bSPAN\(/g, 'span('); // https://github.com/opensearch-project/dashboards-observability/issues/759 return response.ok({ body: ppl }); } catch (error) { - // parse PPL query from error response if exists - // TODO remove after https://github.com/opensearch-project/skills/issues/138 - if (isResponseError(error) && error.body.error?.reason) { - const pplMatch = error.body.error.reason.match(/execute ppl:(.+), get error:/); - if (pplMatch[1]) return response.ok({ body: pplMatch[1] }); + if ( + isResponseError(error) && + error.statusCode === 400 && + error.body.error.details === ERROR_DETAILS.GUARDRAILS_TRIGGERED + ) { + return response.badRequest({ body: ERROR_DETAILS.GUARDRAILS_TRIGGERED }); } return response.custom({ statusCode: error.statusCode || 500, body: error.message }); } @@ -152,10 +153,14 @@ export function registerQueryAssistRoutes(router: IRouter) { }, }); } catch (error) { - return response.custom({ - statusCode: error.statusCode || 500, - body: error.message, - }); + if ( + isResponseError(error) && + error.statusCode === 400 && + error.body.error.details === ERROR_DETAILS.GUARDRAILS_TRIGGERED + ) { + return response.badRequest({ body: ERROR_DETAILS.GUARDRAILS_TRIGGERED }); + } + return response.custom({ statusCode: error.statusCode || 500, body: error.message }); } } );