mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Synonyms UI] Add Synonyms Set Detail UI (#207856)
## Summary Adds route and table for the synonyms set detail page. Edit action is not implemented yet. whole plugin is behind feature flags. Another PR will add them following this. <img width="1205" alt="Screenshot 2025-01-22 at 14 43 33" src="https://github.com/user-attachments/assets/1b15f2e5-55cd-4159-b00e-d8dddf0191e1" /> ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
5624fc2106
commit
0168adbe81
26 changed files with 1199 additions and 12 deletions
|
@ -5,7 +5,10 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const PLUGIN_ROUTE_ROOT = '/app/elasticsearch/synonyms';
|
||||||
|
|
||||||
export enum APIRoutes {
|
export enum APIRoutes {
|
||||||
SYNONYM_SETS = '/internal/search_synonyms/synonyms',
|
SYNONYM_SETS = '/internal/search_synonyms/synonyms',
|
||||||
SYNONYM_SET_ID = '/internal/search_synonyms/synonyms/{synonymsSetId}',
|
SYNONYM_SET_ID = '/internal/search_synonyms/synonyms/{synonymsSetId}',
|
||||||
|
SYNONYM_SET_ID_RULE_ID = '/internal/search_synonyms/synonyms/{synonymsSetId}/{ruleId}',
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,9 @@ import { CoreStart } from '@kbn/core/public';
|
||||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import { I18nProvider } from '@kbn/i18n-react';
|
import { I18nProvider } from '@kbn/i18n-react';
|
||||||
import { Route, Router, Routes } from '@kbn/shared-ux-router';
|
import { Router } from '@kbn/shared-ux-router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AppPluginStartDependencies } from './types';
|
import { AppPluginStartDependencies } from './types';
|
||||||
import { SearchSynonymsOverview } from './components/overview/overview';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({});
|
const queryClient = new QueryClient({});
|
||||||
export const renderApp = async (
|
export const renderApp = async (
|
||||||
|
@ -22,17 +21,15 @@ export const renderApp = async (
|
||||||
services: AppPluginStartDependencies,
|
services: AppPluginStartDependencies,
|
||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
) => {
|
) => {
|
||||||
|
const { SearchSynonymsRouter } = await import('./search_synonyms_router');
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<KibanaRenderContextProvider {...core}>
|
<KibanaRenderContextProvider {...core}>
|
||||||
<KibanaContextProvider services={{ ...core, ...services }}>
|
<KibanaContextProvider services={{ ...core, ...services }}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router history={services.history}>
|
<Router history={services.history}>
|
||||||
<Routes>
|
<SearchSynonymsRouter />
|
||||||
<Route path="/">
|
|
||||||
<SearchSynonymsOverview />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
</Router>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
|
|
|
@ -8,7 +8,15 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiLink,
|
||||||
|
EuiLoadingSpinner,
|
||||||
|
EuiText,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { useKibana } from '../../hooks/use_kibana';
|
import { useKibana } from '../../hooks/use_kibana';
|
||||||
import { SynonymSets } from '../synonym_sets/synonym_sets';
|
import { SynonymSets } from '../synonym_sets/synonym_sets';
|
||||||
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';
|
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';
|
||||||
|
@ -31,7 +39,40 @@ export const SearchSynonymsOverview = () => {
|
||||||
grow={false}
|
grow={false}
|
||||||
data-test-subj="searchSynonymsOverviewPage"
|
data-test-subj="searchSynonymsOverviewPage"
|
||||||
solutionNav={searchNavigation?.useClassicNavigation(history)}
|
solutionNav={searchNavigation?.useClassicNavigation(history)}
|
||||||
|
color="primary"
|
||||||
>
|
>
|
||||||
|
<KibanaPageTemplate.Header
|
||||||
|
pageTitle="Synonyms"
|
||||||
|
restrictWidth
|
||||||
|
color="primary"
|
||||||
|
rightSideItems={[
|
||||||
|
<EuiFlexGroup alignItems="center">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiLink>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.searchSynonyms.synonymsSetDetail.documentationLink"
|
||||||
|
defaultMessage="API Documentation"
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton fill iconType="plusInCircle">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.searchSynonyms.synonymsSetDetail.createButton"
|
||||||
|
defaultMessage="Create"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<EuiText>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.searchSynonyms.synonymsSetDetail.description"
|
||||||
|
defaultMessage="Create and manage synonym sets and synonym rules."
|
||||||
|
/>
|
||||||
|
</EuiText>
|
||||||
|
</KibanaPageTemplate.Header>
|
||||||
<KibanaPageTemplate.Section restrictWidth>
|
<KibanaPageTemplate.Section restrictWidth>
|
||||||
{isInitialLoading && <EuiLoadingSpinner />}
|
{isInitialLoading && <EuiLoadingSpinner />}
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SynonymsGetSynonymsSetsSynonymsSetItem } from '@elastic/elasticsearch/lib/api/types';
|
import { SynonymsGetSynonymsSetsSynonymsSetItem } from '@elastic/elasticsearch/lib/api/types';
|
||||||
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
|
import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { PLUGIN_ROUTE_ROOT } from '../../../common/api_routes';
|
||||||
import { DEFAULT_PAGE_VALUE, paginationToPage } from '../../../common/pagination';
|
import { DEFAULT_PAGE_VALUE, paginationToPage } from '../../../common/pagination';
|
||||||
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';
|
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';
|
||||||
import { DeleteSynonymsSetModal } from './delete_synonyms_set_modal';
|
import { DeleteSynonymsSetModal } from './delete_synonyms_set_modal';
|
||||||
|
|
||||||
export const SynonymSets = () => {
|
export const SynonymSets = () => {
|
||||||
|
const {
|
||||||
|
services: { application },
|
||||||
|
} = useKibana();
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_VALUE.size);
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_VALUE.size);
|
||||||
const { from } = paginationToPage({ pageIndex, pageSize, totalItemCount: 0 });
|
const { from } = paginationToPage({ pageIndex, pageSize, totalItemCount: 0 });
|
||||||
|
@ -37,7 +42,13 @@ export const SynonymSets = () => {
|
||||||
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.nameColumn', {
|
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.nameColumn', {
|
||||||
defaultMessage: 'Synonyms Set',
|
defaultMessage: 'Synonyms Set',
|
||||||
}),
|
}),
|
||||||
render: (name: string) => <div data-test-subj="synonyms-set-item-name">{name}</div>,
|
render: (name: string) => (
|
||||||
|
<div data-test-subj="synonyms-set-item-name">
|
||||||
|
<EuiLink onClick={() => application?.navigateToUrl(`${PLUGIN_ROUTE_ROOT}/sets/${name}`)}>
|
||||||
|
{name}
|
||||||
|
</EuiLink>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'count',
|
field: 'count',
|
||||||
|
@ -78,7 +89,8 @@ export const SynonymSets = () => {
|
||||||
icon: 'pencil',
|
icon: 'pencil',
|
||||||
color: 'text',
|
color: 'text',
|
||||||
type: 'icon',
|
type: 'icon',
|
||||||
onClick: () => {},
|
onClick: (synonymsSet: SynonymsGetSynonymsSetsSynonymsSetItem) =>
|
||||||
|
application?.navigateToUrl(`${PLUGIN_ROUTE_ROOT}/sets/${synonymsSet.synonyms_set}`),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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 { useDeleteSynonymRule } from '../../hooks/use_delete_synonym_rule';
|
||||||
|
import { DeleteSynonymRuleModal } from './delete_synonym_rule_modal';
|
||||||
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
jest.mock('../../hooks/use_delete_synonym_rule', () => ({
|
||||||
|
useDeleteSynonymRule: jest.fn(() => ({
|
||||||
|
mutate: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('DeleteSynonymRuleModal', () => {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use mutation when cancel is pressed', () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
const mutate = jest.fn();
|
||||||
|
(useDeleteSynonymRule as unknown as jest.Mock).mockReturnValue({
|
||||||
|
mutate,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<Wrapper>
|
||||||
|
<DeleteSynonymRuleModal synonymsSetId="123" ruleId="456" closeDeleteModal={onClose} />
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByText('Cancel'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onClose).toHaveBeenCalled();
|
||||||
|
expect(useDeleteSynonymRule).toHaveBeenCalled();
|
||||||
|
expect(mutate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete the synonym rule when delete is pressed', () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
const mutate = jest.fn();
|
||||||
|
|
||||||
|
(useDeleteSynonymRule as unknown as jest.Mock).mockReturnValue({
|
||||||
|
mutate,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Wrapper>
|
||||||
|
<DeleteSynonymRuleModal synonymsSetId="123" ruleId="456" closeDeleteModal={onClose} />
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByText('Delete'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useDeleteSynonymRule).toHaveBeenCalled();
|
||||||
|
expect(mutate).toHaveBeenCalled();
|
||||||
|
expect(mutate).toHaveBeenCalledWith({ synonymsSetId: '123', ruleId: '456' });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiConfirmModal } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { useDeleteSynonymRule } from '../../hooks/use_delete_synonym_rule';
|
||||||
|
|
||||||
|
export interface DeleteSynonymRuleModalProps {
|
||||||
|
synonymsSetId: string;
|
||||||
|
ruleId: string;
|
||||||
|
closeDeleteModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteSynonymRuleModal = ({
|
||||||
|
closeDeleteModal,
|
||||||
|
ruleId,
|
||||||
|
synonymsSetId,
|
||||||
|
}: DeleteSynonymRuleModalProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const onSuccess = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
closeDeleteModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
closeDeleteModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutate: deleteEndpoint } = useDeleteSynonymRule(onSuccess, onError);
|
||||||
|
|
||||||
|
const deleteOperation = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
deleteEndpoint({ synonymsSetId, ruleId });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiConfirmModal
|
||||||
|
title={i18n.translate('xpack.searchSynonyms.deleteSynonymRuleModal.title', {
|
||||||
|
defaultMessage: 'Delete synonym rule',
|
||||||
|
})}
|
||||||
|
onCancel={closeDeleteModal}
|
||||||
|
onConfirm={deleteOperation}
|
||||||
|
cancelButtonText={i18n.translate('xpack.searchSynonyms.deleteSynonymRuleModal.cancelButton', {
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
})}
|
||||||
|
confirmButtonText={i18n.translate(
|
||||||
|
'xpack.searchSynonyms.deleteSynonymRuleModal.confirmButton',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Delete',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
buttonColor="danger"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{i18n.translate('xpack.searchSynonyms.deleteSynonymRuleModal.body', {
|
||||||
|
defaultMessage: 'Are you sure you want to delete the synonym rule {ruleId}?',
|
||||||
|
values: { ruleId },
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</EuiConfirmModal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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 { useParams } from 'react-router-dom';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||||
|
import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
|
import { useKibana } from '../../hooks/use_kibana';
|
||||||
|
import { SynonymsSetRuleTable } from './synonyms_set_rule_table';
|
||||||
|
|
||||||
|
export const SynonymsSetDetail = () => {
|
||||||
|
const { synonymsSetId = '' } = useParams<{
|
||||||
|
synonymsSetId?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
services: { console: consolePlugin, history, searchNavigation },
|
||||||
|
} = useKibana();
|
||||||
|
|
||||||
|
const embeddableConsole = useMemo(
|
||||||
|
() => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
|
||||||
|
[consolePlugin]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KibanaPageTemplate
|
||||||
|
offset={0}
|
||||||
|
restrictWidth={false}
|
||||||
|
grow={false}
|
||||||
|
data-test-subj="searchSynonymsSetDetailPage"
|
||||||
|
solutionNav={searchNavigation?.useClassicNavigation(history)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<KibanaPageTemplate.Header
|
||||||
|
pageTitle={synonymsSetId}
|
||||||
|
restrictWidth
|
||||||
|
color="primary"
|
||||||
|
rightSideItems={[
|
||||||
|
<EuiFlexGroup alignItems="center">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton color="text" iconType="endpoint">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.searchSynonyms.synonymsSetDetail.connectToApiButton"
|
||||||
|
defaultMessage="Connect to API"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonIcon iconType="boxesHorizontal" size="m" color="text" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<KibanaPageTemplate.Section restrictWidth>
|
||||||
|
{synonymsSetId && <SynonymsSetRuleTable synonymsSetId={synonymsSetId} />}
|
||||||
|
</KibanaPageTemplate.Section>
|
||||||
|
{embeddableConsole}
|
||||||
|
</KibanaPageTemplate>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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 { render, screen } from '@testing-library/react';
|
||||||
|
import { SynonymsSetRuleTable } from './synonyms_set_rule_table';
|
||||||
|
|
||||||
|
jest.mock('../../hooks/use_fetch_synonyms_set', () => ({
|
||||||
|
useFetchSynonymsSet: () => ({
|
||||||
|
data: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'rule_id_1',
|
||||||
|
synonyms: 'synonym1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule_id_2',
|
||||||
|
synonyms: 'synonym2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule_id_3',
|
||||||
|
synonyms: 'explicit-from => explicit-to',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'my_synonyms_set',
|
||||||
|
_meta: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
totalItemCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SynonymSetDetail table', () => {
|
||||||
|
it('should render the list with synonym rules', () => {
|
||||||
|
render(<SynonymsSetRuleTable synonymsSetId="synonymSetId" />);
|
||||||
|
const synonymSetTable = screen.getByTestId('synonyms-set-table');
|
||||||
|
expect(synonymSetTable).toBeInTheDocument();
|
||||||
|
|
||||||
|
const synonymsSetExplicitFrom = screen.getByTestId('synonyms-set-item-explicit-from');
|
||||||
|
const synonymsSetExplicitTo = screen.getByTestId('synonyms-set-item-explicit-to');
|
||||||
|
expect(synonymsSetExplicitFrom.textContent?.trim()).toBe('explicit-from');
|
||||||
|
expect(synonymsSetExplicitTo.textContent?.trim()).toBe('explicit-to');
|
||||||
|
|
||||||
|
const synonymsSetEquivalent = screen.getAllByTestId('synonyms-set-item-equivalent');
|
||||||
|
expect(synonymsSetEquivalent).toHaveLength(2);
|
||||||
|
expect(synonymsSetEquivalent[0].textContent).toBe('synonym1');
|
||||||
|
expect(synonymsSetEquivalent[1].textContent).toBe('synonym2');
|
||||||
|
|
||||||
|
expect(screen.getByTestId('tablePaginationPopoverButton')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('pagination-button-0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
EuiBasicTable,
|
||||||
|
EuiBasicTableColumn,
|
||||||
|
EuiButtonIcon,
|
||||||
|
EuiCode,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiText,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { DEFAULT_PAGE_VALUE, paginationToPage } from '../../../common/pagination';
|
||||||
|
import { useFetchSynonymsSet } from '../../hooks/use_fetch_synonyms_set';
|
||||||
|
import { getExplicitSynonym, isExplicitSynonym } from '../../utils/synonyms_utils';
|
||||||
|
import { DeleteSynonymRuleModal } from './delete_synonym_rule_modal';
|
||||||
|
|
||||||
|
export const SynonymsSetRuleTable = ({ synonymsSetId = '' }: { synonymsSetId: string }) => {
|
||||||
|
const [pageIndex, setPageIndex] = React.useState(0);
|
||||||
|
const [pageSize, setPageSize] = React.useState(DEFAULT_PAGE_VALUE.size);
|
||||||
|
const { from } = paginationToPage({ pageIndex, pageSize, totalItemCount: 0 });
|
||||||
|
const [synonymRuleToDelete, setSynonymRuleToDelete] = React.useState<string | null>(null);
|
||||||
|
const { data, isLoading } = useFetchSynonymsSet(synonymsSetId, { from, size: pageSize });
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
initialPageSize: 10,
|
||||||
|
pageSizeOptions: [10, 25, 50],
|
||||||
|
...data._meta,
|
||||||
|
pageSize,
|
||||||
|
pageIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: Array<EuiBasicTableColumn<SynonymsSynonymRule>> = [
|
||||||
|
{
|
||||||
|
field: 'synonyms',
|
||||||
|
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.synonymsColumn', {
|
||||||
|
defaultMessage: 'Synonyms',
|
||||||
|
}),
|
||||||
|
render: (synonyms: string) => {
|
||||||
|
const isExplicit = isExplicitSynonym(synonyms);
|
||||||
|
const [explicitFrom = '', explicitTo = ''] = isExplicit ? getExplicitSynonym(synonyms) : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup responsive={false}>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonIcon
|
||||||
|
iconType="expand"
|
||||||
|
aria-label={i18n.translate(
|
||||||
|
'xpack.searchSynonyms.synonymsSetTable.expandSynonyms.aria.label',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Expand synonyms rule',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{isExplicit ? (
|
||||||
|
<>
|
||||||
|
<EuiFlexItem data-test-subj="synonyms-set-item-explicit-from">
|
||||||
|
<EuiCode>{explicitFrom}</EuiCode>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiText>
|
||||||
|
<b>{'=>'}</b>
|
||||||
|
</EuiText>
|
||||||
|
<EuiFlexItem grow={false} data-test-subj="synonyms-set-item-explicit-to">
|
||||||
|
<EuiCode>{explicitTo}</EuiCode>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EuiFlexItem data-test-subj="synonyms-set-item-equivalent">
|
||||||
|
<EuiCode>{synonyms}</EuiCode>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: '8%',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.actions.delete', {
|
||||||
|
defaultMessage: 'Delete',
|
||||||
|
}),
|
||||||
|
description: i18n.translate(
|
||||||
|
'xpack.searchSynonyms.synonymsSetTable.actions.deleteDescription',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Delete synonym rule',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon: 'trash',
|
||||||
|
color: 'danger',
|
||||||
|
type: 'icon',
|
||||||
|
onClick: (synonymRule: SynonymsSynonymRule) => {
|
||||||
|
if (synonymRule.id) {
|
||||||
|
setSynonymRuleToDelete(synonymRule.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.actions.edit', {
|
||||||
|
defaultMessage: 'Edit',
|
||||||
|
}),
|
||||||
|
description: i18n.translate(
|
||||||
|
'xpack.searchSynonyms.synonymsSetTable.actions.editDescription',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Edit synonym rule',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon: 'pencil',
|
||||||
|
type: 'icon',
|
||||||
|
onClick: () => {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{synonymRuleToDelete && (
|
||||||
|
<DeleteSynonymRuleModal
|
||||||
|
synonymsSetId={synonymsSetId}
|
||||||
|
ruleId={synonymRuleToDelete}
|
||||||
|
closeDeleteModal={() => setSynonymRuleToDelete(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<EuiBasicTable
|
||||||
|
data-test-subj="synonyms-set-table"
|
||||||
|
items={data.data}
|
||||||
|
columns={columns}
|
||||||
|
loading={isLoading}
|
||||||
|
pagination={pagination}
|
||||||
|
onChange={({ page }) => {
|
||||||
|
setPageIndex(page.index);
|
||||||
|
setPageSize(page.size);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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 { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { useKibana } from './use_kibana';
|
||||||
|
|
||||||
|
jest.mock('./use_kibana');
|
||||||
|
|
||||||
|
const mockUseKibana = useKibana as jest.Mock;
|
||||||
|
const mockDelete = jest.fn();
|
||||||
|
const mockDeleteSuccess = jest.fn();
|
||||||
|
const mockDeleteError = jest.fn();
|
||||||
|
|
||||||
|
describe('useDeleteSynonymRule hook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockUseKibana.mockReturnValue({
|
||||||
|
services: {
|
||||||
|
http: {
|
||||||
|
delete: mockDelete,
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
toasts: {
|
||||||
|
addSuccess: mockDeleteSuccess,
|
||||||
|
addError: mockDeleteError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockDelete.mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should delete the synonym rule', async () => {
|
||||||
|
const { useDeleteSynonymRule } = jest.requireActual('./use_delete_synonym_rule');
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteSynonymRule(), { wrapper });
|
||||||
|
|
||||||
|
result.current.mutate({ synonymsSetId: '123', ruleId: '1' });
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mockDelete).toHaveBeenCalledWith('/internal/search_synonyms/synonyms/123/1')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show an error message if delete fails', async () => {
|
||||||
|
const error = {
|
||||||
|
body: { message: 'An error occurred' },
|
||||||
|
};
|
||||||
|
mockDelete.mockRejectedValue(error);
|
||||||
|
const { useDeleteSynonymRule } = jest.requireActual('./use_delete_synonym_rule');
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteSynonymRule(), { wrapper });
|
||||||
|
|
||||||
|
result.current.mutate({ synonymsSetId: '123', ruleId: '1' });
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mockDeleteError).toHaveBeenCalledWith(new Error(error.body.message), {
|
||||||
|
title: 'Error deleting synonym rule',
|
||||||
|
toastMessage: error.body.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 { useQueryClient, useMutation } from '@tanstack/react-query';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { KibanaServerError } from '@kbn/kibana-utils-plugin/common';
|
||||||
|
import { useKibana } from './use_kibana';
|
||||||
|
|
||||||
|
interface MutationArgs {
|
||||||
|
synonymsSetId: string;
|
||||||
|
ruleId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteSynonymRule = (onSuccess?: () => void, onError?: (error: string) => void) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const {
|
||||||
|
services: { http, notifications },
|
||||||
|
} = useKibana();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
async ({ synonymsSetId, ruleId }: MutationArgs) => {
|
||||||
|
return await http.delete<{ acknowledged: boolean }>(
|
||||||
|
`/internal/search_synonyms/synonyms/${synonymsSetId}/${ruleId}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (_, { synonymsSetId, ruleId }) => {
|
||||||
|
queryClient.invalidateQueries(['synonyms-rule-delete', synonymsSetId, ruleId]);
|
||||||
|
notifications?.toasts?.addSuccess({
|
||||||
|
title: i18n.translate('xpack.searchSynonyms.deleteSynonymRuleSuccess', {
|
||||||
|
defaultMessage: 'Synonym rule {ruleId} deleted',
|
||||||
|
values: { ruleId },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: { body: KibanaServerError }) => {
|
||||||
|
if (onError) {
|
||||||
|
onError(error.body.message);
|
||||||
|
} else {
|
||||||
|
notifications?.toasts?.addError(new Error(error.body.message), {
|
||||||
|
title: i18n.translate('xpack.searchSynonyms.deleteSynonymRuleError', {
|
||||||
|
defaultMessage: 'Error deleting synonym rule',
|
||||||
|
}),
|
||||||
|
toastMessage: error.body.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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 { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
const mockHttpGet = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('@tanstack/react-query', () => ({
|
||||||
|
useQuery: jest.fn().mockImplementation(async ({ queryFn, opts }) => {
|
||||||
|
try {
|
||||||
|
const res = await queryFn();
|
||||||
|
return Promise.resolve(res);
|
||||||
|
} catch (e) {
|
||||||
|
// opts.onError(e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./use_kibana', () => ({
|
||||||
|
useKibana: jest.fn().mockReturnValue({
|
||||||
|
services: {
|
||||||
|
http: {
|
||||||
|
get: mockHttpGet,
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
toasts: {
|
||||||
|
addError: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useFetchSynonymRule Hook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return synonym rule', async () => {
|
||||||
|
const synonymRule = {
|
||||||
|
id: '1',
|
||||||
|
synonyms: 'synoym1, synonym2',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockHttpGet.mockReturnValue(synonymRule);
|
||||||
|
const { useFetchSynonymRule } = jest.requireActual('./use_fetch_synonym_rule');
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFetchSynonymRule('my_synonyms_set', '1'));
|
||||||
|
await waitFor(() => expect(result.current).resolves.toStrictEqual(synonymRule));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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 { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useKibana } from './use_kibana';
|
||||||
|
|
||||||
|
export const useFetchSynonymRule = (synonymsSetId: string, ruleId: string) => {
|
||||||
|
const {
|
||||||
|
services: { http },
|
||||||
|
} = useKibana();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['synonyms-rule-fetch', synonymsSetId, ruleId],
|
||||||
|
queryFn: async () => {
|
||||||
|
return await http.get<SynonymsSynonymRule>(
|
||||||
|
`/internal/search_synonyms/synonyms/${synonymsSetId}/${ruleId}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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 { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
const mockHttpGet = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('@tanstack/react-query', () => ({
|
||||||
|
useQuery: jest.fn().mockImplementation(async ({ queryKey, queryFn, opts }) => {
|
||||||
|
try {
|
||||||
|
const res = await queryFn();
|
||||||
|
return Promise.resolve(res);
|
||||||
|
} catch (e) {
|
||||||
|
// opts.onError(e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./use_kibana', () => ({
|
||||||
|
useKibana: jest.fn().mockReturnValue({
|
||||||
|
services: {
|
||||||
|
http: {
|
||||||
|
get: mockHttpGet,
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
toasts: {
|
||||||
|
addError: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useFetchSynonymsSet Hook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return synonyms set', async () => {
|
||||||
|
const synonyms = {
|
||||||
|
_meta: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
totalItemCount: 2,
|
||||||
|
},
|
||||||
|
id: 'my_synonyms_set',
|
||||||
|
synonyms: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
synonyms: 'foo, bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mockHttpGet.mockReturnValue(synonyms);
|
||||||
|
const { useFetchSynonymsSets } = jest.requireActual('./use_fetch_synonyms_sets');
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFetchSynonymsSets());
|
||||||
|
await waitFor(() => expect(result.current).resolves.toStrictEqual(synonyms));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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 { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { DEFAULT_PAGE_VALUE, Page, Paginate } from '../../common/pagination';
|
||||||
|
import { useKibana } from './use_kibana';
|
||||||
|
|
||||||
|
export const useFetchSynonymsSet = (synonymsSetId: string, page: Page = DEFAULT_PAGE_VALUE) => {
|
||||||
|
const {
|
||||||
|
services: { http },
|
||||||
|
} = useKibana();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['synonyms-sets-fetch', synonymsSetId, page.from, page.size],
|
||||||
|
queryFn: async () => {
|
||||||
|
return await http.get<Paginate<SynonymsSynonymRule> & { id: string }>(
|
||||||
|
`/internal/search_synonyms/synonyms/${synonymsSetId}`,
|
||||||
|
{
|
||||||
|
query: { from: page.from, size: page.size },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from './types';
|
} from './types';
|
||||||
import { SYNONYMS_UI_FLAG } from '../common/ui_flags';
|
import { SYNONYMS_UI_FLAG } from '../common/ui_flags';
|
||||||
import { docLinks } from '../common/doc_links';
|
import { docLinks } from '../common/doc_links';
|
||||||
|
import { PLUGIN_ROUTE_ROOT } from '../common/api_routes';
|
||||||
|
|
||||||
export class SearchSynonymsPlugin
|
export class SearchSynonymsPlugin
|
||||||
implements Plugin<SearchSynonymsPluginSetup, SearchSynonymsPluginStart>
|
implements Plugin<SearchSynonymsPluginSetup, SearchSynonymsPluginStart>
|
||||||
|
@ -30,7 +31,7 @@ export class SearchSynonymsPlugin
|
||||||
}
|
}
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: PLUGIN_ID,
|
id: PLUGIN_ID,
|
||||||
appRoute: '/app/elasticsearch/synonyms',
|
appRoute: PLUGIN_ROUTE_ROOT,
|
||||||
title: PLUGIN_TITLE,
|
title: PLUGIN_TITLE,
|
||||||
deepLinks: [
|
deepLinks: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 { Route, Routes } from '@kbn/shared-ux-router';
|
||||||
|
import React from 'react';
|
||||||
|
import { SynonymsSetDetail } from './components/synonyms_set_detail/synonyms_set_detail';
|
||||||
|
import { SearchSynonymsOverview } from './components/overview/overview';
|
||||||
|
|
||||||
|
export const SearchSynonymsRouter = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route exact path="/sets/:synonymsSetId">
|
||||||
|
<SynonymsSetDetail />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/">
|
||||||
|
<SearchSynonymsOverview />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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 { getExplicitSynonym, isExplicitSynonym } from './synonyms_utils';
|
||||||
|
|
||||||
|
describe('isExplicitSynonym util function', () => {
|
||||||
|
it('should return true if synonym includes "=>"', () => {
|
||||||
|
expect(isExplicitSynonym('synonym1 => synonym2')).toBe(true);
|
||||||
|
expect(isExplicitSynonym('synonym1,synonym2, synonym5 => synonym2')).toBe(true);
|
||||||
|
expect(isExplicitSynonym(' synonym1,synonym2, synonym5 => synonym2 ')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should return false if synonym does not include "=>"', () => {
|
||||||
|
expect(isExplicitSynonym('synonym1')).toBe(false);
|
||||||
|
expect(isExplicitSynonym('synonym1,synonym2, synonym5')).toBe(false);
|
||||||
|
expect(isExplicitSynonym(' synonym1,synonym2, synonym5 ')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExplicitSynonym util function', () => {
|
||||||
|
it('should return an array with the explicit synonym', () => {
|
||||||
|
expect(getExplicitSynonym('synonym1 => synonym2')).toEqual(['synonym1', 'synonym2']);
|
||||||
|
expect(getExplicitSynonym('synonym1,synonym2, synonym5 => synonym2')).toEqual([
|
||||||
|
'synonym1,synonym2, synonym5',
|
||||||
|
'synonym2',
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
getExplicitSynonym(' synonym1,synonym2, synonym5 => synonym2 ')
|
||||||
|
).toEqual(['synonym1,synonym2, synonym5', 'synonym2']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const isExplicitSynonym = (synonym: string) => {
|
||||||
|
return synonym.trim().includes('=>');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExplicitSynonym = (synonym: string) => {
|
||||||
|
return [synonym.split('=>')[0].trim(), synonym.split('=>')[1].trim()];
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||||
|
import { deleteSynonymRule } from './delete_synonym_rule';
|
||||||
|
|
||||||
|
describe('delete synonym rule lib function', () => {
|
||||||
|
const mockClient = {
|
||||||
|
synonyms: {
|
||||||
|
deleteSynonymRule: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = () => mockClient as unknown as ElasticsearchClient;
|
||||||
|
it('should delete synonym rule', async () => {
|
||||||
|
mockClient.synonyms.deleteSynonymRule.mockResolvedValue({});
|
||||||
|
|
||||||
|
await deleteSynonymRule(client(), 'my_synonyms_set', 'rule_id_1');
|
||||||
|
|
||||||
|
expect(mockClient.synonyms.deleteSynonymRule).toHaveBeenCalledWith({
|
||||||
|
rule_id: 'rule_id_1',
|
||||||
|
set_id: 'my_synonyms_set',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||||
|
|
||||||
|
export const deleteSynonymRule = async (
|
||||||
|
client: ElasticsearchClient,
|
||||||
|
synonymsSetId: string,
|
||||||
|
ruleId: string
|
||||||
|
) => {
|
||||||
|
return client.synonyms.deleteSynonymRule({ set_id: synonymsSetId, rule_id: ruleId });
|
||||||
|
};
|
|
@ -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 { ElasticsearchClient } from '@kbn/core/server';
|
||||||
|
import { fetchSynonymRule } from './fetch_synonym_rule';
|
||||||
|
|
||||||
|
describe('fetch synonym rule lib function', () => {
|
||||||
|
const mockClient = {
|
||||||
|
synonyms: {
|
||||||
|
getSynonymRule: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = () => mockClient as unknown as ElasticsearchClient;
|
||||||
|
it('should return synonym rule', async () => {
|
||||||
|
mockClient.synonyms.getSynonymRule.mockResolvedValue({
|
||||||
|
id: 'rule_id_1',
|
||||||
|
synonyms: 'synonym1, synonym2',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchSynonymRule(client(), 'my_synonyms_set', 'rule_id_1');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 'rule_id_1',
|
||||||
|
synonyms: 'synonym1, synonym2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||||
|
|
||||||
|
export const fetchSynonymRule = async (
|
||||||
|
client: ElasticsearchClient,
|
||||||
|
synonymsSetId: string,
|
||||||
|
ruleId: string
|
||||||
|
) => {
|
||||||
|
return client.synonyms.getSynonymRule({
|
||||||
|
set_id: synonymsSetId,
|
||||||
|
rule_id: ruleId,
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||||
|
import { fetchSynonymsSet } from './fetch_synonyms_set';
|
||||||
|
|
||||||
|
describe('fetch synonyms set lib function', () => {
|
||||||
|
const mockClient = {
|
||||||
|
synonyms: {
|
||||||
|
getSynonym: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = () => mockClient as unknown as ElasticsearchClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return synonym set', async () => {
|
||||||
|
mockClient.synonyms.getSynonym.mockResolvedValue({
|
||||||
|
synonyms_set: [
|
||||||
|
{
|
||||||
|
id: 'rule_id_1',
|
||||||
|
synonyms: ['synonym1', 'synonym2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchSynonymsSet(client(), 'my_synonyms_set', { from: 0, size: 10 });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
_meta: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
totalItemCount: 2,
|
||||||
|
},
|
||||||
|
id: 'my_synonyms_set',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'rule_id_1',
|
||||||
|
synonyms: ['synonym1', 'synonym2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||||
|
import { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||||
|
import { Page, Paginate, pageToPagination } from '../../common/pagination';
|
||||||
|
|
||||||
|
export const fetchSynonymsSet = async (
|
||||||
|
client: ElasticsearchClient,
|
||||||
|
synonymsSetId: string,
|
||||||
|
{ from, size }: Page
|
||||||
|
): Promise<Paginate<SynonymsSynonymRule> & { id: string }> => {
|
||||||
|
const result = await client.synonyms.getSynonym({
|
||||||
|
id: synonymsSetId,
|
||||||
|
from,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
const _meta = pageToPagination({ from, size, total: result.count });
|
||||||
|
return { _meta, id: synonymsSetId, data: result.synonyms_set };
|
||||||
|
};
|
|
@ -13,6 +13,9 @@ import { errorHandler } from './utils/error_handler';
|
||||||
import { fetchSynonymSets } from './lib/fetch_synonym_sets';
|
import { fetchSynonymSets } from './lib/fetch_synonym_sets';
|
||||||
import { DEFAULT_PAGE_VALUE } from '../common/pagination';
|
import { DEFAULT_PAGE_VALUE } from '../common/pagination';
|
||||||
import { deleteSynonymsSet } from './lib/delete_synonyms_set';
|
import { deleteSynonymsSet } from './lib/delete_synonyms_set';
|
||||||
|
import { fetchSynonymsSet } from './lib/fetch_synonyms_set';
|
||||||
|
import { deleteSynonymRule } from './lib/delete_synonym_rule';
|
||||||
|
import { fetchSynonymRule } from './lib/fetch_synonym_rule';
|
||||||
|
|
||||||
export function defineRoutes({ logger, router }: { logger: Logger; router: IRouter }) {
|
export function defineRoutes({ logger, router }: { logger: Logger; router: IRouter }) {
|
||||||
router.get(
|
router.get(
|
||||||
|
@ -59,6 +62,9 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout
|
||||||
size: request.query.size,
|
size: request.query.size,
|
||||||
});
|
});
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
body: result,
|
body: result,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -97,6 +103,169 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout
|
||||||
const synonymsSetId = request.params.synonymsSetId;
|
const synonymsSetId = request.params.synonymsSetId;
|
||||||
const result = await deleteSynonymsSet(asCurrentUser, synonymsSetId);
|
const result = await deleteSynonymsSet(asCurrentUser, synonymsSetId);
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: result,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
{
|
||||||
|
path: APIRoutes.SYNONYM_SET_ID,
|
||||||
|
options: {
|
||||||
|
access: 'internal',
|
||||||
|
tags: ['synonyms:read'],
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
authz: {
|
||||||
|
requiredPrivileges: ['synonyms:read'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
params: schema.object({
|
||||||
|
synonymsSetId: schema.string(),
|
||||||
|
}),
|
||||||
|
query: schema.object({
|
||||||
|
from: schema.number({ defaultValue: DEFAULT_PAGE_VALUE.from }),
|
||||||
|
size: schema.number({ defaultValue: DEFAULT_PAGE_VALUE.size }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errorHandler(logger)(async (context, request, response) => {
|
||||||
|
const core = await context.core;
|
||||||
|
const {
|
||||||
|
client: { asCurrentUser },
|
||||||
|
} = core.elasticsearch;
|
||||||
|
const user = core.security.authc.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return response.customError({
|
||||||
|
statusCode: 502,
|
||||||
|
body: 'Could not retrieve current user, security plugin is not ready',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const hasSearchSynonymsPrivilege = await asCurrentUser.security.hasPrivileges({
|
||||||
|
cluster: ['manage_search_synonyms'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasSearchSynonymsPrivilege.has_all_requested) {
|
||||||
|
return response.forbidden({
|
||||||
|
body: "Your user doesn't have manage_search_synonyms privileges",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const synonymsSetId = request.params.synonymsSetId;
|
||||||
|
const result = await fetchSynonymsSet(asCurrentUser, synonymsSetId, {
|
||||||
|
from: request.query.from,
|
||||||
|
size: request.query.size,
|
||||||
|
});
|
||||||
|
return response.ok({
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: result,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
{
|
||||||
|
path: APIRoutes.SYNONYM_SET_ID_RULE_ID,
|
||||||
|
options: {
|
||||||
|
access: 'internal',
|
||||||
|
tags: ['synonyms:read'],
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
authz: {
|
||||||
|
requiredPrivileges: ['synonyms:read'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
params: schema.object({
|
||||||
|
synonymsSetId: schema.string(),
|
||||||
|
ruleId: schema.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errorHandler(logger)(async (context, request, response) => {
|
||||||
|
const core = await context.core;
|
||||||
|
const {
|
||||||
|
client: { asCurrentUser },
|
||||||
|
} = core.elasticsearch;
|
||||||
|
const user = core.security.authc.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return response.customError({
|
||||||
|
statusCode: 502,
|
||||||
|
body: 'Could not retrieve current user, security plugin is not ready',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const hasSearchSynonymsPrivilege = await asCurrentUser.security.hasPrivileges({
|
||||||
|
cluster: ['manage_search_synonyms'],
|
||||||
|
});
|
||||||
|
if (!hasSearchSynonymsPrivilege.has_all_requested) {
|
||||||
|
return response.forbidden({
|
||||||
|
body: "Your user doesn't have manage_search_synonyms privileges",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const synonymsSetId = request.params.synonymsSetId;
|
||||||
|
const ruleId = request.params.ruleId;
|
||||||
|
const result = await fetchSynonymRule(asCurrentUser, synonymsSetId, ruleId);
|
||||||
|
return response.ok({
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: result,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
{
|
||||||
|
path: APIRoutes.SYNONYM_SET_ID_RULE_ID,
|
||||||
|
options: {
|
||||||
|
access: 'internal',
|
||||||
|
tags: ['synonyms:write', 'synonyms:read'],
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
authz: {
|
||||||
|
requiredPrivileges: ['synonyms:write', 'synonyms:read'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
params: schema.object({
|
||||||
|
synonymsSetId: schema.string(),
|
||||||
|
ruleId: schema.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errorHandler(logger)(async (context, request, response) => {
|
||||||
|
const core = await context.core;
|
||||||
|
const {
|
||||||
|
client: { asCurrentUser },
|
||||||
|
} = core.elasticsearch;
|
||||||
|
const user = core.security.authc.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return response.customError({
|
||||||
|
statusCode: 502,
|
||||||
|
body: 'Could not retrieve current user, security plugin is not ready',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const hasSearchSynonymsPrivilege = await asCurrentUser.security.hasPrivileges({
|
||||||
|
cluster: ['manage_search_synonyms'],
|
||||||
|
});
|
||||||
|
if (!hasSearchSynonymsPrivilege.has_all_requested) {
|
||||||
|
return response.forbidden({
|
||||||
|
body: "Your user doesn't have manage_search_synonyms privileges",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const synonymsSetId = request.params.synonymsSetId;
|
||||||
|
const ruleId = request.params.ruleId;
|
||||||
|
const result = await deleteSynonymRule(asCurrentUser, synonymsSetId, ruleId);
|
||||||
|
return response.ok({
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
body: result,
|
body: result,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue