diff --git a/common/constants/shared.ts b/common/constants/shared.ts index 1d9bc9b62..3da8eaccd 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -11,6 +11,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 a1fa01383..ecd0abef4 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 000000000..b3c422514 --- /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 000000000..5531d030b --- /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 89ea00700..9ec84e1ea 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 aeda0d7ea..65b5b507d 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 078d7c74a..a6818f988 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 7498ad2e0..9c11fd726 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 b3f992a94..000000000 --- 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 000000000..901279f76 --- /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 86fb12d82..ea2d4b776 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 2b8dc5895..4fa2cd9f0 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 4849d85f1..275158258 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 8d4bfdea0..36690d067 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 5ce0ce057..db0ba00c6 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 04453c627..580a55152 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, + }); + } + } + ); }