mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Index Management] Add support for enrich policies (#164080)
This commit is contained in:
parent
e02c8740ec
commit
d1608f070d
65 changed files with 4663 additions and 27 deletions
|
@ -338,6 +338,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`,
|
||||
dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`,
|
||||
deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`,
|
||||
createEnrichPolicy: `${ELASTICSEARCH_DOCS}put-enrich-policy-api.html`,
|
||||
matchAllQuery: `${ELASTICSEARCH_DOCS}query-dsl-match-all-query.html`,
|
||||
enrichPolicies: `${ELASTICSEARCH_DOCS}ingest-enriching-data.html#enrich-policy`,
|
||||
createIndex: `${ELASTICSEARCH_DOCS}indices-create-index.html`,
|
||||
frozenIndices: `${ELASTICSEARCH_DOCS}frozen-indices.html`,
|
||||
gettingStarted: `${ELASTICSEARCH_DOCS}getting-started.html`,
|
||||
|
|
|
@ -31,6 +31,7 @@ function JsonEditorComp<T extends object = { [key: string]: any }>({
|
|||
defaultValue,
|
||||
codeEditorProps,
|
||||
error: propsError,
|
||||
...rest
|
||||
}: Props<T>) {
|
||||
const {
|
||||
content,
|
||||
|
@ -82,6 +83,7 @@ function JsonEditorComp<T extends object = { [key: string]: any }>({
|
|||
isInvalid={typeof error === 'string'}
|
||||
error={error}
|
||||
fullWidth
|
||||
{...rest}
|
||||
>
|
||||
<CodeEditor
|
||||
languageId="json"
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 } from 'react-dom/test-utils';
|
||||
|
||||
import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { EnrichPolicyCreate } from '../../../public/application/sections/enrich_policy_create';
|
||||
import { indexManagementStore } from '../../../public/application/store';
|
||||
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
||||
|
||||
const testBedConfig: AsyncTestBedConfig = {
|
||||
store: () => indexManagementStore(services as any),
|
||||
memoryRouter: {
|
||||
initialEntries: [`/enrich_policies/create`],
|
||||
componentRoutePath: `/:section(enrich_policies)/create`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
export interface CreateEnrichPoliciesTestBed extends TestBed<TestSubjects> {
|
||||
actions: {
|
||||
clickNextButton: () => Promise<void>;
|
||||
clickBackButton: () => Promise<void>;
|
||||
clickRequestTab: () => Promise<void>;
|
||||
clickCreatePolicy: () => Promise<void>;
|
||||
completeConfigurationStep: ({ indices }: { indices?: string }) => Promise<void>;
|
||||
completeFieldsSelectionStep: () => Promise<void>;
|
||||
isOnConfigurationStep: () => boolean;
|
||||
isOnFieldSelectionStep: () => boolean;
|
||||
isOnCreateStep: () => boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (
|
||||
httpSetup: HttpSetup,
|
||||
overridingDependencies: any = {}
|
||||
): Promise<CreateEnrichPoliciesTestBed> => {
|
||||
const initTestBed = registerTestBed(
|
||||
WithAppDependencies(EnrichPolicyCreate, httpSetup, overridingDependencies),
|
||||
testBedConfig
|
||||
);
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
const isOnConfigurationStep = () => testBed.exists('configurationForm');
|
||||
const isOnFieldSelectionStep = () => testBed.exists('fieldSelectionForm');
|
||||
const isOnCreateStep = () => testBed.exists('creationStep');
|
||||
const clickNextButton = async () => {
|
||||
await act(async () => {
|
||||
testBed.find('nextButton').simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
const clickBackButton = async () => {
|
||||
await act(async () => {
|
||||
testBed.find('backButton').simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
const clickCreatePolicy = async (executeAfter?: boolean) => {
|
||||
await act(async () => {
|
||||
testBed.find(executeAfter ? 'createAndExecuteButton' : 'createButton').simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
|
||||
const clickRequestTab = async () => {
|
||||
await act(async () => {
|
||||
testBed.find('requestTab').simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
|
||||
const completeConfigurationStep = async ({ indices }: { indices?: string }) => {
|
||||
const { form } = testBed;
|
||||
|
||||
form.setInputValue('policyNameField.input', 'test_policy');
|
||||
form.setSelectValue('policyTypeField', 'match');
|
||||
form.setSelectValue('policySourceIndicesField', indices ?? 'test-1');
|
||||
|
||||
await clickNextButton();
|
||||
};
|
||||
|
||||
const completeFieldsSelectionStep = async () => {
|
||||
const { form } = testBed;
|
||||
|
||||
form.setSelectValue('matchField', 'name');
|
||||
form.setSelectValue('enrichFields', 'email');
|
||||
|
||||
await clickNextButton();
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
clickNextButton,
|
||||
clickBackButton,
|
||||
clickRequestTab,
|
||||
clickCreatePolicy,
|
||||
completeConfigurationStep,
|
||||
completeFieldsSelectionStep,
|
||||
isOnConfigurationStep,
|
||||
isOnFieldSelectionStep,
|
||||
isOnCreateStep,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* 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 } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment } from '../helpers';
|
||||
import { getMatchingIndices, getFieldsFromIndices } from '../helpers/fixtures';
|
||||
import { CreateEnrichPoliciesTestBed, setup } from './create_enrich_policy.helpers';
|
||||
import { getESPolicyCreationApiCall } from '../../../common/lib';
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => {
|
||||
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
|
||||
return {
|
||||
...original,
|
||||
// Mocking CodeEditor, which uses React Monaco under the hood
|
||||
CodeEditor: (props: any) => (
|
||||
<input
|
||||
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
|
||||
data-currentvalue={props.value}
|
||||
onChange={(e: any) => {
|
||||
props.onChange(e.jsonContent);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
...original,
|
||||
// Mock EuiComboBox as a simple input instead so that its easier to test
|
||||
EuiComboBox: (props: any) => (
|
||||
<input
|
||||
data-test-subj={props['data-test-subj'] || 'mockEuiCombobox'}
|
||||
data-currentvalue={props.value}
|
||||
onChange={(e: any) => {
|
||||
props.onChange(e.target.value.split(', '));
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Create enrich policy', () => {
|
||||
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: CreateEnrichPoliciesTestBed;
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetMatchingIndices(getMatchingIndices());
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('Has header and docs link', async () => {
|
||||
const { exists, component } = testBed;
|
||||
component.update();
|
||||
|
||||
expect(exists('createEnrichPolicyHeaderContent')).toBe(true);
|
||||
expect(exists('createEnrichPolicyDocumentationLink')).toBe(true);
|
||||
});
|
||||
|
||||
describe('Configuration step', () => {
|
||||
it('Fields have helpers', async () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('typePopoverIcon')).toBe(true);
|
||||
expect(exists('uploadFileLink')).toBe(true);
|
||||
expect(exists('matchAllQueryLink')).toBe(true);
|
||||
});
|
||||
|
||||
it('shows validation errors if form isnt filled', async () => {
|
||||
await testBed.actions.clickNextButton();
|
||||
|
||||
expect(testBed.form.getErrorsMessages()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('Allows to submit the form when fields are filled', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await testBed.actions.completeConfigurationStep({});
|
||||
|
||||
expect(actions.isOnFieldSelectionStep()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fields selection step', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetFieldsFromIndices(getFieldsFromIndices());
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
|
||||
await testBed.actions.completeConfigurationStep({});
|
||||
});
|
||||
|
||||
it('shows validation errors if form isnt filled', async () => {
|
||||
await testBed.actions.clickNextButton();
|
||||
|
||||
expect(testBed.form.getErrorsMessages()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Allows to submit the form when fields are filled', async () => {
|
||||
const { form, actions } = testBed;
|
||||
|
||||
form.setSelectValue('matchField', 'name');
|
||||
form.setSelectValue('enrichFields', 'email');
|
||||
|
||||
await testBed.actions.clickNextButton();
|
||||
|
||||
expect(actions.isOnCreateStep()).toBe(true);
|
||||
});
|
||||
|
||||
it('When no common fields are returned it shows an error callout', async () => {
|
||||
httpRequestsMockHelpers.setGetFieldsFromIndices({
|
||||
commonFields: [],
|
||||
indices: [],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
|
||||
await testBed.actions.completeConfigurationStep({ indices: 'test-1, test-2' });
|
||||
|
||||
expect(testBed.exists('noCommonFieldsError')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Creation step', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetFieldsFromIndices(getFieldsFromIndices());
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
|
||||
await testBed.actions.completeConfigurationStep({});
|
||||
await testBed.actions.completeFieldsSelectionStep();
|
||||
});
|
||||
|
||||
it('Shows CTAs for creating the policy', async () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('createButton')).toBe(true);
|
||||
expect(exists('createAndExecuteButton')).toBe(true);
|
||||
});
|
||||
|
||||
it('Shows policy summary and request', async () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('enrichPolicySummaryList').text()).toContain('test_policy');
|
||||
|
||||
await testBed.actions.clickRequestTab();
|
||||
|
||||
expect(find('requestBody').text()).toContain(getESPolicyCreationApiCall('test_policy'));
|
||||
});
|
||||
|
||||
it('Shows error message when creating the policy fails', async () => {
|
||||
const { exists, actions } = testBed;
|
||||
const error = {
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'something went wrong...',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setCreateEnrichPolicy(undefined, error);
|
||||
|
||||
await actions.clickCreatePolicy();
|
||||
|
||||
expect(exists('errorWhenCreatingCallout')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can navigate back and forth with next/back buttons', async () => {
|
||||
httpRequestsMockHelpers.setGetFieldsFromIndices(getFieldsFromIndices());
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
const { component, actions } = testBed;
|
||||
component.update();
|
||||
|
||||
// Navigate to create step
|
||||
await actions.completeConfigurationStep({});
|
||||
await actions.completeFieldsSelectionStep();
|
||||
|
||||
// Clicking back button should take us to fields selection step
|
||||
await actions.clickBackButton();
|
||||
expect(actions.isOnFieldSelectionStep()).toBe(true);
|
||||
|
||||
// Clicking back button should take us to configuration step
|
||||
await actions.clickBackButton();
|
||||
expect(actions.isOnConfigurationStep()).toBe(true);
|
||||
});
|
||||
});
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
export const indexSettings = {
|
||||
settings: { index: { number_of_shards: '1' } },
|
||||
defaults: { index: { flush_after_merge: '512mb' } },
|
||||
|
@ -45,3 +47,31 @@ export const indexStats = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const createTestEnrichPolicy = (name: string, type: EnrichPolicyType) => ({
|
||||
name,
|
||||
type,
|
||||
sourceIndices: ['users'],
|
||||
matchField: 'email',
|
||||
enrichFields: ['first_name', 'last_name', 'city'],
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const getMatchingIndices = () => ({
|
||||
indices: ['test-1', 'test-2', 'test-3', 'test-4', 'test-5'],
|
||||
});
|
||||
|
||||
export const getFieldsFromIndices = () => ({
|
||||
commonFields: [],
|
||||
indices: [
|
||||
{
|
||||
index: 'test-1',
|
||||
fields: [
|
||||
{ name: 'first_name', type: 'keyword', normalizedType: 'keyword' },
|
||||
{ name: 'age', type: 'long', normalizedType: 'number' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -19,7 +19,8 @@ export interface ResponseError {
|
|||
|
||||
// Register helpers to mock HTTP Requests
|
||||
const registerHttpRequestMockHelpers = (
|
||||
httpSetup: ReturnType<typeof httpServiceMock.createStartContract>
|
||||
httpSetup: ReturnType<typeof httpServiceMock.createStartContract>,
|
||||
shouldDelayResponse: () => boolean
|
||||
) => {
|
||||
const mockResponses = new Map<HttpMethod, Map<string, Promise<unknown>>>(
|
||||
['GET', 'PUT', 'DELETE', 'POST'].map(
|
||||
|
@ -28,7 +29,14 @@ const registerHttpRequestMockHelpers = (
|
|||
);
|
||||
|
||||
const mockMethodImplementation = (method: HttpMethod, path: string) => {
|
||||
return mockResponses.get(method)?.get(path) ?? Promise.resolve({});
|
||||
const responsePromise = mockResponses.get(method)?.get(path) ?? Promise.resolve({});
|
||||
if (shouldDelayResponse()) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(responsePromise), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
return responsePromise;
|
||||
};
|
||||
|
||||
httpSetup.get.mockImplementation((path) =>
|
||||
|
@ -130,6 +138,47 @@ const registerHttpRequestMockHelpers = (
|
|||
const setLoadTelemetryResponse = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse('GET', '/api/ui_counters/_report', response, error);
|
||||
|
||||
const setLoadEnrichPoliciesResponse = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse('GET', `${INTERNAL_API_BASE_PATH}/enrich_policies`, response, error);
|
||||
|
||||
const setGetMatchingIndices = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse(
|
||||
'POST',
|
||||
`${INTERNAL_API_BASE_PATH}/enrich_policies/get_matching_indices`,
|
||||
response,
|
||||
error
|
||||
);
|
||||
|
||||
const setGetFieldsFromIndices = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse(
|
||||
'POST',
|
||||
`${INTERNAL_API_BASE_PATH}/enrich_policies/get_fields_from_indices`,
|
||||
response,
|
||||
error
|
||||
);
|
||||
|
||||
const setCreateEnrichPolicy = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse('POST', `${INTERNAL_API_BASE_PATH}/enrich_policies`, response, error);
|
||||
|
||||
const setDeleteEnrichPolicyResponse = (
|
||||
policyName: string,
|
||||
response?: HttpResponse,
|
||||
error?: ResponseError
|
||||
) =>
|
||||
mockResponse(
|
||||
'DELETE',
|
||||
`${INTERNAL_API_BASE_PATH}/enrich_policies/${policyName}`,
|
||||
response,
|
||||
error
|
||||
);
|
||||
|
||||
const setExecuteEnrichPolicyResponse = (
|
||||
policyName: string,
|
||||
response?: HttpResponse,
|
||||
error?: ResponseError
|
||||
) =>
|
||||
mockResponse('PUT', `${INTERNAL_API_BASE_PATH}/enrich_policies/${policyName}`, response, error);
|
||||
|
||||
const setLoadIndexDetailsResponse = (
|
||||
indexName: string,
|
||||
response?: HttpResponse,
|
||||
|
@ -157,17 +206,30 @@ const registerHttpRequestMockHelpers = (
|
|||
setLoadComponentTemplatesResponse,
|
||||
setLoadNodesPluginsResponse,
|
||||
setLoadTelemetryResponse,
|
||||
setLoadEnrichPoliciesResponse,
|
||||
setDeleteEnrichPolicyResponse,
|
||||
setExecuteEnrichPolicyResponse,
|
||||
setLoadIndexDetailsResponse,
|
||||
setCreateIndexResponse,
|
||||
setGetMatchingIndices,
|
||||
setGetFieldsFromIndices,
|
||||
setCreateEnrichPolicy,
|
||||
};
|
||||
};
|
||||
|
||||
export const init = () => {
|
||||
let isResponseDelayed = false;
|
||||
const getDelayResponse = () => isResponseDelayed;
|
||||
const setDelayResponse = (shouldDelayResponse: boolean) => {
|
||||
isResponseDelayed = shouldDelayResponse;
|
||||
};
|
||||
|
||||
const httpSetup = httpServiceMock.createSetupContract();
|
||||
const httpRequestsMockHelpers = registerHttpRequestMockHelpers(httpSetup);
|
||||
const httpRequestsMockHelpers = registerHttpRequestMockHelpers(httpSetup, getDelayResponse);
|
||||
|
||||
return {
|
||||
httpSetup,
|
||||
httpRequestsMockHelpers,
|
||||
setDelayResponse,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import { LocationDescriptorObject } from 'history';
|
||||
import SemVer from 'semver/classes/semver';
|
||||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
|
@ -15,11 +16,13 @@ import {
|
|||
docLinksServiceMock,
|
||||
uiSettingsServiceMock,
|
||||
themeServiceMock,
|
||||
scopedHistoryMock,
|
||||
executionContextServiceMock,
|
||||
applicationServiceMock,
|
||||
fatalErrorsServiceMock,
|
||||
httpServiceMock,
|
||||
} from '@kbn/core/public/mocks';
|
||||
|
||||
import { GlobalFlyout } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -47,14 +50,21 @@ const { GlobalFlyoutProvider } = GlobalFlyout;
|
|||
export const services = {
|
||||
extensionsService: new ExtensionsService(),
|
||||
uiMetricService: new UiMetricService('index_management'),
|
||||
notificationService: notificationServiceMock.createSetupContract(),
|
||||
};
|
||||
|
||||
services.uiMetricService.setup({ reportUiCounter() {} } as any);
|
||||
setExtensionsService(services.extensionsService);
|
||||
setUiMetricService(services.uiMetricService);
|
||||
|
||||
const history = scopedHistoryMock.create();
|
||||
history.createHref.mockImplementation((location: LocationDescriptorObject) => {
|
||||
return `${location.pathname}?${location.search}`;
|
||||
});
|
||||
|
||||
const appDependencies = {
|
||||
services,
|
||||
history,
|
||||
core: {
|
||||
getUrlForApp: applicationServiceMock.createStartContract().getUrlForApp,
|
||||
executionContext: executionContextServiceMock.createStartContract(),
|
||||
|
|
|
@ -64,8 +64,32 @@ export type TestSubjects =
|
|||
| 'updateEditIndexSettingsButton'
|
||||
| 'updateIndexSettingsErrorCallout'
|
||||
| 'viewButton'
|
||||
| 'detailPanelTabSelected'
|
||||
| 'enrichPoliciesTable'
|
||||
| 'deletePolicyModal'
|
||||
| 'executePolicyModal'
|
||||
| 'policyDetailsFlyout'
|
||||
| 'policyTypeValue'
|
||||
| 'policyIndicesValue'
|
||||
| 'policyMatchFieldValue'
|
||||
| 'policyEnrichFieldsValue'
|
||||
| 'queryEditor'
|
||||
| 'createIndexButton'
|
||||
| 'createIndexNameFieldText'
|
||||
| 'createIndexCancelButton'
|
||||
| 'createEnrichPolicyHeaderContent'
|
||||
| 'createEnrichPolicyDocumentationLink'
|
||||
| 'policyNameField.input'
|
||||
| 'policyTypeField'
|
||||
| 'policySourceIndicesField'
|
||||
| 'typePopoverIcon'
|
||||
| 'uploadFileLink'
|
||||
| 'matchAllQueryLink'
|
||||
| 'matchField'
|
||||
| 'enrichFields'
|
||||
| 'noCommonFieldsError'
|
||||
| 'createButton'
|
||||
| 'createAndExecuteButton'
|
||||
| 'enrichPolicySummaryList'
|
||||
| 'requestBody'
|
||||
| 'errorWhenCreatingCallout'
|
||||
| 'createIndexSaveButton';
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
registerTestBed,
|
||||
TestBed,
|
||||
AsyncTestBedConfig,
|
||||
findTestSubject,
|
||||
} from '@kbn/test-jest-helpers';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { IndexManagementHome } from '../../../public/application/sections/home';
|
||||
import { indexManagementStore } from '../../../public/application/store';
|
||||
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
||||
|
||||
const testBedConfig: AsyncTestBedConfig = {
|
||||
store: () => indexManagementStore(services as any),
|
||||
memoryRouter: {
|
||||
initialEntries: [`/enrich_policies`],
|
||||
componentRoutePath: `/:section(enrich_policies)`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
export interface EnrichPoliciesTestBed extends TestBed<TestSubjects> {
|
||||
actions: {
|
||||
goToEnrichPoliciesTab: () => void;
|
||||
clickReloadPoliciesButton: () => void;
|
||||
clickDeletePolicyAt: (index: number) => Promise<void>;
|
||||
clickConfirmDeletePolicyButton: () => Promise<void>;
|
||||
clickExecutePolicyAt: (index: number) => Promise<void>;
|
||||
clickConfirmExecutePolicyButton: () => Promise<void>;
|
||||
clickEnrichPolicyAt: (index: number) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (
|
||||
httpSetup: HttpSetup,
|
||||
overridingDependencies: any = {}
|
||||
): Promise<EnrichPoliciesTestBed> => {
|
||||
const initTestBed = registerTestBed(
|
||||
WithAppDependencies(IndexManagementHome, httpSetup, overridingDependencies),
|
||||
testBedConfig
|
||||
);
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
const goToEnrichPoliciesTab = () => testBed.find('enrich_policiesTab').simulate('click');
|
||||
|
||||
const clickReloadPoliciesButton = () => testBed.find('reloadPoliciesButton').simulate('click');
|
||||
|
||||
const clickDeletePolicyAt = async (index: number) => {
|
||||
const { rows } = testBed.table.getMetaData('enrichPoliciesTable');
|
||||
|
||||
const deletePolicyButton = findTestSubject(rows[index].reactWrapper, 'deletePolicyButton');
|
||||
|
||||
await act(async () => {
|
||||
deletePolicyButton.simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
|
||||
const clickConfirmDeletePolicyButton = async () => {
|
||||
const modal = testBed.find('deletePolicyModal');
|
||||
const confirmButton = findTestSubject(modal, 'confirmModalConfirmButton');
|
||||
|
||||
await act(async () => {
|
||||
confirmButton.simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
|
||||
const clickExecutePolicyAt = async (index: number) => {
|
||||
const { rows } = testBed.table.getMetaData('enrichPoliciesTable');
|
||||
|
||||
const executePolicyButton = findTestSubject(rows[index].reactWrapper, 'executePolicyButton');
|
||||
|
||||
await act(async () => {
|
||||
executePolicyButton.simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
|
||||
const clickConfirmExecutePolicyButton = async () => {
|
||||
const modal = testBed.find('executePolicyModal');
|
||||
const confirmButton = findTestSubject(modal, 'confirmModalConfirmButton');
|
||||
|
||||
await act(async () => {
|
||||
confirmButton.simulate('click');
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
};
|
||||
|
||||
const clickEnrichPolicyAt = async (index: number) => {
|
||||
const { component, table, router } = testBed;
|
||||
|
||||
const { rows } = table.getMetaData('enrichPoliciesTable');
|
||||
|
||||
const policyLink = findTestSubject(rows[index].reactWrapper, 'enrichPolicyDetailsLink');
|
||||
|
||||
await act(async () => {
|
||||
const { href } = policyLink.props();
|
||||
router.navigateTo(href!);
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
goToEnrichPoliciesTab,
|
||||
clickReloadPoliciesButton,
|
||||
clickDeletePolicyAt,
|
||||
clickConfirmDeletePolicyButton,
|
||||
clickExecutePolicyAt,
|
||||
clickConfirmExecutePolicyButton,
|
||||
clickEnrichPolicyAt,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* 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 } from 'react-dom/test-utils';
|
||||
import { notificationServiceMock } from '@kbn/core/public/mocks';
|
||||
|
||||
import { setupEnvironment } from '../helpers';
|
||||
import { createTestEnrichPolicy } from '../helpers/fixtures';
|
||||
import { EnrichPoliciesTestBed, setup } from './enrich_policies.helpers';
|
||||
import { notificationService } from '../../../public/application/services/notification';
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => {
|
||||
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
|
||||
return {
|
||||
...original,
|
||||
// Mocking CodeEditor, which uses React Monaco under the hood
|
||||
CodeEditor: (props: any) => (
|
||||
<input
|
||||
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
|
||||
data-currentvalue={props.value}
|
||||
onChange={(e: any) => {
|
||||
props.onChange(e.jsonContent);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Enrich policies tab', () => {
|
||||
const { httpSetup, httpRequestsMockHelpers, setDelayResponse } = setupEnvironment();
|
||||
let testBed: EnrichPoliciesTestBed;
|
||||
|
||||
describe('empty states', () => {
|
||||
beforeEach(async () => {
|
||||
setDelayResponse(false);
|
||||
});
|
||||
|
||||
test('displays a loading prompt', async () => {
|
||||
setDelayResponse(true);
|
||||
|
||||
testBed = await setup(httpSetup);
|
||||
|
||||
await act(async () => {
|
||||
testBed.actions.goToEnrichPoliciesTab();
|
||||
});
|
||||
|
||||
const { exists, component } = testBed;
|
||||
component.update();
|
||||
|
||||
expect(exists('sectionLoading')).toBe(true);
|
||||
});
|
||||
|
||||
test('displays a error prompt', async () => {
|
||||
const error = {
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'something went wrong...',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadEnrichPoliciesResponse(undefined, error);
|
||||
|
||||
testBed = await setup(httpSetup);
|
||||
|
||||
await act(async () => {
|
||||
testBed.actions.goToEnrichPoliciesTab();
|
||||
});
|
||||
|
||||
const { exists, component } = testBed;
|
||||
component.update();
|
||||
|
||||
expect(exists('sectionError')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('policies list', () => {
|
||||
let testPolicy: ReturnType<typeof createTestEnrichPolicy>;
|
||||
beforeEach(async () => {
|
||||
testPolicy = createTestEnrichPolicy('policy-match', 'match');
|
||||
|
||||
httpRequestsMockHelpers.setLoadEnrichPoliciesResponse([
|
||||
testPolicy,
|
||||
createTestEnrichPolicy('policy-range', 'range'),
|
||||
]);
|
||||
|
||||
testBed = await setup(httpSetup);
|
||||
await act(async () => {
|
||||
testBed.actions.goToEnrichPoliciesTab();
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
it('shows enrich policies in table', async () => {
|
||||
const { table } = testBed;
|
||||
expect(table.getMetaData('enrichPoliciesTable').rows.length).toBe(2);
|
||||
});
|
||||
|
||||
it('can reload the table data through a call to action', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
// Reset mock to clear calls from setup
|
||||
httpSetup.get.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
actions.clickReloadPoliciesButton();
|
||||
});
|
||||
|
||||
// Should have made a call to load the policies after the reload
|
||||
// button is clicked.
|
||||
expect(httpSetup.get.mock.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('details flyout', () => {
|
||||
it('can open the details flyout', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
await actions.clickEnrichPolicyAt(0);
|
||||
|
||||
expect(exists('policyDetailsFlyout')).toBe(true);
|
||||
});
|
||||
|
||||
it('contains all the necessary policy fields', async () => {
|
||||
const { actions, find } = testBed;
|
||||
|
||||
await actions.clickEnrichPolicyAt(0);
|
||||
|
||||
expect(find('policyTypeValue').text()).toBe(testPolicy.type);
|
||||
expect(find('policyIndicesValue').text()).toBe(testPolicy.sourceIndices.join(', '));
|
||||
expect(find('policyMatchFieldValue').text()).toBe(testPolicy.matchField);
|
||||
expect(find('policyEnrichFieldsValue').text()).toBe(testPolicy.enrichFields.join(', '));
|
||||
|
||||
const codeEditorValue = find('queryEditor')
|
||||
.at(0)
|
||||
.getDOMNode()
|
||||
.getAttribute('data-currentvalue');
|
||||
expect(JSON.parse(codeEditorValue || '')).toEqual(testPolicy.query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('policy actions', () => {
|
||||
const notificationsServiceMock = notificationServiceMock.createStartContract();
|
||||
|
||||
beforeEach(async () => {
|
||||
notificationService.setup(notificationsServiceMock);
|
||||
|
||||
httpRequestsMockHelpers.setLoadEnrichPoliciesResponse([
|
||||
createTestEnrichPolicy('policy-match', 'match'),
|
||||
]);
|
||||
|
||||
testBed = await setup(httpSetup, {
|
||||
services: {
|
||||
notificationService,
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
testBed.actions.goToEnrichPoliciesTab();
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
describe('deletion', () => {
|
||||
it('can delete a policy', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setDeleteEnrichPolicyResponse('policy-match', {
|
||||
acknowledged: true,
|
||||
});
|
||||
|
||||
await actions.clickDeletePolicyAt(0);
|
||||
|
||||
expect(exists('deletePolicyModal')).toBe(true);
|
||||
|
||||
await actions.clickConfirmDeletePolicyButton();
|
||||
|
||||
expect(notificationsServiceMock.toasts.add).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Deleted policy-match',
|
||||
})
|
||||
);
|
||||
expect(httpSetup.delete.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('displays an error toast if it fails', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
const error = {
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'something went wrong...',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setDeleteEnrichPolicyResponse('policy-match', undefined, error);
|
||||
|
||||
await actions.clickDeletePolicyAt(0);
|
||||
|
||||
expect(exists('deletePolicyModal')).toBe(true);
|
||||
|
||||
await actions.clickConfirmDeletePolicyButton();
|
||||
|
||||
expect(notificationsServiceMock.toasts.add).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
title: `Error deleting enrich policy: 'something went wrong...'`,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('can execute a policy', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setExecuteEnrichPolicyResponse('policy-match', {
|
||||
acknowledged: true,
|
||||
});
|
||||
|
||||
await actions.clickExecutePolicyAt(0);
|
||||
|
||||
expect(exists('executePolicyModal')).toBe(true);
|
||||
|
||||
await actions.clickConfirmExecutePolicyButton();
|
||||
|
||||
expect(notificationsServiceMock.toasts.add).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Executed policy-match',
|
||||
})
|
||||
);
|
||||
expect(httpSetup.put.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('displays an error toast if it fails', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
const error = {
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'something went wrong...',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setExecuteEnrichPolicyResponse('policy-match', undefined, error);
|
||||
|
||||
await actions.clickExecutePolicyAt(0);
|
||||
|
||||
expect(exists('executePolicyModal')).toBe(true);
|
||||
|
||||
await actions.clickConfirmExecutePolicyButton();
|
||||
|
||||
expect(notificationsServiceMock.toasts.add).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
title: `Error executing enrich policy: 'something went wrong...'`,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -56,9 +56,15 @@ describe('<IndexManagementHome />', () => {
|
|||
const indexManagementContainer = find('indexManagementHeaderContent');
|
||||
const tabListContainer = indexManagementContainer.find('div.euiTabs');
|
||||
const allTabs = tabListContainer.children();
|
||||
const allTabsLabels = ['Indices', 'Data Streams', 'Index Templates', 'Component Templates'];
|
||||
const allTabsLabels = [
|
||||
'Indices',
|
||||
'Data Streams',
|
||||
'Index Templates',
|
||||
'Component Templates',
|
||||
'Enrich Policies',
|
||||
];
|
||||
|
||||
expect(allTabs.length).toBe(4);
|
||||
expect(allTabs.length).toBe(5);
|
||||
for (let i = 0; i < allTabs.length; i++) {
|
||||
expect(tabListContainer.childAt(i).text()).toEqual(allTabsLabels[i]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 type { EnrichSummary, EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { SerializedEnrichPolicy } from '../types';
|
||||
|
||||
export const getPolicyType = (policy: EnrichSummary): EnrichPolicyType => {
|
||||
if (policy.config.match) {
|
||||
return 'match';
|
||||
}
|
||||
|
||||
if (policy.config.geo_match) {
|
||||
return 'geo_match';
|
||||
}
|
||||
|
||||
if (policy.config.range) {
|
||||
return 'range';
|
||||
}
|
||||
|
||||
throw new Error('Unknown policy type');
|
||||
};
|
||||
|
||||
export const getESPolicyCreationApiCall = (policyName: string) => {
|
||||
return `PUT _enrich/policy/${policyName}`;
|
||||
};
|
||||
|
||||
export const serializeAsESPolicy = (policy: SerializedEnrichPolicy) => {
|
||||
const policyType = policy.type as EnrichPolicyType;
|
||||
|
||||
return {
|
||||
[policyType]: {
|
||||
indices: policy.sourceIndices,
|
||||
match_field: policy.matchField,
|
||||
enrich_fields: policy.enrichFields,
|
||||
query: policy.query,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -23,3 +23,5 @@ export {
|
|||
deserializeComponentTemplateList,
|
||||
serializeComponentTemplate,
|
||||
} from './component_template_serialization';
|
||||
|
||||
export { getPolicyType, serializeAsESPolicy, getESPolicyCreationApiCall } from './enrich_policies';
|
||||
|
|
|
@ -13,4 +13,21 @@ export interface SerializedEnrichPolicy {
|
|||
sourceIndices: string[];
|
||||
matchField: string;
|
||||
enrichFields: string[];
|
||||
query?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface FieldItem {
|
||||
name: string;
|
||||
type: string;
|
||||
normalizedType: string;
|
||||
}
|
||||
|
||||
export interface IndexWithFields {
|
||||
index: string;
|
||||
fields: FieldItem[];
|
||||
}
|
||||
|
||||
export interface FieldFromIndicesRequest {
|
||||
commonFields: FieldItem[];
|
||||
indices: IndexWithFields[];
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { IndexManagementHome, homeSections, Section } from './sections/home';
|
|||
import { TemplateCreate } from './sections/template_create';
|
||||
import { TemplateClone } from './sections/template_clone';
|
||||
import { TemplateEdit } from './sections/template_edit';
|
||||
import { EnrichPolicyCreate } from './sections/enrich_policy_create';
|
||||
import { useAppContext } from './app_context';
|
||||
import {
|
||||
ComponentTemplateCreate,
|
||||
|
@ -51,6 +52,7 @@ export const AppWithoutRouter = () => (
|
|||
component={ComponentTemplateClone}
|
||||
/>
|
||||
<Route exact path="/edit_component_template/:name*" component={ComponentTemplateEdit} />
|
||||
<Route exact path="/enrich_policies/create" component={EnrichPolicyCreate} />
|
||||
<Route path={`/:section(${homeSections.join('|')})`} component={IndexManagementHome} />
|
||||
<Redirect from={`/`} to={`/${Section.Indices}`} />
|
||||
</Routes>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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, { createContext, useContext, useState } from 'react';
|
||||
import type { SerializedEnrichPolicy } from '../../../../common';
|
||||
|
||||
export type DraftPolicy = Partial<SerializedEnrichPolicy>;
|
||||
|
||||
export interface CompletionState {
|
||||
configurationStep: boolean;
|
||||
fieldsSelectionStep: boolean;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
draft: DraftPolicy;
|
||||
updateDraft: React.Dispatch<React.SetStateAction<DraftPolicy>>;
|
||||
completionState: CompletionState;
|
||||
updateCompletionState: React.Dispatch<React.SetStateAction<CompletionState>>;
|
||||
}
|
||||
|
||||
export const CreatePolicyContext = createContext<Context>({} as any);
|
||||
|
||||
export const CreatePolicyContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [draft, updateDraft] = useState<DraftPolicy>({});
|
||||
const [completionState, updateCompletionState] = useState<CompletionState>({
|
||||
configurationStep: false,
|
||||
fieldsSelectionStep: false,
|
||||
});
|
||||
|
||||
const contextValue = {
|
||||
draft,
|
||||
updateDraft,
|
||||
completionState,
|
||||
updateCompletionState,
|
||||
};
|
||||
|
||||
return (
|
||||
<CreatePolicyContext.Provider value={contextValue}>{children}</CreatePolicyContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCreatePolicyContext = () => {
|
||||
const ctx = useContext(CreatePolicyContext);
|
||||
if (!ctx) throw new Error('Cannot use outside of create policy context');
|
||||
|
||||
return ctx;
|
||||
};
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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, useMemo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSteps, EuiStepStatus, EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { useAppContext } from '../../app_context';
|
||||
import { ConfigurationStep, FieldSelectionStep, CreateStep } from './steps';
|
||||
import { useCreatePolicyContext } from './create_policy_context';
|
||||
import { createEnrichPolicy } from '../../services/api';
|
||||
import type { Error } from '../../../shared_imports';
|
||||
import type { SerializedEnrichPolicy } from '../../../../common';
|
||||
|
||||
const CONFIGURATION = 1;
|
||||
const FIELD_SELECTION = 2;
|
||||
const CREATE = 3;
|
||||
|
||||
export const CreatePolicyWizard = () => {
|
||||
const {
|
||||
history,
|
||||
services: { notificationService },
|
||||
} = useAppContext();
|
||||
const { draft, completionState } = useCreatePolicyContext();
|
||||
const [currentStep, setCurrentStep] = useState(CONFIGURATION);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [createError, setCreateError] = useState<Error | null>(null);
|
||||
|
||||
const getStepStatus = useCallback(
|
||||
(forStep: number): EuiStepStatus => {
|
||||
if (currentStep === forStep) {
|
||||
return 'current';
|
||||
}
|
||||
|
||||
return 'incomplete';
|
||||
},
|
||||
[currentStep]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (executePolicyAfterCreation?: boolean) => {
|
||||
setIsLoading(true);
|
||||
const { error, data } = await createEnrichPolicy(
|
||||
draft as SerializedEnrichPolicy,
|
||||
executePolicyAfterCreation
|
||||
);
|
||||
setIsLoading(false);
|
||||
|
||||
if (data) {
|
||||
const toastMessage = executePolicyAfterCreation
|
||||
? i18n.translate(
|
||||
'xpack.idxMgmt.enrichPoliciesCreate.createAndExecuteNotificationMessage',
|
||||
{
|
||||
defaultMessage: 'Created and executed policy: {policyName}',
|
||||
values: { policyName: draft.name },
|
||||
}
|
||||
)
|
||||
: i18n.translate('xpack.idxMgmt.enrichPoliciesCreate.createNotificationMessage', {
|
||||
defaultMessage: 'Created policy: {policyName}',
|
||||
values: { policyName: draft.name },
|
||||
});
|
||||
|
||||
notificationService.showSuccessToast(toastMessage);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setCreateError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
history.push('/enrich_policies');
|
||||
},
|
||||
[draft, history, setIsLoading, setCreateError, notificationService]
|
||||
);
|
||||
|
||||
const changeCurrentStepTo = useCallback(
|
||||
(step: number) => {
|
||||
setCurrentStep(step);
|
||||
setCreateError(null);
|
||||
},
|
||||
[setCurrentStep, setCreateError]
|
||||
);
|
||||
|
||||
const stepDefinitions = useMemo(
|
||||
() => [
|
||||
{
|
||||
step: CONFIGURATION,
|
||||
title: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStepLabel', {
|
||||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
status: completionState.configurationStep ? 'complete' : getStepStatus(CONFIGURATION),
|
||||
children: currentStep === CONFIGURATION && (
|
||||
<ConfigurationStep onNext={() => changeCurrentStepTo(FIELD_SELECTION)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
step: FIELD_SELECTION,
|
||||
title: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStepLabel', {
|
||||
defaultMessage: 'Field selection',
|
||||
}),
|
||||
status: completionState.fieldsSelectionStep ? 'complete' : getStepStatus(FIELD_SELECTION),
|
||||
children: currentStep === FIELD_SELECTION && (
|
||||
<FieldSelectionStep
|
||||
onNext={() => changeCurrentStepTo(CREATE)}
|
||||
onBack={() => changeCurrentStepTo(CONFIGURATION)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
step: CREATE,
|
||||
title: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStepLabel', {
|
||||
defaultMessage: 'Create',
|
||||
}),
|
||||
status: (currentStep === CREATE ? 'current' : 'incomplete') as EuiStepStatus,
|
||||
children: currentStep === CREATE && (
|
||||
<CreateStep
|
||||
onSubmit={onSubmit}
|
||||
isLoading={isLoading}
|
||||
onBack={() => changeCurrentStepTo(FIELD_SELECTION)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[currentStep, changeCurrentStepTo, completionState, getStepStatus, isLoading, onSubmit]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createError && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.idxMgmt.enrichPolicyCreate.errorTitle', {
|
||||
defaultMessage: 'Unable to create your policy',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
data-test-subj="errorWhenCreatingCallout"
|
||||
>
|
||||
<p className="eui-textBreakWord">{createError?.message || createError?.error}</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiSteps steps={stepDefinitions} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui';
|
||||
import { documentationService } from '../../services/documentation';
|
||||
import { breadcrumbService, IndexManagementBreadcrumb } from '../../services/breadcrumbs';
|
||||
|
||||
import { CreatePolicyWizard } from './create_policy_wizard';
|
||||
import { CreatePolicyContextProvider } from './create_policy_context';
|
||||
|
||||
export const EnrichPolicyCreate: React.FunctionComponent<RouteComponentProps> = () => {
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs(IndexManagementBreadcrumb.enrichPoliciesCreate);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CreatePolicyContextProvider>
|
||||
<EuiPageHeader
|
||||
data-test-subj="createEnrichPolicyHeaderContent"
|
||||
pageTitle={
|
||||
<span data-test-subj="appTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.appTitle"
|
||||
defaultMessage="Create enrich policy"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.appDescription"
|
||||
defaultMessage="Specify how to retrieve and enrich your incoming data."
|
||||
/>
|
||||
}
|
||||
bottomBorder
|
||||
rightSideItems={[
|
||||
<EuiButtonEmpty
|
||||
href={documentationService.getCreateEnrichPolicyLink()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="createEnrichPolicyDocumentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.titleDocsLinkText"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<CreatePolicyWizard />
|
||||
</CreatePolicyContextProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { EnrichPolicyCreate } from './enrich_policy_create';
|
|
@ -0,0 +1,352 @@
|
|||
/*
|
||||
* 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 { omit, isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiIcon,
|
||||
EuiCode,
|
||||
EuiButton,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
useForm,
|
||||
Form,
|
||||
fieldValidators,
|
||||
FormSchema,
|
||||
FIELD_TYPES,
|
||||
UseField,
|
||||
TextField,
|
||||
SelectField,
|
||||
JsonEditorField,
|
||||
} from '../../../../shared_imports';
|
||||
|
||||
import { useAppContext } from '../../../app_context';
|
||||
import { IndicesSelector } from './fields/indices_selector';
|
||||
import { documentationService } from '../../../services/documentation';
|
||||
import { useCreatePolicyContext, DraftPolicy } from '../create_policy_context';
|
||||
|
||||
interface Props {
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
export const configurationFormSchema: FormSchema = {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStep.policyNameField', {
|
||||
defaultMessage: 'Policy name',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.emptyField(
|
||||
i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.configurationStep.policyNameRequiredError',
|
||||
{
|
||||
defaultMessage: 'A policy name value is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
type: {
|
||||
type: FIELD_TYPES.SELECT,
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStep.policyTypeLabel', {
|
||||
defaultMessage: 'Policy type',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.emptyField(
|
||||
i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStep.typeRequiredError', {
|
||||
defaultMessage: 'A policy type value is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
sourceIndices: {
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStep.sourceIndicesLabel', {
|
||||
defaultMessage: 'Source indices',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.emptyField(
|
||||
i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.configurationStep.sourceIndicesRequiredError',
|
||||
{
|
||||
defaultMessage: 'At least one source index is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
query: {
|
||||
type: FIELD_TYPES.JSON,
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStep.queryLabel', {
|
||||
defaultMessage: 'Query (optional)',
|
||||
}),
|
||||
serializer: (jsonString: string) => {
|
||||
let parsedJSON: any;
|
||||
try {
|
||||
parsedJSON = JSON.parse(jsonString);
|
||||
} catch {
|
||||
parsedJSON = {};
|
||||
}
|
||||
|
||||
return parsedJSON;
|
||||
},
|
||||
deserializer: (json: any) =>
|
||||
json && typeof json === 'object' ? JSON.stringify(json, null, 2) : '{\n\n}',
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.isJsonField(
|
||||
i18n.translate('xpack.idxMgmt.enrichPolicyCreate.configurationStep.queryInvalidError', {
|
||||
defaultMessage: 'The query is not valid JSON.',
|
||||
}),
|
||||
{ allowEmptyString: true }
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ConfigurationStep = ({ onNext }: Props) => {
|
||||
const {
|
||||
core: {
|
||||
application: { getUrlForApp },
|
||||
},
|
||||
} = useAppContext();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const { draft, updateDraft, updateCompletionState } = useCreatePolicyContext();
|
||||
|
||||
const { form } = useForm({
|
||||
defaultValue: draft,
|
||||
schema: configurationFormSchema,
|
||||
id: 'configurationForm',
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
const { isValid, data } = await form.submit();
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update form completion state
|
||||
updateCompletionState((prevCompletionState) => ({
|
||||
...prevCompletionState,
|
||||
configurationStep: true,
|
||||
}));
|
||||
|
||||
// Update draft state with the data of the form
|
||||
updateDraft((prevDraft: DraftPolicy) => ({
|
||||
...prevDraft,
|
||||
...(isEmpty(data.query) ? omit(data, 'query') : data),
|
||||
}));
|
||||
|
||||
// And then navigate to the next step
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} data-test-subj="configurationForm">
|
||||
<UseField
|
||||
path="name"
|
||||
component={TextField}
|
||||
componentProps={{ fullWidth: false }}
|
||||
data-test-subj="policyNameField"
|
||||
/>
|
||||
|
||||
<UseField
|
||||
path="type"
|
||||
component={SelectField}
|
||||
labelAppend={
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiLink
|
||||
data-test-subj="typePopoverIcon"
|
||||
onClick={() => setIsPopoverOpen((isOpen) => !isOpen)}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</EuiLink>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.typeTitlePopOver"
|
||||
defaultMessage="Determines how to match the data to incoming documents."
|
||||
/>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.matchTypePopOver"
|
||||
defaultMessage="{type} matches an exact value."
|
||||
values={{ type: <EuiCode transparentBackground>Match</EuiCode> }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.geoMatchTypePopOver"
|
||||
defaultMessage="{type} matches a geographic location."
|
||||
values={{ type: <EuiCode transparentBackground>Geo match</EuiCode> }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.rangeTypePopOver"
|
||||
defaultMessage="{type} matches a number, date, or IP address range."
|
||||
values={{ type: <EuiCode transparentBackground>Range</EuiCode> }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</EuiText>
|
||||
</EuiPopover>
|
||||
}
|
||||
componentProps={{
|
||||
fullWidth: false,
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'policyTypeField',
|
||||
options: [
|
||||
{
|
||||
value: 'match',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.configurationStep.matchOption',
|
||||
{ defaultMessage: 'Match' }
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'geo_match',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.configurationStep.geoMatchOption',
|
||||
{ defaultMessage: 'Geo match' }
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'range',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.configurationStep.rangeOption',
|
||||
{ defaultMessage: 'Range' }
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<UseField
|
||||
path="sourceIndices"
|
||||
component={IndicesSelector}
|
||||
labelAppend={
|
||||
<EuiText size="xs">
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
data-test-subj="uploadFileLink"
|
||||
href={getUrlForApp('home', { path: '#/tutorial_directory/fileDataViz' })}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.uploadFileLink"
|
||||
defaultMessage="Upload a file"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'policySourceIndicesField',
|
||||
},
|
||||
fullWidth: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<UseField
|
||||
path="query"
|
||||
component={JsonEditorField}
|
||||
componentProps={{
|
||||
fullWidth: false,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.queryHelpText"
|
||||
defaultMessage="Defaults to: {code} query."
|
||||
values={{
|
||||
code: (
|
||||
<EuiLink
|
||||
external
|
||||
target="_blank"
|
||||
data-test-subj="matchAllQueryLink"
|
||||
href={documentationService.getMatchAllQueryLink()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.matchAllLink"
|
||||
defaultMessage="match_all"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
codeEditorProps: {
|
||||
height: '300px',
|
||||
allowFullScreen: true,
|
||||
'aria-label': i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.configurationStep.ariaLabelQuery',
|
||||
{
|
||||
defaultMessage: 'Query field data editor',
|
||||
}
|
||||
),
|
||||
options: {
|
||||
lineNumbers: 'off',
|
||||
tabSize: 2,
|
||||
automaticLayout: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiFlexGroup
|
||||
data-test-subj="configureStep"
|
||||
justifyContent="spaceBetween"
|
||||
style={{ maxWidth: 400 }}
|
||||
>
|
||||
<EuiFlexItem grow={false} />
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
iconSide="right"
|
||||
iconType="arrowRight"
|
||||
disabled={form.isValid === false}
|
||||
data-test-subj="nextButton"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.configurationStep.nextButtonLabel"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTabbedContent,
|
||||
EuiSpacer,
|
||||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { SerializedEnrichPolicy } from '../../../../../common';
|
||||
import { useCreatePolicyContext } from '../create_policy_context';
|
||||
import { serializeAsESPolicy, getESPolicyCreationApiCall } from '../../../../../common/lib';
|
||||
|
||||
// Beyond a certain point, highlighting the syntax will bog down performance to unacceptable
|
||||
// levels. This way we prevent that happening for very large requests.
|
||||
const getLanguageForQuery = (query: string) => (query.length < 60000 ? 'json' : undefined);
|
||||
|
||||
const SummaryTab = ({ policy }: { policy: SerializedEnrichPolicy }) => {
|
||||
const queryAsString = policy.query ? JSON.stringify(policy.query, null, 2) : '';
|
||||
const language = getLanguageForQuery(queryAsString);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiDescriptionList data-test-subj="enrichPolicySummaryList">
|
||||
{/* Policy name */}
|
||||
{policy.name && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.nameLabel', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyNameValue">
|
||||
{policy.name}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy type */}
|
||||
{policy.type && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.typeLabel', {
|
||||
defaultMessage: 'Type',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyTypeValue">
|
||||
{policy.type}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy source indices */}
|
||||
{policy.sourceIndices && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.sourceIndicesLabel', {
|
||||
defaultMessage: 'Source indices',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyIndicesValue">
|
||||
<EuiText size="s">
|
||||
<ul>
|
||||
{policy.sourceIndices.map((index: string) => (
|
||||
<li key={index} className="eui-textBreakWord">
|
||||
{index}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy match field */}
|
||||
{policy.matchField && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.matchFieldLabel', {
|
||||
defaultMessage: 'Match field',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyMatchFieldValue">
|
||||
{policy.matchField}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy enrich fields */}
|
||||
{policy.enrichFields && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.enrichFieldsLabel', {
|
||||
defaultMessage: 'Enrich fields',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyEnrichFieldsValue">
|
||||
<EuiText size="s">
|
||||
<ul>
|
||||
{policy.enrichFields.map((field: string) => (
|
||||
<li key={field} className="eui-textBreakWord">
|
||||
{field}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy query */}
|
||||
{policy.query && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.queryLabel', {
|
||||
defaultMessage: 'Query',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiCodeBlock language={language} isCopyable>
|
||||
{queryAsString}
|
||||
</EuiCodeBlock>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
</EuiDescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestTab = ({ policy }: { policy: SerializedEnrichPolicy }) => {
|
||||
const request = JSON.stringify(serializeAsESPolicy(policy), null, 2);
|
||||
const language = getLanguageForQuery(request);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.createStep.descriptionText"
|
||||
defaultMessage="This request will create the following enrich policy."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiCodeBlock language={language} isCopyable data-test-subj="requestBody">
|
||||
{getESPolicyCreationApiCall(policy.name)}
|
||||
{`\n`}
|
||||
{request}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CREATE_AND_EXECUTE_POLICY = true;
|
||||
interface Props {
|
||||
onSubmit: (executePolicyAfterCreation?: boolean) => void;
|
||||
onBack: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const CreateStep = ({ onBack, onSubmit, isLoading }: Props) => {
|
||||
const { draft } = useCreatePolicyContext();
|
||||
|
||||
const summaryTabs = [
|
||||
{
|
||||
id: 'summary',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.summaryTabLabel', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
'data-test-subj': 'summaryTab',
|
||||
content: <SummaryTab policy={draft as SerializedEnrichPolicy} />,
|
||||
},
|
||||
{
|
||||
id: 'request',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.createStep.requestTabLabel', {
|
||||
defaultMessage: 'Request',
|
||||
}),
|
||||
'data-test-subj': 'requestTab',
|
||||
content: <RequestTab policy={draft as SerializedEnrichPolicy} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTabbedContent tabs={summaryTabs} />
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiFlexGroup
|
||||
data-test-subj="creationStep"
|
||||
justifyContent="spaceBetween"
|
||||
style={{ maxWidth: 400 }}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
minWidth={false}
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="arrowLeft"
|
||||
data-test-subj="backButton"
|
||||
onClick={onBack}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.createStep.backButtonLabel"
|
||||
defaultMessage="Back"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
onClick={() => onSubmit(CREATE_AND_EXECUTE_POLICY)}
|
||||
isDisabled={isLoading}
|
||||
data-test-subj="createAndExecuteButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.createStep.createAndExecuteButtonLabel"
|
||||
defaultMessage="Create and execute"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
onClick={() => onSubmit()}
|
||||
isDisabled={isLoading}
|
||||
data-test-subj="createButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.createStep.createButtonLabel"
|
||||
defaultMessage="Create"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiIconTip,
|
||||
EuiSpacer,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { FieldIcon as KbnFieldIcon } from '@kbn/react-field';
|
||||
import {
|
||||
useForm,
|
||||
Form,
|
||||
fieldValidators,
|
||||
FormSchema,
|
||||
UseField,
|
||||
FIELD_TYPES,
|
||||
ComboBoxField,
|
||||
} from '../../../../shared_imports';
|
||||
|
||||
import type { IndexWithFields, FieldItem } from '../../../../../common';
|
||||
import { getFieldsFromIndices } from '../../../services/api';
|
||||
import { useCreatePolicyContext, DraftPolicy } from '../create_policy_context';
|
||||
|
||||
interface Props {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const fieldSelectionFormSchema: FormSchema = {
|
||||
matchField: {
|
||||
// Since this ComboBoxField is not a multi-select, we need to serialize/deserialize the value
|
||||
// into a string to be able to save it in the policy.
|
||||
defaultValue: '',
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
serializer: (v: string[]) => {
|
||||
return v.join(', ');
|
||||
},
|
||||
deserializer: (v: string) => {
|
||||
return v.length === 0 ? [] : v.split(', ');
|
||||
},
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.matchFieldField', {
|
||||
defaultMessage: 'Match field',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.emptyField(
|
||||
i18n.translate('xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.matchFieldRequired', {
|
||||
defaultMessage: 'A match field is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
enrichFields: {
|
||||
defaultValue: [],
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.enrichFieldsField', {
|
||||
defaultMessage: 'Enrich fields',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: fieldValidators.emptyField(
|
||||
i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.enrichFieldsRequired',
|
||||
{
|
||||
defaultMessage: 'At least one enrich field is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const buildFieldOption = (field: FieldItem) => ({
|
||||
label: field.name,
|
||||
prepend: <KbnFieldIcon type={field.normalizedType} />,
|
||||
});
|
||||
|
||||
export const FieldSelectionStep = ({ onBack, onNext }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [fieldOptions, setFieldOptions] = useState<EuiComboBoxOptionOption[]>([]);
|
||||
const [matchFieldOptions, setMatchFieldOptions] = useState<EuiComboBoxOptionOption[]>([]);
|
||||
const { draft, updateDraft, updateCompletionState } = useCreatePolicyContext();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFields = async () => {
|
||||
setIsLoading(true);
|
||||
const { data } = await getFieldsFromIndices(draft.sourceIndices as string[]);
|
||||
setIsLoading(false);
|
||||
|
||||
if (data?.commonFields?.length) {
|
||||
setMatchFieldOptions(data.commonFields.map(buildFieldOption));
|
||||
// If there is only one index, we can use the fields of that index as match field options
|
||||
} else if (data?.indices?.length === 1) {
|
||||
setMatchFieldOptions(data.indices[0].fields.map(buildFieldOption));
|
||||
}
|
||||
|
||||
if (data?.indices?.length) {
|
||||
setFieldOptions(
|
||||
data.indices.map((index: IndexWithFields) => ({
|
||||
label: index.index,
|
||||
options: index.fields.map(buildFieldOption),
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFields();
|
||||
}, [draft.sourceIndices]);
|
||||
|
||||
const { form } = useForm({
|
||||
defaultValue: draft,
|
||||
schema: fieldSelectionFormSchema,
|
||||
id: 'fieldSelectionForm',
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
const { isValid, data } = await form.submit();
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update form completion state
|
||||
updateCompletionState((prevCompletionState) => ({
|
||||
...prevCompletionState,
|
||||
fieldsSelectionStep: true,
|
||||
}));
|
||||
|
||||
// Update draft state with the data of the form
|
||||
updateDraft((prevDraft: DraftPolicy) => ({
|
||||
...prevDraft,
|
||||
...data,
|
||||
}));
|
||||
|
||||
// And then navigate to the next step
|
||||
onNext();
|
||||
};
|
||||
|
||||
const hasSelectedMultipleIndices = (draft.sourceIndices?.length ?? 0) > 1;
|
||||
|
||||
return (
|
||||
<Form form={form} data-test-subj="fieldSelectionForm">
|
||||
{!isLoading && hasSelectedMultipleIndices && matchFieldOptions.length === 0 && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.idxMgmt.enrichPolicyCreate.noCommonFieldsFoundError', {
|
||||
defaultMessage: 'No common fields',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
data-test-subj="noCommonFieldsError"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.matchFieldError"
|
||||
defaultMessage="The selected indices don't have any fields in common."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
<UseField
|
||||
path="matchField"
|
||||
component={ComboBoxField}
|
||||
labelAppend={
|
||||
<EuiIconTip
|
||||
data-test-subj="matchFieldPopover"
|
||||
content={i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.matchFieldPopover',
|
||||
{
|
||||
defaultMessage:
|
||||
'The field in your source indices to match with the incoming documents.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
}
|
||||
componentProps={{
|
||||
fullWidth: false,
|
||||
helpText: hasSelectedMultipleIndices
|
||||
? i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.matchFieldHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The match field must be the same across all selected source indices.',
|
||||
}
|
||||
)
|
||||
: undefined,
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'matchField',
|
||||
placeholder: i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.matchFieldPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select a field',
|
||||
}
|
||||
),
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
options: matchFieldOptions,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<UseField
|
||||
path="enrichFields"
|
||||
component={ComboBoxField}
|
||||
labelAppend={
|
||||
<EuiIconTip
|
||||
data-test-subj="enrichFieldsPopover"
|
||||
content={i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.EnrichFieldsFieldPopover',
|
||||
{
|
||||
defaultMessage: 'The fields to add to your incoming documents.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
}
|
||||
componentProps={{
|
||||
fullWidth: false,
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'enrichFields',
|
||||
placeholder: i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.enrichFieldsPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select fields to enrich',
|
||||
}
|
||||
),
|
||||
noSuggestions: false,
|
||||
options: fieldOptions,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiFlexGroup
|
||||
data-test-subj="fieldSelectionStep"
|
||||
justifyContent="spaceBetween"
|
||||
style={{ maxWidth: 400 }}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="arrowLeft"
|
||||
data-test-subj="backButton"
|
||||
onClick={onBack}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.backButtonLabel"
|
||||
defaultMessage="Back"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
iconSide="right"
|
||||
iconType="arrowRight"
|
||||
disabled={form.isValid === false}
|
||||
data-test-subj="nextButton"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicyCreate.fieldSelectionStep.nextButtonLabel"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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, useCallback, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { uniq, isEmpty } from 'lodash';
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import type { EuiComboBoxProps } from '@elastic/eui';
|
||||
import { getMatchingIndices } from '../../../../services/api';
|
||||
import type { FieldHook } from '../../../../../shared_imports';
|
||||
import { getFieldValidityAndErrorMessage } from '../../../../../shared_imports';
|
||||
|
||||
interface IOption {
|
||||
label: string;
|
||||
options: Array<{ value: string; label: string; key?: string }>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps: EuiComboBoxProps<string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const getIndexOptions = async (patternString: string) => {
|
||||
const options: IOption[] = [];
|
||||
|
||||
if (!patternString) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const { data } = await getMatchingIndices(patternString);
|
||||
const matchingIndices = data.indices;
|
||||
|
||||
if (matchingIndices.length) {
|
||||
const matchingOptions = uniq([...matchingIndices]);
|
||||
|
||||
options.push({
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.indicesSelector.optionsLabel', {
|
||||
defaultMessage: 'Based on your indices',
|
||||
}),
|
||||
options: matchingOptions
|
||||
.map((match) => {
|
||||
return {
|
||||
label: match,
|
||||
value: match,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label)),
|
||||
});
|
||||
} else {
|
||||
options.push({
|
||||
label: i18n.translate('xpack.idxMgmt.enrichPolicyCreate.indicesSelector.noMatchingOption', {
|
||||
defaultMessage: 'No indices match your search criteria.',
|
||||
}),
|
||||
options: [],
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
export const IndicesSelector = ({ field, euiFieldProps, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const [indexOptions, setIndexOptions] = useState<IOption[]>([]);
|
||||
const [isIndiciesLoading, setIsIndiciesLoading] = useState<boolean>(false);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
async (search: string) => {
|
||||
const indexPattern = isEmpty(search) ? '*' : search;
|
||||
|
||||
setIsIndiciesLoading(true);
|
||||
setIndexOptions(await getIndexOptions(indexPattern));
|
||||
setIsIndiciesLoading(false);
|
||||
},
|
||||
[setIsIndiciesLoading, setIndexOptions]
|
||||
);
|
||||
|
||||
// Fetch indices on mount so that the ComboBox has some initial options
|
||||
useEffect(() => {
|
||||
if (isEmpty(field.value)) {
|
||||
onSearchChange('*');
|
||||
}
|
||||
}, [field.value, onSearchChange]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={typeof field.helpText === 'function' ? field.helpText() : field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
{...rest}
|
||||
>
|
||||
<EuiComboBox
|
||||
async
|
||||
isLoading={isIndiciesLoading}
|
||||
options={indexOptions}
|
||||
noSuggestions={!indexOptions.length}
|
||||
selectedOptions={((field.value as string[]) || []).map((anIndex: string) => {
|
||||
return {
|
||||
label: anIndex,
|
||||
value: anIndex,
|
||||
};
|
||||
})}
|
||||
onChange={async (selected: EuiComboBoxOptionOption[]) => {
|
||||
field.setValue(selected.map((aSelected) => aSelected.value) as string[]);
|
||||
}}
|
||||
onSearchChange={onSearchChange}
|
||||
onBlur={() => {
|
||||
if (!field.value) {
|
||||
field.setValue([]);
|
||||
}
|
||||
}}
|
||||
data-test-subj="comboBox"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { ConfigurationStep } from './configuration';
|
||||
export { FieldSelectionStep } from './field_selection';
|
||||
export { CreateStep } from './create';
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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, { useEffect, useState, useRef } from 'react';
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { deleteEnrichPolicy } from '../../../../services/api';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
|
||||
export const DeletePolicyModal = ({
|
||||
policyToDelete,
|
||||
callback,
|
||||
}: {
|
||||
policyToDelete: string;
|
||||
callback: (data?: { hasDeletedPolicy: boolean }) => void;
|
||||
}) => {
|
||||
const mounted = useRef(false);
|
||||
const {
|
||||
services: { notificationService },
|
||||
} = useAppContext();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// Since the async action of this component needs to set state after unmounting,
|
||||
// we need to track the mounted state of this component to avoid a memory leak.
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDeletePolicy = () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
deleteEnrichPolicy(policyToDelete)
|
||||
.then(({ data, error }) => {
|
||||
if (data) {
|
||||
const successMessage = i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicies.deleteModal.successDeleteNotificationMessage',
|
||||
{ defaultMessage: 'Deleted {policyToDelete}', values: { policyToDelete } }
|
||||
);
|
||||
notificationService.showSuccessToast(successMessage);
|
||||
|
||||
return callback({ hasDeletedPolicy: true });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicies.deleteModal.errorDeleteNotificationMessage',
|
||||
{
|
||||
defaultMessage: "Error deleting enrich policy: '{error}'",
|
||||
values: { error: error.message },
|
||||
}
|
||||
);
|
||||
notificationService.showDangerToast(errorMessage);
|
||||
}
|
||||
|
||||
callback();
|
||||
})
|
||||
.finally(() => {
|
||||
if (mounted.current) {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnCancel = () => {
|
||||
callback();
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
data-test-subj="deletePolicyModal"
|
||||
title={i18n.translate('xpack.idxMgmt.enrichPolicies.deleteModal.confirmTitle', {
|
||||
defaultMessage: 'Delete enrich policy',
|
||||
})}
|
||||
onCancel={handleOnCancel}
|
||||
onConfirm={handleDeletePolicy}
|
||||
cancelButtonText={i18n.translate('xpack.idxMgmt.enrichPolicies.deleteModal.cancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
confirmButtonText={i18n.translate('xpack.idxMgmt.enrichPolicies.deleteModal.deleteButton', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
confirmButtonDisabled={isDeleting}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.deleteModal.bodyCopy"
|
||||
defaultMessage="You are about to delete the enrich policy {policy}. This action is irreversible."
|
||||
values={{
|
||||
policy: <strong>{policyToDelete}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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, { useEffect, useState, useRef } from 'react';
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { executeEnrichPolicy } from '../../../../services/api';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
|
||||
export const ExecutePolicyModal = ({
|
||||
policyToExecute,
|
||||
callback,
|
||||
}: {
|
||||
policyToExecute: string;
|
||||
callback: (data?: { hasExecutedPolicy: boolean }) => void;
|
||||
}) => {
|
||||
const mounted = useRef(false);
|
||||
const {
|
||||
services: { notificationService },
|
||||
} = useAppContext();
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
|
||||
// Since the async action of this component needs to set state after unmounting,
|
||||
// we need to track the mounted state of this component to avoid a memory leak.
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleExecutePolicy = () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
executeEnrichPolicy(policyToExecute)
|
||||
.then(({ data, error }) => {
|
||||
if (data) {
|
||||
const successMessage = i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicies.executeModal.successExecuteNotificationMessage',
|
||||
{ defaultMessage: 'Executed {policyToExecute}', values: { policyToExecute } }
|
||||
);
|
||||
notificationService.showSuccessToast(successMessage);
|
||||
|
||||
return callback({ hasExecutedPolicy: true });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicies.executeModal.errorExecuteNotificationMessage',
|
||||
{
|
||||
defaultMessage: "Error executing enrich policy: '{error}'",
|
||||
values: { error: error.message },
|
||||
}
|
||||
);
|
||||
notificationService.showDangerToast(errorMessage);
|
||||
}
|
||||
|
||||
callback();
|
||||
})
|
||||
.finally(() => {
|
||||
if (mounted.current) {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnCancel = () => {
|
||||
callback();
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
data-test-subj="executePolicyModal"
|
||||
title={i18n.translate('xpack.idxMgmt.enrichPolicies.executeModal.confirmTitle', {
|
||||
defaultMessage: 'Execute enrich policy',
|
||||
})}
|
||||
onCancel={handleOnCancel}
|
||||
onConfirm={handleExecutePolicy}
|
||||
cancelButtonText={i18n.translate('xpack.idxMgmt.enrichPolicies.executeModal.cancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
confirmButtonText={i18n.translate('xpack.idxMgmt.enrichPolicies.executeModal.executeButton', {
|
||||
defaultMessage: 'Execute',
|
||||
})}
|
||||
confirmButtonDisabled={isExecuting}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.executeModal.bodyCopy"
|
||||
defaultMessage="You are about to execute the enrich policy {policy}."
|
||||
values={{
|
||||
policy: <strong>{policyToExecute}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { DeletePolicyModal } from './delete_policy_modal';
|
||||
export { ExecutePolicyModal } from './execute_policy_modal';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { PolicyDetailsFlyout } from './policy_details_flyout';
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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, { FunctionComponent } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiTitle,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
import type { SerializedEnrichPolicy } from '../../../../../../common';
|
||||
|
||||
export interface Props {
|
||||
policy: SerializedEnrichPolicy;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const PolicyDetailsFlyout: FunctionComponent<Props> = ({ policy, onClose }) => {
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} data-test-subj="policyDetailsFlyout" size="m" maxWidth={550}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle id="policyDetailsFlyoutTitle" data-test-subj="title">
|
||||
<h2>{policy.name}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EuiDescriptionList>
|
||||
{/* Policy type */}
|
||||
{policy.type && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicies.detailsFlyout.typeTitle', {
|
||||
defaultMessage: 'Type',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyTypeValue">
|
||||
{policy.type}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy source indices */}
|
||||
{policy.sourceIndices && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicies.detailsFlyout.sourceIndicesTitle', {
|
||||
defaultMessage: 'Source indices',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyIndicesValue">
|
||||
<span className="eui-textBreakWord">{policy.sourceIndices.join(', ')}</span>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy match field */}
|
||||
{policy.matchField && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicies.detailsFlyout.matchFieldTitle', {
|
||||
defaultMessage: 'Match field',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyMatchFieldValue">
|
||||
{policy.matchField}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy enrich fields */}
|
||||
{policy.enrichFields && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicies.detailsFlyout.enrichFieldsTitle', {
|
||||
defaultMessage: 'Enrich fields',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="policyEnrichFieldsValue">
|
||||
<span className="eui-textBreakWord">{policy.enrichFields.join(', ')}</span>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policy query */}
|
||||
{policy.query && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicies.detailsFlyout.queryTitle', {
|
||||
defaultMessage: 'Query',
|
||||
})}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<CodeEditor
|
||||
languageId="json"
|
||||
isCopyable
|
||||
allowFullScreen
|
||||
value={JSON.stringify(policy.query, null, 2)}
|
||||
data-test-subj="queryEditor"
|
||||
height={250}
|
||||
options={{
|
||||
lineNumbers: 'off',
|
||||
tabSize: 2,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.idxMgmt.enrichPolicies.detailsFlyout.queryAriaLabel',
|
||||
{ defaultMessage: 'Enrich policy query editor' }
|
||||
)}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
</EuiDescriptionList>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
onClick={onClose}
|
||||
flush="left"
|
||||
data-test-subj="closeFlyoutButton"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.enrichPolicies.detailsFlyout.closeButton', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -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 React from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
|
||||
export const EmptyState = () => {
|
||||
const { history } = useAppContext();
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate.EmptyPrompt
|
||||
iconType="managementApp"
|
||||
data-test-subj="sectionEmpty"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.emptyPromptTitle"
|
||||
defaultMessage="Add your first enrich policy"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.emptyPromptDescription"
|
||||
defaultMessage="Use an enrich policy to add data from existing indices into incoming documents during ingest."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="enrichPoliciesEmptyPromptCreateButton"
|
||||
{...reactRouterNavigate(history, '/enrich_policies/create')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.emptyPromptButtonLabel"
|
||||
defaultMessage="Add an enrich policy"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButton, EuiPageTemplate, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { useLoadEnrichPolicies } from '../../../../services/api';
|
||||
|
||||
export const ErrorState = ({
|
||||
error,
|
||||
resendRequest,
|
||||
}: {
|
||||
error: {
|
||||
error: string;
|
||||
message?: string;
|
||||
};
|
||||
resendRequest: ReturnType<typeof useLoadEnrichPolicies>['resendRequest'];
|
||||
}) => {
|
||||
return (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
data-test-subj="sectionError"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.errorTitle"
|
||||
defaultMessage="Unable to load enrich policies"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<EuiText color="subdued">
|
||||
<p className="eui-textBreakWord">{error?.message || error?.error}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiButton iconSide="right" onClick={resendRequest} iconType="refresh" color="danger">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.errorReloadButton"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { ErrorState } from './error_state';
|
||||
export { LoadingState } from './loading_state';
|
||||
export { EmptyState } from './empty_state';
|
|
@ -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 React from 'react';
|
||||
import { EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
export const LoadingState = () => {
|
||||
return (
|
||||
<KibanaPageTemplate.EmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
body={
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.loadingStateLabel"
|
||||
defaultMessage="Loading enrich policies…"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="sectionLoading"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiSpacer, EuiText, EuiLink } from '@elastic/eui';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Location } from 'history';
|
||||
import { parse } from 'query-string';
|
||||
|
||||
import { APP_WRAPPER_CLASS, useExecutionContext } from '../../../../shared_imports';
|
||||
import type { SerializedEnrichPolicy } from '../../../../../common';
|
||||
import { useAppContext } from '../../../app_context';
|
||||
import { useRedirectPath } from '../../../hooks/redirect_path';
|
||||
|
||||
import { breadcrumbService, IndexManagementBreadcrumb } from '../../../services/breadcrumbs';
|
||||
import { documentationService } from '../../../services/documentation';
|
||||
import { useLoadEnrichPolicies } from '../../../services/api';
|
||||
import { PoliciesTable } from './policies_table';
|
||||
import { DeletePolicyModal, ExecutePolicyModal } from './confirm_modals';
|
||||
import { LoadingState, ErrorState, EmptyState } from './empty_states';
|
||||
import { PolicyDetailsFlyout } from './details_flyout';
|
||||
|
||||
const getEnrichPolicyNameFromLocation = (location: Location) => {
|
||||
const { policy } = parse(location.search.substring(1));
|
||||
return policy;
|
||||
};
|
||||
|
||||
export const EnrichPoliciesList: React.FunctionComponent<RouteComponentProps> = ({
|
||||
history,
|
||||
location,
|
||||
}) => {
|
||||
const {
|
||||
core: { executionContext },
|
||||
} = useAppContext();
|
||||
const redirectTo = useRedirectPath(history);
|
||||
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs(IndexManagementBreadcrumb.enrichPolicies);
|
||||
}, []);
|
||||
|
||||
useExecutionContext(executionContext, {
|
||||
type: 'application',
|
||||
page: 'indexManagementEnrichPoliciesTab',
|
||||
});
|
||||
|
||||
// Policy details flyout
|
||||
const enrichPolicyNameFromLocation = getEnrichPolicyNameFromLocation(location);
|
||||
const [showFlyoutFor, setShowFlyoutFor] = useState<SerializedEnrichPolicy | undefined>();
|
||||
|
||||
// Policy table actions
|
||||
const [policyToDelete, setPolicyToDelete] = useState<string | undefined>();
|
||||
const [policyToExecute, setPolicyToExecute] = useState<string | undefined>();
|
||||
|
||||
const {
|
||||
error,
|
||||
isLoading,
|
||||
data: policies,
|
||||
resendRequest: reloadPolicies,
|
||||
} = useLoadEnrichPolicies();
|
||||
|
||||
useEffect(() => {
|
||||
if (enrichPolicyNameFromLocation && policies?.length) {
|
||||
const policy = policies.find((p) => p.name === enrichPolicyNameFromLocation);
|
||||
setShowFlyoutFor(policy);
|
||||
}
|
||||
}, [enrichPolicyNameFromLocation, policies]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorState error={error} resendRequest={reloadPolicies} />;
|
||||
}
|
||||
|
||||
if (policies?.length === 0) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={APP_WRAPPER_CLASS} data-test-subj="enrichPoliciesList">
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.descriptionTitle"
|
||||
defaultMessage="Use an enrich policy to automatically enhance your incoming documents with data from your existing indices. {learnMoreLink}"
|
||||
values={{
|
||||
learnMoreLink: (
|
||||
<EuiLink
|
||||
href={documentationService.getEnrichApisLink()}
|
||||
target="_blank"
|
||||
external
|
||||
data-test-subj="enrichPoliciesLearnMoreLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.list.docsLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<PoliciesTable
|
||||
policies={policies as SerializedEnrichPolicy[]}
|
||||
onReloadClick={reloadPolicies}
|
||||
onDeletePolicyClick={setPolicyToDelete}
|
||||
onExecutePolicyClick={setPolicyToExecute}
|
||||
/>
|
||||
|
||||
{policyToDelete && (
|
||||
<DeletePolicyModal
|
||||
policyToDelete={policyToDelete}
|
||||
callback={(deleteResponse) => {
|
||||
if (deleteResponse?.hasDeletedPolicy) {
|
||||
reloadPolicies();
|
||||
}
|
||||
setPolicyToDelete(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{policyToExecute && (
|
||||
<ExecutePolicyModal
|
||||
policyToExecute={policyToExecute}
|
||||
callback={(executeResponse) => {
|
||||
if (executeResponse?.hasExecutedPolicy) {
|
||||
reloadPolicies();
|
||||
}
|
||||
setPolicyToExecute(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFlyoutFor && (
|
||||
<PolicyDetailsFlyout
|
||||
policy={showFlyoutFor}
|
||||
onClose={() => {
|
||||
setShowFlyoutFor(undefined);
|
||||
redirectTo('/enrich_policies');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { EnrichPoliciesList } from './enrich_policies_list';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { PoliciesTable } from './policies_table';
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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, { FunctionComponent } from 'react';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiSearchBarProps,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import type { SerializedEnrichPolicy } from '../../../../../../common/types';
|
||||
|
||||
export interface Props {
|
||||
policies: SerializedEnrichPolicy[];
|
||||
onReloadClick: () => void;
|
||||
onDeletePolicyClick: (policyName: string) => void;
|
||||
onExecutePolicyClick: (policyName: string) => void;
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 50,
|
||||
pageSizeOptions: [25, 50, 100],
|
||||
};
|
||||
|
||||
export const PoliciesTable: FunctionComponent<Props> = ({
|
||||
policies,
|
||||
onReloadClick,
|
||||
onDeletePolicyClick,
|
||||
onExecutePolicyClick,
|
||||
}) => {
|
||||
const { history } = useAppContext();
|
||||
|
||||
const renderToolsRight = () => {
|
||||
return [
|
||||
<EuiButton
|
||||
key="reloadPolicies"
|
||||
data-test-subj="reloadPoliciesButton"
|
||||
iconType="refresh"
|
||||
color="success"
|
||||
onClick={onReloadClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.table.reloadButton"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
key="createPolicy"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
{...reactRouterNavigate(history, '/enrich_policies/create')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.enrichPolicies.table.createPolicyButton"
|
||||
defaultMessage="Create enrich policy"
|
||||
/>
|
||||
</EuiButton>,
|
||||
];
|
||||
};
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
toolsRight: renderToolsRight(),
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<SerializedEnrichPolicy>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.nameField', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (name: string) => (
|
||||
<EuiLink
|
||||
data-test-subj="enrichPolicyDetailsLink"
|
||||
{...reactRouterNavigate(history, {
|
||||
pathname: '/enrich_policies',
|
||||
search: `policy=${encodeURIComponent(name)}`,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.typeField', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'sourceIndices',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.sourceIndicesField', {
|
||||
defaultMessage: 'Source indices',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (indices: string[]) => <span className="eui-textTruncate">{indices.join(', ')}</span>,
|
||||
},
|
||||
{
|
||||
field: 'matchField',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.matchFieldField', {
|
||||
defaultMessage: 'Match field',
|
||||
}),
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'enrichFields',
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.enrichFieldsField', {
|
||||
defaultMessage: 'Enrich fields',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (fields: string[]) => <span className="eui-textTruncate">{fields.join(', ')}</span>,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.actionsField', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
isPrimary: true,
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.executeAction', {
|
||||
defaultMessage: 'Execute',
|
||||
}),
|
||||
description: i18n.translate('xpack.idxMgmt.enrichPolicies.table.executeDescription', {
|
||||
defaultMessage: 'Execute this enrich policy',
|
||||
}),
|
||||
type: 'icon',
|
||||
icon: 'play',
|
||||
'data-test-subj': 'executePolicyButton',
|
||||
onClick: ({ name }) => onExecutePolicyClick(name),
|
||||
},
|
||||
{
|
||||
isPrimary: true,
|
||||
name: i18n.translate('xpack.idxMgmt.enrichPolicies.table.deleteAction', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
description: i18n.translate('xpack.idxMgmt.enrichPolicies.table.deleteDescription', {
|
||||
defaultMessage: 'Delete this enrich policy',
|
||||
}),
|
||||
type: 'icon',
|
||||
icon: 'trash',
|
||||
color: 'danger',
|
||||
'data-test-subj': 'deletePolicyButton',
|
||||
onClick: ({ name }) => onDeletePolicyClick(name),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="enrichPoliciesTable"
|
||||
items={policies}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
search={search}
|
||||
pagination={pagination}
|
||||
sorting={true}
|
||||
isSelectable={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,7 @@ import { documentationService } from '../../services/documentation';
|
|||
import { useAppContext } from '../../app_context';
|
||||
import { ComponentTemplateList } from '../../components/component_templates';
|
||||
import { IndexList } from './index_list';
|
||||
import { EnrichPoliciesList } from './enrich_policies_list';
|
||||
import { IndexDetailsPage } from './index_list/details_page';
|
||||
import { DataStreamList } from './data_stream_list';
|
||||
import { TemplateList } from './template_list';
|
||||
|
@ -24,6 +25,7 @@ export enum Section {
|
|||
DataStreams = 'data_streams',
|
||||
IndexTemplates = 'templates',
|
||||
ComponentTemplates = 'component_templates',
|
||||
EnrichPolicies = 'enrich_policies',
|
||||
}
|
||||
|
||||
export const homeSections = [
|
||||
|
@ -31,6 +33,7 @@ export const homeSections = [
|
|||
Section.DataStreams,
|
||||
Section.IndexTemplates,
|
||||
Section.ComponentTemplates,
|
||||
Section.EnrichPolicies,
|
||||
];
|
||||
|
||||
interface MatchParams {
|
||||
|
@ -78,6 +81,15 @@ export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: Section.EnrichPolicies,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.home.enrichPoliciesTabTitle"
|
||||
defaultMessage="Enrich Policies"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onSectionChange = (newSection: Section) => {
|
||||
|
@ -142,6 +154,7 @@ export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
]}
|
||||
component={ComponentTemplateList}
|
||||
/>
|
||||
<Route exact path={`/${Section.EnrichPolicies}`} component={EnrichPoliciesList} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { METRIC_TYPE } from '@kbn/analytics';
|
|||
import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
API_BASE_PATH,
|
||||
INTERNAL_API_BASE_PATH,
|
||||
UIM_UPDATE_SETTINGS,
|
||||
UIM_INDEX_CLEAR_CACHE,
|
||||
UIM_INDEX_CLEAR_CACHE_MANY,
|
||||
|
@ -32,7 +33,6 @@ import {
|
|||
UIM_TEMPLATE_UPDATE,
|
||||
UIM_TEMPLATE_CLONE,
|
||||
UIM_TEMPLATE_SIMULATE,
|
||||
INTERNAL_API_BASE_PATH,
|
||||
} from '../../../common/constants';
|
||||
import {
|
||||
TemplateDeserialized,
|
||||
|
@ -45,6 +45,7 @@ import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants';
|
|||
import { useRequest, sendRequest } from './use_request';
|
||||
import { httpService } from './http';
|
||||
import { UiMetricService } from './ui_metric';
|
||||
import type { SerializedEnrichPolicy, FieldFromIndicesRequest } from '../../../common';
|
||||
|
||||
interface ReloadIndicesOptions {
|
||||
asSystemRequest?: boolean;
|
||||
|
@ -320,6 +321,67 @@ export function useLoadNodesPlugins() {
|
|||
});
|
||||
}
|
||||
|
||||
export const useLoadEnrichPolicies = () => {
|
||||
return useRequest<SerializedEnrichPolicy[]>({
|
||||
path: `${INTERNAL_API_BASE_PATH}/enrich_policies`,
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export async function deleteEnrichPolicy(policyName: string) {
|
||||
const result = sendRequest({
|
||||
path: `${INTERNAL_API_BASE_PATH}/enrich_policies/${policyName}`,
|
||||
method: 'delete',
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function executeEnrichPolicy(policyName: string) {
|
||||
const result = sendRequest({
|
||||
path: `${INTERNAL_API_BASE_PATH}/enrich_policies/${policyName}`,
|
||||
method: 'put',
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createEnrichPolicy(
|
||||
policy: SerializedEnrichPolicy,
|
||||
executePolicyAfterCreation?: boolean
|
||||
) {
|
||||
const result = sendRequest({
|
||||
path: `${INTERNAL_API_BASE_PATH}/enrich_policies`,
|
||||
method: 'post',
|
||||
body: JSON.stringify({ policy }),
|
||||
query: {
|
||||
executePolicyAfterCreation,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getMatchingIndices(pattern: string) {
|
||||
const result = sendRequest({
|
||||
path: `${INTERNAL_API_BASE_PATH}/enrich_policies/get_matching_indices`,
|
||||
method: 'post',
|
||||
body: JSON.stringify({ pattern }),
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getFieldsFromIndices(indices: string[]) {
|
||||
const result = sendRequest<FieldFromIndicesRequest>({
|
||||
path: `${INTERNAL_API_BASE_PATH}/enrich_policies/get_fields_from_indices`,
|
||||
method: 'post',
|
||||
body: JSON.stringify({ indices }),
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function loadIndex(indexName: string) {
|
||||
return sendRequest<Index>({
|
||||
path: `${INTERNAL_API_BASE_PATH}/indices/${encodeURIComponent(indexName)}`,
|
||||
|
|
|
@ -10,6 +10,14 @@ import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
|||
|
||||
type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs'];
|
||||
|
||||
export enum IndexManagementBreadcrumb {
|
||||
/**
|
||||
* Enrich policies tab
|
||||
*/
|
||||
enrichPolicies = 'enrichPolicies',
|
||||
enrichPoliciesCreate = 'enrichPoliciesCreate',
|
||||
}
|
||||
|
||||
class BreadcrumbService {
|
||||
private breadcrumbs: {
|
||||
[key: string]: Array<{
|
||||
|
@ -69,6 +77,26 @@ class BreadcrumbService {
|
|||
}),
|
||||
},
|
||||
];
|
||||
|
||||
this.breadcrumbs.enrichPolicies = [
|
||||
...this.breadcrumbs.home,
|
||||
{
|
||||
text: i18n.translate('xpack.idxMgmt.breadcrumb.enrichPolicyLabel', {
|
||||
defaultMessage: 'Enrich policies',
|
||||
}),
|
||||
href: `/enrich_policies`,
|
||||
},
|
||||
];
|
||||
|
||||
this.breadcrumbs.enrichPoliciesCreate = [
|
||||
...this.breadcrumbs.enrichPolicies,
|
||||
{
|
||||
text: i18n.translate('xpack.idxMgmt.breadcrumb.enrichPolicyCreateLabel', {
|
||||
defaultMessage: 'Create enrich policy',
|
||||
}),
|
||||
href: `/enrich_policies/create`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public setBreadcrumbs(type: string): void {
|
||||
|
|
|
@ -14,6 +14,9 @@ class DocumentationService {
|
|||
|
||||
private dataStreams: string = '';
|
||||
private esDocsBase: string = '';
|
||||
private enrichPolicies: string = '';
|
||||
private createEnrichPolicies: string = '';
|
||||
private matchAllQuery: string = '';
|
||||
private indexManagement: string = '';
|
||||
private indexSettings: string = '';
|
||||
private indexTemplates: string = '';
|
||||
|
@ -67,6 +70,9 @@ class DocumentationService {
|
|||
|
||||
this.dataStreams = links.elasticsearch.dataStreams;
|
||||
this.esDocsBase = links.elasticsearch.docsBase;
|
||||
this.enrichPolicies = links.elasticsearch.enrichPolicies;
|
||||
this.createEnrichPolicies = links.elasticsearch.createEnrichPolicy;
|
||||
this.matchAllQuery = links.elasticsearch.matchAllQuery;
|
||||
this.indexManagement = links.management.indexManagement;
|
||||
this.indexSettings = links.elasticsearch.indexSettings;
|
||||
this.indexTemplates = links.elasticsearch.indexTemplates;
|
||||
|
@ -173,6 +179,18 @@ class DocumentationService {
|
|||
return this.mappingRankFeatureFields;
|
||||
}
|
||||
|
||||
public getEnrichApisLink() {
|
||||
return this.enrichPolicies;
|
||||
}
|
||||
|
||||
public getCreateEnrichPolicyLink() {
|
||||
return this.createEnrichPolicies;
|
||||
}
|
||||
|
||||
public getMatchAllQueryLink() {
|
||||
return this.matchAllQuery;
|
||||
}
|
||||
|
||||
public getMetaFieldLink() {
|
||||
return this.mappingMetaFields;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,12 @@ export {
|
|||
EuiCodeEditor,
|
||||
} from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
export type { FormSchema, FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
export type {
|
||||
FormSchema,
|
||||
FieldConfig,
|
||||
FieldHook,
|
||||
FieldValidateResponse,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
export {
|
||||
FIELD_TYPES,
|
||||
|
@ -39,6 +44,7 @@ export {
|
|||
getUseField,
|
||||
UseField,
|
||||
FormDataProvider,
|
||||
getFieldValidityAndErrorMessage,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
export {
|
||||
|
@ -50,8 +56,12 @@ export {
|
|||
export {
|
||||
getFormRow,
|
||||
Field,
|
||||
FormRow,
|
||||
TextField,
|
||||
SelectField,
|
||||
ToggleField,
|
||||
JsonEditorField,
|
||||
ComboBoxField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
|
||||
export { isJSON } from '@kbn/es-ui-shared-plugin/static/validators/string';
|
||||
|
|
|
@ -6,24 +6,9 @@
|
|||
*/
|
||||
|
||||
import { IScopedClusterClient } from '@kbn/core/server';
|
||||
import type { EnrichSummary, EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { EnrichSummary } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { SerializedEnrichPolicy } from '../../common/types';
|
||||
|
||||
const getPolicyType = (policy: EnrichSummary): EnrichPolicyType => {
|
||||
if (policy.config.match) {
|
||||
return 'match';
|
||||
}
|
||||
|
||||
if (policy.config.geo_match) {
|
||||
return 'geo_match';
|
||||
}
|
||||
|
||||
if (policy.config.range) {
|
||||
return 'range';
|
||||
}
|
||||
|
||||
throw new Error('Unknown policy type');
|
||||
};
|
||||
import { getPolicyType } from '../../common/lib';
|
||||
|
||||
export const serializeEnrichmentPolicies = (
|
||||
policies: EnrichSummary[]
|
||||
|
@ -37,6 +22,7 @@ export const serializeEnrichmentPolicies = (
|
|||
sourceIndices: policy.config[policyType].indices,
|
||||
matchField: policy.config[policyType].match_field,
|
||||
enrichFields: policy.config[policyType].enrich_fields,
|
||||
query: policy.config[policyType].query,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -47,6 +33,28 @@ const fetchAll = async (client: IScopedClusterClient) => {
|
|||
return serializeEnrichmentPolicies(res.policies);
|
||||
};
|
||||
|
||||
const create = (
|
||||
client: IScopedClusterClient,
|
||||
policyName: string,
|
||||
serializedPolicy: EnrichSummary['config']
|
||||
) => {
|
||||
return client.asCurrentUser.enrich.putPolicy({
|
||||
name: policyName,
|
||||
...serializedPolicy,
|
||||
});
|
||||
};
|
||||
|
||||
const execute = (client: IScopedClusterClient, policyName: string) => {
|
||||
return client.asCurrentUser.enrich.executePolicy({ name: policyName });
|
||||
};
|
||||
|
||||
const remove = (client: IScopedClusterClient, policyName: string) => {
|
||||
return client.asCurrentUser.enrich.deletePolicy({ name: policyName });
|
||||
};
|
||||
|
||||
export const enrichPoliciesActions = {
|
||||
fetchAll,
|
||||
create,
|
||||
execute,
|
||||
remove,
|
||||
};
|
||||
|
|
|
@ -58,4 +58,312 @@ describe('Enrich policies API', () => {
|
|||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Execute policy - PUT /api/index_management/enrich_policies/{policy}', () => {
|
||||
const executeEnrichPolicy = router.getMockESApiFn('enrich.executePolicy');
|
||||
|
||||
it('correctly executes a policy', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'put',
|
||||
path: addInternalBasePath('/enrich_policies/{name}'),
|
||||
params: {
|
||||
name: 'my-policy',
|
||||
},
|
||||
};
|
||||
|
||||
executeEnrichPolicy.mockResolvedValue({ status: { phase: 'COMPLETE' } });
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: { status: { phase: 'COMPLETE' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if it fails', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'put',
|
||||
path: addInternalBasePath('/enrich_policies/{name}'),
|
||||
params: {
|
||||
name: 'my-policy',
|
||||
},
|
||||
};
|
||||
|
||||
const error = new Error('Oh no!');
|
||||
executeEnrichPolicy.mockRejectedValue(error);
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete policy - DELETE /api/index_management/enrich_policies/{policy}', () => {
|
||||
const deleteEnrichPolicy = router.getMockESApiFn('enrich.deletePolicy');
|
||||
|
||||
it('correctly deletes a policy', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'delete',
|
||||
path: addInternalBasePath('/enrich_policies/{name}'),
|
||||
params: {
|
||||
name: 'my-policy',
|
||||
},
|
||||
};
|
||||
|
||||
deleteEnrichPolicy.mockResolvedValue({ status: { phase: 'COMPLETE' } });
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: { status: { phase: 'COMPLETE' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if it fails', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'delete',
|
||||
path: addInternalBasePath('/enrich_policies/{name}'),
|
||||
params: {
|
||||
name: 'my-policy',
|
||||
},
|
||||
};
|
||||
|
||||
const error = new Error('Oh no!');
|
||||
deleteEnrichPolicy.mockRejectedValue(error);
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create policy - POST /api/index_management/enrich_policies', () => {
|
||||
const createPolicyMock = router.getMockESApiFn('enrich.putPolicy');
|
||||
const executePolicyMock = router.getMockESApiFn('enrich.executePolicy');
|
||||
const deletePolicyMock = router.getMockESApiFn('enrich.deletePolicy');
|
||||
|
||||
it('correctly creates a policy', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies'),
|
||||
body: {
|
||||
policy: {
|
||||
name: 'my-policy',
|
||||
type: 'match',
|
||||
matchField: 'my_field',
|
||||
enrichFields: ['field_1', 'field_2'],
|
||||
sourceIndex: ['index_1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createPolicyMock.mockResolvedValue({ status: { status: 'OK' } });
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: { status: { status: 'OK' } },
|
||||
});
|
||||
|
||||
expect(executePolicyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can create a policy and execute it', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies'),
|
||||
query: {
|
||||
executePolicyAfterCreation: true,
|
||||
},
|
||||
body: {
|
||||
policy: {
|
||||
name: 'my-policy',
|
||||
type: 'match',
|
||||
matchField: 'my_field',
|
||||
enrichFields: ['field_1', 'field_2'],
|
||||
sourceIndex: ['index_1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createPolicyMock.mockResolvedValue({ status: { status: 'OK' } });
|
||||
executePolicyMock.mockResolvedValue({ status: { status: 'OK' } });
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: { status: { status: 'OK' } },
|
||||
});
|
||||
|
||||
expect(executePolicyMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('if when creating policy and executing the execution fails, the policy should be removed', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies'),
|
||||
query: {
|
||||
executePolicyAfterCreation: true,
|
||||
},
|
||||
body: {
|
||||
policy: {
|
||||
name: 'my-policy',
|
||||
type: 'match',
|
||||
matchField: 'my_field',
|
||||
enrichFields: ['field_1', 'field_2'],
|
||||
sourceIndex: ['index_1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
createPolicyMock.mockResolvedValue({ status: { status: 'OK' } });
|
||||
const executeError = new Error('Oh no!');
|
||||
executePolicyMock.mockRejectedValue(executeError);
|
||||
deletePolicyMock.mockResolvedValue({ status: { status: 'OK' } });
|
||||
|
||||
// Expect the API to fail and the policy to be deleted
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(executeError);
|
||||
expect(deletePolicyMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return an error if it fails', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies'),
|
||||
body: {
|
||||
policy: {
|
||||
name: 'my-policy',
|
||||
type: 'match',
|
||||
matchField: 'my_field',
|
||||
enrichFields: ['field_1', 'field_2'],
|
||||
sourceIndex: ['index_1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const error = new Error('Oh no!');
|
||||
createPolicyMock.mockRejectedValue(error);
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fields from indices - POST /api/index_management/enrich_policies/get_fields_from_indices', () => {
|
||||
const fieldCapsMock = router.getMockESApiFn('fieldCaps');
|
||||
|
||||
it('correctly returns fields and common fields for the selected indices', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies/get_fields_from_indices'),
|
||||
body: {
|
||||
indices: ['test-a', 'test-b'],
|
||||
},
|
||||
};
|
||||
|
||||
fieldCapsMock.mockResolvedValue({
|
||||
body: {
|
||||
indices: ['test-a'],
|
||||
fields: {
|
||||
name: { text: { type: 'text' } },
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
});
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: {
|
||||
indices: [
|
||||
{
|
||||
index: 'test-a',
|
||||
fields: [{ name: 'name', type: 'text', normalizedType: 'text' }],
|
||||
},
|
||||
{
|
||||
index: 'test-b',
|
||||
fields: [{ name: 'name', type: 'text', normalizedType: 'text' }],
|
||||
},
|
||||
],
|
||||
commonFields: [{ name: 'name', type: 'text', normalizedType: 'text' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(fieldCapsMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return an error if it fails', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies/get_fields_from_indices'),
|
||||
body: {
|
||||
indices: ['test-a'],
|
||||
},
|
||||
};
|
||||
|
||||
const error = new Error('Oh no!');
|
||||
fieldCapsMock.mockRejectedValue(error);
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get matching indices - POST /api/index_management/enrich_policies/get_matching_indices', () => {
|
||||
const getAliasMock = router.getMockESApiFn('indices.getAlias');
|
||||
const searchMock = router.getMockESApiFn('search');
|
||||
|
||||
it('Return matching indices using alias api', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies/get_matching_indices'),
|
||||
body: {
|
||||
pattern: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
getAliasMock.mockResolvedValue({
|
||||
body: {},
|
||||
statusCode: 200,
|
||||
});
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: {
|
||||
indices: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(searchMock).not.toHaveBeenCalled();
|
||||
expect(getAliasMock).toHaveBeenCalledWith(
|
||||
{ index: '*test*', expand_wildcards: 'open' },
|
||||
{ ignore: [404], meta: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('When alias api fails or returns nothing it fallsback to search api', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'post',
|
||||
path: addInternalBasePath('/enrich_policies/get_matching_indices'),
|
||||
body: {
|
||||
pattern: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
getAliasMock.mockResolvedValue({
|
||||
body: {},
|
||||
statusCode: 404,
|
||||
});
|
||||
|
||||
searchMock.mockResolvedValue({
|
||||
body: {},
|
||||
statusCode: 404,
|
||||
});
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: {
|
||||
indices: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(searchMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { getCommonFields, normalizeFieldsList } from './helpers';
|
||||
|
||||
const commonField = { name: 'name', type: 'keyword', normalizedType: 'keyword' };
|
||||
const fieldsPerIndexMock = [
|
||||
{
|
||||
index: 'index1',
|
||||
fields: [commonField, { name: 'age', type: 'long', normalizedType: 'number' }],
|
||||
},
|
||||
{
|
||||
index: 'index2',
|
||||
fields: [commonField, { name: 'email', type: 'keyword', normalizedType: 'keyword' }],
|
||||
},
|
||||
];
|
||||
|
||||
describe('getCommonFields', () => {
|
||||
it('should return common fields', () => {
|
||||
expect(getCommonFields(fieldsPerIndexMock)).toEqual([commonField]);
|
||||
});
|
||||
|
||||
it('should return empty array if it has no common fields', () => {
|
||||
const mock = [
|
||||
{
|
||||
index: 'index1',
|
||||
fields: [{ name: 'age', type: 'long', normalizedType: 'number' }],
|
||||
},
|
||||
];
|
||||
|
||||
expect(getCommonFields(mock)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeFieldsList', () => {
|
||||
it('knows how to normalize types', () => {
|
||||
const mock = {
|
||||
age: {
|
||||
long: {
|
||||
type: 'long',
|
||||
metadata_field: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
},
|
||||
ignore: {
|
||||
_ignore: {
|
||||
type: '_ignore',
|
||||
metadata_field: false,
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeFieldsList(mock)).toEqual([
|
||||
{
|
||||
name: 'age',
|
||||
type: 'long',
|
||||
normalizedType: 'number',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 { forEach, keys, sortBy, reduce, size } from 'lodash';
|
||||
import { flatMap, flow, groupBy, values as valuesFP, map, pickBy } from 'lodash/fp';
|
||||
|
||||
import type { IScopedClusterClient } from '@kbn/core/server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { FieldCapsResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
export type FieldCapsList = FieldCapsResponse['fields'];
|
||||
|
||||
const normalizedFieldTypes: { [key: string]: string } = {
|
||||
long: 'number',
|
||||
integer: 'number',
|
||||
short: 'number',
|
||||
byte: 'number',
|
||||
double: 'number',
|
||||
float: 'number',
|
||||
half_float: 'number',
|
||||
scaled_float: 'number',
|
||||
};
|
||||
|
||||
interface FieldItem {
|
||||
name: string;
|
||||
type: string;
|
||||
normalizedType: string;
|
||||
}
|
||||
|
||||
interface FieldsPerIndexType {
|
||||
index: string;
|
||||
fields: FieldItem[];
|
||||
}
|
||||
|
||||
interface IndicesAggs extends estypes.AggregationsMultiBucketAggregateBase {
|
||||
buckets: Array<{ key: unknown }>;
|
||||
}
|
||||
|
||||
export function getCommonFields(fieldsPerIndex: FieldsPerIndexType[]) {
|
||||
return flow(
|
||||
// Flatten the fields arrays
|
||||
flatMap('fields'),
|
||||
// Group fields by name
|
||||
groupBy('name'),
|
||||
// Keep groups with more than 1 field
|
||||
pickBy((group) => group.length > 1),
|
||||
// Convert the result object to an array of fields
|
||||
valuesFP,
|
||||
// Take the first item from each group (since we only need one match)
|
||||
map((group) => group[0])
|
||||
)(fieldsPerIndex);
|
||||
}
|
||||
|
||||
export function normalizeFieldsList(fields: FieldCapsList) {
|
||||
const result: FieldItem[] = [];
|
||||
|
||||
forEach(fields, (field, name) => {
|
||||
// If the field exists in multiple indexes, the types may be inconsistent.
|
||||
// In this case, default to the first type.
|
||||
const type = keys(field)[0];
|
||||
|
||||
// Do not include fields that have a type that starts with an underscore (e.g. _id, _source)
|
||||
if (type.startsWith('_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.push({
|
||||
name,
|
||||
type,
|
||||
normalizedType: normalizedFieldTypes[type] || type,
|
||||
});
|
||||
});
|
||||
|
||||
return sortBy(result, 'name');
|
||||
}
|
||||
|
||||
export function getIndexNamesFromAliasesResponse(json: Record<string, any>) {
|
||||
return reduce(
|
||||
json,
|
||||
(list, { aliases }, indexName) => {
|
||||
// Add the index name to the list
|
||||
list.push(indexName);
|
||||
// If the index has aliases, add them to the list as well
|
||||
if (size(aliases) > 0) {
|
||||
list.push(...Object.keys(aliases));
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
[] as string[]
|
||||
);
|
||||
}
|
||||
|
||||
export async function getIndices(dataClient: IScopedClusterClient, pattern: string, limit = 10) {
|
||||
// We will first rely on the indices aliases API to get the list of indices and their aliases.
|
||||
const aliasResult = await dataClient.asCurrentUser.indices.getAlias(
|
||||
{
|
||||
index: pattern,
|
||||
expand_wildcards: 'open',
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (aliasResult.statusCode !== 404) {
|
||||
const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult.body);
|
||||
return indicesFromAliasResponse.slice(0, limit);
|
||||
}
|
||||
|
||||
// If the indices aliases API fails or returns nothing, we will rely on the indices stats API to
|
||||
// get the list of indices.
|
||||
const response = await dataClient.asCurrentUser.search<unknown, { indices: IndicesAggs }>(
|
||||
{
|
||||
index: pattern,
|
||||
body: {
|
||||
size: 0,
|
||||
aggs: {
|
||||
indices: {
|
||||
terms: {
|
||||
field: '_index',
|
||||
size: limit,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.statusCode === 404 || !response.body.aggregations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const indices = response.body.aggregations.indices;
|
||||
|
||||
return indices.buckets ? indices.buckets.map((bucket) => bucket.key) : [];
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { IScopedClusterClient } from '@kbn/core/server';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addInternalBasePath } from '..';
|
||||
import { enrichPoliciesActions } from '../../../lib/enrich_policies';
|
||||
import { serializeAsESPolicy } from '../../../../common/lib';
|
||||
import { normalizeFieldsList, getIndices, FieldCapsList, getCommonFields } from './helpers';
|
||||
import type { SerializedEnrichPolicy } from '../../../../common';
|
||||
|
||||
const validationSchema = schema.object({
|
||||
policy: schema.object({
|
||||
name: schema.string(),
|
||||
type: schema.oneOf([
|
||||
schema.literal('match'),
|
||||
schema.literal('range'),
|
||||
schema.literal('geo_match'),
|
||||
]),
|
||||
matchField: schema.string(),
|
||||
enrichFields: schema.arrayOf(schema.string()),
|
||||
sourceIndices: schema.arrayOf(schema.string()),
|
||||
query: schema.maybe(schema.any()),
|
||||
}),
|
||||
});
|
||||
|
||||
const querySchema = schema.object({
|
||||
executePolicyAfterCreation: schema.maybe(
|
||||
schema.oneOf([schema.literal('true'), schema.literal('false')])
|
||||
),
|
||||
});
|
||||
|
||||
const getMatchingIndicesSchema = schema.object({ pattern: schema.string() }, { unknowns: 'allow' });
|
||||
|
||||
const getFieldsFromIndicesSchema = schema.object({
|
||||
indices: schema.arrayOf(schema.string()),
|
||||
});
|
||||
|
||||
export function registerCreateRoute({ router, lib: { handleEsError } }: RouteDependencies) {
|
||||
router.post(
|
||||
{
|
||||
path: addInternalBasePath('/enrich_policies'),
|
||||
validate: { body: validationSchema, query: querySchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const client = (await context.core).elasticsearch.client as IScopedClusterClient;
|
||||
const executeAfter = Boolean(
|
||||
(request.query as TypeOf<typeof querySchema>)?.executePolicyAfterCreation
|
||||
);
|
||||
|
||||
const { policy } = request.body;
|
||||
const serializedPolicy = serializeAsESPolicy(policy as SerializedEnrichPolicy);
|
||||
|
||||
try {
|
||||
const res = await enrichPoliciesActions.create(client, policy.name, serializedPolicy);
|
||||
|
||||
if (executeAfter) {
|
||||
try {
|
||||
await enrichPoliciesActions.execute(client, policy.name);
|
||||
} catch (error) {
|
||||
// If executing the policy fails, remove the previously created policy and
|
||||
// return the error.
|
||||
await enrichPoliciesActions.remove(client, policy.name);
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
|
||||
return response.ok({ body: res });
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: addInternalBasePath('/enrich_policies/get_matching_indices'),
|
||||
validate: { body: getMatchingIndicesSchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
let { pattern } = request.body;
|
||||
const client = (await context.core).elasticsearch.client as IScopedClusterClient;
|
||||
|
||||
// Add wildcards to the search query to match the behavior of the
|
||||
// index pattern search in the Kibana UI.
|
||||
if (!pattern.startsWith('*')) {
|
||||
pattern = `*${pattern}`;
|
||||
}
|
||||
if (!pattern.endsWith('*')) {
|
||||
pattern = `${pattern}*`;
|
||||
}
|
||||
|
||||
try {
|
||||
const indices = await getIndices(client, pattern);
|
||||
|
||||
return response.ok({ body: { indices } });
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: addInternalBasePath('/enrich_policies/get_fields_from_indices'),
|
||||
validate: { body: getFieldsFromIndicesSchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { indices } = request.body;
|
||||
const client = (await context.core).elasticsearch.client as IScopedClusterClient;
|
||||
|
||||
try {
|
||||
const fieldsPerIndex = await Promise.all(
|
||||
indices.map((index) =>
|
||||
client.asCurrentUser.fieldCaps(
|
||||
{
|
||||
index,
|
||||
fields: ['*'],
|
||||
allow_no_indices: true,
|
||||
ignore_unavailable: true,
|
||||
filters: '-metadata',
|
||||
},
|
||||
{ ignore: [404], meta: true }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const serializedFieldsPerIndex = indices.map((indexName: string, mapIndex: number) => {
|
||||
const fields = fieldsPerIndex[mapIndex];
|
||||
const json = fields.statusCode === 404 ? { fields: [] } : fields.body;
|
||||
|
||||
return {
|
||||
index: indexName,
|
||||
fields: normalizeFieldsList(json.fields as FieldCapsList),
|
||||
};
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
indices: serializedFieldsPerIndex,
|
||||
commonFields: getCommonFields(serializedFieldsPerIndex),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
import { IScopedClusterClient } from '@kbn/core/server';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addInternalBasePath } from '..';
|
||||
import { enrichPoliciesActions } from '../../../lib/enrich_policies';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
name: schema.string(),
|
||||
});
|
||||
|
||||
export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) {
|
||||
router.delete(
|
||||
{
|
||||
path: addInternalBasePath('/enrich_policies/{name}'),
|
||||
validate: { params: paramsSchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { name } = request.params;
|
||||
const client = (await context.core).elasticsearch.client as IScopedClusterClient;
|
||||
|
||||
try {
|
||||
const res = await enrichPoliciesActions.remove(client, name);
|
||||
return response.ok({ body: res });
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -8,7 +8,13 @@
|
|||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
import { registerListRoute } from './register_list_route';
|
||||
import { registerDeleteRoute } from './register_delete_route';
|
||||
import { registerExecuteRoute } from './register_execute_route';
|
||||
import { registerCreateRoute } from './register_create_route';
|
||||
|
||||
export function registerEnrichPoliciesRoute(dependencies: RouteDependencies) {
|
||||
registerListRoute(dependencies);
|
||||
registerDeleteRoute(dependencies);
|
||||
registerExecuteRoute(dependencies);
|
||||
registerCreateRoute(dependencies);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
import { IScopedClusterClient } from '@kbn/core/server';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addInternalBasePath } from '..';
|
||||
import { enrichPoliciesActions } from '../../../lib/enrich_policies';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
name: schema.string(),
|
||||
});
|
||||
|
||||
export function registerExecuteRoute({ router, lib: { handleEsError } }: RouteDependencies) {
|
||||
router.put(
|
||||
{
|
||||
path: addInternalBasePath('/enrich_policies/{name}'),
|
||||
validate: { params: paramsSchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { name } = request.params;
|
||||
const client = (await context.core).elasticsearch.client as IScopedClusterClient;
|
||||
|
||||
try {
|
||||
const res = await enrichPoliciesActions.execute(client, name);
|
||||
return response.ok({ body: res });
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -34,6 +34,8 @@
|
|||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-ui-settings-browser-mocks",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/react-field",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/search-api-panels",
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 expect from 'expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const INTERNAL_API_BASE_PATH = '/internal/index_management';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
|
||||
describe('Create enrich policy', function () {
|
||||
const INDEX_A_NAME = `index-${Math.random()}`;
|
||||
const INDEX_B_NAME = `index-${Math.random()}`;
|
||||
const POLICY_NAME = `policy-${Math.random()}`;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
await es.indices.create({
|
||||
index: INDEX_A_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
email: {
|
||||
type: 'text',
|
||||
},
|
||||
firstName: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await es.indices.create({
|
||||
index: INDEX_B_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
email: {
|
||||
type: 'text',
|
||||
},
|
||||
age: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
log.debug('[Setup error] Error creating test index');
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
try {
|
||||
await es.indices.delete({ index: INDEX_A_NAME });
|
||||
await es.indices.delete({ index: INDEX_B_NAME });
|
||||
} catch (err) {
|
||||
log.debug('[Cleanup error] Error deleting test index');
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
it('Allows to create an enrich policy', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${INTERNAL_API_BASE_PATH}/enrich_policies`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.send({
|
||||
policy: {
|
||||
name: POLICY_NAME,
|
||||
type: 'match',
|
||||
matchField: 'email',
|
||||
enrichFields: ['firstName'],
|
||||
sourceIndices: [INDEX_A_NAME],
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body).toStrictEqual({ acknowledged: true });
|
||||
});
|
||||
|
||||
it('Can retrieve fields from indices', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${INTERNAL_API_BASE_PATH}/enrich_policies/get_fields_from_indices`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.send({ indices: [INDEX_A_NAME, INDEX_B_NAME] })
|
||||
.expect(200);
|
||||
|
||||
expect(body).toStrictEqual({
|
||||
commonFields: [{ name: 'email', type: 'text', normalizedType: 'text' }],
|
||||
indices: [
|
||||
{
|
||||
index: INDEX_A_NAME,
|
||||
fields: [
|
||||
{ name: 'email', type: 'text', normalizedType: 'text' },
|
||||
{ name: 'firstName', type: 'text', normalizedType: 'text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
index: INDEX_B_NAME,
|
||||
fields: [
|
||||
{ name: 'age', type: 'long', normalizedType: 'number' },
|
||||
{ name: 'email', type: 'text', normalizedType: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Can retrieve matching indices', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${INTERNAL_API_BASE_PATH}/enrich_policies/get_matching_indices`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.send({ pattern: 'index-' })
|
||||
.expect(200);
|
||||
|
||||
expect(
|
||||
body.indices.every((value: string) => [INDEX_A_NAME, INDEX_B_NAME].includes(value))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -16,5 +16,6 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./component_templates'));
|
||||
loadTestFile(require.resolve('./cluster_nodes'));
|
||||
loadTestFile(require.resolve('./index_details'));
|
||||
loadTestFile(require.resolve('./create_enrich_policy'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
|
||||
const log = getService('log');
|
||||
const security = getService('security');
|
||||
const comboBox = getService('comboBox');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const es = getService('es');
|
||||
|
||||
const INDEX_NAME = `index-${Math.random()}`;
|
||||
const POLICY_NAME = `policy-${Math.random()}`;
|
||||
|
||||
describe('Create enrich policy', function () {
|
||||
before(async () => {
|
||||
await log.debug('Creating test index');
|
||||
try {
|
||||
await es.indices.create({
|
||||
index: INDEX_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
email: {
|
||||
type: 'text',
|
||||
},
|
||||
age: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log.debug('[Setup error] Error creating test policy');
|
||||
throw e;
|
||||
}
|
||||
|
||||
await log.debug('Navigating to the enrich policies tab');
|
||||
await security.testUser.setRoles(['index_management_user']);
|
||||
await pageObjects.common.navigateToApp('indexManagement');
|
||||
// Navigate to the enrich policies tab
|
||||
await pageObjects.indexManagement.changeTabs('enrich_policiesTab');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
// Click create policy button
|
||||
await testSubjects.click('enrichPoliciesEmptyPromptCreateButton');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await log.debug('Cleaning up created index');
|
||||
|
||||
try {
|
||||
await es.indices.delete({ index: INDEX_NAME });
|
||||
} catch (e) {
|
||||
log.debug('[Teardown error] Error deleting test policy');
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
it('shows create enrich policies page and docs link', async () => {
|
||||
expect(await testSubjects.exists('createEnrichPolicyHeaderContent')).to.be(true);
|
||||
expect(await testSubjects.exists('createEnrichPolicyDocumentationLink')).to.be(true);
|
||||
});
|
||||
|
||||
it('can create an enrich policy', async () => {
|
||||
// Complete configuration step
|
||||
await testSubjects.setValue('policyNameField > input', POLICY_NAME);
|
||||
await testSubjects.setValue('policyTypeField', 'match');
|
||||
await comboBox.set('policySourceIndicesField', INDEX_NAME);
|
||||
await testSubjects.click('nextButton');
|
||||
|
||||
// Complete field selection step
|
||||
await comboBox.set('matchField', 'email');
|
||||
await comboBox.set('enrichFields', 'age');
|
||||
await testSubjects.click('nextButton');
|
||||
|
||||
// Create policy
|
||||
await testSubjects.click('createButton');
|
||||
|
||||
// Expect to be redirected to the enrich policies tab
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
// Expect to have that policy in the table
|
||||
const policyList = await testSubjects.findAll('enrichPolicyDetailsLink');
|
||||
expect(policyList.length).to.be(1);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ loadTestFile }: FtrProviderContext) => {
|
||||
describe('Index Management: create enrich policy', function () {
|
||||
loadTestFile(require.resolve('./create_enrich_policy'));
|
||||
});
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
|
||||
const toasts = getService('toasts');
|
||||
const log = getService('log');
|
||||
const browser = getService('browser');
|
||||
const security = getService('security');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const es = getService('es');
|
||||
|
||||
const ENRICH_INDEX_NAME = 'test-policy-1';
|
||||
const ENRICH_POLICY_NAME = 'test-policy-1';
|
||||
|
||||
describe('Enrich policies tab', function () {
|
||||
before(async () => {
|
||||
await log.debug('Creating required index and enrich policy');
|
||||
try {
|
||||
await es.indices.create({
|
||||
index: ENRICH_INDEX_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await es.enrich.putPolicy({
|
||||
name: ENRICH_POLICY_NAME,
|
||||
match: {
|
||||
indices: ENRICH_INDEX_NAME,
|
||||
match_field: 'name',
|
||||
enrich_fields: ['name'],
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log.debug('[Setup error] Error creating test policy');
|
||||
throw e;
|
||||
}
|
||||
|
||||
await log.debug('Navigating to the enrich policies tab');
|
||||
await security.testUser.setRoles(['index_management_user']);
|
||||
await pageObjects.common.navigateToApp('indexManagement');
|
||||
// Navigate to the enrich policies tab
|
||||
await pageObjects.indexManagement.changeTabs('enrich_policiesTab');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await log.debug('Cleaning up created index and policy');
|
||||
|
||||
try {
|
||||
await es.indices.delete({ index: ENRICH_INDEX_NAME });
|
||||
} catch (e) {
|
||||
log.debug('[Teardown error] Error deleting test policy');
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
it('shows enrich policies page and docs link', async () => {
|
||||
expect(await testSubjects.exists('enrichPoliciesList')).to.be(true);
|
||||
expect(await testSubjects.exists('enrichPoliciesLearnMoreLink')).to.be(true);
|
||||
});
|
||||
|
||||
it('shows the details flyout when clicking on a policy name', async () => {
|
||||
// Open details flyout
|
||||
await pageObjects.indexManagement.clickEnrichPolicyAt(0);
|
||||
// Verify url is stateful
|
||||
const url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain(`/enrich_policies?policy=${ENRICH_POLICY_NAME}`);
|
||||
// Assert that flyout is opened
|
||||
expect(await testSubjects.exists('policyDetailsFlyout')).to.be(true);
|
||||
// Close flyout
|
||||
await testSubjects.click('closeFlyoutButton');
|
||||
});
|
||||
|
||||
it('can execute a policy', async () => {
|
||||
await pageObjects.indexManagement.clickExecuteEnrichPolicyAt(0);
|
||||
await pageObjects.indexManagement.clickConfirmModalButton();
|
||||
|
||||
const successToast = await toasts.getToastElement(1);
|
||||
expect(await successToast.getVisibleText()).to.contain(`Executed ${ENRICH_POLICY_NAME}`);
|
||||
});
|
||||
|
||||
it('can delete a policy', async () => {
|
||||
await pageObjects.indexManagement.clickDeleteEnrichPolicyAt(0);
|
||||
await pageObjects.indexManagement.clickConfirmModalButton();
|
||||
|
||||
const successToast = await toasts.getToastElement(2);
|
||||
expect(await successToast.getVisibleText()).to.contain(`Deleted ${ENRICH_POLICY_NAME}`);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ loadTestFile }: FtrProviderContext) => {
|
||||
describe('Index Management: enrich policies tab', function () {
|
||||
loadTestFile(require.resolve('./enrich_policies_tab'));
|
||||
});
|
||||
};
|
|
@ -92,5 +92,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
expect(componentTemplateList).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enrich policies', () => {
|
||||
it('renders the enrich policies tab', async () => {
|
||||
// Navigate to the component templates tab
|
||||
await pageObjects.indexManagement.changeTabs('enrich_policiesTab');
|
||||
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
// Verify url
|
||||
const url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain(`/enrich_policies`);
|
||||
|
||||
// Verify content
|
||||
const enrichPoliciesList = await testSubjects.exists('sectionEmpty');
|
||||
expect(enrichPoliciesList).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,5 +12,7 @@ export default ({ loadTestFile }: FtrProviderContext) => {
|
|||
loadTestFile(require.resolve('./feature_controls'));
|
||||
loadTestFile(require.resolve('./home_page'));
|
||||
loadTestFile(require.resolve('./index_template_wizard'));
|
||||
loadTestFile(require.resolve('./enrich_policies_tab'));
|
||||
loadTestFile(require.resolve('./create_enrich_policy'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -559,7 +559,7 @@ export default async function ({ readConfigFile }) {
|
|||
|
||||
index_management_user: {
|
||||
elasticsearch: {
|
||||
cluster: ['monitor', 'manage_index_templates'],
|
||||
cluster: ['monitor', 'manage_index_templates', 'manage_enrich'],
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
|
|
|
@ -30,6 +30,25 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext)
|
|||
await testSubjects.click('indexTableIncludeHiddenIndicesToggle');
|
||||
},
|
||||
|
||||
async clickEnrichPolicyAt(indexOfRow: number): Promise<void> {
|
||||
const policyDetailsLinks = await testSubjects.findAll('enrichPolicyDetailsLink');
|
||||
await policyDetailsLinks[indexOfRow].click();
|
||||
},
|
||||
|
||||
async clickDeleteEnrichPolicyAt(indexOfRow: number): Promise<void> {
|
||||
const deleteButons = await testSubjects.findAll('deletePolicyButton');
|
||||
await deleteButons[indexOfRow].click();
|
||||
},
|
||||
|
||||
async clickExecuteEnrichPolicyAt(indexOfRow: number): Promise<void> {
|
||||
const executeButtons = await testSubjects.findAll('executePolicyButton');
|
||||
await executeButtons[indexOfRow].click();
|
||||
},
|
||||
|
||||
async clickConfirmModalButton(): Promise<void> {
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
},
|
||||
|
||||
async clickDetailPanelTabAt(indexOfTab: number): Promise<void> {
|
||||
const tabList = await testSubjects.findAll('detailPanelTab');
|
||||
log.debug(tabList.length);
|
||||
|
@ -87,7 +106,12 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext)
|
|||
},
|
||||
|
||||
async changeTabs(
|
||||
tab: 'indicesTab' | 'data_streamsTab' | 'templatesTab' | 'component_templatesTab'
|
||||
tab:
|
||||
| 'indicesTab'
|
||||
| 'data_streamsTab'
|
||||
| 'templatesTab'
|
||||
| 'component_templatesTab'
|
||||
| 'enrich_policiesTab'
|
||||
) {
|
||||
await testSubjects.click(tab);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 expect from 'expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const INTERNAL_API_BASE_PATH = '/internal/index_management';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
|
||||
describe('Create enrich policy', function () {
|
||||
const INDEX_A_NAME = `index-${Math.random()}`;
|
||||
const INDEX_B_NAME = `index-${Math.random()}`;
|
||||
const POLICY_NAME = `policy-${Math.random()}`;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
await es.indices.create({
|
||||
index: INDEX_A_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
email: {
|
||||
type: 'text',
|
||||
},
|
||||
firstName: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await es.indices.create({
|
||||
index: INDEX_B_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
email: {
|
||||
type: 'text',
|
||||
},
|
||||
age: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
log.debug('[Setup error] Error creating test index');
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
try {
|
||||
await es.indices.delete({ index: INDEX_A_NAME });
|
||||
await es.indices.delete({ index: INDEX_B_NAME });
|
||||
} catch (err) {
|
||||
log.debug('[Cleanup error] Error deleting test index');
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
it('Allows to create an enrich policy', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${INTERNAL_API_BASE_PATH}/enrich_policies`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.send({
|
||||
policy: {
|
||||
name: POLICY_NAME,
|
||||
type: 'match',
|
||||
matchField: 'email',
|
||||
enrichFields: ['firstName'],
|
||||
sourceIndices: [INDEX_A_NAME],
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body).toStrictEqual({ acknowledged: true });
|
||||
});
|
||||
|
||||
it('Can retrieve fields from indices', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${INTERNAL_API_BASE_PATH}/enrich_policies/get_fields_from_indices`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.send({ indices: [INDEX_A_NAME, INDEX_B_NAME] })
|
||||
.expect(200);
|
||||
|
||||
expect(body).toStrictEqual({
|
||||
commonFields: [{ name: 'email', type: 'text', normalizedType: 'text' }],
|
||||
indices: [
|
||||
{
|
||||
index: INDEX_A_NAME,
|
||||
fields: [
|
||||
{ name: 'email', type: 'text', normalizedType: 'text' },
|
||||
{ name: 'firstName', type: 'text', normalizedType: 'text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
index: INDEX_B_NAME,
|
||||
fields: [
|
||||
{ name: 'age', type: 'long', normalizedType: 'number' },
|
||||
{ name: 'email', type: 'text', normalizedType: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Can retrieve matching indices', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`${INTERNAL_API_BASE_PATH}/enrich_policies/get_matching_indices`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('x-elastic-internal-origin', 'xxx')
|
||||
.send({ pattern: 'index-' })
|
||||
.expect(200);
|
||||
|
||||
expect(
|
||||
body.indices.every((value: string) => [INDEX_A_NAME, INDEX_B_NAME].includes(value))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('Index Management APIs', function () {
|
||||
loadTestFile(require.resolve('./index_templates'));
|
||||
loadTestFile(require.resolve('./indices'));
|
||||
loadTestFile(require.resolve('./create_enrich_policies'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common', 'indexManagement', 'header', 'svlCommonPage']);
|
||||
const log = getService('log');
|
||||
const security = getService('security');
|
||||
const comboBox = getService('comboBox');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const es = getService('es');
|
||||
|
||||
const INDEX_NAME = `index-${Math.random()}`;
|
||||
const POLICY_NAME = `policy-${Math.random()}`;
|
||||
|
||||
describe('Create enrich policy', function () {
|
||||
before(async () => {
|
||||
await log.debug('Creating test index');
|
||||
try {
|
||||
await es.indices.create({
|
||||
index: INDEX_NAME,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
email: {
|
||||
type: 'text',
|
||||
},
|
||||
age: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log.debug('[Setup error] Error creating test policy');
|
||||
throw e;
|
||||
}
|
||||
|
||||
await log.debug('Navigating to the enrich policies tab');
|
||||
await pageObjects.svlCommonPage.login();
|
||||
await security.testUser.setRoles(['index_management_user']);
|
||||
await pageObjects.common.navigateToApp('indexManagement');
|
||||
|
||||
// Navigate to the enrich policies tab
|
||||
await pageObjects.indexManagement.changeTabs('enrich_policiesTab');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
// Click create policy button
|
||||
await testSubjects.click('enrichPoliciesEmptyPromptCreateButton');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await log.debug('Cleaning up created index');
|
||||
|
||||
try {
|
||||
await es.indices.delete({ index: INDEX_NAME });
|
||||
} catch (e) {
|
||||
log.debug('[Teardown error] Error deleting test policy');
|
||||
throw e;
|
||||
} finally {
|
||||
await pageObjects.svlCommonPage.forceLogout();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows create enrich policies page and docs link', async () => {
|
||||
expect(await testSubjects.exists('createEnrichPolicyHeaderContent')).to.be(true);
|
||||
expect(await testSubjects.exists('createEnrichPolicyDocumentationLink')).to.be(true);
|
||||
});
|
||||
|
||||
it('can create an enrich policy', async () => {
|
||||
// Complete configuration step
|
||||
await testSubjects.setValue('policyNameField > input', POLICY_NAME);
|
||||
await testSubjects.setValue('policyTypeField', 'match');
|
||||
await comboBox.set('policySourceIndicesField', INDEX_NAME);
|
||||
await testSubjects.click('nextButton');
|
||||
|
||||
// Complete field selection step
|
||||
await comboBox.set('matchField', 'email');
|
||||
await comboBox.set('enrichFields', 'age');
|
||||
await testSubjects.click('nextButton');
|
||||
|
||||
// Create policy
|
||||
await testSubjects.click('createButton');
|
||||
|
||||
// Expect to be redirected to the enrich policies tab
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
// Expect to have that policy in the table
|
||||
const policyList = await testSubjects.findAll('enrichPolicyDetailsLink');
|
||||
expect(policyList.length).to.be(1);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -11,5 +11,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
|
|||
describe('Index Management', function () {
|
||||
loadTestFile(require.resolve('./index_templates'));
|
||||
loadTestFile(require.resolve('./indices'));
|
||||
loadTestFile(require.resolve('./create_enrich_policy'));
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue