diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.test.tsx new file mode 100644 index 0000000000000..40f821bc104ae --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ListUsageResults } from './list_usage_results'; +import { render, screen, fireEvent } from '@testing-library/react'; + +describe('ListUsageResults', () => { + const items = [ + { + label: 'index-1', + type: 'Index', + }, + { + label: 'pipeline-1', + type: 'Pipeline', + }, + ]; + beforeEach(() => { + render(); + }); + it('renders', () => { + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + expect(screen.getAllByTestId('usageItem')).toHaveLength(2); + + expect(screen.getByText('index-1')).toBeInTheDocument(); + expect(screen.getByText('Index')).toBeInTheDocument(); + expect(screen.getByText('pipeline-1')).toBeInTheDocument(); + expect(screen.getByText('Pipeline')).toBeInTheDocument(); + }); + + it('filters list based on search term', () => { + const searchBox = screen.getByRole('searchbox'); + fireEvent.change(searchBox, { target: { value: 'index' } }); + + expect(screen.getAllByTestId('usageItem')).toHaveLength(1); + expect(screen.getByText('index-1')).toBeInTheDocument(); + expect(screen.queryByText('pipeline-1')).not.toBeInTheDocument(); + }); + + it('empty list', () => { + const searchBox = screen.getByRole('searchbox'); + fireEvent.change(searchBox, { target: { value: 'coke' } }); + + expect(screen.queryAllByTestId('usageItem')).toHaveLength(0); + expect(screen.queryByText('index-1')).not.toBeInTheDocument(); + expect(screen.queryByText('pipeline-1')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx new file mode 100644 index 0000000000000..d20520345a8ba --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/list_usage_results.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { InferenceUsageInfo } from '../../../../types'; +import * as i18n from '../delete/confirm_delete_endpoint/translations'; +import { UsageItem } from './usage_item'; + +interface ListUsageResultsProps { + list: InferenceUsageInfo[]; +} + +export const ListUsageResults: React.FC = ({ list }) => { + const [term, setTerm] = useState(''); + + return ( + + + setTerm(e.target.value)} + isClearable={true} + aria-label={i18n.SEARCH_ARIA_LABEL} + fullWidth={true} + data-test-subj="usageFieldSearch" + /> + + + {list + .filter((item) => item.label.toLowerCase().includes(term.toLowerCase())) + .map((item, id) => ( + + ))} + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/render_message_with_icon.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/render_message_with_icon.tsx new file mode 100644 index 0000000000000..dc01662c22677 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/render_message_with_icon.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import React from 'react'; + +interface RenderMessageWithIconProps { + icon: string; + color: string; + label: string; + labelColor?: string; +} +export const RenderMessageWithIcon: React.FC = ({ + icon, + color, + label, + labelColor, +}) => ( + + + + + + + {label} + + + +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.test.tsx new file mode 100644 index 0000000000000..d2ec41680d249 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { ScanUsageResults } from './scan_usage_results'; +import { useKibana } from '../../../../../../hooks/use_kibana'; + +jest.mock('../../../../../../hooks/use_kibana'); +const mockUseKibana = useKibana as jest.Mock; +const mockNavigateToApp = jest.fn(); +const mockOnCheckboxChange = jest.fn(); + +describe('ScanUsageResults', () => { + const items = [ + { + label: 'index-1', + type: 'Index', + }, + { + label: 'pipeline-1', + type: 'Pipeline', + }, + ]; + beforeEach(() => { + mockUseKibana.mockReturnValue({ + services: { + application: { + navigateToApp: mockNavigateToApp, + }, + }, + }); + + render( + + ); + }); + + it('renders', () => { + expect(screen.getByText('Potential Failures')).toBeInTheDocument(); + expect(screen.getByText('Found 2 usages')).toBeInTheDocument(); + expect(screen.getByText('Open Index Management')).toBeInTheDocument(); + expect(screen.getAllByTestId('usageItem')).toHaveLength(2); + + const checkbox = screen.getByTestId('warningCheckbox'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toHaveProperty('checked', false); + }); + + it('opens index management in a new tab', () => { + fireEvent.click(screen.getByTestId('inferenceManagementOpenIndexManagement')); + expect(mockNavigateToApp).toHaveBeenCalledWith('enterprise_search', { + openInNewTab: true, + path: 'content/search_indices', + }); + }); + + it('onCheckboxChange gets called with correct params', () => { + fireEvent.click(screen.getByTestId('warningCheckbox')); + expect(mockOnCheckboxChange).toHaveBeenCalledWith(true); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx new file mode 100644 index 0000000000000..0f4aa09c12be4 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/scan_usage_results.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiText, + EuiButtonEmpty, +} from '@elastic/eui'; +import React from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; +import { InferenceUsageInfo } from '../../../../types'; +import { useKibana } from '../../../../../../hooks/use_kibana'; +import { RenderMessageWithIcon } from './render_message_with_icon'; + +import * as i18n from '../delete/confirm_delete_endpoint/translations'; +import { ListUsageResults } from './list_usage_results'; + +interface ScanUsageResultsProps { + list: InferenceUsageInfo[]; + ignoreWarningCheckbox: boolean; + onCheckboxChange: (state: boolean) => void; +} + +export const ScanUsageResults: React.FC = ({ + list, + ignoreWarningCheckbox, + onCheckboxChange, +}) => { + const { + services: { application }, + } = useKibana(); + const handleNavigateToIndex = () => { + application?.navigateToApp('enterprise_search', { + path: 'content/search_indices', + openInNewTab: true, + }); + }; + + return ( + + + + + + + + + + + + {i18n.COUNT_USAGE_LABEL(list.length)} + + + + + {i18n.OPEN_INDEX_MANAGEMENT} + + + + + + + + + + + + + + + + onCheckboxChange(e.target.checked)} + /> + + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.test.tsx new file mode 100644 index 0000000000000..7315de521a1c3 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { UsageItem } from './usage_item'; +import { InferenceUsageInfo } from '../../../../types'; +import { useKibana } from '../../../../../../hooks/use_kibana'; + +jest.mock('../../../../../../hooks/use_kibana'); +const mockUseKibana = useKibana as jest.Mock; +const mockNavigateToApp = jest.fn(); + +describe('UsageItem', () => { + beforeEach(() => { + mockUseKibana.mockReturnValue({ + services: { + application: { + navigateToApp: mockNavigateToApp, + }, + }, + }); + }); + + describe('index', () => { + const item: InferenceUsageInfo = { + label: 'index-1', + type: 'Index', + }; + + beforeEach(() => { + render(); + }); + + it('renders', () => { + expect(screen.getByText('index-1')).toBeInTheDocument(); + expect(screen.getByText('Index')).toBeInTheDocument(); + }); + + it('opens index in a new tab', () => { + fireEvent.click(screen.getByRole('button')); + expect(mockNavigateToApp).toHaveBeenCalledWith('enterprise_search', { + openInNewTab: true, + path: 'content/search_indices/index-1', + }); + }); + }); + + describe('pipeline', () => { + const item: InferenceUsageInfo = { + label: 'pipeline-1', + type: 'Pipeline', + }; + + beforeEach(() => { + render(); + }); + it('renders', () => { + expect(screen.getByText('pipeline-1')).toBeInTheDocument(); + expect(screen.getByText('Pipeline')).toBeInTheDocument(); + }); + + it('opens pipeline in a new tab', () => { + fireEvent.click(screen.getByRole('button')); + expect(mockNavigateToApp).toHaveBeenCalledWith('management', { + path: 'ingest/ingest_pipelines?pipeline=pipeline-1', + openInNewTab: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.tsx new file mode 100644 index 0000000000000..90bd050d67b81 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/component/usage_item.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiText, + EuiTextTruncate, + EuiIcon, +} from '@elastic/eui'; +import React from 'react'; + +import { useKibana } from '../../../../../../hooks/use_kibana'; +import { InferenceUsageInfo } from '../../../../types'; + +interface UsageProps { + usageItem: InferenceUsageInfo; +} +export const UsageItem: React.FC = ({ usageItem }) => { + const { + services: { application }, + } = useKibana(); + const handleNavigateToIndex = () => { + if (usageItem.type === 'Index') { + application?.navigateToApp('enterprise_search', { + path: `content/search_indices/${usageItem.label}`, + openInNewTab: true, + }); + } else if (usageItem.type === 'Pipeline') { + application?.navigateToApp('management', { + path: `ingest/ingest_pipelines?pipeline=${usageItem.label}`, + openInNewTab: true, + }); + } + }; + + return ( + + + + + + + + + + + + {usageItem.type} + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx index 8fe5c0e94b058..8525fd3f0f901 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx @@ -6,36 +6,128 @@ */ import { render, fireEvent, screen } from '@testing-library/react'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import React from 'react'; import { ConfirmDeleteEndpointModal } from '.'; import * as i18n from './translations'; +import { useScanUsage } from '../../../../../../../hooks/use_scan_usage'; + +jest.mock('../../../../../../../hooks/use_scan_usage'); +const mockUseScanUsage = useScanUsage as jest.Mock; describe('ConfirmDeleteEndpointModal', () => { const mockOnCancel = jest.fn(); const mockOnConfirm = jest.fn(); + const mockProvider = { + inference_id: 'my-hugging-face', + service: 'hugging_face', + service_settings: { + api_key: 'aaaa', + url: 'https://dummy.huggingface.com', + }, + task_settings: {}, + } as any; + + const mockItem = { + endpoint: 'my-hugging-face', + provider: mockProvider, + type: 'text_embedding', + }; + + const Wrapper = () => { + const queryClient = new QueryClient(); + return ( + + + + ); + }; + beforeEach(() => { - render(); + mockUseScanUsage.mockReturnValue({ + data: { + indexes: ['index-1', 'index2'], + pipelines: ['pipeline-1'], + }, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); }); it('renders the modal with correct elements', () => { + render(); + expect(screen.getByText(i18n.DELETE_TITLE)).toBeInTheDocument(); expect(screen.getByText(i18n.CONFIRM_DELETE_WARNING)).toBeInTheDocument(); expect(screen.getByText(i18n.CANCEL)).toBeInTheDocument(); expect(screen.getByText(i18n.DELETE_ACTION_LABEL)).toBeInTheDocument(); + expect(screen.getByText('my-hugging-face')).toBeInTheDocument(); }); it('calls onCancel when the cancel button is clicked', () => { + render(); + fireEvent.click(screen.getByText(i18n.CANCEL)); expect(mockOnCancel).toHaveBeenCalled(); }); - it('calls onConfirm when the delete button is clicked', () => { - fireEvent.click(screen.getByText(i18n.DELETE_ACTION_LABEL)); - expect(mockOnConfirm).toHaveBeenCalled(); + it('useScanUsage gets called with correct params', () => { + render(); + + expect(mockUseScanUsage).toHaveBeenCalledWith({ + type: 'text_embedding', + id: 'my-hugging-face', + }); }); - it('has the delete button focused by default', () => { - expect(document.activeElement).toHaveTextContent(i18n.DELETE_ACTION_LABEL); + describe('endpoint with usage', () => { + it('disables delete endpoint button', () => { + render(); + expect(screen.getByTestId('confirmModalConfirmButton')).toBeDisabled(); + }); + + it('renders warning message', () => { + render(); + expect(screen.getByText(i18n.POTENTIAL_FAILURE_LABEL)).toBeInTheDocument(); + }); + + it('selecting checkbox enables Delete Endpoint button', () => { + render(); + fireEvent.click(screen.getByTestId('warningCheckbox')); + + expect(screen.getByTestId('confirmModalConfirmButton')).toBeEnabled(); + }); + }); + + describe('endpoint without usage', () => { + beforeEach(() => { + mockUseScanUsage.mockReturnValue({ + data: { + indexes: [], + pipelines: [], + }, + }); + + render(); + }); + it('renders no usage message', () => { + expect(screen.getByText(i18n.NO_USAGE_FOUND_LABEL)).toBeInTheDocument(); + }); + + it('enables delete endpoint button', () => { + expect(screen.getByTestId('confirmModalConfirmButton')).toBeEnabled(); + }); + + it('calls onConfirm when the delete button is clicked', () => { + fireEvent.click(screen.getByText(i18n.DELETE_ACTION_LABEL)); + expect(mockOnConfirm).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx index e650192a66dc7..965f512b32d7d 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx @@ -5,19 +5,63 @@ * 2.0. */ -import React from 'react'; -import { EuiConfirmModal } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { EuiButtonEmpty, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; + import * as i18n from './translations'; +import { useScanUsage } from '../../../../../../../hooks/use_scan_usage'; +import { InferenceEndpointUI, InferenceUsageInfo } from '../../../../../types'; +import { RenderMessageWithIcon } from '../../component/render_message_with_icon'; +import { ScanUsageResults } from '../../component/scan_usage_results'; interface ConfirmDeleteEndpointModalProps { onCancel: () => void; onConfirm: () => void; + inferenceEndpoint: InferenceEndpointUI; } export const ConfirmDeleteEndpointModal: React.FC = ({ onCancel, onConfirm, + inferenceEndpoint, }) => { + const [isFetching, setIsFetching] = useState(true); + const [listOfUsages, setListOfUsages] = useState([]); + const [deleteDisabled, setDeleteDisabled] = useState(true); + const [ignoreWarningCheckbox, setIgnoreWarningCheckbox] = useState(false); + + const { data } = useScanUsage({ + type: inferenceEndpoint.type, + id: inferenceEndpoint.endpoint, + }); + + const onCheckboxChange = (state: boolean) => { + setIgnoreWarningCheckbox(state); + if (state) { + setDeleteDisabled(false); + } else { + setDeleteDisabled(true); + } + }; + + useEffect(() => { + if (!data) return; + setIsFetching(false); + + const indices = data.indexes.map((index, id) => ({ label: index, type: 'Index' })); + const pipelines = data.pipelines.map((pipeline, id) => ({ label: pipeline, type: 'Pipeline' })); + const usages: InferenceUsageInfo[] = [...indices, ...pipelines]; + if (usages.length > 0) { + setDeleteDisabled(true); + } else { + setDeleteDisabled(false); + } + + setListOfUsages(usages); + }, [data]); + return ( - {i18n.CONFIRM_DELETE_WARNING} + + {i18n.CONFIRM_DELETE_WARNING} + + + {inferenceEndpoint.endpoint} + + + + {isFetching ? ( + {}} + isLoading + > + {i18n.SCANNING_USAGE_LABEL}… + + ) : listOfUsages.length === 0 ? ( + + ) : ( + + )} + + ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts index 4e306afcc3ac8..d606e6f3c1b0e 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts @@ -19,7 +19,7 @@ export const CONFIRM_DELETE_WARNING = i18n.translate( 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.confirmQuestion', { defaultMessage: - 'Deleting an active endpoint will cause operations targeting associated semantic_text fields and inference pipelines to fail.', + 'Deleting an inference endpoint currently in use will cause failures in the ingest and query attempts.', } ); @@ -29,3 +29,58 @@ export const DELETE_ACTION_LABEL = i18n.translate( defaultMessage: 'Delete endpoint', } ); + +export const SCANNING_USAGE_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.scanningMessage', + { + defaultMessage: 'Scanning for usage', + } +); + +export const NO_USAGE_FOUND_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.noUsageFound', + { + defaultMessage: 'No Usage Found', + } +); + +export const POTENTIAL_FAILURE_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.potentialFailure', + { + defaultMessage: 'Potential Failures', + } +); + +export const IGNORE_POTENTIAL_ERRORS_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.ignoreErrors', + { + defaultMessage: 'Ignore potential errors and force deletion', + } +); + +export const COUNT_USAGE_LABEL = (count: number) => + i18n.translate('xpack.searchInferenceEndpoints.confirmDeleteEndpoint.countUsage', { + defaultMessage: 'Found {count} {count, plural, =1 {usage} other {usages}}', + values: { count }, + }); + +export const SEARCH_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.searchLabel', + { + defaultMessage: 'Search', + } +); + +export const SEARCH_ARIA_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.searchARIALabel', + { + defaultMessage: 'Search indices and pipelines', + } +); + +export const OPEN_INDEX_MANAGEMENT = i18n.translate( + 'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.openIndexManagement', + { + defaultMessage: 'Open Index Management', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.tsx index caedf4e913387..f932a3d25ed21 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.tsx @@ -13,7 +13,7 @@ import { InferenceEndpointUI } from '../../../../types'; import { ConfirmDeleteEndpointModal } from './confirm_delete_endpoint'; interface DeleteActionProps { - selectedEndpoint?: InferenceEndpointUI; + selectedEndpoint: InferenceEndpointUI; } export const DeleteAction: React.FC = ({ selectedEndpoint }) => { @@ -37,7 +37,7 @@ export const DeleteAction: React.FC = ({ selectedEndpoint }) = ({ selectedEndpoint }) setIsModalVisible(false)} onConfirm={onConfirmDeletion} + inferenceEndpoint={selectedEndpoint} /> )} > diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx index 373cfb676c36e..88a18a25b5a79 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx @@ -61,7 +61,7 @@ export const TabularPage: React.FC = ({ inferenceEndpoints }) }, sortable: true, truncateText: true, - width: '400px', + width: '300px', }, { field: 'provider', @@ -74,7 +74,7 @@ export const TabularPage: React.FC = ({ inferenceEndpoints }) return null; }, sortable: false, - width: '592px', + width: '285px', }, { field: 'type', @@ -87,7 +87,7 @@ export const TabularPage: React.FC = ({ inferenceEndpoints }) return null; }, sortable: false, - width: '185px', + width: '100px', }, { actions: [ diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts index 6fb4cb0bcca6b..c1f23a3a4f2e3 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts @@ -61,3 +61,8 @@ export interface InferenceEndpointUI { provider: InferenceAPIConfigResponse; type: string; } + +export interface InferenceUsageInfo { + label: string; + type: string; +} diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx new file mode 100644 index 0000000000000..5f1766b7a6fa4 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; + +import { useScanUsage } from './use_scan_usage'; +import { useKibana } from './use_kibana'; + +jest.mock('./use_kibana'); + +const mockUseKibana = useKibana as jest.Mock; +const mockDelete = jest.fn().mockResolvedValue({ + acknowledge: true, + error_message: 'inference id is being used', + indexes: ['index1', 'index2'], + pipelines: ['pipeline1', 'pipeline2'], +}); +const queryClient = new QueryClient(); + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('useScanUsage', () => { + beforeEach(() => { + mockUseKibana.mockReturnValue({ + services: { + http: { + delete: mockDelete, + }, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should call API endpoint with the correct parameters and return response', async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useScanUsage({ + type: 'text_embedding', + id: 'in-1', + }), + { wrapper } + ); + + await waitForNextUpdate(); + + expect(mockDelete).toHaveBeenCalledWith( + '/internal/inference_endpoint/endpoints/text_embedding/in-1', + { query: { scanUsage: true } } + ); + + expect(result.current.data).toEqual({ + acknowledge: true, + error_message: 'inference id is being used', + indexes: ['index1', 'index2'], + pipelines: ['pipeline1', 'pipeline2'], + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.tsx new file mode 100644 index 0000000000000..2afbd53b76afd --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from './use_kibana'; +import { InferenceUsageResponse } from '../types'; + +interface ScanUsageProps { + type: string; + id: string; +} + +export const useScanUsage = ({ type, id }: ScanUsageProps) => { + const { services } = useKibana(); + + return useQuery({ + queryKey: ['inference-endpoint-scan-usage'], + queryFn: () => + services.http.delete( + `/internal/inference_endpoint/endpoints/${type}/${id}`, + { + query: { + scanUsage: true, + }, + } + ), + }); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/types.ts b/x-pack/plugins/search_inference_endpoints/public/types.ts index 6ac241fbe87b0..4bd83521cf8d6 100644 --- a/x-pack/plugins/search_inference_endpoints/public/types.ts +++ b/x-pack/plugins/search_inference_endpoints/public/types.ts @@ -34,3 +34,10 @@ export interface AppServicesContext { ml?: MlPluginStart; console?: ConsolePluginStart; } + +export interface InferenceUsageResponse { + acknowledge: boolean; + error_message: string; + indexes: string[]; + pipelines: string[]; +} diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts index 15ea49825f096..86fdec622a112 100644 --- a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts +++ b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts @@ -27,6 +27,21 @@ describe('deleteInferenceEndpoint', () => { expect(mockClient.inference.delete).toHaveBeenCalledWith({ inference_id: id, task_type: type, + force: true, + }); + }); + + it('should call the Inference Delete API to return list of usages', async () => { + const type = 'rerank'; + const id = 'model-id-123'; + const scanUsage = true; + + await deleteInferenceEndpoint(mockClient, type, id, scanUsage); + + expect(mockClient.inference.delete).toHaveBeenCalledWith({ + inference_id: id, + task_type: type, + dry_run: true, }); }); }); diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts index 561a1f4f157ab..c6ea0946493cf 100644 --- a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts +++ b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts @@ -16,9 +16,13 @@ function isTaskType(type?: string): type is InferenceTaskType { export const deleteInferenceEndpoint = async ( client: ElasticsearchClient, type: string, - id: string + id: string, + scanUsage?: boolean ) => { if (isTaskType(type)) { - return await client.inference.delete({ inference_id: id, task_type: type }); + if (scanUsage) { + return await client.inference.delete({ inference_id: id, task_type: type, dry_run: true }); + } + return await client.inference.delete({ inference_id: id, task_type: type, force: true }); } }; diff --git a/x-pack/plugins/search_inference_endpoints/server/routes.ts b/x-pack/plugins/search_inference_endpoints/server/routes.ts index b8f41a1422137..80d7a15ab99c4 100644 --- a/x-pack/plugins/search_inference_endpoints/server/routes.ts +++ b/x-pack/plugins/search_inference_endpoints/server/routes.ts @@ -43,6 +43,9 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout type: schema.string(), id: schema.string(), }), + query: schema.object({ + scanUsage: schema.maybe(schema.boolean()), + }), }, }, errorHandler(logger)(async (context, request, response) => { @@ -51,7 +54,8 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout } = (await context.core).elasticsearch; const { type, id } = request.params; - const result = await deleteInferenceEndpoint(asCurrentUser, type, id); + const { scanUsage } = request.query; + const result = await deleteInferenceEndpoint(asCurrentUser, type, id, scanUsage ?? false); return response.ok({ body: result }); }) diff --git a/x-pack/plugins/search_inference_endpoints/tsconfig.json b/x-pack/plugins/search_inference_endpoints/tsconfig.json index 5c7965b2d8b89..5b4a66e37d2f5 100644 --- a/x-pack/plugins/search_inference_endpoints/tsconfig.json +++ b/x-pack/plugins/search_inference_endpoints/tsconfig.json @@ -30,7 +30,8 @@ "@kbn/console-plugin", "@kbn/test-jest-helpers", "@kbn/kibana-utils-plugin", - "@kbn/features-plugin" + "@kbn/features-plugin", + "@kbn/ui-theme" ], "exclude": [ "target/**/*",
{i18n.COUNT_USAGE_LABEL(list.length)}