mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Search] [Synonyms] Synonym Rule flyout update (#213433)
## Summary Updates Synonym Rule Flyout to match the designs. https://github.com/user-attachments/assets/8c034c2a-0b12-4a98-a627-fbef3a2542c7 Flyout tries to handle invalid cases which would throw from the endpoint call <img width="497" alt="Screenshot 2025-03-07 at 17 11 51" src="https://github.com/user-attachments/assets/6e610177-ec56-4420-bcee-4c72935cdbb9" /> <img width="495" alt="Screenshot 2025-03-07 at 17 12 07" src="https://github.com/user-attachments/assets/3fed1ed1-4be4-449e-a30c-c8c13e7d7968" /> <img width="509" alt="Screenshot 2025-03-07 at 17 12 33" src="https://github.com/user-attachments/assets/117dbac5-dfbe-4160-a9d4-a92bcb3bcf89" /> <img width="472" alt="Screenshot 2025-03-07 at 17 12 44" src="https://github.com/user-attachments/assets/70d50693-b2bf-4af4-b363-65f92d6812fd" /> <img width="484" alt="Screenshot 2025-03-07 at 17 12 53" src="https://github.com/user-attachments/assets/ebb8f401-4dd6-4180-9028-396680091a4c" /> <img width="458" alt="Screenshot 2025-03-07 at 17 13 27" src="https://github.com/user-attachments/assets/a7c1244b-3334-44d3-bd4c-e26b463e1b68" /> The text added needs a quick check as well cc: @leemthompo ### 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) --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
005124a9ed
commit
c42d763ce4
11 changed files with 1455 additions and 235 deletions
|
@ -46,7 +46,7 @@ export const SearchSynonymsOverview = () => {
|
|||
solutionNav={searchNavigation?.useClassicNavigation(history)}
|
||||
color="primary"
|
||||
>
|
||||
{synonymsData && !isInitialLoading && !isError && (
|
||||
{!isInitialLoading && !isError && synonymsData?._meta.totalItemCount !== 0 && (
|
||||
<KibanaPageTemplate.Header
|
||||
pageTitle="Synonyms"
|
||||
restrictWidth
|
||||
|
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
empty_from_term: i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.invalidTerm', {
|
||||
defaultMessage: 'Term cannot be empty.',
|
||||
}),
|
||||
empty_to_term: i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.invalidMapTo', {
|
||||
defaultMessage: 'Terms cannot be empty.',
|
||||
}),
|
||||
term_exists: i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.termExists', {
|
||||
defaultMessage: 'Term already exists.',
|
||||
}),
|
||||
multiple_explicit_separator: i18n.translate(
|
||||
'xpack.searchSynonyms.synonymsSetRuleFlyout.invalidMapTo',
|
||||
{ defaultMessage: 'Explicit separator "=>" is not allowed in terms.' }
|
||||
),
|
||||
};
|
|
@ -0,0 +1,366 @@
|
|||
/*
|
||||
* 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 { act, fireEvent, getByRole, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SynonymRuleFlyout } from './synonym_rule_flyout';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { usePutSynonymsRule } from '../../hooks/use_put_synonyms_rule';
|
||||
|
||||
jest.mock('../../hooks/use_put_synonyms_rule', () => ({
|
||||
usePutSynonymsRule: jest.fn().mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
describe('SynonymRuleFlyout', () => {
|
||||
const TEST_IDS = {
|
||||
RuleIdText: 'searchSynonymsSynonymRuleFlyoutRuleIdText',
|
||||
ErrorBanner: 'searchSynonymsSynonymsRuleFlyoutErrorBanner',
|
||||
AddFromTermsInput: 'searchSynonymsSynonymsRuleFlyoutFromTermsInput',
|
||||
FromTermCountLabel: 'searchSynonymsSynonymsRuleFlyoutTermCountLabel',
|
||||
FromTermsSortAZButton: 'searchSynonymsSynonymsRuleFlyoutSortAZButton',
|
||||
FromTermsRemoveAllButton: 'searchSynonymsSynonymsRuleFlyoutRemoveAllButton',
|
||||
FromTermBadge: 'searchSynonymsSynonymsRuleFlyoutFromTermBadge',
|
||||
NoTermsText: 'searchSynonymsSynonymsRuleFlyoutNoTermsText',
|
||||
MapToTermsInput: 'searchSynonymsSynonymsRuleFlyoutMapToTermsInput',
|
||||
HasChangesBadge: 'searchSynonymsSynonymsRuleFlyoutHasChangesBadge',
|
||||
ResetChangesButton: 'searchSynonymsSynonymsRuleFlyoutResetChangesButton',
|
||||
SaveChangesButton: 'searchSynonymsSynonymsRuleFlyoutSaveButton',
|
||||
};
|
||||
const ACTIONS = {
|
||||
AddFromTerm: (term: string) => {
|
||||
act(() => {
|
||||
fireEvent.change(getByRole(screen.getByTestId(TEST_IDS.AddFromTermsInput), 'combobox'), {
|
||||
target: { value: term },
|
||||
});
|
||||
fireEvent.keyDown(getByRole(screen.getByTestId(TEST_IDS.AddFromTermsInput), 'combobox'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
});
|
||||
});
|
||||
},
|
||||
AddMapToTerm: (term: string) => {
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId(TEST_IDS.MapToTermsInput), {
|
||||
target: { value: term },
|
||||
});
|
||||
});
|
||||
},
|
||||
PressSaveChangesButton: () => {
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.SaveChangesButton));
|
||||
});
|
||||
},
|
||||
PressSortAZButton: () => {
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.FromTermsSortAZButton));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const onCloseMock = jest.fn();
|
||||
const mutateMock = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(usePutSynonymsRule as jest.Mock).mockReturnValue({
|
||||
mutate: mutateMock,
|
||||
});
|
||||
});
|
||||
describe('create mode', () => {
|
||||
it('should render the flyout for equivalent synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'create'}
|
||||
synonymsRule={{
|
||||
id: 'generated-id',
|
||||
synonyms: '',
|
||||
}}
|
||||
renderExplicit={false}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
// Header
|
||||
expect(screen.queryByTestId(TEST_IDS.RuleIdText)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TEST_IDS.ErrorBanner)).not.toBeInTheDocument();
|
||||
|
||||
// From terms
|
||||
expect(screen.getByTestId(TEST_IDS.NoTermsText).textContent).toBe('No terms found.');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('0 term');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsSortAZButton)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsRemoveAllButton)).toBeInTheDocument();
|
||||
|
||||
// Map to terms and bottom elements
|
||||
expect(screen.queryByTestId(TEST_IDS.MapToTermsInput)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TEST_IDS.HasChangesBadge)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.ResetChangesButton)).toBeDisabled();
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
});
|
||||
it('should render the flyout for explicit synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'create'}
|
||||
synonymsRule={{
|
||||
id: 'generated-id',
|
||||
synonyms: '',
|
||||
}}
|
||||
renderExplicit={true}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
// Header
|
||||
expect(screen.queryByTestId(TEST_IDS.RuleIdText)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TEST_IDS.ErrorBanner)).not.toBeInTheDocument();
|
||||
|
||||
// From terms
|
||||
expect(screen.getByTestId(TEST_IDS.NoTermsText).textContent).toBe('No terms found.');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('0 term');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsSortAZButton)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsRemoveAllButton)).toBeInTheDocument();
|
||||
|
||||
// Map to terms and bottom elements
|
||||
expect(screen.getByTestId(TEST_IDS.MapToTermsInput)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TEST_IDS.HasChangesBadge)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.ResetChangesButton)).toBeDisabled();
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should call backend with correct payload for equivalent synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'create'}
|
||||
synonymsRule={{
|
||||
id: 'generated-id',
|
||||
synonyms: '',
|
||||
}}
|
||||
renderExplicit={false}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
|
||||
ACTIONS.AddFromTerm('from1');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('1 term');
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeEnabled();
|
||||
|
||||
ACTIONS.AddFromTerm('from2');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('2 terms');
|
||||
|
||||
ACTIONS.PressSaveChangesButton();
|
||||
expect(mutateMock).toHaveBeenCalledWith({
|
||||
synonymsSetId: 'my_synonyms_set',
|
||||
ruleId: 'generated-id',
|
||||
synonyms: 'from1,from2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call backend with correct payload for explicit synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'create'}
|
||||
synonymsRule={{
|
||||
id: 'generated-id',
|
||||
synonyms: '',
|
||||
}}
|
||||
renderExplicit={true}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
ACTIONS.AddFromTerm('from1');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('1 term');
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
|
||||
ACTIONS.AddMapToTerm('to1');
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeEnabled();
|
||||
ACTIONS.PressSaveChangesButton();
|
||||
|
||||
expect(mutateMock).toHaveBeenCalledWith({
|
||||
synonymsSetId: 'my_synonyms_set',
|
||||
ruleId: 'generated-id',
|
||||
synonyms: 'from1 => to1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort items in the flyout', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'create'}
|
||||
synonymsRule={{
|
||||
id: 'generated-id',
|
||||
synonyms: '',
|
||||
}}
|
||||
renderExplicit={false}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
ACTIONS.AddFromTerm('a');
|
||||
ACTIONS.AddFromTerm('b');
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('2 terms');
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[0].textContent?.trim()).toBe('a');
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[1].textContent?.trim()).toBe('b');
|
||||
ACTIONS.PressSortAZButton();
|
||||
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[0].textContent?.trim()).toBe('b');
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[1].textContent?.trim()).toBe('a');
|
||||
ACTIONS.PressSortAZButton();
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[0].textContent?.trim()).toBe('a');
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[1].textContent?.trim()).toBe('b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit mode', () => {
|
||||
it('should render the flyout for equivalent synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'edit'}
|
||||
synonymsRule={{
|
||||
id: 'rule_id_3',
|
||||
synonyms: 'synonym1,synonym2',
|
||||
}}
|
||||
renderExplicit={false}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
// Header
|
||||
expect(screen.getByTestId(TEST_IDS.RuleIdText).textContent).toBe('Rule ID: rule_id_3');
|
||||
expect(screen.queryByTestId(TEST_IDS.ErrorBanner)).not.toBeInTheDocument();
|
||||
|
||||
// From terms
|
||||
expect(screen.queryByTestId(TEST_IDS.NoTermsText)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('2 terms');
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[0].textContent?.trim()).toBe('synonym1');
|
||||
expect(screen.getAllByTestId(TEST_IDS.FromTermBadge)[1].textContent?.trim()).toBe('synonym2');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsSortAZButton)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsRemoveAllButton)).toBeInTheDocument();
|
||||
|
||||
// Map to terms and bottom elements
|
||||
expect(screen.queryByTestId(TEST_IDS.MapToTermsInput)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TEST_IDS.HasChangesBadge)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.ResetChangesButton)).toBeDisabled();
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should call backend with correct payload for equivalent synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'edit'}
|
||||
synonymsRule={{
|
||||
id: 'rule_id_3',
|
||||
synonyms: 'synonym1,synonym2',
|
||||
}}
|
||||
renderExplicit={false}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
ACTIONS.AddFromTerm('synonym3');
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeEnabled();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('3 terms');
|
||||
|
||||
ACTIONS.PressSaveChangesButton();
|
||||
expect(mutateMock).toHaveBeenCalledWith({
|
||||
synonymsSetId: 'my_synonyms_set',
|
||||
ruleId: 'rule_id_3',
|
||||
synonyms: 'synonym1,synonym2,synonym3',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the flyout for explicit synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'edit'}
|
||||
synonymsRule={{
|
||||
id: 'rule_id_3',
|
||||
synonyms: 'explicit-from => explicit-to',
|
||||
}}
|
||||
renderExplicit={true}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
// Header
|
||||
expect(screen.getByTestId(TEST_IDS.RuleIdText).textContent).toBe('Rule ID: rule_id_3');
|
||||
expect(screen.queryByTestId(TEST_IDS.ErrorBanner)).not.toBeInTheDocument();
|
||||
|
||||
// From terms
|
||||
expect(screen.queryByTestId(TEST_IDS.NoTermsText)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermCountLabel).textContent).toBe('1 term');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermBadge).textContent?.trim()).toBe('explicit-from');
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsSortAZButton)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.FromTermsRemoveAllButton)).toBeInTheDocument();
|
||||
|
||||
// Map to terms and bottom elements
|
||||
expect(screen.getByTestId(TEST_IDS.MapToTermsInput)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.MapToTermsInput)).toHaveValue('explicit-to');
|
||||
expect(screen.queryByTestId(TEST_IDS.HasChangesBadge)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TEST_IDS.ResetChangesButton)).toBeDisabled();
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
});
|
||||
it('should call backend with correct payload for explicit synonyms', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<SynonymRuleFlyout
|
||||
onClose={onCloseMock}
|
||||
flyoutMode={'edit'}
|
||||
synonymsRule={{
|
||||
id: 'rule_id_3',
|
||||
synonyms: 'explicit-from => explicit-to',
|
||||
}}
|
||||
renderExplicit={true}
|
||||
synonymsSetId="my_synonyms_set"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeDisabled();
|
||||
ACTIONS.AddMapToTerm('explicit-to-2');
|
||||
expect(screen.getByTestId(TEST_IDS.SaveChangesButton)).toBeEnabled();
|
||||
ACTIONS.PressSaveChangesButton();
|
||||
|
||||
expect(mutateMock).toHaveBeenCalledWith({
|
||||
synonymsSetId: 'my_synonyms_set',
|
||||
ruleId: 'rule_id_3',
|
||||
synonyms: 'explicit-from => explicit-to-2',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,357 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiComboBox,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
EuiHealth,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { synonymsOptionToString } from '../../utils/synonyms_utils';
|
||||
import { usePutSynonymsRule } from '../../hooks/use_put_synonyms_rule';
|
||||
import { useSynonymRuleFlyoutState } from './use_flyout_state';
|
||||
|
||||
interface SynonymRuleFlyoutProps {
|
||||
onClose: () => void;
|
||||
flyoutMode: 'create' | 'edit';
|
||||
synonymsRule: SynonymsSynonymRule;
|
||||
synonymsSetId: string;
|
||||
renderExplicit?: boolean;
|
||||
}
|
||||
|
||||
export const SynonymRuleFlyout: React.FC<SynonymRuleFlyoutProps> = ({
|
||||
flyoutMode,
|
||||
synonymsRule,
|
||||
onClose,
|
||||
renderExplicit = false,
|
||||
synonymsSetId,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [backendError, setBackendError] = React.useState<string | null>(null);
|
||||
const { mutate: putSynonymsRule } = usePutSynonymsRule(
|
||||
() => onClose(),
|
||||
(error) => {
|
||||
setBackendError(error);
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
canSave,
|
||||
currentSortDirection,
|
||||
fromTermErrors,
|
||||
fromTerms,
|
||||
hasChanges,
|
||||
isExplicit,
|
||||
isFromTermsInvalid,
|
||||
isMapToTermsInvalid,
|
||||
mapToTermErrors,
|
||||
mapToTerms,
|
||||
clearFromTerms,
|
||||
onCreateOption,
|
||||
onMapToChange,
|
||||
onSearchChange,
|
||||
onSortTerms,
|
||||
removeTermFromOptions,
|
||||
resetChanges,
|
||||
} = useSynonymRuleFlyoutState({
|
||||
synonymRule: synonymsRule,
|
||||
flyoutMode,
|
||||
renderExplicit,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size={'33%'} outsideClickCloses={false}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiText data-test-subj="searchSynonymsSynonymRuleFlyoutRuleIdText">
|
||||
<b>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.title.ruleId', {
|
||||
defaultMessage: 'Rule ID: {ruleId}',
|
||||
values: { ruleId: synonymsRule.id },
|
||||
})}
|
||||
</b>
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody
|
||||
css={css`
|
||||
.euiFlyoutBody__overflowContent {
|
||||
height: 100%;
|
||||
}
|
||||
`}
|
||||
banner={
|
||||
backendError && (
|
||||
<EuiCallOut
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutErrorBanner"
|
||||
color="danger"
|
||||
title={i18n.translate(
|
||||
'xpack.searchSynonyms.synonymsSetRuleFlyout.errorCallout.title',
|
||||
{
|
||||
defaultMessage: 'An error occured while saving your changes',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{backendError}
|
||||
</EuiCallOut>
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.synonyms', {
|
||||
defaultMessage: 'Add terms to match against',
|
||||
})}
|
||||
isInvalid={isFromTermsInvalid}
|
||||
error={fromTermErrors || null}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutFromTermsInput"
|
||||
isInvalid={isFromTermsInvalid}
|
||||
noSuggestions
|
||||
placeholder={i18n.translate(
|
||||
'xpack.searchSynonyms.synonymsSetRuleFlyout.synonyms.inputPlaceholder',
|
||||
{ defaultMessage: 'Add terms to match against' }
|
||||
)}
|
||||
onCreateOption={onCreateOption}
|
||||
delimiter=","
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
size="s"
|
||||
color="subdued"
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutTermCountLabel"
|
||||
>
|
||||
<p>
|
||||
{fromTerms.length <= 1 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.searchSynonyms.synonymsSetRuleFlyout.synonymsCount.single"
|
||||
defaultMessage="{count} term"
|
||||
values={{ count: fromTerms.length }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.searchSynonyms.synonymsSetRuleFlyout.synonymsCount.multiple"
|
||||
defaultMessage="{count} terms"
|
||||
values={{ count: fromTerms.length }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutSortAZButton"
|
||||
size="s"
|
||||
color="text"
|
||||
onClick={() => onSortTerms()}
|
||||
iconType={currentSortDirection === 'ascending' ? 'sortUp' : 'sortDown'}
|
||||
>
|
||||
{currentSortDirection === 'ascending' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.searchSynonyms.synonymsSetRuleFlyout.sortAZ"
|
||||
defaultMessage="Sort A-Z"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.searchSynonyms.synonymsSetRuleFlyout.sortZA"
|
||||
defaultMessage="Sort Z-A"
|
||||
/>
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutRemoveAllButton"
|
||||
color="danger"
|
||||
size="s"
|
||||
onClick={clearFromTerms}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchSynonyms.synonymsSetRuleFlyout.clearAll"
|
||||
defaultMessage="Remove all"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
max-height: ${isExplicit ? '75%' : '90%'};
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="xs"
|
||||
tabIndex={0}
|
||||
className="eui-yScrollWithShadows"
|
||||
css={css`
|
||||
margin: ${euiTheme.size.xs};
|
||||
`}
|
||||
>
|
||||
<EuiSpacer size="xs" />
|
||||
{fromTerms.map((opt, index) => (
|
||||
<span key={index + '-' + opt.label.trim()}>
|
||||
<EuiBadge
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutFromTermBadge"
|
||||
color="hollow"
|
||||
iconSide="left"
|
||||
iconType="cross"
|
||||
iconOnClick={() => {
|
||||
removeTermFromOptions(opt);
|
||||
}}
|
||||
iconOnClickAriaLabel="remove"
|
||||
>
|
||||
|
||||
{opt.label}
|
||||
</EuiBadge>
|
||||
</span>
|
||||
))}
|
||||
{fromTerms.length === 0 && (
|
||||
<EuiText
|
||||
color="subdued"
|
||||
textAlign="center"
|
||||
size="s"
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutNoTermsText"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchSynonyms.synonymsSetRuleFlyout.noTerms"
|
||||
defaultMessage="No terms found."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
{isExplicit && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.synonymsTo', {
|
||||
defaultMessage: 'Map to this term',
|
||||
})}
|
||||
isInvalid={mapToTerms !== '' && isMapToTermsInvalid}
|
||||
error={mapToTermErrors || null}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutMapToTermsInput"
|
||||
fullWidth
|
||||
value={mapToTerms}
|
||||
isInvalid={mapToTerms !== '' && isMapToTermsInvalid}
|
||||
onChange={(e) => {
|
||||
onMapToChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{hasChanges && (
|
||||
<EuiHealth
|
||||
color="primary"
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutHasChangesBadge"
|
||||
>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.unsavedChanges', {
|
||||
defaultMessage: 'Synonym rule has unsaved changes',
|
||||
})}
|
||||
</EuiHealth>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutResetChangesButton"
|
||||
iconType="refresh"
|
||||
disabled={!hasChanges}
|
||||
onClick={resetChanges}
|
||||
>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.reset', {
|
||||
defaultMessage: 'Reset changes',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutSaveButton"
|
||||
fill
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (!synonymsRule.id) {
|
||||
return;
|
||||
}
|
||||
putSynonymsRule({
|
||||
synonymsSetId,
|
||||
ruleId: synonymsRule.id,
|
||||
synonyms: synonymsOptionToString({
|
||||
fromTerms,
|
||||
toTerms: mapToTerms,
|
||||
isExplicit,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.save', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
* 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 { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
describe('useSynonymRuleFlyoutState hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = new QueryClient();
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
};
|
||||
|
||||
describe('hasChanges', () => {
|
||||
describe('create mode', () => {
|
||||
describe('equivalent terms', () => {
|
||||
it('should be false by default in create mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when fromTerms has changes in create mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onCreateOption } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('explicit terms', () => {
|
||||
it('should be false by default in create mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when fromTerms has changes ', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onCreateOption } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be true when mapToTerms has changes', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onMapToChange } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('edit mode', () => {
|
||||
describe('equivalent terms', () => {
|
||||
it('should be true when fromTerms has changes', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: 'synonym1,synonym2',
|
||||
},
|
||||
flyoutMode: 'edit',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onCreateOption } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('explicit terms', () => {
|
||||
it('should be true when mapToTerms has changes', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: 'synonym1 => synonym2',
|
||||
},
|
||||
flyoutMode: 'edit',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onMapToChange } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset changes', () => {
|
||||
it('should reset changes in equivalent when in edit mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: 'synonym1,synonym2',
|
||||
},
|
||||
flyoutMode: 'edit',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onCreateOption, resetChanges } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
expect(result.current.fromTerms).toEqual([
|
||||
expect.objectContaining({ label: 'synonym1' }),
|
||||
expect.objectContaining({ label: 'synonym2' }),
|
||||
expect.objectContaining({ label: 'test' }),
|
||||
]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
resetChanges();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
expect(result.current.fromTerms).toEqual([
|
||||
expect.objectContaining({ label: 'synonym1' }),
|
||||
expect.objectContaining({ label: 'synonym2' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset changes in explicit when in edit mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: 'synonym1 => synonym2',
|
||||
},
|
||||
flyoutMode: 'edit',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onMapToChange, resetChanges } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
expect(result.current.mapToTerms).toBe('test');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
resetChanges();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
expect(result.current.mapToTerms).toBe('synonym2');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset changes in equivalent when in create mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onCreateOption, resetChanges } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('test');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
expect(result.current.fromTerms).toEqual([expect.objectContaining({ label: 'test' })]);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
resetChanges();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
expect(result.current.fromTerms).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset changes in explicit when in create mode', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
});
|
||||
const { onMapToChange, resetChanges, onCreateOption } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('test');
|
||||
onCreateOption('from');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(true);
|
||||
expect(result.current.fromTerms).toEqual([expect.objectContaining({ label: 'from' })]);
|
||||
expect(result.current.mapToTerms).toBe('test');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
resetChanges();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasChanges).toBe(false);
|
||||
expect(result.current.fromTerms).toEqual([]);
|
||||
expect(result.current.mapToTerms).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromTerms validation', () => {
|
||||
it('should be invalid when fromTerms has multiple explicit separators', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const { onCreateOption, canSave } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('from => term => term');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isFromTermsInvalid).toBe(true);
|
||||
expect(result.current.fromTermErrors).toEqual([
|
||||
'Explicit separator "=>" is not allowed in terms.',
|
||||
]);
|
||||
expect(canSave).toBe(false);
|
||||
});
|
||||
});
|
||||
it('should be invalid when search term exist in fromTerms', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: 'search',
|
||||
},
|
||||
flyoutMode: 'edit',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const { onCreateOption, canSave } = result.current;
|
||||
act(() => {
|
||||
onCreateOption('search');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isFromTermsInvalid).toBe(true);
|
||||
expect(result.current.fromTermErrors).toEqual(['Term already exists.']);
|
||||
expect(canSave).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapToTerms validation', () => {
|
||||
it('shoud be invalid when mapToTerms is empty', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const { onMapToChange, canSave } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isMapToTermsInvalid).toBe(true);
|
||||
expect(result.current.mapToTermErrors).toEqual(['Terms cannot be empty.']);
|
||||
expect(canSave).toBe(false);
|
||||
});
|
||||
});
|
||||
it('should be invalid when mapToTerms has explicit separators', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: '',
|
||||
},
|
||||
flyoutMode: 'create',
|
||||
renderExplicit: true,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const { onMapToChange, canSave } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('from => term => term');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isMapToTermsInvalid).toBe(true);
|
||||
expect(result.current.mapToTermErrors).toEqual([
|
||||
'Explicit separator "=>" is not allowed in terms.',
|
||||
]);
|
||||
expect(canSave).toBe(false);
|
||||
});
|
||||
});
|
||||
it('should be invalid when mapToTerms has empty values', async () => {
|
||||
const { useSynonymRuleFlyoutState } = jest.requireActual('./use_flyout_state');
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSynonymRuleFlyoutState({
|
||||
synonymRule: {
|
||||
synonyms: 'test => thing',
|
||||
},
|
||||
flyoutMode: 'edit',
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const { onMapToChange, canSave } = result.current;
|
||||
act(() => {
|
||||
onMapToChange('from,,term');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isMapToTermsInvalid).toBe(true);
|
||||
expect(result.current.mapToTermErrors).toEqual(['Terms cannot be empty.']);
|
||||
expect(canSave).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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 { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { useState } from 'react';
|
||||
import { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { synonymToComboBoxOption, synonymsOptionToString } from '../../utils/synonyms_utils';
|
||||
import { ERROR_MESSAGES } from './constants';
|
||||
|
||||
export interface InitialFlyoutState {
|
||||
synonymRule: SynonymsSynonymRule;
|
||||
flyoutMode: 'create' | 'edit';
|
||||
renderExplicit?: boolean;
|
||||
}
|
||||
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
export const useSynonymRuleFlyoutState = ({
|
||||
synonymRule,
|
||||
flyoutMode,
|
||||
renderExplicit = false,
|
||||
}: InitialFlyoutState) => {
|
||||
const { parsedFromTerms, parsedToTermsString, parsedIsExplicit } = synonymToComboBoxOption(
|
||||
flyoutMode === 'create' ? '' : synonymRule.synonyms
|
||||
);
|
||||
const sortedParsedFromTerms = [...parsedFromTerms].sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const isExplicit = renderExplicit || parsedIsExplicit;
|
||||
|
||||
const [fromTerms, setFromTerms] = useState<EuiComboBoxOptionOption[]>(sortedParsedFromTerms);
|
||||
|
||||
const [mapToTerms, setMapToTerms] = useState<string>(parsedToTermsString);
|
||||
const [isFromTermsInvalid, setIsFromTermsInvalid] = useState(false);
|
||||
const [isMapToTermsInvalid, setIsMapToTermsInvalid] = useState(false);
|
||||
const [fromTermErrors, setFromTermErrors] = useState<string[]>([]);
|
||||
const [mapToTermErrors, setMapToTermErrors] = useState<string[]>([]);
|
||||
const [currentSortDirection, setCurrentSortDirection] = useState<SortDirection>('ascending');
|
||||
|
||||
const hasChanges =
|
||||
flyoutMode === 'create'
|
||||
? (fromTerms.length !== 0 || mapToTerms.length !== 0) &&
|
||||
synonymsOptionToString({
|
||||
fromTerms,
|
||||
toTerms: mapToTerms,
|
||||
isExplicit,
|
||||
}) !== synonymRule.synonyms
|
||||
: synonymsOptionToString({
|
||||
fromTerms,
|
||||
toTerms: mapToTerms,
|
||||
isExplicit,
|
||||
}) !== synonymRule.synonyms;
|
||||
|
||||
const canSave =
|
||||
fromTerms.length > 0 &&
|
||||
!(isExplicit && !mapToTerms) &&
|
||||
hasChanges &&
|
||||
!isFromTermsInvalid &&
|
||||
!isMapToTermsInvalid;
|
||||
|
||||
const resetChanges = () => {
|
||||
setFromTerms(parsedFromTerms);
|
||||
setMapToTerms(parsedToTermsString);
|
||||
setIsFromTermsInvalid(false);
|
||||
setIsMapToTermsInvalid(false);
|
||||
setFromTermErrors([]);
|
||||
setMapToTermErrors([]);
|
||||
};
|
||||
|
||||
const isValid = (value: string) => {
|
||||
const trimmedValue = value.trim();
|
||||
if (value !== '' && trimmedValue === '') {
|
||||
setIsFromTermsInvalid(true);
|
||||
setFromTermErrors([ERROR_MESSAGES.empty_from_term]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isExplicit && trimmedValue.includes('=>')) {
|
||||
setIsFromTermsInvalid(true);
|
||||
setFromTermErrors([ERROR_MESSAGES.multiple_explicit_separator]);
|
||||
return false;
|
||||
}
|
||||
const exists = fromTerms.find((term) => term.label === value);
|
||||
if (exists !== undefined) {
|
||||
setFromTermErrors([ERROR_MESSAGES.term_exists]);
|
||||
setIsFromTermsInvalid(true);
|
||||
return false;
|
||||
} else {
|
||||
setIsFromTermsInvalid(false);
|
||||
setFromTermErrors([]);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const isMapToValid = (value: string) => {
|
||||
const trimmedValue = value.trim();
|
||||
if (value !== '' && trimmedValue === '') {
|
||||
setIsMapToTermsInvalid(true);
|
||||
setMapToTermErrors([ERROR_MESSAGES.empty_to_term]);
|
||||
return false;
|
||||
}
|
||||
if (trimmedValue.includes('=>')) {
|
||||
setIsMapToTermsInvalid(true);
|
||||
setMapToTermErrors([ERROR_MESSAGES.multiple_explicit_separator]);
|
||||
return false;
|
||||
}
|
||||
if (trimmedValue.split(',').some((term) => term.trim() === '')) {
|
||||
setIsMapToTermsInvalid(true);
|
||||
setMapToTermErrors([ERROR_MESSAGES.empty_to_term]);
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsMapToTermsInvalid(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
const onSortTerms = (direction?: SortDirection) => {
|
||||
if (!direction) {
|
||||
direction = currentSortDirection === 'ascending' ? 'descending' : 'ascending';
|
||||
}
|
||||
fromTerms.sort((a, b) =>
|
||||
direction === 'ascending' ? a.label.localeCompare(b.label) : b.label.localeCompare(a.label)
|
||||
);
|
||||
setCurrentSortDirection(direction);
|
||||
setFromTerms([...fromTerms]);
|
||||
};
|
||||
|
||||
const onSearchChange = (searchValue: string) => {
|
||||
if (!searchValue) {
|
||||
setIsFromTermsInvalid(false);
|
||||
setFromTermErrors([]);
|
||||
return;
|
||||
}
|
||||
setIsFromTermsInvalid(!isValid(searchValue));
|
||||
};
|
||||
|
||||
const onCreateOption = (searchValue: string) => {
|
||||
if (searchValue.trim() === '') {
|
||||
return;
|
||||
}
|
||||
if (!isValid(searchValue)) {
|
||||
return false;
|
||||
}
|
||||
setFromTerms([...fromTerms, { label: searchValue, key: searchValue }]);
|
||||
};
|
||||
|
||||
const removeTermFromOptions = (term: EuiComboBoxOptionOption) => {
|
||||
setFromTerms(fromTerms.filter((t) => t.label !== term.label));
|
||||
};
|
||||
|
||||
const clearFromTerms = () => setFromTerms([]);
|
||||
const onMapToChange = (value: string) => {
|
||||
isMapToValid(value);
|
||||
setMapToTerms(value);
|
||||
};
|
||||
|
||||
return {
|
||||
canSave,
|
||||
clearFromTerms,
|
||||
currentSortDirection,
|
||||
fromTermErrors,
|
||||
fromTerms,
|
||||
hasChanges,
|
||||
isExplicit,
|
||||
isFromTermsInvalid,
|
||||
isMapToTermsInvalid,
|
||||
mapToTermErrors,
|
||||
mapToTerms,
|
||||
onCreateOption,
|
||||
onMapToChange,
|
||||
onSearchChange,
|
||||
onSortTerms,
|
||||
removeTermFromOptions,
|
||||
resetChanges,
|
||||
};
|
||||
};
|
|
@ -1,215 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
EuiHealth,
|
||||
EuiText,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SynonymsSynonymRule } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
getExplicitSynonym,
|
||||
isExplicitSynonym,
|
||||
synonymsOptionToString,
|
||||
synonymsStringToOption,
|
||||
} from '../../utils/synonyms_utils';
|
||||
import { usePutSynonymsRule } from '../../hooks/use_put_synonyms_rule';
|
||||
|
||||
interface SynonymsRuleFlyoutProps {
|
||||
onClose: () => void;
|
||||
flyoutMode: 'create' | 'edit';
|
||||
synonymsRule: SynonymsSynonymRule;
|
||||
synonymsSetId: string;
|
||||
renderExplicit?: boolean;
|
||||
}
|
||||
|
||||
export const SynonymsRuleFlyout: React.FC<SynonymsRuleFlyoutProps> = ({
|
||||
flyoutMode,
|
||||
synonymsRule,
|
||||
onClose,
|
||||
renderExplicit = false,
|
||||
synonymsSetId,
|
||||
}) => {
|
||||
const flyoutHeadingId = useGeneratedHtmlId({ prefix: 'synonymsRuleFlyoutHeading' });
|
||||
|
||||
const { mutate: putSynonymsRule } = usePutSynonymsRule(() => onClose());
|
||||
|
||||
const synonyms = synonymsRule.synonyms.trim();
|
||||
const isExplicit = renderExplicit || isExplicitSynonym(synonyms);
|
||||
|
||||
const [from, to] =
|
||||
flyoutMode === 'create' ? ['', ''] : isExplicit ? getExplicitSynonym(synonyms) : [synonyms, ''];
|
||||
|
||||
const [selectedFromTerms, setSelectedFromTerms] = useState<EuiComboBoxOptionOption[]>(
|
||||
synonymsStringToOption(from)
|
||||
);
|
||||
|
||||
const [selectedToTerms, setSelectedToTerms] = useState<EuiComboBoxOptionOption[]>(
|
||||
synonymsStringToOption(to)
|
||||
);
|
||||
|
||||
const hasChanges =
|
||||
synonyms.trim() !==
|
||||
synonymsOptionToString({ fromTerms: selectedFromTerms, toTerms: selectedToTerms, isExplicit });
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size="s">
|
||||
<EuiFlyoutHeader hasBorder aria-labelledby={flyoutHeadingId}>
|
||||
{flyoutMode === 'edit' ? (
|
||||
<EuiText>
|
||||
<b>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.title.ruleId', {
|
||||
defaultMessage: 'Rule ID: {ruleId}',
|
||||
values: { ruleId: synonymsRule.id },
|
||||
})}
|
||||
</b>
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.title.ruleId', {
|
||||
defaultMessage: 'Rule ID',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutFieldText"
|
||||
value={synonymsRule.id}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.synonyms', {
|
||||
defaultMessage: 'Add terms to match against',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
title="Synonyms"
|
||||
id="synonyms"
|
||||
options={selectedFromTerms}
|
||||
selectedOptions={selectedFromTerms}
|
||||
onChange={(options) => {
|
||||
setSelectedFromTerms(options);
|
||||
}}
|
||||
onCreateOption={(searchValue, options = []) => {
|
||||
if (!searchValue.trim()) {
|
||||
return;
|
||||
}
|
||||
if (!options.find((option) => option.label.trim() === searchValue.toLowerCase())) {
|
||||
setSelectedFromTerms([
|
||||
...selectedFromTerms,
|
||||
{ label: searchValue, key: searchValue },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{isExplicit && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.synonymsTo', {
|
||||
defaultMessage: 'Map to this term',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
title="Synonyms"
|
||||
id="synonyms-to"
|
||||
options={selectedToTerms}
|
||||
selectedOptions={selectedToTerms}
|
||||
onChange={(options) => {
|
||||
setSelectedToTerms(options);
|
||||
}}
|
||||
onCreateOption={(searchValue, options = []) => {
|
||||
if (!searchValue.trim()) {
|
||||
return;
|
||||
}
|
||||
if (!options.find((option) => option.label.trim() === searchValue.toLowerCase())) {
|
||||
setSelectedToTerms([
|
||||
...selectedToTerms,
|
||||
{ label: searchValue, key: searchValue },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{hasChanges && (
|
||||
<EuiHealth color="primary">
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.unsavedChanges', {
|
||||
defaultMessage: 'Synonym rule has unsaved changes',
|
||||
})}
|
||||
</EuiHealth>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutResetChangesButton"
|
||||
iconType={'refresh'}
|
||||
disabled={!hasChanges}
|
||||
onClick={() => {
|
||||
setSelectedFromTerms(synonymsStringToOption(from));
|
||||
setSelectedToTerms(synonymsStringToOption(to));
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.reset', {
|
||||
defaultMessage: 'Reset changes',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="searchSynonymsSynonymsRuleFlyoutSaveButton"
|
||||
fill
|
||||
onClick={() => {
|
||||
if (!synonymsRule.id) {
|
||||
return;
|
||||
}
|
||||
putSynonymsRule({
|
||||
synonymsSetId,
|
||||
ruleId: synonymsRule.id,
|
||||
synonyms: synonymsOptionToString({
|
||||
fromTerms: selectedFromTerms,
|
||||
toTerms: selectedToTerms,
|
||||
isExplicit,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.searchSynonyms.synonymsSetRuleFlyout.save', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { SynonymsSetRuleTable } from './synonyms_set_rule_table';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
jest.mock('../../hooks/use_fetch_synonyms_set', () => ({
|
||||
useFetchSynonymsSet: () => ({
|
||||
|
@ -64,7 +65,11 @@ jest.mock('../../hooks/use_put_synonyms_rule', () => ({
|
|||
|
||||
describe('SynonymSetDetail table', () => {
|
||||
it('should render the list with synonym rules', () => {
|
||||
render(<SynonymsSetRuleTable synonymsSetId="synonymSetId" />);
|
||||
render(
|
||||
<I18nProvider>
|
||||
<SynonymsSetRuleTable synonymsSetId="synonymSetId" />
|
||||
</I18nProvider>
|
||||
);
|
||||
const synonymSetTable = screen.getByTestId('synonyms-set-table');
|
||||
expect(synonymSetTable).toBeInTheDocument();
|
||||
|
||||
|
|
|
@ -27,9 +27,9 @@ import { getExplicitSynonym, isExplicitSynonym } from '../../utils/synonyms_util
|
|||
import { DeleteSynonymRuleModal } from './delete_synonym_rule_modal';
|
||||
import { SynonymsSetEmptyRuleTable } from './empty_rules_table';
|
||||
import { SynonymsSetEmptyRulesCards } from './empty_rules_cards';
|
||||
import { SynonymsRuleFlyout } from './synonyms_set_rule_flyout';
|
||||
import { useFetchSynonymRule } from '../../hooks/use_fetch_synonym_rule';
|
||||
import { useFetchGeneratedRuleId } from '../../hooks/use_fetch_generated_rule_id';
|
||||
import { SynonymRuleFlyout } from '../synonyms_rule_flyout/synonym_rule_flyout';
|
||||
|
||||
export const SynonymsSetRuleTable = ({ synonymsSetId = '' }: { synonymsSetId: string }) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
|
@ -42,7 +42,7 @@ export const SynonymsSetRuleTable = ({ synonymsSetId = '' }: { synonymsSetId: st
|
|||
});
|
||||
const [addNewRulePopoverOpen, setAddNewRulePopoverOpen] = useState(false);
|
||||
|
||||
const [isRuleFlyoutOpen, setIsRuleFlyoutOpen] = useState(false);
|
||||
const [isRuleFlyoutOpen, setIsRuleFlyoutOpen] = useState(true);
|
||||
const [synonymsRuleToEdit, setSynonymsRuleToEdit] = useState<string | null>(null);
|
||||
const [generatedId, setGeneratedId] = useState<string | null>(null);
|
||||
const { data: synonymsRule } = useFetchSynonymRule(synonymsSetId, synonymsRuleToEdit || '');
|
||||
|
@ -74,7 +74,9 @@ export const SynonymsSetRuleTable = ({ synonymsSetId = '' }: { synonymsSetId: st
|
|||
}),
|
||||
render: (synonyms: string, synonymRule: SynonymsSynonymRule) => {
|
||||
const isExplicit = isExplicitSynonym(synonyms);
|
||||
const [explicitFrom = '', explicitTo = ''] = isExplicit ? getExplicitSynonym(synonyms) : [];
|
||||
const { mapFromString: explicitFrom = '', mapToString: explicitTo = '' } = isExplicit
|
||||
? getExplicitSynonym(synonyms)
|
||||
: {};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup responsive={false}>
|
||||
|
@ -187,7 +189,7 @@ export const SynonymsSetRuleTable = ({ synonymsSetId = '' }: { synonymsSetId: st
|
|||
)}
|
||||
|
||||
{isRuleFlyoutOpen && generatedId ? (
|
||||
<SynonymsRuleFlyout
|
||||
<SynonymRuleFlyout
|
||||
synonymsSetId={synonymsSetId}
|
||||
onClose={() => {
|
||||
setIsRuleFlyoutOpen(false);
|
||||
|
@ -201,7 +203,7 @@ export const SynonymsSetRuleTable = ({ synonymsSetId = '' }: { synonymsSetId: st
|
|||
/>
|
||||
) : (
|
||||
synonymsRule && (
|
||||
<SynonymsRuleFlyout
|
||||
<SynonymRuleFlyout
|
||||
synonymsSetId={synonymsSetId}
|
||||
onClose={() => {
|
||||
setIsRuleFlyoutOpen(false);
|
||||
|
|
|
@ -24,13 +24,16 @@ describe('isExplicitSynonym util function', () => {
|
|||
|
||||
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')).toEqual({
|
||||
mapFromString: 'synonym1',
|
||||
mapToString: 'synonym2',
|
||||
});
|
||||
expect(getExplicitSynonym('synonym1,synonym2, synonym5 => synonym2')).toEqual({
|
||||
mapFromString: 'synonym1,synonym2, synonym5',
|
||||
mapToString: 'synonym2',
|
||||
});
|
||||
expect(
|
||||
getExplicitSynonym(' synonym1,synonym2, synonym5 => synonym2 ')
|
||||
).toEqual(['synonym1,synonym2, synonym5', 'synonym2']);
|
||||
).toEqual({ mapFromString: 'synonym1,synonym2, synonym5', mapToString: 'synonym2' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,10 @@ export const isExplicitSynonym = (synonym: string) => {
|
|||
};
|
||||
|
||||
export const getExplicitSynonym = (synonym: string) => {
|
||||
return [synonym.split('=>')[0].trim(), synonym.split('=>')[1].trim()];
|
||||
return {
|
||||
mapFromString: synonym.split('=>')[0].trim(),
|
||||
mapToString: synonym.split('=>')[1].trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const formatSynonymsSetName = (rawName: string) =>
|
||||
|
@ -23,25 +26,46 @@ export const formatSynonymsSetName = (rawName: string) =>
|
|||
.replace(/^[-]+|[-]+$/g, '') // Strip all leading and trailing dashes
|
||||
.toLowerCase();
|
||||
|
||||
type SynonymsToComboBoxOption = (synonymString: string) => {
|
||||
parsedFromTerms: EuiComboBoxOptionOption[];
|
||||
parsedToTermsString: string;
|
||||
parsedIsExplicit: boolean;
|
||||
};
|
||||
|
||||
export const synonymToComboBoxOption: SynonymsToComboBoxOption = (synonymString: string) => {
|
||||
const isExplicit = isExplicitSynonym(synonymString);
|
||||
if (!isExplicit) {
|
||||
return {
|
||||
parsedFromTerms: synonymsStringToOption(synonymString),
|
||||
parsedToTermsString: '',
|
||||
parsedIsExplicit: isExplicit,
|
||||
};
|
||||
} else {
|
||||
const { mapFromString, mapToString } = getExplicitSynonym(synonymString);
|
||||
return {
|
||||
parsedFromTerms: synonymsStringToOption(mapFromString),
|
||||
parsedToTermsString: mapToString,
|
||||
parsedIsExplicit: isExplicit,
|
||||
};
|
||||
}
|
||||
};
|
||||
export const synonymsStringToOption = (synonyms: string) =>
|
||||
synonyms.length === 0
|
||||
? []
|
||||
: synonyms
|
||||
.trim()
|
||||
.split(',')
|
||||
.map((s, index) => ({ label: s, key: index + '-' + s }));
|
||||
.map((s, index) => ({ label: s, key: index + '-' + s.trim() }));
|
||||
|
||||
export const synonymsOptionToString = ({
|
||||
fromTerms,
|
||||
toTerms,
|
||||
isExplicit,
|
||||
}: {
|
||||
fromTerms: EuiComboBoxOptionOption[];
|
||||
toTerms: EuiComboBoxOptionOption[];
|
||||
toTerms: string;
|
||||
isExplicit: boolean;
|
||||
}) =>
|
||||
`${fromTerms.map((s) => s.label).join(',')}${
|
||||
isExplicit ? ' => ' + toTerms.map((s) => s.label).join(',') : ''
|
||||
}`;
|
||||
}) => `${fromTerms.map((s) => s.label).join(',')}${isExplicit ? ' => ' + toTerms.trim() : ''}`;
|
||||
|
||||
export const isPermissionError = (error: { body: KibanaServerError }) => {
|
||||
return error.body.statusCode === 403;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue