Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Search: Inference Management UI] Adding restriction on deleting preconfigured endpoints #196580

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AllInferenceEndpointsTable
};

export const PIPELINE_URL = 'ingest/ingest_pipelines';

export const PRECONFIGURED_ENDPOINTS = {
ELSER: '.elser-2',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These endpoints are renamed in elastic/elasticsearch#115395

E5: '.multi-e5-small',
};
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const ConfirmDeleteEndpointModal: React.FC<ConfirmDeleteEndpointModalProp
onConfirm={onConfirm}
title={i18n.DELETE_TITLE}
confirmButtonDisabled={deleteDisabled}
data-test-subj="deleteModalForInferenceUI"
>
<EuiFlexGroup gutterSize="l" direction="column">
<EuiFlexItem grow={false}>{i18n.CONFIRM_DELETE_WARNING}</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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, screen, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';

import { DeleteAction } from './delete_action';
import { InferenceEndpointUI } from '../../../../types';

describe('Delete Action', () => {
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: InferenceEndpointUI = {
endpoint: 'my-hugging-face',
provider: mockProvider,
type: 'text_embedding',
};

const Wrapper = ({ item }: { item: InferenceEndpointUI }) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<DeleteAction selectedEndpoint={item} />
</QueryClientProvider>
);
};
it('renders', () => {
render(<Wrapper item={mockItem} />);

expect(screen.getByTestId('inferenceUIDeleteAction')).toBeEnabled();
});

it('disable the delete action for preconfigured endpoint', () => {
const preconfiguredMockItem = { ...mockItem, endpoint: '.elser-2' };
render(<Wrapper item={preconfiguredMockItem} />);

expect(screen.getByTestId('inferenceUIDeleteAction')).toBeDisabled();
});

it('loads confirm delete modal', () => {
render(<Wrapper item={mockItem} />);

fireEvent.click(screen.getByTestId('inferenceUIDeleteAction'));
expect(screen.getByTestId('deleteModalForInferenceUI')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { isEndpointPreconfigured } from '../../../../../../utils/preconfigured_endpoint_helper';
import { useDeleteEndpoint } from '../../../../../../hooks/use_delete_endpoint';
import { InferenceEndpointUI } from '../../../../types';
import { ConfirmDeleteEndpointModal } from './confirm_delete_endpoint';
Expand Down Expand Up @@ -39,6 +40,8 @@ export const DeleteAction: React.FC<DeleteActionProps> = ({ selectedEndpoint })
defaultMessage: 'Delete inference endpoint {selectedEndpointName}',
values: { selectedEndpointName: selectedEndpoint.endpoint },
})}
data-test-subj="inferenceUIDeleteAction"
disabled={isEndpointPreconfigured(selectedEndpoint.endpoint)}
key="delete"
iconType="trash"
color="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@
* 2.0.
*/

import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { isEndpointPreconfigured } from '../../../../utils/preconfigured_endpoint_helper';
import * as i18n from './translations';

export interface EndpointInfoProps {
inferenceId: string;
}

export const EndpointInfo: React.FC<EndpointInfoProps> = ({ inferenceId }) => (
<span>
<strong>{inferenceId}</strong>
</span>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<span>
<strong>{inferenceId}</strong>
</span>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>
{isEndpointPreconfigured(inferenceId) ? (
<EuiBetaBadge label={i18n.PRECONFIGURED_LABEL} size="s" color="hollow" />
) : null}
</span>
</EuiFlexItem>
</EuiFlexGroup>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const PRECONFIGURED_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.elasticsearch.endpointInfo.preconfigured',
{
defaultMessage: 'PRECONFIGURED',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ const inferenceEndpoints = [
},
task_settings: {},
},
{
inference_id: '.elser-2',
task_type: 'sparse_embedding',
service: 'elasticsearch',
service_settings: {
num_allocations: 1,
num_threads: 1,
model_id: '.elser_model_2',
},
task_settings: {},
},
{
inference_id: '.multi-e5-small',
task_type: 'text_embedding',
service: 'elasticsearch',
service_settings: {
num_allocations: 1,
num_threads: 1,
model_id: '.multilingual-e5-small',
},
task_settings: {},
},
] as InferenceAPIConfigResponse[];

