mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add support for GeoIP processor databases in Ingest Pipelines (#190830)
Fixes https://github.com/elastic/kibana/issues/190818 ## Summary Elasticsearch has added support for GeoIP, enabling the use of paid GeoIP databases from MaxMind/IPInfo for more accurate and granular geolocation data. As such we should add support to ingest pipelines UI for making this available to the user. * If the user doesn't have enough privileges, the "Manage Pipelines" link and UI won't show. * Users can add two types of databases through the UI: MaxMind and IPinfo. Database names are predefined by ES, and the user cannot enter their own. * Certain types of databases (local and web) can be configured through ES, and these will appear in the UI, but they cannot be deleted as they are read-only. * When configuring a `IP location` processor, the database field will display a list of available and configured databases that the user can select. It also allows for free-text input if the user wants to configure a database that does not yet exist. * The new IP location processor is essentially a clone of the GeoIP processor, which we are moving away from due to copyright issues. However, it was decided that GeoIP will remain as is for backward compatibility, and all new work will only be added to IP location going forward. * I left a few mocks in the `server/routes/api/geoip_database/list.ts ` to try `local/web` types ## Release note The Ingest Pipelines app now supports adding and managing databases for the GeoIP processor. Additionally, the pipeline creation flow now includes support for the IP Location processor. <details> <summary>Screenshots</summary>       </details> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Ignacio Rivas <rivasign@gmail.com> Co-authored-by: Elena Stoeva <elenastoeva99@gmail.com> Co-authored-by: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com> Co-authored-by: Matthew Kime <matt@mattki.me>
This commit is contained in:
parent
7b9ff3d90c
commit
302ac0d336
54 changed files with 2218 additions and 88 deletions
|
@ -113,6 +113,9 @@ xpack.index_management.enableTogglingDataRetention: false
|
|||
# Disable project level rentention checks in DSL form from Index Management UI
|
||||
xpack.index_management.enableProjectLevelRetentionChecks: false
|
||||
|
||||
# Disable Manage Processors UI in Ingest Pipelines
|
||||
xpack.ingest_pipelines.enableManageProcessors: false
|
||||
|
||||
# Keep deeplinks visible so that they are shown in the sidenav
|
||||
dev_tools.deeplinks.navLinkStatus: visible
|
||||
management.deeplinks.navLinkStatus: visible
|
||||
|
|
|
@ -314,6 +314,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.ml.nlp.modelDeployment.vCPURange.medium.static (number?)',
|
||||
'xpack.osquery.actionEnabled (boolean?)',
|
||||
'xpack.remote_clusters.ui.enabled (boolean?)',
|
||||
'xpack.ingest_pipelines.enableManageProcessors (boolean?|never)',
|
||||
/**
|
||||
* NOTE: The Reporting plugin is currently disabled in functional tests (see test/functional/config.base.js).
|
||||
* It will be re-enabled once #102552 is completed.
|
||||
|
|
|
@ -73,12 +73,27 @@ const registerHttpRequestMockHelpers = (
|
|||
const setParseCsvResponse = (response?: object, error?: ResponseError) =>
|
||||
mockResponse('POST', `${API_BASE_PATH}/parse_csv`, response, error);
|
||||
|
||||
const setLoadDatabasesResponse = (response?: object[], error?: ResponseError) =>
|
||||
mockResponse('GET', `${API_BASE_PATH}/databases`, response, error);
|
||||
|
||||
const setDeleteDatabasesResponse = (
|
||||
databaseName: string,
|
||||
response?: object,
|
||||
error?: ResponseError
|
||||
) => mockResponse('DELETE', `${API_BASE_PATH}/databases/${databaseName}`, response, error);
|
||||
|
||||
const setCreateDatabasesResponse = (response?: object, error?: ResponseError) =>
|
||||
mockResponse('POST', `${API_BASE_PATH}/databases`, response, error);
|
||||
|
||||
return {
|
||||
setLoadPipelinesResponse,
|
||||
setLoadPipelineResponse,
|
||||
setDeletePipelineResponse,
|
||||
setCreatePipelineResponse,
|
||||
setParseCsvResponse,
|
||||
setLoadDatabasesResponse,
|
||||
setDeleteDatabasesResponse,
|
||||
setCreateDatabasesResponse,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -10,8 +10,9 @@ import { setup as pipelinesCreateSetup } from './pipelines_create.helpers';
|
|||
import { setup as pipelinesCloneSetup } from './pipelines_clone.helpers';
|
||||
import { setup as pipelinesEditSetup } from './pipelines_edit.helpers';
|
||||
import { setup as pipelinesCreateFromCsvSetup } from './pipelines_create_from_csv.helpers';
|
||||
import { setup as manageProcessorsSetup } from './manage_processors.helpers';
|
||||
|
||||
export { nextTick, getRandomString, findTestSubject } from '@kbn/test-jest-helpers';
|
||||
export { getRandomString, findTestSubject } from '@kbn/test-jest-helpers';
|
||||
|
||||
export { setupEnvironment } from './setup_environment';
|
||||
|
||||
|
@ -21,4 +22,5 @@ export const pageHelpers = {
|
|||
pipelinesClone: { setup: pipelinesCloneSetup },
|
||||
pipelinesEdit: { setup: pipelinesEditSetup },
|
||||
pipelinesCreateFromCsv: { setup: pipelinesCreateFromCsvSetup },
|
||||
manageProcessors: { setup: manageProcessorsSetup },
|
||||
};
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
|
||||
import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers';
|
||||
import { ManageProcessors } from '../../../public/application/sections';
|
||||
import { WithAppDependencies } from './setup_environment';
|
||||
import { getManageProcessorsPath, ROUTES } from '../../../public/application/services/navigation';
|
||||
|
||||
const testBedConfig: AsyncTestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [getManageProcessorsPath()],
|
||||
componentRoutePath: ROUTES.manageProcessors,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
export type ManageProcessorsTestBed = TestBed<ManageProcessorsTestSubjects> & {
|
||||
actions: ReturnType<typeof createActions>;
|
||||
};
|
||||
|
||||
const createActions = (testBed: TestBed) => {
|
||||
const { component, find, form } = testBed;
|
||||
|
||||
const clickDeleteDatabaseButton = async (index: number) => {
|
||||
const allDeleteButtons = find('deleteGeoipDatabaseButton');
|
||||
const deleteButton = allDeleteButtons.at(index);
|
||||
await act(async () => {
|
||||
deleteButton.simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const confirmDeletingDatabase = async () => {
|
||||
await act(async () => {
|
||||
form.setInputValue('geoipDatabaseConfirmation', 'delete');
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
const confirmButton: HTMLButtonElement | null = document.body.querySelector(
|
||||
'[data-test-subj="deleteGeoipDatabaseSubmit"]'
|
||||
);
|
||||
|
||||
expect(confirmButton).not.toBe(null);
|
||||
expect(confirmButton!.disabled).toBe(false);
|
||||
expect(confirmButton!.textContent).toContain('Delete');
|
||||
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const clickAddDatabaseButton = async () => {
|
||||
const button = find('addGeoipDatabaseButton');
|
||||
expect(button).not.toBe(undefined);
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const fillOutDatabaseValues = async (
|
||||
databaseType: string,
|
||||
databaseName: string,
|
||||
maxmind?: string
|
||||
) => {
|
||||
await act(async () => {
|
||||
form.setSelectValue('databaseTypeSelect', databaseType);
|
||||
});
|
||||
component.update();
|
||||
|
||||
if (maxmind) {
|
||||
await act(async () => {
|
||||
form.setInputValue('maxmindField', maxmind);
|
||||
});
|
||||
}
|
||||
await act(async () => {
|
||||
form.setSelectValue('databaseNameSelect', databaseName);
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const confirmAddingDatabase = async () => {
|
||||
const confirmButton: HTMLButtonElement | null = document.body.querySelector(
|
||||
'[data-test-subj="addGeoipDatabaseSubmit"]'
|
||||
);
|
||||
|
||||
expect(confirmButton).not.toBe(null);
|
||||
expect(confirmButton!.disabled).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
return {
|
||||
clickDeleteDatabaseButton,
|
||||
confirmDeletingDatabase,
|
||||
clickAddDatabaseButton,
|
||||
fillOutDatabaseValues,
|
||||
confirmAddingDatabase,
|
||||
};
|
||||
};
|
||||
|
||||
export const setup = async (httpSetup: HttpSetup): Promise<ManageProcessorsTestBed> => {
|
||||
const initTestBed = registerTestBed(
|
||||
WithAppDependencies(ManageProcessors, httpSetup),
|
||||
testBedConfig
|
||||
);
|
||||
const testBed = await initTestBed();
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: createActions(testBed),
|
||||
};
|
||||
};
|
||||
|
||||
export type ManageProcessorsTestSubjects =
|
||||
| 'manageProcessorsTitle'
|
||||
| 'addGeoipDatabaseForm'
|
||||
| 'addGeoipDatabaseButton'
|
||||
| 'geoipDatabaseList'
|
||||
| 'databaseTypeSelect'
|
||||
| 'maxmindField'
|
||||
| 'databaseNameSelect'
|
||||
| 'addGeoipDatabaseSubmit'
|
||||
| 'deleteGeoipDatabaseButton'
|
||||
| 'geoipDatabaseConfirmation'
|
||||
| 'geoipEmptyListPrompt'
|
||||
| 'geoipListLoadingError';
|
|
@ -70,6 +70,9 @@ const appServices = {
|
|||
},
|
||||
overlays: overlayServiceMock.createStartContract(),
|
||||
http: httpServiceMock.createStartContract({ basePath: '/mock' }),
|
||||
config: {
|
||||
enableManageProcessors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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 { ManageProcessorsTestBed } from './helpers/manage_processors.helpers';
|
||||
|
||||
import { setupEnvironment, pageHelpers } from './helpers';
|
||||
import type { GeoipDatabase } from '../../common/types';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
|
||||
const { setup } = pageHelpers.manageProcessors;
|
||||
|
||||
describe('<ManageProcessors />', () => {
|
||||
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: ManageProcessorsTestBed;
|
||||
|
||||
describe('With databases', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
const database1: GeoipDatabase = {
|
||||
name: 'GeoIP2-Anonymous-IP',
|
||||
id: 'geoip2-anonymous-ip',
|
||||
type: 'maxmind',
|
||||
};
|
||||
|
||||
const database2: GeoipDatabase = {
|
||||
name: 'GeoIP2-City',
|
||||
id: 'geoip2-city',
|
||||
type: 'maxmind',
|
||||
};
|
||||
|
||||
const database3: GeoipDatabase = {
|
||||
name: 'GeoIP2-Country',
|
||||
id: 'geoip2-country',
|
||||
type: 'maxmind',
|
||||
};
|
||||
|
||||
const database4: GeoipDatabase = {
|
||||
name: 'Free-IP-to-ASN',
|
||||
id: 'free-ip-to-asn',
|
||||
type: 'ipinfo',
|
||||
};
|
||||
|
||||
const databases = [database1, database2, database3, database4];
|
||||
|
||||
httpRequestsMockHelpers.setLoadDatabasesResponse(databases);
|
||||
|
||||
test('renders the list of databases', async () => {
|
||||
const { exists, find, table } = testBed;
|
||||
|
||||
// Page title
|
||||
expect(exists('manageProcessorsTitle')).toBe(true);
|
||||
expect(find('manageProcessorsTitle').text()).toEqual('Manage Processors');
|
||||
|
||||
// Add database button
|
||||
expect(exists('addGeoipDatabaseButton')).toBe(true);
|
||||
|
||||
// Table has columns for database name and type
|
||||
const { tableCellsValues } = table.getMetaData('geoipDatabaseList');
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const database = databases[i];
|
||||
|
||||
expect(row).toEqual([
|
||||
database.name,
|
||||
database.type === 'maxmind' ? 'MaxMind' : 'IPInfo',
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('deletes a database', async () => {
|
||||
const { actions } = testBed;
|
||||
const databaseIndexToDelete = 0;
|
||||
const databaseName = databases[databaseIndexToDelete].name;
|
||||
httpRequestsMockHelpers.setDeleteDatabasesResponse(databaseName, {});
|
||||
|
||||
await actions.clickDeleteDatabaseButton(databaseIndexToDelete);
|
||||
|
||||
await actions.confirmDeletingDatabase();
|
||||
|
||||
expect(httpSetup.delete).toHaveBeenLastCalledWith(
|
||||
`${API_BASE_PATH}/databases/${databaseName.toLowerCase()}`,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Creates a database', () => {
|
||||
it('creates a MaxMind database when none with the same name exists', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
const databaseName = 'GeoIP2-ISP';
|
||||
const maxmind = '123456';
|
||||
httpRequestsMockHelpers.setCreateDatabasesResponse({
|
||||
name: databaseName,
|
||||
id: databaseName.toLowerCase(),
|
||||
});
|
||||
|
||||
await actions.clickAddDatabaseButton();
|
||||
|
||||
expect(exists('addGeoipDatabaseForm')).toBe(true);
|
||||
|
||||
await actions.fillOutDatabaseValues('maxmind', databaseName, maxmind);
|
||||
|
||||
await actions.confirmAddingDatabase();
|
||||
|
||||
expect(httpSetup.post).toHaveBeenLastCalledWith(`${API_BASE_PATH}/databases`, {
|
||||
asSystemRequest: undefined,
|
||||
body: '{"databaseType":"maxmind","databaseName":"GeoIP2-ISP","maxmind":"123456"}',
|
||||
query: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an IPInfo database when none with the same name exists', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
const databaseName = 'ASN';
|
||||
httpRequestsMockHelpers.setCreateDatabasesResponse({
|
||||
name: databaseName,
|
||||
id: databaseName.toLowerCase(),
|
||||
});
|
||||
|
||||
await actions.clickAddDatabaseButton();
|
||||
|
||||
expect(exists('addGeoipDatabaseForm')).toBe(true);
|
||||
|
||||
await actions.fillOutDatabaseValues('ipinfo', databaseName);
|
||||
|
||||
await actions.confirmAddingDatabase();
|
||||
|
||||
expect(httpSetup.post).toHaveBeenLastCalledWith(`${API_BASE_PATH}/databases`, {
|
||||
asSystemRequest: undefined,
|
||||
body: '{"databaseType":"ipinfo","databaseName":"ASN","maxmind":""}',
|
||||
query: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('No databases', () => {
|
||||
test('displays an empty prompt', async () => {
|
||||
httpRequestsMockHelpers.setLoadDatabasesResponse([]);
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
const { exists, component } = testBed;
|
||||
component.update();
|
||||
|
||||
expect(exists('geoipEmptyListPrompt')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
beforeEach(async () => {
|
||||
const error = {
|
||||
statusCode: 500,
|
||||
error: 'Internal server error',
|
||||
message: 'Internal server error',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setLoadDatabasesResponse(undefined, error);
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup(httpSetup);
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('displays an error callout', async () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('geoipListLoadingError')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,16 +28,15 @@ export interface Pipeline {
|
|||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
export interface PipelinesByName {
|
||||
[key: string]: {
|
||||
description: string;
|
||||
version?: number;
|
||||
processors: Processor[];
|
||||
on_failure?: Processor[];
|
||||
};
|
||||
}
|
||||
|
||||
export enum FieldCopyAction {
|
||||
Copy = 'copy',
|
||||
Rename = 'rename',
|
||||
}
|
||||
|
||||
export type DatabaseType = 'maxmind' | 'ipinfo' | 'web' | 'local' | 'unknown';
|
||||
|
||||
export interface GeoipDatabase {
|
||||
name: string;
|
||||
id: string;
|
||||
type: DatabaseType;
|
||||
}
|
||||
|
|
|
@ -27,20 +27,27 @@ import {
|
|||
PipelinesEdit,
|
||||
PipelinesClone,
|
||||
PipelinesCreateFromCsv,
|
||||
ManageProcessors,
|
||||
} from './sections';
|
||||
import { ROUTES } from './services/navigation';
|
||||
|
||||
export const AppWithoutRouter = () => (
|
||||
<Routes>
|
||||
<Route exact path={ROUTES.list} component={PipelinesList} />
|
||||
<Route exact path={ROUTES.clone} component={PipelinesClone} />
|
||||
<Route exact path={ROUTES.create} component={PipelinesCreate} />
|
||||
<Route exact path={ROUTES.edit} component={PipelinesEdit} />
|
||||
<Route exact path={ROUTES.createFromCsv} component={PipelinesCreateFromCsv} />
|
||||
{/* Catch all */}
|
||||
<Route component={PipelinesList} />
|
||||
</Routes>
|
||||
);
|
||||
export const AppWithoutRouter = () => {
|
||||
const { services } = useKibana();
|
||||
return (
|
||||
<Routes>
|
||||
<Route exact path={ROUTES.list} component={PipelinesList} />
|
||||
<Route exact path={ROUTES.clone} component={PipelinesClone} />
|
||||
<Route exact path={ROUTES.create} component={PipelinesCreate} />
|
||||
<Route exact path={ROUTES.edit} component={PipelinesEdit} />
|
||||
<Route exact path={ROUTES.createFromCsv} component={PipelinesCreateFromCsv} />
|
||||
{services.config.enableManageProcessors && (
|
||||
<Route exact path={ROUTES.manageProcessors} component={ManageProcessors} />
|
||||
)}
|
||||
{/* Catch all */}
|
||||
<Route component={PipelinesList} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export const App: FunctionComponent = () => {
|
||||
const { apiError } = useAuthorizationContext();
|
||||
|
|
|
@ -25,6 +25,7 @@ export { Fingerprint } from './fingerprint';
|
|||
export { Foreach } from './foreach';
|
||||
export { GeoGrid } from './geogrid';
|
||||
export { GeoIP } from './geoip';
|
||||
export { IpLocation } from './ip_location';
|
||||
export { Grok } from './grok';
|
||||
export { Gsub } from './gsub';
|
||||
export { HtmlStrip } from './html_strip';
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiCode } from '@elastic/eui';
|
||||
import { groupBy, map } from 'lodash';
|
||||
|
||||
import {
|
||||
FIELD_TYPES,
|
||||
UseField,
|
||||
ToggleField,
|
||||
ComboBoxField,
|
||||
} from '../../../../../../shared_imports';
|
||||
|
||||
import { useKibana } from '../../../../../../shared_imports';
|
||||
import { FieldNameField } from './common_fields/field_name_field';
|
||||
import { IgnoreMissingField } from './common_fields/ignore_missing_field';
|
||||
import { FieldsConfig, from, to } from './shared';
|
||||
import { TargetField } from './common_fields/target_field';
|
||||
import { PropertiesField } from './common_fields/properties_field';
|
||||
import type { GeoipDatabase } from '../../../../../../../common/types';
|
||||
import { getTypeLabel } from '../../../../../sections/manage_processors/constants';
|
||||
|
||||
const fieldsConfig: FieldsConfig = {
|
||||
/* Optional field config */
|
||||
database_file: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
deserializer: to.arrayOfStrings,
|
||||
serializer: (v: string[]) => (v.length ? v[0] : undefined),
|
||||
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.ipLocationForm.databaseFileLabel', {
|
||||
defaultMessage: 'Database file (optional)',
|
||||
}),
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.pipelineEditor.ipLocationForm.databaseFileHelpText"
|
||||
defaultMessage="GeoIP2 database file in the {ingestGeoIP} configuration directory. Defaults to {databaseFile}."
|
||||
values={{
|
||||
databaseFile: <EuiCode>{'GeoLite2-City.mmdb'}</EuiCode>,
|
||||
ingestGeoIP: <EuiCode>{'ingest-geoip'}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
first_only: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
defaultValue: true,
|
||||
deserializer: to.booleanOrUndef,
|
||||
serializer: from.undefinedIfValue(true),
|
||||
label: i18n.translate(
|
||||
'xpack.ingestPipelines.pipelineEditor.ipLocationForm.firstOnlyFieldLabel',
|
||||
{
|
||||
defaultMessage: 'First only',
|
||||
}
|
||||
),
|
||||
helpText: i18n.translate(
|
||||
'xpack.ingestPipelines.pipelineEditor.ipLocationForm.firstOnlyFieldHelpText',
|
||||
{
|
||||
defaultMessage: 'Use the first matching geo data, even if the field contains an array.',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const IpLocation: FunctionComponent = () => {
|
||||
const { services } = useKibana();
|
||||
const { data, isLoading } = services.api.useLoadDatabases();
|
||||
|
||||
const dataAsOptions = (data || []).map((item) => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
label: item.name,
|
||||
}));
|
||||
const optionsByGroup = groupBy(dataAsOptions, 'type');
|
||||
const groupedOptions = map(optionsByGroup, (items, groupName) => ({
|
||||
label: getTypeLabel(groupName as GeoipDatabase['type']),
|
||||
options: map(items, (item) => item),
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldNameField
|
||||
helpText={i18n.translate(
|
||||
'xpack.ingestPipelines.pipelineEditor.ipLocationForm.fieldNameHelpText',
|
||||
{ defaultMessage: 'Field containing an IP address for the geographical lookup.' }
|
||||
)}
|
||||
/>
|
||||
|
||||
<TargetField
|
||||
helpText={i18n.translate(
|
||||
'xpack.ingestPipelines.pipelineEditor.ipLocationForm.targetFieldHelpText',
|
||||
{
|
||||
defaultMessage: 'Field used to contain geo data properties.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
|
||||
<UseField
|
||||
component={ComboBoxField}
|
||||
config={fieldsConfig.database_file}
|
||||
path="fields.database_file"
|
||||
euiFieldProps={{
|
||||
isLoading,
|
||||
noSuggestions: false,
|
||||
singleSelection: { asPlainText: true },
|
||||
options: groupedOptions,
|
||||
}}
|
||||
/>
|
||||
|
||||
<PropertiesField
|
||||
helpText={i18n.translate(
|
||||
'xpack.ingestPipelines.pipelineEditor.ipLocationForm.propertiesFieldHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Properties added to the target field. Valid properties depend on the database file used.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
|
||||
<UseField component={ToggleField} config={fieldsConfig.first_only} path="fields.first_only" />
|
||||
|
||||
<IgnoreMissingField />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -32,6 +32,7 @@ import {
|
|||
Foreach,
|
||||
GeoGrid,
|
||||
GeoIP,
|
||||
IpLocation,
|
||||
Grok,
|
||||
Gsub,
|
||||
HtmlStrip,
|
||||
|
@ -477,6 +478,24 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
|
|||
},
|
||||
}),
|
||||
},
|
||||
ip_location: {
|
||||
category: processorCategories.DATA_ENRICHMENT,
|
||||
FieldsComponent: IpLocation,
|
||||
docLinkPath: '/geoip-processor.html',
|
||||
label: i18n.translate('xpack.ingestPipelines.processors.label.ipLocation', {
|
||||
defaultMessage: 'IP Location',
|
||||
}),
|
||||
typeDescription: i18n.translate('xpack.ingestPipelines.processors.description.ipLocation', {
|
||||
defaultMessage: 'Adds geo data based on an IP address.',
|
||||
}),
|
||||
getDefaultDescription: ({ field }) =>
|
||||
i18n.translate('xpack.ingestPipelines.processors.defaultDescription.ipLocation', {
|
||||
defaultMessage: 'Adds geo data to documents based on the value of "{field}"',
|
||||
values: {
|
||||
field,
|
||||
},
|
||||
}),
|
||||
},
|
||||
grok: {
|
||||
category: processorCategories.DATA_TRANSFORMATION,
|
||||
FieldsComponent: Grok,
|
||||
|
|
|
@ -13,3 +13,4 @@ export const UIM_PIPELINE_UPDATE = 'pipeline_update';
|
|||
export const UIM_PIPELINE_DELETE = 'pipeline_delete';
|
||||
export const UIM_PIPELINE_DELETE_MANY = 'pipeline_delete_many';
|
||||
export const UIM_PIPELINE_SIMULATE = 'pipeline_simulate';
|
||||
export const UIM_MANAGE_PROCESSORS = 'manage_processes';
|
||||
|
|
|
@ -18,7 +18,7 @@ import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public';
|
|||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
|
||||
import { KibanaContextProvider, KibanaRenderContextProvider } from '../shared_imports';
|
||||
import { ILicense } from '../types';
|
||||
import type { Config, ILicense } from '../types';
|
||||
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
|
||||
|
@ -50,6 +50,7 @@ export interface AppServices {
|
|||
consolePlugin?: ConsolePluginStart;
|
||||
overlays: OverlayStart;
|
||||
http: HttpStart;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
type StartServices = Pick<CoreStart, 'analytics' | 'i18n' | 'theme'>;
|
||||
|
@ -66,7 +67,7 @@ export const renderApp = (
|
|||
render(
|
||||
<KibanaRenderContextProvider {...coreServices}>
|
||||
<AuthorizationProvider
|
||||
privilegesEndpoint={`${API_BASE_PATH}/privileges`}
|
||||
privilegesEndpoint={`${API_BASE_PATH}/privileges/ingest_pipelines`}
|
||||
httpClient={coreServices.http}
|
||||
>
|
||||
<KibanaContextProvider services={services}>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { CoreSetup } from '@kbn/core/public';
|
||||
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
|
||||
import { StartDependencies, ILicense } from '../types';
|
||||
import type { StartDependencies, ILicense, Config } from '../types';
|
||||
import {
|
||||
documentationService,
|
||||
uiMetricService,
|
||||
|
@ -20,13 +20,14 @@ import { renderApp } from '.';
|
|||
|
||||
export interface AppParams extends ManagementAppMountParams {
|
||||
license: ILicense | null;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export async function mountManagementSection(
|
||||
{ http, getStartServices, notifications }: CoreSetup<StartDependencies>,
|
||||
params: AppParams
|
||||
) {
|
||||
const { element, setBreadcrumbs, history, license } = params;
|
||||
const { element, setBreadcrumbs, history, license, config } = params;
|
||||
const [coreStart, depsStart] = await getStartServices();
|
||||
const { docLinks, application, executionContext, overlays } = coreStart;
|
||||
|
||||
|
@ -51,6 +52,7 @@ export async function mountManagementSection(
|
|||
consolePlugin: depsStart.console,
|
||||
overlays,
|
||||
http,
|
||||
config,
|
||||
};
|
||||
|
||||
return renderApp(element, services, { ...coreStart, http });
|
||||
|
|
|
@ -14,3 +14,5 @@ export { PipelinesEdit } from './pipelines_edit';
|
|||
export { PipelinesClone } from './pipelines_clone';
|
||||
|
||||
export { PipelinesCreateFromCsv } from './pipelines_create_from_csv';
|
||||
|
||||
export { ManageProcessors } from './manage_processors';
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
import type { GeoipDatabase } from '../../../../common/types';
|
||||
import { useKibana } from '../../../shared_imports';
|
||||
import {
|
||||
ADD_DATABASE_MODAL_TITLE_ID,
|
||||
ADD_DATABASE_MODAL_FORM_ID,
|
||||
DATABASE_TYPE_OPTIONS,
|
||||
GEOIP_NAME_OPTIONS,
|
||||
IPINFO_NAME_OPTIONS,
|
||||
getAddDatabaseSuccessMessage,
|
||||
addDatabaseErrorTitle,
|
||||
} from './constants';
|
||||
|
||||
export const AddDatabaseModal = ({
|
||||
closeModal,
|
||||
reloadDatabases,
|
||||
databases,
|
||||
}: {
|
||||
closeModal: () => void;
|
||||
reloadDatabases: () => void;
|
||||
databases: GeoipDatabase[];
|
||||
}) => {
|
||||
const [databaseType, setDatabaseType] = useState<string | undefined>(undefined);
|
||||
const [maxmind, setMaxmind] = useState('');
|
||||
const [databaseName, setDatabaseName] = useState('');
|
||||
const [nameExistsError, setNameExistsError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const existingDatabaseNames = useMemo(
|
||||
() => databases.map((database) => database.name),
|
||||
[databases]
|
||||
);
|
||||
const { services } = useKibana();
|
||||
const onDatabaseNameChange = (value: string) => {
|
||||
setDatabaseName(value);
|
||||
setNameExistsError(existingDatabaseNames.includes(value));
|
||||
};
|
||||
const isFormValid = (): boolean => {
|
||||
if (!databaseType || nameExistsError) {
|
||||
return false;
|
||||
}
|
||||
if (databaseType === 'maxmind') {
|
||||
return Boolean(maxmind) && Boolean(databaseName);
|
||||
}
|
||||
return Boolean(databaseName);
|
||||
};
|
||||
const onDatabaseTypeChange = (value: string) => {
|
||||
setDatabaseType(value);
|
||||
};
|
||||
const onAddDatabase = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!isFormValid()) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { error } = await services.api.createDatabase({
|
||||
databaseType: databaseType!,
|
||||
databaseName,
|
||||
maxmind,
|
||||
});
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
services.notifications.toasts.addError(error, {
|
||||
title: addDatabaseErrorTitle,
|
||||
});
|
||||
} else {
|
||||
services.notifications.toasts.addSuccess(getAddDatabaseSuccessMessage(databaseName));
|
||||
await reloadDatabases();
|
||||
closeModal();
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
services.notifications.toasts.addError(e, {
|
||||
title: addDatabaseErrorTitle,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
css={css`
|
||||
width: 500px;
|
||||
`}
|
||||
aria-labelledby={ADD_DATABASE_MODAL_TITLE_ID}
|
||||
onClose={closeModal}
|
||||
initialFocus={'[data-test-subj="databaseTypeSelect"]'}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle id={ADD_DATABASE_MODAL_TITLE_ID}>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addDatabaseModalTitle"
|
||||
defaultMessage="Add database"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiForm
|
||||
fullWidth={true}
|
||||
id={ADD_DATABASE_MODAL_FORM_ID}
|
||||
component="form"
|
||||
onSubmit={(event) => onAddDatabase(event)}
|
||||
data-test-subj="addGeoipDatabaseForm"
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addDatabaseForm.databaseTypeSelectLabel"
|
||||
defaultMessage="Type"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addDatabaseForm.databaseTypeSelectHelpText"
|
||||
defaultMessage="Select which provider you want to use"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
options={DATABASE_TYPE_OPTIONS}
|
||||
hasNoInitialSelection={true}
|
||||
value={databaseType}
|
||||
onChange={(e) => onDatabaseTypeChange(e.target.value)}
|
||||
data-test-subj="databaseTypeSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{databaseType === 'maxmind' && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.licenseCalloutTitle"
|
||||
defaultMessage="Add your MaxMind license key to keystore"
|
||||
/>
|
||||
}
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.licenseCalloutText"
|
||||
defaultMessage="In order to grant access to your MaxMind account, you must add the license key to the keystore."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
{databaseType === 'ipinfo' && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.licenseCalloutTitle"
|
||||
defaultMessage="Add your IP Info license token to keystore"
|
||||
/>
|
||||
}
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.licenseCalloutText"
|
||||
defaultMessage="In order to grant access to your IP Info account, you must add the license token to the keystore."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
|
||||
{databaseType === 'maxmind' && (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addDatabaseForm.maxMindInputLabel"
|
||||
defaultMessage="MaxMind Account ID"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={maxmind}
|
||||
onChange={(e) => setMaxmind(e.target.value)}
|
||||
data-test-subj="maxmindField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{databaseType && (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addDatabaseForm.databaseNameSelectLabel"
|
||||
defaultMessage="Database name"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
options={databaseType === 'maxmind' ? GEOIP_NAME_OPTIONS : IPINFO_NAME_OPTIONS}
|
||||
hasNoInitialSelection={true}
|
||||
value={databaseName}
|
||||
onChange={(e) => onDatabaseNameChange(e.target.value)}
|
||||
data-test-subj="databaseNameSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiForm>
|
||||
|
||||
{nameExistsError && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.nameExistsErrorTitle"
|
||||
defaultMessage="Database already exists"
|
||||
/>
|
||||
}
|
||||
iconType="warning"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.nameExistsErrorText"
|
||||
defaultMessage="Database cannot be added multiple times."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={closeModal}>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addModalCancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
type="submit"
|
||||
form={ADD_DATABASE_MODAL_FORM_ID}
|
||||
disabled={isLoading || !isFormValid()}
|
||||
data-test-subj="addGeoipDatabaseSubmit"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addModalConfirmButtonLabel"
|
||||
defaultMessage="Add database"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { GeoipDatabase } from '../../../../common/types';
|
||||
|
||||
export const ADD_DATABASE_MODAL_TITLE_ID = 'manageProcessorsAddGeoipDatabase';
|
||||
export const ADD_DATABASE_MODAL_FORM_ID = 'manageProcessorsAddGeoipDatabaseForm';
|
||||
export const DATABASE_TYPE_OPTIONS = [
|
||||
{
|
||||
value: 'maxmind',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.maxmindDatabaseType', {
|
||||
defaultMessage: 'MaxMind',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ipinfo',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.ipinfoDatabaseType', {
|
||||
defaultMessage: 'IPInfo',
|
||||
}),
|
||||
},
|
||||
];
|
||||
export const GEOIP_NAME_OPTIONS = [
|
||||
{
|
||||
value: 'GeoIP2-Anonymous-IP',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.anonymousIPDatabaseName', {
|
||||
defaultMessage: 'GeoIP2 Anonymous IP',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'GeoIP2-City',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.cityDatabaseName', {
|
||||
defaultMessage: 'GeoIP2 City',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'GeoIP2-Connection-Type',
|
||||
text: i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.geoip.connectionTypeDatabaseName',
|
||||
{
|
||||
defaultMessage: 'GeoIP2 Connection Type',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'GeoIP2-Country',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.countryDatabaseName', {
|
||||
defaultMessage: 'GeoIP2 Country',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'GeoIP2-Domain',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.domainDatabaseName', {
|
||||
defaultMessage: 'GeoIP2 Domain',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'GeoIP2-Enterprise',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.enterpriseDatabaseName', {
|
||||
defaultMessage: 'GeoIP2 Enterprise',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'GeoIP2-ISP',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.ispDatabaseName', {
|
||||
defaultMessage: 'GeoIP2 ISP',
|
||||
}),
|
||||
},
|
||||
];
|
||||
export const IPINFO_NAME_OPTIONS = [
|
||||
{
|
||||
value: 'asn',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.ipinfo.freeAsnDatabaseName', {
|
||||
defaultMessage: 'Free IP to ASN',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'country',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.ipinfo.freeCountryDatabaseName', {
|
||||
defaultMessage: 'Free IP to Country',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'standard_asn',
|
||||
text: i18n.translate('xpack.ingestPipelines.manageProcessors.ipinfo.asnDatabaseName', {
|
||||
defaultMessage: 'ASN',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'standard_location',
|
||||
text: i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.ipinfo.ipGeolocationDatabaseName',
|
||||
{
|
||||
defaultMessage: 'IP Geolocation',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'standard_privacy',
|
||||
text: i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.ipinfo.privacyDetectionDatabaseName',
|
||||
{
|
||||
defaultMessage: 'Privacy Detection',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const getAddDatabaseSuccessMessage = (databaseName: string): string => {
|
||||
return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.addDatabaseSuccessMessage', {
|
||||
defaultMessage: 'Added database {databaseName}',
|
||||
values: { databaseName },
|
||||
});
|
||||
};
|
||||
|
||||
export const addDatabaseErrorTitle = i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.geoip.addDatabaseErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Error adding database',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_DATABASE_MODAL_TITLE_ID = 'manageProcessorsDeleteGeoipDatabase';
|
||||
export const DELETE_DATABASE_MODAL_FORM_ID = 'manageProcessorsDeleteGeoipDatabaseForm';
|
||||
|
||||
export const getDeleteDatabaseSuccessMessage = (databaseName: string): string => {
|
||||
return i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.geoip.deleteDatabaseSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Deleted database {databaseName}',
|
||||
values: { databaseName },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDatabaseErrorTitle = i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.geoip.deleteDatabaseErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Error deleting database',
|
||||
}
|
||||
);
|
||||
|
||||
export const getTypeLabel = (type: GeoipDatabase['type']): string => {
|
||||
switch (type) {
|
||||
case 'maxmind': {
|
||||
return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.typeMaxmindLabel', {
|
||||
defaultMessage: 'MaxMind',
|
||||
});
|
||||
}
|
||||
case 'ipinfo': {
|
||||
return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.typeIpinfoLabel', {
|
||||
defaultMessage: 'IPInfo',
|
||||
});
|
||||
}
|
||||
case 'web': {
|
||||
return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.webLabel', {
|
||||
defaultMessage: 'Web',
|
||||
});
|
||||
}
|
||||
case 'local': {
|
||||
return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.localLabel', {
|
||||
defaultMessage: 'Local',
|
||||
});
|
||||
}
|
||||
case 'unknown':
|
||||
default: {
|
||||
return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.typeUnknownLabel', {
|
||||
defaultMessage: 'Unknown',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useState } from 'react';
|
||||
import type { GeoipDatabase } from '../../../../common/types';
|
||||
import { useKibana } from '../../../shared_imports';
|
||||
import {
|
||||
DELETE_DATABASE_MODAL_FORM_ID,
|
||||
DELETE_DATABASE_MODAL_TITLE_ID,
|
||||
deleteDatabaseErrorTitle,
|
||||
getDeleteDatabaseSuccessMessage,
|
||||
} from './constants';
|
||||
|
||||
export const DeleteDatabaseModal = ({
|
||||
closeModal,
|
||||
database,
|
||||
reloadDatabases,
|
||||
}: {
|
||||
closeModal: () => void;
|
||||
database: GeoipDatabase;
|
||||
reloadDatabases: () => void;
|
||||
}) => {
|
||||
const [confirmation, setConfirmation] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isValid = confirmation === 'delete';
|
||||
const { services } = useKibana();
|
||||
const onDeleteDatabase = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { error } = await services.api.deleteDatabase(database.id);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
services.notifications.toasts.addError(error, {
|
||||
title: deleteDatabaseErrorTitle,
|
||||
});
|
||||
} else {
|
||||
services.notifications.toasts.addSuccess(getDeleteDatabaseSuccessMessage(database.name));
|
||||
await reloadDatabases();
|
||||
closeModal();
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
services.notifications.toasts.addError(e, {
|
||||
title: deleteDatabaseErrorTitle,
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<EuiModal
|
||||
aria-labelledby={DELETE_DATABASE_MODAL_TITLE_ID}
|
||||
onClose={closeModal}
|
||||
initialFocus="[name=confirmation]"
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle id={DELETE_DATABASE_MODAL_TITLE_ID}>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.deleteDatabaseModalTitle"
|
||||
defaultMessage="Delete {database}"
|
||||
values={{
|
||||
database: database.name,
|
||||
}}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiForm
|
||||
id={DELETE_DATABASE_MODAL_FORM_ID}
|
||||
component="form"
|
||||
onSubmit={(event) => onDeleteDatabase(event)}
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.deleteDatabaseForm.confirmationLabel"
|
||||
defaultMessage={'Please type "delete" to confirm.'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="confirmation"
|
||||
value={confirmation}
|
||||
onChange={(e) => setConfirmation(e.target.value)}
|
||||
data-test-subj="geoipDatabaseConfirmation"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={closeModal}>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.deleteModalCancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
type="submit"
|
||||
form={DELETE_DATABASE_MODAL_FORM_ID}
|
||||
disabled={isLoading || !isValid}
|
||||
color="danger"
|
||||
data-test-subj="deleteGeoipDatabaseSubmit"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.deleteModalConfirmButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiPageTemplate } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
|
||||
export const EmptyList = ({ addDatabaseButton }: { addDatabaseButton: JSX.Element }) => {
|
||||
return (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
iconType="database"
|
||||
iconColor="default"
|
||||
title={
|
||||
<h2 data-test-subj="geoipEmptyListPrompt">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.emptyPromptTitle"
|
||||
defaultMessage="Add your first database for IP Location processor"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.emptyPromptDescription"
|
||||
defaultMessage="Use a custom database when setting up IP Location processor."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={addDatabaseButton}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiInMemoryTableProps,
|
||||
EuiPageTemplate,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { IPINFO_NAME_OPTIONS } from './constants';
|
||||
import type { GeoipDatabase } from '../../../../common/types';
|
||||
import { SectionLoading, useKibana } from '../../../shared_imports';
|
||||
import { getTypeLabel } from './constants';
|
||||
import { EmptyList } from './empty_list';
|
||||
import { AddDatabaseModal } from './add_database_modal';
|
||||
import { DeleteDatabaseModal } from './delete_database_modal';
|
||||
import { getErrorMessage } from './get_error_message';
|
||||
|
||||
export const GeoipList: React.FunctionComponent = () => {
|
||||
const { services } = useKibana();
|
||||
const { data, isLoading, error, resendRequest } = services.api.useLoadDatabases();
|
||||
const [showModal, setShowModal] = useState<'add' | 'delete' | null>(null);
|
||||
const [databaseToDelete, setDatabaseToDelete] = useState<GeoipDatabase | null>(null);
|
||||
const onDatabaseDelete = (item: GeoipDatabase) => {
|
||||
setDatabaseToDelete(item);
|
||||
setShowModal('delete');
|
||||
};
|
||||
let content: JSX.Element;
|
||||
const addDatabaseButton = (
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={() => {
|
||||
setShowModal('add');
|
||||
}}
|
||||
data-test-subj="addGeoipDatabaseButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.addDatabaseButtonLabel"
|
||||
defaultMessage="Add database"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
const tableProps: EuiInMemoryTableProps<GeoipDatabase> = {
|
||||
'data-test-subj': 'geoipDatabaseList',
|
||||
rowProps: () => ({
|
||||
'data-test-subj': 'geoipDatabaseListRow',
|
||||
}),
|
||||
columns: [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.nameColumnTitle', {
|
||||
defaultMessage: 'Database name',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (name: string, row) => {
|
||||
if (row.type === 'ipinfo') {
|
||||
// find the name in the options to get the translated value
|
||||
const option = IPINFO_NAME_OPTIONS.find((opt) => opt.value === name);
|
||||
return option?.text ?? name;
|
||||
}
|
||||
|
||||
return name;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
name: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.typeColumnTitle', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (type: GeoipDatabase['type']) => {
|
||||
return getTypeLabel(type);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
align: 'right',
|
||||
render: (item: GeoipDatabase) => {
|
||||
// Local and web databases are read only and cannot be deleted through UI
|
||||
if (['web', 'local'].includes(item.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
name="Delete"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ingestPipelines.manageProcessors.geoip.list.actionIconLabel',
|
||||
{
|
||||
defaultMessage: 'Delete this database',
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={() => onDatabaseDelete(item)}
|
||||
data-test-subj="deleteGeoipDatabaseButton"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
items: data ?? [],
|
||||
};
|
||||
if (error) {
|
||||
content = (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
color="danger"
|
||||
iconType="warning"
|
||||
title={
|
||||
<h2 data-test-subj="geoipListLoadingError">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.list.loadErrorTitle"
|
||||
defaultMessage="Unable to load geoIP databases"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={<p>{getErrorMessage(error)}</p>}
|
||||
actions={
|
||||
<EuiButton onClick={resendRequest} iconType="refresh" color="danger">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.list.geoipListReloadButton"
|
||||
defaultMessage="Try again"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (isLoading && !data) {
|
||||
content = (
|
||||
<SectionLoading data-test-subj="sectionLoading">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.list.loadingMessage"
|
||||
defaultMessage="Loading geoIP databases..."
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (data && data.length === 0) {
|
||||
content = <EmptyList addDatabaseButton={addDatabaseButton} />;
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.geoip.tableTitle"
|
||||
defaultMessage="GeoIP"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{addDatabaseButton}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
<EuiInMemoryTable
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
{...tableProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{content}
|
||||
{showModal === 'add' && (
|
||||
<AddDatabaseModal
|
||||
closeModal={() => setShowModal(null)}
|
||||
reloadDatabases={resendRequest}
|
||||
databases={data!}
|
||||
/>
|
||||
)}
|
||||
{showModal === 'delete' && databaseToDelete && (
|
||||
<DeleteDatabaseModal
|
||||
database={databaseToDelete}
|
||||
reloadDatabases={resendRequest}
|
||||
closeModal={() => setShowModal(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { EuiCode } from '@elastic/eui';
|
||||
import { ResponseErrorBody } from '@kbn/core-http-browser';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const getErrorMessage = (error: ResponseErrorBody) => {
|
||||
if (error.statusCode === 403) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.deniedPrivilegeDescription"
|
||||
defaultMessage="To manage geoIP databases, you must have the {manage} cluster privilege."
|
||||
values={{
|
||||
manage: <EuiCode>manage</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return error.message;
|
||||
};
|
|
@ -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 { ManageProcessors } from './manage_processors';
|
||||
export { useCheckManageProcessorsPrivileges } from './use_check_manage_processors_privileges';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useKibana } from '../../../shared_imports';
|
||||
import { UIM_MANAGE_PROCESSORS } from '../../constants';
|
||||
import { GeoipList } from './geoip_list';
|
||||
|
||||
export const ManageProcessors: React.FunctionComponent = () => {
|
||||
const { services } = useKibana();
|
||||
// Track component loaded
|
||||
useEffect(() => {
|
||||
services.metric.trackUiMetric(UIM_MANAGE_PROCESSORS);
|
||||
services.breadcrumbs.setBreadcrumbs('manage_processors');
|
||||
}, [services.metric, services.breadcrumbs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader
|
||||
bottomBorder
|
||||
pageTitle={
|
||||
<span data-test-subj="manageProcessorsTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.manageProcessors.pageTitle"
|
||||
defaultMessage="Manage Processors"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<GeoipList />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useKibana } from '../../../shared_imports';
|
||||
|
||||
export const useCheckManageProcessorsPrivileges = () => {
|
||||
const { services } = useKibana();
|
||||
const { isLoading, data: privilegesData } = services.api.useLoadManageProcessorsPrivileges();
|
||||
const hasPrivileges = privilegesData?.hasAllPrivileges;
|
||||
return isLoading ? false : !!hasPrivileges;
|
||||
};
|
|
@ -26,7 +26,14 @@ import {
|
|||
import { Pipeline } from '../../../../common/types';
|
||||
import { useKibana, SectionLoading } from '../../../shared_imports';
|
||||
import { UIM_PIPELINES_LIST_LOAD } from '../../constants';
|
||||
import { getEditPath, getClonePath } from '../../services/navigation';
|
||||
import {
|
||||
getEditPath,
|
||||
getClonePath,
|
||||
getCreateFromCsvPath,
|
||||
getCreatePath,
|
||||
getManageProcessorsPath,
|
||||
} from '../../services/navigation';
|
||||
import { useCheckManageProcessorsPrivileges } from '../manage_processors';
|
||||
|
||||
import { EmptyList } from './empty_list';
|
||||
import { PipelineTable } from './table';
|
||||
|
@ -54,6 +61,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
|
|||
|
||||
const { data, isLoading, error, resendRequest } = services.api.useLoadPipelines();
|
||||
|
||||
const hasManageProcessorsPrivileges = useCheckManageProcessorsPrivileges();
|
||||
// Track component loaded
|
||||
useEffect(() => {
|
||||
services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD);
|
||||
|
@ -142,7 +150,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
|
|||
name: i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', {
|
||||
defaultMessage: 'New pipeline',
|
||||
}),
|
||||
...reactRouterNavigate(history, '/create'),
|
||||
...reactRouterNavigate(history, getCreatePath()),
|
||||
'data-test-subj': `createNewPipeline`,
|
||||
},
|
||||
/**
|
||||
|
@ -152,10 +160,71 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
|
|||
name: i18n.translate('xpack.ingestPipelines.list.table.createPipelineFromCsvButtonLabel', {
|
||||
defaultMessage: 'New pipeline from CSV',
|
||||
}),
|
||||
...reactRouterNavigate(history, '/csv_create'),
|
||||
...reactRouterNavigate(history, getCreateFromCsvPath()),
|
||||
'data-test-subj': `createPipelineFromCsv`,
|
||||
},
|
||||
];
|
||||
const titleActionButtons = [
|
||||
<EuiPopover
|
||||
key="createPipelinePopover"
|
||||
isOpen={showPopover}
|
||||
closePopover={() => setShowPopover(false)}
|
||||
button={
|
||||
<EuiButton
|
||||
fill
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
data-test-subj="createPipelineDropdown"
|
||||
key="createPipelineDropdown"
|
||||
onClick={() => setShowPopover((previousBool) => !previousBool)}
|
||||
>
|
||||
{i18n.translate('xpack.ingestPipelines.list.table.createPipelineDropdownLabel', {
|
||||
defaultMessage: 'Create pipeline',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
panelPaddingSize="none"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
data-test-subj="autoFollowPatternActionContextMenu"
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
items: createMenuItems,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>,
|
||||
];
|
||||
if (services.config.enableManageProcessors && hasManageProcessorsPrivileges) {
|
||||
titleActionButtons.push(
|
||||
<EuiButtonEmpty
|
||||
iconType="wrench"
|
||||
data-test-subj="manageProcessorsLink"
|
||||
{...reactRouterNavigate(history, getManageProcessorsPath())}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.list.manageProcessorsLinkText"
|
||||
defaultMessage="Manage processors"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
titleActionButtons.push(
|
||||
<EuiButtonEmpty
|
||||
href={services.documentation.getIngestNodeUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.list.pipelinesDocsLinkText"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const renderFlyout = (): React.ReactNode => {
|
||||
if (!showFlyout) {
|
||||
|
@ -199,51 +268,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
|
|||
defaultMessage="Use ingest pipelines to remove or transform fields, extract values from text, and enrich your data before indexing into Elasticsearch."
|
||||
/>
|
||||
}
|
||||
rightSideItems={[
|
||||
<EuiPopover
|
||||
key="createPipelinePopover"
|
||||
isOpen={showPopover}
|
||||
closePopover={() => setShowPopover(false)}
|
||||
button={
|
||||
<EuiButton
|
||||
fill
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
data-test-subj="createPipelineDropdown"
|
||||
key="createPipelineDropdown"
|
||||
onClick={() => setShowPopover((previousBool) => !previousBool)}
|
||||
>
|
||||
{i18n.translate('xpack.ingestPipelines.list.table.createPipelineDropdownLabel', {
|
||||
defaultMessage: 'Create pipeline',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
panelPaddingSize="none"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
data-test-subj="autoFollowPatternActionContextMenu"
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
items: createMenuItems,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>,
|
||||
<EuiButtonEmpty
|
||||
href={services.documentation.getIngestNodeUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.list.pipelinesDocsLinkText"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
rightSideItems={titleActionButtons}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { HttpSetup, ResponseErrorBody } from '@kbn/core/public';
|
||||
|
||||
import { FieldCopyAction, Pipeline } from '../../../common/types';
|
||||
import type { FieldCopyAction, GeoipDatabase, Pipeline } from '../../../common/types';
|
||||
import { API_BASE_PATH } from '../../../common/constants';
|
||||
import {
|
||||
UseRequestConfig,
|
||||
|
@ -140,6 +140,39 @@ export class ApiService {
|
|||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public useLoadDatabases() {
|
||||
return this.useRequest<GeoipDatabase[], ResponseErrorBody>({
|
||||
path: `${API_BASE_PATH}/databases`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
public async createDatabase(database: {
|
||||
databaseType: string;
|
||||
maxmind?: string;
|
||||
databaseName: string;
|
||||
}) {
|
||||
return this.sendRequest({
|
||||
path: `${API_BASE_PATH}/databases`,
|
||||
method: 'post',
|
||||
body: JSON.stringify(database),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteDatabase(id: string) {
|
||||
return this.sendRequest({
|
||||
path: `${API_BASE_PATH}/databases/${id}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
public useLoadManageProcessorsPrivileges() {
|
||||
return this.useRequest<{ hasAllPrivileges: boolean }>({
|
||||
path: `${API_BASE_PATH}/privileges/manage_processors`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
|
|
|
@ -48,6 +48,17 @@ export class BreadcrumbService {
|
|||
}),
|
||||
},
|
||||
],
|
||||
manage_processors: [
|
||||
{
|
||||
text: homeBreadcrumbText,
|
||||
href: `/`,
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.ingestPipelines.breadcrumb.manageProcessorsLabel', {
|
||||
defaultMessage: 'Manage processors',
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
private setBreadcrumbsHandler?: SetBreadcrumbs;
|
||||
|
@ -56,7 +67,7 @@ export class BreadcrumbService {
|
|||
this.setBreadcrumbsHandler = setBreadcrumbsHandler;
|
||||
}
|
||||
|
||||
public setBreadcrumbs(type: 'create' | 'home' | 'edit'): void {
|
||||
public setBreadcrumbs(type: 'create' | 'home' | 'edit' | 'manage_processors'): void {
|
||||
if (!this.setBreadcrumbsHandler) {
|
||||
throw new Error('Breadcrumb service has not been initialized');
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ const CREATE_PATH = 'create';
|
|||
|
||||
const CREATE_FROM_CSV_PATH = 'csv_create';
|
||||
|
||||
const MANAGE_PROCESSORS_PATH = 'manage_processors';
|
||||
|
||||
const _getEditPath = (name: string, encode = true): string => {
|
||||
return `${BASE_PATH}${EDIT_PATH}/${encode ? encodeURIComponent(name) : name}`;
|
||||
};
|
||||
|
@ -33,12 +35,17 @@ const _getCreateFromCsvPath = (): string => {
|
|||
return `${BASE_PATH}${CREATE_FROM_CSV_PATH}`;
|
||||
};
|
||||
|
||||
const _getManageProcessorsPath = (): string => {
|
||||
return `${BASE_PATH}${MANAGE_PROCESSORS_PATH}`;
|
||||
};
|
||||
|
||||
export const ROUTES = {
|
||||
list: _getListPath(),
|
||||
edit: _getEditPath(':name', false),
|
||||
create: _getCreatePath(),
|
||||
clone: _getClonePath(':sourceName', false),
|
||||
createFromCsv: _getCreateFromCsvPath(),
|
||||
manageProcessors: _getManageProcessorsPath(),
|
||||
};
|
||||
|
||||
export const getListPath = ({
|
||||
|
@ -52,3 +59,4 @@ export const getCreatePath = (): string => _getCreatePath();
|
|||
export const getClonePath = ({ clonedPipelineName }: { clonedPipelineName: string }): string =>
|
||||
_getClonePath(clonedPipelineName, true);
|
||||
export const getCreateFromCsvPath = (): string => _getCreateFromCsvPath();
|
||||
export const getManageProcessorsPath = (): string => _getManageProcessorsPath();
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { IngestPipelinesPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new IngestPipelinesPlugin();
|
||||
export function plugin(context: PluginInitializerContext) {
|
||||
return new IngestPipelinesPlugin(context);
|
||||
}
|
||||
|
||||
export { INGEST_PIPELINES_APP_LOCATOR, INGEST_PIPELINES_PAGES } from './locator';
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreStart, CoreSetup, Plugin } from '@kbn/core/public';
|
||||
import type { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
|
||||
import { PLUGIN_ID } from '../common/constants';
|
||||
import { uiMetricService, apiService } from './application/services';
|
||||
import { SetupDependencies, StartDependencies, ILicense } from './types';
|
||||
import type { SetupDependencies, StartDependencies, ILicense, Config } from './types';
|
||||
import { IngestPipelinesLocatorDefinition } from './locator';
|
||||
|
||||
export class IngestPipelinesPlugin
|
||||
|
@ -19,6 +19,11 @@ export class IngestPipelinesPlugin
|
|||
{
|
||||
private license: ILicense | null = null;
|
||||
private licensingSubscription?: Subscription;
|
||||
private readonly config: Config;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext<Config>) {
|
||||
this.config = initializerContext.config.get();
|
||||
}
|
||||
|
||||
public setup(coreSetup: CoreSetup<StartDependencies>, plugins: SetupDependencies): void {
|
||||
const { management, usageCollection, share } = plugins;
|
||||
|
@ -49,6 +54,9 @@ export class IngestPipelinesPlugin
|
|||
const unmountAppCallback = await mountManagementSection(coreSetup, {
|
||||
...params,
|
||||
license: this.license,
|
||||
config: {
|
||||
enableManageProcessors: this.config.enableManageProcessors !== false,
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
|
|
@ -25,3 +25,7 @@ export interface StartDependencies {
|
|||
licensing?: LicensingPluginStart;
|
||||
console?: ConsolePluginStart;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
enableManageProcessors: boolean;
|
||||
}
|
||||
|
|
29
x-pack/plugins/ingest_pipelines/server/config.ts
Normal file
29
x-pack/plugins/ingest_pipelines/server/config.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { offeringBasedSchema, schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor } from '@kbn/core-plugins-server';
|
||||
|
||||
const configSchema = schema.object(
|
||||
{
|
||||
enableManageProcessors: offeringBasedSchema({
|
||||
// Manage processors UI is disabled in serverless; refer to the serverless.yml file as the source of truth
|
||||
// We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana
|
||||
serverless: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
},
|
||||
{ defaultValue: undefined }
|
||||
);
|
||||
|
||||
export type IngestPipelinesConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<IngestPipelinesConfigType> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
enableManageProcessors: true,
|
||||
},
|
||||
};
|
|
@ -5,7 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export async function plugin() {
|
||||
import { PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export async function plugin(context: PluginInitializerContext) {
|
||||
const { IngestPipelinesPlugin } = await import('./plugin');
|
||||
return new IngestPipelinesPlugin();
|
||||
return new IngestPipelinesPlugin(context);
|
||||
}
|
||||
|
|
|
@ -5,17 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup, Plugin } from '@kbn/core/server';
|
||||
import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
|
||||
import { IngestPipelinesConfigType } from './config';
|
||||
import { ApiRoutes } from './routes';
|
||||
import { handleEsError } from './shared_imports';
|
||||
import { Dependencies } from './types';
|
||||
|
||||
export class IngestPipelinesPlugin implements Plugin<void, void, any, any> {
|
||||
private readonly apiRoutes: ApiRoutes;
|
||||
private readonly config: IngestPipelinesConfigType;
|
||||
|
||||
constructor() {
|
||||
constructor(initContext: PluginInitializerContext<IngestPipelinesConfigType>) {
|
||||
this.apiRoutes = new ApiRoutes();
|
||||
this.config = initContext.config.get();
|
||||
}
|
||||
|
||||
public setup({ http }: CoreSetup, { security, features }: Dependencies) {
|
||||
|
@ -38,6 +41,7 @@ export class IngestPipelinesPlugin implements Plugin<void, void, any, any> {
|
|||
router,
|
||||
config: {
|
||||
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
|
||||
enableManageProcessors: this.config.enableManageProcessors !== false,
|
||||
},
|
||||
lib: {
|
||||
handleEsError,
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from '../../../types';
|
||||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { serializeGeoipDatabase } from './serialization';
|
||||
import { normalizeDatabaseName } from './normalize_database_name';
|
||||
|
||||
const bodySchema = schema.object({
|
||||
databaseType: schema.oneOf([schema.literal('ipinfo'), schema.literal('maxmind')]),
|
||||
// maxmind is only needed for "geoip" type
|
||||
maxmind: schema.maybe(schema.string({ maxLength: 1000 })),
|
||||
// only allow database names in sync with ES
|
||||
databaseName: schema.oneOf([
|
||||
// geoip names https://github.com/elastic/elasticsearch/blob/f150e2c11df0fe3bef298c55bd867437e50f5f73/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/direct/DatabaseConfiguration.java#L58
|
||||
schema.literal('GeoIP2-Anonymous-IP'),
|
||||
schema.literal('GeoIP2-City'),
|
||||
schema.literal('GeoIP2-Connection-Type'),
|
||||
schema.literal('GeoIP2-Country'),
|
||||
schema.literal('GeoIP2-Domain'),
|
||||
schema.literal('GeoIP2-Enterprise'),
|
||||
schema.literal('GeoIP2-ISP'),
|
||||
// ipinfo names
|
||||
schema.literal('asn'),
|
||||
schema.literal('country'),
|
||||
schema.literal('standard_asn'),
|
||||
schema.literal('standard_location'),
|
||||
schema.literal('standard_privacy'),
|
||||
]),
|
||||
});
|
||||
|
||||
export const registerCreateDatabaseRoute = ({
|
||||
router,
|
||||
lib: { handleEsError },
|
||||
}: RouteDependencies): void => {
|
||||
router.post(
|
||||
{
|
||||
path: `${API_BASE_PATH}/databases`,
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const { client: clusterClient } = (await ctx.core).elasticsearch;
|
||||
const { databaseType, databaseName, maxmind } = req.body;
|
||||
const serializedDatabase = serializeGeoipDatabase({ databaseType, databaseName, maxmind });
|
||||
const normalizedDatabaseName = normalizeDatabaseName(databaseName);
|
||||
|
||||
try {
|
||||
// TODO: Replace this request with the one below when the JS client fixed
|
||||
await clusterClient.asCurrentUser.transport.request({
|
||||
method: 'PUT',
|
||||
path: `/_ingest/ip_location/database/${normalizedDatabaseName}`,
|
||||
body: serializedDatabase,
|
||||
});
|
||||
|
||||
// This request fails because there is a bug in the JS client
|
||||
// await clusterClient.asCurrentUser.ingest.putGeoipDatabase({
|
||||
// id: normalizedDatabaseName,
|
||||
// body: serializedDatabase,
|
||||
// });
|
||||
|
||||
return res.ok({ body: { name: databaseName, id: normalizedDatabaseName } });
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response: res });
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from '../../../types';
|
||||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
database_id: schema.string(),
|
||||
});
|
||||
|
||||
export const registerDeleteDatabaseRoute = ({
|
||||
router,
|
||||
lib: { handleEsError },
|
||||
}: RouteDependencies): void => {
|
||||
router.delete(
|
||||
{
|
||||
path: `${API_BASE_PATH}/databases/{database_id}`,
|
||||
validate: {
|
||||
params: paramsSchema,
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const { client: clusterClient } = (await ctx.core).elasticsearch;
|
||||
const { database_id: databaseID } = req.params;
|
||||
|
||||
try {
|
||||
await clusterClient.asCurrentUser.ingest.deleteGeoipDatabase({ id: databaseID });
|
||||
|
||||
return res.ok();
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response: res });
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 { registerListDatabaseRoute } from './list';
|
||||
export { registerCreateDatabaseRoute } from './create';
|
||||
export { registerDeleteDatabaseRoute } from './delete';
|
|
@ -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 { deserializeGeoipDatabase, type GeoipDatabaseFromES } from './serialization';
|
||||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
export const registerListDatabaseRoute = ({
|
||||
router,
|
||||
lib: { handleEsError },
|
||||
}: RouteDependencies): void => {
|
||||
router.get({ path: `${API_BASE_PATH}/databases`, validate: false }, async (ctx, req, res) => {
|
||||
const { client: clusterClient } = (await ctx.core).elasticsearch;
|
||||
|
||||
try {
|
||||
const data = (await clusterClient.asCurrentUser.ingest.getGeoipDatabase()) as {
|
||||
databases: GeoipDatabaseFromES[];
|
||||
};
|
||||
|
||||
const geoipDatabases = data.databases;
|
||||
|
||||
return res.ok({ body: geoipDatabases.map(deserializeGeoipDatabase) });
|
||||
} catch (error) {
|
||||
const esErrorResponse = handleEsError({ error, response: res });
|
||||
if (esErrorResponse.status === 404) {
|
||||
// ES returns 404 when there are no pipelines
|
||||
// Instead, we return an empty array and 200 status back to the client
|
||||
return res.ok({ body: [] });
|
||||
}
|
||||
return esErrorResponse;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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 const normalizeDatabaseName = (databaseName: string): string => {
|
||||
return databaseName.replace(/\s+/g, '_').toLowerCase();
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 interface GeoipDatabaseFromES {
|
||||
id: string;
|
||||
version: number;
|
||||
modified_date_millis: number;
|
||||
database: {
|
||||
name: string;
|
||||
// maxmind type
|
||||
maxmind?: {
|
||||
account_id: string;
|
||||
};
|
||||
// ipinfo type
|
||||
ipinfo?: {};
|
||||
// local type
|
||||
local?: {};
|
||||
// web type
|
||||
web?: {};
|
||||
};
|
||||
}
|
||||
|
||||
interface SerializedGeoipDatabase {
|
||||
name: string;
|
||||
ipinfo?: {};
|
||||
local?: {};
|
||||
web?: {};
|
||||
maxmind?: {
|
||||
account_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
const getGeoipType = ({ database }: GeoipDatabaseFromES) => {
|
||||
if (database.maxmind && database.maxmind.account_id) {
|
||||
return 'maxmind';
|
||||
}
|
||||
|
||||
if (database.ipinfo) {
|
||||
return 'ipinfo';
|
||||
}
|
||||
|
||||
if (database.local) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
if (database.web) {
|
||||
return 'web';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const deserializeGeoipDatabase = (geoipDatabase: GeoipDatabaseFromES) => {
|
||||
const { database, id } = geoipDatabase;
|
||||
return {
|
||||
name: database.name,
|
||||
id,
|
||||
type: getGeoipType(geoipDatabase),
|
||||
};
|
||||
};
|
||||
|
||||
export const serializeGeoipDatabase = ({
|
||||
databaseType,
|
||||
databaseName,
|
||||
maxmind,
|
||||
}: {
|
||||
databaseType: 'maxmind' | 'ipinfo' | 'local' | 'web';
|
||||
databaseName: string;
|
||||
maxmind?: string;
|
||||
}): SerializedGeoipDatabase => {
|
||||
const database = { name: databaseName } as SerializedGeoipDatabase;
|
||||
|
||||
if (databaseType === 'maxmind') {
|
||||
database.maxmind = { account_id: maxmind ?? '' };
|
||||
}
|
||||
|
||||
if (databaseType === 'ipinfo') {
|
||||
database.ipinfo = {};
|
||||
}
|
||||
|
||||
if (databaseType === 'local') {
|
||||
database.local = {};
|
||||
}
|
||||
|
||||
if (databaseType === 'web') {
|
||||
database.web = {};
|
||||
}
|
||||
|
||||
return database;
|
||||
};
|
|
@ -20,3 +20,9 @@ export { registerSimulateRoute } from './simulate';
|
|||
export { registerDocumentsRoute } from './documents';
|
||||
|
||||
export { registerParseCsvRoute } from './parse_csv';
|
||||
|
||||
export {
|
||||
registerListDatabaseRoute,
|
||||
registerCreateDatabaseRoute,
|
||||
registerDeleteDatabaseRoute,
|
||||
} from './database';
|
||||
|
|
|
@ -6,9 +6,14 @@
|
|||
*/
|
||||
|
||||
import { Privileges } from '@kbn/es-ui-shared-plugin/common';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { RouteDependencies } from '../../types';
|
||||
import { API_BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../../common/constants';
|
||||
|
||||
const requiredPrivilegesMap = {
|
||||
ingest_pipelines: APP_CLUSTER_REQUIRED_PRIVILEGES,
|
||||
manage_processors: ['manage'],
|
||||
};
|
||||
const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] =>
|
||||
Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => {
|
||||
if (!privilegesObject[privilegeName]) {
|
||||
|
@ -20,10 +25,18 @@ const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } =
|
|||
export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${API_BASE_PATH}/privileges`,
|
||||
validate: false,
|
||||
path: `${API_BASE_PATH}/privileges/{permissions_type}`,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
permissions_type: schema.oneOf([
|
||||
schema.literal('ingest_pipelines'),
|
||||
schema.literal('manage_processors'),
|
||||
]),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const permissionsType = req.params.permissions_type;
|
||||
const privilegesResult: Privileges = {
|
||||
hasAllPrivileges: true,
|
||||
missingPrivileges: {
|
||||
|
@ -38,9 +51,10 @@ export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) =
|
|||
|
||||
const { client: clusterClient } = (await ctx.core).elasticsearch;
|
||||
|
||||
const requiredPrivileges = requiredPrivilegesMap[permissionsType];
|
||||
const { has_all_requested: hasAllPrivileges, cluster } =
|
||||
await clusterClient.asCurrentUser.security.hasPrivileges({
|
||||
body: { cluster: APP_CLUSTER_REQUIRED_PRIVILEGES },
|
||||
body: { cluster: requiredPrivileges },
|
||||
});
|
||||
|
||||
if (!hasAllPrivileges) {
|
||||
|
|
|
@ -16,6 +16,9 @@ import {
|
|||
registerSimulateRoute,
|
||||
registerDocumentsRoute,
|
||||
registerParseCsvRoute,
|
||||
registerListDatabaseRoute,
|
||||
registerCreateDatabaseRoute,
|
||||
registerDeleteDatabaseRoute,
|
||||
} from './api';
|
||||
|
||||
export class ApiRoutes {
|
||||
|
@ -28,5 +31,10 @@ export class ApiRoutes {
|
|||
registerSimulateRoute(dependencies);
|
||||
registerDocumentsRoute(dependencies);
|
||||
registerParseCsvRoute(dependencies);
|
||||
if (dependencies.config.enableManageProcessors) {
|
||||
registerListDatabaseRoute(dependencies);
|
||||
registerCreateDatabaseRoute(dependencies);
|
||||
registerDeleteDatabaseRoute(dependencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface RouteDependencies {
|
|||
router: IRouter;
|
||||
config: {
|
||||
isSecurityEnabled: () => boolean;
|
||||
enableManageProcessors: boolean;
|
||||
};
|
||||
lib: {
|
||||
handleEsError: typeof handleEsError;
|
||||
|
|
|
@ -36,7 +36,9 @@
|
|||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/unsaved-changes-prompt",
|
||||
"@kbn/core-http-browser-mocks",
|
||||
"@kbn/shared-ux-table-persist"
|
||||
"@kbn/shared-ux-table-persist",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/core-plugins-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const ingestPipelines = getService('ingestPipelines');
|
||||
const url = `/api/ingest_pipelines/databases`;
|
||||
const databaseName = 'GeoIP2-Anonymous-IP';
|
||||
const normalizedDatabaseName = 'geoip2-anonymous-ip';
|
||||
|
||||
describe('Manage databases', function () {
|
||||
after(async () => {
|
||||
await ingestPipelines.api.deleteGeoipDatabases();
|
||||
});
|
||||
|
||||
describe('Create', () => {
|
||||
it('creates a geoip database when using a correct database name', async () => {
|
||||
const database = { maxmind: '123456', databaseName };
|
||||
const { body } = await supertest
|
||||
.post(url)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send(database)
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
name: databaseName,
|
||||
id: normalizedDatabaseName,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a geoip database when using an incorrect database name', async () => {
|
||||
const database = { maxmind: '123456', databaseName: 'Test' };
|
||||
await supertest.post(url).set('kbn-xsrf', 'xxx').send(database).expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('List', () => {
|
||||
it('returns existing databases', async () => {
|
||||
const { body } = await supertest.get(url).set('kbn-xsrf', 'xxx').expect(200);
|
||||
expect(body).to.eql([
|
||||
{
|
||||
id: normalizedDatabaseName,
|
||||
name: databaseName,
|
||||
type: 'maxmind',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete', () => {
|
||||
it('deletes a geoip database', async () => {
|
||||
await supertest
|
||||
.delete(`${url}/${normalizedDatabaseName}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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 function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Ingest pipelines', () => {
|
||||
loadTestFile(require.resolve('./databases'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
|
@ -70,5 +70,20 @@ export function IngestPipelinesAPIProvider({ getService }: FtrProviderContext) {
|
|||
|
||||
return await es.indices.delete({ index: indexName });
|
||||
},
|
||||
|
||||
async deleteGeoipDatabases() {
|
||||
const { databases } = await es.ingest.getGeoipDatabase();
|
||||
// Remove all geoip databases
|
||||
const databaseIds = databases.map((database: { id: string }) => database.id);
|
||||
|
||||
const deleteDatabase = (id: string) =>
|
||||
es.ingest.deleteGeoipDatabase({
|
||||
id,
|
||||
});
|
||||
|
||||
return Promise.all(databaseIds.map(deleteDatabase)).catch((err) => {
|
||||
log.debug(`[Cleanup error] Error deleting ES resources: ${err.message}`);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
|
|||
describe('Ingest pipelines app', function () {
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
loadTestFile(require.resolve('./ingest_pipelines'));
|
||||
loadTestFile(require.resolve('./manage_processors'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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', 'ingestPipelines', 'savedObjects']);
|
||||
const security = getService('security');
|
||||
const maxMindDatabaseName = 'GeoIP2-Anonymous-IP';
|
||||
const ipInfoDatabaseName = 'ASN';
|
||||
|
||||
// TODO: Fix flaky tests
|
||||
describe.skip('Ingest Pipelines: Manage Processors', function () {
|
||||
this.tags('smoke');
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(['manage_processors_user']);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await pageObjects.common.navigateToApp('ingestPipelines');
|
||||
await pageObjects.ingestPipelines.navigateToManageProcessorsPage();
|
||||
});
|
||||
after(async () => {
|
||||
await security.testUser.restoreDefaults();
|
||||
});
|
||||
|
||||
it('Empty list prompt', async () => {
|
||||
const promptExists = await pageObjects.ingestPipelines.geoipEmptyListPromptExists();
|
||||
expect(promptExists).to.be(true);
|
||||
});
|
||||
|
||||
it('Create a MaxMind database', async () => {
|
||||
await pageObjects.ingestPipelines.openCreateDatabaseModal();
|
||||
await pageObjects.ingestPipelines.fillAddDatabaseForm(
|
||||
'MaxMind',
|
||||
'GeoIP2 Anonymous IP',
|
||||
'123456'
|
||||
);
|
||||
await pageObjects.ingestPipelines.clickAddDatabaseButton();
|
||||
|
||||
// Wait for new row to gets displayed
|
||||
await pageObjects.common.sleep(1000);
|
||||
|
||||
const databasesList = await pageObjects.ingestPipelines.getGeoipDatabases();
|
||||
const databaseExists = Boolean(
|
||||
databasesList.find((databaseRow) => databaseRow.includes(maxMindDatabaseName))
|
||||
);
|
||||
|
||||
expect(databaseExists).to.be(true);
|
||||
});
|
||||
|
||||
it('Create an IPInfo database', async () => {
|
||||
await pageObjects.ingestPipelines.openCreateDatabaseModal();
|
||||
await pageObjects.ingestPipelines.fillAddDatabaseForm('IPInfo', ipInfoDatabaseName);
|
||||
await pageObjects.ingestPipelines.clickAddDatabaseButton();
|
||||
|
||||
// Wait for new row to gets displayed
|
||||
await pageObjects.common.sleep(1000);
|
||||
|
||||
const databasesList = await pageObjects.ingestPipelines.getGeoipDatabases();
|
||||
const databaseExists = Boolean(
|
||||
databasesList.find((databaseRow) => databaseRow.includes(ipInfoDatabaseName))
|
||||
);
|
||||
|
||||
expect(databaseExists).to.be(true);
|
||||
});
|
||||
|
||||
it('Table contains database name and maxmind type', async () => {
|
||||
const databasesList = await pageObjects.ingestPipelines.getGeoipDatabases();
|
||||
const maxMindDatabaseRow = databasesList.find((database) =>
|
||||
database.includes(maxMindDatabaseName)
|
||||
);
|
||||
expect(maxMindDatabaseRow).to.contain(maxMindDatabaseName);
|
||||
expect(maxMindDatabaseRow).to.contain('MaxMind');
|
||||
|
||||
const ipInfoDatabaseRow = databasesList.find((database) =>
|
||||
database.includes(ipInfoDatabaseName)
|
||||
);
|
||||
expect(ipInfoDatabaseRow).to.contain(ipInfoDatabaseName);
|
||||
expect(ipInfoDatabaseRow).to.contain('IPInfo');
|
||||
});
|
||||
|
||||
it('Modal to delete a database', async () => {
|
||||
// Delete both databases
|
||||
await pageObjects.ingestPipelines.deleteDatabase(0);
|
||||
await pageObjects.ingestPipelines.deleteDatabase(0);
|
||||
const promptExists = await pageObjects.ingestPipelines.geoipEmptyListPromptExists();
|
||||
expect(promptExists).to.be(true);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -640,6 +640,20 @@ export default async function ({ readConfigFile }) {
|
|||
],
|
||||
},
|
||||
|
||||
manage_processors_user: {
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
advancedSettings: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
license_management_user: {
|
||||
elasticsearch: {
|
||||
cluster: ['manage'],
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
|
||||
import path from 'path';
|
||||
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function IngestPipelinesPageProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const pageObjects = getPageObjects(['header', 'common']);
|
||||
const aceEditor = getService('aceEditor');
|
||||
const retry = getService('retry');
|
||||
|
||||
return {
|
||||
async sectionHeadingText() {
|
||||
|
@ -113,5 +115,56 @@ export function IngestPipelinesPageProvider({ getService, getPageObjects }: FtrP
|
|||
await testSubjects.click('tablePaginationPopoverButton');
|
||||
await testSubjects.click(`tablePagination-50-rows`);
|
||||
},
|
||||
|
||||
async navigateToManageProcessorsPage() {
|
||||
await testSubjects.click('manageProcessorsLink');
|
||||
await retry.waitFor('Manage Processors page title to be displayed', async () => {
|
||||
return await testSubjects.isDisplayed('manageProcessorsTitle');
|
||||
});
|
||||
},
|
||||
|
||||
async geoipEmptyListPromptExists() {
|
||||
return await testSubjects.exists('geoipEmptyListPrompt');
|
||||
},
|
||||
|
||||
async openCreateDatabaseModal() {
|
||||
await testSubjects.click('addGeoipDatabaseButton');
|
||||
},
|
||||
|
||||
async fillAddDatabaseForm(databaseType: string, databaseName: string, maxmind?: string) {
|
||||
await testSubjects.setValue('databaseTypeSelect', databaseType);
|
||||
|
||||
// Wait for the rest of the fields to get displayed
|
||||
await pageObjects.common.sleep(1000);
|
||||
expect(await testSubjects.exists('databaseNameSelect')).to.be(true);
|
||||
|
||||
if (maxmind) {
|
||||
await testSubjects.setValue('maxmindField', maxmind);
|
||||
}
|
||||
await testSubjects.setValue('databaseNameSelect', databaseName);
|
||||
},
|
||||
|
||||
async clickAddDatabaseButton() {
|
||||
// Wait for button to get enabled
|
||||
await pageObjects.common.sleep(1000);
|
||||
await testSubjects.click('addGeoipDatabaseSubmit');
|
||||
},
|
||||
|
||||
async getGeoipDatabases() {
|
||||
const databases = await testSubjects.findAll('geoipDatabaseListRow');
|
||||
|
||||
const getDatabaseRow = async (database: WebElementWrapper) => {
|
||||
return await database.getVisibleText();
|
||||
};
|
||||
|
||||
return await Promise.all(databases.map((database) => getDatabaseRow(database)));
|
||||
},
|
||||
|
||||
async deleteDatabase(index: number) {
|
||||
const deleteButtons = await testSubjects.findAll('deleteGeoipDatabaseButton');
|
||||
await deleteButtons.at(index)?.click();
|
||||
await testSubjects.setValue('geoipDatabaseConfirmation', 'delete');
|
||||
await testSubjects.click('deleteGeoipDatabaseSubmit');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue