From 88d11181abf37d5412858e6e73e621e6cc31a4be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Mar 2024 23:37:22 +0000 Subject: [PATCH] [Feature] Acceleration Actions Implementation (#1540) * Refactor the icon clicks and introduce vacuum to flyout inside icon Signed-off-by: Ryan Liang * Add vacuum icon for acceleration table Signed-off-by: Ryan Liang * Add DELETE ui flow for acceleration table only Signed-off-by: Ryan Liang * Add VACUUM flow to acceleration table only Signed-off-by: Ryan Liang * Handle skip index naming in overlay Signed-off-by: Ryan Liang * Refactor the overlay logic a bit in acceleration table class Signed-off-by: Ryan Liang * Use util function for creating displayIndexName in overlay Signed-off-by: Ryan Liang * Add delete flow for acceleration table 0 Signed-off-by: Ryan Liang * Bond refresh logic when delete was successful Signed-off-by: Ryan Liang * Add vacuum flow for acceleration table Signed-off-by: Ryan Liang * Rename the class into acceleration operations Signed-off-by: Ryan Liang * Switch the acc flyout icon into broom Signed-off-by: Ryan Liang * add vacuum on acc flyout 0 Signed-off-by: Ryan Liang * Change to closing overlay immediately after clicking confirm Signed-off-by: Ryan Liang * Add flyout reset and fix the confirm behavior Signed-off-by: Ryan Liang * add vacuum on acc flyout final Signed-off-by: Ryan Liang * Add close flyout after the succeed status check Signed-off-by: Ryan Liang * Add teh refresh in acc flyout and trigger after delete/vacuum Signed-off-by: Ryan Liang * remove comment Signed-off-by: Ryan Liang * Mini refactor on load status in operation class Signed-off-by: Ryan Liang * Fix the mv flow Signed-off-by: Ryan Liang * Remove the sql definition tab Signed-off-by: Ryan Liang * Correct the visualization of show refresh interval in flyout Signed-off-by: Ryan Liang * Update the show time logic with refresh + time zone localization Signed-off-by: Ryan Liang * Define the behavior of refresh icon in both table and flyout Signed-off-by: Ryan Liang * Update teh acc table test to fix the build Signed-off-by: Ryan Liang * Update utils to consume the sync action Signed-off-by: Ryan Liang * Add sync flow to table behavior but with fail status Signed-off-by: Ryan Liang * Add sync flow to detail flyout but with fail status Signed-off-by: Ryan Liang * Add the restriction for only sync active acceleration Signed-off-by: Ryan Liang * Fix the navigate to datasource link Signed-off-by: Ryan Liang * Implement the single toast control for each status Signed-off-by: Ryan Liang * Fix keep pulling after switch rendering Signed-off-by: Ryan Liang * Fix types Signed-off-by: Ryan Liang * Remove the sql definition class Signed-off-by: Ryan Liang * Add basic tests for acceleration overlay Signed-off-by: Ryan Liang * Add basic tests for acceleration operation Signed-off-by: Ryan Liang * Remove console log Signed-off-by: Ryan Liang * Remove refresh icon in utils Signed-off-by: Ryan Liang * Remove the final status check for sync action Signed-off-by: Ryan Liang * remove unnecessary check Signed-off-by: Ryan Liang * remove stable datasource from dependencies array Signed-off-by: Ryan Liang * Resolve conflicts Signed-off-by: Ryan Liang * Fix lint Signed-off-by: Ryan Liang * Fix type in types Signed-off-by: Ryan Liang * Finalize the name for skipping index Signed-off-by: Ryan Liang * Refactor testing constants Signed-off-by: Ryan Liang * Upadate the class prop to remove unused index name Signed-off-by: Ryan Liang --------- Signed-off-by: Ryan Liang (cherry picked from commit 376fde407653ed2db9b6c31a410ebef71a1cb9f7) Signed-off-by: github-actions[bot] --- .../acceleration_action_overlay.test.tsx | 71 +++++++++ .../__tests__/acceleration_operation.test.tsx | 52 +++++++ .../__tests__/acceleration_table.test.tsx | 6 +- .../associated_objects_flyout.test.tsx | 2 +- .../acceleration_action_overlay.tsx | 93 ++++++++++++ .../acceleration_details_flyout.tsx | 119 +++++++++++---- .../accelerations/acceleration_operation.tsx | 99 ++++++++++++ .../accelerations/acceleration_table.tsx | 143 ++++++++++++------ .../acceleration_details_tab.tsx | 22 ++- .../flyout_modules/acceleration_sql_tab.tsx | 26 ---- .../utils/acceleration_utils.tsx | 82 +++++++--- .../associated_objects_details_flyout.tsx | 8 +- .../associated_objects_tab.tsx | 2 +- .../modules/associated_objects_table.tsx | 6 +- public/plugin.tsx | 8 +- public/types.ts | 3 +- test/datasources.ts | 31 ++++ 17 files changed, 626 insertions(+), 147 deletions(-) create mode 100644 public/components/datasources/components/__tests__/acceleration_action_overlay.test.tsx create mode 100644 public/components/datasources/components/__tests__/acceleration_operation.test.tsx create mode 100644 public/components/datasources/components/manage/accelerations/acceleration_action_overlay.tsx create mode 100644 public/components/datasources/components/manage/accelerations/acceleration_operation.tsx delete mode 100644 public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_sql_tab.tsx diff --git a/public/components/datasources/components/__tests__/acceleration_action_overlay.test.tsx b/public/components/datasources/components/__tests__/acceleration_action_overlay.test.tsx new file mode 100644 index 0000000000..e72d6c94ad --- /dev/null +++ b/public/components/datasources/components/__tests__/acceleration_action_overlay.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 { EuiOverlayMask, EuiConfirmModal, EuiFieldText } from '@elastic/eui'; +import { + AccelerationActionOverlay, + AccelerationActionOverlayProps, +} from '../manage/accelerations/acceleration_action_overlay'; +import { skippingIndexAcceleration } from '../../../../../test/datasources'; +import { act } from 'react-dom/test-utils'; + +configure({ adapter: new Adapter() }); + +describe('AccelerationActionOverlay Component Tests', () => { + let props: AccelerationActionOverlayProps; + + beforeEach(() => { + props = { + isVisible: true, + actionType: 'delete', + acceleration: skippingIndexAcceleration, + dataSourceName: 'test-datasource', + onCancel: jest.fn(), + onConfirm: jest.fn(), + }; + }); + + it('renders correctly', () => { + const wrapper = mount(); + expect(wrapper.find(EuiOverlayMask).exists()).toBe(true); + expect(wrapper.find(EuiConfirmModal).exists()).toBe(true); + expect(wrapper.text()).toContain('Delete acceleration'); + }); + + it('calls onConfirm when confirm button is clicked and confirm is enabled', async () => { + const wrapper = mount(); + + if (props.actionType === 'vacuum') { + await act(async () => { + const onChange = wrapper.find(EuiFieldText).first().prop('onChange'); + if (typeof onChange === 'function') { + onChange({ + target: { value: props.acceleration!.indexName }, + } as any); + } + }); + wrapper.update(); + } + wrapper + .find('button') + .filterWhere((button) => button.text().includes('Delete')) + .simulate('click'); + expect(props.onConfirm).toHaveBeenCalled(); + }); + + it('calls onCancel when cancel button is clicked', () => { + const wrapper = mount(); + + wrapper + .find('button') + .filterWhere((button) => button.text() === 'Cancel') + .simulate('click'); + + expect(props.onCancel).toHaveBeenCalled(); + }); +}); diff --git a/public/components/datasources/components/__tests__/acceleration_operation.test.tsx b/public/components/datasources/components/__tests__/acceleration_operation.test.tsx new file mode 100644 index 0000000000..8b22c08d38 --- /dev/null +++ b/public/components/datasources/components/__tests__/acceleration_operation.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useAccelerationOperation } from '../manage/accelerations/acceleration_operation'; +import * as useDirectQueryModule from '../../../../framework/datasources/direct_query_hook'; +import * as useToastModule from '../../../common/toast'; +import { DirectQueryLoadingStatus } from '../../../../../common/types/explorer'; +import { skippingIndexAcceleration } from '../../../../../test/datasources'; + +jest.mock('../../../../framework/datasources/direct_query_hook', () => ({ + useDirectQuery: jest.fn(), +})); + +jest.mock('../../../common/toast', () => ({ + useToast: jest.fn(), +})); + +describe('useAccelerationOperation', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useDirectQueryModule.useDirectQuery as jest.Mock).mockReturnValue({ + startLoading: jest.fn(), + stopLoading: jest.fn(), + loadStatus: DirectQueryLoadingStatus.INITIAL, + }); + + (useToastModule.useToast as jest.Mock).mockReturnValue({ + setToast: jest.fn(), + }); + }); + + it('performs acceleration operation and handles success', async () => { + (useDirectQueryModule.useDirectQuery as jest.Mock).mockReturnValue({ + startLoading: jest.fn(), + stopLoading: jest.fn(), + loadStatus: DirectQueryLoadingStatus.SUCCESS, + }); + + const { result } = renderHook(() => useAccelerationOperation('test-datasource')); + + act(() => { + result.current.performOperation(skippingIndexAcceleration, 'delete'); + }); + + expect((useDirectQueryModule.useDirectQuery as jest.Mock).mock.calls.length).toBeGreaterThan(0); + expect((useToastModule.useToast as jest.Mock).mock.calls.length).toBeGreaterThan(0); + }); +}); diff --git a/public/components/datasources/components/__tests__/acceleration_table.test.tsx b/public/components/datasources/components/__tests__/acceleration_table.test.tsx index 8c8ef6eabc..c149e7722a 100644 --- a/public/components/datasources/components/__tests__/acceleration_table.test.tsx +++ b/public/components/datasources/components/__tests__/acceleration_table.test.tsx @@ -177,6 +177,10 @@ describe('AccelerationTable Component', () => { }); wrapper!.update(); - expect(wrapper!.text()).toContain(accelerationCache.lastUpdated); + const expectedLocalizedTime = accelerationCache.lastUpdated + ? new Date(accelerationCache.lastUpdated).toLocaleString() + : ''; + + expect(wrapper!.text()).toContain(expectedLocalizedTime); }); }); diff --git a/public/components/datasources/components/__tests__/associated_objects_flyout.test.tsx b/public/components/datasources/components/__tests__/associated_objects_flyout.test.tsx index a561452557..4e9abae123 100644 --- a/public/components/datasources/components/__tests__/associated_objects_flyout.test.tsx +++ b/public/components/datasources/components/__tests__/associated_objects_flyout.test.tsx @@ -82,7 +82,7 @@ describe('AssociatedObjectsDetailsFlyout Integration Tests', () => { wrapper.update(); }); - const accName = getAccelerationName(mockTableDetail.accelerations[0], 'flint_s3'); + const accName = getAccelerationName(mockTableDetail.accelerations[0]); const accLink = wrapper .find('EuiLink') .findWhere((node) => node.text() === accName) diff --git a/public/components/datasources/components/manage/accelerations/acceleration_action_overlay.tsx b/public/components/datasources/components/manage/accelerations/acceleration_action_overlay.tsx new file mode 100644 index 0000000000..ce5cff4367 --- /dev/null +++ b/public/components/datasources/components/manage/accelerations/acceleration_action_overlay.tsx @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiOverlayMask, EuiConfirmModal, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { CachedAcceleration } from '../../../../../../common/types/data_connections'; +import { + ACC_DELETE_MSG, + ACC_VACUUM_MSG, + ACC_SYNC_MSG, + AccelerationActionType, + getAccelerationName, + getAccelerationFullPath, +} from './utils/acceleration_utils'; + +export interface AccelerationActionOverlayProps { + isVisible: boolean; + actionType: AccelerationActionType; + acceleration: CachedAcceleration | null; + dataSourceName: string; + onCancel: () => void; + onConfirm: () => void; +} + +export const AccelerationActionOverlay: React.FC = ({ + isVisible, + actionType, + acceleration, + dataSourceName, + onCancel, + onConfirm, +}) => { + const [confirmationInput, setConfirmationInput] = useState(''); + + if (!isVisible || !acceleration) { + return null; + } + + const displayIndexName = getAccelerationName(acceleration); + const displayFullPath = getAccelerationFullPath(acceleration, dataSourceName); + + let title = ''; + let description = ''; + let confirmButtonText = 'Confirm'; + let confirmEnabled = true; + + switch (actionType) { + case 'vacuum': + title = `Vacuum acceleration ${displayIndexName} on ${displayFullPath}?`; + description = ACC_VACUUM_MSG; + confirmButtonText = 'Vacuum'; + confirmEnabled = confirmationInput === displayIndexName; + break; + case 'delete': + title = `Delete acceleration ${displayIndexName} on ${displayFullPath}?`; + description = ACC_DELETE_MSG; + confirmButtonText = 'Delete'; + break; + case 'sync': + title = 'Manual sync data?'; + description = ACC_SYNC_MSG; + confirmButtonText = 'Sync'; + break; + } + + return ( + + onConfirm()} + cancelButtonText="Cancel" + confirmButtonText={confirmButtonText} + buttonColor="danger" + defaultFocusedButton="confirm" + confirmButtonDisabled={!confirmEnabled} + > +

{description}

+ {actionType === 'vacuum' && ( + + setConfirmationInput(e.target.value)} + /> + + )} +
+
+ ); +}; 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 108af38b91..76294a209a 100644 --- a/public/components/datasources/components/manage/accelerations/acceleration_details_flyout.tsx +++ b/public/components/datasources/components/manage/accelerations/acceleration_details_flyout.tsx @@ -18,22 +18,22 @@ import { 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'; import { - getRefreshButtonIcon, - onRefreshButtonClick, - onDiscoverButtonClick, - onDeleteButtonClick, + onDiscoverIconClick, + AccelerationActionType, + getAccelerationName, } from './utils/acceleration_utils'; import { coreRefs } from '../../../../../framework/core_refs'; import { OpenSearchDashboardsResponse } from '../../../../../../../../src/core/server/http/router'; import { CachedAcceleration } from '../../../../../../common/types/data_connections'; +import { useAccelerationOperation } from './acceleration_operation'; +import { AccelerationActionOverlay } from './acceleration_action_overlay'; export interface AccelerationDetailsFlyoutProps { - index: string; acceleration: CachedAcceleration; dataSourceName: string; resetFlyout: () => void; + handleRefresh?: () => void; } const getMappings = (index: string): Promise | undefined => { @@ -58,15 +58,42 @@ const handleDetailsFetchingPromise = ( }; export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) => { - const { index, dataSourceName, acceleration, resetFlyout } = props; - console.log(index, acceleration, dataSourceName); + const { dataSourceName, acceleration, resetFlyout, handleRefresh } = props; const { flintIndexName } = acceleration; const [selectedTab, setSelectedTab] = useState('details'); const tabsMap: { [key: string]: any } = { details: AccelerationDetailsTab, schema: AccelerationSchemaTab, - sql_definition: AccelerationSqlTab, }; + const [operationType, setOperationType] = useState(null); + const [showConfirmationOverlay, setShowConfirmationOverlay] = useState(false); + + const { performOperation, operationSuccess } = useAccelerationOperation(props.dataSourceName); + + const displayedIndex = getAccelerationName(acceleration); + + const onConfirmOperation = () => { + if (operationType && props.acceleration) { + performOperation(props.acceleration, operationType); + setShowConfirmationOverlay(false); + } + }; + + const onSyncIconClickHandler = () => { + setOperationType('sync'); + setShowConfirmationOverlay(true); + }; + + const onDeleteIconClickHandler = () => { + setOperationType('delete'); + setShowConfirmationOverlay(true); + }; + + const onVacuumIconClickHandler = () => { + setOperationType('vacuum'); + setShowConfirmationOverlay(true); + }; + const [settings, setSettings] = useState(); const [mappings, setMappings] = useState(); const [indexInfo, setIndexInfo] = useState(); @@ -101,18 +128,26 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) }); }; + useEffect(() => { + if (operationSuccess) { + resetFlyout(); + handleRefresh?.(); + setOperationType(null); + setShowConfirmationOverlay(false); + } + }, [operationSuccess, resetFlyout, handleRefresh]); + useEffect(() => { if (flintIndexName !== undefined && flintIndexName.trim().length > 0) { getAccDetail(flintIndexName); } }, [flintIndexName]); - const DiscoverButton = () => { - // TODO: display button if can be sent to discover + const DiscoverIcon = () => { return ( { - onDiscoverButtonClick(acceleration, dataSourceName); + onDiscoverIconClick(acceleration, dataSourceName); resetFlyout(); }} > @@ -121,22 +156,33 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) ); }; - const RefreshButton = () => { + const SyncIcon = ({ autoRefresh, status }: { autoRefresh: boolean; status: string }) => { + if (autoRefresh || status !== 'active') { + return null; + } return ( - - + + ); }; - const DeleteButton = () => { + const DeleteIcon = () => { return ( - + ); }; + const VacuumIcon = () => { + return ( + + + + ); + }; + const accelerationDetailsTabs = [ { id: 'details', @@ -148,11 +194,6 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) name: 'Schema', disabled: false, }, - { - id: 'sql_definition', - name: 'SQL Definition', - disabled: false, - }, ]; const renderTabs = () => { @@ -175,16 +216,12 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) switch (tab) { case 'details': - propsForTab = { acceleration, settings, mappings, indexInfo, dataSourceName }; + propsForTab = { acceleration, settings, mappings, indexInfo, dataSourceName, resetFlyout }; break; case 'schema': propsForTab = { mappings, indexInfo }; break; - case 'sql_definition': - propsForTab = { mappings }; - break; default: - console.log('Unknown Tab: ', tab); return null; } @@ -199,23 +236,39 @@ export const AccelerationDetailsFlyout = (props: AccelerationDetailsFlyoutProps) -

{index}

+

{displayedIndex}

- - - - + - + + {acceleration.status !== 'deleted' ? ( + + + + ) : ( + + + + )}
{renderTabs()} {renderTabContent(selectedTab)} + {showConfirmationOverlay && operationType && ( + setShowConfirmationOverlay(false)} + onConfirm={onConfirmOperation} + /> + )} ); }; diff --git a/public/components/datasources/components/manage/accelerations/acceleration_operation.tsx b/public/components/datasources/components/manage/accelerations/acceleration_operation.tsx new file mode 100644 index 0000000000..36acdecb29 --- /dev/null +++ b/public/components/datasources/components/manage/accelerations/acceleration_operation.tsx @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; +import { CachedAcceleration } from '../../../../../../common/types/data_connections'; +import { useToast } from '../../../../common/toast'; +import { useDirectQuery } from '../../../../../framework/datasources/direct_query_hook'; +import { DirectQueryLoadingStatus } from '../../../../../../common/types/explorer'; +import { + AccelerationActionType, + generateAccelerationOperationQuery, + getAccelerationName, +} from './utils/acceleration_utils'; + +export const useAccelerationOperation = (dataSource: string) => { + const { startLoading, stopLoading, loadStatus } = useDirectQuery(); + const { setToast } = useToast(); + const [isOperating, setIsOperating] = useState(false); + const [operationSuccess, setOperationSuccess] = useState(false); + const [accelerationToOperate, setAccelerationToOperate] = useState( + null + ); + const [operationType, setOperationType] = useState(null); + const [currentStatus, setCurrentStatus] = useState(null); + + useEffect(() => { + if (!accelerationToOperate || !operationType || loadStatus === currentStatus) return; + + const displayAccelerationName = getAccelerationName(accelerationToOperate); + + let operationInProgressMessage = ''; + let operationSuccessMessage = ''; + let operationFailureMessage = ''; + + switch (operationType) { + case 'delete': + operationInProgressMessage = `Deleting acceleration: ${displayAccelerationName}`; + operationSuccessMessage = `Successfully deleted acceleration: ${displayAccelerationName}`; + operationFailureMessage = `Failed to delete acceleration: ${displayAccelerationName}`; + break; + case 'vacuum': + operationInProgressMessage = `Vacuuming acceleration: ${displayAccelerationName}`; + operationSuccessMessage = `Successfully vacuumed acceleration: ${displayAccelerationName}`; + operationFailureMessage = `Failed to vacuum acceleration: ${displayAccelerationName}`; + break; + case 'sync': + operationInProgressMessage = `Syncing acceleration: ${displayAccelerationName}`; + break; + } + + if (loadStatus === DirectQueryLoadingStatus.SCHEDULED && operationType !== 'sync') { + setIsOperating(true); + setToast(operationInProgressMessage, 'success'); + } else if (loadStatus === DirectQueryLoadingStatus.SUCCESS && operationType !== 'sync') { + setIsOperating(false); + setAccelerationToOperate(null); + setOperationSuccess(true); + setToast(operationSuccessMessage, 'success'); + } else if (loadStatus === DirectQueryLoadingStatus.FAILED && operationType !== 'sync') { + setIsOperating(false); + setOperationSuccess(false); + setToast(operationFailureMessage, 'danger'); + } else if (operationType === 'sync' && loadStatus === DirectQueryLoadingStatus.SCHEDULED) { + setToast(operationInProgressMessage, 'success'); + stopLoading(); + } + + setCurrentStatus(loadStatus); + }, [loadStatus, setToast, accelerationToOperate, operationType, currentStatus]); + + useEffect(() => { + return () => { + stopLoading(); + }; + }, []); + + const performOperation = ( + acceleration: CachedAcceleration, + operation: AccelerationActionType + ) => { + setOperationSuccess(false); + setOperationType(operation); + const operationQuery = generateAccelerationOperationQuery(acceleration, dataSource, operation); + + const requestPayload = { + lang: 'sql', + query: operationQuery, + datasource: dataSource, + }; + + setIsOperating(true); + setAccelerationToOperate(acceleration); + startLoading(requestPayload); + }; + + return { performOperation, isOperating, operationSuccess }; +}; diff --git a/public/components/datasources/components/manage/accelerations/acceleration_table.tsx b/public/components/datasources/components/manage/accelerations/acceleration_table.tsx index e471570584..cb7a301460 100644 --- a/public/components/datasources/components/manage/accelerations/acceleration_table.tsx +++ b/public/components/datasources/components/manage/accelerations/acceleration_table.tsx @@ -17,42 +17,45 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + onDiscoverIconClick, + AccelerationStatus, + ACC_LOADING_MSG, + ACC_PANEL_TITLE, + ACC_PANEL_DESC, + getAccelerationName, + AccelerationActionType, +} from './utils/acceleration_utils'; +import { getRenderAccelerationDetailsFlyout } from '../../../../../plugin'; import { CatalogCacheManager } from '../../../../../framework/catalog_cache/cache_manager'; import { CachedAcceleration, CachedDataSourceStatus, } from '../../../../../../common/types/data_connections'; import { DirectQueryLoadingStatus } from '../../../../../../common/types/explorer'; +import { AccelerationActionOverlay } from './acceleration_action_overlay'; import { isCatalogCacheFetching } from '../associated_objects/utils/associated_objects_tab_utils'; -import { - getRenderAccelerationDetailsFlyout, - getRenderCreateAccelerationFlyout, -} from '../../../../../plugin'; -import { - ACC_LOADING_MSG, - ACC_PANEL_DESC, - ACC_PANEL_TITLE, - AccelerationStatus, - getAccelerationName, - getRefreshButtonIcon, - onDeleteButtonClick, - onDiscoverButtonClick, - onRefreshButtonClick, -} from './utils/acceleration_utils'; +import { getRenderCreateAccelerationFlyout } from '../../../../../plugin'; +import { useAccelerationOperation } from './acceleration_operation'; interface AccelerationTableProps { dataSourceName: string; cacheLoadingHooks: any; } +interface ModalState { + actionType: AccelerationActionType | null; + selectedItem: CachedAcceleration | null; +} + export const AccelerationTable = ({ dataSourceName, cacheLoadingHooks, }: AccelerationTableProps) => { const [accelerations, setAccelerations] = useState([]); const [updatedTime, setUpdatedTime] = useState(); - + const { performOperation, operationSuccess } = useAccelerationOperation(dataSourceName); const { databasesLoadStatus, tablesLoadStatus, @@ -60,6 +63,40 @@ export const AccelerationTable = ({ startLoadingAccelerations, } = cacheLoadingHooks; const [isRefreshing, setIsRefreshing] = useState(false); + const [modalState, setModalState] = useState({ + actionType: null, + selectedItem: null, + }); + + useEffect(() => { + if (operationSuccess) { + handleRefresh(); + } + }, [operationSuccess]); + + const handleActionClick = ( + actionType: ModalState['actionType'], + acceleration: CachedAcceleration + ) => { + setModalState({ + actionType, + selectedItem: acceleration, + }); + }; + + const handleModalClose = () => { + setModalState({ + actionType: null, + selectedItem: null, + }); + }; + + const handleConfirm = () => { + if (!modalState.selectedItem || !modalState.actionType) return; + + performOperation(modalState.selectedItem, modalState.actionType); + handleModalClose(); + }; useEffect(() => { const cachedDataSource = CatalogCacheManager.getOrCreateAccelerationsByDataSource( @@ -69,14 +106,9 @@ export const AccelerationTable = ({ cachedDataSource.status === CachedDataSourceStatus.Empty && !isCatalogCacheFetching(accelerationsLoadStatus) ) { - console.log( - `Cache for dataSource ${dataSourceName} is empty or outdated. Loading accelerations...` - ); setIsRefreshing(true); startLoadingAccelerations(dataSourceName); } else { - console.log(`Using cached accelerations for dataSource: ${dataSourceName}`); - setAccelerations(cachedDataSource.accelerations); setUpdatedTime(cachedDataSource.lastUpdated); } @@ -90,21 +122,18 @@ export const AccelerationTable = ({ setAccelerations(cachedDataSource.accelerations); setUpdatedTime(cachedDataSource.lastUpdated); setIsRefreshing(false); - console.log('Refresh process is success.'); } if (accelerationsLoadStatus === DirectQueryLoadingStatus.FAILED) { setIsRefreshing(false); - console.log('Refresh process is failed.'); } }, [accelerationsLoadStatus]); - const handleRefresh = () => { - console.log('Initiating refresh...'); + const handleRefresh = useCallback(() => { if (!isCatalogCacheFetching(accelerationsLoadStatus)) { setIsRefreshing(true); startLoadingAccelerations(dataSourceName); } - }; + }, [accelerationsLoadStatus]); const RefreshButton = () => { return ( @@ -130,7 +159,8 @@ export const AccelerationTable = ({ ); }; - console.log('HERE IS THE UPDATED TIME', updatedTime); + const localUpdatedTime = updatedTime ? new Date(updatedTime).toLocaleString() : ''; + const AccelerationTableHeader = () => { return ( <> @@ -149,14 +179,16 @@ export const AccelerationTable = ({ - - - {'Last updated'} - - - {updatedTime} - - + {updatedTime && ( + + + {'Last updated'} + + + {localUpdatedTime} + + + )} @@ -181,21 +213,29 @@ export const AccelerationTable = ({ icon: 'discoverApp', type: 'icon', onClick: (acc: CachedAcceleration) => { - onDiscoverButtonClick(acc, dataSourceName); + onDiscoverIconClick(acc, dataSourceName); }, }, { - name: 'Refresh', - description: 'Refresh/Pause/Resume', - icon: getRefreshButtonIcon, - onClick: onRefreshButtonClick, + name: 'Sync', + description: 'Manual Sync Data', + icon: 'inputOutput', + onClick: (item: CachedAcceleration) => handleActionClick('sync', item), + enabled: (item: CachedAcceleration) => !item.autoRefresh && item.status === 'active', }, { name: 'Delete', description: 'Delete acceleration', icon: 'trash', - type: 'icon', - onClick: onDeleteButtonClick, + onClick: (item: CachedAcceleration) => handleActionClick('delete', item), + enabled: (item: CachedAcceleration) => item.status !== 'deleted', + }, + { + name: 'Vacuum', + description: 'Vacuum acceleration', + icon: 'broom', + onClick: (item: CachedAcceleration) => handleActionClick('vacuum', item), + enabled: (item: CachedAcceleration) => item.status === 'deleted', }, ]; @@ -205,12 +245,11 @@ export const AccelerationTable = ({ name: 'Name', sortable: true, render: (indexName: string, acceleration: CachedAcceleration) => { - const displayName = getAccelerationName(acceleration, dataSourceName); + const displayName = getAccelerationName(acceleration); return ( { - console.log(displayName); - renderAccelerationDetailsFlyout(displayName, acceleration, dataSourceName); + renderAccelerationDetailsFlyout(acceleration, dataSourceName, handleRefresh); }} > {displayName} @@ -316,6 +355,18 @@ export const AccelerationTable = ({ /> )} + {(modalState.actionType === 'delete' || + modalState.actionType === 'vacuum' || + modalState.actionType === 'sync') && ( + + )} ); }; 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 a6818f9882..fd78bc0a88 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 @@ -15,6 +15,8 @@ import { EuiTitle, } from '@elastic/eui'; import { AccelerationHealth, AccelerationStatus } from '../utils/acceleration_utils'; +import { coreRefs } from '../../../../../../framework/core_refs'; +import { observabilityDataConnectionsID } from '../../../../../../../common/constants/shared'; interface AccelerationDetailsTabProps { acceleration: { @@ -30,6 +32,7 @@ interface AccelerationDetailsTabProps { mappings: object; indexInfo: any; dataSourceName: string; + resetFlyout: () => void; } export const AccelerationDetailsTab = ({ @@ -38,6 +41,7 @@ export const AccelerationDetailsTab = ({ mappings, indexInfo, dataSourceName, + resetFlyout, }: AccelerationDetailsTabProps) => { const isSkippingIndex = mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.kind === 'skipping'; @@ -46,14 +50,14 @@ export const AccelerationDetailsTab = ({ acceleration.autoRefresh || mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.options.incremental_refresh; const refreshTime = showRefreshTime - ? mappings?.data?.[acceleration.flintIndexName]?.mappings?._meta?.options.refresh_interval + ? 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 = ({ title, description, @@ -97,7 +101,19 @@ export const AccelerationDetailsTab = ({ console.log()}>{dataSourceName}} + description={ + { + coreRefs?.application!.navigateToApp(observabilityDataConnectionsID, { + path: `#/manage/${dataSourceName}`, + replace: true, + }); + resetFlyout(); + }} + > + {dataSourceName} + + } /> diff --git a/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_sql_tab.tsx b/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_sql_tab.tsx deleted file mode 100644 index 4898ab20e0..0000000000 --- a/public/components/datasources/components/manage/accelerations/flyout_modules/acceleration_sql_tab.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import React from 'react'; - -interface AccelerationSqlTabProps { - mappings: any; -} - -export const AccelerationSqlTab = (props: AccelerationSqlTabProps) => { - const { mappings } = props; - // TODO: Retrieve SQL query from backend - console.log(mappings); - - return ( - <> - - - Placeholder - - - ); -}; diff --git a/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx b/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx index 145dc889be..2c206a531b 100644 --- a/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx +++ b/public/components/datasources/components/manage/accelerations/utils/acceleration_utils.tsx @@ -16,12 +16,65 @@ 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 ACC_DELETE_MSG = + 'The acceleration will be deleted. User will no longer be able to view from this acceleration. By default data will be retained in the associated index.'; +export const ACC_VACUUM_MSG = + 'Vacuuming will remove the actual data from the disk since the associated index will be removed from the cluster. To confirm your action, type the name of the acceleration below.'; +export const ACC_SYNC_MSG = 'Syncing data may require querying all data. Do you want to continue?'; -export const getAccelerationName = (acceleration: CachedAcceleration, datasource: string) => { - return ( - acceleration.indexName || - `${datasource}_${acceleration.database}_${acceleration.table}`.replace(/\s+/g, '_') - ); +export type AccelerationActionType = 'delete' | 'vacuum' | 'sync'; + +export const getAccelerationName = (acceleration: CachedAcceleration) => { + return acceleration.indexName || 'skipping_index'; +}; + +export const getAccelerationFullPath = (acceleration: CachedAcceleration, dataSource: string) => { + switch (acceleration.type) { + case 'skipping': + return `${dataSource}.${acceleration.database}.${acceleration.table}`; + case 'materialized': + return `${dataSource}.${acceleration.database}`; + case 'covering': + return `${dataSource}.${acceleration.database}.${acceleration.table}`; + default: + return 'Unknown acceleration type'; + } +}; + +export const generateAccelerationOperationQuery = ( + acceleration: CachedAcceleration, + dataSource: string, + operationType: AccelerationActionType +): string => { + let operationQuery; + + switch (operationType) { + case 'delete': + operationQuery = `DROP`; + break; + case 'vacuum': + operationQuery = `VACUUM`; + break; + case 'sync': + operationQuery = `REFRESH`; + break; + default: + throw new Error(`Unsupported operation type: ${operationType}`); + } + + switch (acceleration.type) { + case 'skipping': + return `${operationQuery} SKIPPING INDEX ON ${dataSource}.${acceleration.database}.${acceleration.table}`; + case 'covering': + if (!acceleration.indexName) { + throw new Error("Index name is required for 'covering' acceleration type."); + } + return `${operationQuery} INDEX ${acceleration.indexName} ON ${dataSource}.${acceleration.database}.${acceleration.table}`; + case 'materialized': + return `${operationQuery} MATERIALIZED VIEW ${dataSource}.${acceleration.database}.${acceleration.indexName}`; + default: + throw new Error(`Unsupported acceleration type: ${acceleration.type}`); + } }; export const AccelerationStatus = ({ status }: { status: string }) => { @@ -70,19 +123,7 @@ export const AccelerationHealth = ({ health }: { health: string }) => { 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: CachedAcceleration, dataSourceName: string) => { +export const onDiscoverIconClick = (acceleration: CachedAcceleration, dataSourceName: string) => { // boolean determining whether its a skipping index table or mv/ci if (acceleration.type === undefined) return; if (acceleration.type === 'skipping') { @@ -96,8 +137,3 @@ export const onDiscoverButtonClick = (acceleration: CachedAcceleration, dataSour redirectToExplorerOSIdx(acceleration.flintIndexName); } }; - -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 73ea3b0278..b234e551af 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 @@ -53,12 +53,14 @@ export interface AssociatedObjectsFlyoutProps { tableDetail: AssociatedObject; datasourceName: string; resetFlyout: () => void; + handleRefresh?: () => void; } export const AssociatedObjectsDetailsFlyout = ({ tableDetail, datasourceName, resetFlyout, + handleRefresh, }: AssociatedObjectsFlyoutProps) => { const { loadStatus, startLoading } = useLoadTableColumnsToCache(); const [tableColumns, setTableColumns] = useState([]); @@ -145,11 +147,7 @@ export const AssociatedObjectsDetailsFlyout = ({ return ( - renderAccelerationDetailsFlyout({ - index: name, - acceleration: item, - dataSourceName: datasourceName, - }) + renderAccelerationDetailsFlyout(name, item, datasourceName, handleRefresh) } > {name} diff --git a/public/components/datasources/components/manage/associated_objects/associated_objects_tab.tsx b/public/components/datasources/components/manage/associated_objects/associated_objects_tab.tsx index 168821c706..c625a367b4 100644 --- a/public/components/datasources/components/manage/associated_objects/associated_objects_tab.tsx +++ b/public/components/datasources/components/manage/associated_objects/associated_objects_tab.tsx @@ -276,7 +276,7 @@ export const AssociatedObjectsTab: React.FC = (props) .map((acceleration: CachedAcceleration) => ({ datasource: datasource.name, id: acceleration.indexName, - name: getAccelerationName(acceleration, datasource.name), + name: getAccelerationName(acceleration), database: acceleration.database, type: ACCELERATION_INDEX_TYPES.find((accelType) => accelType.value === acceleration.type)! .value, diff --git a/public/components/datasources/components/manage/associated_objects/modules/associated_objects_table.tsx b/public/components/datasources/components/manage/associated_objects/modules/associated_objects_table.tsx index 87585a5978..bba26f237c 100644 --- a/public/components/datasources/components/manage/associated_objects/modules/associated_objects_table.tsx +++ b/public/components/datasources/components/manage/associated_objects/modules/associated_objects_table.tsx @@ -75,7 +75,7 @@ export const AssociatedObjectsTable = (props: AssociatedObjectsTableProps) => { const acceleration = cachedAccelerations.find((acc) => acc.indexName === item.id); if (acceleration) { renderAccelerationDetailsFlyout( - getAccelerationName(acceleration, datasourceName), + getAccelerationName(acceleration), acceleration, datasourceName ); @@ -115,7 +115,7 @@ export const AssociatedObjectsTable = (props: AssociatedObjectsTableProps) => { if (accelerations.length === 0) { return '-'; } else if (accelerations.length === 1) { - const name = getAccelerationName(accelerations[0], datasourceName); + const name = getAccelerationName(accelerations[0]); return ( { @@ -159,7 +159,7 @@ export const AssociatedObjectsTable = (props: AssociatedObjectsTableProps) => { if (asscObj.type === 'covering' || asscObj.type === 'materialized') { // find the flint index name through the cached acceleration const acceleration = cachedAccelerations.find( - (acc) => getAccelerationName(acc.indexName, acc, datasourceName) === asscObj.name + (acc) => getAccelerationName(acc) === asscObj.name ); redirectToExplorerOSIdx(acceleration!.flintIndexName); } else if (asscObj.type === 'table') { diff --git a/public/plugin.tsx b/public/plugin.tsx index 851fcdae10..2b8a64f327 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -101,7 +101,7 @@ export const [ getRenderAccelerationDetailsFlyout, setRenderAccelerationDetailsFlyout, ] = createGetterSetter< - (index: string, acceleration: CachedAcceleration, dataSourceName: string) => void + (acceleration: CachedAcceleration, dataSourceName: string, handleRefresh?: () => void) => void >('renderAccelerationDetailsFlyout'); export const [ @@ -394,17 +394,17 @@ export class ObservabilityPlugin // Use overlay service to render flyouts const renderAccelerationDetailsFlyout = ( - index: string, acceleration: CachedAcceleration, - dataSourceName: string + dataSourceName: string, + handleRefresh?: () => void ) => { const accelerationDetailsFlyout = core.overlays.openFlyout( toMountPoint( accelerationDetailsFlyout.close()} + handleRefresh={handleRefresh} /> ) ); diff --git a/public/types.ts b/public/types.ts index b82a7ede4f..fac6d463d7 100644 --- a/public/types.ts +++ b/public/types.ts @@ -38,7 +38,8 @@ export interface ObservabilityStart { renderAccelerationDetailsFlyout: ( index: string, acceleration: CachedAcceleration, - datasourceName: string + datasourceName: string, + handleRefresh?: () => void ) => void; renderAssociatedObjectsDetailsFlyout: ( tableDetail: AssociatedObject, diff --git a/test/datasources.ts b/test/datasources.ts index 8c37eed562..7cb6d3b277 100644 --- a/test/datasources.ts +++ b/test/datasources.ts @@ -7,6 +7,7 @@ import { AccelerationsCacheData, AssociatedObject, AsyncPollingResult, + CachedAcceleration, CachedDataSourceStatus, DataSourceCacheData, DatasourceDetails, @@ -1199,3 +1200,33 @@ export const mockAssociatedObjects: AssociatedObject[] = [ ], }, ]; + +export const skippingIndexAcceleration = { + flintIndexName: 'flint_mys3_default_http_logs_skipping_index', + type: 'skipping', + database: 'default', + table: 'http_logs', + indexName: '', + autoRefresh: false, + status: 'active', +} as CachedAcceleration; + +export const materializedViewAcceleration = { + flintIndexName: 'flint_mys3_default_http_count_view', + type: 'materialized', + database: 'default', + table: '', + indexName: 'http_count_view', + autoRefresh: false, + status: 'active', +} as CachedAcceleration; + +export const coveringIndexAcceleration = { + flintIndexName: 'flint_mys3_default_http_logs_status_clientip_and_day_index', + type: 'covering', + database: 'default', + table: 'http_logs', + indexName: 'status_clientip_and_day', + autoRefresh: true, + status: 'refreshing', +} as CachedAcceleration;