jest.mock('../../hooks/use_delete_endpoint', () => ({
Expand All @@ -58,22 +80,55 @@ describe('When the tabular page is loaded', () => {
render(<TabularPage inferenceEndpoints={inferenceEndpoints} />);

const rows = screen.getAllByRole('row');
expect(rows[1]).toHaveTextContent('local-model');
expect(rows[2]).toHaveTextContent('my-elser-model-05');
expect(rows[3]).toHaveTextContent('third-party-model');
expect(rows[1]).toHaveTextContent('.elser-2');
expect(rows[2]).toHaveTextContent('.multi-e5-small');
expect(rows[3]).toHaveTextContent('local-model');
expect(rows[4]).toHaveTextContent('my-elser-model-05');
expect(rows[5]).toHaveTextContent('third-party-model');
});

it('should display all service and model ids in the table', () => {
render(<TabularPage inferenceEndpoints={inferenceEndpoints} />);

const rows = screen.getAllByRole('row');
expect(rows[1]).toHaveTextContent('Elasticsearch');
expect(rows[1]).toHaveTextContent('.own_model');
expect(rows[1]).toHaveTextContent('.elser_model_2');

expect(rows[2]).toHaveTextContent('Elasticsearch');
expect(rows[2]).toHaveTextContent('.elser_model_2');
expect(rows[2]).toHaveTextContent('.multilingual-e5-small');

expect(rows[3]).toHaveTextContent('OpenAI');
expect(rows[3]).toHaveTextContent('Elasticsearch');
expect(rows[3]).toHaveTextContent('.own_model');

expect(rows[4]).toHaveTextContent('Elasticsearch');
expect(rows[4]).toHaveTextContent('.elser_model_2');

expect(rows[5]).toHaveTextContent('OpenAI');
expect(rows[5]).toHaveTextContent('.own_model');
});

it('should only disable delete action for preconfigured endpoints', () => {
render(<TabularPage inferenceEndpoints={inferenceEndpoints} />);

const deleteActions = screen.getAllByTestId('inferenceUIDeleteAction');

expect(deleteActions[0]).toBeDisabled();
expect(deleteActions[1]).toBeDisabled();
expect(deleteActions[2]).toBeEnabled();
expect(deleteActions[3]).toBeEnabled();
expect(deleteActions[4]).toBeEnabled();
});

it('should show preconfigured badge only for preconfigured endpoints', () => {
render(<TabularPage inferenceEndpoints={inferenceEndpoints} />);

const preconfigured = 'PRECONFIGURED';

const rows = screen.getAllByRole('row');
expect(rows[1]).toHaveTextContent(preconfigured);
expect(rows[2]).toHaveTextContent(preconfigured);
expect(rows[3]).not.toHaveTextContent(preconfigured);
expect(rows[4]).not.toHaveTextContent(preconfigured);
expect(rows[5]).not.toHaveTextContent(preconfigured);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints })
return null;
},
sortable: true,
truncateText: true,
width: '300px',
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { PRECONFIGURED_ENDPOINTS } from '../components/all_inference_endpoints/constants';
import { isEndpointPreconfigured } from './preconfigured_endpoint_helper';

describe('Preconfigured Endpoint helper', () => {
it('return true for preconfigured elser', () => {
expect(isEndpointPreconfigured(PRECONFIGURED_ENDPOINTS.ELSER)).toEqual(true);
});

it('return true for preconfigured e5', () => {
expect(isEndpointPreconfigured(PRECONFIGURED_ENDPOINTS.E5)).toEqual(true);
});

it('return false for other endpoints', () => {
expect(isEndpointPreconfigured('other-endpoints')).toEqual(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { PRECONFIGURED_ENDPOINTS } from '../components/all_inference_endpoints/constants';

export const isEndpointPreconfigured = (endpoint: string) =>
Object.values(PRECONFIGURED_ENDPOINTS).includes(endpoint);